Skip to content

NoSQL data modeling with Claude Code, Codex, or Cursor works when the agent designs from a written list of access patterns, reads the live schema through a read-only MongoDB or Neo4j MCP server, and returns an access-pattern map plus tests proving that every query uses an index. A NoSQL schema without that evidence is a guess.

This page is for developers who let an agent design or change a document, key-value, graph, or vector data model. The failure it prevents looks like this: the MongoDB schema was clean in the demo, one user document with embedded orders, addresses, and a cart. Six months later the orders array holds 4,000 entries, the document is creeping toward MongoDB’s 16 MB limit, and every profile read drags the whole thing over the wire. The agent that wrote the schema never saw your access patterns, so it optimized for the demo.

NoSQL gives you no relational schema to hide behind. The data model is the design, and a wrong embed-versus-reference call or a wrong partition key is expensive to unwind once traffic arrives. The recipes below make the agent justify each call and then prove it on a real engine, so you review a table and a test report instead of every query.

What you’ll walk away with from these NoSQL recipes

Section titled “What you’ll walk away with from these NoSQL recipes”
  • A read-only MCP setup for MongoDB and Neo4j in Claude Code, Codex, and Cursor, with the one flag that makes MongoDB reject collection scans
  • An embed-versus-reference prompt that forces the agent to argue from access patterns and growth bounds
  • An aggregation prompt that treats the explain plan as part of the deliverable
  • A single-table DynamoDB prompt that maps every access pattern to a key condition or a GSI
  • Neo4j 5 and Atlas Vector Search prompts that avoid the removed syntax agents still produce
  • An access-pattern test suite, written by the agent, that CI runs so nobody has to read every query

Every store follows the same four steps. The store changes; the loop does not.

  1. Write the access patterns first. A numbered list of the reads and writes the feature needs, with rough frequency. This list is the spec, and it is the thing you approve.
  2. Have the agent produce the design as an artifact. A table that maps each access pattern to a document shape, key condition, or index, not a paragraph of prose.
  3. Validate on a real engine. The agent reads the real schema and runs explain through MCP, or runs against DynamoDB Local, instead of guessing field names.
  4. Lock it in with tests. One test per access pattern asserts the result and the plan (index used, no scan). CI runs them on every change.

Step 1: Connect the agent to your real schema

Section titled “Step 1: Connect the agent to your real schema”

Without a database connection the agent invents collection and field names. With one, it samples a document first, sees that you already store customerId rather than userId, and follows your convention. The general setup, read-only roles, and the other database servers are covered in database MCP servers; this step is the NoSQL-specific part.

Two servers cover this page, both verified on 2026-09-26:

  • MongoDB MCP Server (npm mongodb-mcp-server 3.0.4, official). Tools include collection-schema, collection-indexes, find, aggregate, and explain. --readOnly removes the create, update, and delete tools. --indexCheck rejects any query that would do a collection scan, which turns “use an index” from advice into a hard failure the agent has to fix.
  • Neo4j Cypher MCP server (PyPI mcp-neo4j-cypher 0.6.0, from neo4j-contrib). Tools are get_neo4j_schema (needs the APOC plugin), read_neo4j_cypher, and write_neo4j_cypher; --read-only removes the write tool. It is a Python package, so it runs with uvx, not npx.

The servers behave the same in all three tools; only the place the config lives differs.

Run in the terminal at the repo root. -s project writes .mcp.json, and the single-quoted ${NEO4J_PASSWORD} is expanded from your shell when the server starts, so the secret stays out of the file:

Terminal window
claude mcp add -s project mongodb \
-e MDB_MCP_CONNECTION_STRING=mongodb://localhost:27017/shop \
-- npx -y mongodb-mcp-server@3.0.4 --readOnly --indexCheck
claude mcp add -s project neo4j \
-e NEO4J_URI=bolt://localhost:7687 -e NEO4J_USERNAME=neo4j \
-e 'NEO4J_PASSWORD=${NEO4J_PASSWORD}' \
-- uvx mcp-neo4j-cypher@0.6.0 --read-only

Step 2: Decide embed versus reference from access patterns

Section titled “Step 2: Decide embed versus reference from access patterns”

The most consequential document-store decision is whether related data is embedded or referenced. The agent gets it right when you hand it the access patterns, the write pattern, and the growth bounds, instead of asking for “a good schema”.

