Skip to content

Commit 7175d49

Browse files
committed
Internal Release 0.5.0
Bump protocol version to 2025-03-26 Provide fallback value for tool_name Make configuration per server Add support for tool annotations document tool annotations
1 parent 2eca368 commit 7175d49

File tree

13 files changed

+353
-42
lines changed

13 files changed

+353
-42
lines changed

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
model_context_protocol (0.4.0)
4+
model_context_protocol (0.5.0)
55
json_rpc_handler (~> 0.1)
66

77
GEM

README.md

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,24 @@ module ModelContextProtocol
7676
end
7777
```
7878

79+
To see sample responses without setting up a client to hit the server, you can simply run
80+
81+
```ruby
82+
server = ModelContextProtocol::Server.new(
83+
name: "my_server",
84+
tools: [SomeTool, AnotherTool],
85+
prompts: [MyPrompt],
86+
context: { user_id: current_user.id },
87+
)
88+
request = {
89+
jsonrpc: "2.0",
90+
method: "tools/list",
91+
params: {},
92+
id: "1"
93+
}
94+
server.handle(request)
95+
```
96+
7997
## Configuration
8098

8199
The gem can be configured using the `ModelContextProtocol.configure` block:
@@ -92,6 +110,26 @@ ModelContextProtocol.configure do |config|
92110

93111
config.instrumentation_callback = -> (data) { puts "Got instrumentation data #{data.inspect}" }
94112
end
113+
114+
or by creating an explicit configuration and passing it into the server.
115+
This is useful for systems where an application hosts more than one MCP server but
116+
they might require different instrumentation callbacks.
117+
118+
configuration = ModelContextProtocol::Configuration.new
119+
configuration.exception_reporter = ->(exception, context) do
120+
# Your exception reporting logic here
121+
# For example with Bugsnag:
122+
Bugsnag.notify(exception) do |report|
123+
report.add_metadata(:model_context_protocol, context)
124+
end
125+
end
126+
127+
configuration.instrumentation_callback = -> (data) { puts "Got instrumentation data #{data.inspect}" }
128+
129+
server = ModelContextProtocol::Server.new(
130+
# ... all other options
131+
configuration:,
132+
)
95133
```
96134

