Skip to main content
Glama
davidedantonio

mcp-mongodb

mcp-mongodb

A Model Context Protocol server that exposes MongoDB collections to LLM clients — read-only by construction, and only the fields you declare.

Most database bridges hand a model a connection and hope the prompt holds. This one starts from nothing: a collection that is not in the config does not exist, a field that is not in allowedFields never leaves the database, the query surface is a closed set of four operations, and no environment variable turns writes on.

// A query the model sends
{ "database": "shop", "collection": "orders", "filter": { "status": "paid" } }

// What actually reaches MongoDB
{
  "filter":     { "$and": [ { "deleted": { "$ne": true } }, { "status": "paid" } ] },
  "projection": { "_id": 1, "number": 1, "status": 1, "total": 1 },
  "limit":      50,
  "maxTimeMS":  10000
}

The scope filter, the projection, the page size and the deadline were not asked for. They cannot be removed.


Contents


Related MCP server: Florentine.ai MCP Server

Quick start

git clone https://github.com/davidedantonio/mcp-mongodb.git
cd mcp-mongodb
npm install

# A throwaway MongoDB with a realistic fixture
npm run demo:up

# Start the server on the stdio transport
npx tsx --env-file=.env index.ts

To see it answer queries without wiring up a client first:

npm run demo:http # with the server running on MCP_TRANSPORT=http

Installation

As a package

npm install -g mcp-mongodb
mcp-mongodb # reads its configuration from the environment

Or without installing, which is how MCP clients usually launch it:

npx mcp-mongodb

As a container

docker build -t mcp-mongodb .

docker run --rm \
  -e MONGODB_URI='mongodb://reader:password@mongo:27017/shop?authSource=shop' \
  -e MONGODB_ALLOWED_DATABASES=shop \
  -e MCP_CONFIG_PATH=/etc/mcp/config.json \
  -e MCP_TRANSPORT=http \
  -e MCP_HTTP_AUTH_TOKEN="$TOKEN" \
  -v "$PWD/config.json:/etc/mcp/config.json:ro" \
  -p 3000:3000 \
  mcp-mongodb

The image carries no configuration. Mount it and point MCP_CONFIG_PATH at it — the same image then serves every environment without a rebuild.

Requirements

Node.js 22 or later, and a MongoDB you can reach. Use a read-only user; see Security notes.

Configuration

Environment

Variable

Default

Meaning

MONGODB_URI

required

Connection string. Use a read-only user.

MONGODB_ALLOWED_DATABASES

required

Comma-separated. A database not listed here is unreachable.

MONGODB_CONNECT_TIMEOUT_MS

5000

Must be lower than the query timeout.

MONGODB_QUERY_TIMEOUT_MS

10000

Becomes maxTimeMS on every query.

MCP_CONFIG_PATH

Path to the config file. Without it, nothing is exposed.

MCP_TRANSPORT

stdio

stdio or http.

MCP_DEFAULT_PAGE_SIZE

50

Page size when the caller does not ask for one.

MCP_MAX_PAGE_SIZE

200

Ceiling the caller cannot raise.

MCP_MAX_RESPONSE_BYTES

262144

A context budget, not a disk one.

MCP_HTTP_PORT

3000

HTTP transport only.

MCP_HTTP_AUTH_TOKEN

Required for the HTTP transport, minimum 32 characters.

MCP_HTTP_ALLOWED_HOSTS

Comma-separated. Enables DNS-rebinding protection.

MCP_HTTP_MAX_BODY_BYTES

1048576

Request body ceiling.

LOG_LEVEL

info

debug, info, warn, error, silent.

The server reads its environment and nothing else — it does not load a .env file on its own. Node can do that for you:

node --env-file=.env lib/index.js

MCP clients pass the variables in their own configuration, and containers get them from the orchestrator. A .env is a convenience for running it by hand.

The config file

Every collection you want reachable gets an entry. Three separate lists, because reading, filtering and sorting are three different permissions:

