model-context-protocol-rb 0.3.4 → 0.5.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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -1
  3. data/README.md +886 -196
  4. data/lib/model_context_protocol/server/cancellable.rb +54 -0
  5. data/lib/model_context_protocol/server/configuration.rb +80 -8
  6. data/lib/model_context_protocol/server/content.rb +321 -0
  7. data/lib/model_context_protocol/server/content_helpers.rb +84 -0
  8. data/lib/model_context_protocol/server/pagination.rb +71 -0
  9. data/lib/model_context_protocol/server/progressable.rb +72 -0
  10. data/lib/model_context_protocol/server/prompt.rb +108 -14
  11. data/lib/model_context_protocol/server/redis_client_proxy.rb +134 -0
  12. data/lib/model_context_protocol/server/redis_config.rb +108 -0
  13. data/lib/model_context_protocol/server/redis_pool_manager.rb +110 -0
  14. data/lib/model_context_protocol/server/registry.rb +94 -18
  15. data/lib/model_context_protocol/server/resource.rb +98 -25
  16. data/lib/model_context_protocol/server/resource_template.rb +26 -13
  17. data/lib/model_context_protocol/server/router.rb +36 -3
  18. data/lib/model_context_protocol/server/stdio_transport/request_store.rb +102 -0
  19. data/lib/model_context_protocol/server/stdio_transport.rb +31 -6
  20. data/lib/model_context_protocol/server/streamable_http_transport/event_counter.rb +35 -0
  21. data/lib/model_context_protocol/server/streamable_http_transport/message_poller.rb +101 -0
  22. data/lib/model_context_protocol/server/streamable_http_transport/notification_queue.rb +80 -0
  23. data/lib/model_context_protocol/server/streamable_http_transport/request_store.rb +224 -0
  24. data/lib/model_context_protocol/server/streamable_http_transport/session_message_queue.rb +120 -0
  25. data/lib/model_context_protocol/server/{session_store.rb → streamable_http_transport/session_store.rb} +30 -16
  26. data/lib/model_context_protocol/server/streamable_http_transport/stream_registry.rb +119 -0
  27. data/lib/model_context_protocol/server/streamable_http_transport.rb +352 -112
  28. data/lib/model_context_protocol/server/tool.rb +79 -53
  29. data/lib/model_context_protocol/server.rb +124 -21
  30. data/lib/model_context_protocol/version.rb +1 -1
  31. data/tasks/mcp.rake +28 -2
  32. data/tasks/templates/dev-http.erb +288 -0
  33. data/tasks/templates/dev.erb +7 -1
  34. metadata +61 -3
@@ -39,8 +39,8 @@ module ModelContextProtocol
39
39
  end
40
40
 
41
41
  def register(klass)
42
- metadata = klass.metadata
43
- entry = {klass: klass}.merge(metadata)
42
+ definition = klass.definition
43
+ entry = {klass: klass}.merge(definition)
44
44
 
45
45
  case klass.ancestors
46
46
  when ->(ancestors) { ancestors.include?(ModelContextProtocol::Server::Prompt) }
@@ -74,45 +74,121 @@ module ModelContextProtocol
74
74
  find_by_name(@tools, name)
75
75
  end
76
76
 
77
- def prompts_data
78
- PromptsData[prompts: @prompts.map { |entry| entry.except(:klass) }]
77
+ def prompts_data(cursor: nil, page_size: nil, cursor_ttl: nil)
78
+ items = @prompts.map { |entry| entry.except(:klass) }
79
+
80
+ if cursor || page_size
81
+ paginated = Server::Pagination.paginate(
82
+ items,
83
+ cursor: cursor,
84
+ page_size: page_size || 100,
85
+ cursor_ttl: cursor_ttl
86
+ )
87
+
88
+ PromptsData[prompts: paginated.items, next_cursor: paginated.next_cursor]
89
+ else
90
+ PromptsData[prompts: items]
91
+ end
79
92
  end
80
93
 
81
- def resources_data
82
- ResourcesData[resources: @resources.map { |entry| entry.except(:klass) }]
94
+ def resources_data(cursor: nil, page_size: nil, cursor_ttl: nil)
95
+ items = @resources.map { |entry| entry.except(:klass) }
96
+
97
+ if cursor || page_size
98
+ paginated = Server::Pagination.paginate(
99
+ items,
100
+ cursor: cursor,
101
+ page_size: page_size || 100,
102
+ cursor_ttl: cursor_ttl
103
+ )
104
+
105
+ ResourcesData[resources: paginated.items, next_cursor: paginated.next_cursor]
106
+ else
107
+ ResourcesData[resources: items]
108
+ end
83
109
  end
