model-context-protocol-rb 0.5.0 → 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 +18 -1
- data/README.md +262 -191
- 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 +423 -167
- 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
|
@@ -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
|
|
@@ -4,13 +4,14 @@ module ModelContextProtocol
|
|
|
4
4
|
include ModelContextProtocol::Server::ContentHelpers
|
|
5
5
|
include ModelContextProtocol::Server::Progressable
|
|
6
6
|
|
|
7
|
-
attr_reader :arguments, :context, :
|
|
7
|
+
attr_reader :arguments, :context, :client_logger, :server_logger
|
|
8
8
|
|
|
9
|
-
def initialize(arguments,
|
|
9
|
+
def initialize(arguments, client_logger, server_logger, context = {})
|
|
10
10
|
validate!(arguments)
|
|
11
11
|
@arguments = arguments
|
|
12
12
|
@context = context
|
|
13
|
-
@
|
|
13
|
+
@client_logger = client_logger
|
|
14
|
+
@server_logger = server_logger
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
def call
|
|
@@ -90,8 +91,8 @@ module ModelContextProtocol
|
|
|
90
91
|
subclass.instance_variable_set(:@defined_arguments, @defined_arguments&.dup)
|
|
91
92
|
end
|
|
92
93
|
|
|
93
|
-
def call(arguments,
|
|
94
|
-
new(arguments,
|
|
94
|
+
def call(arguments, client_logger, server_logger, context = {})
|
|
95
|
+
new(arguments, client_logger, server_logger, context).call
|
|
95
96
|
rescue ArgumentError => error
|
|
96
97
|
raise ModelContextProtocol::Server::ParameterValidationError, error.message
|
|
97
98
|
end
|
|
@@ -127,8 +128,12 @@ module ModelContextProtocol
|
|
|
127
128
|
@prompt_instance.context
|
|
128
129
|
end
|
|
129
130
|
|
|
130
|
-
def
|
|
131
|
-
@prompt_instance.
|
|
131
|
+
def client_logger
|
|
132
|
+
@prompt_instance.client_logger
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def server_logger
|
|
136
|
+
@prompt_instance.server_logger
|
|
132
137
|
end
|
|
133
138
|
|
|
134
139
|
def user_message(&block)
|
|
@@ -3,11 +3,14 @@ module ModelContextProtocol
|
|
|
3
3
|
include ModelContextProtocol::Server::Cancellable
|
|
4
4
|
include ModelContextProtocol::Server::Progressable
|
|
5
5
|
|
|
6
|
-
attr_reader :mime_type, :uri
|
|
6
|
+
attr_reader :mime_type, :uri, :client_logger, :server_logger, :context
|
|
7
7
|
|
|
8
|
-
def initialize
|
|
8
|
+
def initialize(client_logger, server_logger, context = {})
|
|
9
9
|
@mime_type = self.class.mime_type
|
|
10
10
|
@uri = self.class.uri
|
|
11
|
+
@client_logger = client_logger
|
|
12
|
+
@server_logger = server_logger
|
|
13
|
+
@context = context
|
|
11
14
|
end
|
|
12
15
|
|
|
13
16
|
def call
|
|
@@ -71,8 +74,8 @@ module ModelContextProtocol
|
|
|
71
74
|
subclass.instance_variable_set(:@annotations, @annotations&.dup)
|
|
72
75
|
end
|
|
73
76
|
|
|
74
|
-
def call
|
|
75
|
-
new.call
|
|
77
|
+
def call(client_logger, server_logger, context = {})
|
|
78
|
+
new(client_logger, server_logger, context).call
|
|
76
79
|
end
|
|
77
80
|
|
|
78
81
|
def definition
|
|
@@ -20,23 +20,24 @@ module ModelContextProtocol
|
|
|
20
20
|
# @param request_store [Object] the request store for tracking cancellation
|
|
21
21
|
# @param session_id [String, nil] the session ID for HTTP transport
|
|
22
22
|
# @param transport [Object, nil] the transport for sending notifications
|
|
23
|
+
# @param stream_id [String, nil] the specific stream ID for targeted notifications
|
|
23
24
|
# @return [Object] the handler result, or nil if cancelled
|
|
24
|
-
def route(message, request_store: nil, session_id: nil, transport: nil)
|
|
25
|
+
def route(message, request_store: nil, session_id: nil, transport: nil, stream_id: nil)
|
|
25
26
|
method = message["method"]
|
|
26
27
|
handler = @handlers[method]
|
|
27
28
|
raise MethodNotFoundError, "Method not found: #{method}" unless handler
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
jsonrpc_request_id = message["id"]
|
|
30
31
|
progress_token = message.dig("params", "_meta", "progressToken")
|
|
31
32
|
|
|
32
|
-
if
|
|
33
|
-
request_store.register_request(
|
|
33
|
+
if jsonrpc_request_id && request_store
|
|
34
|
+
request_store.register_request(jsonrpc_request_id, session_id)
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
result = nil
|
|
37
38
|
begin
|
|
38
39
|
with_environment(@configuration&.environment_variables) do
|
|
39
|
-
context = {
|
|
40
|
+
context = {jsonrpc_request_id:, request_store:, session_id:, progress_token:, transport:, stream_id:}
|
|
40
41
|
|
|
41
42
|
Thread.current[:mcp_context] = context
|
|
42
43
|
|
|
@@ -45,8 +46,8 @@ module ModelContextProtocol
|
|
|
45
46
|
rescue Server::Cancellable::CancellationError
|
|
46
47
|
return nil
|
|
47
48
|
ensure
|
|
48
|
-
if
|
|
49
|
-
request_store.unregister_request(
|
|
49
|
+
if jsonrpc_request_id && request_store
|
|
50
|
+
request_store.unregister_request(jsonrpc_request_id)
|
|
50
51
|
end
|
|
51
52
|
|
|
52
53
|
Thread.current[:mcp_context] = nil
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require "logger"
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
module ModelContextProtocol
|
|
5
|
+
class Server::ServerLogger < Logger
|
|
6
|
+
class StdoutNotAllowedError < StandardError; end
|
|
7
|
+
|
|
8
|
+
attr_reader :logdev
|
|
9
|
+
|
|
10
|
+
def initialize(logdev: $stderr, level: Logger::INFO, formatter: nil, progname: "MCP-Server")
|
|
11
|
+
super(logdev)
|
|
12
|
+
@logdev = logdev
|
|
13
|
+
|
|
14
|
+
self.level = level
|
|
15
|
+
self.progname = progname
|
|
16
|
+
|
|
17
|
+
self.formatter = formatter || proc do |severity, datetime, progname, msg|
|
|
18
|
+
timestamp = datetime.strftime("%Y-%m-%d %H:%M:%S.%3N")
|
|
19
|
+
prog_name = progname ? "[#{progname}]" : ""
|
|
20
|
+
mcp_context = Thread.current[:mcp_context]
|
|
21
|
+
request_id = mcp_context&.dig(:jsonrpc_request_id)
|
|
22
|
+
request_id_str = request_id ? " [#{request_id}]" : ""
|
|
23
|
+
|
|
24
|
+
"[#{timestamp}] #{prog_name}#{request_id_str} #{severity}: #{msg}\n"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -10,12 +10,12 @@ module ModelContextProtocol
|
|
|
10
10
|
|
|
11
11
|
# Register a new request with its associated thread
|
|
12
12
|
#
|
|
13
|
-
# @param
|
|
13
|
+
# @param jsonrpc_request_id [String] the unique JSON-RPC request identifier
|
|
14
14
|
# @param thread [Thread] the thread processing this request (defaults to current thread)
|
|
15
15
|
# @return [void]
|
|
16
|
-
def register_request(
|
|
16
|
+
def register_request(jsonrpc_request_id, thread = Thread.current)
|
|
17
17
|
@mutex.synchronize do
|
|
18
|
-
@requests[
|
|
18
|
+
@requests[jsonrpc_request_id] = {
|
|
19
19
|
thread:,
|
|
20
20
|
cancelled: false,
|
|
21
21
|
started_at: Time.now
|
|
@@ -25,11 +25,11 @@ module ModelContextProtocol
|
|
|
25
25
|
|
|
26
26
|
# Mark a request as cancelled
|
|
27
27
|
#
|
|
28
|
-
# @param
|
|
28
|
+
# @param jsonrpc_request_id [String] the unique JSON-RPC request identifier
|
|
29
29
|
# @return [Boolean] true if request was found and marked cancelled, false otherwise
|
|
30
|
-
def mark_cancelled(
|
|
30
|
+
def mark_cancelled(jsonrpc_request_id)
|
|
31
31
|
@mutex.synchronize do
|
|
32
|
-
if (request = @requests[
|
|
32
|
+
if (request = @requests[jsonrpc_request_id])
|
|
33
33
|
request[:cancelled] = true
|
|
34
34
|
return true
|
|
35
35
|
end
|
|
@@ -39,31 +39,31 @@ module ModelContextProtocol
|
|
|
39
39
|
|
|
40
40
|
# Check if a request has been cancelled
|
|
41
41
|
#
|
|
42
|
-
# @param
|
|
42
|
+
# @param jsonrpc_request_id [String] the unique JSON-RPC request identifier
|
|
43
43
|
# @return [Boolean] true if the request is cancelled, false otherwise
|
|
44
|
-
def cancelled?(
|
|
44
|
+
def cancelled?(jsonrpc_request_id)
|
|
45
45
|
@mutex.synchronize do
|
|
46
|
-
@requests[
|
|
46
|
+
@requests[jsonrpc_request_id]&.fetch(:cancelled, false) || false
|
|
47
47
|
end
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
# Unregister a request (typically called when request completes)
|
|
51
51
|
#
|
|
52
|
-
# @param
|
|
52
|
+
# @param jsonrpc_request_id [String] the unique JSON-RPC request identifier
|
|
53
53
|
# @return [Hash, nil] the removed request data, or nil if not found
|
|
54
|
-
def unregister_request(
|
|
54
|
+
def unregister_request(jsonrpc_request_id)
|
|
55
55
|
@mutex.synchronize do
|
|
56
|
-
@requests.delete(
|
|
56
|
+
@requests.delete(jsonrpc_request_id)
|
|
57
57
|
end
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
# Get information about a specific request
|
|
61
61
|
#
|
|
62
|
-
# @param
|
|
62
|
+
# @param jsonrpc_request_id [String] the unique JSON-RPC request identifier
|
|
63
63
|
# @return [Hash, nil] request information or nil if not found
|
|
64
|
-
def get_request(
|
|
64
|
+
def get_request(jsonrpc_request_id)
|
|
65
65
|
@mutex.synchronize do
|
|
66
|
-
@requests[
|
|
66
|
+
@requests[jsonrpc_request_id]&.dup
|
|
67
67
|
end
|
|
68
68
|
end
|
|
69
69
|
|
|
@@ -85,9 +85,9 @@ module ModelContextProtocol
|
|
|
85
85
|
removed_ids = []
|
|
86
86
|
|
|
87
87
|
@mutex.synchronize do
|
|
88
|
-
@requests.delete_if do |
|
|
88
|
+
@requests.delete_if do |jsonrpc_request_id, data|
|
|
89
89
|
if data[:started_at] < cutoff_time
|
|
90
|
-
removed_ids <<
|
|
90
|
+
removed_ids << jsonrpc_request_id
|
|
91
91
|
true
|
|
92
92
|
else
|
|
93
93
|
false
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
require_relative "stdio_transport/request_store"
|
|
2
|
-
|
|
3
1
|
module ModelContextProtocol
|
|
4
2
|
class Server::StdioTransport
|
|
5
3
|
Response = Data.define(:id, :result) do
|
|
@@ -14,16 +12,19 @@ module ModelContextProtocol
|
|
|
14
12
|
end
|
|
15
13
|
end
|
|
16
14
|
|
|
17
|
-
attr_reader :router, :configuration, :request_store
|
|
15
|
+
attr_reader :router, :client_logger, :server_logger, :configuration, :request_store
|
|
18
16
|
|
|
19
17
|
def initialize(router:, configuration:)
|
|
20
18
|
@router = router
|
|
21
19
|
@configuration = configuration
|
|
22
|
-
@
|
|
20
|
+
@client_logger = configuration.client_logger
|
|
21
|
+
@server_logger = configuration.server_logger
|
|
22
|
+
@request_store = ModelContextProtocol::Server::StdioTransport::RequestStore.new
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def handle
|
|
26
|
-
|
|
26
|
+
client_logger.connect_transport(self)
|
|
27
|
+
server_logger.info("Starting stdio transport handler")
|
|
27
28
|
|
|
28
29
|
loop do
|
|
29
30
|
line = receive_message
|
|
@@ -45,17 +46,21 @@ module ModelContextProtocol
|
|
|
45
46
|
send_message(Response[id: message["id"], result: result.serialized])
|
|
46
47
|
end
|
|
47
48
|
rescue ModelContextProtocol::Server::ParameterValidationError => validation_error
|
|
48
|
-
|
|
49
|
+
client_logger.error("Validation error", error: validation_error.message)
|
|
50
|
+
server_logger.error("Parameter validation failed in stdio transport: #{validation_error.message}")
|
|
49
51
|
send_message(
|
|
50
52
|
ErrorResponse[id: message["id"], error: {code: -32602, message: validation_error.message}]
|
|
51
53
|
)
|
|
52
54
|
rescue JSON::ParserError => parser_error
|
|
53
|
-
|
|
55
|
+
client_logger.error("Parser error", error: parser_error.message)
|
|
56
|
+
server_logger.error("JSON parsing failed in stdio transport: #{parser_error.message}")
|
|
54
57
|
send_message(
|
|
55
58
|
ErrorResponse[id: "", error: {code: -32700, message: parser_error.message}]
|
|
56
59
|
)
|
|
57
60
|
rescue => error
|
|
58
|
-
|
|
61
|
+
client_logger.error("Internal error", error: error.message, backtrace: error.backtrace.first(5))
|
|
62
|
+
server_logger.error("Internal error in stdio transport: #{error.message}")
|
|
63
|
+
server_logger.debug("Backtrace: #{error.backtrace.join("\n")}")
|
|
59
64
|
send_message(
|
|
60
65
|
ErrorResponse[id: message["id"], error: {code: -32603, message: error.message}]
|
|
61
66
|
)
|
|
@@ -72,7 +77,8 @@ module ModelContextProtocol
|
|
|
72
77
|
$stdout.puts(JSON.generate(notification))
|
|
73
78
|
$stdout.flush
|
|
74
79
|
rescue IOError => e
|
|
75
|
-
@configuration.
|
|
80
|
+
@configuration.client_logger.debug("Failed to send notification", error: e.message)
|
|
81
|
+
@configuration.server_logger.debug("Failed to send notification via stdio: #{e.message}")
|
|
76
82
|
end
|
|
77
83
|
|
|
78
84
|
private
|
|
@@ -84,10 +90,10 @@ module ModelContextProtocol
|
|
|
84
90
|
params = message["params"]
|
|
85
91
|
return unless params
|
|
86
92
|
|
|
87
|
-
|
|
88
|
-
return unless
|
|
93
|
+
jsonrpc_request_id = params["requestId"]
|
|
94
|
+
return unless jsonrpc_request_id
|
|
89
95
|
|
|
90
|
-
@request_store.mark_cancelled(
|
|
96
|
+
@request_store.mark_cancelled(jsonrpc_request_id)
|
|
91
97
|
rescue
|
|
92
98
|
nil
|
|
93
99
|
end
|