ajentik/mcp_server
If you are the rightful owner of mcp_server and would like to certify it and/or have it hosted online, please leave a comment on the right or send an email to henry@mcphub.com.
A pluggable, protocol-agnostic MCP (Model-Context-Protocol) server for Rack-based Ruby applications.
mcp_server
A pluggable, protocol-agnostic MCP (Model-Context-Protocol) server for Rack-based Ruby applications.
Installation
Add this line to your application's Gemfile:
gem 'mcp_server'
And then execute:
bundle install
Or install it yourself as:
gem install mcp_server
Usage
Basic Configuration
Configure the MCP server with your authentication logic and MCP components (tools, prompts, resources):
require 'mcp_server'
require 'mcp'
# Define your MCP tools
class CalculatorTool < MCP::Tool
tool_name "calculator"
description "Performs basic arithmetic operations"
input_schema(
properties: {
operation: { type: "string", enum: ["add", "subtract", "multiply", "divide"] },
a: { type: "number" },
b: { type: "number" }
},
required: ["operation", "a", "b"]
)
def self.call(operation:, a:, b:, server_context:)
# Access context data in your tool
user = server_context[:user]
permissions = server_context[:permissions]
# Your tool logic here
result = case operation
when "add" then a + b
when "subtract" then a - b
when "multiply" then a * b
when "divide" then b != 0 ? a / b : "Error: Division by zero"
end
MCP::Tool::Response.new(
content: [MCP::Content.text("Result: #{result}")]
)
end
end
# Configure the server
MCPServer.configure do |config|
# Define how to authenticate requests (optional)
config.authenticate_with do |request|
# Your authentication logic here
# Return true/false or raise an error
request.env['HTTP_AUTHORIZATION'] == 'Bearer secret-token'
end
# Define how to build the server context (optional)
config.build_context_with do |request|
# Return a hash with any context data your tools need
{
user: User.find_by_token(request.env['HTTP_AUTHORIZATION']),
organization: Organization.from_request(request),
permissions: Permission.for_request(request),
locale: request.env['HTTP_ACCEPT_LANGUAGE'],
custom_data: "any other context you need"
}
end
# Configure MCP components (as callables for dynamic loading)
config.tools = -> { [CalculatorTool] }
config.prompts = -> { [] } # Add your MCP::Prompt classes here
config.resources = -> { [] } # Add your MCP::Resource instances here
# Refer to https://github.com/modelcontextprotocol/ruby-sdk?tab=readme-ov-file#resources
# for more details on how to define resources
config.resources_read_handler do |request_params|
# Handle resource read requests
resource = Resource.find_by_id(request_params['id'])
[{
uri: request_params[:uri],
mimeType: 'application/json',
text: resource.to_json
}]
end
# Only when using the MCP gem's version after commit hash 382ae13
# https://github.com/modelcontextprotocol/ruby-sdk/commit/382ae13e25ba095fbe227b186b3287c3c7eb7ff4
config.transport = MCP::Server::Transports::StdioTransport
# Define a response handler to modify responses (optional)
config.response_handler do |response, request|
# Modify response headers, status, or body
status, headers, body = response
headers['X-Custom-Header'] = 'Modified'
# Disable SSE (Server-Sent Events) by removing the session ID header
# headers.delete('Mcp-Session-Id')
[status, headers, body]
end
end
Server-Sent Events (SSE) and Multi-Process Servers
Important: The current SSE implementation requires keeping stream objects in memory. This means SSE will not work correctly with multi-process servers (like Unicorn, Puma in clustered mode, or Passenger) because:
- Stream objects are stored in the process that handles the initial request
- Subsequent requests may be routed to different processes that don't have access to these streams
- This results in failed SSE connections when requests are handled by different processes
Solutions:
- Use a single-process, multi-threaded server (e.g., Puma in single mode with multiple threads)
- Disable SSE by removing the
Mcp-Session-Id
header in the response handler (see example above)
Mounting in a Rack Application
In a Rails application
Add to your config/routes.rb
:
mount MCPServer::RackApp => '/mcp'
In a Sinatra application
require 'sinatra'
require 'mcp_server'
# Configure MCPServer...
use MCPServer::RackApp
In a plain Rack application (config.ru)
require 'mcp_server'
# Configure MCPServer...
run MCPServer::RackApp
Development
After checking out the repo, run bundle install
to install dependencies.
Running Tests
# Run all tests
rake test
# Run a specific test file
ruby -Ilib:test test/test_configuration.rb
Linting
This project uses StandardRB for Ruby style guidelines:
standardrb
Building the Gem
gem build mcp_server.gemspec
Architecture
The gem follows a modular architecture:
- Configuration: Central configuration object that stores:
- Authentication callback (optional)
- Context building callback (optional) - builds server context passed to tools
- Callables that return arrays of MCP tools, prompts, and resources (for dynamic loading)
- Response handler callback (optional) - for modifying responses before sending
- RackApp: Main Rack application that:
- Handles HTTP requests with JSON-RPC payloads (supports all HTTP methods)
- Performs authentication if configured
- Creates an MCP::Server instance with configured components
- Delegates request handling to the MCP gem or configured transport
- Handles various response body types (nil, string, array, object)
- MCP Integration: Leverages the official MCP Ruby SDK for protocol compliance
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request
License
The gem is available as open source under the terms of the MIT License.