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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8fd0681c65b47196375ec7f4934cadf226ed01b1b289caf296348698808ef5cb
4
- data.tar.gz: 4ce234855d56b724b6f69b97851ff1ee4db65370b7361ea0a65c43a929c448b7
3
+ metadata.gz: f0f2318890bd52a6ade4d17652273ed887c62cc2badab13aabeb79a993c34a11
4
+ data.tar.gz: d85af9411a3ebb83ca4938c61cd586d9511152b11aea9b91e0765493fd39508b
5
5
  SHA512:
6
- metadata.gz: b3bafa3fe92aa05ff380ad5fc311c0143d5e1bb6c3a91e50893115eb01ad2ea8b2d93dde89240815b50629fcb3b3b3467585766ac42d2c7087cd0be55b431455
7
- data.tar.gz: 9d1c4f1c5f93352bf9392f9aeaf63253534348787aecb0866abe160dbf7cebc302bfec751955b19d0a06d9d60675c450960d401af2538aa9b100fe076bc7fd94
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.5.0...HEAD
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 list_changed: true do
249
+ prompts do
253
250
  register TestPrompt
254
251
  end
255
252
 
256
- resources list_changed: true, subscribe: true do
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 list_changed: true do
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 list_changed: true do
388
+ prompts do
367
389
  register MyPrompt
368
390
  register AnotherPrompt
369
391
  end
370
392
 
371
- resources list_changed: true, subscribe: true do
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 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.
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 `logger` and passing a string message.
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
- | `logger` | Within `call` | Logger instance for logging (e.g., `logger.info("message")`) |
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 logger
509
- logger.info("Brainstorming excuses...")
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 are stateless and only have access to their configured properties.
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 response. Arguments from the MCP client and server context are available, along with logging capabilities.
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
- | `logger` | Within `call` | Logger instance for logging (e.g., `logger.info("message")`) |
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
- logger.info("Initiating request for user #{user_id}...")
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
- logger.info("Getting weather data for #{location}...")
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
- logger.info("Silly user doesn't know how to double a number")
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
- logger.info("Getting comprehensive temperature history data")
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
- logger.info("Starting 3 second sleep operation")
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
- logger.info("Starting progressable call with max_duration=#{max_duration}, work_steps=#{work_steps}")
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[:request_id]
28
- if context[:request_store].cancelled?(context[:request_id])
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[:request_id]
39
- if context[:request_store].cancelled?(context[:request_id])
40
- raise CancellationError, "Request #{context[:request_id]} was cancelled"
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::MCPLogger
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, :enabled
34
+ attr_reader :logger_name
33
35
 
34
- def initialize(logger_name: "server", level: "info", enabled: true)
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 if @enabled
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 && @enabled
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 :logger
28
+ attr_reader :client_logger, :server_logger
37
29
 
38
30
  def initialize
39
- @logging_enabled = true
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: @default_log_level,
44
- enabled: @logging_enabled
33
+ level: "info"
45
34
  )
46
- end
47
35
 
48
- def logging_enabled?
49
- @logging_enabled
50
- end
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
- @default_log_level = level.to_s
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
- elapsed_seconds = Time.now - start_time
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
- transport.send_notification("notifications/progress", {
39
- progressToken: progress_token,
40
- progress: progress_pct.round(1),
41
- total: 100,
42
- message: progress_message
43
- })
44
- rescue
45
- nil
46
- end
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
- timer_task.shutdown if elapsed_seconds >= max_duration
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
- timer_task&.shutdown if timer_task&.running?
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