fastify-mcp-routes
Exposes Fastify routes as MCP tools, enabling agents to discover and call existing Fastify HTTP endpoints through app.inject while preserving route hooks, authorization, validation, and schema-derived inputs. Supports read-only defaults, per-route tools or progressive meta-tools, and route metadata from Fastify schemas.
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
fastify-mcp-routes
Your Fastify routes become MCP tools in one register call: no codegen, no OpenAPI file, and your hooks, auth and validation keep running because every tool call goes through app.inject. Read-only by default, with two modes: one tool per route, or three meta-tools that keep tools/list the same size at 5 routes and at 200. A Fastify take on the idea behind fastapi_mcp.
English · Русский
Quick start · Why · Modes · Options · Security · Limits
Quick start
npm install fastify-mcp-routesRegister the plugin before the routes you want to expose:
import Fastify from "fastify";
import { mcpRoutes } from "fastify-mcp-routes";
const app = Fastify();
await app.register(mcpRoutes);
app.get(
"/clients/:id",
{
schema: {
summary: "Get a client by id",
params: {
type: "object",
properties: { id: { type: "string" } },
required: ["id"],
},
querystring: {
type: "object",
properties: { expand: { type: "boolean" } },
},
},
},
async (request) => ({ id: (request.params as { id: string }).id, name: "Alice" }),
);
await app.listen({ port: 3000 });Connect Claude Code to the endpoint:
claude mcp add --transport http -s project app http://localhost:3000/mcp-s project sets the scope: the server is written to .mcp.json in the project root, so everyone who clones the repository gets the same endpoint (Claude Code asks each person to approve it first). The default scope, local, keeps the server private to you in this project, and user makes it available in all your projects.
If the routes need credentials, pass them as headers. The plugin forwards Authorization and x-api-key to the route it calls. Use -s local or -s user here: with -s project the token would be written to .mcp.json and end up in git.
claude mcp add --transport http -s local app http://localhost:3000/mcp --header "Authorization: Bearer $TOKEN"This is what an MCP client gets from tools/list for the app above (the output of a real request, formatted for reading):
{
"tools": [
{
"name": "get_clients_by_id",
"description": "Get a client by id",
"inputSchema": {
"type": "object",
"properties": {
"id": { "type": "string" },
"expand": { "type": "boolean" }
},
"required": ["id"]
},
"annotations": { "readOnlyHint": true }
}
]
}And the result of tools/call with { "id": "42" }:
{ "content": [{ "type": "text", "text": "{\"id\":\"42\",\"name\":\"Alice\"}" }] }RegistermcpRoutes before the routes. The plugin collects routes with the onRoute hook, so routes registered earlier are not visible to it.
Related MCP server: fast-mcp
Why fastify-mcp-routes?
Capability | What it gives you |
No codegen, no OpenAPI file | Tools are built from the routes Fastify already knows, through the |
Your stack keeps running | Calls go through |
Read-only by default | Only |
Two modes | One tool per route, or three meta-tools whose |
Stateless Streamable HTTP |
|
Measurable size |
|
Official SDK | Built on |
How it fits
MCP client
│
POST /mcp (Streamable HTTP, stateless)
│
fastify-mcp-routes
│
app.inject + Authorization and x-api-key
│
Your routes: hooks · auth · validation · handlersamnezia-api is a real Fastify 5 service with JSON-schema routes, the kind of application this plugin targets.
Modes
Set the mode with the mode option.
Mode | Tools in | Use it when |
| One per exposed route | The app has a handful of routes and you want the model to see each one |
| Exactly three: | The app has dozens of routes and |
In progressive mode the model searches first, then reads the schema of one route, then calls it. The same app as above, asked for search_routes with { "query": "client" }, answers:
{
"matched": 1,
"routes": [
{
"name": "get_clients_by_id",
"method": "GET",
"url": "/clients/:id",
"description": "Get a client by id"
}
]
}call_route takes { "name": "get_clients_by_id", "arguments": { "id": "42" } } and returns the same text as the tool in tools mode.
How big is tools/list
The size is estimated as the characters of JSON.stringify(tools) divided by 4, rounded up. This is a rough rule, not a tokenizer. npm run bench builds fixtures with 5, 50 and 200 routes (per resource: list, get, create, update, delete, each with summaries and typed params, query and body) with readOnly: false and prints this table:
Routes | Tools mode, tools | Tools mode, tokens | Progressive, tools | Progressive, tokens | Ratio |
5 | 5 | 337 | 3 | 236 | 1.4x |
50 | 50 | 3373 | 3 | 236 | 14.3x |
200 | 200 | 13518 | 3 | 236 | 57.3x |
The progressive size is the same for every app, because it depends only on the three fixed tool definitions. The tools size grows linearly with the route count.
For your own app, call app.mcpRoutes.stats() after app.ready(). For the Quick start app it returns:
{
"method": "chars/4",
"routes": { "total": 1, "exposed": 1, "hidden": 0 },
"modes": {
"tools": { "tools": 1, "chars": 217, "tokens": 55 },
"progressive": { "tools": 3, "chars": 941, "tokens": 236 }
}
}app.mcpRoutes.routes() lists the exposed routes with their tool names.
Tool names and input
The name of a tool is the first of these that exists:
config.mcp.nameof the route.schema.operationIdof the route, or theoperationIdfrom@fastify/swagger.The method and the URL:
GET /clients/:id/ordersbecomesget_clients_by_id_orders.
Names use letters, digits, _, . and -, are cut at 64 characters, and get a suffix _2, _3 when they repeat.
The description is the first of config.mcp.description, schema.description, schema.summary, the same fields from @fastify/swagger, or METHOD /url.
inputSchema merges params, querystring and body into one flat object:
A path parameter that has no schema becomes a required string.
:id?is optional, and when it is left out the route receives an empty string. A*wildcard becomes the argumentwildcard.If a name repeats, the later source gets a prefix:
query_id,body_id.A body that is not an object with properties (an array, for example) becomes the argument
body.POST,PUTandPATCHroutes without a body schema get an optionalbodyobject.$refto schemas added withapp.addSchemais inlined.Arguments that are not in the schema are ignored.
What a tool call returns:
The route answers | The tool result |
2xx or 3xx with a body | The body as text, unchanged |
2xx or 3xx with an empty body |
|
4xx or 5xx |
|
A path parameter is missing, or the tool is unknown |
|
Security
Read-only by default. Only
GETandHEADbecome tools. Open a mutation withallow: ["POST /clients"]or open everything withreadOnly: false.Closing wins.
deny,config.mcp.hidden: trueandschema.hide: truebeatallow.OPTIONSis never exposed, and neither is the/mcpendpoint itself.Your authorization applies. The plugin copies
Authorizationandx-api-keyfrom the MCP request to the route call. Without them a protected route answers 401 and the tool returnsisError. Change the list withforwardHeaders.Browsers are refused by default. A request with an
Originheader gets 403 unless its hostname is inallowedOrigins. Non-browser MCP clients normally send noOrigin.The endpoint itself is not authenticated.
tools/listshows route names and schemas to anyone who can reach/mcp. Hide internal routes, bind to a private network, or register the plugin inside a scope that has your own auth hook:
await app.register(
async (scope) => {
scope.addHook("onRequest", requireAuth);
await scope.register(mcpRoutes);
scope.get("/reports", reportsHandler);
},
{ prefix: "/api" },
);Here the endpoint is /api/mcp, the hook protects it, and only the routes of this scope become tools.
Options
Option | Default | Description |
|
| Path of the endpoint, relative to the prefix the plugin is registered with |
|
|
|
|
| Expose only |
|
| Rules that open a route even when |
|
| Rules that close a route. They win over |
|
| Headers of the MCP request copied to the route call |
|
| Hostnames accepted in the |
|
| Fill missing descriptions and |
| package name and version |
|
| none | Text for the model returned by |
A rule is "METHOD /url", for example "POST /clients", "DELETE /clients/:id" or "* /admin/*". Without a method it matches every method. * matches any characters. The URL is compared with the registered pattern, including the prefix, so write /clients/:id, not /clients/42.
Per-route settings go to config.mcp:
app.get(
"/internal/stats",
{ config: { mcp: { name: "service_stats", description: "Current service counters" } } },
statsHandler,
);
app.get("/internal/debug", { config: { mcp: { hidden: true } } }, debugHandler);@fastify/swagger
If @fastify/swagger is registered, the plugin reads the OpenAPI document after the app is ready and uses its summary, description and operationId for routes that do not have them in their own schema. This matters when a transform function or another plugin fills the documentation. Set swagger: false to turn it off. The package is an optional peer dependency.
What it does not do
Zod-style schemas. Route schemas must be JSON Schema (plain objects, TypeBox included). Schemas that are class instances, such as those from Zod type providers, are skipped, and the tool then lacks those arguments.
Only tools. No resources, prompts, sessions, resumable streams or server-initiated notifications.
GETandDELETEon/mcpanswer 405. The one exception isDELETEwithContent-Type: application/jsonand an empty body: Fastify's body parser rejects it before the plugin runs, so the answer is 400 withFST_ERR_CTP_EMPTY_JSON_BODY. Send theDELETEwithout that header, or with a body, to get the 405.Text results. Binary and streamed responses are returned as text.
Registration order. Routes registered before the plugin are not seen.
Token estimates are approximate. The
chars/4rule is not a tokenizer.Large apps. In
toolsmodetools/listgrows with the route count. Useprogressivemode ordeny.ESM only. The package requires Node.js 22.12 or newer.
require()works on those versions.
Development
npm ci
npm run lint
npm test
npm run build
npm run benchNode.js 22.12 and newer. CI runs lint, tests, build and bench on Node.js 22 and 24. The tests start a real server with app.listen({ port: 0 }) and talk to it with the official MCP client over Streamable HTTP. Architecture notes are in AGENTS.md, contribution rules in CONTRIBUTING.md.
Support the project
If fastify-mcp-routes is useful to you, you can support its development:
TON:
kyoresuas.tonTON wallet address:
UQATzyMcVoZudjaA5Rc9w5TV_lIqxh9DPk0gSnjkLVvwEVqzUSDT on TRON (TRC-20):
TJSLjDo4yRgHr9JqqEcrkbC51bP9DCiBC7
Please verify the network before transferring cryptocurrency.
Contact
Telegram: @stercuss
Email: hey@kyoresuas.com
Disclaimer
An independent project. Not affiliated with Anthropic, the Model Context Protocol project or Fastify, not sponsored, no advertising of AI services.
License
This server cannot be deployed
Maintenance
Related MCP Connectors
Host any HTTP API as MCP tools with a tamper-evident cross-agent settlement ledger.
MCP server for your apps' tools and custom tools, plus hosted AI agents and approval-gated workflows
Zero-dependency Cloudflare Worker gateway turning websites and REST APIs into MCP tools.
Streamable HTTP MCP gateway for NV oOS fleets — one API key, namespaced tools, quotas and policy.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceAutomatically turns existing Elysia routes into MCP tools, reusing schemas and running full request lifecycle.42 npmMIT
- AlicenseNot gradedqualityAmaintenanceTurns existing FastAPI routes and custom Python functions into MCP tools, exposing them over ASGI, SSE, or stdio so desktop and chat AI clients can call them with native dependency-injected auth, progressive tool discovery, and self-correcting error responses. It also serves interactive in-chat HTML/JS widgets and a built-in browser inspector for testing tools without external CLIs.MIT
- AlicenseNot gradedqualityAmaintenanceTurns existing FastAPI routes (or any OpenAPI spec) into owner-approved MCP tools served from one governed endpoint, keeping tools hidden until an owner reviews a diff and enables them. Agents get incremental discovery via meta-tools, response field projection, per-caller API-key policy, PII redaction, and a JSON-lines audit log.44 PyPI1MIT
- FlicenseNot gradedqualityAmaintenanceEnables Frappe Framework apps to expose registered tools as a Streamable HTTP MCP server, allowing MCP clients to invoke app functions through an API endpoint.-