Skip to content

MCP resources primitive on top of the existing tools-based framework #617

Description

@ujibang

Summary

Add support for the MCP resources primitive on top of the v1 framework + v2 MongoDB integration. Everything that v1 and v2 already expose via the mcp_discover tool is promoted to first-class MCP resources, gaining:

  • Visual picker in MCP host applications (Claude Desktop, Cursor, VS Code) so users can attach a resource to the conversation.
  • resources/read documents-mode — for collection resources, reading the URL with a query string (?filter={...}) returns actual MongoDB documents loaded into the agent's context.
  • resources/subscribe for built-in change notifications on metadata updates.
  • Resource Templates so agents construct URIs without a prior list call.

The McpResource model and the McpAware interface stay unchanged. v3 only adds the new MCP primitive on top — same data, different protocol surface.

Prerequisites:

  • v1 MCP framework (issue ___).
  • v2 MongoDB API as MCP (issue ___) — enables documents-mode for collections.

Scope

What v3 adds

  1. Resources capability announced at initialize:

    { "capabilities": { "tools": { "listChanged": true }, "resources": { "subscribe": true, "listChanged": true } } }
  2. resources/list — same catalog as mcp_discover() (no argument), reformatted for the MCP Resource schema.

  3. resources/read — two modes:

    • Context mode — bare resource URI returns the same context object as mcp_discover(resource). v1 and v3 clients see identical content.
    • Documents mode (collection resources only) — collection URL with query string or document path returns actual documents (proxies to RESTHeart GET).
  4. resources/templates/list — RFC 6570 templates for collection-context, collection-documents, single-document, aggregation, change-stream, GraphQL-app URIs.

  5. resources/subscribe — agents subscribe to a URI and receive notifications/resources/updated when the underlying McpResource changes; notifications/resources/list_changed when entries are added/removed.

  6. mcp_discover deprecation policy — v3 retains it for backwards compatibility but its description marks it as deprecated and points to resources/* as preferred. To be removed in a later major version.

What v3 does NOT change

  • McpResource schema and McpAware interface — exactly as defined in v1.
  • how_to_call — still the only execution composer. Still composes, never executes.
  • Per-kind builders from v2 — reused unchanged. The same CollectionMcpResourceBuilder produces the JSON whether called via mcp_discover or resources/read.
  • Tools manifest — how_to_call remains. mcp_discover is kept (deprecated).

Two reading channels (the v3 design point)

Once v3 lands, the agent has two ways to read collection data:

Use resources/read when… Use how_to_call(action=query) when…
Documents should be loaded into the agent's context to reason about The agent generates code/scripts that run elsewhere
Host UI lets the user attach a resource Host has no HTTP execution; user runs the request manually
Agent has subscribed and wants the latest state on notifications/resources/updated Agent wants to validate the request structure before sending
Single read, no further write Read-then-write flow needing the read's URL/headers/body

Live data remains exclusively how_to_call(action=subscribe) — long-lived streams cannot fit resources/read's request/response shape. The MCP server does not proxy stream traffic in v3 either; it never will.

The how_to_call description is updated in v3 to redirect agents to resources/read when their goal is to get documents (rather than describe how to get them).


URI scheme

The resource URI is the HTTP URL — same convention as v1's mcp_discover argument. No translation, no custom scheme. The same string identifies the resource in MCP and is the URL to invoke via HTTP.


resources/list

Same logical content as mcp_discover() (catalog), reformatted to the MCP Resource schema:

{
  "resources": [
    { "uri": "https://cloud.restheart.com/warehouse/inventory",
      "name": "inventory",
      "description": "Product inventory.",
      "mimeType": "application/json" },
    { "uri": "https://cloud.restheart.com/warehouse/inventory/_aggrs/stockByLoc",
      "name": "inventory.stockByLoc",
      "description": "Total stock grouped by warehouse location.",
      "mimeType": "application/json" },
    { "uri": "https://cloud.restheart.com/graphql/warehouse-graphql",
      "name": "warehouse-graphql",
      "description": "Cross-collection queries.",
      "mimeType": "application/json" },
    { "uri": "https://cloud.restheart.com/echo",
      "name": "echo",
      "description": "Echoes the request body.",
      "mimeType": "application/json" }
  ]
}

Filtered by ACL via the same McpAware.describeMcp(ctx) calls as mcp_discover. Mixes resources contributed by all McpAware implementations (MongoDB-API, GraphQL-API, custom plugins).


resources/templates/list

{
  "resourceTemplates": [
    { "uriTemplate": "https://{host}/{db}/{collection}",
      "name": "Collection — context",
      "mimeType": "application/json" },
    { "uriTemplate": "https://{host}/{db}/{collection}{?filter,sort,keys,page,pagesize,hal}",
      "name": "Collection — documents",
      "mimeType": "application/json" },
    { "uriTemplate": "https://{host}/{db}/{collection}/{docid}",
      "name": "Document",
      "mimeType": "application/json" },
    { "uriTemplate": "https://{host}/{db}/{collection}/_aggrs/{name}",
      "name": "Aggregation — context",
      "mimeType": "application/json" },
    { "uriTemplate": "https://{host}/{db}/{collection}/_streams/{name}",
      "name": "Change stream — context",
      "mimeType": "application/json" },
    { "uriTemplate": "https://{host}/graphql/{app}",
      "name": "GraphQL app — context",
      "mimeType": "application/json" }
  ]
}

The two collection templates (context vs documents) let MCP clients render parameter-input UIs for filter/sort/etc.

Custom plugins contributing their own resources may register additional templates via the v1 framework — see McpResourceTemplate API addition below.


resources/read

Mode detection

URI shape Mode Action
https://{host}/{db} Context Database context
https://{host}/{db}/{collection} Context Collection context
https://{host}/{db}/{collection}?... Documents Proxies GET to RESTHeart
https://{host}/{db}/{collection}/{docid} Documents Proxies GET to RESTHeart
https://{host}/{db}/{collection}/_aggrs/{n} Context Aggregation context
https://{host}/{db}/{collection}/_streams/{n} Context Change stream context
https://{host}/graphql/{app} Context GraphQL app context
Other (custom plugin URI) Context Plugin resource context

For collections, detection rule: query string OR path beyond /{collection} → documents; bare URL → context. Other kinds support context mode only — there is no "documents-mode" of an aggregation or a custom plugin endpoint, because those are invocations (use how_to_call).

Context mode — same content as v1's mcp_discover(resource)

The text field contains the same JSON object mcp_discover(resource) returns. The per-kind builders from v1 and v2 are reused unchanged:

{
  "contents": [
    {
      "uri":      "https://cloud.restheart.com/warehouse/inventory",
      "mimeType": "application/json",
      "text":     { /* identical to mcp_discover(uri) output */ }
    }
  ]
}

