Guide · Amazon Neptune · Graph Database · Gremlin · openCypher
Amazon Neptune for MCP Servers — Graph Database Setup, IAM Auth, and Gremlin Connections
Amazon Neptune is AWS's fully managed graph database service, supporting three query languages: Gremlin (Apache TinkerPop), openCypher (Cypher), and SPARQL (RDF). For MCP server developers, Neptune is the right choice when your tool calls need to navigate entity relationships, traverse knowledge graphs, or query property graph data where join-heavy relational queries become impractical. This guide covers Neptune cluster architecture, IAM authentication setup, connecting with gremlin-javascript inside a Lambda-backed MCP server, and choosing between the three query languages for different data models.
TL;DR
Neptune runs on port 8182. Enable IAM database authentication on the cluster and use the neptune-sigv4-signer package to sign requests. Connect via DriverRemoteConnection with a WebSocket URL that uses wss:// for encrypted transit. Use the writer endpoint for writes (never read from it to avoid reader lag) and the reader endpoint for traversals. Neptune is not publicly accessible — your Lambda, ECS task, or EKS pod must be in the same VPC, with a security group rule allowing TCP 8182 from the compute security group.
When to choose Neptune over DynamoDB or RDS
Neptune solves a specific class of problem. If your MCP tools primarily answer questions like "what are the five-hop neighbors of entity X that share property Y" or "find all paths between node A and node B", Neptune traversals are orders of magnitude faster than equivalent recursive SQL or denormalized DynamoDB access patterns. If your MCP tools are mostly CRUD operations on independent records, DynamoDB or RDS is simpler and cheaper.
| Use case | Best fit |
|---|---|
| AI agent memory — storing entity relationships extracted from documents | Neptune (property graph) |
| Knowledge graph — semantic triples linking concepts | Neptune (SPARQL/RDF or property graph) |
| Recommendation engine — user→item→tag graph traversal | Neptune (Gremlin) |
| Fraud detection — ring of connected accounts | Neptune (openCypher) |
| Simple key-value session storage | DynamoDB |
| Relational data with complex joins but no graph traversal | RDS / Aurora |
Neptune cluster architecture
A Neptune cluster consists of one or more DB instances and a shared cluster storage volume. The storage volume automatically replicates data across three Availability Zones regardless of instance count — high availability is storage-layer, not instance-layer.
Key components of a Neptune cluster:
- Writer instance: exactly one; handles all writes and can serve reads. Write endpoint format:
your-cluster.cluster-XXXXXX.us-east-1.neptune.amazonaws.com. Reads from the writer are consistent but add latency to write-heavy workloads — prefer reader instances for read-only MCP tool calls. - Reader instances: zero or more read-only replicas. Reader endpoint format:
your-cluster.cluster-ro-XXXXXX.us-east-1.neptune.amazonaws.com. The reader endpoint load-balances across all healthy reader instances. Lag behind writer is typically under 100 ms. - Cluster endpoint: always routes to the current writer. Use this for all write operations.
- Neptune Serverless: alternative to provisioned instances; no minimum NCUs (Neptune Capacity Units) when idle. Set
minCapacity: 1andmaxCapacity: 32. Serverless scales on inactivity but has ~5–10 s cold start when the first connection arrives after an idle period.
Instance types for provisioned clusters: db.r6g.large (4 vCPU, 32 GiB, dev/test), db.r6g.xlarge (8 vCPU, 64 GiB, small production), db.r6g.4xlarge (16 vCPU, 128 GiB, medium production). Neptune uses memory for query caching and buffer pool — size for your working set, not raw data size.
CDK cluster setup with IAM authentication
Always enable IAM database authentication on new clusters. It eliminates the need for a database password and integrates cleanly with Lambda execution roles.
import * as neptune from '@aws-cdk/aws-neptune-alpha';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { Stack, StackProps, RemovalPolicy } from 'aws-cdk-lib';
export class McpNeptuneStack extends Stack {
constructor(scope: Construct, id: string, props: StackProps) {
super(scope, id, props);
const vpc = ec2.Vpc.fromLookup(this, 'Vpc', { isDefault: false });
const cluster = new neptune.DatabaseCluster(this, 'McpGraph', {
vpc,
instanceType: neptune.InstanceType.R6G_LARGE,
iamAuthentication: true, // required for IAM auth
removalPolicy: RemovalPolicy.SNAPSHOT,
cloudwatchLogsExports: [
neptune.LogType.AUDIT,
neptune.LogType.SLOWQUERY,
],
parameterGroup: new neptune.ClusterParameterGroup(this, 'ClusterParams', {
parameters: {
neptune_enable_audit_log: '1',
neptune_query_timeout: '30000', // 30 s, default is 120 s
neptune_result_cache: '1', // in-memory query result caching
},
}),
});
// Allow Lambda execution role to connect
const mcpLambdaRole = iam.Role.fromRoleArn(this, 'McpRole', 'arn:aws:iam::...');
cluster.grantConnect(mcpLambdaRole);
}
}
cluster.grantConnect() attaches the neptune-db:connect IAM action to the role. Without it, Neptune rejects the SigV4-signed connection with an HTTP 403 — a confusing error because the WebSocket connection itself may appear to succeed before the query fails.
Connecting from a Lambda MCP server with IAM auth
Neptune's Gremlin endpoint uses WebSockets over TLS (port 8182, path /gremlin). To sign requests with SigV4, use the neptune-sigv4-signer package alongside gremlin (the Apache TinkerPop JavaScript driver).
import gremlin from 'gremlin';
import { createSigner, SignOptions } from 'neptune-sigv4-signer';
const { driver: { DriverRemoteConnection }, structure: { io: { GraphSONWriter, GraphSONReader } } } = gremlin;
const { Graph, process: { AnonymousTraversalSource } } = gremlin.structure;
const { statics: __ } = gremlin.process;
const NEPTUNE_ENDPOINT = process.env.NEPTUNE_ENDPOINT!; // cluster or reader endpoint
const NEPTUNE_PORT = 8182;
const REGION = process.env.AWS_REGION || 'us-east-1';
let g: gremlin.process.GraphTraversalSource | null = null;
let dc: DriverRemoteConnection | null = null;
async function getTraversalSource() {
if (g && dc && !dc.isOpen === false) return g;
const url = `wss://${NEPTUNE_ENDPOINT}:${NEPTUNE_PORT}/gremlin`;
const signer = createSigner({ region: REGION });
dc = new DriverRemoteConnection(url, {
mimeType: 'application/vnd.gremlin-v3.0+json',
headers: await signer.getAuthHeaders(url, 'GET'),
rejectUnauthorized: true,
});
g = AnonymousTraversalSource.traversal().withRemote(dc);
return g;
}
// In a Lambda MCP handler, reconnect on each invocation (Neptune drops idle connections)
export async function handler(event: McpToolEvent) {
const g = await getTraversalSource();
try {
const result = await g.V().hasLabel('Entity').has('name', event.entityName).valueMap(true).next();
return result.value;
} catch (err) {
// Reset connection on any error — Neptune can drop connections silently
if (dc) { try { await dc.close(); } catch {} }
dc = null; g = null;
throw err;
}
}
Important: neptune-sigv4-signer calls STS to get temporary credentials, so the Lambda execution role must have sts:AssumeRole (it does by default) and neptune-db:connect on the cluster ARN (granted by cluster.grantConnect()).
Connection lifecycle in Lambda: Neptune drops idle WebSocket connections after ~20 minutes. In a Lambda MCP server, it's safest to create a fresh connection per invocation rather than reusing a module-level connection — the extra ~100 ms connection overhead is preferable to silent failures on stale connections. If your MCP server runs on ECS or EC2 (long-lived processes), maintain a connection pool with reconnect-on-error logic.
Query language selection guide
Neptune supports three query languages on the same cluster. All three can run against the same underlying graph data. The choice depends on your team's familiarity, the shape of your queries, and performance characteristics.
| Language | Best for | Avoid when | MCP server example |
|---|---|---|---|
| Gremlin (TinkerPop 3) | Imperative graph traversal, complex multi-hop paths, iterative algorithms | Joining results from non-graph sources; ad-hoc analytics | g.V('entity-123').both('mentions').dedup().values('name').toList() |
| openCypher | Declarative pattern matching, teams familiar with Neo4j Cypher, readable queries | Deep recursive traversals >5 hops (Gremlin is faster) | MATCH (e:Entity)-[:MENTIONS]->(c:Concept) WHERE e.id=$id RETURN c.name |
| SPARQL | Linked Data / RDF, ontology-based reasoning, standards-required (government, life sciences) | General application development; most teams find it verbose | SELECT ?name WHERE { <urn:entity:123> schema:mentions ?c . ?c rdfs:label ?name } |
For MCP server knowledge graphs, Gremlin or openCypher is the right choice for the vast majority of use cases. SPARQL is relevant only if you are building on a formal ontology or integrating with an existing RDF triple store. You can mix Gremlin and openCypher on the same cluster if needed — Neptune runs both on the same property graph engine.
Security group configuration
Neptune is never publicly accessible. Your compute (Lambda, ECS, EC2) must be in the same VPC, and the Neptune cluster security group must allow inbound TCP port 8182 from the compute security group.
// CDK: allow Lambda to reach Neptune on port 8182
const neptuneSecurityGroup = new ec2.SecurityGroup(this, 'NeptuneSg', { vpc });
const lambdaSecurityGroup = new ec2.SecurityGroup(this, 'LambdaSg', { vpc });
neptuneSecurityGroup.addIngressRule(
ec2.Peer.securityGroupId(lambdaSecurityGroup.securityGroupId),
ec2.Port.tcp(8182),
'Allow Gremlin/openCypher from MCP Lambda',
);
// Lambda must be deployed into the VPC with this security group
const mcpLambda = new lambda.Function(this, 'McpHandler', {
vpc,
securityGroups: [lambdaSecurityGroup],
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
// ...
});
Placing Lambda in a VPC increases cold start time by roughly 300–500 ms on the first invocation (VPC ENI attachment). This is a one-time cost per Lambda container lifecycle. Neptune connection overhead (IAM auth + TLS handshake) adds another ~100–150 ms per new connection. Plan for 500–700 ms total overhead for cold MCP tool calls that need a new Neptune connection.
Common failures
| Symptom | Cause | Fix |
|---|---|---|
| WebSocket connection refused or timeout | Lambda not in same VPC as Neptune, or security group missing TCP 8182 inbound rule | Add ec2.Port.tcp(8182) ingress rule from Lambda SG to Neptune SG; deploy Lambda in same VPC subnet |
| HTTP 403 on first query after connection opens | IAM database authentication enabled but Lambda role missing neptune-db:connect | Call cluster.grantConnect(lambdaRole) in CDK or add IAM policy with neptune-db:connect on cluster ARN arn:aws:neptune-db:REGION:ACCOUNT:cluster:CLUSTER_ID/* |
GremlinServerUnavailableError on subsequent Lambda invocations | Neptune dropped the idle WebSocket connection; reusing stale module-level connection | Reconnect on error — wrap traversal in try/catch that resets dc and g to null and retries once |
| Query times out after 120 s | Default query timeout is 120,000 ms; unbounded traversal with no limit() | Add limit(N) to all traversals; reduce cluster parameter neptune_query_timeout to 30,000 ms to surface slow queries faster |
SerializationError reading vertex ID | Neptune uses string vertex IDs; Gremlin driver may return ID in a typed wrapper | Use .valueMap(true) which includes ID and label, then read result[T.id] or stringify as result.id.toString() |