Skip to content

Test example code to ensure it runs #55

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ inherit_gem:
plugins:
- rubocop-minitest
- rubocop-rake

Security/Eval:
Exclude:
- test/fixtures/files/code_snippet_wrappers/**/*.rb # We must often resort to eval to access local variable
58 changes: 40 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ gem 'mcp'

And then execute:

```bash
```console
$ bundle install
```

Or install it yourself as:

```bash
```console
$ gem install mcp
```

Expand Down Expand Up @@ -98,6 +98,7 @@ requests.

You can use the `Server#handle_json` method to handle requests.

<!-- SNIPPET ID: rails_controller -->
```ruby
class ApplicationController < ActionController::Base

Expand All @@ -118,6 +119,7 @@ end

If you want to build a local command-line application, you can use the stdio transport:

<!-- SNIPPET ID: stdio_transport -->
```ruby
#!/usr/bin/env ruby
require "mcp"
Expand Down Expand Up @@ -156,7 +158,8 @@ transport.open

You can run this script and then type in requests to the server at the command line.

```bash
<!-- SNIPPET ID: running_stdio_server -->
```console
$ ./examples/stdio_server.rb
{"jsonrpc":"2.0","id":"1","method":"ping"}
{"jsonrpc":"2.0","id":"2","method":"tools/list"}
Expand All @@ -166,6 +169,7 @@ $ ./examples/stdio_server.rb

The gem can be configured using the `MCP.configure` block:

