vector_mcp 0.4.0 → 0.6.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 +45 -1
- data/README.md +39 -0
- data/lib/vector_mcp/definitions.rb +30 -0
- data/lib/vector_mcp/handlers/core.rb +32 -89
- data/lib/vector_mcp/invocation.rb +106 -0
- data/lib/vector_mcp/middleware/anonymizer.rb +186 -0
- data/lib/vector_mcp/middleware/hook.rb +7 -24
- data/lib/vector_mcp/middleware.rb +26 -9
- data/lib/vector_mcp/request_context.rb +97 -14
- data/lib/vector_mcp/security/auth_manager.rb +15 -14
- 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 +0 -47
- data/lib/vector_mcp/security/session_context.rb +11 -27
- data/lib/vector_mcp/security/strategies/api_key.rb +7 -52
- data/lib/vector_mcp/security/strategies/custom.rb +15 -39
- data/lib/vector_mcp/security/strategies/jwt_token.rb +7 -17
- data/lib/vector_mcp/server/capabilities.rb +22 -26
- data/lib/vector_mcp/server/message_handling.rb +24 -17
- data/lib/vector_mcp/server/registry.rb +70 -120
- data/lib/vector_mcp/server.rb +53 -19
- data/lib/vector_mcp/session.rb +55 -27
- data/lib/vector_mcp/token_store.rb +80 -0
- data/lib/vector_mcp/transport/base_session_manager.rb +12 -38
- data/lib/vector_mcp/transport/http_stream/event_store.rb +14 -16
- data/lib/vector_mcp/transport/http_stream/session_manager.rb +20 -61
- data/lib/vector_mcp/transport/http_stream/stream_handler.rb +7 -7
- data/lib/vector_mcp/transport/http_stream.rb +95 -55
- data/lib/vector_mcp/util/token_sweeper.rb +74 -0
- data/lib/vector_mcp/version.rb +1 -1
- data/lib/vector_mcp.rb +11 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '09966af580b3bf36b9e17f89ef7b592391c42e63b974a17a1db6a852466b9a56'
|
|
4
|
+
data.tar.gz: 0d04775542e96421bed373179fbfb13864ddfb0aa8256eabea4fddf966d47860
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 33da62c2ffd5dcab4f5e0f7a74426e52885effb4d09c0d82cb479611df838401bfb0de9edb2777a7bb9f21cca190499fa33ff25ee14c46c42a26183b58efa152
|
|
7
|
+
data.tar.gz: 695aa730c3116bdad34191e3b2b38a0ffea1f10772a2b0e27306f2044b934ab7f576999c13c7bf7568a904af8f82eb8c144830e4a4aebd3aaaec4e8997865430
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,48 @@
|
|
|
1
|
-
## [
|
|
1
|
+
## [0.6.0] – 2026-07-19
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
* **Per-request `VectorMCP::Invocation`**: Each incoming request is now dispatched through its own request-scoped view of the session, carrying that request's `RequestContext` and resolved `Security::SessionContext`. Handlers receive it as their `session` argument; it delegates all session-lived state (`id`, `data`, `initialize!`, `sample`, ...) and additionally exposes the resolved auth API directly (`user`, `authenticated?`, `can?`, ...).
|
|
6
|
+
* **`RequestContext.coerce` and hash-style access**: `RequestContext` is now the single normalization boundary for authentication. It coerces legacy request shapes (raw Rack env, `{headers:, params:}` hashes) in one place and supports `[]`/`dig`/`key?` so custom auth handlers written against the legacy request hash keep working unchanged.
|
|
7
|
+
* **Session lifecycle on `VectorMCP::Session`**: `created_at`, `last_accessed_at`, `touch!`, `expired?(timeout)`, `age`, and a transport-owned `metadata` hash.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
* **Security fix — request identity isolation**: Concurrent POSTs on one session could previously be authenticated against each other's headers, because request data was stored as mutable state on the shared session. Request-scoped data now travels on the per-request `Invocation` and is never written onto the session.
|
|
12
|
+
* **One `Session` class**: The transport-internal `Session` wrapper structs (in `BaseSessionManager` and `HttpStream::SessionManager`) are gone; session managers store and return `VectorMCP::Session` directly. HTTP streaming connection state lives in the `SessionManager::SessionStreaming` module.
|
|
13
|
+
* **Unified handler interface**: Resource handlers' second argument is now the per-request invocation instead of a bare `Security::SessionContext`. Existing handlers keep working — the invocation forwards the full auth API (`user`, `authenticated?`, `can?`, `user_identifier`, ...; note: not `to_h`) — and additionally gain access to session identity and request data.
|
|
14
|
+
* **Session managers no longer auto-create sessions from client-supplied IDs** (session-fixation hardening in `BaseSessionManager`, matching the existing HTTP stream behavior).
|
|
15
|
+
* Authentication strategies consume `RequestContext` directly; the auth request no longer carries the undocumented `:session_id` key.
|
|
16
|
+
|
|
17
|
+
### Deprecated (removal in 0.7)
|
|
18
|
+
|
|
19
|
+
* The request-scoped API on `Session`: `request_context=`, `update_request_context`, `request_headers?`, `request_params?`, `request_header`, `request_param`. These now emit warnings via `VectorMCP.deprecation_warning`; use the per-request invocation passed to handlers instead.
|
|
20
|
+
* The third `session_id` argument of `Server#handle_message` (now optional, defaults to `session.id`; silent deprecation).
|
|
21
|
+
|
|
22
|
+
## [0.5.0] – 2026-04-22
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
* **Token-Based Field Anonymization**: Added a general-purpose anonymization pipeline that substitutes sensitive string fields with stable opaque tokens before tool results reach the LLM and restores them on inbound tool invocations.
|
|
27
|
+
- `VectorMCP::TokenStore` — thread-safe bidirectional value ↔ token store with idempotent tokenization.
|
|
28
|
+
- `VectorMCP::Util::TokenSweeper` — stateless recursive traversal utility for parsed JSON structures.
|
|
29
|
+
- `VectorMCP::Middleware::Anonymizer` — middleware wiring the store and sweeper with application-supplied field rules and optional atomic-key handling; registers via `anonymizer.install_on(server)`.
|
|
30
|
+
|
|
31
|
+
* **OAuth 2.1 Resource Server mode (HTTP Stream transport)**: `enable_authentication!` now accepts a `resource_metadata_url:` option. When set, unauthenticated requests to `/mcp` return HTTP `401` with a `WWW-Authenticate: Bearer realm="mcp", resource_metadata="<url>"` header (RFC 9728), enabling MCP clients such as Claude Desktop to discover the authorization server and initiate OAuth 2.1 + PKCE flows. Opt-in: when `resource_metadata_url` is not set, the existing JSON-RPC `-32401` behavior is unchanged. See [docs/oauth_resource_server.md](docs/oauth_resource_server.md) and [docs/rails_oauth_integration.md](docs/rails_oauth_integration.md) for end-to-end guidance.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
* **Auth Result Value Object**: Replaced the internal authentication result hashes with a dedicated `AuthResult` value object for clearer, type-safe handoff between auth strategies and the security middleware.
|
|
36
|
+
* **Consolidated Middleware Hook Types**: `HOOK_TYPES` and `HOOK_OPERATION_TYPES` are now derived from a single source of truth, eliminating drift between the two lists.
|
|
37
|
+
* **Puma Thread Pool Tuning**: Standalone HTTP stream transport now configures the Puma thread pool explicitly for more predictable concurrency behavior.
|
|
38
|
+
* **Event Store Offset Tracking**: Replaced the O(n) event store index rebuild with O(1) offset tracking, reducing overhead for sessions with long event histories.
|
|
39
|
+
* **HTTP Stream Request Tracking**: Removed the redundant `@request_mutex` from HTTP stream request tracking and simplified the surrounding locking.
|
|
40
|
+
* **Handlers::Core Cleanup**: Renamed misleading identifiers and removed dead legacy branches in `Handlers::Core`.
|
|
41
|
+
* **General Complexity Reduction**: Reduced accidental complexity across six refactoring targets in the core request path.
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
* **TokenStore Read-After-Write Consistency**: Closed a read-after-write consistency hole in `TokenStore` so concurrent tokenization and resolution no longer race.
|
|
2
46
|
|
|
3
47
|
## [0.4.0] – 2026-04-10
|
|
4
48
|
|
data/README.md
CHANGED
|
@@ -14,7 +14,9 @@ VectorMCP is a Ruby implementation of the Model Context Protocol (MCP) server-si
|
|
|
14
14
|
- Class-based tools via `VectorMCP::Tool`, plus the original block-based `register_tool` API
|
|
15
15
|
- Rack and Rails mounting through `server.rack_app`
|
|
16
16
|
- Opt-in authentication and authorization, structured logging, and middleware hooks
|
|
17
|
+
- Request-scoped identity: every request is dispatched through its own invocation, so concurrent requests on one session can never observe each other's headers or auth
|
|
17
18
|
- Image-aware tools/resources/prompts, roots, and server-initiated sampling
|
|
19
|
+
- Token-based field anonymization middleware to keep sensitive values out of LLM context
|
|
18
20
|
|
|
19
21
|
## Requirements
|
|
20
22
|
|
|
@@ -161,6 +163,24 @@ end
|
|
|
161
163
|
|
|
162
164
|
`VectorMCP::Tool` also supports `type: :date` and `type: :datetime`, which are validated as strings in JSON Schema and coerced to `Date` and `Time` before `#call` runs.
|
|
163
165
|
|
|
166
|
+
Handlers that take a second argument receive the per-request invocation, which exposes session identity, the request's headers/params, and the authenticated user in one place:
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
server.register_tool(
|
|
170
|
+
name: "whoami",
|
|
171
|
+
description: "Reports the caller's identity",
|
|
172
|
+
input_schema: { type: "object", properties: {} }
|
|
173
|
+
) do |_args, invocation|
|
|
174
|
+
{
|
|
175
|
+
session: invocation.id,
|
|
176
|
+
user: invocation.user,
|
|
177
|
+
api_key_header: invocation.request_header("X-API-Key")
|
|
178
|
+
}
|
|
179
|
+
end
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Resource handlers get the same invocation as their second argument (it also answers the familiar `user` / `authenticated?` / `can?` queries, so handlers written against the older security-context argument keep working unchanged).
|
|
183
|
+
|
|
164
184
|
## Security and Middleware
|
|
165
185
|
|
|
166
186
|
VectorMCP keeps security opt-in, but the primitives are built in:
|
|
@@ -188,10 +208,27 @@ server.enable_authentication!(strategy: :custom) do |request|
|
|
|
188
208
|
end
|
|
189
209
|
```
|
|
190
210
|
|
|
211
|
+
For MCP clients that speak OAuth 2.1 (e.g. Claude Desktop), pass a `resource_metadata_url:` to turn on RFC 9728 discovery. Unauthenticated requests to `/mcp` return `401` with a `WWW-Authenticate` header pointing at the configured metadata document, and the client drives the rest of the OAuth dance automatically. See [docs/oauth_resource_server.md](./docs/oauth_resource_server.md) for the feature reference and [docs/rails_oauth_integration.md](./docs/rails_oauth_integration.md) for a full Rails + Doorkeeper recipe.
|
|
212
|
+
|
|
191
213
|
Middleware can hook into tool, resource, prompt, sampling, auth, and transport events, including `before_auth`, `after_auth`, `on_auth_error`, `before_request`, `after_response`, and `on_transport_error`.
|
|
192
214
|
|
|
193
215
|
See [security/README.md](./security/README.md) for the full security guide.
|
|
194
216
|
|
|
217
|
+
### Field Anonymization
|
|
218
|
+
|
|
219
|
+
Keep sensitive string values out of the LLM context by substituting them with stable opaque tokens. Values are tokenized on outbound tool results and restored on inbound tool arguments, so the LLM sees only tokens while your handlers receive the original data.
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
anonymizer = VectorMCP::Middleware::Anonymizer.new(
|
|
223
|
+
store: VectorMCP::TokenStore.new,
|
|
224
|
+
field_rules: [
|
|
225
|
+
{ pattern: /email/i, prefix: "EMAIL" },
|
|
226
|
+
{ pattern: /\bssn\b/i, prefix: "SSN" }
|
|
227
|
+
]
|
|
228
|
+
)
|
|
229
|
+
anonymizer.install_on(server)
|
|
230
|
+
```
|
|
231
|
+
|
|
195
232
|
## Transport Notes
|
|
196
233
|
|
|
197
234
|
- VectorMCP ships with streamable HTTP as its built-in transport
|
|
@@ -223,6 +260,8 @@ curl -X POST http://localhost:8080/mcp \
|
|
|
223
260
|
- [CHANGELOG.md](./CHANGELOG.md)
|
|
224
261
|
- [examples/](./examples/)
|
|
225
262
|
- [docs/rails-setup-guide.md](./docs/rails-setup-guide.md)
|
|
263
|
+
- [docs/rails_oauth_integration.md](./docs/rails_oauth_integration.md)
|
|
264
|
+
- [docs/oauth_resource_server.md](./docs/oauth_resource_server.md)
|
|
226
265
|
- [docs/streamable-http-spec-compliance.md](./docs/streamable-http-spec-compliance.md)
|
|
227
266
|
- [security/README.md](./security/README.md)
|
|
228
267
|
- [MCP Specification](https://modelcontextprotocol.io/)
|
|
@@ -29,6 +29,36 @@ module VectorMCP
|
|
|
29
29
|
}.compact # Remove nil values
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
# Class method to create a tool whose input_schema already declares a
|
|
33
|
+
# base64-encoded image property. Mirrors Resource.from_image_file and
|
|
34
|
+
# Prompt.with_image_support — keeps schema-building logic with the
|
|
35
|
+
# definition, not with the registry.
|
|
36
|
+
#
|
|
37
|
+
# @param name [String] Unique name for the tool.
|
|
38
|
+
# @param description [String] Human-readable description.
|
|
39
|
+
# @param image_parameter [String] Name of the image parameter.
|
|
40
|
+
# @param additional_parameters [Hash] Additional JSON Schema properties.
|
|
41
|
+
# @param required_parameters [Array<String>] Required parameter names.
|
|
42
|
+
# @param handler [Proc] Tool handler block.
|
|
43
|
+
# @return [Tool]
|
|
44
|
+
def self.with_image_support(name:, description:, image_parameter: "image",
|
|
45
|
+
additional_parameters: {}, required_parameters: [], &handler)
|
|
46
|
+
image_property = {
|
|
47
|
+
type: "string",
|
|
48
|
+
description: "Base64 encoded image data or file path to image",
|
|
49
|
+
contentEncoding: "base64",
|
|
50
|
+
contentMediaType: "image/*"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
input_schema = {
|
|
54
|
+
type: "object",
|
|
55
|
+
properties: { image_parameter => image_property }.merge(additional_parameters),
|
|
56
|
+
required: required_parameters
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
new(name, description, input_schema, handler)
|
|
60
|
+
end
|
|
61
|
+
|
|
32
62
|
# Checks if this tool supports image inputs based on its input schema.
|
|
33
63
|
# @return [Boolean] True if the tool's input schema includes image properties.
|
|
34
64
|
def supports_image_input?
|
|
@@ -55,7 +55,7 @@ module VectorMCP
|
|
|
55
55
|
context = create_tool_context(tool_name, params, session, server)
|
|
56
56
|
|
|
57
57
|
begin
|
|
58
|
-
session_context =
|
|
58
|
+
session_context = authenticate_request!(session, server, operation_type: :tool_call, operation_name: tool_name)
|
|
59
59
|
|
|
60
60
|
context = server.middleware_manager.execute_hooks(:before_tool_call, context)
|
|
61
61
|
return handle_middleware_error(context) if context.error?
|
|
@@ -64,7 +64,7 @@ module VectorMCP
|
|
|
64
64
|
arguments = context.params["arguments"] || {}
|
|
65
65
|
tool = find_tool!(tool_name, server)
|
|
66
66
|
authorize_action!(session_context, :call, tool, server)
|
|
67
|
-
|
|
67
|
+
validate_tool_arguments!(tool_name, tool, arguments)
|
|
68
68
|
|
|
69
69
|
result = execute_tool_handler(tool, arguments, session)
|
|
70
70
|
context.result = build_tool_result(result)
|
|
@@ -105,7 +105,7 @@ module VectorMCP
|
|
|
105
105
|
context = create_resource_context(uri_s, params, session, server)
|
|
106
106
|
|
|
107
107
|
begin
|
|
108
|
-
session_context =
|
|
108
|
+
session_context = authenticate_request!(session, server, operation_type: :resource_read, operation_name: uri_s)
|
|
109
109
|
|
|
110
110
|
context = server.middleware_manager.execute_hooks(:before_resource_read, context)
|
|
111
111
|
return handle_middleware_error(context) if context.error?
|
|
@@ -114,7 +114,7 @@ module VectorMCP
|
|
|
114
114
|
resource = find_resource!(uri_s, server)
|
|
115
115
|
authorize_action!(session_context, :read, resource, server)
|
|
116
116
|
|
|
117
|
-
content_raw = execute_resource_handler(resource, context.params,
|
|
117
|
+
content_raw = execute_resource_handler(resource, context.params, session)
|
|
118
118
|
contents = process_resource_content(content_raw, resource, uri_s)
|
|
119
119
|
|
|
120
120
|
context.result = { contents: contents }
|
|
@@ -206,7 +206,7 @@ module VectorMCP
|
|
|
206
206
|
prompt = fetch_prompt(prompt_name, server)
|
|
207
207
|
|
|
208
208
|
arguments = context_params["arguments"] || {}
|
|
209
|
-
|
|
209
|
+
validate_prompt_arguments!(prompt_name, prompt, arguments)
|
|
210
210
|
|
|
211
211
|
# Call the registered handler after arguments were validated
|
|
212
212
|
result_data = prompt.handler.call(arguments)
|
|
@@ -280,7 +280,7 @@ module VectorMCP
|
|
|
280
280
|
# @param arguments [Hash] The arguments supplied by the client.
|
|
281
281
|
# @return [void]
|
|
282
282
|
# @raise [VectorMCP::InvalidParamsError] if arguments are invalid (e.g., missing, unknown, wrong type).
|
|
283
|
-
def self.
|
|
283
|
+
def self.validate_prompt_arguments!(prompt_name, prompt, arguments)
|
|
284
284
|
ensure_hash!(prompt_name, arguments, "arguments")
|
|
285
285
|
|
|
286
286
|
arg_defs = prompt.respond_to?(:arguments) ? (prompt.arguments || []) : []
|
|
@@ -291,7 +291,7 @@ module VectorMCP
|
|
|
291
291
|
raise VectorMCP::InvalidParamsError.new("Invalid arguments",
|
|
292
292
|
details: build_invalid_arg_details(prompt_name, missing, unknown))
|
|
293
293
|
end
|
|
294
|
-
private_class_method :
|
|
294
|
+
private_class_method :validate_prompt_arguments!
|
|
295
295
|
|
|
296
296
|
# Ensures a given value is a Hash.
|
|
297
297
|
# @api private
|
|
@@ -369,7 +369,7 @@ module VectorMCP
|
|
|
369
369
|
# @param arguments [Hash] The arguments supplied by the client.
|
|
370
370
|
# @return [void]
|
|
371
371
|
# @raise [VectorMCP::InvalidParamsError] if arguments fail validation.
|
|
372
|
-
def self.
|
|
372
|
+
def self.validate_tool_arguments!(tool_name, tool, arguments)
|
|
373
373
|
return unless tool.input_schema.is_a?(Hash)
|
|
374
374
|
return if tool.input_schema.empty?
|
|
375
375
|
|
|
@@ -397,72 +397,23 @@ module VectorMCP
|
|
|
397
397
|
}
|
|
398
398
|
)
|
|
399
399
|
end
|
|
400
|
-
private_class_method :
|
|
400
|
+
private_class_method :validate_tool_arguments!
|
|
401
401
|
private_class_method :raise_tool_validation_error
|
|
402
402
|
|
|
403
|
-
#
|
|
404
|
-
|
|
405
|
-
# Check security for tool access
|
|
406
|
-
# @api private
|
|
407
|
-
# @param session [VectorMCP::Session] The current session
|
|
408
|
-
# @param tool [VectorMCP::Definitions::Tool] The tool being accessed
|
|
409
|
-
# @param server [VectorMCP::Server] The server instance
|
|
410
|
-
# @return [Hash] Security check result
|
|
411
|
-
def self.check_tool_security(session, tool, server)
|
|
412
|
-
# Extract request context from session for security middleware
|
|
413
|
-
request = extract_request_from_session(session)
|
|
414
|
-
server.security_middleware.process_request(request, action: :call, resource: tool)
|
|
415
|
-
end
|
|
416
|
-
private_class_method :check_tool_security
|
|
417
|
-
|
|
418
|
-
# Check security for resource access
|
|
419
|
-
# @api private
|
|
420
|
-
# @param session [VectorMCP::Session] The current session
|
|
421
|
-
# @param resource [VectorMCP::Definitions::Resource] The resource being accessed
|
|
422
|
-
# @param server [VectorMCP::Server] The server instance
|
|
423
|
-
# @return [Hash] Security check result
|
|
424
|
-
def self.check_resource_security(session, resource, server)
|
|
425
|
-
request = extract_request_from_session(session)
|
|
426
|
-
server.security_middleware.process_request(request, action: :read, resource: resource)
|
|
427
|
-
end
|
|
428
|
-
private_class_method :check_resource_security
|
|
429
|
-
|
|
430
|
-
# Extract request context from session for security processing
|
|
403
|
+
# Extract the request context from the session for security processing.
|
|
431
404
|
# @api private
|
|
432
405
|
# @param session [VectorMCP::Session] The current session
|
|
433
|
-
# @return [
|
|
434
|
-
def self.
|
|
406
|
+
# @return [VectorMCP::RequestContext] Request context for security middleware
|
|
407
|
+
def self.extract_auth_credentials(session)
|
|
435
408
|
# All sessions should have a request_context - this is enforced by Session initialization
|
|
436
409
|
unless session.respond_to?(:request_context) && session.request_context
|
|
437
410
|
raise VectorMCP::InternalError,
|
|
438
411
|
"Session missing request_context - transport layer integration error. Session ID: #{session.id}"
|
|
439
412
|
end
|
|
440
413
|
|
|
441
|
-
|
|
442
|
-
headers: session.request_context.headers,
|
|
443
|
-
params: session.request_context.params,
|
|
444
|
-
session_id: session.id
|
|
445
|
-
}
|
|
446
|
-
end
|
|
447
|
-
private_class_method :extract_request_from_session
|
|
448
|
-
|
|
449
|
-
# Handle security failure by raising appropriate error
|
|
450
|
-
# @api private
|
|
451
|
-
# @param security_result [Hash] The security check result
|
|
452
|
-
# @return [void]
|
|
453
|
-
# @raise [VectorMCP::UnauthorizedError, VectorMCP::ForbiddenError]
|
|
454
|
-
def self.handle_security_failure(security_result)
|
|
455
|
-
case security_result[:error_code]
|
|
456
|
-
when "AUTHENTICATION_REQUIRED"
|
|
457
|
-
raise VectorMCP::UnauthorizedError, security_result[:error]
|
|
458
|
-
when "AUTHORIZATION_FAILED"
|
|
459
|
-
raise VectorMCP::ForbiddenError, security_result[:error]
|
|
460
|
-
else
|
|
461
|
-
# Fallback to generic unauthorized error
|
|
462
|
-
raise VectorMCP::UnauthorizedError, security_result[:error] || "Security check failed"
|
|
463
|
-
end
|
|
414
|
+
session.request_context
|
|
464
415
|
end
|
|
465
|
-
private_class_method :
|
|
416
|
+
private_class_method :extract_auth_credentials
|
|
466
417
|
|
|
467
418
|
# Handle middleware error by returning appropriate response or raising error
|
|
468
419
|
# @api private
|
|
@@ -499,24 +450,19 @@ module VectorMCP
|
|
|
499
450
|
tool
|
|
500
451
|
end
|
|
501
452
|
|
|
502
|
-
#
|
|
503
|
-
def self.
|
|
504
|
-
request =
|
|
505
|
-
|
|
453
|
+
# Authenticate the current request and return the caller's identity.
|
|
454
|
+
def self.authenticate_request!(session, server, operation_type:, operation_name:)
|
|
455
|
+
request = extract_auth_credentials(session)
|
|
456
|
+
# before_auth middleware hooks see (and may modify) the request as a hash;
|
|
457
|
+
# AuthManager coerces it back into a RequestContext at its boundary.
|
|
458
|
+
context = create_auth_context(operation_type, operation_name, request.to_h, session, server)
|
|
506
459
|
context = server.middleware_manager.execute_hooks(:before_auth, context)
|
|
507
460
|
raise context.error if context.error?
|
|
508
461
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
server.security_middleware.authenticate_request(request)
|
|
512
|
-
else
|
|
513
|
-
legacy_result = server.security_middleware.process_request(request)
|
|
514
|
-
legacy_result[:session_context]
|
|
515
|
-
end
|
|
516
|
-
session.security_context = session_context if session.respond_to?(:security_context=)
|
|
462
|
+
session_context = server.security_middleware.authenticate_request(context.params)
|
|
463
|
+
session.security_context = session_context
|
|
517
464
|
|
|
518
|
-
|
|
519
|
-
raise VectorMCP::UnauthorizedError, "Authentication required" if auth_required && !session_context.authenticated?
|
|
465
|
+
raise VectorMCP::UnauthorizedError, "Authentication required" if server.auth_manager.required? && !session_context.authenticated?
|
|
520
466
|
|
|
521
467
|
context.result = session_context
|
|
522
468
|
server.middleware_manager.execute_hooks(:after_auth, context)
|
|
@@ -572,25 +518,22 @@ module VectorMCP
|
|
|
572
518
|
server.resources[uri_s]
|
|
573
519
|
end
|
|
574
520
|
|
|
575
|
-
#
|
|
521
|
+
# Check authorization and raise ForbiddenError if denied
|
|
576
522
|
def self.authorize_action!(session_context, action, resource, server)
|
|
577
|
-
|
|
578
|
-
server.security_middleware.authorize_action(session_context, action, resource)
|
|
579
|
-
else
|
|
580
|
-
legacy_result = server.security_middleware.process_request({}, action: action, resource: resource)
|
|
581
|
-
legacy_result[:success]
|
|
582
|
-
end
|
|
583
|
-
return if authorized
|
|
523
|
+
return if server.security_middleware.authorize_action(session_context, action, resource)
|
|
584
524
|
|
|
585
525
|
raise VectorMCP::ForbiddenError, "Access denied"
|
|
586
526
|
end
|
|
587
527
|
|
|
588
|
-
# Execute resource handler with proper arity handling
|
|
589
|
-
|
|
528
|
+
# Execute resource handler with proper arity handling.
|
|
529
|
+
# The second argument is the per-request invocation, which also exposes
|
|
530
|
+
# the SessionContext auth API for handlers written against the legacy
|
|
531
|
+
# (params, session_context) signature.
|
|
532
|
+
def self.execute_resource_handler(resource, params, invocation)
|
|
590
533
|
if [1, -1].include?(resource.handler.arity)
|
|
591
534
|
resource.handler.call(params)
|
|
592
535
|
else
|
|
593
|
-
resource.handler.call(params,
|
|
536
|
+
resource.handler.call(params, invocation)
|
|
594
537
|
end
|
|
595
538
|
end
|
|
596
539
|
|
|
@@ -633,7 +576,7 @@ module VectorMCP
|
|
|
633
576
|
raise error
|
|
634
577
|
end
|
|
635
578
|
|
|
636
|
-
private_class_method :handle_middleware_error, :create_tool_context, :find_tool!, :
|
|
579
|
+
private_class_method :handle_middleware_error, :create_tool_context, :find_tool!, :authenticate_request!,
|
|
637
580
|
:execute_tool_handler, :build_tool_result, :handle_tool_error, :create_resource_context,
|
|
638
581
|
:find_resource!, :authorize_action!, :execute_resource_handler,
|
|
639
582
|
:process_resource_content, :handle_resource_error, :create_auth_context,
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "forwardable"
|
|
4
|
+
require_relative "request_context"
|
|
5
|
+
require_relative "security/session_context"
|
|
6
|
+
|
|
7
|
+
module VectorMCP
|
|
8
|
+
# A per-request view of a {Session}.
|
|
9
|
+
#
|
|
10
|
+
# Carries the request-scoped state — the incoming request's
|
|
11
|
+
# {RequestContext} and the {Security::SessionContext} resolved for it —
|
|
12
|
+
# while delegating session-lived state (identity, initialization,
|
|
13
|
+
# user data, sampling) to the underlying session. Transports build one
|
|
14
|
+
# Invocation per incoming request and pass it through message dispatch,
|
|
15
|
+
# so concurrent requests on the same session each observe their own
|
|
16
|
+
# request data and authentication result.
|
|
17
|
+
#
|
|
18
|
+
# The Invocation intentionally quacks like a {Session}: tool handlers and
|
|
19
|
+
# request/notification hooks receive it as their `session` argument and
|
|
20
|
+
# can keep using `session.id`, `session.data`, `session.sample`,
|
|
21
|
+
# `session.request_header`, and `session.security_context` unchanged.
|
|
22
|
+
class Invocation
|
|
23
|
+
extend Forwardable
|
|
24
|
+
|
|
25
|
+
# @return [VectorMCP::Session] the underlying session this request belongs to
|
|
26
|
+
attr_reader :session
|
|
27
|
+
|
|
28
|
+
# @return [VectorMCP::RequestContext] this request's context (headers, params, ...)
|
|
29
|
+
attr_reader :request_context
|
|
30
|
+
|
|
31
|
+
# @return [VectorMCP::Security::SessionContext] the security context resolved for this request
|
|
32
|
+
attr_accessor :security_context
|
|
33
|
+
|
|
34
|
+
def_delegators :@session,
|
|
35
|
+
:id, :server, :transport, :data,
|
|
36
|
+
:initialized?, :initialize!, :sample,
|
|
37
|
+
:server_info, :server_capabilities, :protocol_version,
|
|
38
|
+
:client_info, :client_capabilities
|
|
39
|
+
|
|
40
|
+
# The resolved auth API is exposed directly so handlers written against
|
|
41
|
+
# Security::SessionContext (the legacy second argument of resource
|
|
42
|
+
# handlers) keep working when they receive the invocation instead.
|
|
43
|
+
def_delegators :@security_context,
|
|
44
|
+
:user, :authenticated, :authenticated?, :auth_strategy, :authenticated_at,
|
|
45
|
+
:permissions, :can?, :can_access?, :add_permission, :add_permissions,
|
|
46
|
+
:remove_permission, :clear_permissions, :user_identifier, :auth_method,
|
|
47
|
+
:auth_recent?
|
|
48
|
+
|
|
49
|
+
# @param session [VectorMCP::Session] the session the request arrived on
|
|
50
|
+
# @param request_context [RequestContext, Hash, nil] this request's context;
|
|
51
|
+
# defaults to the session's stored context when not provided
|
|
52
|
+
def initialize(session, request_context: nil)
|
|
53
|
+
@session = session
|
|
54
|
+
@request_context = request_context ? RequestContext.coerce(request_context) : session.request_context
|
|
55
|
+
@security_context = Security::SessionContext.anonymous
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Replace this request's context (e.g. from middleware).
|
|
59
|
+
# @param context [RequestContext, Hash] the new request context
|
|
60
|
+
def request_context=(context)
|
|
61
|
+
@request_context = RequestContext.coerce(context)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Merge attributes into this request's context.
|
|
65
|
+
# @param attributes [Hash] attributes to merge
|
|
66
|
+
# @return [RequestContext] the updated request context
|
|
67
|
+
def update_request_context(**attributes)
|
|
68
|
+
@request_context = @request_context.merge(attributes)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @return [Boolean] whether this request carried any headers
|
|
72
|
+
def request_headers?
|
|
73
|
+
@request_context.headers?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @return [Boolean] whether this request carried any query parameters
|
|
77
|
+
def request_params?
|
|
78
|
+
@request_context.params?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @param name [String] the header name
|
|
82
|
+
# @return [String, nil] the header value for this request
|
|
83
|
+
def request_header(name)
|
|
84
|
+
@request_context.header(name)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @param name [String] the parameter name
|
|
88
|
+
# @return [String, nil] the parameter value for this request
|
|
89
|
+
def request_param(name)
|
|
90
|
+
@request_context.param(name)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Invocations compare by their underlying session, so collections that
|
|
94
|
+
# deduplicate sessions (e.g. prompt subscribers) treat every request
|
|
95
|
+
# view of one session as the same subscriber.
|
|
96
|
+
def ==(other)
|
|
97
|
+
other_session = other.is_a?(Invocation) ? other.session : other
|
|
98
|
+
@session == other_session
|
|
99
|
+
end
|
|
100
|
+
alias eql? ==
|
|
101
|
+
|
|
102
|
+
def hash
|
|
103
|
+
@session.hash
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|