{
  "version": 1,
  "databases": {
    "shop": {
      "collections": {
        "orders": {
          "allowedFields": [
            "_id",
            "number",
            "customerCode",
            "status",
            "placedAt",
            "total"
          ],
          "filterableFields": [
            "_id",
            "number",
            "customerCode",
            "status",
            "placedAt"
          ],
          "sortableFields": ["placedAt", "total"],
          "requiredFilter": { "deleted": { "$ne": true } }
        }
      }
    }
  }
}

allowedFields — the only fields that can ever be returned. A query with no projection gets exactly these, never the whole document. This is also the list the server projects on, server-side, so unreadable fields never cross the wire.

filterableFields and sortableFields must be subsets of allowedFields, and both default to empty. A field is not filterable just because it is readable: total might be safe to display and expensive to filter on without an index.

requiredFilter is $and-ed into every query and cannot be overridden — not by a filter naming the same field, not by an aggregation stage. It may key on fields that are not in allowedFields, which is how you scope by a tenant identifier without exposing it.

Prefer { "$ne": true } over false for soft-delete flags. In MongoDB { deleted: false } matches only documents where the field exists and is false. Records written before the flag existed have no field at all and silently vanish. { "$ne": true } matches false, null and absent.

The whole file is validated at startup, including a scan of every requiredFilter for dangerous operators — a mistake there fails the boot with a path, not the first query with a driver error.

Tools

Four tools, one per operation. Each tool's description is generated from your config, so the model is told which fields it may use instead of guessing:

Find documents in a MongoDB collection.

Available collections:
shop.orders
  readable:   _id, number, customerCode, status, placedAt, total
  filterable: _id, number, customerCode, status, placedAt
  sortable:   placedAt, total

find

Argument

Type

Notes

database, collection

string

Required.

filter

object

Over filterable fields only.

projection

object

Inclusions only. { "field": 0 } is refused.

sort

object

1 or -1, over sortable fields only.

limit

integer

Clamped to MCP_MAX_PAGE_SIZE.

skip

integer

For paging with nextSkip.

aggregate

Takes a pipeline. Read-only stages only; the scope filter and the projection are prepended, and a page limit is appended.

count

Takes a filter and returns { "count": n }, scoped like everything else.

distinct

Takes a field and an optional filter.

What comes back

Extended JSON with a pagination envelope, so BSON types survive the trip and can be sent straight back in a filter:

{
  "documents": [
    {
      "_id": { "$oid": "6a9d192c31f0a4f251d920bb" },
      "number": "ORD-2026-0103",
      "placedAt": { "$date": "2026-08-01T09:00:00Z" },
      "total": { "$numberDecimal": "11768.04" }
    }
  ],
  "returned": 1,
  "hasMore": true,
  "nextSkip": 1
}

That $oid can go straight back into filter and it will match — the server revives Extended JSON on the way in as well as rendering it on the way out.

A refusal comes back as an error payload with a path, so the model can correct itself rather than retry the same query:

{
  "error": "The field \"iban\" is not filterable on this collection",
  "path": "filter"
}

Resources

URI

Contents

mongodb://collections

Every database and collection the server exposes.

mongodb://collection/{database}/{collection}

Readable, filterable and sortable fields, plus whether the collection is scoped.

Both are built from the config and never touch the cluster: they cannot fail, cannot be slow, and cannot become a second way to read what the tools refuse. The per-collection resource reports scoped: true but never the contents of requiredFilter — that is where tenant identifiers live.

Connecting a client

All examples use absolute paths. A client starts the server as a subprocess from its own working directory, not from your project, so relative paths will not resolve.

Claude Desktop

~/Library/Application Support/Claude/claude_desktop_config.json on macOS, %APPDATA%\Claude\claude_desktop_config.json on Windows:

