## Motivation and Context The README.md documents the "Rails (mount)" integration pattern, but the repository had no runnable Rails example. This adds a minimal Rails application under examples/rails that mounts StreamableHTTPTransport at /mcp and demonstrates class-based tools and a resource with a read handler. The MCP server is built in config/routes.rb rather than in an initializer because Zeitwerk sets up autoloading after initializers run, so the tool classes in app/tools cannot be referenced there. Code reloading is disabled because the transport holds the server built at boot. The app is booted with puma instead of rackup because rackup inserts `Rack::Lint` in its development default, which rejects the capitalized response header names that the transport currently emits. Closes #44. ## How Has This Been Tested? The curl walkthrough in examples/rails/README.md has been verified end to end against the running app: initialize and the initialized notification, tools/list, tools/call for both tools, resources/list, resources/read, the standalone SSE stream, and session termination via DELETE. ## Breaking Changes None. This only adds an example application and documentation links.
4.5 KiB
4.5 KiB
Rails Example MCP Server
A minimal Rails application that serves an MCP server over the Streamable HTTP transport, following the "Rails (mount)" pattern from the top-level README.
The application provides:
add_tooltool (app/tools/add_tool.rb) - adds two numbers, demonstratesinput_schemagreeting_tooltool (app/tools/greeting_tool.rb) - greets a name, demonstratesannotationsandserver_contextexample://rails/readmeresource - a text resource served viaresources_read_handler
The MCP server and transport are built once at boot in config/routes.rb, where the transport is mounted at /mcp.
Requirements
- Ruby >= 3.2 (required by Rails 8; the
mcpgem itself supports older Rubies) - curl >= 7.82 (for the
--jsonflag used below)
Running
$ cd examples/rails
$ bundle install
$ bundle exec puma --port 9292
The MCP endpoint is now available at http://localhost:9292/mcp.
Testing with cURL
POST requests must include an Accept header that allows both application/json and text/event-stream,
per the MCP Streamable HTTP transport spec. Responses arrive as SSE data: lines.
- Initialize a session and capture the session ID:
SESSION_ID=$(curl -s -D - -o /dev/null http://localhost:9292/mcp \
-H "Accept: application/json, text/event-stream" \
--json '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}' \
| grep -i "^mcp-session-id:" | cut -d' ' -f2 | tr -d '\r')
- Complete the handshake (expect
202 Accepted):
curl -i http://localhost:9292/mcp \
-H "Accept: application/json, text/event-stream" \
-H "Mcp-Session-Id: $SESSION_ID" \
--json '{"jsonrpc":"2.0","method":"notifications/initialized"}'
- List and call tools:
curl http://localhost:9292/mcp \
-H "Accept: application/json, text/event-stream" \
-H "Mcp-Session-Id: $SESSION_ID" \
--json '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
curl http://localhost:9292/mcp \
-H "Accept: application/json, text/event-stream" \
-H "Mcp-Session-Id: $SESSION_ID" \
--json '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"add_tool","arguments":{"a":5,"b":3}}}'
curl http://localhost:9292/mcp \
-H "Accept: application/json, text/event-stream" \
-H "Mcp-Session-Id: $SESSION_ID" \
--json '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"greeting_tool","arguments":{"name":"Rails"}}}'
- List and read the resource:
curl http://localhost:9292/mcp \
-H "Accept: application/json, text/event-stream" \
-H "Mcp-Session-Id: $SESSION_ID" \
--json '{"jsonrpc":"2.0","id":5,"method":"resources/list"}'
curl http://localhost:9292/mcp \
-H "Accept: application/json, text/event-stream" \
-H "Mcp-Session-Id: $SESSION_ID" \
--json '{"jsonrpc":"2.0","id":6,"method":"resources/read","params":{"uri":"example://rails/readme"}}'
- Optionally, open the standalone SSE stream for server-to-client messages (in another terminal):
curl -N http://localhost:9292/mcp \
-H "Accept: text/event-stream" \
-H "Mcp-Session-Id: $SESSION_ID"
- End the session:
curl -i -X DELETE http://localhost:9292/mcp -H "Mcp-Session-Id: $SESSION_ID"
Testing with MCP Inspector
Start MCP Inspector with npx @modelcontextprotocol/inspector,
set Transport Type to "Streamable HTTP", and connect to http://localhost:9292/mcp.
Notes
StreamableHTTPTransportkeeps session and SSE state in memory, so it must run in a single process. Puma runs in single mode (workers 0) by default; do not enable clustered mode for this app. When running multiple instances behind a load balancer, use sticky sessions keyed on theMcp-Session-Idheader, or passstateless: trueto the transport.- The server is built once at boot in
config/routes.rb, so code reloading is disabled (config.enable_reloading = falseinconfig/application.rb). Restart the server after changing tool or resource code. Note that tool classes cannot be referenced fromconfig/initializersbecause Zeitwerk has not set up autoloading at that point; routes load late enough that they can. - For a per-request server with request-specific tools or context, see the "Rails (controller)" section in the top-level README,
which uses
stateless: trueandtransport.handle_request(request)inside a controller action.