<!-- SNIPPET ID: configuration -->
```ruby
MCP.configure do |config|
config.exception_reporter = ->(exception, server_context) {
Expand All @@ -186,6 +190,7 @@ or by creating an explicit configuration and passing it into the server.
This is useful for systems where an application hosts more than one MCP server but
they might require different instrumentation callbacks.

<!-- SNIPPET ID: per_server_configuration -->
```ruby
configuration = MCP::Configuration.new
configuration.exception_reporter = ->(exception, server_context) {
Expand Down Expand Up @@ -218,6 +223,8 @@ server_context: { [String, Symbol] => Any }
```

**Example:**

<!-- SNIPPET ID: server_context -->
```ruby
server = MCP::Server.new(
name: "my_server",
Expand Down Expand Up @@ -259,6 +266,7 @@ instrumentation_callback = ->(data) { ... }
```

**Example:**
<!-- SNIPPET ID: instrumentation_callback -->
```ruby
config.instrumentation_callback = ->(data) {
puts "Instrumentation: #{data.inspect}"
Expand All @@ -267,16 +275,22 @@ config.instrumentation_callback = ->(data) {

### Server Protocol Version

The server's protocol version can be overridden using the `protocol_version` class method:
The server's protocol version can be overridden via the `Configuration#protocol_version` method:

<!-- SNIPPET ID: set_server_protocol_version -->
```ruby
MCP::Server.protocol_version = "2024-11-05"
MCP.configure do |config|
config.protocol_version = "2024-11-05"
end
```

This will make all new server instances use the specified protocol version instead of the default version. The protocol version can be reset to the default by setting it to `nil`:

<!-- SNIPPET ID: unset_server_protocol_version -->
```ruby
MCP::Server.protocol_version = nil
MCP.configure do |config|
config.protocol_version = nil
end
```

Be sure to check the [MCP spec](https://modelcontextprotocol.io/specification/2025-03-26) for the protocol version to understand the supported features for the version being set.
Expand Down Expand Up @@ -309,6 +323,7 @@ This gem provides a `MCP::Tool` class that can be used to create tools in two wa

1. As a class definition:

<!-- SNIPPET ID: tool_class_definition -->
```ruby
class MyTool < MCP::Tool
description "This tool performs specific functionality..."
Expand Down Expand Up @@ -336,6 +351,7 @@ tool = MyTool

2. By using the `MCP::Tool.define` method with a block:

<!-- SNIPPET ID: tool_definition_with_block -->
```ruby
tool = MCP::Tool.define(
name: "my_tool",
Expand Down Expand Up @@ -372,12 +388,13 @@ The `MCP::Prompt` class provides two ways to create prompts:

1. As a class definition with metadata:

<!-- SNIPPET ID: prompt_class_definition -->
```ruby
class MyPrompt < MCP::Prompt
prompt_name "my_prompt" # Optional - defaults to underscored class name
description "This prompt performs specific functionality..."
arguments [
Prompt::Argument.new(
MCP::Prompt::Argument.new(
name: "message",
description: "Input message",
required: true
Expand All @@ -386,16 +403,16 @@ class MyPrompt < MCP::Prompt

class << self
def template(args, server_context:)
Prompt::Result.new(
MCP::Prompt::Result.new(
description: "Response description",
messages: [
Prompt::Message.new(
MCP::Prompt::Message.new(
role: "user",
content: Content::Text.new("User message")
content: MCP::Content::Text.new("User message")
),
Prompt::Message.new(
MCP::Prompt::Message.new(
role: "assistant",
content: Content::Text.new(args["message"])
content: MCP::Content::Text.new(args[:message])
)
]
)
Expand All @@ -408,28 +425,29 @@ prompt = MyPrompt

2. Using the `MCP::Prompt.define` method:

<!-- SNIPPET ID: prompt_definition_with_block -->
```ruby
prompt = MCP::Prompt.define(
name: "my_prompt",
description: "This prompt performs specific functionality...",
arguments: [
Prompt::Argument.new(
MCP::Prompt::Argument.new(
name: "message",
description: "Input message",
required: true
)
]
) do |args, server_context:|
Prompt::Result.new(
MCP::Prompt::Result.new(
description: "Response description",
messages: [
Prompt::Message.new(
MCP::Prompt::Message.new(
role: "user",
content: Content::Text.new("User message")
content: MCP::Content::Text.new("User message")
),
Prompt::Message.new(
MCP::Prompt::Message.new(
role: "assistant",
content: Content::Text.new(args["message"])
content: MCP::Content::Text.new(args[:message])
)
]
)
Expand All @@ -450,6 +468,7 @@ e.g. around authentication state or user preferences.

Register prompts with the MCP server:

<!-- SNIPPET ID: prompts_usage -->
```ruby
server = MCP::Server.new(
name: "my_server",
Expand All @@ -468,6 +487,7 @@ The server will handle prompt listing and execution through the MCP protocol met
The server allows registering a callback to receive information about instrumentation.
To register a handler pass a proc/lambda to as `instrumentation_callback` into the server constructor.

<!-- SNIPPET ID: prompts_instrumentation_callback -->
```ruby
MCP.configure do |config|
config.instrumentation_callback = ->(data) {
Expand All @@ -493,6 +513,7 @@ MCP spec includes [Resources](https://modelcontextprotocol.io/docs/concepts/reso

The `MCP::Resource` class provides a way to register resources with the server.

<!-- SNIPPET ID: resources -->
```ruby
resource = MCP::Resource.new(
uri: "https://example.com/my_resource",
Expand All @@ -509,6 +530,7 @@ server = MCP::Server.new(

The server must register a handler for the `resources/read` method to retrieve a resource dynamically.

<!-- SNIPPET ID: resources_read_handler -->
```ruby
server.resources_read_handler do |params|
[{
Expand Down
5 changes: 5 additions & 0 deletions lib/mcp/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,13 @@ def handle_json(request)
end

def define_tool(name: nil, description: nil, input_schema: nil, annotations: nil, &block)
@capabilities.support_tools
tool = Tool.define(name:, description:, input_schema:, annotations:, &block)
@tools[tool.name_value] = tool
end

def define_prompt(name: nil, description: nil, arguments: [], &block)
@capabilities.support_prompts
prompt = Prompt.define(name:, description:, arguments:, &block)
@prompts[prompt.name_value] = prompt
end
Expand Down Expand Up @@ -128,6 +130,7 @@ def resources_list_handler(&block)
end

def resources_read_handler(&block)
@capabilities.support_resources
@handlers[Methods::RESOURCES_READ] = block
end

Expand All @@ -142,6 +145,7 @@ def tools_list_handler(&block)
end

def tools_call_handler(&block)
@capabilities.support_tools
@handlers[Methods::TOOLS_CALL] = block
end

Expand All @@ -151,6 +155,7 @@ def prompts_list_handler(&block)
end

def prompts_get_handler(&block)
@capabilities.support_prompts
@handlers[Methods::PROMPTS_GET] = block
end

Expand Down
41 changes: 41 additions & 0 deletions test/fixtures/files/code_snippet_wrappers/readme/configuration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

require "mcp"

# Stub Bugsnag
class Bugsnag
class Report
attr_reader :metadata

def initialize
@metadata = {}
end

def add_metadata(key, value)
@metadata[key] = value
end
end

class << self
def notify(exception)
report = Report.new
yield report
puts "Bugsnag notified of #{exception.inspect} with metadata #{report.metadata.inspect}"
end
end
end

require_relative "code_snippet"

puts MCP::Server.new(
tools: [
MCP::Tool.define(name: "error_tool") { raise "boom" },
],
).handle_json(
{
jsonrpc: "2.0",
id: "1",
method: "tools/call",
params: { name: "error_tool", arguments: {} },
}.to_json,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

require "mcp"

MCP.configure do |config|
eval(File.read("code_snippet.rb"), binding)

config.instrumentation_callback.call({ example: "data" })
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

require "mcp"

# Minimally mock Bugsnag for the test
module Bugsnag
class Report
attr_reader :metadata

def initialize
@metadata = {}
end

def add_metadata(key, value)
@metadata[key] = value
end
end

class << self
def notify(exception)
report = Report.new
yield report
puts "Bugsnag notified of #{exception.inspect} with metadata #{report.metadata.inspect}"
end
end
end

b = binding
eval(File.read("code_snippet.rb"), b)
server = b.local_variable_get(:server)

server.define_tool(name: "error_tool") { raise "boom" }

puts server.handle_json({
jsonrpc: "2.0",
id: "1",
method: "tools/call",
params: { name: "error_tool", arguments: {} },
}.to_json)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

require "mcp"

require_relative "code_snippet"

b = binding
eval(File.read("code_snippet.rb"), b)
prompt = b.local_variable_get(:prompt)

server = MCP::Server.new(prompts: [prompt])

[
{ jsonrpc: "2.0", id: "1", method: "prompts/list" },
{ jsonrpc: "2.0", id: "2", method: "prompts/get", params: { name: "my_prompt", arguments: { message: "Test message" } } },
].each { |request| puts server.handle_json(request.to_json) }
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true

require "mcp"
require_relative "code_snippet"

puts MCP::Server.new.handle_json({ jsonrpc: "2.0", id: "1", method: "ping" }.to_json)
Loading