vector_mcp 0.3.4 → 0.4.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.
@@ -53,6 +53,16 @@ module VectorMCP
53
53
  DEFAULT_EVENT_RETENTION = 100 # Keep last 100 events for resumability
54
54
  DEFAULT_REQUEST_TIMEOUT = 30 # Default timeout for server-initiated requests
55
55
 
56
+ # Default allowed origins — restrict to localhost by default for security.
57
+ DEFAULT_ALLOWED_ORIGINS = %w[
58
+ http://localhost
59
+ https://localhost
60
+ http://127.0.0.1
61
+ https://127.0.0.1
62
+ http://[::1]
63
+ https://[::1]
64
+ ].freeze
65
+
56
66
  # Initializes a new HTTP Stream transport.
57
67
  #
58
68
  # @param server [VectorMCP::Server] The server instance that will handle messages
@@ -62,7 +72,8 @@ module VectorMCP
62
72
  # @option options [String] :path_prefix ("/mcp") The base path for HTTP endpoints
63
73
  # @option options [Integer] :session_timeout (300) Session timeout in seconds
64
74
  # @option options [Integer] :event_retention (100) Number of events to retain for resumability
65
- # @option options [Array<String>] :allowed_origins (["*"]) List of allowed origins for CORS. Use ["*"] to allow all origins.
75
+ # @option options [Array<String>] :allowed_origins Allowed origins for CORS validation.
76
+ # Defaults to localhost origins only. Pass ["*"] to allow all origins (NOT recommended for production).
66
77
  def initialize(server, options = {})
67
78
  @server = server
68
79
  @logger = server.logger
@@ -95,13 +106,29 @@ module VectorMCP
95
106
  start_time = Time.now
96
107
  path = env["PATH_INFO"]
97
108
  method = env["REQUEST_METHOD"]
109
+ transport_context = build_transport_context(env, method, path, start_time)
110
+
111
+ logger.debug { "Processing HTTP request #{method} #{path}" }
98
112
 
99
- # Processing HTTP request
113
+ transport_context = execute_transport_hooks(:before_request, transport_context)
114
+ raise transport_context.error if transport_context.error?
115
+ return transport_context.result if transport_context.result
100
116
 
101
117
  response = route_request(path, method, env)
118
+ transport_context.result = response
119
+ transport_context = execute_transport_hooks(:after_response, transport_context)
120
+ raise transport_context.error if transport_context.error?
121
+
122
+ response = transport_context.result || response
102
123
  log_request_completion(method, path, start_time, response[0])
103
124
  response
104
125
  rescue StandardError => e
126
+ if transport_context
127
+ transport_context.error = e
128
+ transport_context = execute_transport_hooks(:on_transport_error, transport_context)
129
+ return transport_context.result if transport_context.result
130
+ end
131
+
105
132
  handle_request_error(method, path, e)
106
133
  end
107
134
 
@@ -133,16 +160,6 @@ module VectorMCP
133
160
  @stream_handler.send_message_to_session(session, message)
134
161
  end
135
162
 
136
- # Broadcasts a notification to all active sessions.
137
- #
138
- # @param method [String] The notification method name
139
- # @param params [Hash, Array, nil] The notification parameters
140
- # @return [Integer] Number of sessions the notification was sent to
141
- def broadcast_notification(method, params = nil)
142
- message = build_notification(method, params)
143
- @session_manager.broadcast_message(message)
144
- end
145
-
146
163
  # Sends a server-initiated JSON-RPC request compatible with Session expectations.
147
164
  # This method will block until a response is received or the timeout is reached.
148
165
  # For HTTP transport, this requires finding an appropriate session with streaming connection.
@@ -307,10 +324,26 @@ module VectorMCP
307
324
  # @param env [Hash] The Rack environment
308
325
  # @return [Array] Rack response triplet
309
326
  def route_request(path, method, env)
327
+ return route_mounted_request(path, method, env) if @mounted
328
+
329
+ # Standalone mode: unchanged behavior
310
330
  return handle_health_check if path == "/"
311
331
  return not_found_response unless path == @path_prefix
312
332
 
