ruby-sdk/examples/rails/README.md
Koichi ITO 16a6df340c Add Rails example app
## 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.
2026-07-09 01:37:58 +09:00

116 lines
4.5 KiB
Markdown

# 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](../../README.md).
The application provides:
- `add_tool` tool (`app/tools/add_tool.rb`) - adds two numbers, demonstrates `input_schema`
- `greeting_tool` tool (`app/tools/greeting_tool.rb`) - greets a name, demonstrates `annotations` and `server_context`
- `example://rails/readme` resource - a text resource served via `resources_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 `mcp` gem itself supports older Rubies)
- curl >= 7.82 (for the `--json` flag used below)
## Running
```console
$ 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.
1. Initialize a session and capture the session ID:
```console
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')
```
2. Complete the handshake (expect `202 Accepted`):
```console
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"}'
```
3. List and call tools:
```console
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"}}}'
```
4. List and read the resource:
```console
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"}}'
```
5. Optionally, open the standalone SSE stream for server-to-client messages
(in another terminal):
```console
curl -N http://localhost:9292/mcp \
-H "Accept: text/event-stream" \
-H "Mcp-Session-Id: $SESSION_ID"
```
6. End the session:
```console
curl -i -X DELETE http://localhost:9292/mcp -H "Mcp-Session-Id: $SESSION_ID"
```
## Testing with MCP Inspector
Start [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) with `npx @modelcontextprotocol/inspector`,
set Transport Type to "Streamable HTTP", and connect to `http://localhost:9292/mcp`.
## Notes
- `StreamableHTTPTransport` keeps 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 the `Mcp-Session-Id` header,
or pass `stateless: true` to the transport.
- The server is built once at boot in `config/routes.rb`, so code reloading is disabled
(`config.enable_reloading = false` in `config/application.rb`).
Restart the server after changing tool or resource code. Note that tool classes cannot be referenced from
`config/initializers` because 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: true` and `transport.handle_request(request)` inside a controller action.