Documents mode (collections only, new)

Proxies GET to RESTHeart's existing collection handler — no MongoDB logic reimplemented. The proxy parses the URI, builds a MongoRequest carrying the MCP session's principal, invokes the same handler chain a normal GET would. ACL, JSON Schema validation, pagination — all apply unchanged.

{
  "contents": [{
    "uri":      "https://cloud.restheart.com/warehouse/inventory?filter=...&pagesize=20",
    "mimeType": "application/json",
    "text":     [
      { "_id": "...", "sku": "AB-0007", "quantity": 2, "location": "A1", "reserved": 0 },
      { "_id": "...", "sku": "CD-0042", "quantity": 5, "location": "B2", "reserved": 1 }
    ],
    "_meta":    { "total_count": 142, "page": 1, "next": "?filter=...&page=2" }
  }]
}

Single document mode

{
  "contents": [{
    "uri":      "https://cloud.restheart.com/warehouse/inventory/65f3a1b2c4d5e6f7890123ab",
    "mimeType": "application/json",
    "text":     { "_id": "65f3...", "sku": "AB-1234", "quantity": 100, "location": "A1" }
  }]
}

resources/subscribe

Standard MCP subscribe. The client receives:

  • notifications/resources/updated — when the subscribed URI's underlying McpResource changes (the contributing McpAware implementation calls its InvalidationHook).
  • notifications/resources/list_changed — when an McpResource is added/removed anywhere in the catalog.

The subscription registry is per-URI. Multiple sessions can subscribe to the same URI; each receives notifications independently.

The infrastructure reuses the v1 InvalidationHook mechanism — no new hook surface for plugins.


Architecture additions

New classes — under the existing org.restheart.ai.mcp package:

McpResourceHandler.java           # resources/list, resources/read (mode dispatch), resources/templates/list
DocumentReadProxy.java            # resources/read documents-mode (proxies to RESTHeart GET)
McpSubscriptionManager.java       # resources/subscribe; per-URI subscriber registry
api/
  McpResourceTemplate.java        # NEW: plugin-supplied resource templates

Plugin component summaries (additions only)

  • McpResourceHandlerresources/list, resources/read (mode-dispatched), resources/templates/list. For context mode, dispatches to the same builders that mcp_discover uses (no duplication). For documents mode (collection URIs only), dispatches to DocumentReadProxy.
  • DocumentReadProxy — proxies documents-mode to MongoDB-API. Builds MongoRequest carrying the MCP session principal. Surfaces RESTHeart pagination headers as _meta fields. This is the only new code path that touches the MongoDB module from the MCP framework — and it does so only via the already-public REST handler entry point.
  • McpSubscriptionManager — per-URI registry. On InvalidationHook fire from any McpAware, delivers notifications/resources/updated to subscribed sessions; broadcasts notifications/resources/list_changed when the catalog changes.

