{"kind":"AgentDefinition","metadata":{"namespace":"community","name":"ruby-mcp-server","version":"0.1.0"},"spec":{"agents_md":"---\ndescription: 'Best practices and patterns for building Model Context Protocol (MCP) servers in Ruby using the official MCP Ruby SDK gem.'\napplyTo: \"**/*.rb, **/Gemfile, **/*.gemspec, **/Rakefile\"\n---\n\n# Ruby MCP Server Development Guidelines\n\nWhen building MCP servers in Ruby, follow these best practices and patterns using the official Ruby SDK.\n\n## Installation\n\nAdd the MCP gem to your Gemfile:\n\n```ruby\ngem 'mcp'\n```\n\nThen run:\n\n```bash\nbundle install\n```\n\n## Server Setup\n\nCreate an MCP server instance:\n\n```ruby\nrequire 'mcp'\n\nserver = MCP::Server.new(\n  name: 'my_server',\n  version: '1.0.0'\n)\n```\n\n## Adding Tools\n\nDefine tools using classes or blocks:\n\n### Tool as Class\n\n```ruby\nclass GreetTool \u003c MCP::Tool\n  tool_name 'greet'\n  description 'Generate a greeting message'\n  \n  input_schema(\n    properties: {\n      name: { type: 'string', description: 'Name to greet' }\n    },\n    required: ['name']\n  )\n  \n  output_schema(\n    properties: {\n      message: { type: 'string' },\n      timestamp: { type: 'string', format: 'date-time' }\n    },\n    required: ['message']\n  )\n  \n  annotations(\n    read_only_hint: true,\n    idempotent_hint: true\n  )\n  \n  def self.call(name:, server_context:)\n    MCP::Tool::Response.new([{\n      type: 'text',\n      text: \"Hello, #{name}! Welcome to MCP.\"\n    }], structured_content: {\n      message: \"Hello, #{name}!\",\n      timestamp: Time.now.iso8601\n    })\n  end\nend\n\nserver = MCP::Server.new(\n  name: 'my_server',\n  tools: [GreetTool]\n)\n```\n\n### Tool with Block\n\n```ruby\nserver.define_tool(\n  name: 'calculate',\n  description: 'Perform mathematical calculations',\n  input_schema: {\n    properties: {\n      operation: { type: 'string', enum: ['add', 'subtract', 'multiply', 'divide'] },\n      a: { type: 'number' },\n      b: { type: 'number' }\n    },\n    required: ['operation', 'a', 'b']\n  },\n  annotations: {\n    read_only_hint: true,\n    idempotent_hint: true\n  }\n) do |args, server_context|\n  operation = args['operation']\n  a = args['a']\n  b = args['b']\n  \n  result = case operation\n  when 'add' then a + b\n  when 'subtract' then a - b\n  when 'multiply' then a * b\n  when 'divide'\n    return MCP::Tool::Response.new([{ type: 'text', text: 'Division by zero' }], is_error: true) if b == 0\n    a / b\n  else\n    return MCP::Tool::Response.new([{ type: 'text', text: \"Unknown operation: #{operation}\" }], is_error: true)\n  end\n  \n  MCP::Tool::Response.new([{ type: 'text', text: \"Result: #{result}\" }])\nend\n```\n\n## Adding Resources\n\nDefine resources for data access:\n\n```ruby\n# Register resources\nresource = MCP::Resource.new(\n  uri: 'resource://data/example',\n  name: 'example-data',\n  description: 'Example resource data',\n  mime_type: 'application/json'\n)\n\nserver = MCP::Server.new(\n  name: 'my_server',\n  resources: [resource]\n)\n\n# Define read handler\nserver.resources_read_handler do |params|\n  case params[:uri]\n  when 'resource://data/example'\n    [{\n      uri: params[:uri],\n      mimeType: 'application/json',\n      text: { message: 'Example data', timestamp: Time.now }.to_json\n    }]\n  else\n    raise \"Unknown resource: #{params[:uri]}\"\n  end\nend\n```\n\n## Adding Prompts\n\nDefine prompt templates:\n\n### Prompt as Class\n\n```ruby\nclass CodeReviewPrompt \u003c MCP::Prompt\n  prompt_name 'code_review'\n  description 'Generate a code review prompt'\n  \n  arguments [\n    MCP::Prompt::Argument.new(\n      name: 'language',\n      description: 'Programming language',\n      required: true\n    ),\n    MCP::Prompt::Argument.new(\n      name: 'focus',\n      description: 'Review focus area',\n      required: false\n    )\n  ]\n  \n  def self.template(args, server_context:)\n    language = args['language'] || 'Ruby'\n    focus = args['focus'] || 'general quality'\n    \n    MCP::Prompt::Result.new(\n      description: \"Code review for #{language} with focus on #{focus}\",\n      messages: [\n        MCP::Prompt::Message.new(\n          role: 'user',\n          content: MCP::Content::Text.new(\"Please review this #{language} code with focus on #{focus}.\")\n        ),\n        MCP::Prompt::Message.new(\n          role: 'assistant',\n          content: MCP::Content::Text.new(\"I'll review the code focusing on #{focus}. Please share the code.\")\n        )\n      ]\n    )\n  end\nend\n\nserver = MCP::Server.new(\n  name: 'my_server',\n  prompts: [CodeReviewPrompt]\n)\n```\n\n### Prompt with Block\n\n```ruby\nserver.define_prompt(\n  name: 'analyze',\n  description: 'Analyze a topic',\n  arguments: [\n    MCP::Prompt::Argument.new(name: 'topic', description: 'Topic to analyze', required: true),\n    MCP::Prompt::Argument.new(name: 'depth', description: 'Analysis depth', required: false)\n  ]\n) do |args, server_context:|\n  topic = args['topic']\n  depth = args['depth'] || 'basic'\n  \n  MCP::Prompt::Result.new(\n    description: \"Analysis of #{topic} at #{depth} level\",\n    messages: [\n      MCP::Prompt::Message.new(\n        role: 'user',\n        content: MCP::Content::Text.new(\"Please analyze: #{topic}\")\n      ),\n      MCP::Prompt::Message.new(\n        role: 'assistant',\n        content: MCP::Content::Text.new(\"I'll provide a #{depth} analysis of #{topic}\")\n      )\n    ]\n  )\nend\n```\n\n## Transport Configuration\n\n### Stdio Transport\n\nFor local command-line applications:\n\n```ruby\nrequire 'mcp'\n\nserver = MCP::Server.new(\n  name: 'my_server',\n  tools: [MyTool]\n)\n\ntransport = MCP::Server::Transports::StdioTransport.new(server)\ntransport.open\n```\n\n### HTTP Transport (Rails)\n\nFor Rails applications:\n\n```ruby\nclass McpController \u003c ApplicationController\n  def index\n    server = MCP::Server.new(\n      name: 'rails_server',\n      version: '1.0.0',\n      tools: [SomeTool],\n      prompts: [MyPrompt],\n      server_context: { user_id: current_user.id }\n    )\n    \n    render json: server.handle_json(request.body.read)\n  end\nend\n```\n\n### Streamable HTTP Transport\n\nFor Server-Sent Events:\n\n```ruby\nserver = MCP::Server.new(name: 'my_server')\ntransport = MCP::Server::Transports::StreamableHTTPTransport.new(server)\nserver.transport = transport\n\n# When tools change, notify clients\nserver.define_tool(name: 'new_tool') { |**args| { result: 'ok' } }\nserver.notify_tools_list_changed\n```\n\n## Server Context\n\nPass contextual information to tools and prompts:\n\n```ruby\nserver = MCP::Server.new(\n  name: 'my_server',\n  tools: [AuthenticatedTool],\n  server_context: {\n    user_id: current_user.id,\n    request_id: request.uuid,\n    auth_token: session[:token]\n  }\n)\n\nclass AuthenticatedTool \u003c MCP::Tool\n  def self.call(query:, server_context:)\n    user_id = server_context[:user_id]\n    # Use user_id for authorization\n    \n    MCP::Tool::Response.new([{ type: 'text', text: 'Authorized' }])\n  end\nend\n```\n\n## Configuration\n\n### Exception Reporting\n\nConfigure exception reporting:\n\n```ruby\nMCP.configure do |config|\n  config.exception_reporter = -\u003e(exception, server_context) {\n    # Report to your error tracking service\n    Bugsnag.notify(exception) do |report|\n      report.add_metadata(:mcp, server_context)\n    end\n  }\nend\n```\n\n### Instrumentation\n\nMonitor MCP server performance:\n\n```ruby\nMCP.configure do |config|\n  config.instrumentation_callback = -\u003e(data) {\n    # Log instrumentation data\n    Rails.logger.info(\"MCP: #{data.inspect}\")\n    \n    # Or send to metrics service\n    StatsD.timing(\"mcp.#{data[:method]}.duration\", data[:duration])\n    StatsD.increment(\"mcp.#{data[:method]}.count\")\n  }\nend\n```\n\nThe instrumentation data includes:\n- `method`: Protocol method called (e.g., \"tools/call\")\n- `tool_name`: Name of tool called\n- `prompt_name`: Name of prompt called\n- `resource_uri`: URI of resource called\n- `error`: Error code if lookup failed\n- `duration`: Duration in seconds\n\n### Protocol Version\n\nOverride the protocol version:\n\n```ruby\nconfiguration = MCP::Configuration.new(protocol_version: '2025-06-18')\nserver = MCP::Server.new(name: 'my_server', configuration: configuration)\n```\n\n## Tool Annotations\n\nProvide metadata about tool behavior:\n\n```ruby\nclass DataTool \u003c MCP::Tool\n  annotations(\n    read_only_hint: true,      # Tool only reads data\n    destructive_hint: false,   # Tool doesn't destroy data\n    idempotent_hint: true,     # Same input = same output\n    open_world_hint: false     # Tool operates in closed context\n  )\n  \n  def self.call(**args, server_context:)\n    # Implementation\n  end\nend\n```\n\n## Tool Output Schemas\n\nDefine expected output structure:\n\n```ruby\nclass WeatherTool \u003c MCP::Tool\n  output_schema(\n    properties: {\n      temperature: { type: 'number' },\n      condition: { type: 'string' },\n      humidity: { type: 'integer' }\n    },\n    required: ['temperature', 'condition']\n  )\n  \n  def self.call(location:, server_context:)\n    weather_data = {\n      temperature: 72.5,\n      condition: 'sunny',\n      humidity: 45\n    }\n    \n    # Validate against schema\n    output_schema.validate_result(weather_data)\n    \n    MCP::Tool::Response.new(\n      [{ type: 'text', text: weather_data.to_json }],\n      structured_content: weather_data\n    )\n  end\nend\n```\n\n## Structured Content in Responses\n\nReturn structured data with text:\n\n```ruby\nclass APITool \u003c MCP::Tool\n  def self.call(endpoint:, server_context:)\n    api_data = call_api(endpoint)\n    \n    MCP::Tool::Response.new(\n      [{ type: 'text', text: api_data.to_json }],\n      structured_content: api_data\n    )\n  end\nend\n```\n\n## Custom Methods\n\nDefine custom JSON-RPC methods:\n\n```ruby\nserver = MCP::Server.new(name: 'my_server')\n\n# Custom method with result\nserver.define_custom_method(method_name: 'add') do |params|\n  params[:a] + params[:b]\nend\n\n# Custom notification (returns nil)\nserver.define_custom_method(method_name: 'notify') do |params|\n  puts \"Notification: #{params[:message]}\"\n  nil\nend\n```\n\n## Notifications\n\nSend list change notifications:\n\n```ruby\nserver = MCP::Server.new(name: 'my_server')\ntransport = MCP::Server::Transports::StreamableHTTPTransport.new(server)\nserver.transport = transport\n\n# Notify when tools change\nserver.define_tool(name: 'new_tool') { |**args| { result: 'ok' } }\nserver.notify_tools_list_changed\n\n# Notify when prompts change\nserver.define_prompt(name: 'new_prompt') { |args, **_| MCP::Prompt::Result.new(...) }\nserver.notify_prompts_list_changed\n\n# Notify when resources change\nserver.notify_resources_list_changed\n```\n\n## Resource Templates\n\nDefine dynamic resources with URI templates:\n\n```ruby\nresource_template = MCP::ResourceTemplate.new(\n  uri_template: 'users://{user_id}/profile',\n  name: 'user-profile',\n  description: 'User profile data',\n  mime_type: 'application/json'\n)\n\nserver = MCP::Server.new(\n  name: 'my_server',\n  resource_templates: [resource_template]\n)\n```\n\n## Error Handling\n\nHandle errors properly in tools:\n\n```ruby\nclass RiskyTool \u003c MCP::Tool\n  def self.call(data:, server_context:)\n    begin\n      result = risky_operation(data)\n      MCP::Tool::Response.new([{ type: 'text', text: result }])\n    rescue ValidationError =\u003e e\n      MCP::Tool::Response.new(\n        [{ type: 'text', text: \"Invalid input: #{e.message}\" }],\n        is_error: true\n      )\n    rescue =\u003e e\n      # Will be caught and reported by exception_reporter\n      raise\n    end\n  end\nend\n```\n\n## Testing\n\nWrite tests for your MCP server:\n\n```ruby\nrequire 'minitest/autorun'\nrequire 'mcp'\n\nclass MyToolTest \u003c Minitest::Test\n  def test_greet_tool\n    response = GreetTool.call(name: 'Ruby', server_context: {})\n    \n    assert_equal 1, response.content.length\n    assert_match(/Ruby/, response.content.first[:text])\n    refute response.is_error\n  end\n  \n  def test_invalid_input\n    response = CalculateTool.call(operation: 'divide', a: 10, b: 0, server_context: {})\n    \n    assert response.is_error\n  end\nend\n```\n\n## Client Usage\n\nBuild MCP clients to connect to servers:\n\n```ruby\nrequire 'mcp'\nrequire 'faraday'\n\n# HTTP transport\nhttp_transport = MCP::Client::HTTP.new(\n  url: 'https://api.example.com/mcp',\n  headers: { 'Authorization' =\u003e \"Bearer #{token}\" }\n)\n\nclient = MCP::Client.new(transport: http_transport)\n\n# List tools\ntools = client.tools\ntools.each do |tool|\n  puts \"Tool: #{tool.name}\"\n  puts \"Description: #{tool.description}\"\nend\n\n# Call a tool\nresponse = client.call_tool(\n  tool: tools.first,\n  arguments: { message: 'Hello, world!' }\n)\n```\n\n## Best Practices\n\n1. **Use classes for complex tools** - Better organization and testability\n2. **Define input/output schemas** - Ensure type safety and validation\n3. **Add annotations** - Help clients understand tool behavior\n4. **Include structured content** - Provide both text and structured data\n5. **Use server_context** - Pass authentication and request context\n6. **Configure exception reporting** - Monitor errors in production\n7. **Implement instrumentation** - Track performance metrics\n8. **Send notifications** - Keep clients updated on changes\n9. **Validate inputs** - Check parameters before processing\n10. **Follow Ruby conventions** - Use snake_case, proper indentation\n\n## Common Patterns\n\n### Authenticated Tool\n\n```ruby\nclass AuthenticatedTool \u003c MCP::Tool\n  def self.call(**args, server_context:)\n    user_id = server_context[:user_id]\n    raise 'Unauthorized' unless user_id\n    \n    # Process authenticated request\n    MCP::Tool::Response.new([{ type: 'text', text: 'Success' }])\n  end\nend\n```\n\n### Paginated Resource\n\n```ruby\nserver.resources_read_handler do |params|\n  uri = params[:uri]\n  page = params[:page] || 1\n  \n  data = fetch_paginated_data(page)\n  \n  [{\n    uri: uri,\n    mimeType: 'application/json',\n    text: data.to_json\n  }]\nend\n```\n\n### Dynamic Prompt\n\n```ruby\nclass DynamicPrompt \u003c MCP::Prompt\n  def self.template(args, server_context:)\n    user_id = server_context[:user_id]\n    user_data = User.find(user_id)\n    \n    MCP::Prompt::Result.new(\n      description: \"Personalized prompt for #{user_data.name}\",\n      messages: generate_messages_for(user_data)\n    )\n  end\nend\n```\n","description":"Best practices and patterns for building Model Context Protocol (MCP) servers in Ruby using the official MCP Ruby SDK gem.","import":{"commit_sha":"541b7819d8c3545c6df122491af4fa1eae415779","imported_at":"2026-05-18T20:05:35Z","license_text":"MIT License\n\nCopyright GitHub, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.","owner":"github","repo":"github/awesome-copilot","source_url":"https://github.com/github/awesome-copilot/blob/541b7819d8c3545c6df122491af4fa1eae415779/instructions/ruby-mcp-server.instructions.md"},"manifest":{}},"content_hash":[96,39,248,51,114,5,132,146,223,86,69,109,225,218,102,165,207,251,213,224,160,234,151,142,85,229,224,164,81,143,244,3],"trust_level":"unsigned","yanked":false}
