model-context-protocol-rb 0.5.1 → 0.6.0
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 +13 -1
- data/README.md +60 -25
- data/lib/model_context_protocol/server/cancellable.rb +5 -5
- data/lib/model_context_protocol/server/{mcp_logger.rb → client_logger.rb} +7 -10
- data/lib/model_context_protocol/server/configuration.rb +17 -34
- data/lib/model_context_protocol/server/global_config/server_logging.rb +78 -0
- data/lib/model_context_protocol/server/progressable.rb +43 -21
- data/lib/model_context_protocol/server/prompt.rb +12 -7
- data/lib/model_context_protocol/server/redis_pool_manager.rb +1 -1
- data/lib/model_context_protocol/server/resource.rb +7 -4
- data/lib/model_context_protocol/server/router.rb +8 -7
- data/lib/model_context_protocol/server/server_logger.rb +28 -0
- data/lib/model_context_protocol/server/stdio_transport/request_store.rb +17 -17
- data/lib/model_context_protocol/server/stdio_transport.rb +18 -12
- data/lib/model_context_protocol/server/streamable_http_transport/message_poller.rb +9 -9
- data/lib/model_context_protocol/server/streamable_http_transport/request_store.rb +36 -36
- data/lib/model_context_protocol/server/streamable_http_transport/server_request_store.rb +231 -0
- data/lib/model_context_protocol/server/streamable_http_transport.rb +419 -181
- data/lib/model_context_protocol/server/tool.rb +6 -5
- data/lib/model_context_protocol/server.rb +15 -13
- data/lib/model_context_protocol/version.rb +1 -1
- metadata +9 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f0f2318890bd52a6ade4d17652273ed887c62cc2badab13aabeb79a993c34a11
|
|
4
|
+
data.tar.gz: d85af9411a3ebb83ca4938c61cd586d9511152b11aea9b91e0765493fd39508b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6557e13bb133a661f26de2d53e6b8b82ca044f26ed2ec288cc03b297699b0cef4de7d540014bbf345317c1d456cc3b27986df146b52604922636e80f8eae5bcb
|
|
7
|
+
data.tar.gz: 065604a28cc5b21bf690d7c138a2be3015ba1d862db7e7393dbcead3ef94f86d54c70e0d7abad51d21f7b60aa65c6b49a54143851326c57a71813a186b698c9c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.6.0] - 2025-01-26
|
|
4
|
+
|
|
5
|
+
- Implement server logging capability for internal server diagnostics.
|
|
6
|
+
- Add informational and debug logging in streamable HTTP transport.
|
|
7
|
+
- (Fix) Ensure stream monitor thread shuts down quickly.
|
|
8
|
+
- (Fix) Ensure streams are closed when the server shuts down.
|
|
9
|
+
- (Fix) Fix stream handling in streamable HTTP transport.
|
|
10
|
+
- (Fix) Ensure empty options don't break the registry.
|
|
11
|
+
- (Fix) Ensure progressable timer tasks do not run indefinitely.
|
|
12
|
+
- (Breaking) Update connection pool dependency; requires updating other gems that depend on connection_pool.
|
|
13
|
+
|
|
3
14
|
## [0.5.1] - 2025-09-23
|
|
4
15
|
|
|
5
16
|
- (Fix) Ensure streams are properly closed when clients disconnect.
|
|
@@ -78,7 +89,8 @@
|
|
|
78
89
|
|
|
79
90
|
- Initial release
|
|
80
91
|
|
|
81
|
-
[Unreleased]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.
|
|
92
|
+
[Unreleased]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.6.0...HEAD
|
|
93
|
+
[0.6.0]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.5.1...v0.6.0
|
|
82
94
|
[0.5.1]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.5.0...v0.5.1
|
|
83
95
|
[0.5.0]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.4.0...v0.5.0
|
|
84
96
|
[0.4.0]: https://github.com/dickdavis/model-context-protocol-rb/compare/v0.3.4...v0.4.0
|
data/README.md
CHANGED
|
@@ -14,6 +14,7 @@ Provides simple abstractions that allow you to serve prompts, resources, resourc
|
|
|
14
14
|
- [Pagination Configuration Options](#pagination-configuration-options)
|
|
15
15
|
- [Transport Configuration Options](#transport-configuration-options)
|
|
16
16
|
- [Redis Configuration](#redis-configuration)
|
|
17
|
+
- [Server Logging Configuration](#server-logging-configuration)
|
|
17
18
|
- [Registry Configuration Options](#registry-configuration-options)
|
|
18
19
|
- [Prompts](#prompts)
|
|
19
20
|
- [Resources](#resources)
|
|
@@ -91,7 +92,6 @@ class ModelContextProtocolController < ActionController::API
|
|
|
91
92
|
config.name = "MyMCPServer"
|
|
92
93
|
config.title = "My MCP Server"
|
|
93
94
|
config.version = "1.0.0"
|
|
94
|
-
config.logging_enabled = true
|
|
95
95
|
config.registry = build_registry
|
|
96
96
|
config.context = {
|
|
97
97
|
user_id: current_user.id,
|
|
@@ -210,9 +210,6 @@ server = ModelContextProtocol::Server.new do |config|
|
|
|
210
210
|
Use this server when you need to interact with the local development environment.
|
|
211
211
|
INSTRUCTIONS
|
|
212
212
|
|
|
213
|
-
# Enable or disable MCP server logging
|
|
214
|
-
config.logging_enabled = true
|
|
215
|
-
|
|
216
213
|
# Configure pagination options for the following methods:
|
|
217
214
|
# prompts/list, resources/list, resource_template/list, tools/list
|
|
218
215
|
config.pagination = {
|
|
@@ -249,11 +246,11 @@ server = ModelContextProtocol::Server.new do |config|
|
|
|
249
246
|
|
|
250
247
|
# Register prompts, resources, resource templates, and tools
|
|
251
248
|
config.registry = ModelContextProtocol::Server::Registry.new do
|
|
252
|
-
prompts
|
|
249
|
+
prompts do
|
|
253
250
|
register TestPrompt
|
|
254
251
|
end
|
|
255
252
|
|
|
256
|
-
resources
|
|
253
|
+
resources do
|
|
257
254
|
register TestResource
|
|
258
255
|
end
|
|
259
256
|
|
|
@@ -261,7 +258,7 @@ server = ModelContextProtocol::Server.new do |config|
|
|
|
261
258
|
register TestResourceTemplate
|
|
262
259
|
end
|
|
263
260
|
|
|
264
|
-
tools
|
|
261
|
+
tools do
|
|
265
262
|
register TestTool
|
|
266
263
|
end
|
|
267
264
|
end
|
|
@@ -281,7 +278,6 @@ The following table details all available configuration options for the MCP serv
|
|
|
281
278
|
| `version` | String | Yes | - | Version of the MCP server |
|
|
282
279
|
| `title` | String | No | - | Human-readable display name for the MCP server |
|
|
283
280
|
| `instructions` | String | No | - | Instructions for how the MCP server should be used by LLMs |
|
|
284
|
-
| `logging_enabled` | Boolean | No | `true` | Enable or disable MCP server logging |
|
|
285
281
|
| `pagination` | Hash/Boolean | No | See pagination table | Pagination configuration (or `false` to disable) |
|
|
286
282
|
| `context` | Hash | No | `{}` | Contextual variables available to prompts, resources, and tools |
|
|
287
283
|
| `transport` | Hash | No | `{ type: :stdio }` | Transport configuration |
|
|
@@ -347,6 +343,30 @@ end
|
|
|
347
343
|
| `reaper_interval` | Integer | No | `60` | Reaper check interval in seconds |
|
|
348
344
|
| `idle_timeout` | Integer | No | `300` | Idle connection timeout in seconds |
|
|
349
345
|
|
|
346
|
+
### Server Logging Configuration
|
|
347
|
+
|
|
348
|
+
Server logging can be configured globally to customize how your MCP server writes debug and operational logs. This logging is separate from client logging (which sends messages to MCP clients via the protocol) and is used for server-side debugging, monitoring, and troubleshooting:
|
|
349
|
+
|
|
350
|
+
```ruby
|
|
351
|
+
ModelContextProtocol::Server.configure_server_logging do |config|
|
|
352
|
+
config.logdev = $stderr # or a file path like '/var/log/mcp-server.log'
|
|
353
|
+
config.level = Logger::INFO # Logger::DEBUG, Logger::INFO, Logger::WARN, Logger::ERROR, Logger::FATAL
|
|
354
|
+
config.progname = "MyMCPServer" # Program name for log entries
|
|
355
|
+
config.formatter = proc do |severity, datetime, progname, msg|
|
|
356
|
+
"#{datetime.strftime('%Y-%m-%d %H:%M:%S')} #{severity} [#{progname}] #{msg}\n"
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
| Option | Type | Required | Default | Description |
|
|
362
|
+
|--------|------|----------|---------|-------------|
|
|
363
|
+
| `logdev` | IO/String | No | `$stderr` | Log destination (IO object or file path) |
|
|
364
|
+
| `level` | Integer | No | `Logger::INFO` | Minimum log level to output |
|
|
365
|
+
| `progname` | String | No | `"MCP-Server"` | Program name for log entries |
|
|
366
|
+
| `formatter` | Proc | No | Default timestamp format | Custom log formatter |
|
|
367
|
+
|
|
368
|
+
**Note:** When using `:stdio` transport, server logging must not use `$stdout` as it conflicts with the MCP protocol communication. Use `$stderr` or a file instead.
|
|
369
|
+
|
|
350
370
|
### Registry Configuration Options
|
|
351
371
|
|
|
352
372
|
The registry is configured using `ModelContextProtocol::Server::Registry.new` and supports the following block types:
|
|
@@ -360,15 +380,17 @@ The registry is configured using `ModelContextProtocol::Server::Registry.new` an
|
|
|
360
380
|
|
|
361
381
|
Within each block, use `register ClassName` to register your handlers.
|
|
362
382
|
|
|
383
|
+
**Note:** The `list_changed` and `subscribe` options are accepted for capability advertisement but the list changed notification functionality is not yet implemented (see [Feature Support](#feature-support-server)).
|
|
384
|
+
|
|
363
385
|
**Example:**
|
|
364
386
|
```ruby
|
|
365
387
|
config.registry = ModelContextProtocol::Server::Registry.new do
|
|
366
|
-
prompts
|
|
388
|
+
prompts do
|
|
367
389
|
register MyPrompt
|
|
368
390
|
register AnotherPrompt
|
|
369
391
|
end
|
|
370
392
|
|
|
371
|
-
resources
|
|
393
|
+
resources do
|
|
372
394
|
register MyResource
|
|
373
395
|
end
|
|
374
396
|
|
|
@@ -384,9 +406,9 @@ end
|
|
|
384
406
|
|
|
385
407
|
The `ModelContextProtocol::Server::Prompt` base class allows subclasses to define a prompt that the MCP client can use.
|
|
386
408
|
|
|
387
|
-
Define the prompt properties and then implement the `call` method to build your prompt. Any arguments passed to the
|
|
409
|
+
Define the prompt properties and then implement the `call` method to build your prompt. Any arguments passed to the prompt 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.
|
|
388
410
|
|
|
389
|
-
You can also log from within your prompt by calling a valid logger level method on the `
|
|
411
|
+
You can also send MCP log messages to clients from within your prompt by calling a valid logger level method on the `client_logger` and passing a string message. For server-side debugging and monitoring, use the `server_logger` to write logs that are not sent to clients.
|
|
390
412
|
|
|
391
413
|
### Prompt Definition
|
|
392
414
|
|
|
@@ -452,7 +474,8 @@ The `arguments` passed from an MCP client are available, as well as the `context
|
|
|
452
474
|
|----------|---------|-------------|
|
|
453
475
|
| `arguments` | Within `call` | Hash containing client-provided arguments (symbol keys) |
|
|
454
476
|
| `context` | Within `call` | Hash containing server configuration context values |
|
|
455
|
-
| `
|
|
477
|
+
| `client_logger` | Within `call` | Client logger instance for sending MCP log messages (e.g., `client_logger.info("message")`) |
|
|
478
|
+
| `server_logger` | Within `call` | Server logger instance for debugging and monitoring (e.g., `server_logger.debug("message")`) |
|
|
456
479
|
|
|
457
480
|
### Examples
|
|
458
481
|
|
|
@@ -505,8 +528,12 @@ class TestPrompt < ModelContextProtocol::Server::Prompt
|
|
|
505
528
|
|
|
506
529
|
# The call method is invoked by the MCP Server to generate a response to resource/read requests
|
|
507
530
|
def call
|
|
508
|
-
# You can use the
|
|
509
|
-
|
|
531
|
+
# You can use the client_logger
|
|
532
|
+
client_logger.info("Brainstorming excuses...")
|
|
533
|
+
|
|
534
|
+
# Server logging for debugging and monitoring (not sent to client)
|
|
535
|
+
server_logger.debug("Prompt called with arguments: #{arguments}")
|
|
536
|
+
server_logger.info("Generating excuse brainstorming prompt")
|
|
510
537
|
|
|
511
538
|
# Build an array of user and assistant messages
|
|
512
539
|
messages = message_history do
|
|
@@ -541,6 +568,8 @@ The `ModelContextProtocol::Server::Resource` base class allows subclasses to def
|
|
|
541
568
|
|
|
542
569
|
Define the resource properties and optionally annotations, 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.
|
|
543
570
|
|
|
571
|
+
You can also send MCP log messages to clients from within your resource by calling a valid logger level method on the `client_logger` and passing a string message. For server-side debugging and monitoring, use the `server_logger` to write logs that are not sent to clients.
|
|
572
|
+
|
|
544
573
|
### Resource Definition
|
|
545
574
|
|
|
546
575
|
Use the `define` block to set [resource properties](https://spec.modelcontextprotocol.io/specification/2025-06-18/server/resources/) and configure annotations.
|
|
@@ -578,12 +607,15 @@ Define your resource properties and annotations, implement the `call` method to
|
|
|
578
607
|
|
|
579
608
|
### Available Instance Variables
|
|
580
609
|
|
|
581
|
-
Resources
|
|
610
|
+
Resources have access to their configured properties and server context.
|
|
582
611
|
|
|
583
612
|
| Variable | Context | Description |
|
|
584
613
|
|----------|---------|-------------|
|
|
585
614
|
| `mime_type` | Within `call` | The configured MIME type for this resource |
|
|
586
615
|
| `uri` | Within `call` | The configured URI identifier for this resource |
|
|
616
|
+
| `client_logger` | Within `call` | Client logger instance for sending MCP log messages (e.g., `client_logger.info("message")`) |
|
|
617
|
+
| `server_logger` | Within `call` | Server logger instance for debugging and monitoring (e.g., `server_logger.debug("message")`) |
|
|
618
|
+
| `context` | Within `call` | Hash containing server configuration context values |
|
|
587
619
|
|
|
588
620
|
### Examples
|
|
589
621
|
|
|
@@ -724,7 +756,9 @@ end
|
|
|
724
756
|
|
|
725
757
|
The `ModelContextProtocol::Server::Tool` base class allows subclasses to define a tool that the MCP client can use.
|
|
726
758
|
|
|
727
|
-
Define the tool properties and schemas, then implement the `call` method to build your tool
|
|
759
|
+
Define the tool properties and schemas, 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 prompt responds with appropriately formatted response data.
|
|
760
|
+
|
|
761
|
+
You can also send MCP log messages to clients from within your tool by calling a valid logger level method on the `client_logger` and passing a string message. For server-side debugging and monitoring, use the `server_logger` to write logs that are not sent to clients.
|
|
728
762
|
|
|
729
763
|
### Tool Definition
|
|
730
764
|
|
|
@@ -781,7 +815,8 @@ Arguments from MCP clients and server context are available, along with logging
|
|
|
781
815
|
|----------|---------|-------------|
|
|
782
816
|
| `arguments` | Within `call` | Hash containing client-provided arguments (symbol keys) |
|
|
783
817
|
| `context` | Within `call` | Hash containing server configuration context values |
|
|
784
|
-
| `
|
|
818
|
+
| `client_logger` | Within `call` | Client logger instance for sending MCP log messages (e.g., `client_logger.info("message")`) |
|
|
819
|
+
| `server_logger` | Within `call` | Server logger instance for debugging and monitoring (e.g., `server_logger.debug("message")`) |
|
|
785
820
|
|
|
786
821
|
### Examples
|
|
787
822
|
|
|
@@ -835,11 +870,11 @@ class TestToolWithStructuredContentResponse < ModelContextProtocol::Server::Tool
|
|
|
835
870
|
def call
|
|
836
871
|
# Use values provided by the server as context
|
|
837
872
|
user_id = context[:user_id]
|
|
838
|
-
|
|
873
|
+
client_logger.info("Initiating request for user #{user_id}...")
|
|
839
874
|
|
|
840
875
|
# Use values provided by clients as tool arguments
|
|
841
876
|
location = arguments[:location]
|
|
842
|
-
|
|
877
|
+
client_logger.info("Getting weather data for #{location}...")
|
|
843
878
|
|
|
844
879
|
# Returns a hash that validates against the output schema
|
|
845
880
|
weather_data = get_weather_data(location)
|
|
@@ -883,7 +918,7 @@ class TestToolWithTextResponse < ModelContextProtocol::Server::Tool
|
|
|
883
918
|
end
|
|
884
919
|
|
|
885
920
|
def call
|
|
886
|
-
|
|
921
|
+
client_logger.info("Silly user doesn't know how to double a number")
|
|
887
922
|
number = arguments[:number].to_i
|
|
888
923
|
calculation = number * 2
|
|
889
924
|
|
|
@@ -975,7 +1010,7 @@ class TestToolWithResourceResponse < ModelContextProtocol::Server::Tool
|
|
|
975
1010
|
return respond_with :error, text: "Resource `#{name}` not found"
|
|
976
1011
|
end
|
|
977
1012
|
|
|
978
|
-
resource_data = resource_klass.call
|
|
1013
|
+
resource_data = resource_klass.call(client_logger, context)
|
|
979
1014
|
|
|
980
1015
|
respond_with content: embedded_resource_content(resource: resource_data)
|
|
981
1016
|
end
|
|
@@ -1003,7 +1038,7 @@ class TestToolWithMixedContentResponse < ModelContextProtocol::Server::Tool
|
|
|
1003
1038
|
end
|
|
1004
1039
|
|
|
1005
1040
|
def call
|
|
1006
|
-
|
|
1041
|
+
client_logger.info("Getting comprehensive temperature history data")
|
|
1007
1042
|
|
|
1008
1043
|
zip = arguments[:zip]
|
|
1009
1044
|
temperature_history = retrieve_temperature_history(zip:)
|
|
@@ -1085,7 +1120,7 @@ class TestToolWithCancellableSleep < ModelContextProtocol::Server::Tool
|
|
|
1085
1120
|
end
|
|
1086
1121
|
|
|
1087
1122
|
def call
|
|
1088
|
-
|
|
1123
|
+
client_logger.info("Starting 3 second sleep operation")
|
|
1089
1124
|
|
|
1090
1125
|
result = cancellable do
|
|
1091
1126
|
sleep 3
|
|
@@ -1126,7 +1161,7 @@ class TestToolWithProgressableAndCancellable < ModelContextProtocol::Server::Too
|
|
|
1126
1161
|
def call
|
|
1127
1162
|
max_duration = arguments[:max_duration] || 10
|
|
1128
1163
|
work_steps = arguments[:work_steps] || 10
|
|
1129
|
-
|
|
1164
|
+
client_logger.info("Starting progressable call with max_duration=#{max_duration}, work_steps=#{work_steps}")
|
|
1130
1165
|
|
|
1131
1166
|
result = progressable(max_duration:, message: "Processing #{work_steps} items") do
|
|
1132
1167
|
cancellable do
|
|
@@ -24,8 +24,8 @@ module ModelContextProtocol
|
|
|
24
24
|
executing_thread = Concurrent::AtomicReference.new(nil)
|
|
25
25
|
|
|
26
26
|
timer_task = Concurrent::TimerTask.new(execution_interval: interval) do
|
|
27
|
-
if context && context[:request_store] && context[:
|
|
28
|
-
if context[:request_store].cancelled?(context[:
|
|
27
|
+
if context && context[:request_store] && context[:jsonrpc_request_id]
|
|
28
|
+
if context[:request_store].cancelled?(context[:jsonrpc_request_id])
|
|
29
29
|
thread = executing_thread.get
|
|
30
30
|
thread&.raise(CancellationError, "Request was cancelled") if thread&.alive?
|
|
31
31
|
end
|
|
@@ -35,9 +35,9 @@ module ModelContextProtocol
|
|
|
35
35
|
begin
|
|
36
36
|
executing_thread.set(Thread.current)
|
|
37
37
|
|
|
38
|
-
if context && context[:request_store] && context[:
|
|
39
|
-
if context[:request_store].cancelled?(context[:
|
|
40
|
-
raise CancellationError, "Request #{context[:
|
|
38
|
+
if context && context[:request_store] && context[:jsonrpc_request_id]
|
|
39
|
+
if context[:request_store].cancelled?(context[:jsonrpc_request_id])
|
|
40
|
+
raise CancellationError, "Request #{context[:jsonrpc_request_id]} was cancelled"
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
|
|
@@ -3,11 +3,13 @@ require "forwardable"
|
|
|
3
3
|
require "json"
|
|
4
4
|
|
|
5
5
|
module ModelContextProtocol
|
|
6
|
-
class Server::
|
|
6
|
+
class Server::ClientLogger
|
|
7
7
|
extend Forwardable
|
|
8
8
|
|
|
9
9
|
def_delegators :@internal_logger, :datetime_format=, :formatter=, :progname, :progname=
|
|
10
10
|
|
|
11
|
+
VALID_LOG_LEVELS = %w[debug info notice warning error critical alert emergency].freeze
|
|
12
|
+
|
|
11
13
|
LEVEL_MAP = {
|
|
12
14
|
"debug" => Logger::DEBUG,
|
|
13
15
|
"info" => Logger::INFO,
|
|
@@ -29,11 +31,10 @@ module ModelContextProtocol
|
|
|
29
31
|
}.freeze
|
|
30
32
|
|
|
31
33
|
attr_accessor :transport
|
|
32
|
-
attr_reader :logger_name
|
|
34
|
+
attr_reader :logger_name
|
|
33
35
|
|
|
34
|
-
def initialize(logger_name: "server", level: "info"
|
|
36
|
+
def initialize(logger_name: "server", level: "info")
|
|
35
37
|
@logger_name = logger_name
|
|
36
|
-
@enabled = enabled
|
|
37
38
|
@internal_logger = Logger.new(nil)
|
|
38
39
|
@internal_logger.level = LEVEL_MAP[level] || Logger::INFO
|
|
39
40
|
@transport = nil
|
|
@@ -42,13 +43,11 @@ module ModelContextProtocol
|
|
|
42
43
|
|
|
43
44
|
%i[debug info warn error fatal unknown].each do |severity|
|
|
44
45
|
define_method(severity) do |message = nil, **data, &block|
|
|
45
|
-
return true unless @enabled
|
|
46
46
|
add(Logger.const_get(severity.to_s.upcase), message, data, &block)
|
|
47
47
|
end
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
def add(severity, message = nil, data = {}, &block)
|
|
51
|
-
return true unless @enabled
|
|
52
51
|
return true if severity < @internal_logger.level
|
|
53
52
|
|
|
54
53
|
message = block.call if message.nil? && block_given?
|
|
@@ -70,14 +69,12 @@ module ModelContextProtocol
|
|
|
70
69
|
|
|
71
70
|
def connect_transport(transport)
|
|
72
71
|
@transport = transport
|
|
73
|
-
flush_queued_messages
|
|
72
|
+
flush_queued_messages
|
|
74
73
|
end
|
|
75
74
|
|
|
76
75
|
private
|
|
77
76
|
|
|
78
77
|
def send_notification(severity, message, data)
|
|
79
|
-
return unless @enabled
|
|
80
|
-
|
|
81
78
|
notification_params = {
|
|
82
79
|
level: REVERSE_LEVEL_MAP[severity] || "info",
|
|
83
80
|
logger: @logger_name,
|
|
@@ -99,7 +96,7 @@ module ModelContextProtocol
|
|
|
99
96
|
end
|
|
100
97
|
|
|
101
98
|
def flush_queued_messages
|
|
102
|
-
return unless @transport
|
|
99
|
+
return unless @transport
|
|
103
100
|
@queued_messages.each do |params|
|
|
104
101
|
@transport.send_notification("notifications/message", params)
|
|
105
102
|
end
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
require_relative "mcp_logger"
|
|
2
|
-
|
|
3
1
|
module ModelContextProtocol
|
|
4
2
|
class Server::Configuration
|
|
5
3
|
# Raised when configured with invalid name.
|
|
@@ -23,48 +21,25 @@ module ModelContextProtocol
|
|
|
23
21
|
# Raised when transport configuration is invalid
|
|
24
22
|
class InvalidTransportError < StandardError; end
|
|
25
23
|
|
|
26
|
-
# Raised when an invalid log level is provided
|
|
27
|
-
class InvalidLogLevelError < StandardError; end
|
|
28
|
-
|
|
29
24
|
# Raised when pagination configuration is invalid
|
|
30
25
|
class InvalidPaginationError < StandardError; end
|
|
31
26
|
|
|
32
|
-
# Valid MCP log levels per the specification
|
|
33
|
-
VALID_LOG_LEVELS = %w[debug info notice warning error critical alert emergency].freeze
|
|
34
|
-
|
|
35
27
|
attr_accessor :name, :registry, :version, :transport, :pagination, :title, :instructions
|
|
36
|
-
attr_reader :
|
|
28
|
+
attr_reader :client_logger, :server_logger
|
|
37
29
|
|
|
38
30
|
def initialize
|
|
39
|
-
@
|
|
40
|
-
@default_log_level = "info"
|
|
41
|
-
@logger = ModelContextProtocol::Server::MCPLogger.new(
|
|
31
|
+
@client_logger = ModelContextProtocol::Server::ClientLogger.new(
|
|
42
32
|
logger_name: "server",
|
|
43
|
-
level:
|
|
44
|
-
enabled: @logging_enabled
|
|
33
|
+
level: "info"
|
|
45
34
|
)
|
|
46
|
-
end
|
|
47
35
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def logging_enabled=(value)
|
|
53
|
-
@logging_enabled = value
|
|
54
|
-
@logger = ModelContextProtocol::Server::MCPLogger.new(
|
|
55
|
-
logger_name: "server",
|
|
56
|
-
level: @default_log_level,
|
|
57
|
-
enabled: value
|
|
58
|
-
)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def default_log_level=(level)
|
|
62
|
-
unless VALID_LOG_LEVELS.include?(level.to_s)
|
|
63
|
-
raise InvalidLogLevelError, "Invalid log level: #{level}. Valid levels are: #{VALID_LOG_LEVELS.join(", ")}"
|
|
36
|
+
server_logger_params = if ModelContextProtocol::Server::GlobalConfig::ServerLogging.configured?
|
|
37
|
+
ModelContextProtocol::Server::GlobalConfig::ServerLogging.logger_params
|
|
38
|
+
else
|
|
39
|
+
{}
|
|
64
40
|
end
|
|
65
41
|
|
|
66
|
-
@
|
|
67
|
-
@logger.set_mcp_level(@default_log_level)
|
|
42
|
+
@server_logger = ModelContextProtocol::Server::ServerLogger.new(**server_logger_params)
|
|
68
43
|
end
|
|
69
44
|
|
|
70
45
|
def transport_type
|
|
@@ -123,12 +98,13 @@ module ModelContextProtocol
|
|
|
123
98
|
raise InvalidServerNameError unless valid_name?
|
|
124
99
|
raise InvalidRegistryError unless valid_registry?
|
|
125
100
|
raise InvalidServerVersionError unless valid_version?
|
|
101
|
+
|
|
126
102
|
validate_transport!
|
|
127
103
|
validate_pagination!
|
|
128
104
|
validate_title!
|
|
129
105
|
validate_instructions!
|
|
130
|
-
|
|
131
106
|
validate_environment_variables!
|
|
107
|
+
validate_server_logging_transport_constraints!
|
|
132
108
|
end
|
|
133
109
|
|
|
134
110
|
def environment_variables
|
|
@@ -176,6 +152,13 @@ module ModelContextProtocol
|
|
|
176
152
|
end
|
|
177
153
|
end
|
|
178
154
|
|
|
155
|
+
def validate_server_logging_transport_constraints!
|
|
156
|
+
return unless transport_type == :stdio && server_logger.logdev == $stdout
|
|
157
|
+
|
|
158
|
+
raise ModelContextProtocol::Server::ServerLogger::StdoutNotAllowedError,
|
|
159
|
+
"StdioTransport cannot log to stdout. Use stderr or a file instead."
|
|
160
|
+
end
|
|
161
|
+
|
|
179
162
|
def valid_name?
|
|
180
163
|
name&.is_a?(String)
|
|
181
164
|
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
require "singleton"
|
|
2
|
+
|
|
3
|
+
module ModelContextProtocol
|
|
4
|
+
module Server::GlobalConfig
|
|
5
|
+
class ServerLogging
|
|
6
|
+
include Singleton
|
|
7
|
+
|
|
8
|
+
class NotConfiguredError < StandardError
|
|
9
|
+
def initialize
|
|
10
|
+
super("Server logging not configured. Call ModelContextProtocol::Server.configure_server_logging first")
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class LoggerConfig
|
|
15
|
+
attr_accessor :logdev, :level, :formatter, :progname
|
|
16
|
+
|
|
17
|
+
def initialize
|
|
18
|
+
@level = Logger::INFO
|
|
19
|
+
@progname = "MCP-Server"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_h
|
|
23
|
+
{
|
|
24
|
+
logdev: @logdev,
|
|
25
|
+
level: @level,
|
|
26
|
+
formatter: @formatter,
|
|
27
|
+
progname: @progname
|
|
28
|
+
}.compact
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.configure(&block)
|
|
33
|
+
instance.configure(&block)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.configured?
|
|
37
|
+
instance.configured?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.logger_params
|
|
41
|
+
instance.logger_params
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.reset!
|
|
45
|
+
instance.reset!
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def configure(&block)
|
|
49
|
+
raise ArgumentError, "Configuration block required" unless block_given?
|
|
50
|
+
|
|
51
|
+
@config = LoggerConfig.new
|
|
52
|
+
yield(@config)
|
|
53
|
+
@configured = true
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def configured?
|
|
57
|
+
@configured == true
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def logger_params
|
|
61
|
+
raise NotConfiguredError unless configured?
|
|
62
|
+
|
|
63
|
+
@config.to_h
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def reset!
|
|
67
|
+
@configured = false
|
|
68
|
+
@config = nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def initialize
|
|
74
|
+
reset!
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -16,56 +16,78 @@ module ModelContextProtocol
|
|
|
16
16
|
# end
|
|
17
17
|
def progressable(max_duration:, message: nil, &block)
|
|
18
18
|
context = Thread.current[:mcp_context]
|
|
19
|
-
|
|
20
19
|
return yield unless context && context[:progress_token] && context[:transport]
|
|
21
20
|
|
|
22
21
|
progress_token = context[:progress_token]
|
|
23
22
|
transport = context[:transport]
|
|
23
|
+
jsonrpc_request_id = context[:jsonrpc_request_id]
|
|
24
|
+
request_store = context[:request_store]
|
|
25
|
+
stream_id = context[:stream_id]
|
|
26
|
+
|
|
24
27
|
start_time = Time.now
|
|
25
28
|
update_interval = [1.0, max_duration * 0.05].max
|
|
26
29
|
|
|
27
30
|
timer_task = Concurrent::TimerTask.new(execution_interval: update_interval) do
|
|
28
|
-
|
|
29
|
-
progress_pct = [(elapsed_seconds / max_duration) * 100, 99].min
|
|
30
|
-
|
|
31
|
-
progress_message = if message
|
|
32
|
-
"#{message} (#{elapsed_seconds.round(1)}s / ~#{max_duration}s)"
|
|
33
|
-
else
|
|
34
|
-
"Processing... (#{elapsed_seconds.round(1)}s / ~#{max_duration}s)"
|
|
35
|
-
end
|
|
31
|
+
Thread.current[:mcp_context] = {jsonrpc_request_id:}
|
|
36
32
|
|
|
37
33
|
begin
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
34
|
+
if request_store && jsonrpc_request_id
|
|
35
|
+
break if request_store.cancelled?(jsonrpc_request_id)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
elapsed_seconds = Time.now - start_time
|
|
39
|
+
progress_pct = [(elapsed_seconds / max_duration) * 100, 99].min
|
|
40
|
+
|
|
41
|
+
progress_message = if message
|
|
42
|
+
"#{message} (#{elapsed_seconds.round(1)}s / ~#{max_duration}s)"
|
|
43
|
+
else
|
|
44
|
+
"Processing... (#{elapsed_seconds.round(1)}s / ~#{max_duration}s)"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
begin
|
|
48
|
+
transport.send_notification("notifications/progress", {
|
|
49
|
+
progressToken: progress_token,
|
|
50
|
+
progress: progress_pct.round(1),
|
|
51
|
+
total: 100,
|
|
52
|
+
message: progress_message
|
|
53
|
+
}, session_id: stream_id)
|
|
54
|
+
rescue
|
|
55
|
+
break
|
|
56
|
+
end
|
|
47
57
|
|
|
48
|
-
|
|
58
|
+
timer_task.shutdown if elapsed_seconds >= max_duration
|
|
59
|
+
ensure
|
|
60
|
+
Thread.current[:mcp_context] = nil
|
|
61
|
+
end
|
|
49
62
|
end
|
|
50
63
|
|
|
51
64
|
begin
|
|
52
65
|
timer_task.execute
|
|
66
|
+
|
|
53
67
|
result = yield
|
|
54
68
|
|
|
69
|
+
original_context = Thread.current[:mcp_context]
|
|
70
|
+
Thread.current[:mcp_context] = {jsonrpc_request_id:}
|
|
71
|
+
|
|
55
72
|
begin
|
|
56
73
|
transport.send_notification("notifications/progress", {
|
|
57
74
|
progressToken: progress_token,
|
|
58
75
|
progress: 100,
|
|
59
76
|
total: 100,
|
|
60
77
|
message: "Completed"
|
|
61
|
-
})
|
|
78
|
+
}, session_id: stream_id)
|
|
62
79
|
rescue
|
|
63
80
|
nil
|
|
81
|
+
ensure
|
|
82
|
+
Thread.current[:mcp_context] = original_context
|
|
64
83
|
end
|
|
65
84
|
|
|
66
85
|
result
|
|
67
86
|
ensure
|
|
68
|
-
|
|
87
|
+
if timer_task&.running?
|
|
88
|
+
timer_task.shutdown
|
|
89
|
+
sleep(0.1) if timer_task.running?
|
|
90
|
+
end
|
|
69
91
|
end
|
|
70
92
|
end
|
|
71
93
|
end
|