84
110
 
85
- def resource_templates_data
86
- ResourceTemplatesData[resource_templates: @resource_templates.map { |entry| entry.except(:klass, :completions) }]
111
+ def resource_templates_data(cursor: nil, page_size: nil, cursor_ttl: nil)
112
+ items = @resource_templates.map { |entry| entry.except(:klass, :completions) }
113
+
114
+ if cursor || page_size
115
+ paginated = Server::Pagination.paginate(
116
+ items,
117
+ cursor: cursor,
118
+ page_size: page_size || 100,
119
+ cursor_ttl: cursor_ttl
120
+ )
121
+
122
+ ResourceTemplatesData[resource_templates: paginated.items, next_cursor: paginated.next_cursor]
123
+ else
124
+ ResourceTemplatesData[resource_templates: items]
125
+ end
87
126
  end
88
127
 
89
- def tools_data
90
- ToolsData[tools: @tools.map { |entry| entry.except(:klass) }]
128
+ def tools_data(cursor: nil, page_size: nil, cursor_ttl: nil)
129
+ items = @tools.map { |entry| entry.except(:klass) }
130
+
131
+ if cursor || page_size
132
+ paginated = Server::Pagination.paginate(
133
+ items,
134
+ cursor: cursor,
135
+ page_size: page_size || 100,
136
+ cursor_ttl: cursor_ttl
137
+ )
138
+
139
+ ToolsData[tools: paginated.items, next_cursor: paginated.next_cursor]
140
+ else
141
+ ToolsData[tools: items]
142
+ end
91
143
  end
92
144
 
93
145
  private
94
146
 
95
- PromptsData = Data.define(:prompts) do
147
+ PromptsData = Data.define(:prompts, :next_cursor) do
148
+ def initialize(prompts:, next_cursor: nil)
149
+ super
150
+ end
151
+
96
152
  def serialized
97
- {prompts:}
153
+ result = {prompts:}
154
+ result[:nextCursor] = next_cursor if next_cursor
155
+ result
98
156
  end
99
157
  end
100
158
 
101
- ResourcesData = Data.define(:resources) do
159
+ ResourcesData = Data.define(:resources, :next_cursor) do
160
+ def initialize(resources:, next_cursor: nil)
161
+ super
162
+ end
163
+
102
164
  def serialized
103
- {resources:}
165
+ result = {resources:}
166
+ result[:nextCursor] = next_cursor if next_cursor
167
+ result
104
168
  end
105
169
  end
106
170
 
107
- ResourceTemplatesData = Data.define(:resource_templates) do
171
+ ResourceTemplatesData = Data.define(:resource_templates, :next_cursor) do
172
+ def initialize(resource_templates:, next_cursor: nil)
173
+ super
174
+ end
175
+
108
176
  def serialized
109
- {resourceTemplates: resource_templates}
177
+ result = {resourceTemplates: resource_templates}
178
+ result[:nextCursor] = next_cursor if next_cursor
179
+ result
110
180
  end
111
181
  end
112
182
 
113
- ToolsData = Data.define(:tools) do
183
+ ToolsData = Data.define(:tools, :next_cursor) do
184
+ def initialize(tools:, next_cursor: nil)
185
+ super
186
+ end
187
+
114
188
  def serialized
115
- {tools:}
189
+ result = {tools:}
190
+ result[:nextCursor] = next_cursor if next_cursor
191
+ result
116
192
  end
117
193
  end
118
194
 
@@ -1,12 +1,13 @@
1
1
  module ModelContextProtocol
2
2
  class Server::Resource
3
- attr_reader :mime_type, :uri, :context, :logger
3
+ include ModelContextProtocol::Server::Cancellable
4
+ include ModelContextProtocol::Server::Progressable
4
5
 
5
- def initialize(logger, context = {})
6
+ attr_reader :mime_type, :uri
7
+
8
+ def initialize
6
9
  @mime_type = self.class.mime_type
7
10
  @uri = self.class.uri
8
- @context = context
9
- @logger = logger
10
11
  end
11
12
 
12
13
  def call
@@ -15,59 +16,76 @@ module ModelContextProtocol
15
16
 
16
17
  TextResponse = Data.define(:resource, :text) do
17
18
  def serialized
