vector_mcp 0.3.3 → 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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -0
  3. data/README.md +132 -342
  4. data/lib/vector_mcp/handlers/core.rb +82 -27
  5. data/lib/vector_mcp/image_util.rb +53 -5
  6. data/lib/vector_mcp/log_filter.rb +48 -0
  7. data/lib/vector_mcp/middleware/base.rb +1 -5
  8. data/lib/vector_mcp/middleware/context.rb +11 -1
  9. data/lib/vector_mcp/rails/tool.rb +85 -0
  10. data/lib/vector_mcp/request_context.rb +1 -1
  11. data/lib/vector_mcp/security/middleware.rb +2 -2
  12. data/lib/vector_mcp/security/strategies/api_key.rb +27 -4
  13. data/lib/vector_mcp/security/strategies/jwt_token.rb +10 -5
  14. data/lib/vector_mcp/server/capabilities.rb +4 -10
  15. data/lib/vector_mcp/server/message_handling.rb +2 -2
  16. data/lib/vector_mcp/server/registry.rb +36 -4
  17. data/lib/vector_mcp/server.rb +49 -41
  18. data/lib/vector_mcp/session.rb +5 -3
  19. data/lib/vector_mcp/tool.rb +221 -0
  20. data/lib/vector_mcp/transport/base_session_manager.rb +1 -17
  21. data/lib/vector_mcp/transport/http_stream/event_store.rb +33 -13
  22. data/lib/vector_mcp/transport/http_stream/session_manager.rb +39 -14
  23. data/lib/vector_mcp/transport/http_stream/stream_handler.rb +133 -47
  24. data/lib/vector_mcp/transport/http_stream.rb +294 -33
  25. data/lib/vector_mcp/version.rb +1 -1
  26. data/lib/vector_mcp.rb +7 -8
  27. metadata +5 -10
  28. data/lib/vector_mcp/transport/sse/client_connection.rb +0 -113
  29. data/lib/vector_mcp/transport/sse/message_handler.rb +0 -166
  30. data/lib/vector_mcp/transport/sse/puma_config.rb +0 -77
  31. data/lib/vector_mcp/transport/sse/stream_manager.rb +0 -92
  32. data/lib/vector_mcp/transport/sse.rb +0 -377
  33. data/lib/vector_mcp/transport/sse_session_manager.rb +0 -188
  34. data/lib/vector_mcp/transport/stdio.rb +0 -473
  35. data/lib/vector_mcp/transport/stdio_session_manager.rb +0 -181
@@ -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
@@ -330,28 +363,87 @@ module VectorMCP
330
363
  # @param env [Hash] The Rack environment
331
364
  # @return [Array] Rack response triplet
332
365
  def handle_post_request(env)
333
- session_id = extract_session_id(env)
334
- session = @session_manager.get_or_create_session(session_id, env)
366
+ unless valid_post_accept?(env)
367
+ logger.warn { "POST request with unsupported Accept header: #{env["HTTP_ACCEPT"]}" }
368
+ return not_acceptable_response("Not Acceptable: POST requires Accept: application/json")
369
+ end
335
370
 
371
+ session_id = extract_session_id(env)
336
372
  request_body = read_request_body(env)
337
- message = parse_json_message(request_body)
373
+ parsed = parse_json_message(request_body)
338
374
 
339
- # Check if this is a response to a server-initiated request
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
+
381
+ session = resolve_session_for_post(session_id, parsed, env)
382
+ return session if session.is_a?(Array) # Rack error response
383
+
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
389
+ end
390
+
391
+ handle_single_request(parsed, session, env)
392
+ rescue JSON::ParserError => e
393
+ json_error_response(nil, -32_700, "Parse error", { details: e.message })
394
+ end
395
+
396
+ # Handles a single JSON-RPC message from a POST request.
397
+ #
398
+ # @param message [Hash] Parsed JSON-RPC message
399
+ # @param session [Session] The resolved session
400
+ # @param env [Hash] The Rack environment
401
+ # @return [Array] Rack response triplet
402
+ def handle_single_request(message, session, env)
340
403
  if outgoing_response?(message)
