model-context-protocol-rb 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 99ee11ebae0c9c984a15954b386e89489f6715a1d371b288ece0438d066436d7
4
- data.tar.gz: cdf41e8742941e9a0a41c714127d02f5a189d901a95a35e6f61a16f1f7b8a704
3
+ metadata.gz: 5cce144594a63393d124f7edc41428730824f2e5df282b21458b506d1ac59376
4
+ data.tar.gz: f7a1de949d083fbcf9df57f500cad32869aa2c42b7dda17726509bab58ffc0b2
5
5
  SHA512:
6
- metadata.gz: aad64bd6e42b2ecea2b742bb23b902ea1bfae3e77cffc5009b0a83712df7efac417b680bb460649bb73fff31d2d554d33a17d6fe96adb35f3cc9d0addbbcf9be
7
- data.tar.gz: 6fa834a1c2bf4494748474dc26a29bede650b1a1913c7c543a7c195fd24fe99eda5f1610c40a63067f454b50f669b1fb47030e106fa6ba80cbde522bccfb9518
6
+ metadata.gz: f374a5e4cf00d1f905f86c51be35c812b6074c188dca28e2724a9e478afdba641337b4f17404ed44cdb22a86baf88dbcaf03df4896e5b039f1717eb3c8afbb71
7
+ data.tar.gz: 8bb5dd4e584f9df32265d1a88d9469e73b2c70225a88da510240bb5b7f80ef4f94b13738ed66a1f1bcec39012832efc0032d72d7ff93ede737ec31d737589243
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.3] - 2025-09-02
4
+
5
+ - (Breaking) Added logging support.
6
+ - Requires updating the `enable_log` configuration option to `logging_enabled`.
7
+ - Added experimental Streamable HTTP transport.
8
+ - (Breaking) Renamed params to arguments in prompts, resources, and tools.
9
+ - Requires updating all references to `params` in prompts, resources, and tools to `arguments` with symbolized keys.
10
+ - Improved ergonomics of completions and resource templates.
11
+ - Added support for providing context to prompts, resources, and tools.
12
+
3
13
  ## [0.3.2] - 2025-05-10
4
14
 
5
15
  - Added resource template support.
@@ -37,8 +47,9 @@
37
47
 
38
48
  - Initial release
39
49
 
40
- [Unreleased]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.3.2...HEAD
41
- [0.3.1]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.3.1...v0.3.2
50
+ [Unreleased]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.3.3...HEAD
51
+ [0.3.3]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.3.2...v0.3.3
52
+ [0.3.2]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.3.1...v0.3.2
42
53
  [0.3.1]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.3.0...v0.3.1
43
54
  [0.3.0]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.2.0...v0.3.0
44
55
  [0.2.0]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.1.0...v0.2.0
data/README.md CHANGED
@@ -30,7 +30,7 @@ Build a simple MCP server by registering your prompts, resources, resource templ
30
30
  server = ModelContextProtocol::Server.new do |config|
31
31
  config.name = "MCP Development Server"
32
32
  config.version = "1.0.0"
33
- config.enable_log = true
33
+ config.logging_enabled = true
34
34
 
35
35
  # Environment Variables - https://modelcontextprotocol.io/docs/tools/debugging#environment-variables
36
36
  # Require specific environment variables to be set
@@ -39,6 +39,12 @@ server = ModelContextProtocol::Server.new do |config|
39
39
  # Set environment variables programmatically
40
40
  config.set_environment_variable("DEBUG_MODE", "true")
41
41
 
42
+ # Provide prompts, resources, and tools with contextual variables
43
+ config.context = {
44
+ user_id: "123456",
45
+ request_id: SecureRandom.uuid
46
+ }
47
+
42
48
  config.registry = ModelContextProtocol::Server::Registry.new do
43
49
  prompts list_changed: true do
44
50
  register TestPrompt
@@ -61,45 +67,159 @@ end
61
67
  server.start
62
68
  ```
63
69
 
70
+ ### Transport Configuration
71
+
72
+ The MCP server supports different transport mechanisms for communication with clients. By default, it uses stdio (standard input/output), but you can also configure it to use streamable HTTP transport for distributed deployments.
73
+
74
+ #### Stdio Transport (Default)
75
+
76
+ When no transport is specified, the server uses stdio transport, which is suitable for single-process communication:
77
+
78
+ ```ruby
79
+ server = ModelContextProtocol::Server.new do |config|
80
+ config.name = "MCP Development Server"
81
+ config.version = "1.0.0"
82
+ # No transport specified - uses stdio by default
83
+ config.registry = ModelContextProtocol::Server::Registry.new
84
+ end
85
+
86
+ server.start
87
+ ```
88
+
89
+ #### Streamable HTTP Transport
90
+
91
+ For distributed deployments with load balancers and multiple server instances, use the streamable HTTP transport with Redis-backed session management:
92
+
93
+ ```ruby
94
+ require 'redis'
95
+
96
+ server = ModelContextProtocol::Server.new do |config|
97
+ config.name = "MCP Development Server"
98
+ config.version = "1.0.0"
99
+
100
+ # Configure streamable HTTP transport
101
+ config.transport = {
102
+ type: :streamable_http,
103
+ redis_client: Redis.new(url: ENV['REDIS_URL']),
104
+ session_ttl: 3600 # Optional: session timeout in seconds (default: 3600)
105
+ }
106
+
107
+ config.registry = ModelContextProtocol::Server::Registry.new
108
+ end
109
+
110
+ # For HTTP frameworks, handle the request and return the response
111
+ result = server.start
112
+ # result will be a hash like: {json: {...}, status: 200, headers: {...}}
113
+ ```
114
+
115
+ **Key Features:**
116
+ - **Distributed Sessions**: Redis-backed session storage enables multiple server instances
117
+ - **Load Balancer Support**: Sessions persist across different server instances
118
+ - **HTTP Methods**: Supports POST (requests), GET (Server-Sent Events), DELETE (cleanup)
119
+ - **Cross-Server Routing**: Messages are routed between servers via Redis pub/sub
120
+
121
+ **Integration Example (Rails):**
122
+
123
+ ```ruby
124
+ class McpController < ApplicationController
125
+ def handle
126
+ server = ModelContextProtocol::Server.new do |config|
127
+ config.name = "Rails MCP Server"
128
+ config.version = "1.0.0"
129
+ config.transport = {
130
+ type: :streamable_http,
131
+ redis_client: Redis.new(url: ENV['REDIS_URL']),
132
+ request: request,
133
+ response: response
134
+ }
135
+ config.registry = build_registry
136
+ end
137
+
138
+ result = server.start
139
+ render json: result[:json], status: result[:status], headers: result[:headers]
140
+ end
141
+ end
142
+ ```
143
+
64
144
  Messages from the MCP client will be routed to the appropriate custom handler. This SDK provides several classes that should be used to build your handlers.
65
145
 
146
+ ### Server features
147
+
66
148
  #### Prompts
67
149
 
68
150
  The `ModelContextProtocol::Server::Prompt` base class allows subclasses to define a prompt that the MCP client can use. Define the [appropriate metadata](https://spec.modelcontextprotocol.io/specification/2024-11-05/server/prompts/) in the `with_metadata` block.
69
151
 
70
- Define any arguments using the `with_argument` block. You can mark an argument as required, and you can optionally provide the class name of a service object that provides completions. See [Completions](#completions) for more information.
152
+ Define any arguments using the `with_argument` block. You can mark an argument as required, and you can optionally provide a completion class. See [Completions](#completions) for more information.
153
+
154
+ Then implement the `call` method to build your prompt. Any arguments passed to the tool from the MCP client will be available in the `arguments` hash with symbol keys (e.g., `arguments[:argument_name]`), and any context values provided in the server configuration will be available in the `context` hash. Use the `respond_with` instance method to ensure your prompt responds with appropriately formatted response data.
71
155
 
72
- Then implement the `call` method to build your prompt. Use the `respond_with` instance method to ensure your prompt responds with appropriately formatted response data.
156
+ You can also log from within your prompt by calling a valid logger level method on the `logger` and passing a string message.
73
157
 
74
158
  This is an example prompt that returns a properly formatted response:
75
159
 
76
160
  ```ruby
