## Motivation and Context The Rack 3 SPEC requires response header names to be lowercase. https://github.com/rack/rack/blob/main/SPEC.rdoc says in its "The Headers" section: > Header keys must not contain uppercase ASCII characters (A-Z). https://github.com/rack/rack/blob/main/SPEC.rdoc#the-headers However, `StreamableHTTPTransport` emitted capitalized names such as `Content-Type` and `Mcp-Session-Id`. Any `Rack::Lint`-wrapped stack, including the default development mode of rackup, rejects every response from the transport with a `LintError` 500. Lowercasing the names restores Rack 3 compliance. HTTP header names are case-insensitive on the wire, so HTTP clients are unaffected; only in-process consumers that read the raw Rack headers hash see the new keys. Lowercase is also the only representation valid across HTTP versions: RFC 9113 makes uppercase field names a protocol error on HTTP/2, so proxies and h2-capable servers already require it. The TypeScript SDK sets `headers['mcp-session-id']` and the Python SDK defines `MCP_SESSION_ID_HEADER = "mcp-session-id"`, so lowercase also matches what the reference SDKs emit; the MCP spec's `Mcp-Session-Id` casing is prose convention, not a wire requirement. Examples and documentation deliberately keep the spec's `Mcp-Session-Id` casing where it does not name a Rack response hash key: curl invocations and `examples/streamable_http_client.rb` set request headers, which the Rack response rule does not govern; the rack-cors `expose:` entry carries a header name as an `Access-Control-Expose-Headers` value, which browsers match case-insensitively; and README/docs prose follows the MCP specification's documentation convention. Only code reading the raw Rack response headers hash, a case-sensitive Hash lookup, had to switch to lowercase keys. AGENTS.md now records the lowercase rule under the code style guidelines, since the MCP spec's `Mcp-Session-Id` prose casing invites reintroducing capitalized names in future transport work. ## How Has This Been Tested? Added a regression test asserting that header names are lowercase in initialize, GET SSE, DELETE, and parse error responses. Existing tests that read the raw headers hash were updated to the new keys. Verified with rake test, rake rubocop, and rake conformance. ## Breaking Changes Code that reads the raw Rack response headers hash returned by `handle_request`, or by middleware around the mounted transport, with exact-case keys such as `"Mcp-Session-Id"` must switch to the lowercase keys. Clients speaking HTTP are unaffected because header names are case-insensitive on the wire.
5 KiB
5 KiB
AGENTS.md
Project overview
This is the official Ruby SDK for the Model Context Protocol (MCP), implementing both server and client functionality for JSON-RPC 2.0 based communication between LLM applications and context providers.
Dev environment setup
- Ruby 3.2.0+ required to run the full test suite, including all Sorbet-related features
- Run
bundle installto install dependencies - Dependencies:
json-schema>= 4.1 - Schema validation
Build and test commands
bundle install- Install dependenciesrake test- Run all testsrake rubocop- Run linterrake- Run tests and linting (default task)ruby -I lib -I test test/path/to/specific_test.rb- Run single test filegem build mcp.gemspec- Build the gem
Testing instructions
- Test files are in
test/directory with_test.rbsuffix - Run full test suite with
rake test - Run individual tests with
ruby -I lib -I test test/path/to/file_test.rb - Tests should pass before submitting PRs
Code style guidelines
- Follow RuboCop rules (run
rake rubocop) - Use frozen string literals
- Follow Ruby community conventions
- Keep dependencies minimal
- Use lowercase HTTP response header names (e.g.
mcp-session-id); the Rack 3 SPEC requires this, and the MCP spec'sMcp-Session-Idcasing is prose convention only
Commit message conventions
- Use conventional commit format when possible
- Include clear, descriptive commit messages
- Releases are triggered by updating version in
lib/mcp/version.rband merging to main
Release process
- Follow Keep a Changelog format in CHANGELOG.md
- Update CHANGELOG.md before cutting releases
- Use git history and PR merge commits to construct changelog entries
- Format entries as: "Terse description of the change (#nnn)"
- Keep entries in flat list format (no nesting)
- Git tags mark commits that cut new releases
- Exclude maintenance PRs that don't concern end users
- Check upstream remote for PRs if available
Architecture overview
Core Components
MCP::Server (lib/mcp/server.rb):
- Main server class handling JSON-RPC requests and holding shared configuration (tools, prompts, resources, handlers, capabilities)
- Implements MCP protocol methods: initialize, ping, tools/list, tools/call, prompts/list, prompts/get, resources/list, resources/read
- Supports custom method registration via
define_custom_method - Handles instrumentation, exception reporting, and notifications
- Uses JsonRpcHandler for request processing
MCP::ServerSession (lib/mcp/server_session.rb):
- Per-connection state: client info, logging level
- Created by the transport layer for each client connection
- Delegates request handling to the shared
Server
MCP::Client (lib/mcp/client.rb):
- Client interface for communicating with MCP servers
- Transport-agnostic design with pluggable transport layers
- Supports tool listing and invocation
Transport Layer:
MCP::Server::Transports::StdioTransport- Command-line stdio transportMCP::Server::Transports::StreamableHttpTransport- HTTP with streaming supportMCP::Client::HTTP- HTTP client transport (requires faraday gem)
Protocol Components:
MCP::Tool- Tool definition with input/output schemas and annotationsMCP::Prompt- Prompt templates with argument validationMCP::Resource- Resource registration and retrievalMCP::Configuration- Global configuration with exception reporting and instrumentation
Key Patterns
Three Ways to Define Components:
- Class inheritance (e.g.,
class MyTool < MCP::Tool) - Define methods (e.g.,
MCP::Tool.define(name: "my_tool") { ... }) - Server registration (e.g.,
server.define_tool(name: "my_tool") { ... })
Schema Validation:
- Tools support input_schema and output_schema for JSON Schema validation
- Protocol version 2025-03-26+ supports tool annotations (destructive_hint, idempotent_hint, etc.)
- Validation is configurable via
configuration.validate_tool_call_arguments
Session Architecture:
Serverholds shared configuration (tools, prompts, resources, handlers)ServerSessionholds per-connection state (client info, logging level)- Both
StdioTransportandStreamableHTTPTransportcreate aServerSessionper connection, making the session model transparent across transports - Session-scoped notifications (
notify_progress,notify_log_message) are sent only to the originating client viaServerSession - Server-wide notifications (
notify_tools_list_changed, etc.) broadcast to all sessions viaServer
Context Passing:
server_contexthash passed through tool/prompt calls for request-specific data- Methods can accept
server_context:keyword argument for accessing context
Integration patterns
- Rails/Rack apps: Mount
StreamableHTTPTransportas a Rack app (e.g.,mount transport => "/mcp") - Command-line tools: Use
StdioTransport.new(server).openfor CLI applications - HTTP services: Use
StreamableHTTPTransportfor web-based servers