Skip to content

ajentik/ruby_mcp_server

Repository files navigation

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:

  1. Use a single-process, multi-threaded server (e.g., Puma in single mode with multiple threads)
  2. 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

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

License

The gem is available as open source under the terms of the MIT License.

About

Configurable Rack MCP Server App

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages