Stone.jsDocs
Paradigm

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

terminalbash
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.

app/Application.tsdeclarativeimperative
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

app/NotesController.tsdeclarativeimperative
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:

app/NotesController.tsts
@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.

app/NotesController.tsts
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

fieldTypeDescription
namestringStated as mcp: 'create-note' or mcp.name. Otherwise the route's own name.
descriptionstringStated as mcp.description. Otherwise the route's openapi summary or description.
inputSchemaJSON SchemaStated as mcp.inputSchema. Otherwise the route's validation schema, converted; failing that, its path parameters.
outputSchemaJSON SchemaStated 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.
the chaints
tools/call  ->  a real request to the route
                ->  rate limit -> auth -> authz -> validation -> handler

The 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.

app/AppConfig.tsts
blueprint.set('stone.mcp.route', {
  auth: true,
  rateLimit: { max: 60, window: 60, by: 'user' }
})

#Configuration

OptionTypeDefaultDescription
pathstring'/mcp'Where the endpoint is served.
namestring·What the server calls itself. Defaults to the application name.
versionstring'0.0.0'The version an agent sees.
instructionsstring·Handed to the agent once, alongside the tool list. Where to say what the API is for.
requireDescriptionbooleanfalseLeave out a tool that has no description, instead of exposing it with a warning.
routeobject·Anything to put on the endpoint route: auth, authz, rateLimit, middleware.
filterfunction·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.


Stone.js

Your app exists in every runtime. Until you run it.

An open-source project by Stone Foundation
Created by Mr. Stone (Evens Pierre)