vector_mcp 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +82 -0
- data/README.md +147 -337
- data/lib/vector_mcp/definitions.rb +30 -0
- data/lib/vector_mcp/handlers/core.rb +78 -81
- data/lib/vector_mcp/image_util.rb +34 -11
- data/lib/vector_mcp/middleware/anonymizer.rb +186 -0
- data/lib/vector_mcp/middleware/base.rb +1 -5
- data/lib/vector_mcp/middleware/context.rb +11 -1
- data/lib/vector_mcp/middleware/hook.rb +7 -24
- data/lib/vector_mcp/middleware.rb +26 -9
- data/lib/vector_mcp/rails/tool.rb +85 -0
- data/lib/vector_mcp/request_context.rb +1 -1
- data/lib/vector_mcp/security/auth_manager.rb +12 -13
- data/lib/vector_mcp/security/auth_result.rb +33 -0
- data/lib/vector_mcp/security/authorization.rb +5 -9
- data/lib/vector_mcp/security/middleware.rb +2 -2
- data/lib/vector_mcp/security/session_context.rb +11 -27
- data/lib/vector_mcp/security/strategies/api_key.rb +1 -5
- data/lib/vector_mcp/security/strategies/custom.rb +10 -37
- data/lib/vector_mcp/security/strategies/jwt_token.rb +1 -10
- data/lib/vector_mcp/server/capabilities.rb +22 -32
- data/lib/vector_mcp/server/message_handling.rb +21 -14
- data/lib/vector_mcp/server/registry.rb +102 -120
- data/lib/vector_mcp/server.rb +98 -57
- data/lib/vector_mcp/session.rb +5 -3
- data/lib/vector_mcp/token_store.rb +80 -0
- data/lib/vector_mcp/tool.rb +221 -0
- data/lib/vector_mcp/transport/base_session_manager.rb +1 -17
- data/lib/vector_mcp/transport/http_stream/event_store.rb +29 -17
- data/lib/vector_mcp/transport/http_stream/session_manager.rb +41 -36
- data/lib/vector_mcp/transport/http_stream/stream_handler.rb +132 -47
- data/lib/vector_mcp/transport/http_stream.rb +242 -124
- data/lib/vector_mcp/util/token_sweeper.rb +74 -0
- data/lib/vector_mcp/version.rb +1 -1
- data/lib/vector_mcp.rb +8 -8
- metadata +8 -10
- data/lib/vector_mcp/transport/sse/client_connection.rb +0 -113
- data/lib/vector_mcp/transport/sse/message_handler.rb +0 -166
- data/lib/vector_mcp/transport/sse/puma_config.rb +0 -77
- data/lib/vector_mcp/transport/sse/stream_manager.rb +0 -92
- data/lib/vector_mcp/transport/sse.rb +0 -377
- data/lib/vector_mcp/transport/sse_session_manager.rb +0 -188
- data/lib/vector_mcp/transport/stdio.rb +0 -473
- data/lib/vector_mcp/transport/stdio_session_manager.rb +0 -181
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VectorMCP
|
|
4
|
+
module Util
|
|
5
|
+
# Stateless recursive traversal utility for parsed JSON-like structures.
|
|
6
|
+
#
|
|
7
|
+
# {.sweep} walks Hashes, Arrays, and String leaves, yielding each String
|
|
8
|
+
# value together with its parent Hash key (or the enclosing Hash key of
|
|
9
|
+
# the nearest containing Array). The block's return value replaces the
|
|
10
|
+
# String in the output. All other scalar types (Integer, Float, nil,
|
|
11
|
+
# Boolean, etc.) are returned unchanged and are not yielded.
|
|
12
|
+
#
|
|
13
|
+
# The method is purely functional: it never mutates the input structure
|
|
14
|
+
# and always returns a fresh Hash/Array spine when containers are
|
|
15
|
+
# encountered. Circular references are detected via an identity-compared
|
|
16
|
+
# visited set and the originally-referenced node is returned unchanged
|
|
17
|
+
# on cycles.
|
|
18
|
+
module TokenSweeper
|
|
19
|
+
# Traverse +obj+ and return a new structure with String leaves
|
|
20
|
+
# transformed by +block+.
|
|
21
|
+
#
|
|
22
|
+
# @param obj [Object] the object to sweep (typically Hash/Array/String/scalar).
|
|
23
|
+
# @yield [value, parent_key] invoked for each String leaf.
|
|
24
|
+
# @yieldparam value [String] the String value.
|
|
25
|
+
# @yieldparam parent_key [Object, nil] the Hash key under which +value+
|
|
26
|
+
# lives, or +nil+ when the String is a top-level scalar; propagated
|
|
27
|
+
# from the nearest containing Hash when inside Arrays.
|
|
28
|
+
# @yieldreturn [Object] the replacement value.
|
|
29
|
+
# @return [Object] the transformed structure.
|
|
30
|
+
def self.sweep(obj, &block)
|
|
31
|
+
raise ArgumentError, "TokenSweeper.sweep requires a block" unless block
|
|
32
|
+
|
|
33
|
+
walk(obj, nil, {}.compare_by_identity, &block)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class << self
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def walk(obj, parent_key, visited, &)
|
|
40
|
+
case obj
|
|
41
|
+
when Hash then walk_hash(obj, visited, &)
|
|
42
|
+
when Array then walk_array(obj, parent_key, visited, &)
|
|
43
|
+
when String then yield(obj, parent_key)
|
|
44
|
+
else obj
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def walk_hash(hash, visited, &)
|
|
49
|
+
return hash if visited[hash]
|
|
50
|
+
|
|
51
|
+
visited[hash] = true
|
|
52
|
+
begin
|
|
53
|
+
hash.each_with_object({}) do |(key, value), out|
|
|
54
|
+
out[key] = walk(value, key, visited, &)
|
|
55
|
+
end
|
|
56
|
+
ensure
|
|
57
|
+
visited.delete(hash)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def walk_array(array, parent_key, visited, &)
|
|
62
|
+
return array if visited[array]
|
|
63
|
+
|
|
64
|
+
visited[array] = true
|
|
65
|
+
begin
|
|
66
|
+
array.map { |element| walk(element, parent_key, visited, &) }
|
|
67
|
+
ensure
|
|
68
|
+
visited.delete(array)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
data/lib/vector_mcp/version.rb
CHANGED
data/lib/vector_mcp.rb
CHANGED
|
@@ -8,20 +8,21 @@ 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/util/token_sweeper"
|
|
12
|
+
require_relative "vector_mcp/token_store"
|
|
11
13
|
require_relative "vector_mcp/log_filter"
|
|
12
14
|
require_relative "vector_mcp/image_util"
|
|
13
15
|
require_relative "vector_mcp/handlers/core"
|
|
14
|
-
require_relative "vector_mcp/transport/stdio"
|
|
15
|
-
# require_relative "vector_mcp/transport/sse" # Load on demand
|
|
16
16
|
require_relative "vector_mcp/logger"
|
|
17
17
|
require_relative "vector_mcp/middleware"
|
|
18
18
|
require_relative "vector_mcp/server"
|
|
19
|
+
require_relative "vector_mcp/tool"
|
|
19
20
|
|
|
20
21
|
# The VectorMCP module provides a full-featured, opinionated Ruby implementation
|
|
21
22
|
# of the **Model Context Protocol (MCP)**. It gives developers everything needed
|
|
22
23
|
# to spin up an MCP-compatible server—including:
|
|
23
24
|
#
|
|
24
|
-
# * **Transport adapters** (
|
|
25
|
+
# * **Transport adapters** (streamable HTTP)
|
|
25
26
|
# * **High-level abstractions** for *tools*, *resources*, and *prompts*
|
|
26
27
|
# * **JSON-RPC 2.0** message handling with sensible defaults and detailed
|
|
27
28
|
# error reporting helpers
|
|
@@ -40,11 +41,10 @@ require_relative "vector_mcp/server"
|
|
|
40
41
|
# input_schema: {type: "object", properties: {text: {type: "string"}}}
|
|
41
42
|
# ) { |args| args["text"] }
|
|
42
43
|
#
|
|
43
|
-
# server.run # => starts the
|
|
44
|
+
# server.run # => starts the HTTP stream transport and begins processing JSON-RPC messages
|
|
44
45
|
# ```
|
|
45
46
|
#
|
|
46
|
-
#
|
|
47
|
-
# order to serve multiple concurrent clients over HTTP.
|
|
47
|
+
# The default HTTP stream transport supports multiple concurrent clients over HTTP.
|
|
48
48
|
#
|
|
49
49
|
module VectorMCP
|
|
50
50
|
class << self
|
|
@@ -68,8 +68,8 @@ module VectorMCP
|
|
|
68
68
|
# Any positional or keyword arguments are forwarded verbatim to the underlying
|
|
69
69
|
# constructor, so refer to {VectorMCP::Server#initialize} for the full list of
|
|
70
70
|
# accepted parameters.
|
|
71
|
-
def new(
|
|
72
|
-
Server.new(
|
|
71
|
+
def new(*, **)
|
|
72
|
+
Server.new(*, **)
|
|
73
73
|
end
|
|
74
74
|
end
|
|
75
75
|
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.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sergio Bayona
|
|
@@ -132,15 +132,18 @@ files:
|
|
|
132
132
|
- lib/vector_mcp/log_filter.rb
|
|
133
133
|
- lib/vector_mcp/logger.rb
|
|
134
134
|
- lib/vector_mcp/middleware.rb
|
|
135
|
+
- lib/vector_mcp/middleware/anonymizer.rb
|
|
135
136
|
- lib/vector_mcp/middleware/base.rb
|
|
136
137
|
- lib/vector_mcp/middleware/context.rb
|
|
137
138
|
- lib/vector_mcp/middleware/hook.rb
|
|
138
139
|
- lib/vector_mcp/middleware/manager.rb
|
|
140
|
+
- lib/vector_mcp/rails/tool.rb
|
|
139
141
|
- lib/vector_mcp/request_context.rb
|
|
140
142
|
- lib/vector_mcp/sampling/request.rb
|
|
141
143
|
- lib/vector_mcp/sampling/result.rb
|
|
142
144
|
- lib/vector_mcp/security.rb
|
|
143
145
|
- lib/vector_mcp/security/auth_manager.rb
|
|
146
|
+
- lib/vector_mcp/security/auth_result.rb
|
|
144
147
|
- lib/vector_mcp/security/authorization.rb
|
|
145
148
|
- lib/vector_mcp/security/middleware.rb
|
|
146
149
|
- lib/vector_mcp/security/session_context.rb
|
|
@@ -152,20 +155,15 @@ files:
|
|
|
152
155
|
- lib/vector_mcp/server/message_handling.rb
|
|
153
156
|
- lib/vector_mcp/server/registry.rb
|
|
154
157
|
- lib/vector_mcp/session.rb
|
|
158
|
+
- lib/vector_mcp/token_store.rb
|
|
159
|
+
- lib/vector_mcp/tool.rb
|
|
155
160
|
- lib/vector_mcp/transport/base_session_manager.rb
|
|
156
161
|
- lib/vector_mcp/transport/http_stream.rb
|
|
157
162
|
- lib/vector_mcp/transport/http_stream/event_store.rb
|
|
158
163
|
- lib/vector_mcp/transport/http_stream/session_manager.rb
|
|
159
164
|
- 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
165
|
- lib/vector_mcp/util.rb
|
|
166
|
+
- lib/vector_mcp/util/token_sweeper.rb
|
|
169
167
|
- lib/vector_mcp/version.rb
|
|
170
168
|
homepage: https://github.com/sergiobayona/vector_mcp
|
|
171
169
|
licenses:
|
|
@@ -182,7 +180,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
182
180
|
requirements:
|
|
183
181
|
- - ">="
|
|
184
182
|
- !ruby/object:Gem::Version
|
|
185
|
-
version: 3.
|
|
183
|
+
version: '3.2'
|
|
186
184
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
187
185
|
requirements:
|
|
188
186
|
- - ">="
|
|
@@ -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
|
|
@@ -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}: #{VectorMCP::LogFilter.filter_string(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
|