97135
### Exception Reporting
@@ -125,10 +163,23 @@ This gem provides a `ModelContextProtocol::Tool` class that can be used to creat
125163
```ruby
126164
class MyTool < ModelContextProtocol::Tool
127165
description "This tool performs specific functionality..."
128-
input_schema [{ type: "text", name: "message" }]
166+
input_schema [{ type: "text", text: "message" }]
167+
annotations(
168+
title: "My Tool",
169+
read_only_hint: true,
170+
destructive_hint: false,
171+
idempotent_hint: true,
172+
open_world_hint: false
173+
)
129174

130-
def self.call(message, context:)
131-
Tool::Response.new([{ type: "text", content: "OK" }])
175+
input_schema type: 'object',
176+
properties: {
177+
message: { type: 'string' },
178+
},
179+
required: ['message']
180+
181+
def self.call(message:, context:)
182+
Tool::Response.new([{ type: "text", text: "OK" }])
132183
end
133184
end
134185

@@ -138,14 +189,33 @@ tool = MyTool
138189
2. By using the `ModelContextProtocol::Tool.define` method with a block:
139190

140191
```ruby
141-
tool = ModelContextProtocol::Tool.define(name: "my_tool", description: "This tool performs specific functionality...") do |args, context|
142-
Tool::Response.new([{ type: "text", content: "OK" }])
192+
tool = ModelContextProtocol::Tool.define(
193+
name: "my_tool",
194+
description: "This tool performs specific functionality...",
195+
annotations: {
196+
title: "My Tool",
197+
read_only_hint: true
198+
}
199+
) do |args, context|
200+
Tool::Response.new([{ type: "text", text: "OK" }])
143201
end
144202
```
145203

146204
The context parameter is the context passed into the server and can be used to pass per request information,
147205
e.g. around authentication state.
148206

207+
### Tool Annotations
208+
209+
Tools can include annotations that provide additional metadata about their behavior. The following annotations are supported:
210+
211+
- `title`: A human-readable title for the tool
212+
- `read_only_hint`: Indicates if the tool only reads data (doesn't modify state)
213+
- `destructive_hint`: Indicates if the tool performs destructive operations
214+
- `idempotent_hint`: Indicates if the tool's operations are idempotent
215+
- `open_world_hint`: Indicates if the tool operates in an open world context
216+
217+
Annotations can be set either through the class definition using the `annotations` class method or when defining a tool using the `define` method.
218+
149219
## Prompts
150220

151221
MCP spec includes [Prompts](https://modelcontextprotocol.io/docs/concepts/prompts), which enable servers to define reusable prompt templates and workflows that clients can easily surface to users and LLMs.

lib/model_context_protocol/configuration.rb

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,57 @@
22

33
module ModelContextProtocol
44
class Configuration
5-
attr_accessor :exception_reporter
6-
attr_accessor :instrumentation_callback
5+
attr_writer :exception_reporter, :instrumentation_callback
76

8-
def initialize
9-
@exception_reporter = ->(exception, context) {} # Default no-op reporter
10-
@instrumentation_callback = ->(data) {} # Default no-op callback
7+
def initialize(exception_reporter: nil, instrumentation_callback: nil)
8+
@exception_reporter = exception_reporter
9+
@instrumentation_callback = instrumentation_callback
10+
end
11+
12+
def exception_reporter
13+
@exception_reporter || default_exception_reporter
14+
end
15+
16+
def exception_reporter?
17+
!@exception_reporter.nil?
18+
end
19+
20+
def instrumentation_callback
21+
@instrumentation_callback || default_instrumentation_callback
22+
end
23+
24+
def instrumentation_callback?
25+
!@instrumentation_callback.nil?
26+
end
27+
28+
def merge(other)
29+
return self if other.nil?
30+
31+
exception_reporter = if other.exception_reporter?
32+
other.exception_reporter
33+
else
34+
@exception_reporter
35+
end
36+
instrumentation_callback = if other.instrumentation_callback?
37+
other.instrumentation_callback
38+
else
39+
@instrumentation_callback
40+
end
41+
42+
Configuration.new(
43+
exception_reporter:,
44+
instrumentation_callback:,
45+
)
46+
end
47+
48+
private
49+
50+
def default_exception_reporter
51+
@default_exception_reporter ||= ->(exception, context) {}
52+
end
53+
54+
def default_instrumentation_callback
55+
@default_instrumentation_callback ||= ->(data) {}
1156
end
1257
end
1358
end

lib/model_context_protocol/content.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def initialize(text, annotations: nil)
1212
end
1313

1414
def to_h
15-
{ text:, annotations: }.compact
15+
{ text:, annotations:, type: "text" }.compact
1616
end
1717
end
1818

@@ -26,7 +26,7 @@ def initialize(data, mime_type, annotations: nil)
2626
end
2727

2828
def to_h
29-
{ data:, mime_type:, annotations: }.compact
29+
{ data:, mime_type:, annotations:, type: "image" }.compact
3030
end
3131
end
3232
end

lib/model_context_protocol/instrumentation.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def instrument_call(method, &block)
1212
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
1313
add_instrumentation_data(duration: end_time - start_time)
1414

15-
ModelContextProtocol.configuration.instrumentation_callback.call(@instrumentation_data)
15+
configuration.instrumentation_callback.call(@instrumentation_data)
1616

1717
result
1818
end

lib/model_context_protocol/prompt.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ def validate_arguments!(args)
114114
private
115115

116116
def required_args
117-
arguments_value.filter_map { |arg| arg.name if arg.required }
117+
arguments_value.filter_map { |arg| arg.name.to_sym if arg.required }
118118
end
119119
end
120120
end

lib/model_context_protocol/server.rb

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,21 @@ def initialize(message, request)
1212
end
1313
end
1414

15-
PROTOCOL_VERSION = "2024-11-05"
15+
PROTOCOL_VERSION = "2025-03-26"
1616

1717
include Instrumentation
1818

19-
attr_accessor :name, :tools, :prompts, :resources, :context
19+
attr_accessor :name, :tools, :prompts, :resources, :context, :configuration
2020

21-
def initialize(name: "model_context_protocol", tools: [], prompts: [], resources: [], context: nil)
21+
def initialize(name: "model_context_protocol", tools: [], prompts: [], resources: [], context: nil,
22+
configuration: nil)
2223
@name = name
2324
@tools = tools.to_h { |t| [t.name_value, t] }
2425
@prompts = prompts.to_h { |p| [p.name_value, p] }
2526
@resources = resources
2627
@resource_index = resources.index_by(&:uri)
2728
@context = context
29+
@configuration = ModelContextProtocol.configuration.merge(configuration)
2830
@handlers = {
2931
"resources/list" => method(:list_resources),
3032
"resources/read" => method(:read_resource),
@@ -186,7 +188,7 @@ def read_resource(request)
186188
end
187189

188190
def report_exception(exception, context = {})
189-
ModelContextProtocol.configuration.exception_reporter.call(exception, context)
191+
configuration.exception_reporter.call(exception, context)
190192
end
191193
end
192194
end

lib/model_context_protocol/tool.rb

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,30 +15,60 @@ def to_h
1515
end
1616
end
1717

18+
class Annotations
19+
attr_reader :title, :read_only_hint, :destructive_hint, :idempotent_hint, :open_world_hint
20+
21+
def initialize(title: nil, read_only_hint: nil, destructive_hint: nil, idempotent_hint: nil, open_world_hint: nil)
22+
@title = title
23+
@read_only_hint = read_only_hint
24+
@destructive_hint = destructive_hint
25+
@idempotent_hint = idempotent_hint
26+
@open_world_hint = open_world_hint
27+
end
28+
29+
def to_h
30+
{
31+
title:,
32+
readOnlyHint: read_only_hint,
33+
destructiveHint: destructive_hint,
34+
idempotentHint: idempotent_hint,
35+
openWorldHint: open_world_hint,
36+
}.compact
37+
end
38+
end
39+
1840
class << self
1941
NOT_SET = Object.new
2042

2143
attr_reader :description_value
2244
attr_reader :input_schema_value
45+
attr_reader :annotations_value
2346

2447
def call(*args, context:)
2548
raise NotImplementedError, "Subclasses must implement call"
2649
end
2750

2851
def to_h
29-
{ name: name_value, description: description_value, inputSchema: input_schema_value }
52+
result = {
53+
name: name_value,
54+
description: description_value,
55+
inputSchema: input_schema_value,
56+
}
57+
result[:annotations] = annotations_value.to_h if annotations_value
58+
result
3059
end
3160

3261
def inherited(subclass)
3362
super
3463
subclass.instance_variable_set(:@name_value, nil)
3564
subclass.instance_variable_set(:@description_value, nil)
3665
subclass.instance_variable_set(:@input_schema_value, nil)
66+
subclass.instance_variable_set(:@annotations_value, nil)
3767
end
3868

3969
def tool_name(value = NOT_SET)
4070
if value == NOT_SET
41-
@name_value
71+
name_value
4272
else
4373
@name_value = value
4474
end
@@ -64,11 +94,20 @@ def input_schema(value = NOT_SET)
6494
end
6595
end
6696

67-
def define(name: nil, description: nil, input_schema: nil, &block)
97+
def annotations(hash = NOT_SET)
98+
if hash == NOT_SET
99+
@annotations_value
100+
else
101+
@annotations_value = Annotations.new(**hash)
102+
end
103+
end
104+
105+
def define(name: nil, description: nil, input_schema: nil, annotations: nil, &block)
68106
Class.new(self) do
69107
tool_name name
70108
description description
71109
input_schema input_schema
110+
self.annotations(annotations) if annotations
72111
define_singleton_method(:call) do |*args, context:|
73112
instance_exec(*args, context:, &block)
74113
end

lib/model_context_protocol/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# frozen_string_literal: true
22

33
module ModelContextProtocol
4-
VERSION = "0.4.0"
4+
VERSION = "0.5.0"
55
end

test/model_context_protocol/instrumentation_test.rb

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,11 @@ module ModelContextProtocol
66
class InstrumentationTest < ActiveSupport::TestCase
77
class Subject
88
include Instrumentation
9-
attr_reader :instrumentation_data_received
9+
attr_reader :instrumentation_data_received, :configuration
1010

1111
def initialize
12-
ModelContextProtocol.configure do |config|
13-
config.instrumentation_callback = ->(data) { @instrumentation_data_received = data }
14-
end
12+
@configuration = ModelContextProtocol::Configuration.new
13+
@configuration.instrumentation_callback = ->(data) { @instrumentation_data_received = data }
1514
end
1615

1716
def instrumented_method

0 commit comments

Comments
 (0)