313
- # Validate origin for security (MCP specification requirement)
333
+ validate_and_dispatch(method, env)
334
+ end
335
+
336
+ # Routes requests when mounted inside another Rack app (e.g., Rails).
337
+ # PATH_INFO is relative to the mount point: "/" = MCP endpoint, "/health" = health check.
338
+ def route_mounted_request(path, method, env)
339
+ return handle_health_check if path == "/health"
340
+ return not_found_response unless ["", "/"].include?(path)
341
+
342
+ validate_and_dispatch(method, env)
343
+ end
344
+
345
+ # Validates origin and dispatches to the appropriate handler by HTTP method.
346
+ def validate_and_dispatch(method, env)
314
347
  return forbidden_response("Origin not allowed") unless valid_origin?(env)
315
348
 
316
349
  case method
@@ -339,14 +372,23 @@ module VectorMCP
339
372
  request_body = read_request_body(env)
340
373
  parsed = parse_json_message(request_body)
341
374
 
375
+ # MCP spec: POST body MUST be a single JSON-RPC message, not a batch array
376
+ if parsed.is_a?(Array)
377
+ return json_error_response(nil, -32_600, "Invalid Request",
378
+ { details: "Batch requests are not supported. Send a single JSON-RPC message per POST." })
379
+ end
380
+
342
381
  session = resolve_session_for_post(session_id, parsed, env)
343
382
  return session if session.is_a?(Array) # Rack error response
344
383
 
345
- if parsed.is_a?(Array)
346
- handle_batch_request(parsed, session)
347
- else
348
- handle_single_request(parsed, session, env)
384
+ # Validate MCP-Protocol-Version header (skip for initialize requests)
385
+ is_initialize = parsed.is_a?(Hash) && parsed["method"] == "initialize"
386
+ unless is_initialize
387
+ version_error = validate_protocol_version_header(env)
388
+ return version_error if version_error
349
389
  end
390
+
391
+ handle_single_request(parsed, session, env)
350
392
  rescue JSON::ParserError => e
351
393
  json_error_response(nil, -32_700, "Parse error", { details: e.message })
352
394
  end
@@ -363,59 +405,16 @@ module VectorMCP
363
405
  return [202, { "Mcp-Session-Id" => session.id }, []]
364
406
  end
365
407
 
366
- result = @server.handle_message(message, session.context, session.id)
367
- build_rpc_response(env, result, message["id"], session.id)
368
- rescue VectorMCP::ProtocolError => e
369
- build_protocol_error_response(env, e, session_id: session.id)
370
- end
371
-
372
- # Handles a batch of JSON-RPC messages per JSON-RPC 2.0 spec.
373
- #
374
- # @param messages [Array] Array of parsed JSON-RPC messages
375
- # @param session [Session] The resolved session
376
- # @return [Array] Rack response triplet
377
- def handle_batch_request(messages, session)
378
- return json_error_response(nil, -32_600, "Invalid Request", { details: "Empty batch" }) if messages.empty?
379
-
380
- responses = messages.filter_map do |message|
381
- next batch_invalid_item_error unless message.is_a?(Hash)
382
-
383
- process_batch_item(message, session)
384
- end
385
-
386
- return [204, { "Mcp-Session-Id" => session.id }, []] if responses.empty?
387
-
388
- headers = { "Content-Type" => "application/json", "Mcp-Session-Id" => session.id }
389
- [200, headers, [responses.to_json]]
390
- end
391
-
392
- # Processes a single item within a batch request.
393
- #
394
- # @param message [Hash] A single JSON-RPC message
395
- # @param session [Session] The resolved session
396
- # @return [Hash, nil] Response hash or nil for notifications/outgoing responses
397
- def process_batch_item(message, session)
398
- if outgoing_response?(message)
399
- handle_outgoing_response(message)
400
- return nil
408
+ # Notifications: has method, no id -> 202 Accepted with no body (MCP spec requirement)
409
+ if message["method"] && !message.key?("id")
410
+ @server.handle_message(message, session.context, session.id)
411
+ return [202, { "Mcp-Session-Id" => session.id }, []]
401
412
  end
402
413
 
403
414
  result = @server.handle_message(message, session.context, session.id)
