MCP configuration reference#
All options are class properties on ActionController::MCPServer, usually set in
src/config.cr:
ActionController::MCPServer.tap do |mcp|
mcp.server_name = "my-app"
mcp.server_version = "1.0.0"
# describe your domain, then how to use toolboxes
mcp.instructions = "Manages meeting rooms and bookings.\n\n#{mcp.toolbox_instructions}"
end
Server#
| Option | Default | Purpose |
|---|---|---|
server_name |
"action-controller" |
the server name reported to clients |
server_version |
"1.0.0" |
the server version reported to clients |
instructions |
nil (toolbox_instructions) |
text given to the model when it connects. Describe your domain here and append toolbox_instructions. "" sends none |
ui_base |
nil |
the folder of UI cards, ui:// paths resolve against it |
ui_meta |
nil |
the default card settings (UIMeta: CSP domains, permissions, border) |
tool_proxy |
true |
groups tools into toolboxes, run with the call_read_only and call_tool meta tools. false lists every tool instead |
description_path |
"mcp.yml" |
the generated tool descriptions, relative to the working directory |
cache_ttl |
5.minutes |
how long clients may cache the server description and the tool, prompt and resource lists (ttlMs), and resource reads without their own cache_ttl: |
keep_alive |
15.seconds |
how often an SSE comment is sent on listen streams and long running requests |
listen_timeout |
nil |
ends subscription streams after this long. nil never does |
update_debounce |
250.milliseconds |
coalesces bursts of resource changes into one notification |
max_subscriptions |
100 |
the most resources one subscriptions/listen can watch |
allowed_origins |
[] |
browser origins allowed in addition to same-origin. "*" allows any |
forward_headers |
["Authorization", "Cookie", "X-API-Key"] |
request headers copied onto tool calls and prompts |
excluded_response_headers |
noise, credentials, transport and browser policy headers | response headers left out of tool results, matched case-insensitively. A trailing * matches a prefix |
Authentication#
Authentication is optional and off by default. It's enabled when auth_probe,
authenticator or resource_metadata is set. See Authentication.
| Option | Default | Purpose |
|---|---|---|
auth_probe |
nil |
a route requested in-process with the forwarded headers. A 2xx response means authenticated |
authenticator |
nil |
Proc(HTTP::Request, Bool), a custom check used instead of the probe |
auth_cache_ttl |
1.minute |
how long a successful check is cached for each credential |
resource_metadata |
nil |
Proc(HTTP::Request, ResourceMetadata), which advertises your OAuth authorization server |
Methods#
| Method | Purpose |
|---|---|
mount(router, path = "/mcp", endpoints = true) |
registers the endpoint (and the protected resource metadata), plus the controller endpoints |
mount_endpoints(router) |
registers just the controller endpoints |
endpoint_paths |
the path templates of the controller endpoints |
icon(src, **fields) |
adds an icon for the global server, see icons |
watch(template, &block) |
registers the watcher for a subscribable resource template, see subscriptions |
toolbox_instructions |
the toolbox usage instructions, to append to your own instructions. Empty when tool_proxy is false |
write_description(path) |
generates mcp.yml from the code. Needs the source code |
generate_description |
the same, returning the Description |
description= |
replaces the loaded description (nil reloads it), useful in specs |
Transport details#
- Transport: Streamable HTTP, for protocol versions
2026-07-28,2025-11-25,2025-06-18and2025-03-26. - Stateless: the server keeps nothing between requests, so there are no MCP sessions. Run as many instances as you like behind a load balancer, no sticky sessions or shared storage needed.
POST: every request is aPOST, answered withapplication/json.GETandDELETErespond405 Method Not Allowed.subscriptions/listenand long running requests from clients that accepttext/event-streamare answered with an event stream.2026-07-28requests: carry their protocol version and client capabilities in_meta, and theMCP-Protocol-Version,Mcp-MethodandMcp-Nameheaders must match the request body, otherwise the response is400 Bad Request. Clients describe the server withserver/discover. Results includeresultTypeand the server info in_meta, and lists includettlMs(fromcache_ttl) andcacheScope(privatewhen authentication is enabled, as results depend on the user, otherwisepublic).- Earlier versions: start with
initialize. NoMcp-Session-Idis issued, and one sent by a client is ignored. Requests without anMCP-Protocol-Versionheader are treated as2025-03-26. - Notifications: the tool, prompt and resource lists never change, so there's
nothing to notify about them.
subscriptions/listenstreamsnotifications/resources/updatedfor subscribable resources (2026-07-28clients), andnotifications/progressis sent for long running requests that include aprogressToken. The deprecatednotifications/message(logging) isn't supported. - Authentication: when enabled, every request is authenticated.
Originheaders: must be same-origin or listed inallowed_origins, which protects against DNS rebinding.- Unhandled exceptions: are logged and returned as a generic
500tool error.Server.beforehandlers (such asErrorHandler) don't run for in-process tool calls.
Tool results#
A tool call returns the route's response as {status, headers, body}. The same JSON is
the text content (which most clients give the model) and structuredContent:
{
"status": 200,
"headers": {"X-Total-Count": "120", "Link": "</api/users?page=2>; rel=\"next\""},
"body": [{"id": 1, "name": "Steve"}]
}
status: the HTTP status code.headers: the response headers, minus those matchingexcluded_response_headers. Left out when there are none. This is how the model sees pagination (Link,X-Total-Count,Content-Range),Locationafter a create,Retry-AfterandETag.body: the parsed JSON, or the text of any other response. Left out when empty.- Errors: a status of 400 or above sets
isError, and the error body is returned in the same shape. A401when authentication is enabled challenges the client to sign in again instead. - Images and audio: returned as an
imageoraudiocontent block, followed by the envelope without a body.
By default these headers are excluded:
| Kind | Headers |
|---|---|
| Noise | Date, Content-Length, X-Request-ID, Content-Type, Server, Vary, Cache-Control, Pragma, Expires, Alt-Svc |
| Credentials | Set-Cookie, Cookie, Authorization, WWW-Authenticate, Proxy-* |
| Transport | Connection, Keep-Alive, Transfer-Encoding, Content-Encoding, Trailer, Upgrade |
| Browser policy | Strict-Transport-Security, Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Access-Control-* |
Add to the list, rather than replacing it, so credentials stay excluded:
ActionController::MCPServer.excluded_response_headers += ["X-Runtime", "X-Internal-*"]