341
404
  handle_outgoing_response(message)
342
- # For responses, return 202 Accepted with no body
343
405
  return [202, { "Mcp-Session-Id" => session.id }, []]
344
406
  end
345
407
 
346
- result = @server.handle_message(message, session.context, session.id)
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 }, []]
412
+ end
347
413
 
348
- # Set session ID header in response
349
- headers = { "Mcp-Session-Id" => session.id }
350
- json_rpc_response(result, message["id"], headers)
414
+ result = @server.handle_message(message, session.context, session.id)
415
+ build_rpc_response(env, result, message["id"], session.id)
351
416
  rescue VectorMCP::ProtocolError => e
352
- json_error_response(e.request_id, e.code, e.message, e.details)
353
- rescue JSON::ParserError => e
354
- json_error_response(nil, -32_700, "Parse error", { details: e.message })
417
+ build_protocol_error_response(env, e, session_id: session.id)
418
+ end
419
+
420
+ # Resolves or creates the session for a POST request following MCP spec rules:
421
+ # - session_id present and known → return existing session (updating request context)
422
+ # - session_id present but unknown/expired → 404 Not Found
423
+ # - no session_id + initialize request → create new session
424
+ # - no session_id + other request → 400 Bad Request
425
+ #
426
+ # @param session_id [String, nil] Client-supplied Mcp-Session-Id header value
427
+ # @param message [Hash] Parsed JSON-RPC message
428
+ # @param env [Hash] Rack environment
429
+ # @return [Session, Array] Session object or Rack error response triplet
430
+ def resolve_session_for_post(session_id, message, env)
431
+ is_initialize = message.is_a?(Hash) && message["method"] == "initialize"
432
+
433
+ if session_id
434
+ session = @session_manager.get_session(session_id)
435
+ return not_found_response("Unknown or expired session") unless session
436
+
437
+ if env
438
+ request_context = VectorMCP::RequestContext.from_rack_env(env, "http_stream")
439
+ session.context.request_context = request_context
440
+ end
441
+ session
442
+ elsif is_initialize
443
+ @session_manager.create_session(nil, env)
444
+ else
445
+ bad_request_response("Missing Mcp-Session-Id header")
446
+ end
355
447
  end
356
448
 
357
449
  # Handles GET requests (SSE streaming)
@@ -359,12 +451,20 @@ module VectorMCP
359
451
  # @param env [Hash] The Rack environment
360
452
  # @return [Array] Rack response triplet
361
453
  def handle_get_request(env)
454
+ unless valid_get_accept?(env)
455
+ logger.warn { "GET request with unsupported Accept header: #{env["HTTP_ACCEPT"]}" }
456
+ return not_acceptable_response("Not Acceptable: GET requires Accept: text/event-stream")
457
+ end
458
+
362
459
  session_id = extract_session_id(env)
363
460
  return bad_request_response("Missing Mcp-Session-Id header") unless session_id
364
461
 
365
462
  session = @session_manager.get_or_create_session(session_id, env)
366
463
  return not_found_response unless session
367
464
 
465
+ version_error = validate_protocol_version_header(env)
466
+ return version_error if version_error
467
+
368
468
  @stream_handler.handle_streaming_request(env, session)
369
469
  end
370
470
 
@@ -376,6 +476,9 @@ module VectorMCP
376
476
  session_id = extract_session_id(env)
377
477
  return bad_request_response("Missing Mcp-Session-Id header") unless session_id
378
478
 
479
+ version_error = validate_protocol_version_header(env)
480
+ return version_error if version_error
481
+
379
482
  success = @session_manager.terminate_session(session_id)
380
483
  if success
381
484
  [204, {}, []]
@@ -469,8 +572,85 @@ module VectorMCP
469
572
  [400, { "Content-Type" => "application/json" }, [response.to_json]]
