Skip to content

MongoDB API as MCP — MongoService and GraphQLService implement McpAware #616

Description

@ujibang

Summary

Make RESTHeart's MongoDB-backed services first-class MCP citizens by implementing the McpAware interface introduced in v1 (see prerequisite issue). This issue does not add new MCP infrastructure — the v1 framework (tools mcp_discover and how_to_call, transport, OAuth integration, registry) already covers everything. v2 just adds the implementations that expose MongoDB collections, aggregations, change streams, and GraphQL apps as MCP resources.

The implementations read description metadata from where it naturally lives:

  • Collection metadata (already supported by RESTHeart) — mcp block alongside the existing JSON Schema, aggrs, streams arrays.
  • aggrs array entries — each aggregation can carry its own mcp block declaring parameter types.
  • streams array entries — each change stream can carry its own mcp block declaring event semantics.
  • GraphQL app documents in the gql-apps collection — mcp block at the top level.

The MCP server learns about these resources through MongoService.describeMcp() and GraphQLService.describeMcp(). No code in McpService, McpDiscoverTool, McpHowToCallTool knows anything about MongoDB.

Prerequisite: v1 MCP server framework (issue ___).


What changes

Two services gain McpAware

@RegisterPlugin(name = "mongo-service", ..., mcp = true)
public class MongoService implements Service<...>, McpAware {
    @Override
    public List<McpResource> describeMcp(McpContext ctx) {
        // Iterate over MCP-enabled databases, collections, aggregations, change streams
        // (filtered by ctx.principal()'s ACL), produce one McpResource per item.
        ...
    }
}

@RegisterPlugin(name = "graphql-service", ..., mcp = true)
public class GraphQLService implements Service<...>, McpAware {
    @Override
    public List<McpResource> describeMcp(McpContext ctx) {
        // Iterate over MCP-enabled GraphQL apps in gql-apps collection,
        // produce one McpResource per app.
        ...
    }
}

Five McpResource.kind values

The framework's kind field (open string in v1) gains five recommended values:

kind URI shape
database https://{host}/{db}
collection https://{host}/{db}/{collection}
aggregation https://{host}/{db}/{collection}/_aggrs/{name}
change-stream https://{host}/{db}/{collection}/_streams/{name}
graphql-app https://{host}/graphql/{app}

Live invalidation

When MongoDB metadata changes (collection metadata, gql-apps), the corresponding McpAware implementation invokes its InvalidationHook (registered with the MCP server in v1). The MCP server invalidates its cache and emits notifications/tools/list_changed. No new infrastructure — just wiring an existing v1 hook to an existing RESTHeart MongoInterceptor.


The mcp metadata block — uniform across kinds

The same shape used in v1 for plugin configuration applies here too. The MongoDB-API implementation reads it from the relevant document.

{
  "mcp": {
    "enabled":      true,
    "description":  "Human-readable description.",
    "params":       { /* declared parameter types — see below */ },
    "examples":     [ /* curated invocation examples */ ],
    "event_type":   "Description of event payload (change-streams only)."
  }
}
Field Required Applicable to Description
enabled yes all kinds Opt-in flag. Resource appears in mcp_discover only if true
description yes all kinds Surfaced in catalog and resource context
params conditional aggregations Parameter types for $var references in the pipeline. Other kinds: parameters are derived
examples no all kinds Curated how_to_call invocations
event_type no change-streams Description of the MongoDB Change Event shape this stream emits

Where the mcp block lives per kind

Collection — in the collection's metadata document, alongside existing jsonSchema, aggrs, streams:

{
  "mcp":        { "enabled": true, "description": "Product inventory.", "examples": [ ... ] },
  "jsonSchema": { /* existing */ },
  "aggrs":      [ /* existing, with per-entry mcp blocks */ ],
  "streams":    [ /* existing, with per-entry mcp blocks */ ]
}

Aggregation — at the entry level inside aggrs:

{
  "aggrs": [
    {
      "uri": "stockByLoc",
      "stages": [ { "$match": { "location": { "$var": "location" } } }, { "$group": { ... } } ],
      "mcp": {
        "enabled": true,
        "description": "Total stock grouped by location.",
        "params": {
          "location": { "type": "string", "enum": ["A1","A2","B1","B2"], "required": false }
        }
      }
    }
  ]
}

params declares only the types — the names of the $var references are extracted automatically from the pipeline.

Change stream — at the entry level inside streams:

{
  "streams": [
    {
      "uri": "low-stock",
      "stages": [ { "$match": { "fullDocument.quantity": { "$lt": 10 } } } ],
      "mcp": {
        "enabled": true,
        "description": "Low-stock alerts.",
        "event_type": "Update events where fullDocument.quantity < 10"
      }
    }
  ]
}

GraphQL app — at the top level of the gql-apps document:

