diff --git a/CHANGELOG.md b/CHANGELOG.md index f747adeae..188dde1b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Changed +- Active PR #272 hardens the MCP `2026-07-28` `tools/call` adapter so a validated call requires matching transport/request protocol versions and per-request client-capabilities presence; optional self-reported `clientInfo` is neither required nor retained as authority, and the former constructor shape now fails closed for otherwise valid legacy calls. This is active-PR evidence, not protected-main shipment. - Aligned the hourly product-development branch-coverage toolchain and its one-shot materializer with the reviewed `nightly-2026-08-18` pin, and corrected the official Dependabot Rust-toolchain reference. - Separated logical origin authority from resolved network destination authority; an origin grant no longer implies permission to connect to every resolver result. - Separated resolved-address authorization from direct transport evidence; an approved IP now becomes a usable stream only after the operating system reports the exact requested IP and port. @@ -102,4 +103,4 @@ All notable changes to OriginWeave are documented in this file. The format follo - The hourly product agent has no Git metadata or repository authority. A separate post-verification publisher opens one PR and cannot approve or merge it. - The unprivileged OpenCode user is restricted to loopback egress during model execution, preventing runner-wide allow-listed endpoints from becoming direct source-exfiltration channels. -[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD \ No newline at end of file +[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD diff --git a/Cargo.lock b/Cargo.lock index 848cb7320..284c2b41a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -288,6 +288,14 @@ dependencies = [ "originweave-core", ] +[[package]] +name = "originweave-mcp" +version = "0.1.0" +dependencies = [ + "originweave-core", + "originweave-policy", +] + [[package]] name = "originweave-network" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 0d5ab469c..eb6d6613b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "crates/originweave-core", "crates/originweave-bap", + "crates/originweave-mcp", "crates/originweave-policy", "crates/originweave-resource", "crates/originweave-evidence", diff --git a/crates/originweave-core/src/root.rs b/crates/originweave-core/src/root.rs index c47a136d4..e39f9a598 100644 --- a/crates/originweave-core/src/root.rs +++ b/crates/originweave-core/src/root.rs @@ -11,7 +11,5 @@ mod contracts; pub use contracts::*; -/// Stateless MCP routing validation that maps only explicit tools to typed actions. -pub mod mcp; /// Deterministic fail-closed release benchmark acceptance aggregation. pub mod release_acceptance; diff --git a/crates/originweave-mcp/Cargo.toml b/crates/originweave-mcp/Cargo.toml new file mode 100644 index 000000000..72aeed352 --- /dev/null +++ b/crates/originweave-mcp/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "originweave-mcp" +description = "OriginWeave MCP adapter contracts." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +publish = false + +[dependencies] +originweave-core = { path = "../originweave-core" } +originweave-policy = { path = "../originweave-policy" } + +[lints] +workspace = true diff --git a/crates/originweave-mcp/src/lib.rs b/crates/originweave-mcp/src/lib.rs new file mode 100644 index 000000000..9a3c8008b --- /dev/null +++ b/crates/originweave-mcp/src/lib.rs @@ -0,0 +1,92 @@ +//! Fail-closed MCP adapter contracts for OriginWeave. +//! +//! This crate owns MCP protocol-generation, discovery, and stateless tool-routing +//! contracts. It maps reviewed MCP protocol values into existing OriginWeave +//! action contracts but grants no policy, browser, network, secret, or evidence +//! authority. + +#![forbid(unsafe_code)] +#![deny(missing_docs)] + +use originweave_core::{ActionRequest, PolicyContext}; +use originweave_policy::Decision; + +pub(crate) use originweave_core::{ActionKind, Capability, RiskClass}; + +mod request; +mod routing; + +pub use request::{McpToolBoundaryError, ValidatedMcpToolCall}; +pub use routing::{ + MAX_MCP_METHOD_NAME_BYTES, MAX_MCP_TOOL_NAME_BYTES, MCP_PROTOCOL_VERSION, + MCP_TOOLS_CALL_METHOD, MCP_TOOLS_LIST_METHOD, McpCacheScope, McpResultType, + McpToolCatalogEntry, McpToolsListBoundaryError, McpToolsListPage, ValidatedMcpToolsListRequest, + mcp_tools_list_page, supported_mcp_tools, +}; + +/// A fail-closed rejection owned by the MCP routing boundary rather than policy. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum McpRouteRejection { + /// The validated MCP route resolves to a different action than the typed request. + ActionMismatch, +} + +/// Evaluate one validated MCP route through the ordinary OriginWeave policy boundary. +/// +/// Route validation proves only protocol integrity. It grants no capability, origin, approval, +/// secret, browser, network, or evidence authority. A route/action mismatch is returned as an +/// MCP-owned rejection before the request reaches policy. Callers may execute only +/// `Ok(Decision::Allow)`; every other result remains non-authorizing. +pub fn evaluate_mcp( + call: &ValidatedMcpToolCall, + request: &ActionRequest, + context: &PolicyContext, +) -> Result { + if call.action_kind() != request.action() { + return Err(McpRouteRejection::ActionMismatch); + } + + Ok(originweave_policy::evaluate(request, context)) +} + +#[cfg(test)] +mod tests { + use std::error::Error; + + use super::routing::McpToolBoundaryError; + + #[test] + fn private_routing_error_diagnostics_are_total_and_source_free() { + let cases = [ + ( + McpToolBoundaryError::UnsupportedProtocolVersion, + "unsupported MCP protocol version", + ), + ( + McpToolBoundaryError::HeaderBodyMismatch, + "MCP routing headers do not match the request body", + ), + ( + McpToolBoundaryError::InvalidMethod, + "MCP method violates the bounded ASCII routing syntax", + ), + ( + McpToolBoundaryError::UnsupportedMethod, + "only MCP tools/call requests can enter the typed action boundary", + ), + ( + McpToolBoundaryError::InvalidToolName, + "MCP tool name violates the bounded ASCII routing syntax", + ), + ( + McpToolBoundaryError::UnknownTool, + "MCP tool is not mapped to an OriginWeave typed action", + ), + ]; + + for (error, message) in cases { + assert_eq!(error.to_string(), message); + assert!(error.source().is_none()); + } + } +} diff --git a/crates/originweave-mcp/src/request.rs b/crates/originweave-mcp/src/request.rs new file mode 100644 index 000000000..78dc4cf4c --- /dev/null +++ b/crates/originweave-mcp/src/request.rs @@ -0,0 +1,239 @@ +//! MCP 2026-07-28 request-envelope validation for typed tool calls. +//! +//! This adapter layer binds transport protocol metadata to the existing bounded +//! tool-routing validator. It deliberately retains no client identity or +//! capability contents and grants no OriginWeave browser, policy, secret, or +//! evidence authority. + +use std::fmt; + +use crate::{ActionKind, MCP_PROTOCOL_VERSION, routing}; + +/// A deterministic failure while validating one MCP `tools/call` request. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum McpToolBoundaryError { + /// The transport request omitted the required MCP protocol-version header. + MissingProtocolVersionHeader, + /// The structured request metadata omitted the required MCP protocol version. + MissingProtocolVersionMetadata, + /// The transport protocol version disagrees with the structured request metadata. + ProtocolVersionHeaderBodyMismatch, + /// The request names an MCP protocol generation this adapter does not support. + UnsupportedProtocolVersion, + /// The structured request metadata omitted the required client-capabilities object. + MissingClientCapabilities, + /// MCP routing metadata disagrees with the method or tool name in the body. + HeaderBodyMismatch, + /// The request method violates the bounded ASCII MCP routing syntax. + InvalidMethod, + /// The request method is not the supported `tools/call` operation. + UnsupportedMethod, + /// The tool name violates the bounded ASCII MCP routing syntax. + InvalidToolName, + /// The tool name has no explicit mapping to an OriginWeave typed action. + UnknownTool, +} + +impl fmt::Display for McpToolBoundaryError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::MissingProtocolVersionHeader => { + formatter.write_str("MCP protocol version header is required") + } + Self::MissingProtocolVersionMetadata => { + formatter.write_str("MCP request metadata protocol version is required") + } + Self::ProtocolVersionHeaderBodyMismatch => { + formatter.write_str("MCP protocol version header does not match request metadata") + } + Self::UnsupportedProtocolVersion => { + formatter.write_str("unsupported MCP protocol version") + } + Self::MissingClientCapabilities => { + formatter.write_str("MCP request metadata client capabilities are required") + } + Self::HeaderBodyMismatch => { + formatter.write_str("MCP routing headers do not match the request body") + } + Self::InvalidMethod => { + formatter.write_str("MCP method violates the bounded ASCII routing syntax") + } + Self::UnsupportedMethod => formatter + .write_str("only MCP tools/call requests can enter the typed action boundary"), + Self::InvalidToolName => { + formatter.write_str("MCP tool name violates the bounded ASCII routing syntax") + } + Self::UnknownTool => { + formatter.write_str("MCP tool is not mapped to an OriginWeave typed action") + } + } + } +} + +impl std::error::Error for McpToolBoundaryError {} + +impl From for McpToolBoundaryError { + fn from(error: routing::McpToolBoundaryError) -> Self { + match error { + routing::McpToolBoundaryError::UnsupportedProtocolVersion => { + Self::UnsupportedProtocolVersion + } + routing::McpToolBoundaryError::HeaderBodyMismatch => Self::HeaderBodyMismatch, + routing::McpToolBoundaryError::InvalidMethod => Self::InvalidMethod, + routing::McpToolBoundaryError::UnsupportedMethod => Self::UnsupportedMethod, + routing::McpToolBoundaryError::InvalidToolName => Self::InvalidToolName, + routing::McpToolBoundaryError::UnknownTool => Self::UnknownTool, + } + } +} + +/// An MCP tool call whose required request metadata and routing envelope were validated. +/// +/// The value proves protocol-envelope integrity only. Client capability contents and optional +/// `clientInfo` are deliberately not retained because self-reported client metadata is not an +/// OriginWeave authorization signal. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ValidatedMcpToolCall { + routed: routing::ValidatedMcpToolCall, +} + +impl ValidatedMcpToolCall { + /// Fail closed for the pre-2026-07-28 constructor shape. + /// + /// This compatibility surface preserves deterministic routing diagnostics for malformed + /// legacy callers, but a syntactically valid route is rejected because this signature cannot + /// prove the required per-request protocol metadata or client-capabilities presence. New + /// adapters must use [`Self::new_with_request_metadata`]. + pub fn new( + protocol_version: &str, + routing_method: &str, + routing_tool_name: &str, + body_method: &str, + body_tool_name: &str, + ) -> Result { + let _ = routing::ValidatedMcpToolCall::new( + protocol_version, + routing_method, + routing_tool_name, + body_method, + body_tool_name, + ) + .map_err(McpToolBoundaryError::from)?; + + Err(McpToolBoundaryError::MissingProtocolVersionMetadata) + } + + /// Validate one MCP 2026-07-28 `tools/call` request envelope. + /// + /// The transport protocol-version header and structured request `_meta` protocol version are + /// both mandatory, are bounded before comparison, must agree exactly, and must equal + /// [`MCP_PROTOCOL_VERSION`]. A trusted structured parser must also attest that the request's + /// client-capabilities object was present. Capability contents and optional `clientInfo` grant + /// no OriginWeave authority and are not retained. After metadata validation, the existing + /// bounded method/tool validator performs the explicit tool-to-action mapping. + pub fn new_with_request_metadata( + protocol_version_header: Option<&str>, + protocol_version_metadata: Option<&str>, + client_capabilities_present: bool, + routing_method: &str, + routing_tool_name: &str, + body_method: &str, + body_tool_name: &str, + ) -> Result { + let protocol_version_header = + protocol_version_header.ok_or(McpToolBoundaryError::MissingProtocolVersionHeader)?; + let protocol_version_metadata = protocol_version_metadata + .ok_or(McpToolBoundaryError::MissingProtocolVersionMetadata)?; + + if protocol_version_header.len() > MCP_PROTOCOL_VERSION.len() + || protocol_version_metadata.len() > MCP_PROTOCOL_VERSION.len() + { + return Err(McpToolBoundaryError::UnsupportedProtocolVersion); + } + if protocol_version_header != protocol_version_metadata { + return Err(McpToolBoundaryError::ProtocolVersionHeaderBodyMismatch); + } + if protocol_version_metadata != MCP_PROTOCOL_VERSION { + return Err(McpToolBoundaryError::UnsupportedProtocolVersion); + } + if !client_capabilities_present { + return Err(McpToolBoundaryError::MissingClientCapabilities); + } + + let routed = routing::ValidatedMcpToolCall::new( + protocol_version_metadata, + routing_method, + routing_tool_name, + body_method, + body_tool_name, + ) + .map_err(McpToolBoundaryError::from)?; + + Ok(Self { routed }) + } + + /// Validate one MCP 2026-07-28 stdio `tools/call` request envelope. + /// + /// Stdio has no HTTP routing headers, so callers provide only request-body protocol metadata, + /// capability presence, method, and tool name. The body values are correlated with themselves + /// inside the existing pure envelope validator only to reuse its bounds and catalog checks; no + /// HTTP header value is accepted, retained, or surfaced as evidence by this constructor. + pub fn new_for_stdio( + protocol_version_metadata: Option<&str>, + client_capabilities_present: bool, + body_method: &str, + body_tool_name: &str, + ) -> Result { + let protocol_version_metadata = protocol_version_metadata + .ok_or(McpToolBoundaryError::MissingProtocolVersionMetadata)?; + + Self::new_with_request_metadata( + Some(protocol_version_metadata), + Some(protocol_version_metadata), + client_capabilities_present, + body_method, + body_tool_name, + body_method, + body_tool_name, + ) + } + + /// Return the canonical static tool name selected by the explicit mapping. + #[must_use] + pub const fn tool_name(&self) -> &'static str { + self.routed.tool_name() + } + + /// Return the existing OriginWeave typed action selected by this tool. + #[must_use] + pub const fn action_kind(&self) -> ActionKind { + self.routed.action_kind() + } +} + +impl routing::ValidatedMcpToolsListRequest { + /// Validate one MCP 2026-07-28 stdio `tools/list` request envelope. + /// + /// Stdio carries the protocol metadata and method in the JSON-RPC request body and has no HTTP + /// routing headers. The body method/version are correlated with themselves inside the existing + /// pure list validator only to reuse its bounded syntax, cache, and cursor checks; callers cannot + /// supply or obtain fabricated HTTP header evidence through this constructor. + pub fn new_for_stdio( + protocol_version_metadata: Option<&str>, + client_capabilities_present: bool, + body_method: &str, + cursor: Option<&str>, + ) -> Result { + let protocol_version_metadata = protocol_version_metadata + .ok_or(routing::McpToolsListBoundaryError::MissingProtocolVersionMetadata)?; + + Self::new( + Some(protocol_version_metadata), + Some(protocol_version_metadata), + client_capabilities_present, + body_method, + body_method, + cursor, + ) + } +} diff --git a/crates/originweave-core/src/mcp.rs b/crates/originweave-mcp/src/routing.rs similarity index 100% rename from crates/originweave-core/src/mcp.rs rename to crates/originweave-mcp/src/routing.rs diff --git a/crates/originweave-core/tests/mcp_authority_route.rs b/crates/originweave-mcp/tests/mcp_authority_route.rs similarity index 76% rename from crates/originweave-core/tests/mcp_authority_route.rs rename to crates/originweave-mcp/tests/mcp_authority_route.rs index 80357ec63..24d7623fe 100644 --- a/crates/originweave-core/tests/mcp_authority_route.rs +++ b/crates/originweave-mcp/tests/mcp_authority_route.rs @@ -1,14 +1,16 @@ use std::error::Error; -use originweave_core::mcp::{ +use originweave_core::{ActionKind, Capability, RiskClass}; +use originweave_mcp::{ MAX_MCP_METHOD_NAME_BYTES, MAX_MCP_TOOL_NAME_BYTES, MCP_PROTOCOL_VERSION, MCP_TOOLS_CALL_METHOD, McpToolBoundaryError, ValidatedMcpToolCall, supported_mcp_tools, }; -use originweave_core::{ActionKind, Capability, RiskClass}; fn validate(tool_name: &str) -> Result { - ValidatedMcpToolCall::new( - MCP_PROTOCOL_VERSION, + ValidatedMcpToolCall::new_with_request_metadata( + Some(MCP_PROTOCOL_VERSION), + Some(MCP_PROTOCOL_VERSION), + true, MCP_TOOLS_CALL_METHOD, tool_name, MCP_TOOLS_CALL_METHOD, @@ -16,6 +18,23 @@ fn validate(tool_name: &str) -> Result Result { + ValidatedMcpToolCall::new_with_request_metadata( + Some(MCP_PROTOCOL_VERSION), + Some(MCP_PROTOCOL_VERSION), + true, + routing_method, + routing_tool_name, + body_method, + body_tool_name, + ) +} + #[test] fn supported_mcp_tools_map_to_exact_originweave_actions() -> Result<(), Box> { let cases = [ @@ -130,9 +149,10 @@ fn mcp_tool_catalog_is_deterministic_complete_and_action_unambiguous() -> Result expected_action.required_capability() ); assert_eq!(entry.risk_class(), expected_action.risk_class()); - - let call = validate(entry.tool_name())?; - assert_eq!(call.action_kind(), entry.action_kind()); + assert_eq!( + validate(entry.tool_name())?.action_kind(), + entry.action_kind() + ); } for (index, entry) in catalog.iter().enumerate() { @@ -150,20 +170,9 @@ fn mcp_tool_catalog_is_deterministic_complete_and_action_unambiguous() -> Result } #[test] -fn mcp_route_rejects_protocol_header_body_and_method_drift() { +fn modern_route_rejects_header_body_and_method_drift() { assert_eq!( - ValidatedMcpToolCall::new( - "2025-11-25", - MCP_TOOLS_CALL_METHOD, - "originweave.observe", - MCP_TOOLS_CALL_METHOD, - "originweave.observe", - ), - Err(McpToolBoundaryError::UnsupportedProtocolVersion) - ); - assert_eq!( - ValidatedMcpToolCall::new( - MCP_PROTOCOL_VERSION, + modern_route( MCP_TOOLS_CALL_METHOD, "originweave.observe", "tools/list", @@ -172,8 +181,7 @@ fn mcp_route_rejects_protocol_header_body_and_method_drift() { Err(McpToolBoundaryError::HeaderBodyMismatch) ); assert_eq!( - ValidatedMcpToolCall::new( - MCP_PROTOCOL_VERSION, + modern_route( MCP_TOOLS_CALL_METHOD, "originweave.observe", MCP_TOOLS_CALL_METHOD, @@ -182,8 +190,7 @@ fn mcp_route_rejects_protocol_header_body_and_method_drift() { Err(McpToolBoundaryError::HeaderBodyMismatch) ); assert_eq!( - ValidatedMcpToolCall::new( - MCP_PROTOCOL_VERSION, + modern_route( "resources/read", "originweave.observe", "resources/read", @@ -194,64 +201,31 @@ fn mcp_route_rejects_protocol_header_body_and_method_drift() { } #[test] -fn mcp_route_bounds_each_untrusted_method_before_cross_field_comparison() { +fn modern_route_bounds_each_untrusted_method_before_cross_field_comparison() { let at_limit = "x".repeat(MAX_MCP_METHOD_NAME_BYTES); let oversized_routing = "r".repeat(MAX_MCP_METHOD_NAME_BYTES + 1); let oversized_body = "b".repeat(MAX_MCP_METHOD_NAME_BYTES + 1); + for (routing_method, body_method) in [ + ("", MCP_TOOLS_CALL_METHOD), + (MCP_TOOLS_CALL_METHOD, ""), + (&oversized_routing, MCP_TOOLS_CALL_METHOD), + (MCP_TOOLS_CALL_METHOD, &oversized_body), + ("tools call", "tools call"), + ] { + assert_eq!( + modern_route( + routing_method, + "originweave.observe", + body_method, + "originweave.observe", + ), + Err(McpToolBoundaryError::InvalidMethod) + ); + } + assert_eq!( - ValidatedMcpToolCall::new( - MCP_PROTOCOL_VERSION, - "", - "originweave.observe", - MCP_TOOLS_CALL_METHOD, - "originweave.observe", - ), - Err(McpToolBoundaryError::InvalidMethod) - ); - assert_eq!( - ValidatedMcpToolCall::new( - MCP_PROTOCOL_VERSION, - MCP_TOOLS_CALL_METHOD, - "originweave.observe", - "", - "originweave.observe", - ), - Err(McpToolBoundaryError::InvalidMethod) - ); - assert_eq!( - ValidatedMcpToolCall::new( - MCP_PROTOCOL_VERSION, - &oversized_routing, - "originweave.observe", - MCP_TOOLS_CALL_METHOD, - "originweave.observe", - ), - Err(McpToolBoundaryError::InvalidMethod) - ); - assert_eq!( - ValidatedMcpToolCall::new( - MCP_PROTOCOL_VERSION, - MCP_TOOLS_CALL_METHOD, - "originweave.observe", - &oversized_body, - "originweave.observe", - ), - Err(McpToolBoundaryError::InvalidMethod) - ); - assert_eq!( - ValidatedMcpToolCall::new( - MCP_PROTOCOL_VERSION, - "tools call", - "originweave.observe", - "tools call", - "originweave.observe", - ), - Err(McpToolBoundaryError::InvalidMethod) - ); - assert_eq!( - ValidatedMcpToolCall::new( - MCP_PROTOCOL_VERSION, + modern_route( &at_limit, "originweave.observe", &at_limit, @@ -262,9 +236,10 @@ fn mcp_route_bounds_each_untrusted_method_before_cross_field_comparison() { } #[test] -fn mcp_route_rejects_unbounded_malformed_and_unmapped_tool_names() { +fn modern_route_rejects_unbounded_malformed_and_unmapped_tool_names() { let at_limit = "x".repeat(MAX_MCP_TOOL_NAME_BYTES); let oversized = "x".repeat(MAX_MCP_TOOL_NAME_BYTES + 1); + for tool_name in [ "", "originweave legal", @@ -287,22 +262,59 @@ fn mcp_route_rejects_unbounded_malformed_and_unmapped_tool_names() { validate("third_party.arbitrary_javascript"), Err(McpToolBoundaryError::UnknownTool) ); -} -#[test] -fn mcp_route_validates_each_untrusted_tool_name_before_cross_field_comparison() { let oversized_routing = "r".repeat(MAX_MCP_TOOL_NAME_BYTES + 1); let oversized_body = "b".repeat(MAX_MCP_TOOL_NAME_BYTES + 1); + assert_eq!( + modern_route( + MCP_TOOLS_CALL_METHOD, + &oversized_routing, + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + ), + Err(McpToolBoundaryError::InvalidToolName) + ); + assert_eq!( + modern_route( + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + MCP_TOOLS_CALL_METHOD, + &oversized_body, + ), + Err(McpToolBoundaryError::InvalidToolName) + ); + assert_eq!( + modern_route( + MCP_TOOLS_CALL_METHOD, + "originweave/observe", + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + ), + Err(McpToolBoundaryError::InvalidToolName) + ); +} +#[test] +fn legacy_constructor_preserves_routing_diagnostics_but_never_admits_valid_calls() { assert_eq!( ValidatedMcpToolCall::new( MCP_PROTOCOL_VERSION, MCP_TOOLS_CALL_METHOD, - &oversized_routing, + "originweave.observe", MCP_TOOLS_CALL_METHOD, "originweave.observe", ), - Err(McpToolBoundaryError::InvalidToolName) + Err(McpToolBoundaryError::MissingProtocolVersionMetadata) + ); + assert_eq!( + ValidatedMcpToolCall::new( + "2025-11-25", + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + ), + Err(McpToolBoundaryError::UnsupportedProtocolVersion) ); assert_eq!( ValidatedMcpToolCall::new( @@ -310,9 +322,29 @@ fn mcp_route_validates_each_untrusted_tool_name_before_cross_field_comparison() MCP_TOOLS_CALL_METHOD, "originweave.observe", MCP_TOOLS_CALL_METHOD, - &oversized_body, + "originweave.extract", ), - Err(McpToolBoundaryError::InvalidToolName) + Err(McpToolBoundaryError::HeaderBodyMismatch) + ); + assert_eq!( + ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + "tools call", + "originweave.observe", + "tools call", + "originweave.observe", + ), + Err(McpToolBoundaryError::InvalidMethod) + ); + assert_eq!( + ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + "resources/read", + "originweave.observe", + "resources/read", + "originweave.observe", + ), + Err(McpToolBoundaryError::UnsupportedMethod) ); assert_eq!( ValidatedMcpToolCall::new( @@ -320,19 +352,45 @@ fn mcp_route_validates_each_untrusted_tool_name_before_cross_field_comparison() MCP_TOOLS_CALL_METHOD, "originweave/observe", MCP_TOOLS_CALL_METHOD, - "originweave.observe", + "originweave/observe", ), Err(McpToolBoundaryError::InvalidToolName) ); + assert_eq!( + ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + MCP_TOOLS_CALL_METHOD, + "originweave.unknown", + MCP_TOOLS_CALL_METHOD, + "originweave.unknown", + ), + Err(McpToolBoundaryError::UnknownTool) + ); } #[test] fn mcp_boundary_errors_are_deterministic_and_do_not_echo_untrusted_values() { let cases = [ + ( + McpToolBoundaryError::MissingProtocolVersionHeader, + "MCP protocol version header is required", + ), + ( + McpToolBoundaryError::MissingProtocolVersionMetadata, + "MCP request metadata protocol version is required", + ), + ( + McpToolBoundaryError::ProtocolVersionHeaderBodyMismatch, + "MCP protocol version header does not match request metadata", + ), ( McpToolBoundaryError::UnsupportedProtocolVersion, "unsupported MCP protocol version", ), + ( + McpToolBoundaryError::MissingClientCapabilities, + "MCP request metadata client capabilities are required", + ), ( McpToolBoundaryError::HeaderBodyMismatch, "MCP routing headers do not match the request body", diff --git a/crates/originweave-mcp/tests/mcp_modern_request_metadata.rs b/crates/originweave-mcp/tests/mcp_modern_request_metadata.rs new file mode 100644 index 000000000..d551bf782 --- /dev/null +++ b/crates/originweave-mcp/tests/mcp_modern_request_metadata.rs @@ -0,0 +1,85 @@ +use originweave_mcp::{ + MCP_PROTOCOL_VERSION, MCP_TOOLS_CALL_METHOD, McpToolBoundaryError, ValidatedMcpToolCall, +}; + +fn modern_call( + protocol_version_header: Option<&str>, + protocol_version_metadata: Option<&str>, + client_capabilities_present: bool, +) -> Result { + ValidatedMcpToolCall::new_with_request_metadata( + protocol_version_header, + protocol_version_metadata, + client_capabilities_present, + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + ) +} + +#[test] +fn legacy_tools_call_shape_fails_closed_without_request_metadata() { + assert_eq!( + ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + ), + Err(McpToolBoundaryError::MissingProtocolVersionMetadata) + ); +} + +#[test] +fn modern_tools_call_requires_both_protocol_version_surfaces() { + assert_eq!( + modern_call(None, Some(MCP_PROTOCOL_VERSION), true), + Err(McpToolBoundaryError::MissingProtocolVersionHeader) + ); + assert_eq!( + modern_call(Some(MCP_PROTOCOL_VERSION), None, true), + Err(McpToolBoundaryError::MissingProtocolVersionMetadata) + ); +} + +#[test] +fn modern_tools_call_bounds_and_cross_checks_protocol_versions() { + let oversized = format!("{MCP_PROTOCOL_VERSION}x"); + + assert_eq!( + modern_call(Some(&oversized), Some(MCP_PROTOCOL_VERSION), true), + Err(McpToolBoundaryError::UnsupportedProtocolVersion) + ); + assert_eq!( + modern_call(Some(MCP_PROTOCOL_VERSION), Some(&oversized), true), + Err(McpToolBoundaryError::UnsupportedProtocolVersion) + ); + assert_eq!( + modern_call(Some(MCP_PROTOCOL_VERSION), Some("2025-11-25"), true), + Err(McpToolBoundaryError::ProtocolVersionHeaderBodyMismatch) + ); + assert_eq!( + modern_call(Some("2025-11-25"), Some("2025-11-25"), true), + Err(McpToolBoundaryError::UnsupportedProtocolVersion) + ); +} + +#[test] +fn modern_tools_call_requires_per_request_client_capabilities() { + assert_eq!( + modern_call( + Some(MCP_PROTOCOL_VERSION), + Some(MCP_PROTOCOL_VERSION), + false, + ), + Err(McpToolBoundaryError::MissingClientCapabilities) + ); + + let call = modern_call(Some(MCP_PROTOCOL_VERSION), Some(MCP_PROTOCOL_VERSION), true); + assert_eq!( + call.as_ref().map(ValidatedMcpToolCall::tool_name), + Ok("originweave.observe") + ); +} diff --git a/crates/originweave-mcp/tests/mcp_stdio_transport.rs b/crates/originweave-mcp/tests/mcp_stdio_transport.rs new file mode 100644 index 000000000..3d7dd5a10 --- /dev/null +++ b/crates/originweave-mcp/tests/mcp_stdio_transport.rs @@ -0,0 +1,73 @@ +use originweave_mcp::{ + MCP_PROTOCOL_VERSION, MCP_TOOLS_CALL_METHOD, MCP_TOOLS_LIST_METHOD, McpToolBoundaryError, + McpToolsListBoundaryError, ValidatedMcpToolCall, ValidatedMcpToolsListRequest, +}; + +#[test] +fn modern_stdio_tools_call_admits_body_metadata_without_http_headers() { + let call = ValidatedMcpToolCall::new_for_stdio( + Some(MCP_PROTOCOL_VERSION), + true, + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + ); + + assert_eq!( + call.as_ref().map(ValidatedMcpToolCall::tool_name), + Ok("originweave.observe") + ); +} + +#[test] +fn modern_stdio_tools_call_still_requires_body_protocol_metadata_and_capabilities() { + assert_eq!( + ValidatedMcpToolCall::new_for_stdio( + None, + true, + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + ), + Err(McpToolBoundaryError::MissingProtocolVersionMetadata) + ); + assert_eq!( + ValidatedMcpToolCall::new_for_stdio( + Some(MCP_PROTOCOL_VERSION), + false, + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + ), + Err(McpToolBoundaryError::MissingClientCapabilities) + ); +} + +#[test] +fn modern_stdio_tools_list_admits_body_metadata_without_http_headers() { + let request = ValidatedMcpToolsListRequest::new_for_stdio( + Some(MCP_PROTOCOL_VERSION), + true, + MCP_TOOLS_LIST_METHOD, + None, + ); + + assert_eq!( + request.as_ref().map(ValidatedMcpToolsListRequest::method), + Ok(MCP_TOOLS_LIST_METHOD) + ); +} + +#[test] +fn modern_stdio_tools_list_still_requires_body_protocol_metadata_and_capabilities() { + assert_eq!( + ValidatedMcpToolsListRequest::new_for_stdio(None, true, MCP_TOOLS_LIST_METHOD, None,), + Err(McpToolsListBoundaryError::MissingProtocolVersionMetadata) + ); + assert_eq!( + ValidatedMcpToolsListRequest::new_for_stdio( + Some(MCP_PROTOCOL_VERSION), + false, + MCP_TOOLS_LIST_METHOD, + None, + ), + Err(McpToolsListBoundaryError::MissingClientCapabilities) + ); +} diff --git a/crates/originweave-core/tests/mcp_tools_list_cache.rs b/crates/originweave-mcp/tests/mcp_tools_list_cache.rs similarity index 99% rename from crates/originweave-core/tests/mcp_tools_list_cache.rs rename to crates/originweave-mcp/tests/mcp_tools_list_cache.rs index 9d3681673..0d9b903e2 100644 --- a/crates/originweave-core/tests/mcp_tools_list_cache.rs +++ b/crates/originweave-mcp/tests/mcp_tools_list_cache.rs @@ -1,6 +1,6 @@ use std::error::Error; -use originweave_core::mcp::{ +use originweave_mcp::{ MAX_MCP_METHOD_NAME_BYTES, MCP_PROTOCOL_VERSION, MCP_TOOLS_LIST_METHOD, McpCacheScope, McpResultType, McpToolsListBoundaryError, ValidatedMcpToolsListRequest, mcp_tools_list_page, supported_mcp_tools, diff --git a/crates/originweave-policy/tests/mcp_route_binding.rs b/crates/originweave-mcp/tests/policy_route_binding.rs similarity index 80% rename from crates/originweave-policy/tests/mcp_route_binding.rs rename to crates/originweave-mcp/tests/policy_route_binding.rs index 8e9661af6..3c5a594ae 100644 --- a/crates/originweave-policy/tests/mcp_route_binding.rs +++ b/crates/originweave-mcp/tests/policy_route_binding.rs @@ -2,12 +2,15 @@ use std::collections::BTreeSet; -use originweave_core::mcp::{MCP_PROTOCOL_VERSION, MCP_TOOLS_CALL_METHOD, ValidatedMcpToolCall}; use originweave_core::{ ActionIntentDigest, ActionKind, ActionRequest, ApprovalEvidence, Capability, ExecutionPurpose, InstructionSource, Origin, PolicyContext, RobotsDecision, SecretDelivery, SessionMode, }; -use originweave_policy::{Decision, DenialReason, evaluate_mcp}; +use originweave_mcp::{ + MCP_PROTOCOL_VERSION, MCP_TOOLS_CALL_METHOD, McpRouteRejection, ValidatedMcpToolCall, + evaluate_mcp, +}; +use originweave_policy::{Decision, DenialReason}; const VALID_INTENT: &str = "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; @@ -21,8 +24,10 @@ fn intent() -> ActionIntentDigest { } fn validated_call(tool_name: &str) -> ValidatedMcpToolCall { - ValidatedMcpToolCall::new( - MCP_PROTOCOL_VERSION, + ValidatedMcpToolCall::new_with_request_metadata( + Some(MCP_PROTOCOL_VERSION), + Some(MCP_PROTOCOL_VERSION), + true, MCP_TOOLS_CALL_METHOD, tool_name, MCP_TOOLS_CALL_METHOD, @@ -65,7 +70,7 @@ fn matching_mcp_route_enters_the_existing_policy_boundary() { &context(BTreeSet::from([Capability::Observe])), ); - assert_eq!(decision, Decision::Allow); + assert_eq!(decision, Ok(Decision::Allow)); } #[test] @@ -77,7 +82,7 @@ fn mismatched_mcp_route_cannot_be_reinterpreted_as_another_action() { &context(BTreeSet::from([Capability::Navigate])), ); - assert_eq!(decision, Decision::Deny(DenialReason::McpActionMismatch)); + assert_eq!(decision, Err(McpRouteRejection::ActionMismatch)); } #[test] @@ -91,6 +96,8 @@ fn matching_mcp_route_does_not_bypass_existing_policy_denials() { assert_eq!( decision, - Decision::Deny(DenialReason::MissingCapability(Capability::Navigate)) + Ok(Decision::Deny(DenialReason::MissingCapability( + Capability::Navigate + ))) ); } diff --git a/crates/originweave-policy/src/lib.rs b/crates/originweave-policy/src/lib.rs index dbfb3c16d..243ae8ce7 100644 --- a/crates/originweave-policy/src/lib.rs +++ b/crates/originweave-policy/src/lib.rs @@ -15,7 +15,6 @@ pub use sensitive_data::{ evaluate_handle_use, }; -use originweave_core::mcp::ValidatedMcpToolCall; use originweave_core::{ ActionRequest, ApprovalEvidence, ApprovalScope, Capability, ExecutionPurpose, InstructionSource, PolicyContext, RiskClass, RobotsDecision, SecretDelivery, SessionMode, @@ -41,8 +40,6 @@ pub enum DenialReason { ModePurposeMismatch, /// Page or document content attempted to become a trusted instruction. UntrustedInstructionSource, - /// The validated MCP route resolved to a different action than the policy request. - McpActionMismatch, /// The session lacks the exact capability required by the action. MissingCapability(Capability), /// The target origin is outside the session's read grant. @@ -69,23 +66,6 @@ pub enum DenialReason { ApprovalScopeMismatch, } -/// Evaluate a policy request only when it matches an already validated MCP route. -/// -/// Matching routing metadata grants no authority. Once route and request action agree, the request -/// still passes through the existing action policy unchanged. -#[must_use] -pub fn evaluate_mcp( - call: &ValidatedMcpToolCall, - request: &ActionRequest, - context: &PolicyContext, -) -> Decision { - if call.action_kind() != request.action() { - return Decision::Deny(DenialReason::McpActionMismatch); - } - - evaluate(request, context) -} - /// Evaluate a typed browser action against one explicit policy context. #[must_use] pub fn evaluate(request: &ActionRequest, context: &PolicyContext) -> Decision { diff --git a/docs/traceability/mcp-authority-route.md b/docs/traceability/mcp-authority-route.md index 94f181ed4..35c3332db 100644 --- a/docs/traceability/mcp-authority-route.md +++ b/docs/traceability/mcp-authority-route.md @@ -1,58 +1,78 @@ # MCP 2026-07-28 authority-route traceability - **`tools/call` capability maturity:** `IMPLEMENTED_ON_PROTECTED_MAIN` -- **`tools/list` capability maturity:** `IMPLEMENTED_ON_ACTIVE_PR` -- **Protected-main owning work:** merged PR #168 `feat(mcp): bind stateless tool routing to typed actions` -- **Active follow-on:** PR #170 `feat(mcp): expose conservative tools list cache contract` +- **`tools/list` capability maturity:** `IMPLEMENTED_ON_PROTECTED_MAIN` +- **Protected-main owning work:** merged PR #168 (`tools/call`) and PR #170 (`tools/list`) +- **Active architecture repair:** PR #272 (`originweave-mcp` adapter boundary) - **Complete MCP adapter status:** `PLANNED` - **Governing decision:** ADR 0107 ## Scope -Protected main at `b05d5acca82b9d916ada2c8e82f59f92a89817e1` contains the bounded Rust control-plane foundation for MCP `2026-07-28` `tools/call` routing that merged through PR #168. It validates the represented stateless routing envelope, bounds and syntax-validates both attacker-controlled method fields and both attacker-controlled tool-name fields before correlation, maps only an explicit reviewed `originweave.*` catalog to existing typed `ActionKind` values, derives discovery metadata from the same catalog, and rejects route/action mismatch before ordinary deterministic policy evaluation. Methods are nonempty reviewed-ASCII routing tokens of at most 64 bytes; tool names are nonempty reviewed-ASCII identifiers of at most 128 bytes. Invalid method metadata is rejected distinctly from a bounded but unsupported MCP method. +Protected `main@87c4daa1830bac5a5228b6036752ad5633232085` contains the bounded Rust MCP `2026-07-28` routing and discovery foundation merged through PRs #168 and #170 plus the current repository CI lifecycle authority through #286. That protected-main implementation bounds and syntax-validates attacker-controlled routing fields, maps only the explicit reviewed `originweave.*` catalog to existing typed `ActionKind` values, derives discovery metadata from that same catalog, and rejects route/action mismatch before ordinary deterministic policy evaluation. -A successful `ValidatedMcpToolCall` proves routing integrity only. It grants no capability, origin, approval, secret, browser, tenant, persistence, network, or evidence authority. `originweave_policy::evaluate_mcp` still delegates to the ordinary policy evaluator after the route/action match. +A successful MCP routing value proves protocol integrity only. It grants no capability, origin, approval, secret, browser, tenant, persistence, network, evidence, or ambient execution authority. Browser and policy authority remain in their OriginWeave bounded contexts. -Active PR #170 builds on that protected-main catalog with a conservative typed `tools/list` request/result boundary. Its current branch requires matching MCP protocol metadata, required client-capability presence, bounded and syntax-validated routing/body methods, exact `tools/list` routing, and no caller-supplied cursor because the fixed catalog issues none. Its result is one complete page with zero freshness, private cache scope, and no continuation cursor. This active-PR slice remains non-shipped until it reaches protected main and does not grant any OriginWeave action authority. +PR #272 is an active DDD repair that moves the external MCP protocol surface into `originweave-mcp` while preserving inward dependency direction: the adapter may consume stable core contracts and the protocol-independent policy API, but core and policy must not depend outward on MCP transport types. The move is active-PR evidence, not protected-main shipment. + +The current #272 generation adopts protected #286 non-destructively. Its reconciliation keeps the protected CI/MV3 workflow and lifecycle contract byte-for-byte, retains the MCP workspace membership assertion, and moves the MCP-specific dependency-direction assertions into a focused repository contract instead of overwriting the generic governance contract. The effective PR delta therefore contains no `.github/**` mutation. + +## Final 2026-07-28 per-request envelope + +The final MCP `2026-07-28` request envelope requires `io.modelcontextprotocol/protocolVersion` and `io.modelcontextprotocol/clientCapabilities` on each request. Client capabilities are per-request state and must not be inferred from earlier requests. `io.modelcontextprotocol/clientInfo` is optional/SHOULD self-reported metadata for display, logging, or debugging; OriginWeave does not use it as browser or policy authority. + +Transport binding is explicit. Streamable HTTP requires the request protocol version to agree with the `MCP-Protocol-Version` header and keeps the reviewed routing-header correlation checks. Stdio carries the JSON-RPC request body without those HTTP routing headers, so a valid stdio request must be admitted from its required body metadata rather than from fabricated HTTP evidence. + +PR #272 now exposes separate adapter entry points for those two cases. `ValidatedMcpToolCall::new_with_request_metadata` retains the HTTP header↔body checks. `ValidatedMcpToolCall::new_for_stdio` and `ValidatedMcpToolsListRequest::new_for_stdio` accept only the body protocol version, per-request capabilities-presence attestation, and body routing values. The stdio constructors reuse the same bounded syntax, catalog, and cursor validators internally, but they accept, retain, and expose no HTTP header value. Missing or unsupported protocol metadata, missing capabilities, malformed or unsupported methods, malformed or unknown tools, and unissued cursors remain fail-closed. + +The lower routing validator and reviewed tool-to-`ActionKind` catalog remain internal implementation details of `originweave-mcp`; core and policy receive no MCP request-envelope types. Policy evaluation still consumes only the typed action contract after the adapter has established protocol integrity. + +## Executable RED and repair lineage + +Test-only head `bbe6b219a33f78e3b8b1c0166a00e5c34a2ede22` introduced `crates/originweave-mcp/tests/mcp_stdio_transport.rs` before production constructors existed. Repository-native CI run `33646560232` subsequently acquired hosted runners and produced an executable RED rather than a queue-only signal: + +- Production coverage job `100302670895` failed with Rust `E0599` because `ValidatedMcpToolCall::new_for_stdio` and `ValidatedMcpToolsListRequest::new_for_stdio` did not exist. Six call sites in the stdio contract failed to compile. +- Rust contracts job `100302670660` first passed 154 Python repository-contract tests, then failed `cargo fmt --all --check`. Its canonical rustfmt artifact was `9875815906`, archive SHA-256 `bb5f01d2f6a90f22bc31a7ec34337691b982f73bf56f6064cc01ffb49c024cb6`. + +The causal source repair is commit `09ffcccfd91d478120642a4db9bda501655e4533`. It adds only binding-specific stdio constructors inside `originweave-mcp` and adopts the canonical rustfmt output for files identified by the failed Rust-contract job. It does not move MCP transport authority into core or policy and does not infer browser authorization from protocol success. + +This predecessor RED is durable evidence, but it is not current-head GREEN. Every later commit requires fresh exact-head CI, full owned-production coverage/rustdoc, security gates, and required central review workflows before promotion. ## Product-status reconciliation -`docs/PRD.md` PRD-INT-004 and `docs/TRD.md` Section 12 intentionally remain **Planned** at the complete-adapter level. That status is not contradicted by the bounded `tools/call` foundation now on protected main or by active PR #170: both are reusable control-plane contracts below the complete product adapter. `README.md` and `CHANGELOG.md` distinguish protected-main routing from the active discovery refinement, and ADR 0107 records the protocol/version and authority boundary. +`docs/PRD.md` PRD-INT-004 and the corresponding TRD complete-adapter work remain **Planned**. Protected-main routing/discovery contracts and PR #272's architecture repair are reusable control-plane slices below the complete product adapter. They do not establish a complete MCP server/runtime. -The following remain outside protected main and PR #170 and must not be inferred from either: +The following remain outside the protected-main bounded contract and must not be inferred from it: -- Streamable HTTP transport parsing and header materialization; -- JSON-RPC/HTTP response serialization of the typed discovery page; +- complete Streamable HTTP transport parsing and response serialization; +- complete stdio process/runtime framing beyond the request-envelope binding proved here; - OAuth and authenticated MCP deployment policy; -- browser-control I/O or BiDi/CDP/WebMCP translation; +- browser-control I/O or WebDriver BiDi/CDP translation; - secret materialization or broker transport; - persistence, durable audit storage, or WARC/PROV export; -- general pagination/subscription state beyond the fixed no-cursor catalog; and +- general pagination/subscription runtime beyond the currently reviewed contracts; and - an OriginWeave Protocol version transition. -## Version boundary +## Version and authority boundary -The protected-main routing foundation and active discovery refinement accept only protocol generation `2026-07-28`. MCP versioning is independent of the OriginWeave Protocol. A later MCP revision does not silently change OriginWeave action, risk, capability, approval, secret, origin, tenant, browser, or evidence semantics. +MCP versioning is independent of the OriginWeave Protocol. A later MCP revision does not silently change OriginWeave action, risk, capability, approval, secret, origin, tenant, browser, or evidence semantics. MCP routing metadata and optional client identity remain adapter data, not domain authority. -The reviewed primary source is: +## Executable evidence -Model Context Protocol. (2026, July 28). *Specification: 2026-07-28*. https://modelcontextprotocol.io/specification/2026-07-28 +Protected-main production/test surfaces currently include the deterministic `tools/call`/`tools/list` routing and discovery contracts and protocol-independent policy evaluator. Active PR #272 relocates the external-protocol implementation to `crates/originweave-mcp/` and adds modern HTTP metadata validation plus explicit stdio binding tests for `tools/call` and `tools/list`. -The canonical bibliography remains `docs/doctoring.md`. +Exact-current CI/security/review evidence must be regenerated after every branch mutation. Predecessor, protected-main, skipped, status-only, model, or cancelled evidence is not current-head proof for PR #272. -## Executable evidence +## Primary sources -Protected-main PR #168 production/test surfaces include: +Model Context Protocol. (2026, July 28). *The 2026-07-28 specification*. https://modelcontextprotocol.io/specification/2026-07-28 -- `crates/originweave-core/src/mcp.rs` — bounded deterministic catalog plus method/tool routing validation in the `ValidatedMcpToolCall` primitive; -- `crates/originweave-core/tests/mcp_authority_route.rs` — mapping, exact method/tool bounds, empty/oversized/malformed inputs, version/method/header-body correlation, and error-contract evidence; -- `crates/originweave-policy/src/lib.rs` — `evaluate_mcp` route/action guard before normal policy evaluation; and -- `crates/originweave-policy/tests/mcp_route_binding.rs` — confused-deputy and policy-preservation evidence. +Model Context Protocol. (2026). *Supporting protocol revision 2026-07-28* [TypeScript SDK migration guide]. https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/migration/support-2026-07-28.md -Active PR #170 additionally exercises its discovery contract in `crates/originweave-core/tests/mcp_tools_list_cache.rs`, including result/cache semantics, required protocol/client metadata, bounded protocol and method validation, routing correlation, cursor rejection, and public error contracts. +Model Context Protocol. (2026). *2026-07-28 protocol type definitions* [TypeScript source]. https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/core-internal/src/types/spec.types.2026-07-28.ts -Exact current-head CI/security/review evidence must be regenerated after every branch mutation. Protected-main evidence proves only the merged `tools/call` foundation; predecessor or protected-main results are not current-head proof for active PR #170. +The canonical broader bibliography remains `docs/doctoring.md`. ## Promotion rule -The bounded `tools/call` routing foundation is already `IMPLEMENTED_ON_PROTECTED_MAIN`. The `tools/list` discovery refinement may change to `IMPLEMENTED_ON_PROTECTED_MAIN` only after PR #170 reaches protected `main` under live governance and exact-head acceptance. Neither promotion makes the complete MCP adapter implemented; each remaining transport/runtime boundary requires its own integrated evidence. +The bounded `tools/call` and `tools/list` foundations are already `IMPLEMENTED_ON_PROTECTED_MAIN`. PR #272 may change the adapter architecture only after its exact current head proves repository-native CI, full owned-production coverage/rustdoc, security gates, required central workflows, and live review governance. Neither that promotion nor the existing protected-main contracts makes the complete MCP adapter implemented; each remaining transport/runtime boundary requires its own integrated evidence. diff --git a/tests/test_mcp_adapter_repository_contract.py b/tests/test_mcp_adapter_repository_contract.py new file mode 100644 index 000000000..cd3ca6525 --- /dev/null +++ b/tests/test_mcp_adapter_repository_contract.py @@ -0,0 +1,48 @@ +"""Repository contract for the MCP adapter dependency direction.""" + +from __future__ import annotations + +import pathlib +import tomllib +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] + + +class McpAdapterRepositoryContractTests(unittest.TestCase): + """Keep MCP transport types outside shared domain authority.""" + + def test_mcp_adapter_isolated_from_shared_domain_contracts(self) -> None: + """MCP may depend inward on policy; policy must not depend outward on MCP.""" + + self.assertFalse((ROOT / "crates/originweave-core/src/mcp.rs").exists()) + + mcp_manifest = tomllib.loads( + (ROOT / "crates/originweave-mcp/Cargo.toml").read_text(encoding="utf-8") + ) + self.assertEqual( + set(mcp_manifest.get("dependencies", {})), + {"originweave-core", "originweave-policy"}, + ) + + policy_manifest = tomllib.loads( + (ROOT / "crates/originweave-policy/Cargo.toml").read_text(encoding="utf-8") + ) + self.assertEqual(set(policy_manifest.get("dependencies", {})), {"originweave-core"}) + + policy_source = (ROOT / "crates/originweave-policy/src/lib.rs").read_text( + encoding="utf-8" + ) + self.assertNotIn("originweave_mcp", policy_source) + self.assertNotIn("ValidatedMcpToolCall", policy_source) + self.assertNotIn("Mcp", policy_source) + + mcp_source = (ROOT / "crates/originweave-mcp/src/lib.rs").read_text( + encoding="utf-8" + ) + self.assertIn("originweave_policy", mcp_source) + self.assertIn("evaluate_mcp", mcp_source) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_repository_contract.py b/tests/test_repository_contract.py index 00ceb5a12..b12f10d95 100644 --- a/tests/test_repository_contract.py +++ b/tests/test_repository_contract.py @@ -21,6 +21,7 @@ def test_workspace_declares_all_independently_reusable_crates(self) -> None: { "crates/originweave-core", "crates/originweave-bap", + "crates/originweave-mcp", "crates/originweave-policy", "crates/originweave-destination", "crates/originweave-network",