Skip to content

Commit 2eca368

Browse files
committed
Internal Release 0.4.0
- Added support for handle_json method from JSON RPC gem to allow consumers to pass JSON strings - Added context parameter to prompt templating system - Updated tool and prompt registration to use class references instead of instances - Changed tool response is_error to isError to match protocol specification
1 parent 0645351 commit 2eca368

File tree

9 files changed

+306
-151
lines changed

9 files changed

+306
-151
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.3.0)
4+
model_context_protocol (0.4.0)
55
json_rpc_handler (~> 0.1)
66

77
GEM

README.md

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,31 @@ module ModelContextProtocol
4646
def index
4747
server = ModelContextProtocol::Server.new(
4848
name: "my_server",
49-
tools: [someTool, anotherTool],
50-
prompts: [myPrompt],
51-
context: nil,
49+
tools: [SomeTool, AnotherTool],
50+
prompts: [MyPrompt],
51+
context: { user_id: current_user.id },
5252
)
53-
render(json: server.handle(request.body.read).to_h)
53+
render(json: server.handle_json(request.body.read).to_h)
54+
end
55+
end
56+
end
57+
```
58+
59+
or, if you want/need to parse the json yourself you can do the following
60+
61+
```ruby
62+
module ModelContextProtocol
63+
class ApplicationController < ActionController::Base
64+
65+
sig { void }
66+
def index
67+
server = ModelContextProtocol::Server.new(
68+
name: "my_server",
69+
tools: [SomeTool, AnotherTool],
70+
prompts: [MyPrompt],
71+
context: { user_id: current_user.id },
72+
)
73+
render(json: server.handle(JSON.parse(request.body.read)).to_h)
5474
end
5575
end
5676
end
@@ -77,16 +97,19 @@ end
7797
### Exception Reporting
7898

7999
The exception reporter receives two arguments:
100+
80101
- `exception`: The Ruby exception object that was raised
81102
- `context`: A hash containing contextual information about where the error occurred
82103

83104
The context hash includes:
105+
84106
- For tool calls: `{ tool_name: "name", arguments: { ... } }`
85107
- For general request handling: `{ request: { ... } }`
86108

87109
When an exception occurs:
110+
88111
1. The exception is reported via the configured reporter
89-
2. For tool calls, a generic error response is returned to the client: `{ error: "Internal error occurred", is_error: true }`
112+
2. For tool calls, a generic error response is returned to the client: `{ error: "Internal error occurred", isError: true }`
90113
3. For other requests, the exception is re-raised after reporting
91114

92115
If no exception reporter is configured, a default no-op reporter is used that silently ignores exceptions.
@@ -104,12 +127,12 @@ class MyTool < ModelContextProtocol::Tool
104127
description "This tool performs specific functionality..."
105128
input_schema [{ type: "text", name: "message" }]
106129

107-
def call(message, context:)
130+
def self.call(message, context:)
108131
Tool::Response.new([{ type: "text", content: "OK" }])
109132
end
110133
end
111134

112-
tool = MyTool.new
135+
tool = MyTool
113136
```
114137

115138
2. By using the `ModelContextProtocol::Tool.define` method with a block:
@@ -143,22 +166,26 @@ class MyPrompt < ModelContextProtocol::Prompt
143166
)
144167
]
145168

146-
def template(args)
147-
Prompt::Result.new(
148-
description: "Response description",
149-
messages: [
150-
Prompt::Message.new(
151-
role: "user",
152-
content: Content::Text.new("User message")
153-
),
154-
Prompt::Message.new(
155-
role: "assistant",
156-
content: Content::Text.new(args["message"])
157-
)
158-
]
159-
)
169+
class << self
170+
def template(args, context:)
171+
Prompt::Result.new(
172+
description: "Response description",
173+
messages: [
174+
Prompt::Message.new(
175+
role: "user",
176+
content: Content::Text.new("User message")
177+
),
178+
Prompt::Message.new(
179+
role: "assistant",
180+
content: Content::Text.new(args["message"])
181+
)
182+
]
183+
)
184+
end
160185
end
161186
end
187+
188+
prompt = MyPrompt
162189
```
163190

164191
2. Using the `ModelContextProtocol::Prompt.define` method:
@@ -174,7 +201,7 @@ prompt = ModelContextProtocol::Prompt.define(
174201
required: true
175202
)
176203
]
177-
) do |args|
204+
) do |args, context:|
178205
Prompt::Result.new(
179206
description: "Response description",
180207
messages: [
@@ -191,6 +218,9 @@ prompt = ModelContextProtocol::Prompt.define(
191218
end
192219
```
193220

221+
The context parameter is the context passed into the server and can be used to pass per request information,
222+
e.g. around authentication state or user preferences.
223+
194224
### Key Components
195225

196226
- `Prompt::Argument` - Defines input parameters for the prompt template
@@ -205,8 +235,8 @@ Register prompts with the MCP server:
205235
```ruby
206236
server = ModelContextProtocol::Server.new(
207237
name: "my_server",
208-
prompts: [MyPrompt.new],
209-
context: nil,
238+
prompts: [MyPrompt],
239+
context: { user_id: current_user.id },
210240
)
211241
```
212242

lib/model_context_protocol/prompt.rb

Lines changed: 43 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -45,68 +45,77 @@ def to_h
4545
end
4646

4747
class << self
48+
NOT_SET = Object.new
49+
4850
attr_reader :description_value
4951
attr_reader :arguments_value
5052

53+
def template(args, context:)
54+
raise NotImplementedError, "Subclasses must implement template"
55+
end
56+
57+
def to_h
58+
{ name: name_value, description: description_value, arguments: arguments_value.map(&:to_h) }.compact
59+
end
60+
5161
def inherited(subclass)
5262
super
5363
subclass.instance_variable_set(:@name_value, nil)
5464
subclass.instance_variable_set(:@description_value, nil)
5565
subclass.instance_variable_set(:@arguments_value, nil)
5666
end
5767

58-
def prompt_name(value)
59-
@name_value = value
68+
def prompt_name(value = NOT_SET)
69+
if value == NOT_SET
70+
@name_value
71+
else
72+
@name_value = value
73+
end
6074
end
6175

6276
def name_value
6377
@name_value || StringUtils.handle_from_class_name(name)
6478
end
6579

66-
def description(value)
67-
@description_value = value
80+
def description(value = NOT_SET)
81+
if value == NOT_SET
82+
@description_value
83+
else
84+
@description_value = value
85+
end
6886
end
6987

70-
def arguments(value)
71-
@arguments_value = value
88+
def arguments(value = NOT_SET)
89+
if value == NOT_SET
90+
@arguments_value
91+
else
92+
@arguments_value = value
93+
end
7294
end
7395

7496
def define(name: nil, description: nil, arguments: [], &block)
75-
new(name:, description:, arguments:).tap do |prompt|
76-
prompt.define_singleton_method(:template) do |args|
77-
instance_exec(args, &block)
97+
Class.new(self) do
98+
prompt_name name
99+
description description
100+
arguments arguments
101+
define_singleton_method(:template) do |args, context:|
102+
instance_exec(args, context:, &block)
78103
end
79104
end
80105
end
81-
end
82106

83-
attr_reader :name, :description, :arguments
107+
def validate_arguments!(args)
108+
missing = required_args - args.keys
109+
return if missing.empty?
84110

85-
def initialize(name: nil, description: nil, arguments: nil)
86-
@name = name || self.class.name_value
87-
@description = description || self.class.description_value
88-
@arguments = arguments || self.class.arguments_value
89-
end
90-
91-
def template(args)
92-
raise NotImplementedError, "Prompt subclasses must implement template"
93-
end
94-
95-
def validate_arguments!(args)
96-
missing = required_args - args.keys
97-
return if missing.empty?
98-
99-
raise ArgumentError, "Missing required arguments: #{missing.join(", ")}"
100-
end
101-
102-
def to_h
103-
{ name:, description:, arguments: arguments.map(&:to_h) }.compact
104-
end
111+
raise ArgumentError, "Missing required arguments: #{missing.join(", ")}"
112+
end
105113

106-
private
114+
private
107115

108-
def required_args
109-
arguments.filter_map { |arg| arg.name if arg.required }
116+
def required_args
117+
arguments_value.filter_map { |arg| arg.name if arg.required }
118+
end
110119
end
111120
end
112121
end

lib/model_context_protocol/server.rb

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ def initialize(message, request)
2020

2121
def initialize(name: "model_context_protocol", tools: [], prompts: [], resources: [], context: nil)
2222
@name = name
23-
@tools = tools.to_h { |t| [t.name, t] }
24-
@prompts = prompts.to_h { |p| [p.name, p] }
23+
@tools = tools.to_h { |t| [t.name_value, t] }
24+
@prompts = prompts.to_h { |p| [p.name_value, p] }
2525
@resources = resources
2626
@resource_index = resources.index_by(&:uri)
2727
@context = context
@@ -39,21 +39,13 @@ def initialize(name: "model_context_protocol", tools: [], prompts: [], resources
3939

4040
def handle(request)
4141
JsonRpcHandler.handle(request) do |method|
42-
instrument_call(method) do
43-
case method
44-
when "tools/list"
45-
->(params) { { tools: @handlers["tools/list"].call(params) } }
46-
when "prompts/list"
47-
->(params) { { prompts: @handlers["prompts/list"].call(params) } }
48-
when "resources/list"
49-
->(params) { { resources: @handlers["resources/list"].call(params) } }
50-
else
51-
@handlers[method]
52-
end
53-
rescue => e
54-
report_exception(e, { request: request })
55-
raise RequestHandlerError.new("Internal error handling #{request[:method]} request", request)
56-
end
42+
handle_request(request, method)
43+
end
44+
end
45+
46+
def handle_json(request)
47+
JsonRpcHandler.handle_json(request) do |method|
48+
handle_request(request, method)
5749
end
5850
end
5951

@@ -83,6 +75,24 @@ def prompts_get_handler(&block)
8375

8476
private
8577

78+
def handle_request(request, method)
79+
instrument_call(method) do
80+
case method
81+
when "tools/list"
82+
->(params) { { tools: @handlers["tools/list"].call(params) } }
83+
when "prompts/list"
84+
->(params) { { prompts: @handlers["prompts/list"].call(params) } }
85+
when "resources/list"
86+
->(params) { { resources: @handlers["resources/list"].call(params) } }
87+
else
88+
@handlers[method]
89+
end
90+
rescue => e
91+
report_exception(e, { request: request })
92+
raise RequestHandlerError.new("Internal error handling #{request[:method]} request", request)
93+
end
94+
end
95+
8696
def capabilities
8797
@capabilities ||= {
8898
prompts: {},
@@ -152,7 +162,7 @@ def get_prompt(request)
152162
prompt_args = request[:arguments]
153163
prompt.validate_arguments!(prompt_args)
154164

155-
prompt.template(prompt_args).to_h
165+
prompt.template(prompt_args, context:).to_h
156166
end
157167

158168
def list_resources(request)
@@ -172,7 +182,6 @@ def read_resource(request)
172182
end
173183

174184
add_instrumentation_data(resource_uri:)
175-
176185
resource.to_h
177186
end
178187

0 commit comments

Comments
 (0)