18
- {contents: [{mimeType: resource.mime_type, text:, uri: resource.uri}]}
19
+ content = {mimeType: resource.mime_type, text:, uri: resource.uri}
20
+ content[:title] = resource.class.title if resource.class.title
21
+ annotations = resource.class.annotations&.serialized
22
+ content[:annotations] = annotations if annotations
23
+ {contents: [content]}
19
24
  end
20
25
  end
21
26
  private_constant :TextResponse
22
27
 
23
28
  BinaryResponse = Data.define(:blob, :resource) do
24
29
  def serialized
25
- {contents: [{blob:, mimeType: resource.mime_type, uri: resource.uri}]}
30
+ content = {blob:, mimeType: resource.mime_type, uri: resource.uri}
31
+ content[:title] = resource.class.title if resource.class.title
32
+ annotations = resource.class.annotations&.serialized
33
+ content[:annotations] = annotations if annotations
34
+ {contents: [content]}
26
35
  end
27
36
  end
28
37
  private_constant :BinaryResponse
29
38
 
30
- private def respond_with(type, **options)
31
- case [type, options]
32
- in [:text, {text:}]
39
+ private def respond_with(**kwargs)
40
+ case [kwargs]
41
+ in [{text:}]
33
42
  TextResponse[resource: self, text:]
34
- in [:binary, {blob:}]
35
- BinaryResponse[blob:, resource: self]
43
+ in [{binary:}]
44
+ BinaryResponse[blob: binary, resource: self]
36
45
  else
37
- raise ModelContextProtocol::Server::ResponseArgumentsError, "Invalid arguments: #{type}, #{options}"
46
+ raise ModelContextProtocol::Server::ResponseArgumentsError, "Invalid arguments: #{options}"
38
47
  end
39
48
  end
40
49
 
41
50
  class << self
42
- attr_reader :name, :description, :mime_type, :uri
51
+ attr_reader :name, :description, :title, :mime_type, :uri, :annotations
43
52
 
44
- def with_metadata(&block)
45
- metadata_dsl = MetadataDSL.new
46
- metadata_dsl.instance_eval(&block)
53
+ def define(&block)
54
+ definition_dsl = DefinitionDSL.new
55
+ definition_dsl.instance_eval(&block)
47
56
 
48
- @name = metadata_dsl.name
49
- @description = metadata_dsl.description
50
- @mime_type = metadata_dsl.mime_type
51
- @uri = metadata_dsl.uri
57
+ @name = definition_dsl.name
58
+ @description = definition_dsl.description
59
+ @title = definition_dsl.title
60
+ @mime_type = definition_dsl.mime_type
61
+ @uri = definition_dsl.uri
62
+ @annotations = definition_dsl.defined_annotations
52
63
  end
53
64
 
54
65
  def inherited(subclass)
55
66
  subclass.instance_variable_set(:@name, @name)
56
67
  subclass.instance_variable_set(:@description, @description)
68
+ subclass.instance_variable_set(:@title, @title)
57
69
  subclass.instance_variable_set(:@mime_type, @mime_type)
58
70
  subclass.instance_variable_set(:@uri, @uri)
71
+ subclass.instance_variable_set(:@annotations, @annotations&.dup)
59
72
  end
60
73
 
61
- def call(logger, context = {})
62
- new(logger, context).call
74
+ def call
75
+ new.call
63
76
  end
64
77
 
65
- def metadata
66
- {name: @name, description: @description, mimeType: @mime_type, uri: @uri}
78
+ def definition
79
+ result = {name: @name, description: @description, mimeType: @mime_type, uri: @uri}
80
+ result[:title] = @title if @title
81
+ result[:annotations] = @annotations.serialized if @annotations
82
+ result
67
83
  end
68
84
  end
69
85
 
70
- class MetadataDSL
86
+ class DefinitionDSL
87
+ attr_reader :defined_annotations
88
+
71
89
  def name(value = nil)
72
90
  @name = value if value
73
91
  @name
@@ -78,6 +96,11 @@ module ModelContextProtocol
78
96
  @description
79
97
  end
80
98
 
99
+ def title(value = nil)
100
+ @title = value if value
101
+ @title
102
+ end
103
+
81
104
  def mime_type(value = nil)
82
105
  @mime_type = value if value
83
106
  @mime_type
@@ -87,6 +110,56 @@ module ModelContextProtocol
87
110
  @uri = value if value
88
111
  @uri
89
112
  end
