Skip to content

Commit d2b6a33

Browse files
committed
Internal Release 0.5.1
- Allow protocol version to be set at the server level through configuration instead of a class method, providing more flexibility in protocol version management [#43](Shopify/mcp-ruby#43) @topherbullock - Fixed instrumentation handling to ensure accurate timing information and proper error reporting [#42](Shopify/mcp-ruby#42) @TobiasBales - Improved error handling and reporting in the server - Fixed timing information collection in instrumentation - Added proper error types for different failure scenarios - Enhanced test coverage for error cases - [Allow protocol version to be set at the server level #43](Shopify/mcp-ruby#43) @topherbullock - [Fix instrumentation handling #42](Shopify/mcp-ruby#42) @TobiasBales
1 parent 7175d49 commit d2b6a33

File tree

13 files changed

+248
-86
lines changed

13 files changed

+248
-86
lines changed

.cursor/rules/release-changelogs.mdc

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
description: writing changelog markdown when cutting a new release of the gem
3+
globs:
4+
alwaysApply: false
5+
---
6+
- output the changelog as markdown when asked.
7+
- git tags are used to mark the commit that cut a new release of the gem
8+
- the gem version is located in [version.rb](mdc:lib/model_context_protocol/version.rb)
9+
- use the git history, especially merge commits from PRs to construct the changelog
10+
- when necessary, look at the diff of files changed to determine whether a PR should be listed in
11+
- ## Added; adds new functionality
12+
- ## Changed; alters functionality; especially backward compatible changes
13+
- ## Fixed; bugfixes that are forward compatible
14+
15+
use the following format for changelogs:
16+
17+
https://cloudsmith.io/~shopify/repos/gems/packages/detail/ruby/mcp-ruby/{gem version}/
18+
19+
# Changelog
20+
21+
## Added
22+
- New functionality added that was not present before
23+
24+
## Changed
25+
- Alterations to functionality that may indicate breaking changes
26+
27+
## Fixed
28+
- Bug fixes
29+
30+
#### Full change list:
31+
- [Name of the PR #123](mdc:https:/github.com/Shopify/mcp-ruby/pull/123) @github-author-username
32+
- [Name of the PR #456](mdc:https:/github.com/Shopify/mcp-ruby/pull/456) @another-github-author

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.5.0)
4+
model_context_protocol (0.5.1)
55
json_rpc_handler (~> 0.1)
66

77
GEM

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,22 @@ server = ModelContextProtocol::Server.new(
132132
)
133133
```
134134

135+
### Server Protocol Version
136+
137+
The server's protocol version can be overridden using the `protocol_version` class method:
138+
139+
```ruby
140+
ModelContextProtocol::Server.protocol_version = "2024-11-05"
141+
```
142+
143+
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`:
144+
145+
```ruby
146+
ModelContextProtocol::Server.protocol_version = nil
147+
```
148+
149+
Be sure to check the [MCP spec](https://spec.modelcontextprotocol.io/specification/2024-11-05/) for the protocol version to understand the supported features for the version being set.
150+
135151
### Exception Reporting
136152

137153
The exception reporter receives two arguments:

lib/model_context_protocol.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
require "model_context_protocol/prompt"
1010
require "model_context_protocol/version"
1111
require "model_context_protocol/configuration"
12+
require "model_context_protocol/methods"
1213

1314
module ModelContextProtocol
1415
class << self

lib/model_context_protocol/configuration.rb

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,22 @@
22

33
module ModelContextProtocol
44
class Configuration
5-
attr_writer :exception_reporter, :instrumentation_callback
5+
DEFAULT_PROTOCOL_VERSION = "2025-03-26"
66

7-
def initialize(exception_reporter: nil, instrumentation_callback: nil)
7+
attr_writer :exception_reporter, :instrumentation_callback, :protocol_version
8+
9+
def initialize(exception_reporter: nil, instrumentation_callback: nil, protocol_version: nil)
810
@exception_reporter = exception_reporter
911
@instrumentation_callback = instrumentation_callback
12+
@protocol_version = protocol_version
13+
end
14+
15+
def protocol_version
16+
@protocol_version || DEFAULT_PROTOCOL_VERSION
17+
end
18+
19+
def protocol_version?
20+
!@protocol_version.nil?
1021
end
1122

1223
def exception_reporter
@@ -38,10 +49,16 @@ def merge(other)
3849
else
3950
@instrumentation_callback
4051
end
52+
protocol_version = if other.protocol_version?
53+
other.protocol_version
54+
else
55+
@protocol_version
56+
end
4157

4258
Configuration.new(
4359
exception_reporter:,
4460
instrumentation_callback:,
61+
protocol_version:,
4562
)
4663
end
4764

lib/model_context_protocol/instrumentation.rb

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,19 @@ module ModelContextProtocol
44
module Instrumentation
55
def instrument_call(method, &block)
66
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
7-
@instrumentation_data = {}
8-
add_instrumentation_data(method:)
7+
begin
8+
@instrumentation_data = {}
9+
add_instrumentation_data(method:)
910

10-
result = yield block
11+
result = yield block
1112

12-
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
13-
add_instrumentation_data(duration: end_time - start_time)
13+
result
14+
ensure
15+
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
16+
add_instrumentation_data(duration: end_time - start_time)
1417

15-
configuration.instrumentation_callback.call(@instrumentation_data)
16-
17-
result
18+
configuration.instrumentation_callback.call(@instrumentation_data)
19+
end
1820
end
1921

2022
def add_instrumentation_data(**kwargs)

lib/model_context_protocol/methods.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# frozen_string_literal: true
2+
3+
module ModelContextProtocol
4+
module Methods
5+
RESOURCES_LIST = "resources/list"
6+
RESOURCES_READ = "resources/read"
7+
TOOLS_LIST = "tools/list"
8+
TOOLS_CALL = "tools/call"
9+
PROMPTS_LIST = "prompts/list"
10+
PROMPTS_GET = "prompts/get"
11+
INITIALIZE = "initialize"
12+
PING = "ping"
13+
end
14+
end

lib/model_context_protocol/prompt.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,9 @@ def validate_arguments!(args)
108108
missing = required_args - args.keys
109109
return if missing.empty?
110110

111-
raise ArgumentError, "Missing required arguments: #{missing.join(", ")}"
111+
raise ModelContextProtocol::Server::RequestHandlerError.new(
112+
"Missing required arguments: #{missing.join(", ")}", nil, error_type: :missing_required_arguments
113+
)
112114
end
113115

114116
private

lib/model_context_protocol/server.rb

Lines changed: 62 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,22 @@
22

33
require "json_rpc_handler"
44
require_relative "instrumentation"
5+
require_relative "methods"
56

67
module ModelContextProtocol
78
class Server
89
class RequestHandlerError < StandardError
9-
def initialize(message, request)
10+
attr_reader :error_type
11+
attr_reader :original_error
12+
13+
def initialize(message, request, error_type: :internal_error, original_error: nil)
1014
super(message)
1115
@request = request
16+
@error_type = error_type
17+
@original_error = original_error
1218
end
1319
end
1420

15-
PROTOCOL_VERSION = "2025-03-26"
16-
1721
include Instrumentation
1822

1923
attr_accessor :name, :tools, :prompts, :resources, :context, :configuration
@@ -28,14 +32,14 @@ def initialize(name: "model_context_protocol", tools: [], prompts: [], resources
2832
@context = context
2933
@configuration = ModelContextProtocol.configuration.merge(configuration)
3034
@handlers = {
31-
"resources/list" => method(:list_resources),
32-
"resources/read" => method(:read_resource),
33-
"tools/list" => method(:list_tools),
34-
"tools/call" => method(:call_tool),
35-
"prompts/list" => method(:list_prompts),
36-
"prompts/get" => method(:get_prompt),
37-
"initialize" => method(:init),
38-
"ping" => ->(_) { {} },
35+
Methods::RESOURCES_LIST => method(:list_resources),
36+
Methods::RESOURCES_READ => method(:read_resource),
37+
Methods::TOOLS_LIST => method(:list_tools),
38+
Methods::TOOLS_CALL => method(:call_tool),
39+
Methods::PROMPTS_LIST => method(:list_prompts),
40+
Methods::PROMPTS_GET => method(:get_prompt),
41+
Methods::INITIALIZE => method(:init),
42+
Methods::PING => ->(_) { {} },
3943
}
4044
end
4145

@@ -52,47 +56,61 @@ def handle_json(request)
5256
end
5357

5458
def resources_list_handler(&block)
55-
@handlers["resources/list"] = block
59+
@handlers[Methods::RESOURCES_LIST] = block
5660
end
5761

5862
def resources_read_handler(&block)
59-
@handlers["resources/read"] = block
63+
@handlers[Methods::RESOURCES_READ] = block
6064
end
6165

6266
def tools_list_handler(&block)
63-
@handlers["tools/list"] = block
67+
@handlers[Methods::TOOLS_LIST] = block
6468
end
6569

6670
def tools_call_handler(&block)
67-
@handlers["tools/call"] = block
71+
@handlers[Methods::TOOLS_CALL] = block
6872
end
6973

7074
def prompts_list_handler(&block)
71-
@handlers["prompts/list"] = block
75+
@handlers[Methods::PROMPTS_LIST] = block
7276
end
7377

7478
def prompts_get_handler(&block)
75-
@handlers["prompts/get"] = block
79+
@handlers[Methods::PROMPTS_GET] = block
7680
end
7781

7882
private
7983

8084
def handle_request(request, method)
81-
instrument_call(method) do
82-
case method
83-
when "tools/list"
84-
->(params) { { tools: @handlers["tools/list"].call(params) } }
85-
when "prompts/list"
86-
->(params) { { prompts: @handlers["prompts/list"].call(params) } }
87-
when "resources/list"
88-
->(params) { { resources: @handlers["resources/list"].call(params) } }
89-
else
90-
@handlers[method]
91-
end
92-
rescue => e
93-
report_exception(e, { request: request })
94-
raise RequestHandlerError.new("Internal error handling #{request[:method]} request", request)
85+
handler = @handlers[method]
86+
unless handler
87+
instrument_call("unsupported_method") {}
88+
return
9589
end
90+
91+
->(params) {
92+
instrument_call(method) do
93+
case method
94+
when Methods::TOOLS_LIST
95+
{ tools: @handlers[Methods::TOOLS_LIST].call(params) }
96+
when Methods::PROMPTS_LIST
97+
{ prompts: @handlers[Methods::PROMPTS_LIST].call(params) }
98+
when Methods::RESOURCES_LIST
99+
{ resources: @handlers[Methods::RESOURCES_LIST].call(params) }
100+
else
101+
@handlers[method].call(params)
102+
end
103+
rescue => e
104+
report_exception(e, { request: request })
105+
if e.is_a?(RequestHandlerError)
106+
add_instrumentation_data(error: e.error_type)
107+
raise e
108+
end
109+
110+
add_instrumentation_data(error: :internal_error)
111+
raise RequestHandlerError.new("Internal error handling #{method} request", request, original_error: e)
112+
end
113+
}
96114
end
97115

98116
def capabilities
@@ -111,52 +129,49 @@ def server_info
111129
end
112130

113131
def init(request)
114-
add_instrumentation_data(method: "initialize")
132+
add_instrumentation_data(method: Methods::INITIALIZE)
115133
{
116-
protocolVersion: PROTOCOL_VERSION,
134+
protocolVersion: configuration.protocol_version,
117135
capabilities: capabilities,
118136
serverInfo: server_info,
119137
}
120138
end
121139

122140
def list_tools(request)
123-
add_instrumentation_data(method: "tools/list")
141+
add_instrumentation_data(method: Methods::TOOLS_LIST)
124142
@tools.map { |_, tool| tool.to_h }
125143
end
126144

127145
def call_tool(request)
128-
add_instrumentation_data(method: "tools/call")
146+
add_instrumentation_data(method: Methods::TOOLS_CALL)
129147
tool_name = request[:name]
130148
tool = tools[tool_name]
131149
unless tool
132150
add_instrumentation_data(error: :tool_not_found)
133-
raise "Tool not found #{tool_name}"
151+
raise RequestHandlerError.new("Tool not found #{tool_name}", request, error_type: :tool_not_found)
134152
end
135153

136154
add_instrumentation_data(tool_name:)
137155

138156
begin
139-
result = tool.call(**request[:arguments], context:)
140-
result.to_h
157+
tool.call(**request[:arguments], context:).to_h
141158
rescue => e
142-
report_exception(e, { tool_name: tool_name, arguments: request[:arguments] })
143-
add_instrumentation_data(error: :internal_error)
144-
raise RequestHandlerError.new("Internal error calling tool #{tool_name}", request)
159+
raise RequestHandlerError.new("Internal error calling tool #{tool_name}", request, original_error: e)
145160
end
146161
end
147162

148163
def list_prompts(request)
149-
add_instrumentation_data(method: "prompts/list")
164+
add_instrumentation_data(method: Methods::PROMPTS_LIST)
150165
@prompts.map { |_, prompt| prompt.to_h }
151166
end
152167

153168
def get_prompt(request)
154-
add_instrumentation_data(method: "prompts/get")
169+
add_instrumentation_data(method: Methods::PROMPTS_GET)
155170
prompt_name = request[:name]
156171
prompt = @prompts[prompt_name]
157172
unless prompt
158173
add_instrumentation_data(error: :prompt_not_found)
159-
raise "Prompt not found #{prompt_name}"
174+
raise RequestHandlerError.new("Prompt not found #{prompt_name}", request, error_type: :prompt_not_found)
160175
end
161176

162177
add_instrumentation_data(prompt_name:)
@@ -168,19 +183,19 @@ def get_prompt(request)
168183
end
169184

170185
def list_resources(request)
171-
add_instrumentation_data(method: "resources/list")
186+
add_instrumentation_data(method: Methods::RESOURCES_LIST)
172187

173188
@resources.map(&:to_h)
174189
end
175190

176191
def read_resource(request)
177-
add_instrumentation_data(method: "resources/read")
192+
add_instrumentation_data(method: Methods::RESOURCES_READ)
178193
resource_uri = request[:uri]
179194

180195
resource = @resource_index[resource_uri]
181196
unless resource
182197
add_instrumentation_data(error: :resource_not_found)
183-
raise "Resource not found #{resource_uri}"
198+
raise RequestHandlerError.new("Resource not found #{resource_uri}", request, error_type: :resource_not_found)
184199
end
185200

186201
add_instrumentation_data(resource_uri:)

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.5.0"
4+
VERSION = "0.5.1"
55
end

0 commit comments

Comments
 (0)