{
  "mcpServers": {
    "mongodb": {
      "command": "npx",
      "args": ["-y", "mcp-mongodb"],
      "env": {
        "MONGODB_URI": "mongodb://reader:password@localhost:27017/shop?authSource=shop",
        "MONGODB_ALLOWED_DATABASES": "shop",
        "MCP_CONFIG_PATH": "/absolute/path/to/config.json"
      }
    }
  }
}

Restart the app after editing.

Claude Code

From your terminal, outside a session. Note the -- separator: everything after it is the command to run.

claude mcp add mongodb \
  --env MONGODB_URI='mongodb://reader:password@localhost:27017/shop?authSource=shop' \
  --env MONGODB_ALLOWED_DATABASES=shop \
  --env MCP_CONFIG_PATH=/absolute/path/to/config.json \
  -- npx -y mcp-mongodb

claude mcp list      # ✔ Connected

Add --scope user for all your projects, or --scope project to write it to .mcp.json and share it with the team. Keep credentials out of a committed .mcp.json.

VS Code and Cursor

.vscode/mcp.json — the top-level key is servers, not mcpServers:

{
  "servers": {
    "mongodb": {
      "command": "npx",
      "args": ["-y", "mcp-mongodb"],
      "env": {
        "MONGODB_URI": "mongodb://reader:password@localhost:27017/shop?authSource=shop",
        "MONGODB_ALLOWED_DATABASES": "shop",
        "MCP_CONFIG_PATH": "/absolute/path/to/config.json"
      }
    }
  }
}

Cursor uses the same shape at .cursor/mcp.json. Both support ${input:...} variables for secrets rather than literals in the file.

MCP Inspector

The fastest way to see raw requests and responses while developing:

npx @modelcontextprotocol/inspector npx tsx index.ts

HTTP transport

For a server other machines reach over the network:

MCP_TRANSPORT=http \
MCP_HTTP_PORT=3000 \
MCP_HTTP_AUTH_TOKEN="$(openssl rand -hex 32)" \
MCP_HTTP_ALLOWED_HOSTS='mcp.internal.example.com' \
npx mcp-mongodb

The endpoint is /mcp and every request needs Authorization: Bearer <token>. The server refuses to start without MCP_HTTP_AUTH_TOKEN: over stdio the client is whoever launched the process, but an open HTTP port in front of a database is not a state you should reach by forgetting something.

Set MCP_HTTP_ALLOWED_HOSTS too. It turns on DNS-rebinding protection, without which a browser on someone else's machine can resolve a hostname to 127.0.0.1 and reach your port through their network.

TLS is not terminated here — put it behind a reverse proxy.

In Claude Code:

claude mcp add --transport http mongodb https://mcp.internal.example.com/mcp \
  --header "Authorization: Bearer $TOKEN"

How a query is handled

Five steps, in this order, for every tool call:

Step

What it does

1

Parse

Arguments become a typed command. Four operations, closed set.

2

Validate

Operators, stages, joined collections and every field path are checked against the allowlists. Anything not explicitly permitted is refused.

3

Enforce

The required filter is $and-ed in, the projection is built from allowedFields, the page size is clamped, a deadline is attached.

4

Execute

The driver receives a plan that is already safe. It makes no decisions.

5

Render

Extended JSON, trimmed to the byte budget, with hasMore so the model can page.

Step 2 only ever rejects. Step 3 only ever rewrites. Keeping them apart is what makes the second one possible to reason about: a projection that is merely validated is no projection at all when the caller sends none.

What is always refused

  • Server-side JavaScript: $where, $function, $accumulator, $code — anywhere in the command, however deeply nested

  • $out and $merge, regardless of any setting: a write belongs in an explicit write operation, not appended to a read pipeline

  • Exclusion projections such as { "secret": 0 }, which return everything except the named field — the exact inverse of an allowlist

  • $$ROOT and $$CURRENT, which hand back the whole document

  • $lookup in localField/foreignField form into a scoped collection — that form has no pipeline, so there is nowhere to inject the foreign collection's scope. The pipeline form is required and gets scoped properly.

  • $graphLookup into a scoped collection, whose recursive traversal cannot be constrained at all

  • Introspection stages: $collStats, $indexStats, $currentOp, $planCacheStats and friends, which leak cluster topology

  • Prototype-polluting keys: __proto__, constructor, prototype