77
161
  class TestPrompt < ModelContextProtocol::Server::Prompt
162
+ ToneCompletion = ModelContextProtocol::Server::Completion.define do
163
+ hints = ["whiny", "angry", "callous", "desperate", "nervous", "sneaky"]
164
+ values = hints.grep(/#{argument_value}/)
165
+
166
+ respond_with values:
167
+ end
168
+
78
169
  with_metadata do
79
- name "test_prompt"
80
- description "A test prompt"
170
+ name "brainstorm_excuses"
171
+ description "A prompt for brainstorming excuses to get out of something"
81
172
  end
82
173
 
83
174
  with_argument do
84
- name "message"
85
- description "The thing to do"
175
+ name "undesirable_activity"
176
+ description "The thing to get out of"
86
177
  required true
87
- completion TestCompletion
88
178
  end
89
179
 
90
180
  with_argument do
91
- name "other"
92
- description "Another thing to do"
181
+ name "tone"
182
+ description "The general tone to be used in the generated excuses"
93
183
  required false
184
+ completion ToneCompletion
94
185
  end
95
186
 
96
187
  def call
188
+ logger.info("Brainstorming excuses...")
97
189
  messages = [
98
190
  {
99
191
  role: "user",
100
192
  content: {
101
193
  type: "text",
102
- text: "Do this: #{params["message"]}"
194
+ text: "My wife wants me to: #{arguments[:undesirable_activity]}... Can you believe it?"
195
+ }
196
+ },
197
+ {
198
+ role: "assistant",
199
+ content: {
200
+ type: "text",
201
+ text: "Oh, that's just downright awful. What are you going to do?"
202
+ }
203
+ },
204
+ {
205
+ role: "user",
206
+ content: {
207
+ type: "text",
208
+ text: "Well, I'd like to get out of it, but I'm going to need your help."
209
+ }
210
+ },
211
+ {
212
+ role: "assistant",
213
+ content: {
214
+ type: "text",
215
+ text: "Anything for you."
216
+ }
217
+ },
218
+ {
219
+ role: "user",
220
+ content: {
221
+ type: "text",
222
+ text: "Can you generate some excuses for me?" + (arguments[:tone] ? "Make them as #{arguments[:tone]} as possible." : "")
103
223
  }
104
224
  }
105
225
  ]
@@ -113,21 +233,35 @@ end
113
233
 
114
234
  The `ModelContextProtocol::Server::Resource` base class allows subclasses to define a resource that the MCP client can use. Define the [appropriate metadata](https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/) in the `with_metadata` block.
115
235
 
116
- Then, implement the `call` method to build your resource. Use the `respond_with` instance method to ensure your resource responds with appropriately formatted response data.
236
+ Then, implement the `call` method to build your resource. Any context values provided in the server configuration will be available in the `context` hash. Use the `respond_with` instance method to ensure your resource responds with appropriately formatted response data.
237
+
238
+ You can also log from within your resource by calling a valid logger level method on the `logger` and passing a string message.
117
239
 
118
240
  This is an example resource that returns a text response:
119
241
 
120
242
  ```ruby
121
243
  class TestResource < ModelContextProtocol::Server::Resource
122
244
  with_metadata do
123
- name "Test Resource"
124
- description "A test resource"
245
+ name "top-secret-plans.txt"
246
+ description "Top secret plans to do top secret things"
125
247
  mime_type "text/plain"
126
- uri "resource://test-resource"
248
+ uri "file:///top-secret-plans.txt"
127
249
  end
128
250
 
129
251
  def call
130
- respond_with :text, text: "Here's the data"
252
+ unless authorized?(context[:user_id])
253
+ logger.info("This fool thinks he can get my top secret plans...")
254
+ return respond_with :text, text: "Nothing to see here, move along."
255
+ end
256
+
257
+ respond_with :text, text: "I'm finna eat all my wife's leftovers."
258
+ end
259
+
260
+ private
261
+
262
+ def authorized?(user_id)
263
+ authorized_users = ["42", "123456"]
264
+ authorized_users.any?(user_id)
131
265
  end
132
266
  end
133
267
  ```
@@ -137,14 +271,15 @@ This is an example resource that returns binary data:
137
271
  ```ruby
138
272
  class TestBinaryResource < ModelContextProtocol::Server::Resource
139
273
  with_metadata do
140
- name "Project Logo"
274
+ name "project-logo.png"
141
275
  description "The logo for the project"
142
- mime_type "image/jpeg"
143
- uri "resource://project-logo"
276
+ mime_type "image/png"
277
+ uri "file:///project-logo.png"
144
278
  end
145
279
 
146
280
  def call
147
281
  # In a real implementation, we would retrieve the binary resource
282
+ # This is a small valid base64 encoded string (represents "test")
148
283
  data = "dGVzdA=="
149
284
  respond_with :binary, blob: data
150
285
  end
@@ -158,24 +293,22 @@ The `ModelContextProtocol::Server::ResourceTemplate` base class allows subclasse
158
293
  This is an example resource template that provides a completion for a parameter of the URI template:
159
294
 
160
295
  ```ruby
161
- class TestResourceTemplateCompletion < ModelContextProtocol::Server::Completion
162
- def call
296
+ class TestResourceTemplate < ModelContextProtocol::Server::ResourceTemplate
297
+ Completion = ModelContextProtocol::Server::Completion.define do
163
298
  hints = {
164
- "name" => ["test-resource", "project-logo"]
299
+ "name" => ["top-secret-plans.txt"]
165
300
  }
166
301
  values = hints[argument_name].grep(/#{argument_value}/)
167
302
 
168
303
  respond_with values:
169
304
  end
170
- end
171
305
 
172
- class TestResourceTemplate < ModelContextProtocol::Server::ResourceTemplate
173
306
  with_metadata do
174
- name "Test Resource Template"
175
- description "A test resource template"
307
+ name "project-document-resource-template"
308
+ description "A resource template for retrieving project documents"
176
309
  mime_type "text/plain"
177
- uri_template "resource://{name}" do
178
- completion :name, TestResourceTemplateCompletion
310
+ uri_template "file:///{name}" do
311
+ completion :name, Completion
179
312
  end
180
313
  end
181
314
  end
@@ -185,7 +318,9 @@ end
185
318
 
186
319
  The `ModelContextProtocol::Server::Tool` base class allows subclasses to define a tool that the MCP client can use. Define the [appropriate metadata](https://spec.modelcontextprotocol.io/specification/2024-11-05/server/tools/) in the `with_metadata` block.
187
320
 
188
- Then implement the `call` method to build your tool. Use the `respond_with` instance method to ensure your tool responds with appropriately formatted response data.
321
+ Then, implement the `call` method to build your tool. Any arguments passed to the tool from the MCP client will be available in the `arguments` hash with symbol keys (e.g., `arguments[:argument_name]`), and any context values provided in the server configuration will be available in the `context` hash. Use the `respond_with` instance method to ensure your tool responds with appropriately formatted response data.
322
+
323
+ You can also log from within your tool by calling a valid logger level method on the `logger` and passing a string message.
189
324
 
190
325
  This is an example tool that returns a text response:
191
326
 
@@ -208,9 +343,12 @@ class TestToolWithTextResponse < ModelContextProtocol::Server::Tool
208
343
  end
209
344
 
210
345
  def call
211
- number = params["number"].to_i
212
- result = number * 2
213
- respond_with :text, text: "#{number} doubled is #{result}"
346
+ user_id = context[:user_id]
347
+ number = arguments[:number].to_i
348
+ logger.info("Silly user doesn't know how to double a number")
349
+ calculation = number * 2
350
+ salutation = user_id ? "User #{user_id}, " : ""
351
+ respond_with :text, text: salutation << "#{number} doubled is #{calculation}"
214
352
  end
215
353
  end
216
354
  ```
@@ -242,7 +380,7 @@ class TestToolWithImageResponse < ModelContextProtocol::Server::Tool
242
380
 
243
381
  def call
244
382
  # Map format to mime type
245
- mime_type = case params["format"].downcase
383
+ mime_type = case arguments[:format].downcase
246
384
  when "svg"
247
385
  "image/svg+xml"
248
386
  when "jpg", "jpeg"
@@ -311,7 +449,7 @@ class TestToolWithResourceResponse < ModelContextProtocol::Server::Tool
311
449
  end
312
450
 
313
451
  def call
314
- title = params["title"].downcase
452
+ title = arguments[:title].downcase
315
453
  # In a real implementation, we would do a lookup to get the document data
316
454
  document = "richtextdata"
317
455
  respond_with :resource, uri: "resource://document/#{title}", text: document, mime_type: "application/rtf"
@@ -15,6 +15,12 @@ module ModelContextProtocol
15
15
  new(...).call
16
16
  end
17
17
 
18
+ def self.define(&block)
19
+ Class.new(self) do
20
+ define_method(:call, &block)
21
+ end
22
+ end
23
+
18
24
  private
19
25
 
20
26
  Response = Data.define(:values, :total, :hasMore) do
@@ -1,3 +1,5 @@
1
+ require_relative "mcp_logger"
2
+
1
3
  module ModelContextProtocol
2
4
  class Server::Configuration
3
5
  # Raised when configured with invalid name.
@@ -12,16 +14,74 @@ module ModelContextProtocol
12
14
  # Raised when a required environment variable is not set
13
15
  class MissingRequiredEnvironmentVariable < StandardError; end
14
16
 
15
- attr_accessor :enable_log, :name, :registry, :version
17
+ # Raised when transport configuration is invalid
18
+ class InvalidTransportError < StandardError; end
19
+
20
+ # Raised when an invalid log level is provided
21
+ class InvalidLogLevelError < StandardError; end
22
+
23
+ # Valid MCP log levels per the specification
24
+ VALID_LOG_LEVELS = %w[debug info notice warning error critical alert emergency].freeze
25
+
26
+ attr_accessor :name, :registry, :version, :transport
27
+ attr_reader :logger
28
+
29
+ def initialize
30
+ # Always create a logger - enabled by default
31
+ @logging_enabled = true
32
+ @default_log_level = "info"
33
+ @logger = ModelContextProtocol::Server::MCPLogger.new(
34
+ logger_name: "server",
35
+ level: @default_log_level,
36
+ enabled: @logging_enabled
37
+ )
38
+ end
16
39
 
17
40
  def logging_enabled?
18
- enable_log || false
41
+ @logging_enabled
42
+ end
43
+
44
+ def logging_enabled=(value)
45
+ @logging_enabled = value
46
+ @logger = ModelContextProtocol::Server::MCPLogger.new(
47
+ logger_name: "server",
48
+ level: @default_log_level,
49
+ enabled: value
50
+ )
51
+ end
52
+
53
+ def default_log_level=(level)
54
+ unless VALID_LOG_LEVELS.include?(level.to_s)
55
+ raise InvalidLogLevelError, "Invalid log level: #{level}. Valid levels are: #{VALID_LOG_LEVELS.join(", ")}"
56
+ end
57
+
58
+ @default_log_level = level.to_s
59
+ @logger.set_mcp_level(@default_log_level)
60
+ end
61
+
62
+ def transport_type
63
+ case transport
64
+ when Hash
65
+ transport[:type] || transport["type"]
66
+ when Symbol, String
67
+ transport.to_sym
68
+ end
69
+ end
70
+
71
+ def transport_options
72
+ case transport
73
+ when Hash
74
+ transport.except(:type, "type").transform_keys(&:to_sym)
75
+ else
76
+ {}
77
+ end
19
78
  end
20
79
 
21
80
  def validate!
22
81
  raise InvalidServerNameError unless valid_name?
23
82
  raise InvalidRegistryError unless valid_registry?
24
83
  raise InvalidServerVersionError unless valid_version?
84
+ validate_transport!
25
85
 
26
86
  validate_environment_variables!
27
87
  end
@@ -51,6 +111,14 @@ module ModelContextProtocol
51
111
  environment_variables[key.to_s.upcase] = value
52
112
  end
53
113
 
114
+ def context
115
+ @context ||= {}
116
+ end
117
+
118
+ def context=(context_hash = {})
119
+ @context = context_hash
120
+ end
121
+
54
122
  private
55
123
 
56
124
  def required_environment_variables
@@ -74,5 +142,29 @@ module ModelContextProtocol
74
142
  def valid_version?
75
143
  version&.is_a?(String)
76
144
  end
145
+
146
+ def validate_transport!
147
+ case transport_type
148
+ when :streamable_http
149
+ validate_streamable_http_transport!
150
+ when :stdio, nil
151
+ # stdio transport has no required options
152
+ else
153
+ raise InvalidTransportError, "Unknown transport type: #{transport_type}" if transport_type
154
+ end
155
+ end
156
+
157
+ def validate_streamable_http_transport!
158
+ options = transport_options
159
+
160
+ unless options[:redis_client]
161
+ raise InvalidTransportError, "streamable_http transport requires redis_client option"
162
+ end
163
+
164
+ redis_client = options[:redis_client]
165
+ unless redis_client.respond_to?(:hset) && redis_client.respond_to?(:expire)
166
+ raise InvalidTransportError, "redis_client must be a Redis-compatible client"
167
+ end
168
+ end
77
169
  end
78
170
  end
@@ -0,0 +1,109 @@
1
+ require "logger"
2
+ require "forwardable"
3
+ require "json"
4
+
5
+ module ModelContextProtocol
6
+ class Server::MCPLogger
7
+ extend Forwardable
8
+
9
+ def_delegators :@internal_logger, :datetime_format=, :formatter=, :progname, :progname=
10
+
11
+ LEVEL_MAP = {
12
+ "debug" => Logger::DEBUG,
13
+ "info" => Logger::INFO,
14
+ "notice" => Logger::INFO,
15
+ "warning" => Logger::WARN,
16
+ "error" => Logger::ERROR,
17
+ "critical" => Logger::FATAL,
18
+ "alert" => Logger::FATAL,
19
+ "emergency" => Logger::UNKNOWN
20
+ }.freeze
21
+
22
+ REVERSE_LEVEL_MAP = {
23
+ Logger::DEBUG => "debug",
24
+ Logger::INFO => "info",
25
+ Logger::WARN => "warning",
26
+ Logger::ERROR => "error",
27
+ Logger::FATAL => "critical",
28
+ Logger::UNKNOWN => "emergency"
29
+ }.freeze
30
+
31
+ attr_accessor :transport
32
+ attr_reader :logger_name, :enabled
33
+
34
+ def initialize(logger_name: "server", level: "info", enabled: true)
35
+ @logger_name = logger_name
36
+ @enabled = enabled
37
+ @internal_logger = Logger.new(nil)
38
+ @internal_logger.level = LEVEL_MAP[level] || Logger::INFO
39
+ @transport = nil
40
+ @queued_messages = []
41
+ end
42
+
43
+ %i[debug info warn error fatal unknown].each do |severity|
44
+ define_method(severity) do |message = nil, **data, &block|
45
+ return true unless @enabled
46
+ add(Logger.const_get(severity.to_s.upcase), message, data, &block)
47
+ end
48
+ end
49
+
50
+ def add(severity, message = nil, data = {}, &block)
51
+ return true unless @enabled
52
+ return true if severity < @internal_logger.level
53
+
54
+ message = block.call if message.nil? && block_given?
55
+ send_notification(severity, message, data)
56
+ true
57
+ end
58
+
59
+ def level=(value)
60
+ @internal_logger.level = value
61
+ end
62
+
63
+ def level
64
+ @internal_logger.level
65
+ end
66
+
67
+ def set_mcp_level(mcp_level)
68
+ self.level = LEVEL_MAP[mcp_level] || Logger::INFO
69
+ end
70
+
71
+ def connect_transport(transport)
72
+ @transport = transport
73
+ flush_queued_messages if @enabled
74
+ end
75
+
76
+ private
77
+
78
+ def send_notification(severity, message, data)
79
+ return unless @enabled
80
+
81
+ notification_params = {
82
+ level: REVERSE_LEVEL_MAP[severity] || "info",
83
+ logger: @logger_name,
84
+ data: format_data(message, data)
85
+ }
86
+
87
+ if @transport
88
+ @transport.send_notification("notifications/message", notification_params)
89
+ else
90
+ @queued_messages << notification_params
91
+ end
92
+ end
93
+
94
+ def format_data(message, additional_data)
95
+ data = {}
96
+ data[:message] = message.to_s if message
97
+ data.merge!(additional_data) unless additional_data.empty?
98
+ data
99
+ end
100
+
101
+ def flush_queued_messages
102
+ return unless @transport && @enabled
103
+ @queued_messages.each do |params|
104
+ @transport.send_notification("notifications/message", params)
105
+ end
106
+ @queued_messages.clear
107
+ end
108
+ end
109
+ end
@@ -1,10 +1,12 @@
1
1
  module ModelContextProtocol
2
2
  class Server::Prompt
3
- attr_reader :params
3
+ attr_reader :params, :context, :logger
4
4
 
5
- def initialize(params)
5
+ def initialize(params, logger, context = {})
6
6
  validate!(params)
7
7
  @params = params
8
+ @context = context
9
+ @logger = logger
8
10
  end
9
11
 
10
12
  def call
@@ -24,8 +26,8 @@ module ModelContextProtocol
24
26
 
25
27
  private def validate!(params = {})
26
28
  arguments = self.class.arguments || []
27
- required_args = arguments.select { |arg| arg[:required] }.map { |arg| arg[:name] }
28
- valid_arg_names = arguments.map { |arg| arg[:name] }
29
+ required_args = arguments.select { |arg| arg[:required] }.map { |arg| arg[:name].to_sym }
30
+ valid_arg_names = arguments.map { |arg| arg[:name].to_sym }
29
31
 
30
32
  missing_args = required_args - params.keys
31
33
  unless missing_args.empty?
@@ -73,8 +75,8 @@ module ModelContextProtocol
73
75
  subclass.instance_variable_set(:@arguments, @arguments&.dup)
74
76
  end
75
77
 
76
- def call(params)
77
- new(params).call
78
+ def call(params, logger, context = {})
79
+ new(params, logger, context).call
78
80
  rescue ArgumentError => error
79
81
  raise ModelContextProtocol::Server::ParameterValidationError, error.message
80
82
  end
@@ -1,10 +1,12 @@
1
1
  module ModelContextProtocol
2
2
  class Server::Resource
3
- attr_reader :mime_type, :uri
3
+ attr_reader :mime_type, :uri, :context, :logger
4
4
 
5
- def initialize
5
+ def initialize(logger, context = {})
6
6
  @mime_type = self.class.mime_type
7
7
  @uri = self.class.uri
8
+ @context = context
9
+ @logger = logger
8
10
  end
9
11
 
10
12
  def call
@@ -56,8 +58,8 @@ module ModelContextProtocol
56
58
  subclass.instance_variable_set(:@uri, @uri)
57
59
  end
58
60
 
59
- def call
60
- new.call
61
+ def call(logger, context = {})
62
+ new(logger, context).call
61
63
  end
62
64
 
63
65
  def metadata
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "securerandom"
5
+
6
+ module ModelContextProtocol
7
+ class Server
8
+ class SessionStore
9
+ def initialize(redis_client, ttl: 3600)
10
+ @redis = redis_client
11
+ @ttl = ttl
12
+ end
13
+
14
+ def create_session(session_id, data)
15
+ session_data = {
16
+ id: session_id,
17
+ server_instance: data[:server_instance],
18
+ context: data[:context] || {},
19
+ created_at: data[:created_at] || Time.now.to_f,
20
+ last_activity: Time.now.to_f,
21
+ active_stream: false
22
+ }
23
+
24
+ @redis.hset("session:#{session_id}", session_data.transform_values(&:to_json))
25
+ @redis.expire("session:#{session_id}", @ttl)
26
+ session_id
27
+ end
28
+
29
+ def mark_stream_active(session_id, server_instance)
30
+ @redis.multi do |multi|
31
+ multi.hset("session:#{session_id}",
32
+ "active_stream", true.to_json,
33
+ "stream_server", server_instance.to_json,
34
+ "last_activity", Time.now.to_f.to_json)
35
+ multi.expire("session:#{session_id}", @ttl)
36
+ end
37
+ end
38
+
39
+ def mark_stream_inactive(session_id)
40
+ @redis.multi do |multi|
41
+ multi.hset("session:#{session_id}",
42
+ "active_stream", false.to_json,
43
+ "stream_server", nil.to_json,
44
+ "last_activity", Time.now.to_f.to_json)
45
+ multi.expire("session:#{session_id}", @ttl)
46
+ end
47
+ end
48
+
49
+ def get_session_server(session_id)
50
+ server_data = @redis.hget("session:#{session_id}", "stream_server")
51
+ server_data ? JSON.parse(server_data) : nil
52
+ end
53
+
54
+ def session_exists?(session_id)
55
+ @redis.exists("session:#{session_id}") == 1
56
+ end
57
+
58
+ def session_has_active_stream?(session_id)
59
+ stream_data = @redis.hget("session:#{session_id}", "active_stream")
60
+ stream_data ? JSON.parse(stream_data) : false
61
+ end
62
+
63
+ def get_session_context(session_id)
64
+ context_data = @redis.hget("session:#{session_id}", "context")
65
+ context_data ? JSON.parse(context_data) : {}
66
+ end
67
+
68
+ def cleanup_session(session_id)
69
+ @redis.del("session:#{session_id}")
70
+ end
71
+
72
+ def route_message_to_session(session_id, message)
73
+ server_instance = get_session_server(session_id)
74
+ return false unless server_instance
75
+
76
+ # Publish to server-specific channel
77
+ @redis.publish("server:#{server_instance}:messages", {
78
+ session_id: session_id,
79
+ message: message
80
+ }.to_json)
81
+ true
82
+ end
83
+
84
+ def subscribe_to_server(server_instance, &block)
85
+ @redis.subscribe("server:#{server_instance}:messages") do |on|
86
+ on.message do |channel, message|
87
+ data = JSON.parse(message)
88
+ yield(data)
89
+ end
90
+ end
91
+ end
92
+
93
+ def get_all_active_sessions
94
+ keys = @redis.keys("session:*")
95
+ active_sessions = []
96
+
97
+ keys.each do |key|
98
+ session_id = key.sub("session:", "")
99
+ if session_has_active_stream?(session_id)
100
+ active_sessions << session_id
101
+ end
102
+ end
103
+
104
+ active_sessions
105
+ end
106
+ end
107
+ end
108
+ end
@@ -12,16 +12,19 @@ module ModelContextProtocol
12
12
  end
13
13
  end
14
14
 
15
- attr_reader :logger, :router
15
+ attr_reader :router, :configuration
16
16
 
17
- def initialize(logger:, router:)
18
- @logger = logger
17
+ def initialize(router:, configuration:)
19
18
  @router = router
19
+ @configuration = configuration
20
20
  end
21
21
 
22
- def begin
22
+ def handle
23
+ # Connect logger to transport
24
+ @configuration.logger.connect_transport(self)
25
+
23
26
  loop do
24
- line = $stdin.gets
27
+ line = receive_message
25
28
  break unless line
26
29
 
27
30
  begin
@@ -31,18 +34,17 @@ module ModelContextProtocol
31
34
  result = router.route(message)
32
35
  send_message(Response[id: message["id"], result: result.serialized])
33
36
  rescue ModelContextProtocol::Server::ParameterValidationError => validation_error
34
- log("Validation error: #{validation_error.message}")
37
+ @configuration.logger.error("Validation error", error: validation_error.message)
35
38
  send_message(
36
39
  ErrorResponse[id: message["id"], error: {code: -32602, message: validation_error.message}]
37
40
  )
38
41
  rescue JSON::ParserError => parser_error
39
- log("Parser error: #{parser_error.message}")
42
+ @configuration.logger.error("Parser error", error: parser_error.message)
40
43
  send_message(
41
44
  ErrorResponse[id: "", error: {code: -32700, message: parser_error.message}]
42
45
  )
43
46
  rescue => error
44
- log("Internal error: #{error.message}")
45
- log(error.backtrace)
47
+ @configuration.logger.error("Internal error", error: error.message, backtrace: error.backtrace.first(5))
46
48
  send_message(
47
49
  ErrorResponse[id: message["id"], error: {code: -32603, message: error.message}]
48
50
  )
@@ -50,10 +52,23 @@ module ModelContextProtocol
50
52
  end
51
53
  end
52
54
 
55
+ def send_notification(method, params)
56
+ notification = {
57
+ jsonrpc: "2.0",
58
+ method: method,
59
+ params: params
60
+ }
61
+ $stdout.puts(JSON.generate(notification))
62
+ $stdout.flush
63
+ rescue IOError => e
64
+ # Handle broken pipe gracefully
65
+ @configuration.logger.debug("Failed to send notification", error: e.message) if @configuration.logging_enabled?
66
+ end
67
+
53
68
  private
54
69
 
55
- def log(output, level = :error)
56
- logger.send(level.to_sym, output)
70
+ def receive_message
71
+ $stdin.gets
57
72
  end
58
73
 
59
74
  def send_message(message)
@@ -0,0 +1,291 @@
1
+ require "json"
2
+ require "securerandom"
3
+
4
+ module ModelContextProtocol
5
+ class Server::StreamableHttpTransport
6
+ Response = Data.define(:id, :result) do
7
+ def serialized
8
+ {jsonrpc: "2.0", id:, result:}
9
+ end
10
+ end
11
+
12
+ ErrorResponse = Data.define(:id, :error) do
13
+ def serialized
14
+ {jsonrpc: "2.0", id:, error:}
15
+ end
16
+ end
17
+ def initialize(router:, configuration:)
18
+ @router = router
19
+ @configuration = configuration
20
+
21
+ transport_options = @configuration.transport_options
22
+ @redis = transport_options[:redis_client]
23
+
24
+ @session_store = ModelContextProtocol::Server::SessionStore.new(
25
+ @redis,
26
+ ttl: transport_options[:session_ttl] || 3600
27
+ )
28
+
29
+ @server_instance = "#{Socket.gethostname}-#{Process.pid}-#{SecureRandom.hex(4)}"
30
+ @local_streams = {}
31
+ @notification_queue = []
32
+
33
+ setup_redis_subscriber
34
+ end
35
+
36
+ def handle
37
+ @configuration.logger.connect_transport(self)
38
+
39
+ request = @configuration.transport_options[:request]
40
+ response = @configuration.transport_options[:response]
41
+
42
+ unless request && response
43
+ raise ArgumentError, "StreamableHTTP transport requires request and response objects in transport_options"
44
+ end
45
+
46
+ case request.method
47
+ when "POST"
48
+ handle_post_request(request)
49
+ when "GET"
50
+ handle_sse_request(request, response)
51
+ when "DELETE"
52
+ handle_delete_request(request)
53
+ else
54
+ error_response = ErrorResponse[id: nil, error: {code: -32601, message: "Method not allowed"}]
55
+ {json: error_response.serialized, status: 405}
56
+ end
57
+ end
58
+
59
+ def send_notification(method, params)
60
+ notification = {
61
+ jsonrpc: "2.0",
62
+ method: method,
63
+ params: params
64
+ }
65
+
66
+ if has_active_streams?
67
+ deliver_to_active_streams(notification)
68
+ else
69
+ @notification_queue << notification
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def handle_post_request(request)
76
+ body_string = request.body.read
77
+ body = JSON.parse(body_string)
78
+ session_id = request.headers["Mcp-Session-Id"]
79
+
80
+ case body["method"]
81
+ when "initialize"
82
+ handle_initialization(body)
83
+ else
84
+ handle_regular_request(body, session_id)
85
+ end
86
+ rescue JSON::ParserError
87
+ error_response = ErrorResponse[id: "", error: {code: -32700, message: "Parse error"}]
88
+ {json: error_response.serialized, status: 400}
89
+ rescue ModelContextProtocol::Server::ParameterValidationError => validation_error
90
+ @configuration.logger.error("Validation error", error: validation_error.message)
91
+ error_response = ErrorResponse[id: body&.dig("id"), error: {code: -32602, message: validation_error.message}]
92
+ {json: error_response.serialized, status: 400}
93
+ rescue => e
94
+ @configuration.logger.error("Error handling POST request", error: e.message, backtrace: e.backtrace.first(5))
95
+ error_response = ErrorResponse[id: body&.dig("id"), error: {code: -32603, message: "Internal error"}]
96
+ {json: error_response.serialized, status: 500}
97
+ end
98
+
99
+ def handle_initialization(body)
100
+ session_id = SecureRandom.uuid
101
+
102
+ @session_store.create_session(session_id, {
103
+ server_instance: @server_instance,
104
+ context: @configuration.context || {},
105
+ created_at: Time.now.to_f
106
+ })
107
+
108
+ result = @router.route(body)
109
+ response = Response[id: body["id"], result: result.serialized]
110
+
111
+ {
112
+ json: response.serialized,
113
+ status: 200,
114
+ headers: {"Mcp-Session-Id" => session_id}
115
+ }
116
+ end
117
+
118
+ def handle_regular_request(body, session_id)
119
+ unless session_id && @session_store.session_exists?(session_id)
120
+ error_response = ErrorResponse[id: body["id"], error: {code: -32600, message: "Invalid or missing session ID"}]
121
+ return {json: error_response.serialized, status: 400}
122
+ end
123
+
124
+ result = @router.route(body)
125
+ response = Response[id: body["id"], result: result.serialized]
126
+
127
+ if @session_store.session_has_active_stream?(session_id)
128
+ deliver_to_session_stream(session_id, response.serialized)
129
+ {json: {accepted: true}, status: 200}
130
+ else
131
+ {json: response.serialized, status: 200}
132
+ end
133
+ end
134
+
135
+ def handle_sse_request(request, response)
136
+ session_id = request.headers["Mcp-Session-Id"]
137
+
138
+ unless session_id && @session_store.session_exists?(session_id)
139
+ error_response = ErrorResponse[id: nil, error: {code: -32600, message: "Invalid or missing session ID"}]
140
+ return {json: error_response.serialized, status: 400}
141
+ end
142
+
143
+ @session_store.mark_stream_active(session_id, @server_instance)
144
+
145
+ {
146
+ stream: true,
147
+ headers: {
148
+ "Content-Type" => "text/event-stream",
149
+ "Cache-Control" => "no-cache",
150
+ "Connection" => "keep-alive"
151
+ },
152
+ stream_proc: create_sse_stream_proc(session_id)
153
+ }
154
+ end
155
+
156
+ def handle_delete_request(request)
157
+ session_id = request.headers["Mcp-Session-Id"]
158
+
159
+ if session_id
160
+ cleanup_session(session_id)
161
+ end
162
+
163
+ {json: {success: true}, status: 200}
164
+ end
165
+
166
+ def create_sse_stream_proc(session_id)
167
+ proc do |stream|
168
+ register_local_stream(session_id, stream)
169
+
170
+ flush_notifications_to_stream(stream)
171
+
172
+ start_keepalive_thread(session_id, stream)
173
+
174
+ loop do
175
+ break unless stream_connected?(stream)
176
+ sleep 0.1
177
+ end
178
+ ensure
179
+ cleanup_local_stream(session_id)
180
+ end
181
+ end
182
+
183
+ def register_local_stream(session_id, stream)
184
+ @local_streams[session_id] = stream
185
+ end
186
+
187
+ def cleanup_local_stream(session_id)
188
+ @local_streams.delete(session_id)
189
+ @session_store.mark_stream_inactive(session_id)
190
+ end
191
+
192
+ def stream_connected?(stream)
193
+ return false unless stream
194
+
195
+ begin
196
+ stream.write(": ping\n\n")
197
+ stream.flush if stream.respond_to?(:flush)
198
+ true
199
+ rescue IOError, Errno::EPIPE, Errno::ECONNRESET
200
+ false
201
+ end
202
+ end
203
+
204
+ def start_keepalive_thread(session_id, stream)
205
+ Thread.new do
206
+ loop do
207
+ sleep 30
208
+ break unless stream_connected?(stream)
209
+
210
+ begin
211
+ send_ping_to_stream(stream)
212
+ rescue IOError, Errno::EPIPE, Errno::ECONNRESET
213
+ break
214
+ end
215
+ end
216
+ rescue => e
217
+ @configuration.logger.error("Keepalive thread error", error: e.message)
218
+ ensure
219
+ cleanup_local_stream(session_id)
220
+ end
221
+ end
222
+
223
+ def send_ping_to_stream(stream)
224
+ stream.write(": ping #{Time.now.iso8601}\n\n")
225
+ stream.flush if stream.respond_to?(:flush)
226
+ end
227
+
228
+ def send_to_stream(stream, data)
229
+ message = data.is_a?(String) ? data : data.to_json
230
+ stream.write("data: #{message}\n\n")
231
+ stream.flush if stream.respond_to?(:flush)
232
+ end
233
+
234
+ def deliver_to_session_stream(session_id, data)
235
+ if @local_streams[session_id]
236
+ begin
237
+ send_to_stream(@local_streams[session_id], data)
238
+ return true
239
+ rescue IOError, Errno::EPIPE, Errno::ECONNRESET
240
+ cleanup_local_stream(session_id)
241
+ end
242
+ end
243
+
244
+ @session_store.route_message_to_session(session_id, data)
245
+ end
246
+
247
+ def cleanup_session(session_id)
248
+ cleanup_local_stream(session_id)
249
+ @session_store.cleanup_session(session_id)
250
+ end
251
+
252
+ def setup_redis_subscriber
253
+ Thread.new do
254
+ @session_store.subscribe_to_server(@server_instance) do |data|
255
+ session_id = data["session_id"]
256
+ message = data["message"]
257
+
258
+ if @local_streams[session_id]
259
+ begin
260
+ send_to_stream(@local_streams[session_id], message)
261
+ rescue IOError, Errno::EPIPE, Errno::ECONNRESET
262
+ cleanup_local_stream(session_id)
263
+ end
264
+ end
265
+ end
266
+ rescue => e
267
+ @configuration.logger.error("Redis subscriber error", error: e.message, backtrace: e.backtrace.first(5))
268
+ sleep 5
269
+ retry
270
+ end
271
+ end
272
+
273
+ def has_active_streams?
274
+ @local_streams.any?
275
+ end
276
+
277
+ def deliver_to_active_streams(notification)
278
+ @local_streams.each do |session_id, stream|
279
+ send_to_stream(stream, notification)
280
+ rescue IOError, Errno::EPIPE, Errno::ECONNRESET
281
+ cleanup_local_stream(session_id)
282
+ end
283
+ end
284
+
285
+ def flush_notifications_to_stream(stream)
286
+ while (notification = @notification_queue.shift)
287
+ send_to_stream(stream, notification)
288
+ end
289
+ end
290
+ end
291
+ end
@@ -2,11 +2,13 @@ require "json-schema"
2
2
 
3
3
  module ModelContextProtocol
4
4
  class Server::Tool
5
- attr_reader :params
5
+ attr_reader :params, :context, :logger
6
6
 
7
- def initialize(params)
7
+ def initialize(params, logger, context = {})
8
8
  validate!(params)
9
9
  @params = params
10
+ @context = context
11
+ @logger = logger
10
12
  end
11
13
 
12
14
  def call
@@ -90,8 +92,8 @@ module ModelContextProtocol
90
92
  subclass.instance_variable_set(:@input_schema, @input_schema)
91
93
  end
92
94
 
93
- def call(params)
94
- new(params).call
95
+ def call(params, logger, context = {})
96
+ new(params, logger, context).call
95
97
  rescue JSON::Schema::ValidationError => validation_error
96
98
  raise ModelContextProtocol::Server::ParameterValidationError, validation_error.message
97
99
  rescue ModelContextProtocol::Server::ResponseArgumentsError => response_arguments_error
@@ -19,13 +19,25 @@ module ModelContextProtocol
19
19
 
20
20
  def start
21
21
  configuration.validate!
22
- logdev = configuration.logging_enabled? ? $stderr : File::NULL
23
- StdioTransport.new(logger: Logger.new(logdev), router:).begin
22
+
23
+ transport = case configuration.transport_type
24
+ when :stdio, nil
25
+ StdioTransport.new(router: @router, configuration: @configuration)
26
+ when :streamable_http
27
+ StreamableHttpTransport.new(
28
+ router: @router,
29
+ configuration: @configuration
30
+ )
31
+ else
32
+ raise ArgumentError, "Unknown transport: #{configuration.transport_type}"
33
+ end
34
+
35
+ transport.handle
24
36
  end
25
37
 
26
38
  private
27
39
 
28
- PROTOCOL_VERSION = "2024-11-05".freeze
40
+ PROTOCOL_VERSION = "2025-06-18".freeze
29
41
  private_constant :PROTOCOL_VERSION
30
42
 
31
43
  InitializeResponse = Data.define(:protocol_version, :capabilities, :server_info) do
@@ -44,6 +56,12 @@ module ModelContextProtocol
44
56
  end
45
57
  end
46
58
 
59
+ LoggingSetLevelResponse = Data.define do
60
+ def serialized
61
+ {}
62
+ end
63
+ end
64
+
47
65
  def map_handlers
48
66
  router.map("initialize") do |_message|
49
67
  InitializeResponse[
@@ -60,6 +78,17 @@ module ModelContextProtocol
60
78
  PingResponse[]
61
79
  end
62
80
 
81
+ router.map("logging/setLevel") do |message|
82
+ level = message["params"]["level"]
83
+
84
+ unless Configuration::VALID_LOG_LEVELS.include?(level)
85
+ raise ParameterValidationError, "Invalid log level: #{level}. Valid levels are: #{Configuration::VALID_LOG_LEVELS.join(", ")}"
86
+ end
87
+
88
+ configuration.logger.set_mcp_level(level)
89
+ LoggingSetLevelResponse[]
90
+ end
91
+
63
92
  router.map("completion/complete") do |message|
64
93
  type = message["params"]["ref"]["type"]
65
94
 
@@ -94,7 +123,7 @@ module ModelContextProtocol
94
123
  raise ModelContextProtocol::Server::ParameterValidationError, "resource not found for #{uri}"
95
124
  end
96
125
 
97
- resource.call
126
+ resource.call(configuration.logger, configuration.context)
98
127
  end
99
128
 
100
129
  router.map("resources/templates/list") do |message|
@@ -106,7 +135,12 @@ module ModelContextProtocol
106
135
  end
107
136
 
108
137
  router.map("prompts/get") do |message|
109
- configuration.registry.find_prompt(message["params"]["name"]).call(message["params"]["arguments"])
138
+ arguments = message["params"]["arguments"]
139
+ symbolized_arguments = arguments.transform_keys(&:to_sym)
140
+ configuration
141
+ .registry
142
+ .find_prompt(message["params"]["name"])
143
+ .call(symbolized_arguments, configuration.logger, configuration.context)
110
144
  end
111
145
 
112
146
  router.map("tools/list") do
@@ -114,7 +148,12 @@ module ModelContextProtocol
114
148
  end
115
149
 
116
150
  router.map("tools/call") do |message|
117
- configuration.registry.find_tool(message["params"]["name"]).call(message["params"]["arguments"])
151
+ arguments = message["params"]["arguments"]
152
+ symbolized_arguments = arguments.transform_keys(&:to_sym)
153
+ configuration
154
+ .registry
155
+ .find_tool(message["params"]["name"])
156
+ .call(symbolized_arguments, configuration.logger, configuration.context)
118
157
  end
119
158
  end
120
159
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ModelContextProtocol
4
- VERSION = "0.3.2"
4
+ VERSION = "0.3.3"
5
5
  end
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env <%= @ruby_path %>
2
2
 
3
3
  require "bundler/setup"
4
+ require "securerandom"
4
5
  require_relative "../lib/model_context_protocol"
5
6
 
6
7
  Dir[File.join(__dir__, "../spec/support/**/*.rb")].each { |file| require file }
@@ -8,7 +9,15 @@ Dir[File.join(__dir__, "../spec/support/**/*.rb")].each { |file| require file }
8
9
  server = ModelContextProtocol::Server.new do |config|
9
10
  config.name = "MCP Development Server"
10
11
  config.version = "1.0.0"
11
- config.enable_log = true
12
+ config.logging_enabled = true
13
+
14
+ config.set_environment_variable("MCP_ENV", "development")
15
+
16
+ config.context = {
17
+ user_id: "123456",
18
+ request_id: SecureRandom.uuid
19
+ }
20
+
12
21
  config.registry = ModelContextProtocol::Server::Registry.new do
13
22
  prompts list_changed: true do
14
23
  register TestPrompt
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: model-context-protocol-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dick Davis
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-05-10 00:00:00.000000000 Z
11
+ date: 2025-09-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json-schema
@@ -58,12 +58,15 @@ files:
58
58
  - lib/model_context_protocol/server.rb
59
59
  - lib/model_context_protocol/server/completion.rb
60
60
  - lib/model_context_protocol/server/configuration.rb
61
+ - lib/model_context_protocol/server/mcp_logger.rb
61
62
  - lib/model_context_protocol/server/prompt.rb
62
63
  - lib/model_context_protocol/server/registry.rb
63
64
  - lib/model_context_protocol/server/resource.rb
64
65
  - lib/model_context_protocol/server/resource_template.rb
65
66
  - lib/model_context_protocol/server/router.rb
67
+ - lib/model_context_protocol/server/session_store.rb
66
68
  - lib/model_context_protocol/server/stdio_transport.rb
69
+ - lib/model_context_protocol/server/streamable_http_transport.rb
67
70
  - lib/model_context_protocol/server/tool.rb
68
71
  - lib/model_context_protocol/version.rb
69
72
  - tasks/mcp.rake