McpAware
RESTHeart Cloud|
Note
|
McpAware is available starting from RESTHeart v9.9.
|
McpAware is the interface any plugin implements to become discoverable and callable by an AI agent over the Model Context Protocol — no MCP-specific transport code, no hand-written tool definitions. It lives in restheart-commons, so any Service, Interceptor, or other plugin, in any module, can implement it.
The actual /mcp endpoint — the server that discovers every McpAware plugin and speaks MCP’s JSON-RPC-over-HTTP transport to agents — ships as part of restheart-ai. If you’re looking to expose your MongoDB collections or GraphQL apps rather than write a plugin, see MCP for MongoDB & GraphQL instead; this page is for plugin developers.
How it works
The server never calls your plugin’s data for the agent — it only describes how to. Two tools do all the work: list_apis (what exists) and how_to_call (how to invoke it). The agent takes the descriptor how_to_call returns and makes the real HTTP request itself, directly against your plugin’s own endpoint.
sequenceDiagram
participant Agent
participant MCP as RESTHeart /mcp
participant Plugin as Your McpAware plugin
Agent->>MCP: list_apis()
MCP-->>Agent: catalog (uri, kind, description)
Agent->>MCP: how_to_call(resource, action, args)
MCP-->>Agent: { method, url, headers, body }
Agent->>Plugin: the real request, directly
Plugin-->>Agent: real data
This means RESTHeart’s existing ACL applies exactly as it does to any other client — an action the agent’s credentials can’t perform still fails with a normal 403, the same way it would for a human hitting the same endpoint. describeMcp never filters by principal; the MCP server adds discovery, not a new security boundary.
Implementing McpAware
Two ways to implement it, depending on whether your plugin exposes one fixed thing or many dynamic ones.
Mode A — a fixed, code-baked resource
If your plugin is a single endpoint with a stable shape, override defaultMcpConfig(). PingService (restheart-core) is the real example:
public class PingService implements ByteArrayService, McpAware {
@Override
public Map<String, Object> defaultMcpConfig() {
return Map.of(
"description", "Liveness probe. Returns a greeting message along with RESTHeart version and build time.",
"actions", Map.of(
"ping", Map.of(
"method", "GET",
"description", "Returns a greeting message, client IP, host, RESTHeart version and build time.")));
}
}
The default describeMcp(ctx) implementation turns this into one McpResource, at ctx.baseUrl() + ctx.pluginUri(), merged with anything an operator overrides via the plugin’s own mcp-config configuration key.
Mode B — many dynamic resources
If your plugin’s resources come from somewhere else (a database, a registry) and change at runtime, override describeMcp(McpContext ctx) directly and build McpResource instances yourself. This is how MongoService and GraphQLService do it — see MCP for MongoDB & GraphQL for what they actually expose.
@Override
public List<McpResource> describeMcp(McpContext ctx) {
var resources = new ArrayList<McpResource>();
// ... discover what to expose, e.g. by querying MongoDB, a registry, etc.
resources.add(McpResource.builder()
.uri(ctx.baseUrl() + "/my-resource")
.kind("my-kind")
.description("...")
.action("doSomething", a -> a.method("POST").bodySchema(Map.of(...)))
.build());
return resources;
}
McpContext carries principal, baseUrl, pluginName, pluginUri, and the plugin’s own resolved pluginConfiguration — everything needed to build absolute URIs without depending on HttpServerExchange or any other transport-level type.
Calling a resource: args.body
how_to_call composes a descriptor, it never executes it. For an action backed by a body_schema, the actual body goes under an args.body key — not as top-level args:
{ "resource": "https://host/my-resource", "action": "doSomething",
"args": { "body": { "field": "value" } } }
Any other top-level args entries become the query string instead — this applies uniformly across HTTP, SSE, and WebSocket transports.
Enabling and disabling
Any McpAware plugin is exposed by default; set mcp: false in its own configuration to opt out entirely — the same override mechanism as enabled/uri:
myPlugin:
mcp: false
Staying fresh: the catalog cache
list_apis/how_to_call read from a cache, not from every McpAware implementation’s describeMcp() on every call — refreshed at most every catalog-ttl-seconds. Change what your plugin exposes and an agent may see the old state for up to that long; there’s no live invalidation watching your plugin’s own data source. When a cache entry expires, already-connected agents get a notifications/tools/list_changed push telling them to refetch.
mcpService:
catalog-ttl-seconds: 30 # default 300
This is a deliberate trade-off: instant, source-specific invalidation would mean different wiring per McpAware implementation (a MongoDB write, a config reload, …); a single TTL is one uniform mechanism for all of them, at the cost of a bounded staleness window.