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
10 changes: 4 additions & 6 deletions crates/brokkr-cli/src/broker/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ impl Display for Fatal {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Fatal::Failed(failure) => failure.fmt(formatter),
Fatal::Unsupported => Refused::Version.fmt(formatter),
Fatal::Unsupported => {
formatter.write_str("MCP protocol version is not supported by this broker")
}
Fatal::Missing(tool) => {
write!(formatter, "MCP server does not offer granted tool '{tool}'")
}
Expand All @@ -141,7 +143,6 @@ enum Refused {
Limit,
Invalid,
Active,
Version,
Budget,
}

Expand All @@ -150,7 +151,7 @@ impl Refused {
fn code(&self) -> i64 {
match self {
Refused::Method => -32601,
Refused::Ungranted { .. } | Refused::Version => -32602,
Refused::Ungranted { .. } => -32602,
Refused::Limit | Refused::Invalid | Refused::Active => -32600,
Refused::Budget => -32603,
}
Expand All @@ -168,9 +169,6 @@ impl Display for Refused {
Refused::Limit => formatter.write_str("MCP request exceeds the broker limit"),
Refused::Invalid => formatter.write_str("MCP tool call request is invalid"),
Refused::Active => formatter.write_str("MCP broker already has an active call"),
Refused::Version => {
formatter.write_str("MCP protocol version is not supported by this broker")
}
Refused::Budget => formatter.write_str("capability evidence budget is exhausted"),
}
}
Expand Down
10 changes: 6 additions & 4 deletions crates/brokkr-cli/src/broker/rpc/calls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
//! CR3, SC4), which `serve` takes up once its server has initialized
//! (U6f1b). The harness's
//! frames are read whole, at most 1 MiB and 64 deep, and each is checked
//! before anything is recorded: initialize at MB3's version, ping and the
//! before anything is recorded: initialize, answered at MB3's version
//! whatever version it asks for (U9b1c), ping and the
//! filtered listing are answered here, a cancellation of the active call
//! is passed on, notifications are taken, and any other method refused. A
//! tools/call naming a tool in seat-record's vocabulary within SC4's 256
Expand Down Expand Up @@ -449,8 +450,7 @@ impl<'s, W: Write + AsFd, E: Read + AsFd> Session<'s, '_, '_, W, E> {
Err((id, refused)) => return self.refuse(id.as_ref(), &refused),
};
match request {
Request::Initialize(id, version) if version == VERSION => self.greet(&id),
Request::Initialize(id, _) => self.refuse(Some(&id), &Refused::Version),
Request::Initialize(id) => self.greet(&id),
Request::Ping(id) => self.tell(Some(&id), Reply::Result(json!({}))),
Request::List(id) => self.listing(&id),
Request::Call(id, called) => self.call(id, called),
Expand All @@ -464,7 +464,9 @@ impl<'s, W: Write + AsFd, E: Read + AsFd> Session<'s, '_, '_, W, E> {
}

/// Answer the harness's initialize `id` as the plan's server, at MB3's
/// version and the dialect's pinned one, offering tools alone.
/// version and the dialect's pinned one, offering tools alone: MB3's
/// version whatever version it asked, as MCP 2025-06-18's lifecycle
/// negotiates one (U9b1c), and the session held to it.
fn greet(&mut self, id: &Id) -> Result<(), Ended> {
let server = json!({"name": self.plan.server, "version": self.plan.dialect.version});
let tools = json!({"tools": {}});
Expand Down
13 changes: 7 additions & 6 deletions crates/brokkr-cli/src/broker/rpc/calls/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,13 @@ struct Asked {
_keyed: Keyed,
}

/// A harness's initialize parameters: the version it asks for, beside
/// whatever else it says.
/// A harness's initialize parameters: the version it asks for, which must
/// be a string, whatever it names, beside whatever else it says. MB3's
/// version answers any (U9b1c), as MCP's version negotiation has it.
#[derive(Deserialize)]
struct Greeting {
#[serde(rename = "protocolVersion")]
version: String,
_version: String,
#[serde(flatten)]
_keyed: Keyed,
}
Expand Down Expand Up @@ -68,7 +69,7 @@ pub(super) struct Called {

/// A harness request, checked whole.
pub(super) enum Request {
Initialize(Id, String),
Initialize(Id),
Ping(Id),
List(Id),
Call(Id, Called),
Expand All @@ -81,7 +82,7 @@ impl Request {
/// The id the harness would be answered by, none for a notification.
pub(super) fn id(&self) -> Option<&Id> {
match self {
Request::Initialize(id, _) | Request::Ping(id) | Request::List(id) => Some(id),
Request::Initialize(id) | Request::Ping(id) | Request::List(id) => Some(id),
Request::Call(id, _) | Request::Other(id) => Some(id),
Request::Cancel(_) | Request::Told => None,
}
Expand All @@ -105,7 +106,7 @@ pub(super) fn decoded(bytes: &[u8]) -> Result<Request, (Option<Id>, Refused)> {
}
(_, None) => Ok(Request::Told),
("initialize", Some(id)) => match Greeting::deserialize(params) {
Ok(greeting) => Ok(Request::Initialize(id, greeting.version)),
Ok(_) => Ok(Request::Initialize(id)),
Err(_) => Err((Some(id), Refused::Invalid)),
},
("ping", Some(id)) => Ok(Request::Ping(id)),
Expand Down
15 changes: 6 additions & 9 deletions crates/brokkr-cli/src/broker/rpc/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,11 +274,6 @@ fn each_cause_reads_in_mb3s_words_and_latches_through_the_ledgers_own() {
-32600,
"MCP broker already has an active call",
),
(
Refused::Version,
-32602,
"MCP protocol version is not supported by this broker",
),
];
for (refused, code, text) in refused {
assert_eq!(
Expand Down Expand Up @@ -1030,7 +1025,6 @@ fn exchange(frames: Vec<Value>, answers: Vec<Value>) -> impl FnOnce(&mut Harness

#[test]
fn the_harness_is_answered_here_and_its_granted_call_forwarded_once_recorded() {
let version = "MCP protocol version is not supported by this broker";
let greeting = json!({"protocolVersion": "2025-06-18", "capabilities": {"tools": {}},
"serverInfo": {"name": "cap-library-docs", "version": "1.4.2"}});
let asked = vec![
Expand All @@ -1043,31 +1037,34 @@ fn the_harness_is_answered_here_and_its_granted_call_forwarded_once_recorded() {
request(json!("p"), "ping", json!({})),
request(json!(1), "tools/list", json!({})),
request(json!(2), "resources/list", json!({})),
// MCP's negotiation: another version is answered with MB3's.
request(
json!(3),
"initialize",
json!({"protocolVersion": "2024-11-05"}),
json!({"protocolVersion": "2025-11-25"}),
),
json!({"jsonrpc": "2.0", "id": 4, "method": "initialize"}),
request(json!(5), "initialize", json!(["2025-06-18"])),
request(json!(6), "initialize", json!({"protocolVersion": 7})),
request(
json!("c"),
"tools/call",
json!({"name": "lookup", "arguments": {"q": "x"}}),
),
];
let answers = vec![
result(&json!(0), greeting),
result(&json!(0), greeting.clone()),
result(&json!("p"), json!({})),
result(&json!(1), json!({"tools": granted()})),
refusal(
json!(2),
-32601,
"MCP method is not admitted by this broker",
),
refusal(json!(3), -32602, version),
result(&json!(3), greeting),
refusal(json!(4), -32600, "MCP tool call request is invalid"),
refusal(json!(5), -32600, "MCP tool call request is invalid"),
refusal(json!(6), -32600, "MCP tool call request is invalid"),
echoed("c", "lookup"),
];
// The call, asked last, is answered once the server has answered it.
Expand Down
4 changes: 3 additions & 1 deletion crates/brokkr-cli/tests/capability_broker/enabled.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
//! Linux only, as a server box is; macOS's refusal is `capability_broker`'s
//! `the_compile_fence_refuses_an_unused_mcp_grant_only_where_it_stands`.
//! Children: `transport` (42.2), `matrix`, `reach`, `sites`, `boxes` and
//! `windows` (46.1), `lifetime` (46.2's two worktrees), `dispatch` (ruling 21).
//! `windows` (46.1), `lifetime` (46.2's two worktrees), `dispatch` (ruling 21),
//! `negotiation` (U9b1c's 46.3).
//!
//! The engine runs here as an operator runs it, with no wrapper: `brokkr
//! driver` sets `no_new_privs` on itself before it spawns a brokered harness
Expand All @@ -25,6 +26,7 @@ mod boxes;
mod dispatch;
mod lifetime;
mod matrix;
mod negotiation;
mod reach;
mod sites;
mod transport;
Expand Down
82 changes: 82 additions & 0 deletions crates/brokkr-cli/tests/capability_broker/enabled/negotiation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//! Task 46.3 (U9b1c; MB3): MCP's version negotiation through the real
//! binary. A harness asking a version other than MB3's, as Claude Code asks
//! 2025-11-25, is answered with the broker's own 2025-06-18 result, as MCP
//! 2025-06-18's lifecycle requires of a server that does not support the
//! version asked
//! (<https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle>),
//! and the session is held to 2025-06-18: a method it does not admit still
//! refuses, and an initialize with no string version still refuses as before.

use serde_json::{json, Value};

use super::World;

/// What the harness says to its broker, one frame a line: a greeting asking
/// 2025-11-25, its listing, a method 2025-06-18 does not admit, a greeting
/// asking MB3's own version, one with no string version, and one call of
/// `resolve`.
const FRAMES: &str = r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"harness","version":"1"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
{"jsonrpc":"2.0","id":3,"method":"resources/list","params":{}}
{"jsonrpc":"2.0","id":4,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{}}}
{"jsonrpc":"2.0","id":5,"method":"initialize","params":{"protocolVersion":20251125,"capabilities":{}}}
{"jsonrpc":"2.0","id":"c","method":"tools/call","params":{"name":"resolve","arguments":{}}}
"#;

/// The broker's error answer to the harness's request `id`.
fn refused(id: u8, code: i64, message: &str) -> Value {
json!({"jsonrpc": "2.0", "id": id, "error": {"code": code, "message": message}})
}

/// A harness asking 2025-11-25 is answered 2025-06-18, as one asking
/// 2025-06-18 is; it lists exactly the granted tools and completes a call
/// whose response is retained and opens through `inspect`; a method
/// 2025-06-18 does not admit refuses, and so does a version that is no
/// string, each with its exact cause.
#[test]
fn a_harness_asking_another_version_is_answered_mb3s_and_completes_a_granted_call() {
let Some(world) = World::new() else {
return;
};
world.frames(FRAMES);
let ended = world.completes();
assert_eq!(world.broker_code(), Some(0));
let greeting = |id: u8| {
json!({"jsonrpc": "2.0", "id": id, "result": {"protocolVersion": "2025-06-18",
"capabilities": {"tools": {}},
"serverInfo": {"name": "cap-library-docs", "version": "1.4.2"}}})
};
let tools = json!([{"name": "resolve", "inputSchema": {"type": "object"}},
{"name": "read", "inputSchema": {"type": "object"}}]);
let heard = std::fs::read_to_string(world.bin("heard")).unwrap();
let heard: Vec<Value> = heard
.lines()
.map(|line| serde_json::from_str(line).unwrap())
.collect();
let said = json!({"content": [{"type": "text", "text": "resolved"}]});
assert_eq!(
heard,
[
greeting(1),
json!({"jsonrpc": "2.0", "id": 2, "result": {"tools": tools}}),
refused(3, -32601, "MCP method is not admitted by this broker"),
greeting(4),
refused(5, -32600, "MCP tool call request is invalid"),
json!({"jsonrpc": "2.0", "id": "c", "result": said}),
]
);
let view = world.opened(&ended.run);
let digest = brokkr_core::canonical::sha256_bytes(
br#"{"result":{"content":[{"text":"resolved","type":"text"}]}}"#,
);
assert_eq!(
(&view["response"], &view["call"]["state"]),
(&json!({"result": said}), &json!("succeeded")),
"{view}"
);
assert_eq!(
world.calls(&ended.run),
[("resolve".into(), "succeeded".into(), Some(digest))]
);
}
Loading
Loading