470
573
  end
471
574
 
472
- def not_found_response
473
- [404, { "Content-Type" => "text/plain" }, ["Not Found"]]
575
+ def build_rpc_response(env, result, request_id, session_id)
576
+ headers = { "Mcp-Session-Id" => session_id }
577
+ if client_accepts_sse?(env)
578
+ sse_rpc_response(result, request_id, headers, session_id: session_id)
579
+ else
580
+ json_rpc_response(result, request_id, headers)
581
+ end
582
+ end
583
+
584
+ def build_protocol_error_response(env, error, session_id: nil)
585
+ if client_accepts_sse?(env)
586
+ sse_error_response(error.request_id, error.code, error.message, error.details, session_id: session_id)
587
+ else
588
+ json_error_response(error.request_id, error.code, error.message, error.details)
589
+ end
590
+ end
591
+
592
+ def client_accepts_sse?(env)
593
+ accept = env["HTTP_ACCEPT"] || ""
594
+ accept.include?("text/event-stream")
595
+ end
596
+
597
+ def format_sse_event(data, type, event_id, retry_ms: nil)
598
+ lines = []
599
+ lines << "id: #{event_id}" if event_id
600
+ lines << "event: #{type}" if type
601
+ lines << "retry: #{retry_ms}" if retry_ms
602
+ lines << "data: #{data}"
603
+ lines << ""
604
+ "#{lines.join("\n")}\n"
605
+ end
606
+
607
+ def sse_rpc_response(result, request_id, headers = {}, session_id: nil)
608
+ response = { jsonrpc: "2.0", id: request_id, result: result }
609
+ event_data = response.to_json
610
+ stream_id = generate_sse_stream_id(session_id, :post)
611
+
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)
618
+ sse_event = format_sse_event(event_data, "message", event_id)
619
+
620
+ response_headers = {
621
+ "Content-Type" => "text/event-stream",
622
+ "Cache-Control" => "no-cache",
623
+ "Connection" => "keep-alive",
624
+ "X-Accel-Buffering" => "no"
625
+ }.merge(headers)
626
+
627
+ [200, response_headers, [prime_event, sse_event]]
628
+ end
629
+
630
+ def sse_error_response(id, code, err_message, data = nil, session_id: nil)
631
+ error_obj = { code: code, message: err_message }
632
+ error_obj[:data] = data if data
633
+ response = { jsonrpc: "2.0", id: id, error: error_obj }
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"
640
+
641
+ event_id = @event_store.store_event(event_data, "message", session_id: session_id, stream_id: stream_id)
642
+ sse_event = format_sse_event(event_data, "message", event_id)
643
+
644
+ response_headers = {
645
+ "Content-Type" => "text/event-stream",
646
+ "Cache-Control" => "no-cache"
647
+ }
648
+
649
+ [200, response_headers, [prime_event, sse_event]]
650
+ end
651
+
652
+ def not_found_response(message = "Not Found")
653
+ [404, { "Content-Type" => "text/plain" }, [message]]
474
654
  end
475
655
 
476
656
  def bad_request_response(message = "Bad Request")
@@ -478,7 +658,8 @@ module VectorMCP
478
658
  end
479
659
 
480
660
  def forbidden_response(message = "Forbidden")
481
- [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]]
482
663
  end
483
664
 
484
665
  def method_not_allowed_response(allowed_methods)
@@ -486,7 +667,46 @@ module VectorMCP
486
667
  ["Method Not Allowed"]]
487
668
  end
488
669
 
