Skip to content

Commit 38f7755

Browse files
committed
Test example code
This adds sanity tests to ensure that example code in the `examples/` directory and `README.md` isn't outright wrong. These are not intended to serve as unit tests for the example functionality, just as smoke tests to catch things like API changes that require updating examples. Including a sanity test ensures that the example code isn't outright wrong. This is not intended to serve as unit tests for the example functionality. Some minor changes are included which facilitate this work: - Use `console` instead of `bash` codeblock language `console` highlights `$ ` prefixed lines differently from the following lines, which clearly distinguishes between commands and input/output. - Set `file_fixture_path` This allows us to use `ActiveSupport::TestCase#file_fixture`. - Add `ReadmeTestHelper` This helper provides utilities for extracting code snippets from `README.md`. Some of these tests revealed that either the examples were busted, or even bugs in the implementation. - Test README per-server configuration example This test revealed that the `define_` helper methods were failing to ensure the server supports the type of capability they were defining. - Test README protocol version examples This revealed that the `protocol_version` accessors weren't available on the `Server` at all, and the examples were incorrect. - Test README tool definition examples This revealed that the `define` example doesn't work, and the fix is unclear.
1 parent 529f3bb commit 38f7755

22 files changed

+718
-18
lines changed

.rubocop.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,7 @@ inherit_gem:
44
plugins:
55
- rubocop-minitest
66
- rubocop-rake
7+
8+
Security/Eval:
9+
Exclude:
10+
- test/fixtures/files/code_snippet_wrappers/**/*.rb # We must often resort to eval to access local variable

README.md

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ gem 'mcp'
1212

1313
And then execute:
1414

15-
```bash
15+
```console
1616
$ bundle install
1717
```
1818

1919
Or install it yourself as:
2020

21-
```bash
21+
```console
2222
$ gem install mcp
2323
```
2424

@@ -98,6 +98,7 @@ requests.
9898

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

101+
<!-- SNIPPET ID: rails_controller -->
101102
```ruby
102103
class ApplicationController < ActionController::Base
103104

@@ -118,6 +119,7 @@ end
118119

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

122+
<!-- SNIPPET ID: stdio_transport -->
121123
```ruby
122124
#!/usr/bin/env ruby
123125
require "mcp"
@@ -156,7 +158,8 @@ transport.open
156158

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

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

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

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

193+
<!-- SNIPPET ID: per_server_configuration -->
189194
```ruby
190195
configuration = MCP::Configuration.new
191196
configuration.exception_reporter = ->(exception, server_context) {
@@ -218,6 +223,8 @@ server_context: { [String, Symbol] => Any }
218223
```
219224

220225
**Example:**
226+
227+
<!-- SNIPPET ID: server_context -->
221228
```ruby
222229
server = MCP::Server.new(
223230
name: "my_server",
@@ -259,6 +266,7 @@ instrumentation_callback = ->(data) { ... }
259266
```
260267

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

268276
### Server Protocol Version
269277

270-
The server's protocol version can be overridden using the `protocol_version` class method:
278+
The server's protocol version can be overridden via the `Configuration#protocol_version` method:
271279

280+
<!-- SNIPPET ID: set_server_protocol_version -->
272281
```ruby
273-
MCP::Server.protocol_version = "2024-11-05"
282+
MCP.configure do |config|
283+
config.protocol_version = "2024-11-05"
284+
end
274285
```
275286

276287
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`:
277288

289+
<!-- SNIPPET ID: unset_server_protocol_version -->
278290
```ruby
279-
MCP::Server.protocol_version = nil
291+
MCP.configure do |config|
292+
config.protocol_version = nil
293+
end
280294
```
281295

282296
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.
@@ -309,6 +323,7 @@ This gem provides a `MCP::Tool` class that can be used to create tools in two wa
309323

310324
1. As a class definition:
311325

326+
<!-- SNIPPET ID: tool_class_definition -->
312327
```ruby
313328
class MyTool < MCP::Tool
314329
description "This tool performs specific functionality..."
@@ -336,6 +351,7 @@ tool = MyTool
336351

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

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

373389
1. As a class definition with metadata:
374390