{
  "_id": "warehouse-graphql",
  "descriptor": { "name": "warehouse-graphql", "uri": "warehouse-graphql", "enabled": true },
  "schema": "type Inventory { ... } ...",
  "mappings": { ... },
  "mcp": {
    "enabled": true,
    "description": "Cross-collection queries combining inventory, orders, suppliers.",
    "examples": [
      { "description": "Find low-stock items", "args": { "query": "{ lowStock { sku quantity } }" } }
    ]
  }
}

What the implementations derive automatically

For each kind, certain context is computed without operator declaration. Operators only declare what cannot be inferred.

Collection

The CollectionMcpResourceBuilder produces an McpResource with:

  • kind: "collection"
  • transports: [{name: "http", actions: ["query","get","create","update","delete"]}]
  • actions for query, get, create, update, delete — methods/path-templates fixed by REST conventions, per-action availability derived from ACL (a method disabled by ACL → the corresponding action absent from actions)
  • body_schema for create and update derived from the collection's existing JSON Schema
  • params for query are the standard RESTHeart query params (filter, sort, keys, page, pagesize)
  • auth derived from the security config and the principal's effective permissions
  • linked resources: aggregations (URIs of MCP-enabled aggrs), streams (URIs of MCP-enabled streams)
  • examples from mcp.examples, rendered through how_to_call for canonical descriptors

Aggregation

