-
Notifications
You must be signed in to change notification settings - Fork 2
Refactoring of mapping between the upstream tool/resource/prompt name #124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
8a910d5
Added VirtualHost mapping
cafalchio 8227d1d
Changed call_tool to use mapping
cafalchio 1477bc6
Changed prompts and resources to use mapping
cafalchio 94f4411
Removed unused code, renamed file, fmt and clippy
cafalchio 1c8fcef
Fixed tests
cafalchio f09e69a
Added empty defaul values, clippy fmt
cafalchio a14a8eb
fixed clippy and e2e test
cafalchio d745ed4
Created ServiceRoute struct
cafalchio 6aece99
Added missing resource_tempolates
cafalchio 7194f25
fixed typo
cafalchio 8021d6e
Updated wiki
cafalchio 0eedda8
Removed NameAlias not needed
cafalchio d2396c7
removed unused code
cafalchio 2ea8795
Added type alias for clarity
cafalchio 02ac728
Added missing type alias for backend
cafalchio d327d15
Added VirtualHostId
cafalchio 3f04483
Fixed typo downstream
cafalchio 806153c
added DownstreamBackendName UpstreamName to ServiceRoute
cafalchio 484e5fb
Ran secrets
cafalchio d2a12e9
secrets
cafalchio File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,141 +1,52 @@ | ||
| # MCP Routing Semantics | ||
|
|
||
| > This page describes the **current transitional routing behavior**. Its live | ||
| > upstream fan-out and durable-session assumptions are not the Phase 3 target. | ||
| > See [ContextForge 2.0 Target Architecture and Roadmap](mcp-capability-allocation.md) | ||
| > for the proposed ownership boundary and migration. | ||
| The external dataplane is a **pure stateless router**. No session state, no `BackendTransports`, no sticky-routing requirement. | ||
|
|
||
| ## Backend Prefix Contract | ||
| ## How a request is routed | ||
|
|
||
| Backend map keys become public identifiers only for **multi-backend virtual hosts without an explicit tool alias**: | ||
| 1. `validate_stateless` extracts `VirtualHost` from request extensions (set by `virtual_host_config` layer from the JWT virtual-host ID). | ||
| 2. Downstream name is looked up in `VirtualHost::tools`, `::resources`, or `::prompts` — an O(1) table lookup. | ||
| 3. `connect_backend_for_request` opens a fresh `StreamableHttpClientTransport`, runs the call, closes the connection. | ||
|
|
||
| ```text | ||
| backend tool "increment" on backend "gateway-one" → "gateway-one-increment" | ||
| backend resource "counter" on backend "gateway-one" → "gateway-one-counter" | ||
| ``` | ||
|
|
||
| Single-backend virtual hosts: identifiers pass through **unchanged**. | ||
|
|
||
| > **Breaking change rule:** changing a backend map key changes downstream identifiers for multi-backend virtual hosts. Do not rename without updating merge logic, split logic, and tests. | ||
|
|
||
| ## Tool Aliases | ||
|
|
||
| `BackendMCPGateway.tool_name_aliases` maps `{downstream_alias: upstream_original}`. Aliases take precedence over prefix fallback. They are advertised and routed exactly as published (case, dots, underscores preserved). | ||
|
|
||
| ## List Operations (fan-out) | ||
|
|
||
| All four list methods fan out to all connected backends concurrently and merge results: | ||
|
|
||
| ```text | ||
| list_tools / list_resources / list_prompts / list_resource_templates | ||
| → all connected backends → merged sorted output | ||
| ``` | ||
| The control plane builds and publishes the routing tables to Redis; the dataplane never derives names at call time. | ||
|
|
||
| Failed/unavailable backends are logged and skipped. Single-backend: identifiers unchanged. Multi-backend: prefixed with backend map key. | ||
| ## Routing table shape | ||
|
|
||
| ## Routed Operations (single backend) | ||
| ```rust | ||
| VirtualHost { backends: HashMap<String, BackendMCPGateway>, | ||
| tools: HashMap<String, ServiceRoute>, | ||
| resources: HashMap<String, ServiceRoute>, | ||
| resource_templates: HashMap<String, ServiceRoute>, | ||
| prompts: HashMap<String, ServiceRoute> } | ||
|
|
||
| Calls targeting one object use the inverse rule. The name splitter walks configured backend names and requires a `-` immediately after the backend name: | ||
|
|
||
| ```text | ||
| gateway-one-increment → backend: gateway-one, tool: increment | ||
| gateway-oneincrement → rejected (no - separator) | ||
| ServiceRoute { backend_name: String, // key into VirtualHost::backends | ||
| upstream_name: String } // name/URI forwarded to the backend | ||
| ``` | ||
|
|
||
| `call_tool` resolves explicit alias first, then falls back to single/multi-backend logic. | ||
|
|
||
| Methods using the same conditional routing: `read_resource`, `subscribe`, `unsubscribe`, `get_prompt`, `complete`. | ||
| Source: [`user_store.rs`](../../crates/contextforge-data-plane-apis/src/user_store.rs) | ||
|
|
||
| ## Federated Pagination | ||
| ## Method quick reference | ||
|
|
||
| The gateway wraps per-backend cursors inside its own opaque token (JSON, treated as opaque by MCP clients). First request: all backends queried. Resume: cursor decoded, exhausted backends skipped. New cursor emitted when any backend has more pages. | ||
| | Method | Behavior | | ||
| | --- | --- | | ||
| | `initialize` (`2026-07-28`) | `INVALID_REQUEST` — not supported by this dataplane. | | ||
| | `initialize` (legacy) | Stub `InitializeResult`; no backend fanout. Supports older clients during migration. | | ||
| | `list_tools`, `list_resources`, `list_resource_templates`, `list_prompts` | `INVALID_REQUEST` — delegated to control plane. | | ||
| | `call_tool` | Lookup in `tools` map → pre-hook → fresh connection → call → post-hook → close. Forwards cancellation; tracks progress tokens. | | ||
| | `read_resource` | Lookup in `resources` map → fresh connection → call with upstream URI → close. | | ||
| | `get_prompt` | Lookup in `prompts` map → pre-hook → fresh connection → call → post-hook → close. | | ||
| | `subscribe`, `unsubscribe`, `complete` | `INVALID_REQUEST` — delegated to control plane. | | ||
| | `ping` | Local success; no backend fanout. | | ||
| | `DELETE` | RMCP handles; `session_id_layer` removes the `LocalUserSessionStore` entry. No backend state to clean up. | | ||
|
|
||
| **Known limitation:** if backend set changes between pages, removed backend's cursor is silently dropped. | ||
| ## Header forwarding | ||
|
|
||
| ## Session State (local process) | ||
| Applied in order per upstream call: Host (from backend URL, HTTPS only) → passthrough (`BackendMCPGateway::passthrough_headers`) → `Mcp-Param-*` auto-forward → trace context → add (`add_headers`, overrides passthrough) → remove (`remove_headers`, applied last). | ||
|
|
||
| Backend RMCP services are stored in `BackendTransports` keyed by: | ||
| ```text | ||
| principal (claims.sub) + backend_name (map key) + downstream_session_id | ||
| ``` | ||
|
|
||
| This is **local process state only**. Implications: | ||
| - After `initialize`, later requests must reach the same process. | ||
| - Sticky routing required for load-balanced deployments. | ||
| - Gateway restart → all sessions lost → clients must re-run `initialize`. | ||
| - Multi-runtime mode (`--single-runtime false`): each runtime thread has its own `BackendTransports` with no cross-thread affinity. | ||
|
|
||
| **Exception: `call_tool` uses per-request backend lifecycle.** Each tool call creates a fresh backend connection, executes the call with plugin hooks, then closes the connection. This bypasses `BackendTransports` entirely and does not require session affinity for tool calls specifically (though other MCP methods still do). | ||
|
|
||
|
|
||
| ```mermaid | ||
| sequenceDiagram | ||
| participant C as MCP Client | ||
| participant GW as Gateway (RMCP) | ||
| participant BT as BackendTransports<br/>(local process state) | ||
| participant LU as LocalUserSessionStore<br/>(LRU 50k / 1h) | ||
| participant BA as Backend A | ||
| participant BB as Backend B | ||
|
|
||
| C->>GW: POST initialize (Mcp-Session-Id: S) | ||
| GW->>BA: initialize (concurrent) | ||
| GW->>BB: initialize (concurrent) | ||
| BA-->>GW: InitializeResult | ||
| BB-->>GW: InitializeResult | ||
| GW->>BT: store RunningService keyed by sub+backend+S | ||
| GW->>LU: store session entry for sub+S | ||
| GW-->>C: merged InitializeResult | ||
|
|
||
| C->>GW: POST call_tool (Mcp-Session-Id: S) | ||
| GW->>BT: lookup sub+backend+S → Arc<RunningService> | ||
| BT-->>GW: RunningService handle | ||
| GW->>BA: call_tool (routed by name prefix) | ||
| BA-->>GW: ToolResult | ||
| GW-->>C: ToolResult | ||
|
|
||
| C->>GW: DELETE (Mcp-Session-Id: S) | ||
| GW->>GW: RMCP handles DELETE | ||
| GW->>LU: remove sub+S entry | ||
| GW->>BT: remove all sub+*+S entries | ||
| GW-->>C: 200 OK | ||
| ``` | ||
| Protected headers that config can never touch: `Host`, `Content-Length`, `Content-Type`, all RFC 7230 hop-by-hop headers, `Mcp-Session-Id`, `Accept`, `Last-Event-Id`, and all computed MCP standard headers (`Mcp-Method`, `Mcp-Name`, `Mcp-Protocol-Version`, `Mcp-Param-*`). | ||
|
|
||
| ## Capability Merge | ||
| For clients on `≥ 2026-07-28`, `call_tool` validates `Mcp-Param-*` headers against `BackendMCPGateway::tool_schemas` before contacting the backend. | ||
|
|
||
| On `initialize`, the gateway builds one downstream `InitializeResult` — not a passthrough of any one backend. The source of truth is each backend's `InitializeResult`; the gateway reads `peer_info().capabilities` from each running service and stores them with the backend transport state. | ||
| ## Plugin hooks | ||
|
|
||
| The merge rule (gateway-aware, not a raw union): | ||
| - Enable a top-level capability when ≥1 backend supports it **and** the gateway has a routing story for it. | ||
| - `resources.subscribe` preserved if any backend advertises it (the gateway routes subscribe/unsubscribe and forwards resource-update notifications). | ||
| - `listChanged` not yet advertised (gateway doesn't emit downstream list-changed notifications when upstream lists change). | ||
| - Single-backend passthrough is not a stable contract (`HashMap` iteration order). | ||
| - If no backend reports supported capabilities, returns `ServerCapabilities::default()`. | ||
|
|
||
| **Do not** initialize the downstream capability from just one backend entry — the gateway fronts multiple backends, `HashMap` iteration is non-deterministic, and list methods already merge across all backends. | ||
|
|
||
| ## Cleanup | ||
|
|
||
| `DELETE` with `Mcp-session-id`: | ||
| ```text | ||
| → RMCP handles request | ||
| → on success: remove LocalUserSessionStore entry + BackendTransports entries for principal+session | ||
| ``` | ||
| If RMCP rejects the delete, local state is untouched. | ||
|
|
||
|
|
||
| ## MCP Method Quick Reference | ||
|
|
||
| | Method | Group | Behavior | | ||
| | --- | --- | --- | | ||
| | `initialize` | Session | Concurrent fanout to all backends; failure of one backend is non-fatal (stored with no service). Returns merged capability set. Requires `DownstreamSessionId`, `UserConfig`, `VirtualHostId`, `ContextForgeClaims`. | | ||
| | `list_tools` | List | Fan-out all connected backends → merged sorted result. Cursor-based pagination across backends. | | ||
| | `list_resources` | List | Same as list_tools. | | ||
| | `list_prompts` | List | Same as list_tools. | | ||
| | `list_resource_templates` | List | Same — both name and URI template get prefixed for multi-backend. | | ||
| | `call_tool` | Targeted | **Per-request backend lifecycle:** creates fresh connection via `connect_backend_for_request`, runs pre-hook, executes call, runs post-hook, closes connection. Resolves alias → single/multi-backend fallback. Forwards downstream cancellation to backend. Tracks backend progress tokens: RMCP assigns a new token per backend request; the gateway maps each backend token to the downstream token. Request enqueue and mapping publication are serialized against progress lookup so an immediate backend notification cannot overtake registration. When the notification matches an in-flight token, the gateway restores the downstream token and forwards it to the client. Does not use session-backed `BackendTransports`. | | ||
| | `read_resource` | Targeted | Single-backend: URI unchanged. Multi-backend: strips prefix. | | ||
| | `subscribe` / `unsubscribe` | Targeted | Same resource-URI routing; forwards/stops resource-update notifications. | | ||
| | `get_prompt` | Targeted | Single-backend: name unchanged. Multi-backend: strips prefix. Runs pre/post prompt hooks around the backend call: the pre hook may rewrite arguments or deny, the post hook may rewrite or reject the rendered messages. | | ||
| | `complete` | Targeted | Routes on prompt name or resource URI inside `ref`. | | ||
| | `ping` | Local | Returns success; no backend fanout. | | ||
| | `DELETE` | Session | RMCP handles first; on success `session_id_layer` removes local session + backend transports. | | ||
| `call_tool` and `get_prompt` run `before_*/after_*` hooks when a `GatewayPluginRuntimeHandle` is configured. Pre-hook may rewrite arguments or deny; post-hook may rewrite or reject the response. Pre-hook state is passed to the post-hook. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.