- Ruby 99.8%
- Shell 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
## Motivation and Context `ClientCredentialsProvider` builds a fixed `client_credentials` token request: `grant_type`, `scope`, `resource`, and the client authentication that `Flow#post_to_token_endpoint` adds. An authorization server that requires further parameters, such as Auth0's `audience`, cannot be used through `oauth:` at all, and giving up `oauth:` also gives up the bearer header and the 401-driven retry that the transport provides. RFC 6749 Section 8.2 allows extension parameters on the token request, and the TypeScript SDK lets a provider shape its token request through `prepareTokenRequest`. `ClientCredentialsProvider.new` now takes `token_request_params:`, a Hash of String keys and values added to every token request the provider makes, and `Flow#post_to_token_endpoint` reads the same name by duck typing, so a `Provider` or `CrossAppAccessProvider` subclass that defines the method gets the same treatment on its authorization code exchange, refresh, or `jwt-bearer` request. `Provider.new` and `CrossAppAccessProvider.new` take the same keyword, held by `StorageBackedProvider` like `authorization_request_validator`, so every bundled provider refuses a bad value before anything is stored. The parameters go underneath the flow's own, so the SDK's values always win, and a key the SDK sets itself (`Flow::RESERVED_TOKEN_REQUEST_PARAMS`) is refused rather than silently overridden: with `ArgumentError` from the constructor, before any `client_information` is written, and again with `ArgumentError` from the flow, before the token request is sent. Only Strings are accepted because `URI.encode_www_form` encodes other scalars and Arrays in ways the caller did not write, and the provider keeps a frozen copy so a later change to the caller's Hash cannot alter what is sent. A Hash comparing keys by identity is refused too, since two equal keys are two entries there, sent twice by a provider method or silently collapsed into one by the copy. The flow raises `ArgumentError` rather than `AuthorizationError` because `MCP::Client::HTTP` treats a failed refresh as a reason to run the interactive flow, which would then fail the same way after the user signed in. Both refusals raise `Flow::InvalidTokenRequestParamsError`, a subclass of `ArgumentError`, so a caller can tell them from an unknown keyword, and the copy duplicates keys as well as values, since `Hash` copies only keys whose class is exactly `String`. Fixes #554. ## How Has This Been Tested? New tests cover every token request the parameters can ride (the `client_credentials` grant under all three client authentication methods, the authorization code exchange, refresh, and `jwt-bearer`), both refusal boundaries (the constructors of all three providers, and the flow for a provider defining the method), and the transport surfacing a refused hook from a refresh attempt without starting the interactive flow. All of them fail against the previous library. ## Breaking Changes None. The keyword is optional and the hook is opt-in; a provider that does not define `token_request_params` sends the same requests as before. |
||
| .github | ||
| bin | ||
| conformance | ||
| docs | ||
| examples | ||
| lib | ||
| test | ||
| .gitattributes | ||
| .gitignore | ||
| .rubocop.yml | ||
| AGENTS.md | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| CODE_OF_CONDUCT.md | ||
| dev.yml | ||
| Gemfile | ||
| LICENSE | ||
| mcp.gemspec | ||
| Rakefile | ||
| README.md | ||
| RELEASE.md | ||
| ROADMAP.md | ||
| SECURITY.md | ||
| VERSIONING.md | ||
MCP Ruby SDK

The official Ruby SDK for Model Context Protocol servers and clients.
Detailed guides are available at https://ruby.sdk.modelcontextprotocol.io.
Features
- Build MCP servers that expose tools, prompts, and resources to any MCP host
- Build MCP clients that connect to any MCP server, with automatic lifecycle negotiation and OAuth 2.1 authorization
- Speak every standard transport: stdio and Streamable HTTP (including SSE), with a Rails integration
- Cover the full protocol surface: server-to-client requests, multi round-trip requests, notifications, progress, logging, cancellation, completions, and pagination
Installation
Add this line to your application's Gemfile:
gem "mcp"
And then execute:
$ bundle install
Or install it yourself as:
$ gem install mcp
You may need to add additional dependencies depending on which features you wish to access.
Quick Start
The following minimal programs show both sides of the protocol: a server that exposes a single tool, and a client that spawns such a server and drives it over stdio.
MCP Server
A minimal server defines a tool and serves it over the stdio transport:
require "mcp"
# Create a simple tool
class ExampleTool < MCP::Tool
description "A simple example tool that echoes back its arguments"
input_schema(
properties: {
message: { type: "string" },
},
required: ["message"]
)
class << self
def call(message:, server_context:)
MCP::Tool::Response.new([{
type: "text",
text: "Hello from example tool! Message: #{message}",
}])
end
end
end
# Set up the server
server = MCP::Server.new(
name: "example_server",
tools: [ExampleTool],
)
# Create and start the transport
transport = MCP::Server::Transports::StdioTransport.new(server)
transport.open
Save the script as server.rb, run it, and send JSON-RPC requests via stdin:
$ ruby server.rb
{"jsonrpc":"2.0","id":"1","method":"ping"}
{"jsonrpc":"2.0","id":"2","method":"tools/list"}
{"jsonrpc":"2.0","id":"3","method":"tools/call","params":{"name":"example_tool","arguments":{"message":"Hello"}}}
The same server can also run over Streamable HTTP, including mounted inside a Rails application; see Server Transports.
MCP Client
A minimal client spawns a stdio server as a subprocess, connects, and lists and calls its tools:
stdio_transport = MCP::Client::Stdio.new(
command: "bundle",
args: ["exec", "ruby", "path/to/server.rb"],
env: { "API_KEY" => "my_secret_key" },
read_timeout: 30
)
client = MCP::Client.new(transport: stdio_transport)
# Perform the MCP initialization handshake before sending any requests.
client.connect
# List available tools.
tools = client.tools
tools.each do |tool|
puts "Tool: #{tool.name} - #{tool.description}"
end
# Call a specific tool.
response = client.call_tool(
tool: tools.first,
arguments: { message: "Hello, world!" }
)
# Close the transport when done.
stdio_transport.close
The same client can connect to Streamable HTTP servers with MCP::Client::HTTP;
see Client Transports.
Examples
Runnable examples are available in examples/,
including a complete Rails application in examples/rails.
Documentation
License
This project is licensed under the Apache License 2.0 for new contributions, with existing code under MIT. See the LICENSE file for details.