module
ActionController::MCPServer
Overview
Exposes the application's annotated routes as an MCP server using the Streamable HTTP transport.
Controllers are presented as toolboxes and their routes as tools. To keep the model's context lean only these tools are listed, plus any root tools:
list_toolboxeslists the controllers and their descriptionsopen_toolbox(name)returns the definitions of the controller's toolscall_read_only(name, arguments)andcall_tool(name, arguments)run the tools
The server is stateless, there are no sessions, so requests can be served by any
instance of the application. Both the 2026-07-28 protocol and the earlier versions,
which start with initialize, are supported.
Controllers can also be served as their own MCP server (@[AC::MCP(endpoint: true)]),
and tools can render MCP Apps cards (@[AC::MCP(ui:)]), see ActionController::MCP.
Tool calls are dispatched in-process through the application router, so filters, authentication and error handlers apply exactly as they would for a regular request.
require "action-controller/mcp"
server = ActionController::Server.new
ActionController::MCPServer.mount(server, "/mcp")
server.run
Tool descriptions are extracted from the source code comments, so they need to be generated at build time and shipped alongside the binary:
ActionController::MCPServer.write_description("mcp.yml")
the file is lazily loaded from .description_path the first time it is needed.
Routes can be excluded using @[AC::MCP(hide: true)]
Extended Modules
Defined in:
action-controller/mcp.craction-controller/mcp/auth.cr
action-controller/mcp/description.cr
action-controller/mcp/icons.cr
action-controller/mcp/invoker.cr
action-controller/mcp/previewer.cr
action-controller/mcp/protocol.cr
action-controller/mcp/request_context.cr
action-controller/mcp/streamed.cr
action-controller/mcp/subscription.cr
action-controller/mcp/transport.cr
action-controller/mcp/ui.cr
action-controller/mcp/uri_template.cr
Constant Summary
-
LEGACY_VERSIONS =
{"2025-11-25", "2025-06-18", "2025-03-26"} -
protocol versions that start with
initialize, newest first -
MODERN_VERSIONS =
{"2026-07-28"} -
stateless protocol versions, where every request carries its version in
_meta -
PROTOCOL_VERSIONS =
MODERN_VERSIONS + LEGACY_VERSIONS -
supported protocol versions, newest first
-
TOOLBOX_INSTRUCTIONS =
"Tools are grouped into toolboxes. Call list_toolboxes to discover what is available and\nopen_toolbox for the definitions of the tools in a toolbox. Run them with the tool named\nin their proxy field, passing the tool name and its arguments: call_read_only for tools\nthat only read data, call_tool for the rest."
Class Method Summary
-
.allowed_origins : Array(String)
browser origins permitted to connect, in addition to same origin requests.
-
.allowed_origins=(allowed_origins : Array(String))
browser origins permitted to connect, in addition to same origin requests.
-
.auth_cache_ttl : Time::Span
how long a successful authentication check is cached for
-
.auth_cache_ttl=(auth_cache_ttl : Time::Span)
how long a successful authentication check is cached for
-
.auth_probe : String | Nil
a route used to authenticate MCP requests, i.e.
-
.auth_probe=(auth_probe : String | Nil)
a route used to authenticate MCP requests, i.e.
-
.authenticator : Proc(HTTP::Request, Bool) | Nil
authenticates MCP requests, returning
trueif the request is permitted. -
.authenticator=(authenticator : Proc(HTTP::Request, Bool) | Nil)
authenticates MCP requests, returning
trueif the request is permitted. -
.cache_ttl : Time::Span
how long clients can cache the server description and lists (
ttlMs), 2026-07-28 onwards -
.cache_ttl=(cache_ttl : Time::Span)
how long clients can cache the server description and lists (
ttlMs), 2026-07-28 onwards -
.description_path : String
location of the MCP description file, generated using
#write_description -
.description_path=(description_path : String)
location of the MCP description file, generated using
#write_description -
.excluded_response_headers : Array(String)
response headers left out of tool results, matched case-insensitively.
-
.excluded_response_headers=(excluded_response_headers : Array(String))
response headers left out of tool results, matched case-insensitively.
-
.forward_headers : Array(String)
request headers copied from the MCP request to the route being invoked, typically used for authentication
-
.forward_headers=(forward_headers : Array(String))
request headers copied from the MCP request to the route being invoked, typically used for authentication
-
.icons : Array(JSON::Any)
the global server's icons, see
#icon - .instructions : String
-
.instructions=(instructions : String | Nil)
usage instructions provided to the model,
niluses.toolbox_instructionsand an empty string provides none. -
.keep_alive : Time::Span
how often an SSE comment is sent on idle event streams, which keeps proxies from timing them out and detects clients that have disconnected
-
.keep_alive=(keep_alive : Time::Span)
how often an SSE comment is sent on idle event streams, which keeps proxies from timing them out and detects clients that have disconnected
-
.listen_timeout : Time::Span | Nil
how long a
subscriptions/listenstream stays open,nilkeeps it open until the client disconnects. -
.listen_timeout=(listen_timeout : Time::Span | Nil)
how long a
subscriptions/listenstream stays open,nilkeeps it open until the client disconnects. -
.max_subscriptions : Int32
the maximum number of resources a single
subscriptions/listencan subscribe to -
.max_subscriptions=(max_subscriptions : Int32)
the maximum number of resources a single
subscriptions/listencan subscribe to -
.resource_metadata : Proc(HTTP::Request, ResourceMetadata) | Nil
advertises the OAuth authorization server so MCP clients can obtain and refresh access tokens.
-
.resource_metadata=(resource_metadata : Proc(HTTP::Request, ResourceMetadata) | Nil)
advertises the OAuth authorization server so MCP clients can obtain and refresh access tokens.
-
.server_name : String
reported to clients during initialization
-
.server_name=(server_name : String)
reported to clients during initialization
-
.server_version : String
reported to clients during initialization
-
.server_version=(server_version : String)
reported to clients during initialization
-
.tool_proxy=(tool_proxy : Bool)
groups the tools into toolboxes, run by the
call_read_onlyandcall_toolmeta tools. -
.tool_proxy? : Bool
groups the tools into toolboxes, run by the
call_read_onlyandcall_toolmeta tools. -
.toolbox_instructions : String
explains how to use toolboxes, empty when
tool_proxyis disabled -
.ui_base : String | Nil
the folder of MCP Apps cards,
@[AC::MCP(ui: "bookings/card.html")]renders<ui_base>/bookings/card.html. -
.ui_base=(ui_base : String | Nil)
the folder of MCP Apps cards,
@[AC::MCP(ui: "bookings/card.html")]renders<ui_base>/bookings/card.html. -
.ui_meta : UIMeta | Nil
the default
_meta.uiof every card (CSP domains, border...), a card can override it with a.meta.jsonfile next to it, i.e. -
.ui_meta=(ui_meta : UIMeta | Nil)
the default
_meta.uiof every card (CSP domains, border...), a card can override it with a.meta.jsonfile next to it, i.e. -
.update_debounce : Time::Span
updates to a resource are sent at most once per period, clients read the resource for its current state so bursts of changes are coalesced
-
.update_debounce=(update_debounce : Time::Span)
updates to a resource are sent at most once per period, clients read the resource for its current state so bursts of changes are coalesced
-
.watchers : Hash(String, Watcher)
the watchers of resource templates with
@[AC::MCP(subscribe: true)]
Instance Method Summary
-
#auth_enabled? : Bool
authentication is optional and enabled when any of
.authenticator,.auth_probeor.resource_metadatais configured -
#description(composition : Composition = Composition.default) : Description
the tool descriptions, lazily loaded from
.description_path. -
#description=(description : Description | Nil)
replaces the current description,
nilwill reload it on next use -
#endpoint_paths(composition : Composition = Composition.default) : Array(String)
the path templates of the controller endpoints,
@[AC::MCP(endpoint: true)] -
#generate_description(docs : Bool = true, composition : Composition = Composition.default) : Description
generates the MCP description from the compiled routes.
-
#icon(src : String, **fields) : Nil
adds an icon for the global server,
srcfollows the@[AC::Icon]rules and every other argument is passed through as is -
#mount(router : Router, path : String = "/mcp", endpoints : Bool = true, composition : Composition = composition_for(router)) : Transport
mounts the MCP endpoint at the path provided.
-
#mount_card_previewer(router : Router, path : String = "/mcp/card/preview", mcp : String | Nil = nil, composition : Composition = composition_for(router)) : CardPreviewer
mounts a browser test harness for the MCP Apps cards, intended for development.
-
#mount_endpoints(router : Router, composition : Composition = composition_for(router)) : Array(Transport)
mounts a server for each controller annotated
@[AC::MCP(endpoint: true)] -
#mounted_path(router : Router) : String | Nil
the path of the global MCP server mounted on the router,
nilif there isn't one -
#watch(template : String, &block : Hash(String, String), HTTP::Request, Proc(Nil) -> Proc(Nil)) : Nil
watches the resources of a template for changes, so clients can subscribe to them using
subscriptions/listen. -
#write_description(path : String = description_path, composition : Composition = Composition.default) : Nil
generates the description, including source code comments, and saves it to a file
Class Method Detail
browser origins permitted to connect, in addition to same origin requests.
"*" permits any origin
browser origins permitted to connect, in addition to same origin requests.
"*" permits any origin
how long a successful authentication check is cached for
a route used to authenticate MCP requests, i.e. /api/v1/users/current.
the route is requested in-process with the forwarded headers, a successful (2xx) response indicates the request is authenticated
a route used to authenticate MCP requests, i.e. /api/v1/users/current.
the route is requested in-process with the forwarded headers, a successful (2xx) response indicates the request is authenticated
authenticates MCP requests, returning true if the request is permitted.
optional, see .auth_probe for a simpler alternative
authenticates MCP requests, returning true if the request is permitted.
optional, see .auth_probe for a simpler alternative
how long clients can cache the server description and lists (ttlMs), 2026-07-28 onwards
how long clients can cache the server description and lists (ttlMs), 2026-07-28 onwards
location of the MCP description file, generated using #write_description
location of the MCP description file, generated using #write_description
response headers left out of tool results, matched case-insensitively. An entry
ending in * matches by prefix. Everything else (Link, X-Total-Count,
Content-Range, Location, ETag, ...) is returned to the model
response headers left out of tool results, matched case-insensitively. An entry
ending in * matches by prefix. Everything else (Link, X-Total-Count,
Content-Range, Location, ETag, ...) is returned to the model
request headers copied from the MCP request to the route being invoked, typically used for authentication
request headers copied from the MCP request to the route being invoked, typically used for authentication
usage instructions provided to the model, nil uses .toolbox_instructions and an
empty string provides none. Include .toolbox_instructions when describing your domain
how often an SSE comment is sent on idle event streams, which keeps proxies from timing them out and detects clients that have disconnected
how often an SSE comment is sent on idle event streams, which keeps proxies from timing them out and detects clients that have disconnected
how long a subscriptions/listen stream stays open, nil keeps it open until the
client disconnects. The stream is then completed and clients listen again
how long a subscriptions/listen stream stays open, nil keeps it open until the
client disconnects. The stream is then completed and clients listen again
the maximum number of resources a single subscriptions/listen can subscribe to
the maximum number of resources a single subscriptions/listen can subscribe to
advertises the OAuth authorization server so MCP clients can obtain and refresh access tokens. The request is provided for multi-tenant deployments
advertises the OAuth authorization server so MCP clients can obtain and refresh access tokens. The request is provided for multi-tenant deployments
groups the tools into toolboxes, run by the call_read_only and call_tool meta
tools. When disabled every tool is listed
groups the tools into toolboxes, run by the call_read_only and call_tool meta
tools. When disabled every tool is listed
explains how to use toolboxes, empty when tool_proxy is disabled
the folder of MCP Apps cards, @[AC::MCP(ui: "bookings/card.html")] renders
<ui_base>/bookings/card.html. Cards aren't served when nil
the folder of MCP Apps cards, @[AC::MCP(ui: "bookings/card.html")] renders
<ui_base>/bookings/card.html. Cards aren't served when nil
the default _meta.ui of every card (CSP domains, border...), a card can override it
with a .meta.json file next to it, i.e. bookings/card.meta.json
the default _meta.ui of every card (CSP domains, border...), a card can override it
with a .meta.json file next to it, i.e. bookings/card.meta.json
updates to a resource are sent at most once per period, clients read the resource for its current state so bursts of changes are coalesced
updates to a resource are sent at most once per period, clients read the resource for its current state so bursts of changes are coalesced
the watchers of resource templates with @[AC::MCP(subscribe: true)]
Instance Method Detail
authentication is optional and enabled when any of .authenticator,
.auth_probe or .resource_metadata is configured
the tool descriptions, lazily loaded from .description_path.
if the file doesn't exist the description is generated from the compiled routes, however it will not include the documentation comments
replaces the current description, nil will reload it on next use
the path templates of the controller endpoints, @[AC::MCP(endpoint: true)]
generates the MCP description from the compiled routes.
docs: true extracts the source code comments using crystal docs,
which requires access to the source code
adds an icon for the global server, src follows the @[AC::Icon] rules and every
other argument is passed through as is
ActionController::MCPServer.icon "icons/logo.svg", sizes: ["any"]
ActionController::MCPServer.icon "https://example.com/logo-dark.png", sizes: ["48x48"], theme: "dark"
mounts the MCP endpoint at the path provided.
tool calls are dispatched via the router, typically ActionController::Server.
The OAuth protected resource metadata is served at /.well-known/oauth-protected-resource<path>
endpoints: true also mounts the controller endpoints, @[AC::MCP(endpoint: true)],
see #mount_endpoints
mounts a browser test harness for the MCP Apps cards, intended for development.
Mount the MCP server first, the previewer connects to the path it was mounted at.
mcp overrides it, i.e. when a proxy serves the application under a prefix
GET <path> lists the cards and the tools that render them, each tool has a page
(<path>/tools/<name>) with a form for its arguments that renders the card in a
mock MCP Apps host. The page is an MCP client of the global MCP server, so tool calls,
including those made by the card, use the same routes and authentication as any
other client.
The tool pages accept query params, for linking and automated tests:
- the tool arguments by name,
?id=42, the card renders once all the required arguments are provided viewport_size=desktop|tablet|mobileandtheme=light|darkheader.<Name>=<value>request headers, i.e.header.Authorization=Bearer%20...
The page's assets are only compiled in when this method is called, see the README for excluding it from release builds.
mounts a server for each controller annotated @[AC::MCP(endpoint: true)]
the path of the global MCP server mounted on the router, nil if there isn't one
watches the resources of a template for changes, so clients can subscribe to them
using subscriptions/listen. The template must match a route annotated
@[AC::MCP(resource: template, subscribe: true)].
A client can only subscribe to resources it can read: the route is requested before the watcher is called. The block is called once per subscription and returns a proc that stops watching, called when the client disconnects
ActionController::MCPServer.watch("app://widgets/{id}") do |variables, _request, changed|
subscription = Widget.on_change(variables["id"]) { changed.call }
-> { subscription.cancel }
end
generates the description, including source code comments, and saves it to a file