The stage list is an allowlist, not a denylist: a stage added by a future MongoDB release is refused until someone adds it deliberately.

The demo environment

demo/ holds a disposable MongoDB with a fixture built to make each guarantee observable. See demo/README.md for the details.

npm run demo:up # MongoDB, seeded, plus a read-only user
npm run demo:reset # wipe the volume and reseed
npm run demo:down

The fixture is deterministic: 5 customers, 8 products, 120 orders, 48 invoices. Twelve orders deliberately have no deleted field, so { deleted: false } finds 101 where { "$ne": true } finds 113 — the soft-delete trap, reproducible.

Development

npm test # unit tests
npm run test:watch
npm run test:coverage
npm run test:integration # needs `npm run demo:up`

npm run typecheck
npm run lint
npm run lint:fix

npm run build # emits lib/

Unit tests live beside the source in test/ and touch nothing external. The integration suite runs against the demo fixture and skips itself when the database is not reachable: a missing environment is a missing environment, not a broken build.

Coverage is measured over src/ and index.ts only — counting the tests and the demo would inflate the number without saying anything about the code that ships. mongo.ts is covered by the integration suite, so the figure is low when it is skipped.

Architecture

Module

Responsibility

index.ts

Composition root. The only place that reads the environment, opens the config file and builds the logger.

src/mcp-core.ts

Registers the tools and resources. Reads no environment, creates no logger, starts no transport.

src/validator.ts

Decides whether a command is allowed. Only ever rejects.

src/enforcer.ts

Turns an allowed command into an execution plan. The only place permitted to add constraints.

src/mongo.ts

Runs the plan. Makes no decisions.

src/response.ts

Extended JSON, the byte budget, the pagination envelope.

src/bson.ts

Both directions of the BSON boundary.

src/operators.ts

Every allowlist, as data, in one place.

src/config.ts, src/types.ts, src/logger.ts

Schemas, validation and structured logging to stderr.

createMongoMcpServer(deps) takes its dependencies rather than building them, which is what makes the whole query layer testable without a cluster.

Security notes

The database user is the first line of defence, not this code. Give it the read role on the exposed databases and nothing more. A connection with no write privilege cannot write, whatever a bug in this repository might allow.

Field allowlisting protects fields, not content. If a free-text field is readable because it needs to be, and someone typed an IBAN into it, that IBAN comes out — the server did exactly its job. The same goes for a $regex over a permitted field used to extract data a character at a time.

Logging goes to stderr only, because under the stdio transport stdout is the JSON-RPC channel. The connection string, tokens and authorization headers are redacted. Driver errors are logged in full and reduced to a generic message before they reach the model: a MongoServerError can carry hostnames and replica-set topology.

What this server does not do yet: per-caller identity (the scope filter is static, taken from the config, not from who is asking), an audit trail, and rate limiting. If you are pointing it at data that matters, read those three lines twice.

License

MIT © Davide D'Antonio

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.

No tool schema history has been recorded yet.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables natural language querying of MongoDB data by transforming AI agent questions into MongoDB aggregations. Supports secure data separation, semantic vector search, and advanced lookup capabilities for database interactions.
    3,646
    6
    MIT
  • F
    license
    Not graded
    quality
    C
    maintenance
    Provides read-only access to legacy MongoDB instances (<4.0) via the Model Context Protocol, enabling AI models to query databases, collections, and run aggregations safely.
    2
    -
  • A
    license
    B
    quality
    D
    maintenance
    Enables AI assistants to interact with MongoDB 3.6+ databases, supporting collection listing, document queries, counting, and aggregations through natural language.
    5
    1
    MIT

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/davidedantonio/mcp-mongodb'

If you have feedback or need assistance with the MCP directory API, please join our Discord server