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
@@ -1,166 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
-
5
- module VectorMCP
6
- module Transport
7
- class SSE
8
- # Handles JSON-RPC message processing for POST requests.
9
- # Processes incoming messages and sends responses via SSE streams.
10
- class MessageHandler
11
- # Initializes a new message handler.
12
- #
13
- # @param server [VectorMCP::Server] The MCP server instance
14
- # @param session [VectorMCP::Session] The server session
15
- # @param logger [Logger] Logger instance for debugging
16
- def initialize(server, session, logger)
17
- @server = server
18
- @session = session
19
- @logger = logger
20
- end
21
-
22
- # Handles a POST message request from a client.
23
- #
24
- # @param env [Hash] Rack environment hash
25
- # @param client_conn [ClientConnection] The client connection
26
- # @return [Array] Rack response triplet
27
- def handle_post_message(env, client_conn)
28
- request_body = read_request_body(env)
29
- return error_response(nil, -32_600, "Request body is empty") if request_body.nil? || request_body.empty?
30
-
31
- message = parse_json_message(request_body, client_conn)
32
- return message if message.is_a?(Array) # Error response
33
-
34
- process_message(message, client_conn)
35
- rescue VectorMCP::ProtocolError => e
36
- @logger.error { "Protocol error for client #{client_conn.session_id}: #{e.message}" }
37
- request_id = e.request_id || message&.dig("id")
38
- enqueue_error_response(client_conn, request_id, e.code, e.message, e.details)
39
- error_response(request_id, e.code, e.message, e.details)
40
- rescue StandardError => e
41
- @logger.error { "Unexpected error for client #{client_conn.session_id}: #{e.message}\n#{e.backtrace.join("\n")}" }
42
- request_id = message&.dig("id")
43
- enqueue_error_response(client_conn, request_id, -32_603, "Internal server error")
44
- error_response(request_id, -32_603, "Internal server error")
45
- end
46
-
47
- private
48
-
49
- # Reads the request body from the Rack environment.
50
- #
51
- # @param env [Hash] Rack environment
52
- # @return [String, nil] Request body as string
53
- def read_request_body(env)
54
- input = env["rack.input"]
55
- return nil unless input
56
-
57
- body = input.read
58
- input.rewind if input.respond_to?(:rewind)
59
- body
60
- end
61
-
62
- # Parses JSON message from request body.
63
- #
64
- # @param body_str [String] JSON string from request body
65
- # @param client_conn [ClientConnection] Client connection for error handling
66
- # @return [Hash, Array] Parsed message or error response triplet
67
- def parse_json_message(body_str, client_conn)
68
- JSON.parse(body_str)
69
- rescue JSON::ParserError => e
70
- @logger.error { "JSON parse error for client #{client_conn.session_id}: #{e.message}" }
71
- malformed_id = VectorMCP::Util.extract_id_from_invalid_json(body_str)
72
- enqueue_error_response(client_conn, malformed_id, -32_700, "Parse error")
73
- error_response(malformed_id, -32_700, "Parse error")
74
- end
75
-
76
- # Processes a valid JSON-RPC message.
77
- #
78
- # @param message [Hash] Parsed JSON-RPC message
79
- # @param client_conn [ClientConnection] Client connection
80
- # @return [Array] Rack response triplet
81
- def process_message(message, client_conn)
82
- # Handle the message through the server
83
- response_data = @server.handle_message(message, @session, client_conn.session_id)
84
-
85
- # If it's a request (has id), send response via SSE
86
- if message["id"]
87
- enqueue_success_response(client_conn, message["id"], response_data)
88
- else
89
- @logger.debug { "Processed notification for client #{client_conn.session_id}" }
90
- end
91
-
92
- # Always return 202 Accepted for valid POST messages
93
- success_response(message["id"])
94
- end
95
-
96
- # Enqueues a successful response to the client's SSE stream.
97
- #
98
- # @param client_conn [ClientConnection] Client connection
99
- # @param request_id [String, Integer] Original request ID
100
- # @param result [Object] Response result data
101
- def enqueue_success_response(client_conn, request_id, result)
102
- response = {
103
- jsonrpc: "2.0",
104
- id: request_id,
105
- result: result
106
- }
107
- StreamManager.enqueue_message(client_conn, response)
108
- end
109
-
110
- # Enqueues an error response to the client's SSE stream.
111
- #
112
- # @param client_conn [ClientConnection] Client connection
113
- # @param request_id [String, Integer, nil] Original request ID
114
- # @param code [Integer] Error code
115
- # @param message [String] Error message
116
- # @param data [Object, nil] Additional error data
117
- def enqueue_error_response(client_conn, request_id, code, message, data = nil)
118
- error_payload = { code: code, message: message }
119
- error_payload[:data] = data if data
120
-
121
- error_response = {
122
- jsonrpc: "2.0",
123
- id: request_id,
124
- error: error_payload
125
- }
126
- StreamManager.enqueue_message(client_conn, error_response)
127
- end
128
-
129
- # Creates a successful HTTP response for the POST request.
130
- #
131
- # @param request_id [String, Integer, nil] Request ID
132
- # @return [Array] Rack response triplet
133
- def success_response(request_id)
134
- body = { status: "accepted", id: request_id }.to_json
135
- [202, { "Content-Type" => "application/json" }, [body]]
136
- end
137
-
138
- # Creates an error HTTP response for the POST request.
139
- #
140
- # @param id [String, Integer, nil] Request ID
141
- # @param code [Integer] Error code
142
- # @param message [String] Error message
143
- # @param data [Object, nil] Additional error data
144
- # @return [Array] Rack response triplet
145
- def error_response(id, code, message, data = nil)
146
- status = case code
147
- when -32_700, -32_600, -32_602 then 400 # Parse, Invalid Request, Invalid Params
148
- when -32_601, -32_001 then 404 # Method Not Found, Not Found
149
- else 500 # Internal Error, Server Error
150
- end
151
-
152
- error_payload = { code: code, message: message }
153
- error_payload[:data] = data if data
154
-
155
- body = {
156
- jsonrpc: "2.0",
157
- id: id,
158
- error: error_payload
159
- }.to_json
160
-
161
- [status, { "Content-Type" => "application/json" }, [body]]
162
- end
163
- end
164
- end
165
- end
166
- end
@@ -1,77 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module VectorMCP
4
- module Transport
5
- class SSE
6
- # Configures Puma server for production-ready SSE transport.
7
- # Handles server setup, threading, and resource management.
8
- class PumaConfig
9
- attr_reader :host, :port, :logger
10
-
11
- # Initializes Puma configuration.
12
- #
13
- # @param host [String] Host to bind to
14
- # @param port [Integer] Port to listen on
15
- # @param logger [Logger] Logger instance
16
- def initialize(host, port, logger)
17
- @host = host
18
- @port = port
19
- @logger = logger
20
- end
21
-
22
- # Configures a Puma server instance.
23
- #
24
- # @param server [Puma::Server] The Puma server to configure
25
- def configure(server)
26
- server.add_tcp_listener(host, port)
27
-
28
- # Configure threading for production use
29
- configure_threading(server)
30
-
31
- # Set up server options
32
- configure_server_options(server)
33
-
34
- logger.debug { "Puma server configured for #{host}:#{port}" }
35
- end
36
-
37
- private
38
-
39
- # Configures threading parameters for optimal performance.
40
- #
41
- # @param server [Puma::Server] The Puma server
42
- def configure_threading(server)
43
- # Set thread pool size based on CPU cores and expected load
44
- min_threads = 2
45
- max_threads = [4, Etc.nprocessors * 2].max
46
-
47
- # Puma 6.x does not expose min_threads= and max_threads= as public API.
48
- # Thread pool sizing should be set via Puma DSL/config before server creation.
49
- # For legacy compatibility, set if possible, otherwise log a warning.
50
- if server.respond_to?(:min_threads=) && server.respond_to?(:max_threads=)
51
- server.min_threads = min_threads
52
- server.max_threads = max_threads
53
- logger.debug { "Puma configured with #{min_threads}-#{max_threads} threads" }
54
- else
55
- logger.warn { "Puma::Server does not support direct thread pool sizing; set threads via Puma config DSL before server creation." }
56
- end
57
- end
58
-
59
- # Configures server-specific options.
60
- #
61
- # @param server [Puma::Server] The Puma server
62
- def configure_server_options(server)
63
- # Set server-specific options for SSE handling
64
- server.leak_stack_on_error = false if server.respond_to?(:leak_stack_on_error=)
65
-
66
- # Configure timeouts appropriate for SSE connections
67
- # SSE connections should be long-lived, so we set generous timeouts
68
- if server.respond_to?(:first_data_timeout=)
69
- server.first_data_timeout = 30 # 30 seconds to send first data
70
- end
71
-
72
- logger.debug { "Puma server options configured for SSE transport" }
73
- end
74
- end
75
- end
76
- end
77
- end
@@ -1,92 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module VectorMCP
4
- module Transport
5
- class SSE
6
- # Manages Server-Sent Events streaming for client connections.
7
- # Handles creation of streaming responses and message broadcasting.
8
- class StreamManager
9
- class << self
10
- # Creates an SSE streaming response body for a client connection.
11
- #
12
- # @param client_conn [ClientConnection] The client connection to stream to
13
- # @param endpoint_url [String] The URL for the client to POST messages to
14
- # @param logger [Logger] Logger instance for debugging
15
- # @return [Enumerator] Rack-compatible streaming response body
16
- def create_sse_stream(client_conn, endpoint_url, logger)
17
- Enumerator.new do |yielder|
18
- # Send initial endpoint event
19
- yielder << format_sse_event("endpoint", endpoint_url)
20
- logger.debug { "Sent endpoint event to client #{client_conn.session_id}: #{endpoint_url}" }
21
-
22
- # Start streaming thread for this client
23
- client_conn.stream_thread = Thread.new do
24
- stream_messages_to_client(client_conn, yielder, logger)
25
- end
26
-
27
- # Keep the connection alive by yielding from the streaming thread
28
- client_conn.stream_thread.join
29
- rescue StandardError => e
30
- logger.error { "Error in SSE stream for client #{client_conn.session_id}: #{e.message}\n#{e.backtrace.join("\n")}" }
31
- ensure
32
- logger.debug { "SSE stream ended for client #{client_conn.session_id}" }
33
- client_conn.close
34
- end
35
- end
36
-
37
- # Enqueues a message to a specific client connection.
38
- #
39
- # @param client_conn [ClientConnection] The target client connection
40
- # @param message [Hash] The JSON-RPC message to send
41
- # @return [Boolean] true if message was enqueued successfully
42
- def enqueue_message(client_conn, message)
43
- return false unless client_conn && !client_conn.closed?
44
-
45
- client_conn.enqueue_message(message)
46
- end
47
-
48
- private
49
-
50
- # Streams messages from a client's queue to the SSE connection.
51
- # This method runs in a dedicated thread per client.
52
- #
53
- # @param client_conn [ClientConnection] The client connection
54
- # @param yielder [Enumerator::Yielder] The response yielder
55
- # @param logger [Logger] Logger instance
56
- def stream_messages_to_client(client_conn, yielder, logger)
57
- logger.debug { "Starting message streaming thread for client #{client_conn.session_id}" }
58
-
59
- loop do
60
- message = client_conn.dequeue_message
61
- break if message.nil? # Queue closed or connection closed
62
-
63
- begin
64
- json_message = message.to_json
65
- sse_data = format_sse_event("message", json_message)
66
- yielder << sse_data
67
-
68
- logger.debug { "Streamed message to client #{client_conn.session_id}: #{json_message}" }
69
- rescue StandardError => e
70
- logger.error { "Error streaming message to client #{client_conn.session_id}: #{e.message}" }
71
- break
72
- end
73
- end
74
-
75
- logger.debug { "Message streaming thread ended for client #{client_conn.session_id}" }
76
- rescue StandardError => e
77
- logger.error { "Fatal error in streaming thread for client #{client_conn.session_id}: #{e.message}\n#{e.backtrace.join("\n")}" }
78
- end
79
-
80
- # Formats data as a Server-Sent Event.
81
- #
82
- # @param event [String] The event type
83
- # @param data [String] The event data
84
- # @return [String] Properly formatted SSE event
85
- def format_sse_event(event, data)
86
- "event: #{event}\ndata: #{data}\n\n"
87
- end
88
- end
89
- end
90
- end
91
- end
92
- end