113
+
114
+ def annotations(&block)
115
+ @defined_annotations = AnnotationsDSL.new
116
+ @defined_annotations.instance_eval(&block)
117
+ @defined_annotations
118
+ end
119
+ end
120
+
121
+ class AnnotationsDSL
122
+ VALID_AUDIENCE_VALUES = [:user, :assistant].freeze
123
+
124
+ def initialize
125
+ @audience = nil
126
+ @priority = nil
127
+ @last_modified = nil
128
+ end
129
+
130
+ def audience(value)
131
+ normalized_value = Array(value).map(&:to_sym)
132
+ invalid_values = normalized_value - VALID_AUDIENCE_VALUES
133
+ unless invalid_values.empty?
134
+ raise ArgumentError, "Invalid audience values: #{invalid_values.join(", ")}. Valid values are: #{VALID_AUDIENCE_VALUES.join(", ")}"
135
+ end
136
+ @audience = normalized_value.map(&:to_s)
137
+ end
138
+
139
+ def priority(value)
140
+ unless value.is_a?(Numeric) && value >= 0.0 && value <= 1.0
141
+ raise ArgumentError, "Priority must be a number between 0.0 and 1.0, got: #{value}"
142
+ end
143
+ @priority = value.to_f
144
+ end
145
+
146
+ def last_modified(value)
147
+ # Validate ISO 8601 format
148
+ begin
149
+ Time.iso8601(value)
150
+ rescue ArgumentError
151
+ raise ArgumentError, "lastModified must be in ISO 8601 format (e.g., '2025-01-12T15:00:58Z'), got: #{value}"
152
+ end
153
+ @last_modified = value
154
+ end
155
+
156
+ def serialized
157
+ result = {}
158
+ result[:audience] = @audience if @audience
159
+ result[:priority] = @priority if @priority
160
+ result[:lastModified] = @last_modified if @last_modified
161
+ result.empty? ? nil : result
162
+ end
90
163
  end
91
164
  end
92
165
  end
@@ -3,15 +3,15 @@ module ModelContextProtocol
3
3
  class << self
4
4
  attr_reader :name, :description, :mime_type, :uri_template, :completions
5
5
 
6
- def with_metadata(&block)
7
- metadata_dsl = MetadataDSL.new
8
- metadata_dsl.instance_eval(&block)
9
-
10
- @name = metadata_dsl.name
11
- @description = metadata_dsl.description
12
- @mime_type = metadata_dsl.mime_type
13
- @uri_template = metadata_dsl.uri_template
14
- @completions = metadata_dsl.completions
6
+ def define(&block)
7
+ definition_dsl = DefinitionDSL.new
8
+ definition_dsl.instance_eval(&block)
9
+
10
+ @name = definition_dsl.name
11
+ @description = definition_dsl.description
12
+ @mime_type = definition_dsl.mime_type
13
+ @uri_template = definition_dsl.uri_template
14
+ @completions = definition_dsl.completions
15
15
  end
16
16
 
17
17
  def inherited(subclass)
@@ -32,7 +32,7 @@ module ModelContextProtocol
32
32
  completion.call(param_name.to_s, value)
33
33
  end
34
34
 