489
- # Validates the Origin header for security
670
+ def not_acceptable_response(message = "Not Acceptable")
671
+ [406, { "Content-Type" => "text/plain" }, [message]]
672
+ end
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
+
687
+ def valid_post_accept?(env)
688
+ accept = env["HTTP_ACCEPT"]
689
+ return true if accept.nil? || accept.strip.empty?
690
+ return true if accept.include?("*/*")
691
+
692
+ # MCP spec: client MUST include both application/json AND text/event-stream
693
+ accept.include?("application/json") && accept.include?("text/event-stream")
694
+ end
695
+
696
+ def valid_get_accept?(env)
697
+ accept = env["HTTP_ACCEPT"]
698
+ return true if accept.nil? || accept.strip.empty?
699
+
700
+ accept.include?("text/event-stream") || accept.include?("*/*")
701
+ end
702
+
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.).
490
710
  #
491
711
  # @param env [Hash] The Rack environment
492
712
  # @return [Boolean] True if origin is allowed, false otherwise
@@ -496,7 +716,9 @@ module VectorMCP
496
716
  origin = env["HTTP_ORIGIN"]
497
717
  return true if origin.nil? # Allow requests without Origin header (e.g., server-to-server)
498
718
 
499
- @allowed_origins.include?(origin)
719
+ @allowed_origins.any? do |allowed|
720
+ origin == allowed || origin.start_with?("#{allowed}:")
721
+ end
500
722
  end
501
723
 
502
724
  # Logging and error handling
@@ -510,6 +732,29 @@ module VectorMCP
510
732
  [500, { "Content-Type" => "text/plain" }, ["Internal Server Error"]]
511
733
  end
512
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
+
513
758
  def handle_fatal_error(error)
514
759
  logger.fatal { "Fatal error in HttpStream transport: #{error.message}" }
515
760
  exit(1)
@@ -657,8 +902,8 @@ module VectorMCP
657
902
  #
658
903
  # @param obj [Object] The object to transform (Hash, Array, or other)
659
904
  # @return [Object] The transformed object
660
- def deep_transform_keys(obj, &block)
661
- transform_object_keys(obj, &block)
905
+ def deep_transform_keys(obj, &)
906
+ transform_object_keys(obj, &)
662
907
  end
663
908
 
664
909
  # Core transformation logic extracted for better maintainability
@@ -692,7 +937,18 @@ module VectorMCP
692
937
  @path_prefix = normalize_path_prefix(options[:path_prefix] || DEFAULT_PATH_PREFIX)
693
938
  @session_timeout = options[:session_timeout] || DEFAULT_SESSION_TIMEOUT
694
939
  @event_retention = options[:event_retention] || DEFAULT_EVENT_RETENTION
695
- @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
696
952
  end
697
953
 
698
954
  # Initialize core HTTP stream components
@@ -719,6 +975,11 @@ module VectorMCP
719
975
  @request_id_counter = Concurrent::AtomicFixnum.new(0)
720
976
  end
721
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
+
722
983
  # Generate a unique, thread-safe request ID for server-initiated requests
723
984
  #
724
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.3"
5
+ VERSION = "0.4.0"
6
6
  end
data/lib/vector_mcp.rb CHANGED
@@ -8,19 +8,19 @@ require_relative "vector_mcp/errors"
8
8
  require_relative "vector_mcp/definitions"
9
9
  require_relative "vector_mcp/session"
10
10
  require_relative "vector_mcp/util"
11
+ require_relative "vector_mcp/log_filter"
11
12
  require_relative "vector_mcp/image_util"
12
13
  require_relative "vector_mcp/handlers/core"
13
- require_relative "vector_mcp/transport/stdio"
14
- # require_relative "vector_mcp/transport/sse" # Load on demand
15
14
  require_relative "vector_mcp/logger"
16
15
  require_relative "vector_mcp/middleware"
17
16
  require_relative "vector_mcp/server"
17
+ require_relative "vector_mcp/tool"
18
18
 
19
19
  # The VectorMCP module provides a full-featured, opinionated Ruby implementation
20
20
  # of the **Model Context Protocol (MCP)**. It gives developers everything needed
21
21
  # to spin up an MCP-compatible server—including:
22
22
  #