404
- return nil if result.nil? && message["id"].nil?
405
-
406
- { jsonrpc: "2.0", id: message["id"], result: result }
415
+ build_rpc_response(env, result, message["id"], session.id)
407
416
  rescue VectorMCP::ProtocolError => e
408
- { jsonrpc: "2.0", id: e.request_id, error: { code: e.code, message: e.message, data: e.details } }
409
- rescue StandardError => e
410
- { jsonrpc: "2.0", id: message["id"],
411
- error: { code: -32_603, message: "Internal error", data: { details: e.message } } }
412
- end
413
-
414
- # Returns an error object for non-Hash items in a batch.
415
- #
416
- # @return [Hash] JSON-RPC error object
417
- def batch_invalid_item_error
418
- { jsonrpc: "2.0", id: nil, error: { code: -32_600, message: "Invalid Request" } }
417
+ build_protocol_error_response(env, e, session_id: session.id)
419
418
  end
420
419
 
421
420
  # Resolves or creates the session for a POST request following MCP spec rules:
@@ -425,12 +424,11 @@ module VectorMCP
425
424
  # - no session_id + other request → 400 Bad Request
426
425
  #
427
426
  # @param session_id [String, nil] Client-supplied Mcp-Session-Id header value
428
- # @param message [Hash, Array] Parsed JSON-RPC message or batch array
427
+ # @param message [Hash] Parsed JSON-RPC message
429
428
  # @param env [Hash] Rack environment
430
429
  # @return [Session, Array] Session object or Rack error response triplet
431
430
  def resolve_session_for_post(session_id, message, env)
432
- first_message = message.is_a?(Array) ? message.first : message
433
- is_initialize = first_message.is_a?(Hash) && first_message["method"] == "initialize"
431
+ is_initialize = message.is_a?(Hash) && message["method"] == "initialize"
434
432
 
435
433
  if session_id
436
434
  session = @session_manager.get_session(session_id)
@@ -464,6 +462,9 @@ module VectorMCP
464
462
  session = @session_manager.get_or_create_session(session_id, env)
465
463
  return not_found_response unless session
466
464
 
465
+ version_error = validate_protocol_version_header(env)
466
+ return version_error if version_error
467
+
467
468
  @stream_handler.handle_streaming_request(env, session)
468
469
  end
469
470
 
@@ -475,6 +476,9 @@ module VectorMCP
475
476
  session_id = extract_session_id(env)
476
477
  return bad_request_response("Missing Mcp-Session-Id header") unless session_id
477
478
 
479
+ version_error = validate_protocol_version_header(env)
480
+ return version_error if version_error
481
+
478
482
  success = @session_manager.terminate_session(session_id)
479
483
  if success
480
484
  [204, {}, []]
@@ -590,10 +594,11 @@ module VectorMCP
590
594
  accept.include?("text/event-stream")
591
595
  end
592
596
 
593
- def format_sse_event(data, type, event_id)
597
+ def format_sse_event(data, type, event_id, retry_ms: nil)
594
598
  lines = []
595
- lines << "id: #{event_id}"
599
+ lines << "id: #{event_id}" if event_id
596
600
  lines << "event: #{type}" if type
601
+ lines << "retry: #{retry_ms}" if retry_ms
597
602
  lines << "data: #{data}"
598
603
  lines << ""
599
604
  "#{lines.join("\n")}\n"
@@ -602,8 +607,14 @@ module VectorMCP
602
607
  def sse_rpc_response(result, request_id, headers = {}, session_id: nil)
603
608
  response = { jsonrpc: "2.0", id: request_id, result: result }
604
609
  event_data = response.to_json
610
+ stream_id = generate_sse_stream_id(session_id, :post)
605
611
 
606
- event_id = @event_store.store_event(event_data, "message", session_id: session_id)
612
+ # Priming event per MCP spec: event ID + empty data field
613
+ prime_event_id = @event_store.store_event("", nil, session_id: session_id, stream_id: stream_id)
614
+ prime_event = "id: #{prime_event_id}\ndata:\n\n"
615
+
616
+ # Actual response event
617
+ event_id = @event_store.store_event(event_data, "message", session_id: session_id, stream_id: stream_id)
607
618
  sse_event = format_sse_event(event_data, "message", event_id)
608
619
 
609
620
  response_headers = {
@@ -613,7 +624,7 @@ module VectorMCP
613
624
  "X-Accel-Buffering" => "no"
614
625
  }.merge(headers)
615
626
 
616
- [200, response_headers, [sse_event]]
627
+ [200, response_headers, [prime_event, sse_event]]
617
628
  end
618
629
 
619
630
  def sse_error_response(id, code, err_message, data = nil, session_id: nil)
@@ -621,8 +632,13 @@ module VectorMCP
621
632
  error_obj[:data] = data if data
622
633
  response = { jsonrpc: "2.0", id: id, error: error_obj }
623
634
  event_data = response.to_json
635
+ stream_id = generate_sse_stream_id(session_id, :post)
636
+
637
+ # Priming event per MCP spec
638
+ prime_event_id = @event_store.store_event("", nil, session_id: session_id, stream_id: stream_id)
639
+ prime_event = "id: #{prime_event_id}\ndata:\n\n"
624
640
 
625
- event_id = @event_store.store_event(event_data, "message", session_id: session_id)
641
+ event_id = @event_store.store_event(event_data, "message", session_id: session_id, stream_id: stream_id)
626
642
  sse_event = format_sse_event(event_data, "message", event_id)
627
643
 
628
644
  response_headers = {
@@ -630,7 +646,7 @@ module VectorMCP
630
646
  "Cache-Control" => "no-cache"
631
647
  }
632
648
 
633
- [200, response_headers, [sse_event]]
649
+ [200, response_headers, [prime_event, sse_event]]
634
650
  end
635
651
 
636
652
  def not_found_response(message = "Not Found")
@@ -642,7 +658,8 @@ module VectorMCP
642
658
  end
643
659
 
644
660
  def forbidden_response(message = "Forbidden")
645
- [403, { "Content-Type" => "text/plain" }, [message]]
661
+ error = { jsonrpc: "2.0", error: { code: -32_600, message: message } }
662
+ [403, { "Content-Type" => "application/json" }, [error.to_json]]
646
663
  end
647
664
 
648
665
  def method_not_allowed_response(allowed_methods)
@@ -654,11 +671,26 @@ module VectorMCP
654
671
  [406, { "Content-Type" => "text/plain" }, [message]]
655
672
  end
656
673
 
674
+ # Validates the MCP-Protocol-Version header per spec.
675
+ # Returns nil if valid, or a 400 Rack response if unsupported.
676
+ def validate_protocol_version_header(env)
677
+ version = env["HTTP_MCP_PROTOCOL_VERSION"]
678
+ return nil if version.nil? # Backwards compatibility: assume 2025-03-26
679
+
680
+ unless VectorMCP::Server::SUPPORTED_PROTOCOL_VERSIONS.include?(version)
681
+ return bad_request_response("Unsupported MCP-Protocol-Version: #{version}")
682
+ end
683
+
684
+ nil
685
+ end
686
+
657
687
  def valid_post_accept?(env)
658
688
  accept = env["HTTP_ACCEPT"]
659
689
  return true if accept.nil? || accept.strip.empty?
690
+ return true if accept.include?("*/*")
660
691
 
661
- accept.include?("application/json") || accept.include?("*/*")
692
+ # MCP spec: client MUST include both application/json AND text/event-stream
693
+ accept.include?("application/json") && accept.include?("text/event-stream")
662
694
  end
663
695
 
664
696
  def valid_get_accept?(env)
@@ -668,7 +700,13 @@ module VectorMCP
668
700
  accept.include?("text/event-stream") || accept.include?("*/*")
669
701
  end
670
702
 
671
- # Validates the Origin header for security
703
+ # Validates the Origin header for security.
704
+ #
705
+ # Matches are checked both exactly and as prefix (so that
706
+ # +http://localhost+ in the allowed list matches +http://localhost:3000+).
707
+ #
708
+ # Requests without an Origin header are allowed through because they
709
+ # originate from non-browser contexts (curl, server-to-server, etc.).
672
710
  #
673
711
  # @param env [Hash] The Rack environment
674
712
  # @return [Boolean] True if origin is allowed, false otherwise
@@ -678,7 +716,9 @@ module VectorMCP
678
716
  origin = env["HTTP_ORIGIN"]
679
717
  return true if origin.nil? # Allow requests without Origin header (e.g., server-to-server)
680
718
 
681
- @allowed_origins.include?(origin)
719
+ @allowed_origins.any? do |allowed|
720
+ origin == allowed || origin.start_with?("#{allowed}:")
721
+ end
682
722
  end
683
723
 
684
724
  # Logging and error handling
@@ -692,6 +732,29 @@ module VectorMCP
692
732
  [500, { "Content-Type" => "text/plain" }, ["Internal Server Error"]]
693
733
  end
694
734
 
735
+ def build_transport_context(env, method, path, start_time)
736
+ request_context = VectorMCP::RequestContext.from_rack_env(env, "http_stream")
737
+
738
+ VectorMCP::Middleware::Context.new(
739
+ operation_type: :transport,
740
+ operation_name: "#{method} #{path}",
741
+ params: request_context.to_h,
742
+ session: nil,
743
+ server: @server,
744
+ metadata: {
745
+ start_time: start_time,
746
+ path: path,
747
+ method: method
748
+ }
749
+ )
750
+ end
751
+
752
+ def execute_transport_hooks(hook_type, context)
753
+ return context unless @server.respond_to?(:middleware_manager) && @server.middleware_manager
754
+
755
+ @server.middleware_manager.execute_hooks(hook_type, context)
756
+ end
757
+
695
758
  def handle_fatal_error(error)
696
759
  logger.fatal { "Fatal error in HttpStream transport: #{error.message}" }
697
760
  exit(1)
@@ -839,8 +902,8 @@ module VectorMCP
839
902
  #
840
903
  # @param obj [Object] The object to transform (Hash, Array, or other)
841
904
  # @return [Object] The transformed object
842
- def deep_transform_keys(obj, &block)
843
- transform_object_keys(obj, &block)
905
+ def deep_transform_keys(obj, &)
906
+ transform_object_keys(obj, &)
844
907
  end
845
908
 
846
909
  # Core transformation logic extracted for better maintainability
@@ -874,7 +937,18 @@ module VectorMCP
874
937
  @path_prefix = normalize_path_prefix(options[:path_prefix] || DEFAULT_PATH_PREFIX)
875
938
  @session_timeout = options[:session_timeout] || DEFAULT_SESSION_TIMEOUT
876
939
  @event_retention = options[:event_retention] || DEFAULT_EVENT_RETENTION
877
- @allowed_origins = options[:allowed_origins] || ["*"]
940
+ @allowed_origins = options[:allowed_origins] || DEFAULT_ALLOWED_ORIGINS
941
+ @mounted = options.fetch(:mounted, false)
942
+
943
+ warn_on_permissive_origins if @allowed_origins.include?("*")
944
+ end
945
+
946
+ # Logs a security warning when wildcard origin is configured.
947
+ def warn_on_permissive_origins
948
+ logger.warn do
949
+ "[SECURITY] allowed_origins includes '*', which permits cross-origin requests from any website. " \
950
+ "This is not recommended for production. Specify explicit origins instead."
951
+ end
878
952
  end
879
953
 
880
954
  # Initialize core HTTP stream components
@@ -901,6 +975,11 @@ module VectorMCP
901
975
  @request_id_counter = Concurrent::AtomicFixnum.new(0)
902
976
  end
903
977
 
978
+ def generate_sse_stream_id(session_id, origin)
979
+ session_label = session_id || "anonymous"
980
+ "#{session_label}-#{origin}-#{SecureRandom.hex(4)}"
981
+ end
982
+
904
983
  # Generate a unique, thread-safe request ID for server-initiated requests
905
984
  #
906
985
  # @return [String] A unique request ID in format: vecmcp_http_{pid}_{random}_{counter}
@@ -2,5 +2,5 @@
2
2
 
3
3
  module VectorMCP
4
4
  # The current version of the VectorMCP gem.
5
- VERSION = "0.3.4"
5
+ VERSION = "0.4.0"
6
6
  end
