Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions _context/wiki/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,16 @@ flowchart TD

RMCP enforces its configured request-body cap and validates modern standard
headers before dispatch. The `tools/call` handler then resolves the request's
backend and original tool name and validates `Mcp-Param-*` from the request
context against the schema published in `UserConfig`; it does not call backend
`tools/list`.
Validated headers are forwarded unchanged; request plugins run afterward, so a
plugin that changes an annotated argument also owns the resulting upstream
backend and original tool name. When `UserConfig` contains that tool's input
schema, it validates recognized `Mcp-Param-*` headers against the request body;
it does not call backend `tools/list`. Without a published schema, parameter
headers are unrecognized and forwarded without local validation.
Published annotations are validated for MCP token, uniqueness, primitive type,
and properties-only reachability constraints. Nested annotations read the exact
argument path. Present non-null values require a matching header; absent or
null values require no header.
Parameter headers are forwarded unchanged; request plugins run afterward, so a
plugin that changes an annotated argument also owns any resulting upstream
mismatch.

Order is invariant: auth/config before backend selection; request plugins before upstream; response plugins before returning.
Expand Down
13 changes: 12 additions & 1 deletion _context/wiki/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,12 +129,23 @@ BackendMCPGateway
add_headers: HashMap<String, String> ← injected after passthrough
remove_headers: Vec<String> ← stripped after add
allowed_tool_names: Vec<String> ← model exists, NOT currently enforced
tool_schemas: HashMap<String, JsonObject> ← required; upstream name → input schema
tool_schemas: HashMap<String, JsonObject> ← optional, defaults to {}; upstream name → input schema
tool_name_aliases: HashMap<String, String> ← downstream_alias → upstream_original
allowed_resource_names: Vec<String> ← model exists, NOT currently enforced
allowed_prompt_names: Vec<String> ← model exists, NOT currently enforced
```

`tool_schemas` lets the dataplane recognize and validate `x-mcp-header`
annotations without calling backend `tools/list`. The control plane may omit the
field or individual unannotated tools. Without a published schema, parameter
headers are forwarded as unrecognized intermediary headers and are not locally
validated. A published annotation must name a non-empty, case-insensitively
unique HTTP token on a `string`, `integer`, or `boolean` property reachable from
the schema root through `properties` keys only. Nested properties use their
exact property path. For a recognized annotation, a non-null argument requires
an equal header; an absent or null argument requires the header to be absent.
Integer values are limited to the IEEE 754 safe range.

**Header apply order:** `passthrough_headers` → `add_headers` (override passthrough) → `remove_headers` (applied last).

**`passthrough_headers` is session-scoped.** Values are snapshotted from the `initialize` request and baked into the backend transport for the session lifetime. Post-`initialize` calls (tool calls, list calls) reuse those headers. Request-scoped propagation requires per-request transport reconstruction (future work).
Expand Down
13 changes: 10 additions & 3 deletions _context/wiki/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,16 @@ bounded by the HTTP transport.
Backend header policy cannot add, remove, or replace MCP standard or parameter
headers. For modern `tools/call`, the dataplane resolves the authenticated
user, virtual host, backend, and original tool name before validating
`Mcp-Param-*` against the control-plane-published input schema. A missing schema
or header/body mismatch fails closed with JSON-RPC `-32020`.
Validation does not call backend `tools/list`. Validated values are forwarded
recognized `Mcp-Param-*` against the control-plane-published input schema. A
recognized missing, malformed, unexpected, conflicting repeated, or mismatched header fails closed
with JSON-RPC `-32020`. Schema annotations also fail closed unless their names
are non-empty, case-insensitively unique HTTP tokens, their properties have an
allowed primitive type, and their paths are statically reachable through
`properties` only. Nested values are checked at their exact path, and integers
must remain in the IEEE 754 safe range. When no schema is published, parameter
headers are unrecognized and forwarded without local validation; their absence
does not block the tool call.
Validation does not call backend `tools/list`. Parameter values are forwarded
unchanged, while RMCP regenerates method, routed-name, and protocol-version
headers. If a plugin later changes an annotated argument, the original header
remains and the upstream server may reject the mismatch.
Expand Down
7 changes: 7 additions & 0 deletions _context/wiki/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ Protocol-sensitive tests and fixtures must cover MCP `2026-07-28` and `2025-11-2

These run in `cargo nextest run` with no Docker dependencies.

Parameter-header integration tests verify that calls without a published tool
schema skip local `Mcp-Param-*` validation and still reach the backend. Unit and
integration coverage also includes missing, malformed, unexpected, repeated,
and mismatched recognized headers; Base64 encoding; nested paths; numerically
equivalent integers; and invalid annotation names, types, duplicates, and
non-`properties` paths.

## MCP Conformance

[`cf-integration`](https://crates.io/crates/cf-integration)
Expand Down
1 change: 1 addition & 0 deletions crates/contextforge-data-plane-apis/src/user_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ pub struct BackendMCPGateway {
#[serde(default)]
pub completion: HashMap<String, String>,
/// Input schemas keyed by the original upstream tool name.
#[serde(default)]
pub tool_schemas: HashMap<String, serde_json::Map<String, serde_json::Value>>,
}

Expand Down
15 changes: 15 additions & 0 deletions crates/contextforge-data-plane-apis/tests/user_store.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use contextforge_data_plane_apis::user_store::BackendMCPGateway;
use serde_json::json;

#[test]
fn backend_config_without_tool_schemas_defaults_to_empty_map() {
let config: BackendMCPGateway = serde_json::from_value(json!({
"name": "backend",
"url": "http://localhost:8000/mcp",
"mcp_protocol_version": "2026_07_28",
"passthrough_headers": []
}))
.expect("backend config without tool schemas should deserialize");

assert!(config.tool_schemas.is_empty());
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,14 @@ pub(super) async fn call_tool(
data: None,
})?;

if cx.protocol_version().is_some_and(|version| version >= ProtocolVersion::STANDARD_HEADERS) {
if cx.protocol_version().is_some_and(|version| version >= ProtocolVersion::STANDARD_HEADERS)
&& let Some(tool_schema) = backend.tool_schemas.get(&tool_name)
{
let downstream_headers = cx
.extensions
.get::<Parts>()
.map(|parts| &parts.headers)
.ok_or_else(|| ErrorData::internal_error("Routing problem... request headers not found", None))?;
let tool_schema = backend.tool_schemas.get(&tool_name).ok_or_else(|| {
ErrorData::header_mismatch(format!("Missing published schema for tool '{tool_name}'"), None)
})?;
mcp_standard_headers::validate_tool_params(downstream_headers, request.arguments.as_ref(), tool_schema)
.map_err(|message| ErrorData::header_mismatch(message, None))?;
}
Expand Down
Loading