A good answer embeds line items (bounded, immutable, always read together), stores a customer: { id, name } snapshot instead of running $lookup on every read, and serves pattern 2 with a compound index on { customerId: 1, createdAt: -1 }. Pattern 1 returns the whole document, so it cannot be a covered query; an agent that claims otherwise has not reasoned about projections, and you should send it back. The access-pattern table is the artifact you approve. The code follows from it.

Step 3: Make every aggregation prove its index

Section titled “Step 3: Make every aggregation prove its index”

Aggregation pipelines are where the agent saves the most time and where it most often ships a query that returns correct results while scanning the whole collection. Make the execution plan part of the deliverable.

Because the server runs with --indexCheck, a pipeline whose first $match scans the collection fails outright instead of returning a plausible answer. The agent sees the failure, proposes the index, and on your local database (where you dropped --readOnly) creates it and re-runs explain until the plan starts with IXSCAN. The ratio of totalDocsExamined to nReturned is the number to read: close to 1 is healthy, hundreds to one means the index is the wrong shape.

Step 4: Design a single-table DynamoDB schema

Section titled “Step 4: Design a single-table DynamoDB schema”

DynamoDB punishes vague prompts hardest. A low-cardinality partition key creates a hot partition that throttles under load, and a missing access pattern means a Scan in production. There is no read-only schema to introspect here, so the agent works from the list and proves the design against DynamoDB Local (the amazon/dynamodb-local Docker image).

The executable part is small. This is the query for pattern 3, using AWS SDK v3 (@aws-sdk/lib-dynamodb 3.1141.0 on npm, 2026-09-26); the v2 .query(params).promise() chain does not apply:

import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, QueryCommand } from '@aws-sdk/lib-dynamodb';
const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}));
// GSI1: GSI1PK = USER#<id>, GSI1SK = TASK#<dueDate>#<taskId>
export async function getUserTasks(userId, fromDate) {
const { Items } = await ddb.send(
new QueryCommand({
TableName: 'SaaSPlatform',
IndexName: 'GSI1',
KeyConditionExpression: 'GSI1PK = :pk AND GSI1SK >= :start',
ExpressionAttributeValues: {
':pk': `USER#${userId}`,
':start': `TASK#${fromDate}`,
},
ScanIndexForward: true, // ascending by due date
}),
);
return Items;
}

Batch writes use BatchWriteCommand, which still takes at most 25 items per request.

For highly connected data (social graphs, recommendations, fraud rings), ask for the node and relationship model first, then the Cypher. Insist on Neo4j 5 syntax: training data is full of the Neo4j 4 constraint form CREATE CONSTRAINT ON ... ASSERT, which Neo4j 5 rejects.

The shape of a correct answer:

CREATE CONSTRAINT user_id IF NOT EXISTS
FOR (u:User) REQUIRE u.id IS UNIQUE;
CREATE CONSTRAINT post_id IF NOT EXISTS
FOR (p:Post) REQUIRE p.id IS UNIQUE;
// Posts friends liked in the last 7 days that this user has not viewed
MATCH (user:User {id: $userId})-[:FRIENDS]-(friend:User)-[l:LIKED]->(post:Post)
WHERE l.at > datetime() - duration('P7D')
AND NOT EXISTS { (user)-[:VIEWED]->(post) }
WITH post, count(DISTINCT friend) AS friendLikes
RETURN post.id, post.content, friendLikes
ORDER BY friendLikes DESC
LIMIT 10;

The user_id constraint gives the query its starting point: EXPLAIN should show NodeUniqueIndexSeek on :User(id). An AllNodesScan means the constraint is missing or the label is misspelled.

Step 6: Add vector search on MongoDB Atlas

Section titled “Step 6: Add vector search on MongoDB Atlas”

Storing embeddings next to documents gives you semantic search without a separate vector database. On MongoDB Atlas, query with the $vectorSearch aggregation stage against an index of type vectorSearch. The older knnVector field mapping belongs to Atlas Search, is deprecated, and does not serve $vectorSearch, yet agents still generate it.

from pymongo.operations import SearchIndexModel
# A vectorSearch index needs SearchIndexModel(type="vectorSearch").
# A plain-dict create_search_index() call creates an Atlas Search index instead.
db.products.create_search_index(
model=SearchIndexModel(
definition={
"fields": [
{"type": "vector", "path": "text_embedding",
"numDimensions": 384, "similarity": "cosine"},
{"type": "filter", "path": "price"},
{"type": "filter", "path": "categories"},
]
},
name="product_embeddings",
type="vectorSearch",
)
)
pipeline = [
{"$vectorSearch": {
"index": "product_embeddings",
"path": "text_embedding",
"queryVector": query_embedding, # 384 floats from the same model
"numCandidates": 200,
"limit": 10,
"filter": {"price": {"$lte": 100}},
}},
{"$addFields": {"score": {"$meta": "vectorSearchScore"}}},
{"$project": {"text_embedding": 0}},
]

How do you verify a NoSQL design without reading every query?

Section titled “How do you verify a NoSQL design without reading every query?”

You approve two things: the access-pattern table from the design step, and the test report. You do not read each pipeline, key condition, or Cypher query, because the tests assert what a reviewer would look for, on a real engine, every time CI runs. This is the same shift the site describes in reading evidence instead of code.

StoreWhat the test assertsHow it fails
MongoDBEach access pattern returns the right documents, and its explain output contains no COLLSCANA missing or wrongly ordered index
MongoDBThe $jsonSchema validator rejects a document with a missing required fieldThe shape drifted from the approved design
DynamoDBEach access pattern is served by Query or GetItem; a middleware throws on any ScanCommandAn access pattern the key design does not cover
Neo4jEXPLAIN on each query shows no AllNodesScan; SHOW CONSTRAINTS lists every unique keyA dropped constraint or a misspelled label
Vectorrecall@10 on the golden queries stays at or above the recorded baseline; every embedding length equals numDimensionsA model change without re-embedding

The plan assertion is a check over the whole plan, so it survives the differences between a find plan and an aggregation plan:

const plan = await db.collection('orders').aggregate(pipeline).explain('executionStats');
expect(JSON.stringify(plan)).not.toContain('COLLSCAN');

Run these tests against a real engine in CI: a MongoDB container through Testcontainers (@testcontainers/mongodb 12.1.0 on npm, 2026-09-26), DynamoDB Local, and a Neo4j container. A mocked driver cannot produce a query plan, so it proves nothing here. The wider setup is in integration test patterns, and seeding realistic volumes is in test data strategies.

Who signs off: the developer who owns the feature approves the access-pattern table before any code exists, and CI must be green before merge. A reviewer reads the table, the test names, and the report. When the agent changes an assertion instead of a query, treat that as a failed change, as described in protecting the test oracle.

  • An array grows without bound toward the 16 MB limit. Symptom: profile reads slow down month over month, then writes fail. Recovery: move the array into a child collection keyed by the parent id (or bucket it by month), backfill with a migration, and add the unbounded-growth question to the design prompt so the next model gets it.
  • A DynamoDB partition runs hot. A key such as TENANT#<id> throttles once one tenant’s traffic exceeds a single partition’s throughput limit, which extra provisioned capacity does not raise. Recovery: add a shard suffix (TENANT#<id>#<0-9>), write to a random shard, and scatter-gather on read.
  • A GSI projection misses an attribute. Queries against the index return items without the field, and the code reads undefined. You cannot change a GSI’s projection after creation. Recovery: create a new GSI with the right projection, switch reads, then delete the old one.
  • A Neo4j supernode. A user with millions of FRIENDS relationships turns every traversal from that user into a scan. Recovery: cap fan-out in the query (WITH friend LIMIT 1000), or model the hot relationship as its own node.
  • Stale embeddings. Vector results degrade silently after you change embedding models without re-embedding existing documents. Recovery: the dimension test catches a changed length; for a same-length model swap, re-embed everything and re-record the recall baseline in the same change.
  • The agent invents schema. Queries reference fields you do not have. Recovery: check that the MCP server is connected (/mcp in Claude Code, codex mcp list in Codex) and tell the agent to call collection-schema or get_neo4j_schema before writing anything.
  • The agent tries to write through MCP. With --readOnly the write tools do not exist, so the attempt fails harmlessly. Recovery: keep the flag and route the change through a migration; never remove it to get past the error. The broader threat model is in securing MCP servers.

Did this guide help you?

One tap, no sign-in.

not at all a lot