@stone-js/mcp
v0.8.19
Published
MCP for Stone.js: your existing routes become tools an AI agent can call, through the chain that already protects them.
Maintainers
Readme
Stone.js - MCP
MCP for Stone.js. Your routes become tools an AI agent can call, through the chain that already protects them.
A tool is a route that said so. Nothing is registered twice, nothing is described twice, and a tool call is a real request: the rate limit, the authentication, the authorization and the validation that guard the route are the ones that guard the tool.
Installation
npm install @stone-js/mcp
# optional, to derive tool arguments from your validation schemas:
npm install @stone-js/openapiPeer dependencies:
@stone-js/coreand@stone-js/http-core.@stone-js/openapiis optional.
Enable it
Declarative:
import { Mcp } from '@stone-js/mcp'
import { StoneApp } from '@stone-js/core'
@Mcp()
@Routing()
@StoneApp({ name: 'app' })
export class Application {}Imperative, through stone.mcp:
import { defineConfig, defineStoneApp } from '@stone-js/core'
import { mcpBlueprint } from '@stone-js/mcp'
export const App = defineStoneApp({ name: 'app' }, [mcpBlueprint])
export const AppConfig = defineConfig((blueprint) => blueprint.set('stone.mcp', {
instructions: 'Tools for managing notes. Read before you write.',
route: { auth: true }
}))Enabling the module adds one route, /mcp, and changes nothing else. No route becomes a tool until
it says so.
Declare a tool
The short form names it:
@Post('/notes', { mcp: 'create-note' })
create (event: IncomingHttpEvent) { … }The long form states what the short form would have to derive:
@Post('/notes', {
mcp: {
name: 'create-note',
description: 'Create a note for the signed-in user.',
annotations: { destructiveHint: false }
}
})On the handler instead, the same shape as @Validate and @Returns:
class NotesController {
@Tool({ name: 'create-note', description: 'Create a note for the signed-in user.' })
@Post('/notes')
create (event: IncomingHttpEvent) { … }
}Both are read. The route wins when both are present, because with a router in play a route is the single description of itself.
What is derived, and from where
| Field | Stated on the declaration | Otherwise derived from |
|---|---|---|
| name | mcp: 'create-note' or mcp.name | the route's name |
| description | mcp.description | the route's openapi summary or description |
| inputSchema | mcp.inputSchema | the route's validation schema, converted; failing that, its path parameters |
| outputSchema | mcp.outputSchema | nothing. A shape nobody promised is not sent |
Every source is optional. An application with openapi and validation writes almost nothing. An
application with neither still gets working tools, built from what a route always has: a path, a
method, and its parameters. That degradation is the design, not a fallback.
A tool with no description is exposed and logged: an agent reading a bare name will guess, and it
guesses worst on the routes that write. Set stone.mcp.requireDescription to leave those out
instead.
What happens on a call
tools/call -> a real request to the route -> rate limit -> auth -> authz -> validation -> handlerThe caller's headers travel with it, so the bearer an agent was given is the bearer the route authenticates. An agent acts for someone, and that someone is the principal. Nothing here carries a permission model of its own: a second set of rules would be a second thing to keep in step with the first.
A tool that fails answers a result with isError, not a protocol error. An agent reads it, explains
it and tries something else, which is what should happen when an authorization refuses.
Protecting the endpoint
The endpoint is a route, so it is guarded like one:
blueprint.set('stone.mcp.route', {
auth: true,
rateLimit: { max: 60, window: 60, by: 'user' }
})Configuration
| Key | Meaning |
|---|---|
| path | Where the endpoint is served. Defaults to /mcp. |
| name / version | What the server calls itself during initialize. |
| instructions | Handed to the agent once, alongside the tool list. |
| requireDescription | Leave out a tool with no description. Defaults to false. |
| route | Anything to put on the endpoint's route: auth, authz, rateLimit, middleware. |
| filter | The last word on which tools are exposed, for what a declaration cannot know. |
Why there is no stream
An MCP server over HTTP is one POST endpoint: the client posts JSON-RPC, the server answers JSON, the connection closes. A stream is only needed for what a server sends unprompted, progress on a long tool or a server-initiated sampling request, and an API exposing its own routes sends none of them.
That is why this runs unchanged on a long-lived Node server, on a Lambda, or at the edge. There is nothing to keep open, and no session to hold.
Documentation
See the official documentation for the full guide.
