ruby-sdk/examples/rails
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Koichi ITO bfc6e551a3 Update examples for the 2026-07-28 era
## Motivation and Context

The runnable examples drifted from the SDK they ship with:

- POST requests whose `Accept` header does not cover both `application/json` and `text/event-stream` are answered 406,
  so the cURL walkthroughs  no longer worked as printed.
- `MCP::Client#connect` now defaults to `mode: :auto`, which adopts the sessionless modern lifecycle (2026-07-28)
  against a dual-era server; the Streamable HTTP client example requires a session and broke against its own server.
- POST responses on an established session arrive as an SSE stream, which the hand-rolled HTTP client did not parse.
- `notification_tool` claimed to send SSE notifications but only returned a `Tool::Response`.
- `http_client.rb` read a resource URI the server never registered and skipped `notifications/initialized`.

Handshake examples now offer the latest handshake protocol version (`2025-11-25`) instead of `2024-11-05`,
the Streamable HTTP client pins `mode: :legacy` for its session-based demo, `notification_tool` reports
real `notifications/progress` events requested through `_meta.progressToken`, and the interactive client streams
those events from the POST response so they are visible on screen.

The modern lifecycle itself had no runnable example, so a new server and client pair (`modern_http_server.rb`,
`modern_http_client.rb`) shows the sessionless flow end to end: `server/discover` capability discovery,
the per-request `_meta` envelope with the `Mcp-Method` and `Mcp-Name` headers (stamped by the SDK client automatically),
`resultType` stamping, SEP-2549 cache hints, a SEP-2322 multi round-trip `deploy` tool resumed automatically by
the client's elicitation handler, and the removal of legacy-only methods such as `ping`. The same server keeps serving
the legacy `initialize` flow, routed per request by the `MCP-Protocol-Version` header. `examples/README.md` documents
the pair and adds a cURL walkthrough covering the modern headers, the envelope, and the multi round-trip exchange.

## How Has This Been Tested?

- `ruby examples/stdio_client.rb` against `stdio_server.rb`
- `ruby examples/http_client.rb` against `http_server.rb`
- `ruby examples/streamable_http_client.rb` against `streamable_http_server.rb`, including the progress stream
- `ruby examples/modern_http_client.rb` against `modern_http_server.rb` (discover, modern adoption, cache hints,
  `resultType`, the multi round-trip resume, `ping` rejection)
- The cURL walkthroughs in the banners and `examples/README.md`, including the `requestState` echo and a legacy `initialize`
  against the modern example server

## Breaking Changes

None.
2026-08-28 12:03:52 +09:00
..
app/tools Add Rails example app 2026-07-09 01:37:58 +09:00
config Add Rails example app 2026-07-09 01:37:58 +09:00
.gitignore Add Rails example app 2026-07-09 01:37:58 +09:00
config.ru Add Rails example app 2026-07-09 01:37:58 +09:00
Gemfile Add Rails example app 2026-07-09 01:37:58 +09:00
README.md Update examples for the 2026-07-28 era 2026-08-28 12:03:52 +09:00

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_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

$ 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:
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-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}' \
  | grep -i "^mcp-session-id:" | cut -d' ' -f2 | tr -d '\r')
  1. 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"}'
  1. 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"}}}'
  1. 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"}}'
  1. 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"
  1. 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

  • 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.