Extensions
MCP server
Your routes become tools an AI agent can call. One endpoint appears, no route changes, and a tool call arrives at its route as an ordinary request: the same rate limit, the same authentication, the same authorization, the same validation.
#Install
npm i @stone-js/mcp
npm i @stone-js/openapi # optional, to derive tool arguments from your schemas#Enable it
The principle
An agent is a caller like any other. What it may do is what the caller it acts for may do, decided where that is already decided.
In Stone.js
A tool is a route that said so. The module adds one endpoint and derives the tool list from the router; a call is dispatched back into the router, so nothing is described twice and nothing is guarded twice.
import { Mcp } from '@stone-js/mcp'
import { Routing } from '@stone-js/router'
import { StoneApp } from '@stone-js/core'
@Mcp({ instructions: 'Tools for managing notes. Read before you write.' })
@Routing()
@StoneApp({ name: 'app' })
export class Application {}import { mcpBlueprint } from '@stone-js/mcp'
import { defineConfig, defineStoneApp } from '@stone-js/core'
// Enable the module on the manifest, exactly where the decorator sits
export const App = defineStoneApp({ name: 'app' }, [mcpBlueprint])
// Then configure it
export const AppConfig = defineConfig((blueprint) => blueprint.set('stone.mcp', {
instructions: 'Tools for managing notes. Read before you write.',
route: { auth: true }
}))That adds one route, /mcp, and changes nothing else. No route becomes a tool until it says so.
#Declare a tool
export class NotesController {
@Post('/notes', { mcp: 'create-note' })
create (event: IncomingHttpEvent) { /* ... */ }
}export const NotesController = defineEventHandler({}, {
create: definePost('/notes', { mcp: 'create-note' })
})The long form states what the short form would otherwise 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, for an application that writes its declarations on the method.
import { Tool } from '@stone-js/mcp'
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 | Type | Description |
|---|---|---|
| name | string | Stated as mcp: 'create-note' or mcp.name. Otherwise the route's own name. |
| description | string | Stated as mcp.description. Otherwise the route's openapi summary or description. |
| inputSchema | JSON Schema | Stated as mcp.inputSchema. Otherwise the route's validation schema, converted; failing that, its path parameters. |
| outputSchema | JSON Schema | Stated as mcp.outputSchema, or nothing. A shape nobody promised is not sent. |
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
An agent acts for someone. That someone is the principal.
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. 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.
#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
| Option | Type | Default | Description |
|---|---|---|---|
| path | string | '/mcp' | Where the endpoint is served. |
| name | string | · | What the server calls itself. Defaults to the application name. |
| version | string | '0.0.0' | The version an agent sees. |
| instructions | string | · | Handed to the agent once, alongside the tool list. Where to say what the API is for. |
| requireDescription | boolean | false | Leave out a tool that has no description, instead of exposing it with a warning. |
| route | object | · | Anything to put on the endpoint route: auth, authz, rateLimit, middleware. |
| filter | function | · | The last word on which tools are exposed, for what a declaration cannot know: an environment, a flag, a caller. |
#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.