data/lib/vector_mcp.rb CHANGED
@@ -11,17 +11,16 @@ require_relative "vector_mcp/util"
11
11
  require_relative "vector_mcp/log_filter"
12
12
  require_relative "vector_mcp/image_util"
13
13
  require_relative "vector_mcp/handlers/core"
14
- require_relative "vector_mcp/transport/stdio"
15
- # require_relative "vector_mcp/transport/sse" # Load on demand
16
14
  require_relative "vector_mcp/logger"
17
15
  require_relative "vector_mcp/middleware"
18
16
  require_relative "vector_mcp/server"
17
+ require_relative "vector_mcp/tool"
19
18
 
20
19
  # The VectorMCP module provides a full-featured, opinionated Ruby implementation
21
20
  # of the **Model Context Protocol (MCP)**. It gives developers everything needed
22
21
  # to spin up an MCP-compatible server—including:
23
22
  #
24
- # * **Transport adapters** (synchronous `stdio` or HTTP + SSE)
23
+ # * **Transport adapters** (streamable HTTP)
25
24
  # * **High-level abstractions** for *tools*, *resources*, and *prompts*
26
25
  # * **JSON-RPC 2.0** message handling with sensible defaults and detailed
27
26
  # error reporting helpers
@@ -40,11 +39,10 @@ require_relative "vector_mcp/server"
40
39
  # input_schema: {type: "object", properties: {text: {type: "string"}}}
41
40
  # ) { |args| args["text"] }
42
41
  #
43
- # server.run # => starts the stdio transport and begins processing JSON-RPC messages
42
+ # server.run # => starts the HTTP stream transport and begins processing JSON-RPC messages
44
43
  # ```
45
44
  #
46
- # For production you could instead pass an `SSE` transport instance to `run` in
47
- # order to serve multiple concurrent clients over HTTP.
45
+ # The default HTTP stream transport supports multiple concurrent clients over HTTP.
48
46
  #
49
47
  module VectorMCP
50
48
  class << self
@@ -68,8 +66,8 @@ module VectorMCP
68
66
  # Any positional or keyword arguments are forwarded verbatim to the underlying
69
67
  # constructor, so refer to {VectorMCP::Server#initialize} for the full list of
70
68
  # accepted parameters.
71
- def new(*args, **kwargs)
72
- Server.new(*args, **kwargs)
69
+ def new(*, **)
70
+ Server.new(*, **)
73
71
  end
74
72
  end
75
73
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vector_mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.4
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergio Bayona
@@ -136,6 +136,7 @@ files:
136
136
  - lib/vector_mcp/middleware/context.rb
137
137
  - lib/vector_mcp/middleware/hook.rb
138
138
  - lib/vector_mcp/middleware/manager.rb
139
+ - lib/vector_mcp/rails/tool.rb
139
140
  - lib/vector_mcp/request_context.rb
140
141
  - lib/vector_mcp/sampling/request.rb
141
142
  - lib/vector_mcp/sampling/result.rb
@@ -152,19 +153,12 @@ files:
152
153
  - lib/vector_mcp/server/message_handling.rb
153
154
  - lib/vector_mcp/server/registry.rb
154
155
  - lib/vector_mcp/session.rb
156
+ - lib/vector_mcp/tool.rb
155
157
  - lib/vector_mcp/transport/base_session_manager.rb
156
158
  - lib/vector_mcp/transport/http_stream.rb
157
159
  - lib/vector_mcp/transport/http_stream/event_store.rb
158
160
  - lib/vector_mcp/transport/http_stream/session_manager.rb
159
161
  - lib/vector_mcp/transport/http_stream/stream_handler.rb
160
- - lib/vector_mcp/transport/sse.rb
161
- - lib/vector_mcp/transport/sse/client_connection.rb
162
- - lib/vector_mcp/transport/sse/message_handler.rb
163
- - lib/vector_mcp/transport/sse/puma_config.rb
164
- - lib/vector_mcp/transport/sse/stream_manager.rb
165
- - lib/vector_mcp/transport/sse_session_manager.rb
166
- - lib/vector_mcp/transport/stdio.rb
167
- - lib/vector_mcp/transport/stdio_session_manager.rb
168
162
  - lib/vector_mcp/util.rb