Updates to v1 components

  • McpService — adds dispatch for resources/list, resources/read, resources/templates/list, resources/subscribe.
  • DiscoverTool — description updated to mark it as deprecated and point to resources/* as preferred.
  • HowToCallTool — description updated to recommend resources/read when the goal is to load documents into context (covering the case where the agent picks the wrong channel).

New API surface for plugins (small)

A plugin that wants to contribute its own URI templates — useful for parameterised resources like a custom report endpoint — implements an additional optional method:

public interface McpAware {
    List<McpResource> describeMcp(McpContext ctx);
    default List<McpResourceTemplate> describeTemplates(McpContext ctx) { return List.of(); }
    default void registerInvalidationHook(InvalidationHook hook) {}
}

MongoMcpAwareImpl and GraphqlMcpAwareImpl from v2 implement describeTemplates to return the MongoDB-related templates listed above. Custom plugins typically don't need to.


Configuration

plugins-args:
  mcp-service:
    # ... v1 + v2 config ...
    resources:
      enabled: true                       # toggle the resources primitive
      include-document-mode: true         # enable documents-mode resources/read on collections
      subscribe-enabled: true
    deprecated-tools:
      mcp-discover:
        enabled: true                     # keep mcp_discover available alongside resources/* (default: true)

Security

Same model as v1 + v2. Additionally:

  • resources/list returns only resources the principal can access (same ACL filter as mcp_discover — both go through McpAware.describeMcp(ctx)).
  • resources/read documents-mode delegates to RESTHeart's existing collection handler — full ACL, JSON Schema validation, pagination apply unchanged.
  • Unknown/malformed URIs → JSON-RPC error -32002.

Testing

Unit tests

Test class Covers
McpResourceHandlerTest resources/list mixed catalog (MongoDB + GraphQL + custom plugins) + ACL; resources/read mode dispatch; templates response; equivalence with mcp_discover for context content
DocumentReadProxyTest Documents mode for query / single doc; ACL; pagination → _meta
McpSubscriptionManagerTest Subscribe/unsubscribe; updated/list_changed delivery; multi-session delivery

Integration tests

Test Scenario
McpResourcesV3IT initializeresources/list (mixed: MongoDB collection + aggregation + change-stream + GraphQL app + custom plugin) → resources/read on each → assert content equals mcp_discover(uri) for the same URI
McpDocumentsModeIT resources/read with query string → documents matching shape of GET. Document URL → single doc. ACL filtering. Pagination → _meta
McpResourceTemplatesIT resources/templates/list → all six MongoDB templates present and well-formed; custom plugin contributing additional templates also surfaces
McpSubscribeIT Subscribe to a collection URI → modify mcp metadata via REST → notifications/resources/updated on GET stream. Add new MCP-enabled resource → notifications/resources/list_changed. Subscribe to a custom plugin resource → custom invalidation also delivered
McpDiscoverDeprecatedIT mcp_discover still works in v3; description includes deprecation hint
McpHowToCallHintIT how_to_call(action=query) description and result include a hint pointing to the equivalent resources/read URI

Implementation plan

Phase Deliverable
1 McpResourceHandler skeleton: resources/list + resources/templates/list + resources/read context-mode (delegating to v1 builders + McpAware.describeMcp) + McpResourcesV3IT
2 DocumentReadProxy: documents-mode proxy to MongoDB-API + McpDocumentsModeIT
3 McpResourceTemplate API addition + extend McpAware with describeTemplates default method + tests
4 Implement describeTemplates on MongoMcpAwareImpl and GraphqlMcpAwareImpl + McpResourceTemplatesIT
5 McpSubscriptionManager + extend McpAware's InvalidationHook to also fire notifications/resources/* + McpSubscribeIT
6 Update McpService dispatcher to route resources/*
7 Update initialize to declare resources capability
8 Update mcp_discover description (deprecation hint) and how_to_call description (point to resources/read for reads) + McpDiscoverDeprecatedIT + McpHowToCallHintIT
9 Documentation update; migration guide for v1+v2 clients to v3

Open questions

  1. Should mcp_discover be removed in v3 or only deprecated? Removing it is cleaner but breaks v1+v2 clients. Proposal: deprecate in v3.0, remove in v4.0.
  2. Documents-mode for aggregations? resources/read on /_aggrs/{n} could execute the aggregation with default args and return results. Probably not — aggregations need parameters and that's how_to_call(action=execute) territory. Proposal: aggregations stay context-only via resources/read.
  3. Resource picker UX with parameterised templates — when MCP hosts present the Collection — documents template to the user, they need to render a UI for filter, sort, etc. RESTHeart could publish a JSON Schema for these query parameters to help host UIs. Proposal: investigate after host adoption is observed.
  4. resources/list pagination — the MCP spec supports cursor for paginating long lists. For deployments with thousands of MCP-enabled resources, this may be needed. Proposal: implement after v3 lands.
  5. McpResourceTemplate reach — should custom plugins be able to register any RFC 6570 template, or only ones that match URIs they own? Proposal: only their own (validated against the plugin's defaultURI + uri config).

Non-goals for v3

Sub-issue Rationale
MCP prompts support Separate primitive; orthogonal
Streaming proxy via MCP Change streams stay direct WebSocket/SSE to RESTHeart
Per-field ACL in resource context Requires field-level ACL, separate feature

Related

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions