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 caseBest fit
AI agent memory — storing entity relationships extracted from documentsNeptune (property graph)
Knowledge graph — semantic triples linking conceptsNeptune (SPARQL/RDF or property graph)
Recommendation engine — user→item→tag graph traversalNeptune (Gremlin)
Fraud detection — ring of connected accountsNeptune (openCypher)
Simple key-value session storageDynamoDB
Relational data with complex joins but no graph traversalRDS / 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:

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.

LanguageBest forAvoid whenMCP server example
Gremlin (TinkerPop 3)Imperative graph traversal, complex multi-hop paths, iterative algorithmsJoining results from non-graph sources; ad-hoc analyticsg.V('entity-123').both('mentions').dedup().values('name').toList()
openCypherDeclarative pattern matching, teams familiar with Neo4j Cypher, readable queriesDeep recursive traversals >5 hops (Gremlin is faster)MATCH (e:Entity)-[:MENTIONS]->(c:Concept) WHERE e.id=$id RETURN c.name
SPARQLLinked Data / RDF, ontology-based reasoning, standards-required (government, life sciences)General application development; most teams find it verboseSELECT ?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

SymptomCauseFix
WebSocket connection refused or timeoutLambda not in same VPC as Neptune, or security group missing TCP 8182 inbound ruleAdd 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 opensIAM database authentication enabled but Lambda role missing neptune-db:connectCall 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 invocationsNeptune dropped the idle WebSocket connection; reusing stale module-level connectionReconnect on error — wrap traversal in try/catch that resets dc and g to null and retries once
Query times out after 120 sDefault 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 IDNeptune uses string vertex IDs; Gremlin driver may return ID in a typed wrapperUse .valueMap(true) which includes ID and label, then read result[T.id] or stringify as result.id.toString()