The AggregationMcpResourceBuilder:

  • kind: "aggregation"
  • transports: [{name: "http", actions: ["execute"]}]
  • single action execute with method: GET, path_template: "/_aggrs/<uri>"
  • params is exactly mcp.params (declared types) augmented with auto-discovered names from the pipeline:
    • $var references in stages extracted by PipelineParamScanner (a derivation helper provided by v1's framework, used here)
    • any $var not declared in mcp.params → fallback {type: "string"} and a warning surfaced in the resource context
    • any declared params key not used in the pipeline → warning logged for the operator
  • pipeline_summary produced by the PipelineSummarizer helper (best-effort, agent hint)

Change stream

The ChangeStreamMcpResourceBuilder:

  • kind: "change-stream"
  • transports: [{name: "websocket", actions: ["subscribe"], url_scheme: "wss"}, {name: "sse", actions: ["subscribe"], media_type: "text/event-stream"}] — RESTHeart serves both on the same _streams/<n> endpoint
  • single action subscribe with the URL <base>/<db>/<coll>/_streams/<uri> (scheme adjusted per transport)
  • event_type from mcp.event_type if declared
  • pipeline_summary from PipelineSummarizer

GraphQL app

The GraphqlAppMcpResourceBuilder:

  • kind: "graphql-app"
  • transports: [{name: "http", actions: ["execute"]}]
  • single action execute with method: POST, body-schema derived from SDL via SdlContextBuilder
  • params enumerates the GraphQL queries and mutations parsed from the SDL (not mcp.params — for GraphQL the SDL is the source of parameter types)
  • auth from security config

Database

Optional, opt-in via a mcp block on the database metadata:

  • kind: "database"
  • transports: [] — databases are not invokable, just discovery namespaces
  • description from mcp.description
  • references the MCP-enabled collections within the database

ACL filtering

Each implementation's describeMcp(ctx) filters by the principal:

  • MongoService — for each candidate database / collection / aggregation / stream, check the principal's effective permissions via mongoAclAuthorizer. Skip resources the principal cannot access (read-only check is enough for catalog presence; per-action permissions are reflected in the actions map).
  • GraphQLService — check the principal's permissions on /graphql/{app}. Skip apps the principal cannot reach.

The framework's existing ACL helpers (AclContextBuilder from v1, used to populate the auth.current_principal.effective_permissions field) are reused.


Live invalidation

MongoService and GraphQLService register a MongoInterceptor (response phase) on writes that affect MCP resources:

  • PUT /{db}/{collection} (or operations on metadata) → invalidate the collection's cached McpResource and any descendants (aggregations, streams).
  • Writes to gql-apps → invalidate the GraphQL app's cached McpResource.

When the cache is invalidated, the implementation calls the v1 InvalidationHook it registered at boot. The MCP server then emits notifications/tools/list_changed.

This pattern reuses entirely v1 infrastructure — no new wiring at the framework level.


Architecture additions

All new code lives in org.restheart.mongodb.mcp (or appropriate sub-package of the mongodb and graphql modules — not in the MCP framework module):

org.restheart.mongodb.mcp/
  CollectionMcpResourceBuilder.java
  AggregationMcpResourceBuilder.java
  ChangeStreamMcpResourceBuilder.java
  DatabaseMcpResourceBuilder.java
  MongoMcpAwareImpl.java              # The McpAware implementation hooked into MongoService
  MongoMetadataWatcher.java           # Interceptor for live invalidation

org.restheart.graphql.mcp/
  GraphqlAppMcpResourceBuilder.java
  GraphqlMcpAwareImpl.java
  GraphqlAppsWatcher.java

Helpers used (from v1 framework):

  • SchemaContextBuilder — JSON Schema → property descriptions for body_schema
  • SdlContextBuilder — GraphQL SDL → parsed queries/mutations
  • AclContextBuilder — ACL → effective permissions
  • AuthContextBuilder — OAuth metadata
  • PipelineParamScanner$var discovery in aggregation pipelines
  • PipelineSummarizer — pipeline/stream stages → readable summary
  • ExampleRenderermcp.examples args → rendered descriptors via how_to_call

Note: some of these helpers (SchemaContextBuilder, SdlContextBuilder, PipelineParamScanner, PipelineSummarizer) were listed as v1 framework concerns in earlier drafts — they actually fit better here as MongoDB/GraphQL-specific helpers used by the MongoDB and GraphQL McpAware implementations. The v1 framework knows nothing about MongoDB. Only the MongoDB and GraphQL modules know.

The clean separation: v1 framework provides ConfigBackedMcpAware for plain plugins; v2 modules provide their own McpAware implementations for dynamic, metadata-driven resources.


Example walkthrough

Setup

Operator adds an mcp block to a collection's metadata (existing RESTHeart endpoint):

PATCH /warehouse/inventory
Content-Type: application/json

{
  "mcp": {
    "enabled": true,
    "description": "Product inventory.",
    "examples": [
      { "description": "Find low-stock", "action": "query",
        "args": { "filter": { "quantity": { "$lt": 10 } } } }
    ]
  }
}

Catalog

// Agent calls mcp_discover()
{
  "resources": [
    { "uri": "https://cloud.restheart.com/warehouse/inventory",
      "kind": "collection",
      "description": "Product inventory." }
  ]
}

Resource context

// Agent calls mcp_discover(resource: ".../warehouse/inventory")
{
  "uri": "https://cloud.restheart.com/warehouse/inventory",
  "kind": "collection",
  "description": "Product inventory.",
  "transports": [{ "name": "http", "actions": ["query","get","create","update","delete"] }],
  "actions": {
    "query":  { "method": "GET", "path_template": "", "params": { "filter": {...}, "sort": {...}, ... } },
    "create": { "method": "POST", "body_schema": { /* derived from JSON Schema */ } },
    "update": { "method": "PATCH", "path_template": "/{id}", "params": { "id": ... }, "body_schema": {...} },
    ...
  },
  "auth": { ... },
  "examples": [
    { "description": "Find low-stock", "action": "query",
      "args": { "filter": { "quantity": { "$lt": 10 } } },
      "rendered": { "transport": "http", "method": "GET", "url": "https://.../warehouse/inventory?filter=...", ... } }
  ]
}

Invocation

// Agent calls how_to_call(resource: ".../warehouse/inventory", action: "query",
//                         args: { filter: { quantity: { $lt: 5 } } })
{
  "transport": "http",
  "method": "GET",
  "url": "https://cloud.restheart.com/warehouse/inventory?filter=%7B%22quantity%22%3A%7B%22%24lt%22%3A5%7D%7D",
  "headers": { "Authorization": "Bearer <token>" }
}

The agent issues the HTTP request directly — the MCP server has done its job.


Configuration

No MCP-specific configuration is added to MongoService or GraphQLService beyond the standard mcp = true flag (already covered by v1):

plugins-args:
  mongo-service:
    mcp: true                        # default true once this issue lands; operator can disable
  graphql-service:
    mcp: true                        # default true once this issue lands; operator can disable

Per-resource opt-in is at the metadata level (the mcp.enabled field in each collection / app document), not in plugin configuration.


Security

  • ACL filtering in describeMcp(ctx) ensures the catalog reflects only resources the principal can access.
  • Per-action availability in the actions map reflects the principal's effective permissions on each method.
  • Sensitive metadata fields (e.g. internal RESTHeart configuration) are never surfaced — only the declared mcp block content plus derived public information.
  • mcp = false precedence (v1 rule) applies: if mcp is disabled at the MongoService plugin level, no MongoDB resources appear in the catalog regardless of per-collection mcp.enabled flags.

Testing

Unit tests

Test class Covers
CollectionMcpResourceBuilderTest Full collection context: schema → body_schema; ACL → per-action availability; linked aggrs/streams
AggregationMcpResourceBuilderTest $var discovery; mcp.params matching; undeclared → fallback; declared-unused → warning; pipeline_summary
ChangeStreamMcpResourceBuilderTest WebSocket + SSE transports; URL scheme adjustment; event_type
GraphqlAppMcpResourceBuilderTest SDL parsing; queries/mutations enumeration; body_schema from SDL
DatabaseMcpResourceBuilderTest Database namespace listing
MongoMcpAwareImplTest Iteration over MCP-enabled resources; ACL filtering
GraphqlMcpAwareImplTest Iteration over MCP-enabled gql-apps; ACL filtering
MongoMetadataWatcherTest Interception detects mcp/jsonSchema/aggrs/streams changes; invalidation hook fires
GraphqlAppsWatcherTest Interception detects mcp/schema changes; invalidation fires

Integration tests

Test Scenario
McpMongoCollectionIT Add mcp block to a collection → mcp_discover returns it → context shows schema, operations, ACL-filtered actions, examples. how_to_call for each action returns valid descriptor that succeeds against RESTHeart
McpMongoAggregationIT Aggregation with $var references → mcp_discover shows params (declared + auto-discovered). how_to_call(action=execute, args={avars:{...}}) returns valid descriptor. Undeclared $var falls back to string + warning
McpMongoChangeStreamIT Stream with mcp block → mcp_discover shows two transports. how_to_call(action=subscribe, transport=websocket) returns wss descriptor; transport=sse returns http+SSE descriptor. Both descriptors work against RESTHeart
McpGraphqlAppIT GraphQL app with mcp block → mcp_discover shows SDL-parsed queries. how_to_call(action=execute, args={query, variables}) returns valid POST descriptor
McpAclFilteringIT Principal A has access to collection X but not Y → mcp_discover returns only X. Principal B has read-only on X → actions map for X has only query/get enabled
McpLiveInvalidationIT Add mcp to a new collection via REST → notifications/tools/list_changed on GET stream → mcp_discover returns updated catalog. Modify mcp.description → updated catalog reflects the change
McpFlagPrecedenceIT MongoService plugin mcp: false in config → no MongoDB resources in catalog regardless of per-collection flags

Implementation plan

Phase Deliverable
1 MongoDB derivation helpers: PipelineParamScanner, PipelineSummarizer, SchemaContextBuilder (mongodb module) + tests
2 GraphQL derivation helper: SdlContextBuilder (graphql module) + tests
3 Per-kind McpResourceBuilder classes + tests
4 MongoMcpAwareImpl + ACL filtering + integration with MongoService (@RegisterPlugin(mcp = true) + implements McpAware)
5 GraphqlMcpAwareImpl + integration with GraphQLService
6 MongoMetadataWatcher + GraphqlAppsWatcher (MongoInterceptors wired to InvalidationHook)
7 Integration tests: McpMongoCollectionIT, McpMongoAggregationIT, McpMongoChangeStreamIT, McpGraphqlAppIT
8 ACL and live invalidation integration tests
9 Documentation: how to opt-in per collection/app; mcp block reference; aggregation $var declaration guide
10 Examples: a worked example collection with all four kinds (collection, aggregation, stream, graphql-app) under examples/mcp-mongodb/

Open questions

  1. mcp flag default for MongoService/GraphQLService — once this lands, should the default be mcp = true or mcp = false? Proposal: true by default (consistent with the principle that opt-in is per-resource via the metadata mcp.enabled flag, not at the service level). Operators concerned about exposure can flip the service flag off.
  2. PipelineParamScanner strictness — undeclared $var falls back to {type: "string"} with a warning. Should it instead be a hard error to enforce explicit type declaration? Proposal: warning, configurable to error in v2.x.
  3. pipeline_summary operator override — should operators be able to override the heuristic-generated summary via mcp.pipeline_summary? Proposal: yes, optional override.
  4. Resource-scoped Protected Resource Metadata/.well-known/oauth-protected-resource/mongo-service with MongoDB-specific scopes vs the global metadata? Probably overkill; defer.
  5. Cross-collection $ref in JSON Schema — when a property has $ref pointing to another collection's schema, surface as an inline expansion or as a hint pointing to the other collection's MCP URI? Proposal: hint (consistent with v1's "describe, don't duplicate" principle).

Non-goals for v2

Sub-issue Rationale
MCP resources primitive v3 — see follow-up. Native resources/list / resources/read / resources/subscribe on top of the v1 + v2 model
Auto-generated examples for aggregation pipelines Requires parameter introspection beyond what's in params; non-trivial
Per-field ACL in body_schema Requires field-level ACL, separate feature
File bucket resources Out of scope; can be added as another McpAware implementation later

Related

  • Prerequisite: v1 MCP server framework (issue ___)
  • RESTHeart change streamsrestheart-docs/cloud/change-streams.md, restheart-docs/sse/tutorial.md
  • JSON Schema validation — restheart-docs/mongodb-rest/json-schema-validation.md
  • ACL — restheart-docs/security/authorization.md
  • GraphQL apps — restheart-docs/mongodb-graphql/
  • Aggregation pipelines — restheart-docs/mongodb-rest/aggregations.md
  • MCP specification (2025-03-26) — https://modelcontextprotocol.io/specification/2025-03-26

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions