MCP
Install the MCP plugin so AI assistants can call your API as tools, on any adapter.
@kizunajs/mcp adds an MCP (Model Context Protocol) endpoint to your API. Every route that declares tool is one an AI assistant can discover and call.
Say which routes a model may call
A route publishes itself. tool: true takes the route's summary as what a model reads before calling:
See Tools for the rest of what tool takes.
The endpoint answers at /mcp by default. On Next.js it is served by the catch-all route file that already serves your routes, so with basePath: '/api' it lands at /api/mcp.
Which routes publish
Each route decides, so the plugin takes no list. A route with no tool is an HTTP endpoint and nothing more.
Two shapes never publish, even when they say tool:
- A route whose
contentTypeismultipart/form-dataorapplication/x-www-form-urlencoded, because tool input is JSON. - A route whose response streams, because a tool result is one value.
A hidden route can't say tool at all: the two together are a type error.
What to leave unpublished
Every published tool's name, description, and input schema sits in the model's context on every turn, called or not, and the more tools there are the more often the model picks the wrong one. So publish deliberately:
- What an assistant cannot use. File uploads, CSV exports, anything returning bytes.
- What exists for your own infrastructure. Health checks, readiness probes, webhook receivers.
- What you do not want reached by inference. Deleting a workspace is a person's decision, guard or no guard. If it does publish, give it
needsApproval.
What each tool looks like
| Part | Where it comes from |
|---|---|
| Name | The route key in snake_case, so users.getUser becomes users_get_user |
| Title | tool.title |
| Description | tool.description or the route's summary, then its description, its method and path, and what it requires |
| Input | { params, query, body }, holding only the keys the route declares |
| Output | The success response's schema, so an assistant knows the shape before it calls |
| Annotations | The method's HTTP semantics, per RFC 9110, and anything tool overrides |
The specification allows the dot, but Claude Code and VS Code rewrite it before the name reaches the model, so publishing the rewritten form is what keeps your docs and the model's tool name the same. Names are capped at 128 characters. A key over that throws at startup, as do two keys that converge on one name.
The annotations tell a client how careful to be with a tool before calling it:
| Method | Hints set |
|---|---|
GET, HEAD, OPTIONS | readOnlyHint, idempotentHint |
PUT | idempotentHint |
DELETE | idempotentHint, destructiveHint |
POST, PATCH | none |
An absent hint is not a neutral one. MCP defaults destructiveHint and openWorldHint to true and readOnlyHint to false, so a client already treats POST and PATCH as destructive calls against an open world. The hints worth setting are the ones that make a tool safer than that, which is why GET gets readOnlyHint and POST gets nothing. Set one on the route's tool to say what the method cannot.
Handlers receive the same framework context they do over HTTP, passed through from the transport request, so one reading req or c keeps working as a tool.
What the model reads
From each route
A route's summary and description are what the tool carries:
The tool that reaches the model carries both, followed by the request line and the identities the route requires:
These are the same two fields that become operation.summary and operation.description in your OpenAPI document, and the doc comment on your generated Swift and Kotlin methods, which uses the summary and falls back to the description. Write them once for every reader. OpenAPI wants a short summary and a verbose account of the behaviour, which is what a model wants too, so naming the limit, the ordering, and the matching rule costs nothing and serves all three.
When the wording a model needs differs from the wording your API reference needs, tool.description overrides the summary for the model alone:
From the whole server
instructions is a single string clients put in front of the model before it picks any tool. Kizuna builds one from your groups, so the groups in your API arrive with their titles and descriptions, and your own text is appended to it:
Use it for what belongs to no single route: the order operations happen in, the conventions every route shares, which tool to reach for when two look alike.
Authentication
Secured tools authenticate via the headers of the MCP transport request. The endpoint runs the guards that sit on your identities, exactly like the HTTP pipeline: the credential is extracted from the headers, the guard verifies it, the route's auth is checked, and the handler receives its typed identity context.
A route's identities appear in its tool description, along with the roles and permissions that go with them, so an assistant can tell it will be refused without spending a call to find out:
Configure the MCP client to send the credential as a header on the connection. For example, with Claude Code:
For an apiKey identity, send the header the identity names (e.g. x-workspace-token) instead.
When a guard denies the call, or the caller's role is not one the route accepts, the tool call returns an error result (isError: true) carrying the status and detail, so the assistant sees exactly why it was refused:
Routes with auth: false run without guards, credentials or not.
Asking before it runs
A route with needsApproval asks the person before the handler is reached, over MCP's elicitation:
The first call answers with the elicitation rather than a result, asking Allow "Delete the workspace, owner only"? from the route's summary, or its title when it has one. Approving runs the handler. Declining leaves it uncalled and answers isError: true.
OAuth
Point oauth at your oauth2 identity and the endpoint serves the discovery and challenges the MCP authorization specification asks of an OAuth 2.1 resource server. MCP clients log in through your authorization server (better-auth, Auth0, Keycloak) on their own.
resource is the canonical URI of the endpoint as clients reach it, and the audience your guard checks tokens against. scheme names the identity whose guard verifies the token on every request. The identity's issuer and flow scopes become the RFC 9728 metadata document, served at /.well-known/oauth-protected-resource/mcp.
With oauth configured, denials from that identity move to the HTTP level, where the specification puts them:
- No token, or a bad one, answers
401withWWW-Authenticate: Bearer resource_metadata="...". - A token that lacks a permission its role holds answers
403witherror="insufficient_scope"and the permissions it is missing. - A role that lacks a permission the route requires answers a plain
403.
The guard runs once per request, its context reaches handlers under auth.<scheme> as always, and every other identity keeps the in-band tool errors above.
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 |
instructions | built from your groups | Appended to the generated overview |
oauth | off | Serve the endpoint as an OAuth 2.1 resource server (OAuth) |
Connecting clients
Claude Code
VS Code
Cursor
Tool input structure
Tool inputs use structured keys that mirror the Kizuna handler args:
Only the keys the route declares are present.
Tool responses
Every tool returns a JSON text block with the HTTP status and parsed response body:
The same body is attached as structuredContent, matching the output schema the tool advertised, so a client can read the response as data rather than parsing the text.
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:
It takes one option the plugin does not: onlyReadOnly keeps only the methods RFC 9110 calls safe, so nothing the assistant reaches can change data.
Reference
mcpPlugincreateMcpServer- Tools for what a route's
tooldeclares - Create a Plugin to write your own