MCP
Install the MCP plugin so AI assistants can call your API as tools, on any adapter.
MCP support is new and still settling. The plugin options and the generated tool surface may change before v2, so pin your version if you depend on them.
@ts-kizuna/mcp adds an MCP (Model Context Protocol) endpoint to your API. Each route becomes an MCP tool that AI assistants can discover and call.
pnpm add @ts-kizuna/mcpbun add @ts-kizuna/mcpnpm install @ts-kizuna/mcpInstall it
MCP is a plugin. You install it where the rest of your API's vocabulary is declared, and api.mount serves it:
import { Kizuna } from '@ts-kizuna/core';
import { mcpPlugin } from '@ts-kizuna/mcp';
export const k = new Kizuna({
tags,
identities,
plugins: {
mcp: mcpPlugin({
name: 'My API',
}),
},
});Then pass its server half to server.api, where the MCP SDK and the transport live:
import { mcpPluginServer } from '@ts-kizuna/mcp/server';
export const api = server.api({
router,
plugins: {
mcp: mcpPluginServer(),
},
});The endpoint answers at /mcp by default. On Next.js it is served by the catch-all route file that already serves your contract, so with basePath: '/api' it lands at /api/mcp.
It is not in your client
The MCP endpoint is a plugin route, so it lives outside contract.routes. Your fetch client, OpenAPI document and generated Swift and Kotlin clients never see it, which is what you want: it is a protocol endpoint, not part of your API's surface.
How it works
Each route becomes an MCP tool:
- Tool name: the dotted route key from the routes (e.g.
users.getUser) - Tool description: from the route's
summaryanddescription, falling back toMETHOD /path - Tool input: structured as
{ params, query, body }matching the route's schemas - Tool execution: invokes the corresponding handler directly and returns
{ status, body }
Routes with contentType: 'multipart/form-data' or 'application/x-www-form-urlencoded' are excluded by default since MCP tools receive JSON.
Handlers receive the same framework context they do over HTTP, passed through from the transport request, so a handler reading req or c keeps working when it is called as a tool.
Auth
Secured tools authenticate via the headers of the MCP transport request. When an assistant calls one, the endpoint runs the guards registered on server.api, exactly like the HTTP pipeline: each identity's credential is extracted from the headers, the guard verifies it, access gates apply, and the handler receives its typed identity context as usual. Installing the plugin on a secured API gives you a secured MCP endpoint.
Configure the MCP client to send the credential as a header on the connection. For example, with Claude Code:
claude mcp add --transport http my-api http://localhost:3000/mcp --header "Authorization: Bearer <token>"For an apiKey identity, send the header the identity names (e.g. x-workspace-token) instead.
When a guard denies the call, or an access gate fails, the tool call returns an error result (isError: true) carrying the status and detail, so the assistant sees exactly why it was refused:
{
"status": 401,
"body": {
"detail": "Unauthorized"
}
}Public routes (false in the auth map) run without guards, credentials or not.
Options
| Option | Default | Description |
|---|---|---|
path | '/mcp' | Path the endpoint is served from |
name | 'MCP Server' | Human-readable name shown to AI assistants |
version | '1.0.0' | Semantic version string |
routeFilter | excludes multipart and form-urlencoded | Predicate to filter which routes become tools |
Connecting clients
Claude Code
claude mcp add --transport http my-api http://localhost:3000/mcpVS Code
{
"mcp.servers": {
"my-api": {
"command": "npx",
"args": ["-y", "mcp-remote", "http://localhost:3000/mcp"]
}
}
}Cursor
{
"mcpServers": {
"my-api": {
"command": "npx",
"args": ["-y", "mcp-remote", "http://localhost:3000/mcp"]
}
}
}Tool input structure
Tool inputs use structured keys that mirror the kizuna handler args:
{
"params": { "id": "42" },
"query": { "page": 1, "limit": 25 },
"body": { "name": "Alice", "email": "alice@example.com" }
}Only the keys relevant to the route are present. A GET /users/:id route has params only. A POST /users with a body has body only.
Tool responses
Every tool returns a JSON text block with the HTTP status and parsed response body:
{
"status": 200,
"body": {
"id": "42",
"name": "Alice"
}
}Responses with status >= 400 set isError: true on the MCP result so the AI knows the call failed.
Standalone server
If you want to drive the transport yourself, for stdio or SSE, createMcpServer returns a raw McpServer:
import { createMcpServer } from '@ts-kizuna/mcp';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { api } from './api';
const server = createMcpServer(api, {
name: 'My API',
version: '1.0.0',
});
await server.connect(new StdioServerTransport());Reference
mcpPlugincreateMcpServer- Create a Plugin to write your own