23
- # * **Transport adapters** (synchronous `stdio` or HTTP + SSE)
23
+ # * **Transport adapters** (streamable HTTP)
24
24
  # * **High-level abstractions** for *tools*, *resources*, and *prompts*
25
25
  # * **JSON-RPC 2.0** message handling with sensible defaults and detailed
26
26
  # error reporting helpers
@@ -39,11 +39,10 @@ require_relative "vector_mcp/server"
39
39
  # input_schema: {type: "object", properties: {text: {type: "string"}}}
40
40
  # ) { |args| args["text"] }
41
41
  #
42
- # 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
43
43
  # ```
44
44
  #
45
- # For production you could instead pass an `SSE` transport instance to `run` in
46
- # order to serve multiple concurrent clients over HTTP.
45
+ # The default HTTP stream transport supports multiple concurrent clients over HTTP.
47
46
  #
48
47
  module VectorMCP
49
48
  class << self
@@ -67,8 +66,8 @@ module VectorMCP
67
66
  # Any positional or keyword arguments are forwarded verbatim to the underlying
68
67
  # constructor, so refer to {VectorMCP::Server#initialize} for the full list of
69
68
  # accepted parameters.
70
- def new(*args, **kwargs)
71
- Server.new(*args, **kwargs)
69
+ def new(*, **)
70
+ Server.new(*, **)
72
71
  end
73
72
  end
74
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.3
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergio Bayona
@@ -129,12 +129,14 @@ files:
129
129
  - lib/vector_mcp/errors.rb
130
130
  - lib/vector_mcp/handlers/core.rb
131
131
  - lib/vector_mcp/image_util.rb
132
+ - lib/vector_mcp/log_filter.rb
132
133
  - lib/vector_mcp/logger.rb
133
134
  - lib/vector_mcp/middleware.rb
134
135
  - lib/vector_mcp/middleware/base.rb
135
136
  - lib/vector_mcp/middleware/context.rb
136
137
  - lib/vector_mcp/middleware/hook.rb
137
138
  - lib/vector_mcp/middleware/manager.rb
139
+ - lib/vector_mcp/rails/tool.rb
138
140
  - lib/vector_mcp/request_context.rb
139
141
  - lib/vector_mcp/sampling/request.rb
140
142
  - lib/vector_mcp/sampling/result.rb
@@ -151,19 +153,12 @@ files:
151
153
  - lib/vector_mcp/server/message_handling.rb
152
154
  - lib/vector_mcp/server/registry.rb
153
155
  - lib/vector_mcp/session.rb
156
+ - lib/vector_mcp/tool.rb
154
157
  - lib/vector_mcp/transport/base_session_manager.rb
155
158
  - lib/vector_mcp/transport/http_stream.rb
156
159
  - lib/vector_mcp/transport/http_stream/event_store.rb
157
160
  - lib/vector_mcp/transport/http_stream/session_manager.rb
158
161
  - lib/vector_mcp/transport/http_stream/stream_handler.rb
159
- - lib/vector_mcp/transport/sse.rb
160
- - lib/vector_mcp/transport/sse/client_connection.rb
161
- - lib/vector_mcp/transport/sse/message_handler.rb
162
- - lib/vector_mcp/transport/sse/puma_config.rb
163
- - lib/vector_mcp/transport/sse/stream_manager.rb
164
- - lib/vector_mcp/transport/sse_session_manager.rb
165
- - lib/vector_mcp/transport/stdio.rb
166
- - lib/vector_mcp/transport/stdio_session_manager.rb
167
162
  - lib/vector_mcp/util.rb
168
163
  - lib/vector_mcp/version.rb
169
164
  homepage: https://github.com/sergiobayona/vector_mcp
@@ -181,7 +176,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
181
176
  requirements:
182
177
  - - ">="
183
178
  - !ruby/object:Gem::Version
184
- version: 3.0.6
179
+ version: '3.2'
185
180
  required_rubygems_version: !ruby/object:Gem::Requirement
186
181
  requirements:
187
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}: #{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