35
- def metadata
35
+ def definition
36
36
  {
37
37
  name: @name,
38
38
  description: @description,
@@ -43,7 +43,7 @@ module ModelContextProtocol
43
43
  end
44
44
  end
45
45
 
46
- class MetadataDSL
46
+ class DefinitionDSL
47
47
  attr_reader :completions
48
48
 
49
49
  def initialize
@@ -85,8 +85,21 @@ module ModelContextProtocol
85
85
  @completions = {}
86
86
  end
87
87
 
88
- def completion(param_name, completion_class)
89
- @completions[param_name.to_s] = completion_class
88
+ def completion(param_name, completion_class_or_values)
89
+ @completions[param_name.to_s] = if completion_class_or_values.is_a?(Array)
90
+ create_array_completion(completion_class_or_values)
91
+ else
92
+ completion_class_or_values
93
+ end
94
+ end
95
+
96
+ private
97
+
98
+ def create_array_completion(values)
99
+ ModelContextProtocol::Server::Completion.define do
100
+ filtered_values = values.grep(/#{argument_value}/)
101
+ respond_with values: filtered_values
102
+ end
90
103
  end
91
104
  end
92
105
  end
@@ -1,3 +1,5 @@
1
+ require_relative "cancellable"
2
+
1
3
  module ModelContextProtocol
2
4
  class Server::Router
3
5
  # Raised when an invalid method is provided.
@@ -12,14 +14,45 @@ module ModelContextProtocol
12
14
  @handlers[method] = handler
13
15
  end
14
16
 
15
- def route(message)
17
+ # Route a message to its handler with request tracking support
18
+ #
19
+ # @param message [Hash] the JSON-RPC message
20
+ # @param request_store [Object] the request store for tracking cancellation
21
+ # @param session_id [String, nil] the session ID for HTTP transport
22
+ # @param transport [Object, nil] the transport for sending notifications
23
+ # @return [Object] the handler result, or nil if cancelled
24
+ def route(message, request_store: nil, session_id: nil, transport: nil)
16
25
  method = message["method"]
17
26
  handler = @handlers[method]
18
27
  raise MethodNotFoundError, "Method not found: #{method}" unless handler
19
28
 
20
- with_environment(@configuration&.environment_variables) do
21
- handler.call(message)
29
+ request_id = message["id"]
30
+ progress_token = message.dig("params", "_meta", "progressToken")
31
+
32
+ if request_id && request_store
33
+ request_store.register_request(request_id, session_id)
34
+ end
35
+
36
+ result = nil
37
+ begin
38
+ with_environment(@configuration&.environment_variables) do
39
+ context = {request_id:, request_store:, session_id:, progress_token:, transport:}
40
+
41
+ Thread.current[:mcp_context] = context
42
+
43
+ result = handler.call(message)
44
+ end
45
+ rescue Server::Cancellable::CancellationError
46
+ return nil
47
+ ensure
48
+ if request_id && request_store
49
+ request_store.unregister_request(request_id)
50
+ end
51
+
52
+ Thread.current[:mcp_context] = nil
22
53
  end
54
+
55
+ result
23
56
  end
24
57
 
25
58
  private
@@ -0,0 +1,102 @@
1
+ module ModelContextProtocol
2
+ class Server::StdioTransport
3
+ # Thread-safe in-memory storage for tracking active requests and their cancellation status.
4
+ # This store is used by StdioTransport to manage request lifecycle and handle cancellation.
5
+ class RequestStore
6
+ def initialize
7
+ @mutex = Mutex.new
8
+ @requests = {}
9
+ end
10
+
11
+ # Register a new request with its associated thread
12
+ #
13
+ # @param request_id [String] the unique request identifier
14
+ # @param thread [Thread] the thread processing this request (defaults to current thread)
15
+ # @return [void]
16
+ def register_request(request_id, thread = Thread.current)
17
+ @mutex.synchronize do
18
+ @requests[request_id] = {
19
+ thread:,
20
+ cancelled: false,
21
+ started_at: Time.now
22
+ }
23
+ end
24
+ end
25
+
26
+ # Mark a request as cancelled
27
+ #
28
+ # @param request_id [String] the unique request identifier
29
+ # @return [Boolean] true if request was found and marked cancelled, false otherwise
30
+ def mark_cancelled(request_id)
31
+ @mutex.synchronize do
32
+ if (request = @requests[request_id])
33
+ request[:cancelled] = true
34
+ return true
35
+ end
36
+ false
37
+ end
38
+ end
39
+
40
+ # Check if a request has been cancelled
41
+ #
42
+ # @param request_id [String] the unique request identifier
43
+ # @return [Boolean] true if the request is cancelled, false otherwise
44
+ def cancelled?(request_id)
45
+ @mutex.synchronize do
46
+ @requests[request_id]&.fetch(:cancelled, false) || false
47
+ end
48
+ end
49
+
50
+ # Unregister a request (typically called when request completes)
51
+ #
52
+ # @param request_id [String] the unique request identifier
53
+ # @return [Hash, nil] the removed request data, or nil if not found
54
+ def unregister_request(request_id)
55
+ @mutex.synchronize do
56
+ @requests.delete(request_id)
57
+ end
58
+ end
59
+
60
+ # Get information about a specific request
61
+ #
62
+ # @param request_id [String] the unique request identifier
63
+ # @return [Hash, nil] request information or nil if not found
64
+ def get_request(request_id)
65
+ @mutex.synchronize do
66
+ @requests[request_id]&.dup
67
+ end
68
+ end
69
+
70
+ # Get all active request IDs
71
+ #
72
+ # @return [Array<String>] list of active request IDs
73
+ def active_requests
74
+ @mutex.synchronize do
75
+ @requests.keys.dup
76
+ end
77
+ end
78
+
79
+ # Clean up old requests (useful for preventing memory leaks)
80
+ #
81
+ # @param max_age_seconds [Integer] maximum age of requests to keep
82
+ # @return [Array<String>] list of cleaned up request IDs
83
+ def cleanup_old_requests(max_age_seconds = 300)
84
+ cutoff_time = Time.now - max_age_seconds
85
+ removed_ids = []
86
+
87
+ @mutex.synchronize do
88
+ @requests.delete_if do |request_id, data|
89
+ if data[:started_at] < cutoff_time
90
+ removed_ids << request_id
91
+ true
92
+ else
93
+ false
94
+ end
95
+ end
96
+ end
97
+
98
+ removed_ids
99
+ end
100
+ end
101
+ end
102
+ end
@@ -1,3 +1,5 @@
1
+ require_relative "stdio_transport/request_store"
2
+
1
3
  module ModelContextProtocol
2
4
  class Server::StdioTransport
3
5
  Response = Data.define(:id, :result) do
@@ -12,15 +14,15 @@ module ModelContextProtocol
12
14
  end
13
15
  end
14
16
 
15
- attr_reader :router, :configuration
17
+ attr_reader :router, :configuration, :request_store
16
18
 
17
19
  def initialize(router:, configuration:)
18
20
  @router = router
19
21
  @configuration = configuration
22
+ @request_store = RequestStore.new
20
23
  end
21
24
 
22
25
  def handle
23
- # Connect logger to transport
24
26
  @configuration.logger.connect_transport(self)
25
27
 
26
28
  loop do
@@ -29,10 +31,19 @@ module ModelContextProtocol
29
31
 
30
32
  begin
31
33
  message = JSON.parse(line.chomp)
32
- next if message["method"].start_with?("notifications")
33
34
 
34
- result = router.route(message)
35
- send_message(Response[id: message["id"], result: result.serialized])
35
+ if message["method"] == "notifications/cancelled"
36
+ handle_cancellation(message)
37
+ next
38
+ end
39
+
40
+ next if message["method"]&.start_with?("notifications/")
41
+
42
+ result = router.route(message, request_store: @request_store, transport: self)
43
+
44
+ if result
45
+ send_message(Response[id: message["id"], result: result.serialized])
46
+ end
36
47
  rescue ModelContextProtocol::Server::ParameterValidationError => validation_error
37
48
  @configuration.logger.error("Validation error", error: validation_error.message)
38
49
  send_message(
@@ -61,12 +72,26 @@ module ModelContextProtocol
61
72
  $stdout.puts(JSON.generate(notification))
62
73
  $stdout.flush
63
74
  rescue IOError => e
64
- # Handle broken pipe gracefully
65
75
  @configuration.logger.debug("Failed to send notification", error: e.message) if @configuration.logging_enabled?
66
76
  end
67
77
 
68
78
  private
69
79
 
80
+ # Handle a cancellation notification from the client
81
+ #
82
+ # @param message [Hash] the cancellation notification message
83
+ def handle_cancellation(message)
84
+ params = message["params"]
85
+ return unless params
86
+
87
+ request_id = params["requestId"]
88
+ return unless request_id
89
+
90
+ @request_store.mark_cancelled(request_id)
91
+ rescue
92
+ nil
93
+ end
94
+
70
95
  def receive_message
71
96
  $stdin.gets
72
97
  end
@@ -0,0 +1,35 @@
1
+ module ModelContextProtocol
2
+ class Server::StreamableHttpTransport
3
+ class EventCounter
4
+ COUNTER_KEY_PREFIX = "event_counter:"
5
+
6
+ def initialize(redis_client, server_instance)
7
+ @redis = redis_client
8
+ @server_instance = server_instance
9
+ @counter_key = "#{COUNTER_KEY_PREFIX}#{server_instance}"
10
+
11
+ if @redis.exists(@counter_key) == 0
12
+ @redis.set(@counter_key, 0)
13
+ end
14
+ end
15
+
16
+ def next_event_id
17
+ count = @redis.incr(@counter_key)
18
+ "#{@server_instance}-#{count}"
19
+ end
20
+
21
+ def current_count
22
+ count = @redis.get(@counter_key)
23
+ count ? count.to_i : 0
24
+ end
25
+
26
+ def reset
27
+ @redis.set(@counter_key, 0)
28
+ end
29
+
30
+ def set_count(value)
31
+ @redis.set(@counter_key, value.to_i)
32
+ end
33
+ end
34
+ end
35
+ end