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 three tools are listed by default:

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.cr
action-controller/mcp/auth.cr
action-controller/mcp/description.cr
action-controller/mcp/invoker.cr
action-controller/mcp/protocol.cr
action-controller/mcp/session.cr
action-controller/mcp/transport.cr

Constant Summary

DEFAULT_INSTRUCTIONS = "Tools are grouped into toolboxes. Call list_toolboxes to discover what is available,\nopen_toolbox to load the tools in a toolbox and close_toolbox once you no longer need them."
PROTOCOL_VERSIONS = {"2025-11-25", "2025-06-18", "2025-03-26"}

supported protocol versions, newest first

PROXY_INSTRUCTIONS = "#{DEFAULT_INSTRUCTIONS}\nopen_toolbox returns the definitions of the tools it loads. If they don't appear in your\navailable tools, run them with the tool named in their proxy field, passing the tool name\nand its arguments: call_read_only for tools that only read data, call_tool for the rest."

Class Method Summary

Instance Method Summary

Class Method Detail

def self.allowed_origins : Array(String) #

browser origins permitted to connect, in addition to same origin requests. "*" permits any origin


[View source]
def self.allowed_origins=(allowed_origins : Array(String)) #

browser origins permitted to connect, in addition to same origin requests. "*" permits any origin


[View source]
def self.auth_cache_ttl : Time::Span #

how long a successful authentication check is cached for


[View source]
def self.auth_cache_ttl=(auth_cache_ttl : Time::Span) #

how long a successful authentication check is cached for


[View source]
def self.auth_probe : String | Nil #

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


[View source]
def self.auth_probe=(auth_probe : String | Nil) #

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


[View source]
def self.authenticator : Proc(HTTP::Request, Bool) | Nil #

authenticates MCP requests, returning true if the request is permitted.

optional, see .auth_probe for a simpler alternative


[View source]
def self.authenticator=(authenticator : Proc(HTTP::Request, Bool) | Nil) #

authenticates MCP requests, returning true if the request is permitted.

optional, see .auth_probe for a simpler alternative


[View source]
def self.description_path : String #

location of the MCP description file, generated using #write_description


[View source]
def self.description_path=(description_path : String) #

location of the MCP description file, generated using #write_description


[View source]
def self.excluded_response_headers : Array(String) #

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


[View source]
def self.excluded_response_headers=(excluded_response_headers : Array(String)) #

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


[View source]
def self.forward_headers : Array(String) #

request headers copied from the MCP request to the route being invoked, typically used for authentication


[View source]
def self.forward_headers=(forward_headers : Array(String)) #

request headers copied from the MCP request to the route being invoked, typically used for authentication


[View source]
def self.instructions : String #

[View source]
def self.instructions=(instructions : String | Nil) #

usage instructions provided to the model, nil uses .toolbox_instructions and an empty string provides none. Include .toolbox_instructions when describing your domain


[View source]
def self.resource_metadata : Proc(HTTP::Request, ResourceMetadata) | Nil #

advertises the OAuth authorization server so MCP clients can obtain and refresh access tokens. The request is provided for multi-tenant deployments


[View source]
def self.resource_metadata=(resource_metadata : Proc(HTTP::Request, ResourceMetadata) | Nil) #

advertises the OAuth authorization server so MCP clients can obtain and refresh access tokens. The request is provided for multi-tenant deployments


[View source]
def self.server_name : String #

reported to clients during initialization


[View source]
def self.server_name=(server_name : String) #

reported to clients during initialization


[View source]
def self.server_version : String #

reported to clients during initialization


[View source]
def self.server_version=(server_version : String) #

reported to clients during initialization


[View source]
def self.session_timeout : Time::Span #

sessions inactive for this period are discarded


[View source]
def self.session_timeout=(session_timeout : Time::Span) #

sessions inactive for this period are discarded


[View source]
def self.tool_proxy=(tool_proxy : Bool) #

adds a call_tool meta tool that runs the tools in open toolboxes, for clients that don't refresh their tools when notified with tools/list_changed


[View source]
def self.tool_proxy? : Bool #

adds a call_tool meta tool that runs the tools in open toolboxes, for clients that don't refresh their tools when notified with tools/list_changed


[View source]
def self.toolbox_instructions : String #

explains how to use toolboxes, taking tool_proxy into account


[View source]

Instance Method Detail

def auth_enabled? : Bool #

authentication is optional and enabled when any of .authenticator, .auth_probe or .resource_metadata is configured


[View source]
def description : Description #

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


[View source]
def description=(description : Description | Nil) #

replaces the current description, nil will reload it on next use


[View source]
def endpoint_paths : Array(String) #

the path templates of the controller endpoints, @[AC::MCP(endpoint: true)]


[View source]
def generate_description(docs : Bool = true) : Description #

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


[View source]
def mount(router : Router, path : String = "/mcp", endpoints : Bool = true) : Transport #

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


[View source]
def mount_endpoints(router : Router) : Array(Transport) #

mounts a server for each controller annotated @[AC::MCP(endpoint: true)]


[View source]
def write_description(path : String = description_path) : Nil #

generates the description, including source code comments, and saves it to a file


[View source]