391+
<!-- SNIPPET ID: prompt_class_definition -->
375392
```ruby
376393
class MyPrompt < MCP::Prompt
377394
prompt_name "my_prompt" # Optional - defaults to underscored class name
378395
description "This prompt performs specific functionality..."
379396
arguments [
380-
Prompt::Argument.new(
397+
MCP::Prompt::Argument.new(
381398
name: "message",
382399
description: "Input message",
383400
required: true
@@ -386,16 +403,16 @@ class MyPrompt < MCP::Prompt
386403

387404
class << self
388405
def template(args, server_context:)
389-
Prompt::Result.new(
406+
MCP::Prompt::Result.new(
390407
description: "Response description",
391408
messages: [
392-
Prompt::Message.new(
409+
MCP::Prompt::Message.new(
393410
role: "user",
394-
content: Content::Text.new("User message")
411+
content: MCP::Content::Text.new("User message")
395412
),
396-
Prompt::Message.new(
413+
MCP::Prompt::Message.new(
397414
role: "assistant",
398-
content: Content::Text.new(args["message"])
415+
content: MCP::Content::Text.new(args[:message])
399416
)
400417
]
401418
)
@@ -408,28 +425,29 @@ prompt = MyPrompt
408425

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

428+
<!-- SNIPPET ID: prompt_definition_with_block -->
411429
```ruby
412430
prompt = MCP::Prompt.define(
413431
name: "my_prompt",
414432
description: "This prompt performs specific functionality...",
415433
arguments: [
416-
Prompt::Argument.new(
434+
MCP::Prompt::Argument.new(
417435
name: "message",
418436
description: "Input message",
419437
required: true
420438
)
421439
]
422440
) do |args, server_context:|
423-
Prompt::Result.new(
441+
MCP::Prompt::Result.new(
424442
description: "Response description",
425443
messages: [
426-
Prompt::Message.new(
444+
MCP::Prompt::Message.new(
427445
role: "user",
428-
content: Content::Text.new("User message")
446+
content: MCP::Content::Text.new("User message")
429447
),
430-
Prompt::Message.new(
448+
MCP::Prompt::Message.new(
431449
role: "assistant",
432-
content: Content::Text.new(args["message"])
450+
content: MCP::Content::Text.new(args[:message])
433451
)
434452
]
435453
)
@@ -450,6 +468,7 @@ e.g. around authentication state or user preferences.
450468

451469
Register prompts with the MCP server:
452470

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

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

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

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

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

533+
<!-- SNIPPET ID: resources_read_handler -->
512534
```ruby
513535
server.resources_read_handler do |params|
514536
[{

lib/mcp/server.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,13 @@ def handle_json(request)
8989
end
9090

9191
def define_tool(name: nil, description: nil, input_schema: nil, annotations: nil, &block)
92+
@capabilities.support_tools
9293
tool = Tool.define(name:, description:, input_schema:, annotations:, &block)
9394
@tools[tool.name_value] = tool
9495
end
9596

9697
def define_prompt(name: nil, description: nil, arguments: [], &block)
98+
@capabilities.support_prompts
9799
prompt = Prompt.define(name:, description:, arguments:, &block)
98100
@prompts[prompt.name_value] = prompt
99101
end
@@ -128,6 +130,7 @@ def resources_list_handler(&block)
128130
end
129131

130132
def resources_read_handler(&block)
133+
@capabilities.support_resources
131134
@handlers[Methods::RESOURCES_READ] = block
132135
end
133136

@@ -142,6 +145,7 @@ def tools_list_handler(&block)
142145
end
143146

144147
def tools_call_handler(&block)
148+
@capabilities.support_tools
145149
@handlers[Methods::TOOLS_CALL] = block
146150
end
147151

@@ -151,6 +155,7 @@ def prompts_list_handler(&block)
151155
end
152156

153157
def prompts_get_handler(&block)
158+
@capabilities.support_prompts
154159
@handlers[Methods::PROMPTS_GET] = block
155160
end
156161

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# frozen_string_literal: true
2+
3+
require "mcp"
4+
5+
# Stub Bugsnag
6+
class Bugsnag
7+
class Report
8+
attr_reader :metadata
9+
10+
def initialize
11+
@metadata = {}
12+
end
13+
14+
def add_metadata(key, value)
15+
@metadata[key] = value
16+
end
17+
end
18+
19+
class << self
20+
def notify(exception)
21+
report = Report.new
22+
yield report
23+
puts "Bugsnag notified of #{exception.inspect} with metadata #{report.metadata.inspect}"
24+
end
25+
end
26+
end
27+
28+
require_relative "code_snippet"
29+
30+
puts MCP::Server.new(
31+
tools: [
32+
MCP::Tool.define(name: "error_tool") { raise "boom" },
33+
],
34+
).handle_json(
35+
{
36+
jsonrpc: "2.0",
37+
id: "1",
38+
method: "tools/call",
39+
params: { name: "error_tool", arguments: {} },
40+
}.to_json,
41+
)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# frozen_string_literal: true
2+
3+
require "mcp"
4+
5+
MCP.configure do |config|
6+
eval(File.read("code_snippet.rb"), binding)
7+
8+
config.instrumentation_callback.call({ example: "data" })
9+
end
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# frozen_string_literal: true
2+
3+
require "mcp"
4+
5+
# Minimally mock Bugsnag for the test
6+
module Bugsnag
7+
class Report
8+
attr_reader :metadata
9+
10+
def initialize
11+
@metadata = {}
12+
end
13+
14+
def add_metadata(key, value)
15+
@metadata[key] = value
16+
end
17+
end
18+
19+
class << self
20+
def notify(exception)
21+
report = Report.new
22+
yield report
23+
puts "Bugsnag notified of #{exception.inspect} with metadata #{report.metadata.inspect}"
24+
end
25+
end
26+
end
27+
28+
b = binding
29+
eval(File.read("code_snippet.rb"), b)
30+
server = b.local_variable_get(:server)
31+
32+
server.define_tool(name: "error_tool") { raise "boom" }
33+
34+
puts server.handle_json({
35+
jsonrpc: "2.0",
36+
id: "1",
37+
method: "tools/call",
38+
params: { name: "error_tool", arguments: {} },
39+
}.to_json)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# frozen_string_literal: true
2+
3+
require "mcp"
4+
5+
require_relative "code_snippet"
6+
7+
b = binding
8+
eval(File.read("code_snippet.rb"), b)
9+
prompt = b.local_variable_get(:prompt)
10+
11+
server = MCP::Server.new(prompts: [prompt])
12+
13+
[
14+
{ jsonrpc: "2.0", id: "1", method: "prompts/list" },
15+
{ jsonrpc: "2.0", id: "2", method: "prompts/get", params: { name: "my_prompt", arguments: { message: "Test message" } } },
16+
].each { |request| puts server.handle_json(request.to_json) }
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
prompt_class_definition.rb
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# frozen_string_literal: true
2+
3+
require "mcp"
4+
require_relative "code_snippet"
5+
6+
puts MCP::Server.new.handle_json({ jsonrpc: "2.0", id: "1", method: "ping" }.to_json)

0 commit comments

Comments
 (0)