ServiceStack/src/ServiceStack.AI.Chat/MCP.md
The built-in MCP server exposes selected AI.Chat tools to external AI assistants such as OpenCode, Claude Code, Cursor, VS Code, and other clients supporting MCP Streamable HTTP. It allows those assistants to use the same ServiceStack tools and API Tools as the built-in AI.Chat UI without publishing a second integration service.
For ServiceStack API discovery and calling behavior, read API_TOOLS.md.
AI.Chat extensions register tools once in a shared ToolRegistry. The MCP extension projects an explicitly selected subset of those tools into MCP definitions:
inputSchema.outputSchema.This keeps the built-in chat UI and external assistants on the same executable tool implementations, authorization context, and schemas.
Nothing is exposed by default. Select tool groups and/or individual tools explicitly:
services.AddPlugin(new ChatFeature
{
Tools =
{
EnableApiTools = true,
},
ApiTools =
{
IncludeTags = ["CoffeeShop"],
},
Mcp =
{
ToolGroups = ["api_tools", "bookings"],
Tools = ["another_specific_tool"],
ServerName = "coffee-shop",
Instructions = "Use API Tools to inspect the current menu before placing an order.",
},
});
ToolGroups = ["all"] or Tools = ["all"] exposes the entire registered tool set and should be used only when that broad access is intentional. Selecting an unexposed tool name in tools/call cannot reach it.
The default endpoint is:
{scheme}://{host}/chat/mcp
The /chat prefix follows ChatFeature.RoutePrefix; changing the route prefix changes the MCP URL.
The server implements stateless MCP Streamable HTTP:
POST containing JSON-RPC 2.0.Mcp-Session-Id is issued or required.GET and DELETE on the MCP endpoint return HTTP 405.Supported MCP protocol versions are:
2025-06-182025-03-262024-11-05When the requested version is supported, the server returns it. Otherwise it responds with its latest version.
Supported methods are:
initializepingtools/listtools/callnotifications/* as no-response notificationsThe server advertises the tools capability with listChanged: false; tools are built at application startup and do not change during the process lifetime.
MCP routes use the same ChatFeature authentication gate as other protected AI.Chat routes.
When ChatFeature.RequireAuth is enabled, external clients should normally send a ServiceStack API key:
Authorization: Bearer ak-example-key
IdentityChatAuth resolves the API key onto the request. Tools execute as the API key's user and retain that user's roles and scopes. API Tools additionally enforce each API's authentication, API-key, role, permission, claim, and scope requirements during discovery and execution.
When RequireAuth = false, MCP calls use the unauthenticated/default AI.Chat identity. Use open access only when every exposed tool is safe for anonymous callers.
The exact configuration file shape is client-specific. A typical remote MCP entry is:
{
"type": "remote",
"url": "http://localhost:5000/chat/mcp",
"oauth": false,
"headers": {
"Authorization": "Bearer ak-example-key"
}
}
For OpenCode versions whose configuration maps server names directly under mcp:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"northwind-coffeeshop": {
"type": "remote",
"url": "http://localhost:5000/chat/mcp",
"enabled": true,
"oauth": false,
"headers": {
"Authorization": "Bearer {env:NORTHWIND_API_KEY}"
}
}
}
}
Prefer an environment variable or the client's secret store rather than committing an API key.
tools/list returns only tools selected by Mcp.ToolGroups and Mcp.Tools. Each MCP tool may contain:
{
"name": "api_search",
"description": "Search the APIs of the App...",
"inputSchema": {
"type": "object",
"properties": {}
},
"outputSchema": {
"type": "object",
"properties": {}
},
"annotations": {
"readOnlyHint": true
}
}
Safety annotations are mapped as follows:
| ServiceStack safety | MCP annotations |
|---|---|
ReadOnly | readOnlyHint: true |
Write | readOnlyHint: false, destructiveHint: false |
Destructive | readOnlyHint: false, destructiveHint: true |
Auto or unspecified custom tool | No annotation |
Annotations are hints to the client, not an authorization boundary. The server still controls which tools are exposed and which caller may execute them.
A client calls a selected tool with standard MCP parameters:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "api_call",
"arguments": {
"name": "GetCoffeeShopMenu",
"args": {}
}
}
}
Only properties declared in the registered tool's input schema are passed to its handler. Unknown top-level MCP tool arguments are discarded. API-specific arguments inside api_call.args are validated separately by API Tools.
The same tool handler used by AI.Chat executes with a ChatContext containing the authenticated username and HTTP request.
MCP always returns a content array. Text results are represented as:
{
"content": [
{
"type": "text",
"text": "{\"status\":\"success\"}"
}
],
"structuredContent": {
"status": "success"
}
}
When a tool's textual result is a JSON object, the server also supplies structuredContent. Tools with an outputSchema should return structured JSON objects so strict MCP clients can validate the result.
API Tools expose output schemas and return structured objects for successful search, describe, and call operations.
Tool result media is converted to MCP content:
MaxInlineResourceBytes are returned inline as base64 image or audio content.resource_link entries.Configure the limit when necessary:
Mcp =
{
MaxInlineResourceBytes = 8 * 1024 * 1024,
}
Avoid returning large media or datasets through a model context when a compact summary or resource link is sufficient.
AI.Chat can pause a thread and display its editable schema-generated approval form. A generic MCP client cannot display or complete that ServiceStack UI flow. ApprovalMode defines how the MCP boundary handles mutating operations (Write or Destructive safety, or APIs marked RequiresApproval = true).
Mcp =
{
ToolGroups = ["api_tools"],
ApprovalMode = McpApprovalMode.ConfirmationToken, // Default
ConfirmationTokenExpiry = TimeSpan.FromMinutes(5),
}
This is the default mode. When a mutating operation is called without a token:
requires_confirmation status containing a summary, argument parameters, and a signed, short-lived confirmationToken.api_call with the confirmationToken.Read-only operations (IGet, QueryBase, QueryDb<>, QueryData<>) execute immediately without requiring a token.
Two-Phase Confirmation is a security primitive; its cross-cluster correctness depends on shared state:
Mcp.SigningSecret (or HostConfig.AdminAuthSecret) with a value of at least 32 bytes, sourced from a secret store / env var. If neither is set, McpExtension generates an ephemeral per-process secret and logs a warning: tokens will not survive a restart and are rejected across load-balanced instances.ICacheClient (Redis via IRedisClientsManager, OrmLiteCacheClient on a shared DB, etc.) for the single-use replay set. Otherwise McpExtension falls back to an in-process ConcurrentDictionary and logs a warning: replay protection is per-process only and silently degrades in a farm. The used-token set uses TTL keys under urn:mcp:used:{jti} so it self-cleans.ApprovalHandler must be deterministic. The handler runs on both Phase 1 (mint) and Phase 2 (verify); its returned Arguments are hashed both times and compared. Any per-call mutation (attaching request IP, timestamps, correlation IDs, etc.) will break verification with "Arguments have been modified since confirmation was issued". Side-effects such as audit logs also run twice per successful mutation.Mcp =
{
ToolGroups = ["api_tools"],
ApprovalMode = McpApprovalMode.Reject,
}
Before executing a tool, MCP runs its approval preflight. If the tool would require interactive approval, execution is refused with an approval-required error. Use this for strictly read-only exposure.
Mcp =
{
ToolGroups = ["api_tools"],
ApprovalMode = McpApprovalMode.DelegateToClient,
}
MCP executes the tool immediately on the first call. The client is expected to use standard MCP safety annotations (readOnlyHint: false, destructiveHint: true) and its own native confirmation dialog to ask the user before mutating calls.
Use this mode only when:
Expose the api_tools group to give an external assistant:
api_searchapi_describeapi_callRecommended assistant behavior remains search, describe, resolve prerequisites, preview, then call. For example:
See API_TOOLS.md for the complete contracts and agent guidance.
Any registered ChatTool can be exposed by group or name. Custom extensions can register a handler, output schema, safety, and optional approval preflight:
ctx.RegisterTool(
definition,
handler,
group: "reports",
approvalHandler: null,
outputSchema: reportOutputSchema,
safety: ToolSafety.ReadOnly);
ServiceStack Commands registered through ctx.RegisterTool<TCommand>() can also be selected for MCP. Their Request DTO supplies the input schema and execution uses the same Commands infrastructure.
The API key is missing, invalid, or does not satisfy ChatFeature.RequiredRole. Confirm the Authorization: Bearer header and restart the client after changing configuration.
The client attempted GET SSE or session deletion. Configure it for MCP Streamable HTTP using JSON-RPC POST, not the older SSE transport.
The requested tool was not selected by ToolGroups or Tools, or its extension is disabled. Check the application's MCP configuration and restart it; the list is fixed at startup.
The API may not be opted in, may be excluded, or may be inaccessible to the API-key user. Use api_search rather than guessing its name.
The server has ApprovalMode = McpApprovalMode.Reject. Switch to ConfirmationToken (the default) to expose a two-phase approval flow to the assistant, or DelegateToClient when the MCP client's own confirmation dialog is trusted.
The Phase 2 argument hash does not match the token. Common causes:
Canonicalize doesn't handle (report this).ApprovalHandler is non-deterministic — it mutates Arguments differently on each call (see Production deployment).A strict client received text that was not a JSON object from a tool advertising outputSchema. This can happen when an execution error is returned as plain text. Inspect server logs and the textual tool result; do not assume the operation succeeded.
Read/search/preview tools may work while the final write is rejected by approval policy, caller authorization, DTO validation, or service business rules. Treat each stage independently and preserve the actual error.
RejectToolsRequiringApproval = true unless client-side confirmation is intentional.Destructive, even when implemented with POST.An MCP assistant is another API client. It should receive no more authority than the user it represents, and server-side authorization and validation remain the final enforcement boundary.