model-context-protocol-rb 0.3.2 → 0.3.4
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 +4 -4
- data/CHANGELOG.md +18 -2
- data/README.md +172 -34
- data/lib/model_context_protocol/server/completion.rb +6 -0
- data/lib/model_context_protocol/server/configuration.rb +94 -2
- data/lib/model_context_protocol/server/mcp_logger.rb +109 -0
- data/lib/model_context_protocol/server/prompt.rb +21 -19
- data/lib/model_context_protocol/server/resource.rb +6 -4
- data/lib/model_context_protocol/server/session_store.rb +108 -0
- data/lib/model_context_protocol/server/stdio_transport.rb +26 -11
- data/lib/model_context_protocol/server/streamable_http_transport.rb +291 -0
- data/lib/model_context_protocol/server/tool.rb +10 -8
- data/lib/model_context_protocol/server.rb +45 -6
- data/lib/model_context_protocol/version.rb +1 -3
- data/tasks/templates/dev.erb +10 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e004a2e471748b63aa9f5742300fd2b648aabb84d02e75949f9741f1c78aa963
|
4
|
+
data.tar.gz: ecf369882f10f2cb19099d47d991ba70e6d33e0841ebda48e6917fb98780f2fb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9fa10a6fe78390f0d8c6303efcc6135751b50a2c4dd3cc2b0ed0d07e0647e9555a3317a7b68b2794d52f9d6ac3e8e38a569bda96e20bc2e0193dadefab346dd6
|
7
|
+
data.tar.gz: fda6f707e7814911a4e9175cd40fbab41f45dc775db644185e4bc2008fe71bddd83c89ed321cfeee67f93cbf7c29bc6e69b46596893e3ae2242fa757fd9f4f75
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,19 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.3.4] - 2025-09-02
|
4
|
+
|
5
|
+
- (Fix) Fixes broken arguments usage in prompts and tools.
|
6
|
+
|
7
|
+
## [0.3.3] - 2025-09-02
|
8
|
+
|
9
|
+
- (Breaking) Added logging support.
|
10
|
+
- Requires updating the `enable_log` configuration option to `logging_enabled`.
|
11
|
+
- Added experimental Streamable HTTP transport.
|
12
|
+
- (Breaking) Renamed params to arguments in prompts, resources, and tools.
|
13
|
+
- Requires updating all references to `params` in prompts, resources, and tools to `arguments` with symbolized keys.
|
14
|
+
- Improved ergonomics of completions and resource templates.
|
15
|
+
- Added support for providing context to prompts, resources, and tools.
|
16
|
+
|
3
17
|
## [0.3.2] - 2025-05-10
|
4
18
|
|
5
19
|
- Added resource template support.
|
@@ -37,8 +51,10 @@
|
|
37
51
|
|
38
52
|
- Initial release
|
39
53
|
|
40
|
-
[Unreleased]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.3.
|
41
|
-
[0.3.
|
54
|
+
[Unreleased]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.3.4...HEAD
|
55
|
+
[0.3.4]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.3.3...v0.3.4
|
56
|
+
[0.3.3]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.3.2...v0.3.3
|
57
|
+
[0.3.2]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.3.1...v0.3.2
|
42
58
|
[0.3.1]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.3.0...v0.3.1
|
43
59
|
[0.3.0]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.2.0...v0.3.0
|
44
60
|
[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.
|
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
|
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
|
-
|
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 "
|
80
|
-
description "A
|
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 "
|
85
|
-
description "The thing to
|
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 "
|
92
|
-
description "
|
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: "
|
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 "
|
124
|
-
description "
|
245
|
+
name "top-secret-plans.txt"
|
246
|
+
description "Top secret plans to do top secret things"
|
125
247
|
mime_type "text/plain"
|
126
|
-
uri "
|
248
|
+
uri "file:///top-secret-plans.txt"
|
127
249
|
end
|
128
250
|
|
129
251
|
def call
|
130
|
-
|
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 "
|
274
|
+
name "project-logo.png"
|
141
275
|
description "The logo for the project"
|
142
|
-
mime_type "image/
|
143
|
-
uri "
|
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
|
162
|
-
|
296
|
+
class TestResourceTemplate < ModelContextProtocol::Server::ResourceTemplate
|
297
|
+
Completion = ModelContextProtocol::Server::Completion.define do
|
163
298
|
hints = {
|
164
|
-
"name" => ["
|
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 "
|
175
|
-
description "A
|
307
|
+
name "project-document-resource-template"
|
308
|
+
description "A resource template for retrieving project documents"
|
176
309
|
mime_type "text/plain"
|
177
|
-
uri_template "
|
178
|
-
completion :name,
|
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
|
-
|
212
|
-
|
213
|
-
|
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
|
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 =
|
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"
|
@@ -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
|
-
|
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
|
-
|
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
|