169
163
  - lib/vector_mcp/version.rb
170
164
  homepage: https://github.com/sergiobayona/vector_mcp
@@ -182,7 +176,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
182
176
  requirements:
183
177
  - - ">="
184
178
  - !ruby/object:Gem::Version
185
- version: 3.0.6
179
+ version: '3.2'
186
180
  required_rubygems_version: !ruby/object:Gem::Requirement
187
181
  requirements:
188
182
  - - ">="
@@ -1,113 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module VectorMCP
4
- module Transport
5
- class SSE
6
- # Manages individual client connection state for SSE transport.
7
- # Each client connection has a unique session ID, message queue, and streaming thread.
8
- class ClientConnection
9
- attr_reader :session_id, :message_queue, :logger
10
- attr_accessor :stream_thread, :stream_io
11
-
12
- # Initializes a new client connection.
13
- #
14
- # @param session_id [String] Unique identifier for this client session
15
- # @param logger [Logger] Logger instance for debugging and error reporting
16
- def initialize(session_id, logger)
17
- @session_id = session_id
18
- @logger = logger
19
- @message_queue = Queue.new
20
- @stream_thread = nil
21
- @stream_io = nil
22
- @closed = false
23
- @mutex = Mutex.new
24
-
25
- logger.debug { "Client connection created: #{session_id}" }
26
- end
27
-
28
- # Checks if the connection is closed
29
- #
30
- # @return [Boolean] true if connection is closed
31
- def closed?
32
- @mutex.synchronize { @closed }
33
- end
34
-
35
- # Closes the client connection and cleans up resources.
36
- # This method is thread-safe and can be called multiple times.
37
- def close
38
- @mutex.synchronize do
39
- return if @closed
40
-
41
- @closed = true
42
- logger.debug { "Closing client connection: #{session_id}" }
43
-
44
- # Close the message queue to signal streaming thread to stop
45
- @message_queue.close if @message_queue.respond_to?(:close)
46
-
47
- # Close the stream I/O if it exists
48
- begin
49
- @stream_io&.close
50
- rescue StandardError => e
51
- logger.warn { "Error closing stream I/O for #{session_id}: #{e.message}" }
52
- end
53
-
54
- # Stop the streaming thread
55
- if @stream_thread&.alive?
56
- @stream_thread.kill
57
- @stream_thread.join(1) # Wait up to 1 second for clean shutdown
58
- end
59
-
60
- logger.debug { "Client connection closed: #{session_id}" }
61
- end
62
- end
63
-
64
- # Enqueues a message to be sent to this client.
65
- # This method is thread-safe.
66
- #
67
- # @param message [Hash] The JSON-RPC message to send
68
- # @return [Boolean] true if message was enqueued successfully
69
- def enqueue_message(message)
70
- return false if closed?
71
-
72
- begin
73
- @message_queue.push(message)
74
- logger.debug { "Message enqueued for client #{session_id}: #{VectorMCP::LogFilter.filter_hash(message).inspect}" }
75
- true
76
- rescue ClosedQueueError
77
- logger.warn { "Attempted to enqueue message to closed queue for client #{session_id}" }
78
- false
79
- rescue StandardError => e
80
- logger.error { "Error enqueuing message for client #{session_id}: #{e.message}" }
81
- false
82
- end
83
- end
84
-
85
- # Dequeues the next message from the client's message queue.
86
- # This method blocks until a message is available or the queue is closed.
87
- #
88
- # @return [Hash, nil] The next message, or nil if queue is closed
89
- def dequeue_message
90
- return nil if closed?
91
-
92
- begin
93
- @message_queue.pop
94
- rescue ClosedQueueError
95
- nil
96
- rescue StandardError => e
97
- logger.error { "Error dequeuing message for client #{session_id}: #{e.message}" }
98
- nil
99
- end
100
- end
101
-
102
- # Gets the current queue size
103
- #
104
- # @return [Integer] Number of messages waiting in the queue
105
- def queue_size
106
- @message_queue.size
107
- rescue StandardError
108
- 0
109
- end
110
- end
111
- end
112
- end
113
- end