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
ExampleRenderer — mcp.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
Resource context
Invocation
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
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.
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.
pipeline_summary operator override — should operators be able to override the heuristic-generated summary via mcp.pipeline_summary? Proposal: yes, optional override.
- Resource-scoped Protected Resource Metadata —
/.well-known/oauth-protected-resource/mongo-service with MongoDB-specific scopes vs the global metadata? Probably overkill; defer.
- 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 streams —
restheart-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
Summary
Make RESTHeart's MongoDB-backed services first-class MCP citizens by implementing the
McpAwareinterface introduced in v1 (see prerequisite issue). This issue does not add new MCP infrastructure — the v1 framework (toolsmcp_discoverandhow_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:
mcpblock alongside the existing JSON Schema,aggrs,streamsarrays.aggrsarray entries — each aggregation can carry its ownmcpblock declaring parameter types.streamsarray entries — each change stream can carry its ownmcpblock declaring event semantics.gql-appscollection —mcpblock at the top level.The MCP server learns about these resources through
MongoService.describeMcp()andGraphQLService.describeMcp(). No code inMcpService,McpDiscoverTool,McpHowToCallToolknows anything about MongoDB.Prerequisite: v1 MCP server framework (issue ___).
What changes
Two services gain
McpAwareFive
McpResource.kindvaluesThe framework's
kindfield (open string in v1) gains five recommended values:kinddatabasehttps://{host}/{db}collectionhttps://{host}/{db}/{collection}aggregationhttps://{host}/{db}/{collection}/_aggrs/{name}change-streamhttps://{host}/{db}/{collection}/_streams/{name}graphql-apphttps://{host}/graphql/{app}Live invalidation
When MongoDB metadata changes (collection metadata,
gql-apps), the correspondingMcpAwareimplementation invokes itsInvalidationHook(registered with the MCP server in v1). The MCP server invalidates its cache and emitsnotifications/tools/list_changed. No new infrastructure — just wiring an existing v1 hook to an existing RESTHeartMongoInterceptor.The
mcpmetadata block — uniform across kindsThe 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)." } }enabledmcp_discoveronly if truedescriptionparams$varreferences in the pipeline. Other kinds: parameters are derivedexampleshow_to_callinvocationsevent_typeWhere the
mcpblock lives per kindCollection — 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 } } } } ] }paramsdeclares only the types — the names of the$varreferences 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-appsdocument:{ "_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
CollectionMcpResourceBuilderproduces anMcpResourcewith:kind: "collection"transports: [{name: "http", actions: ["query","get","create","update","delete"]}]actionsforquery,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 fromactions)body_schemaforcreateandupdatederived from the collection's existing JSON Schemaparamsforqueryare the standard RESTHeart query params (filter,sort,keys,page,pagesize)authderived from the security config and the principal's effective permissionsaggregations(URIs of MCP-enabledaggrs),streams(URIs of MCP-enabled streams)examplesfrommcp.examples, rendered throughhow_to_callfor canonical descriptorsAggregation
The
AggregationMcpResourceBuilder:kind: "aggregation"transports: [{name: "http", actions: ["execute"]}]executewithmethod: GET,path_template: "/_aggrs/<uri>"paramsis exactlymcp.params(declared types) augmented with auto-discovered names from the pipeline:$varreferences instagesextracted byPipelineParamScanner(a derivation helper provided by v1's framework, used here)$varnot declared inmcp.params→ fallback{type: "string"}and a warning surfaced in the resource contextparamskey not used in the pipeline → warning logged for the operatorpipeline_summaryproduced by thePipelineSummarizerhelper (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>endpointsubscribewith the URL<base>/<db>/<coll>/_streams/<uri>(scheme adjusted per transport)event_typefrommcp.event_typeif declaredpipeline_summaryfromPipelineSummarizerGraphQL app
The
GraphqlAppMcpResourceBuilder:kind: "graphql-app"transports: [{name: "http", actions: ["execute"]}]executewithmethod: POST, body-schema derived from SDL viaSdlContextBuilderparamsenumerates the GraphQL queries and mutations parsed from the SDL (notmcp.params— for GraphQL the SDL is the source of parameter types)authfrom security configDatabase
Optional, opt-in via a
mcpblock on the database metadata:kind: "database"transports: []— databases are not invokable, just discovery namespacesdescriptionfrommcp.descriptionACL filtering
Each implementation's
describeMcp(ctx)filters by the principal:MongoService— for each candidate database / collection / aggregation / stream, check the principal's effective permissions viamongoAclAuthorizer. Skip resources the principal cannot access (read-only check is enough for catalog presence; per-action permissions are reflected in theactionsmap).GraphQLService— check the principal's permissions on/graphql/{app}. Skip apps the principal cannot reach.The framework's existing ACL helpers (
AclContextBuilderfrom v1, used to populate theauth.current_principal.effective_permissionsfield) are reused.Live invalidation
MongoServiceandGraphQLServiceregister aMongoInterceptor(response phase) on writes that affect MCP resources:PUT /{db}/{collection}(or operations on metadata) → invalidate the collection's cachedMcpResourceand any descendants (aggregations, streams).gql-apps→ invalidate the GraphQL app's cachedMcpResource.When the cache is invalidated, the implementation calls the v1
InvalidationHookit registered at boot. The MCP server then emitsnotifications/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 themongodbandgraphqlmodules — not in the MCP framework module):Helpers used (from v1 framework):
SchemaContextBuilder— JSON Schema → property descriptions forbody_schemaSdlContextBuilder— GraphQL SDL → parsed queries/mutationsAclContextBuilder— ACL → effective permissionsAuthContextBuilder— OAuth metadataPipelineParamScanner—$vardiscovery in aggregation pipelinesPipelineSummarizer— pipeline/stream stages → readable summaryExampleRenderer—mcp.examplesargs → rendered descriptors viahow_to_callNote: 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 GraphQLMcpAwareimplementations. The v1 framework knows nothing about MongoDB. Only the MongoDB and GraphQL modules know.The clean separation: v1 framework provides
ConfigBackedMcpAwarefor plain plugins; v2 modules provide their ownMcpAwareimplementations for dynamic, metadata-driven resources.Example walkthrough
Setup
Operator adds an
mcpblock 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
Resource context
Invocation
The agent issues the HTTP request directly — the MCP server has done its job.
Configuration
No MCP-specific configuration is added to
MongoServiceorGraphQLServicebeyond the standardmcp = trueflag (already covered by v1):Per-resource opt-in is at the metadata level (the
mcp.enabledfield in each collection / app document), not in plugin configuration.Security
describeMcp(ctx)ensures the catalog reflects only resources the principal can access.actionsmap reflects the principal's effective permissions on each method.mcpblock content plus derived public information.mcp = falseprecedence (v1 rule) applies: ifmcpis disabled at theMongoServiceplugin level, no MongoDB resources appear in the catalog regardless of per-collectionmcp.enabledflags.Testing
Unit tests
CollectionMcpResourceBuilderTestAggregationMcpResourceBuilderTest$vardiscovery;mcp.paramsmatching; undeclared → fallback; declared-unused → warning;pipeline_summaryChangeStreamMcpResourceBuilderTestevent_typeGraphqlAppMcpResourceBuilderTestDatabaseMcpResourceBuilderTestMongoMcpAwareImplTestGraphqlMcpAwareImplTestMongoMetadataWatcherTestGraphqlAppsWatcherTestIntegration tests
McpMongoCollectionITmcpblock to a collection →mcp_discoverreturns it → context shows schema, operations, ACL-filtered actions, examples.how_to_callfor each action returns valid descriptor that succeeds against RESTHeartMcpMongoAggregationIT$varreferences →mcp_discovershowsparams(declared + auto-discovered).how_to_call(action=execute, args={avars:{...}})returns valid descriptor. Undeclared$varfalls back to string + warningMcpMongoChangeStreamITmcpblock →mcp_discovershows two transports.how_to_call(action=subscribe, transport=websocket)returns wss descriptor;transport=ssereturns http+SSE descriptor. Both descriptors work against RESTHeartMcpGraphqlAppITmcpblock →mcp_discovershows SDL-parsed queries.how_to_call(action=execute, args={query, variables})returns valid POST descriptorMcpAclFilteringITmcp_discoverreturns only X. Principal B has read-only on X →actionsmap for X has onlyquery/getenabledMcpLiveInvalidationITmcpto a new collection via REST →notifications/tools/list_changedon GET stream →mcp_discoverreturns updated catalog. Modifymcp.description→ updated catalog reflects the changeMcpFlagPrecedenceITMongoServicepluginmcp: falsein config → no MongoDB resources in catalog regardless of per-collection flagsImplementation plan
PipelineParamScanner,PipelineSummarizer,SchemaContextBuilder(mongodb module) + testsSdlContextBuilder(graphql module) + testsMcpResourceBuilderclasses + testsMongoMcpAwareImpl+ ACL filtering + integration withMongoService(@RegisterPlugin(mcp = true)+implements McpAware)GraphqlMcpAwareImpl+ integration withGraphQLServiceMongoMetadataWatcher+GraphqlAppsWatcher(MongoInterceptors wired toInvalidationHook)McpMongoCollectionIT,McpMongoAggregationIT,McpMongoChangeStreamIT,McpGraphqlAppIT$vardeclaration guideexamples/mcp-mongodb/Open questions
mcpflag default forMongoService/GraphQLService— once this lands, should the default bemcp = trueormcp = false? Proposal:trueby default (consistent with the principle that opt-in is per-resource via the metadatamcp.enabledflag, not at the service level). Operators concerned about exposure can flip the service flag off.PipelineParamScannerstrictness — undeclared$varfalls 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.pipeline_summaryoperator override — should operators be able to override the heuristic-generated summary viamcp.pipeline_summary? Proposal: yes, optional override./.well-known/oauth-protected-resource/mongo-servicewith MongoDB-specific scopes vs the global metadata? Probably overkill; defer.$refin JSON Schema — when a property has$refpointing 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
resourcesprimitiveresources/list/resources/read/resources/subscribeon top of the v1 + v2 modelparams; non-trivialbody_schemaMcpAwareimplementation laterRelated
restheart-docs/cloud/change-streams.md,restheart-docs/sse/tutorial.mdrestheart-docs/mongodb-rest/json-schema-validation.mdrestheart-docs/security/authorization.mdrestheart-docs/mongodb-graphql/restheart-docs/mongodb-rest/aggregations.md