forked from 1b-agent/opencadstudio-upstream
refactor(mcp): spec-pure serverInfo, build identity via experimental
serverInfo carries only name/title/version so strict clients never reject the handshake. Build extras (build_rev, build_profile, tool_schema) live canonically in bridge_identity, surfaced via capabilities.experimental and the in-band bridge stamp - no copies to drift. Adds conformance guards: spec-pure initialize with unknown-version fallback, and JSON-RPC errors for unknown methods, missing tool names and missing resource URIs.
This commit is contained in:
1 parent
de4b47e7bd
commit
22715c9485
1 file changed
+71
-6
+71
-6
@@ -2004,14 +2004,15 @@ fn response(id: Value, result: Value) -> Value {
|
|||||||
json!({"jsonrpc":"2.0","id":id,"result":result})
|
json!({"jsonrpc":"2.0","id":id,"result":result})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Spec-pure implementation block: exactly name/title/version, so strict
|
||||||
|
/// clients (deny_unknown_fields, strict Zod schemas) never reject the
|
||||||
|
/// handshake. Build extras live canonically in [`bridge_identity`],
|
||||||
|
/// surfaced via `capabilities.experimental` and the in-band `bridge` stamp.
|
||||||
fn server_info() -> Value {
|
fn server_info() -> Value {
|
||||||
json!({
|
json!({
|
||||||
"name":"OpenCADStudio",
|
"name":"OpenCADStudio",
|
||||||
"title":"Open CAD Studio",
|
"title":"Open CAD Studio",
|
||||||
"version":env!("OCS_APP_VERSION"),
|
"version":env!("OCS_APP_VERSION"),
|
||||||
"build_rev":env!("OCS_GIT_REV"),
|
|
||||||
"build_profile":env!("OCS_BUILD_PROFILE"),
|
|
||||||
"tool_schema":tool_schema_digest(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2041,8 +2042,9 @@ fn with_bridge_identity(mut value: Value) -> Value {
|
|||||||
|
|
||||||
/// Sha256 hex digest of the canonical [`tool_definitions`] JSON.
|
/// Sha256 hex digest of the canonical [`tool_definitions`] JSON.
|
||||||
///
|
///
|
||||||
/// Published via [`server_info`] so MCP clients can detect a stale bridge
|
/// Published via `capabilities.experimental` and the in-band [`bridge_identity`]
|
||||||
/// (running an older binary than the one on disk) without guessing.
|
/// stamp so MCP clients can detect a stale bridge (running an older binary
|
||||||
|
/// than the one on disk) without guessing.
|
||||||
fn tool_schema_digest() -> String {
|
fn tool_schema_digest() -> String {
|
||||||
let canonical = serde_json::to_string(&tool_definitions()).unwrap_or_default();
|
let canonical = serde_json::to_string(&tool_definitions()).unwrap_or_default();
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
@@ -2187,7 +2189,8 @@ fn handle_message(
|
|||||||
"protocolVersion":protocol,
|
"protocolVersion":protocol,
|
||||||
"capabilities":{
|
"capabilities":{
|
||||||
"tools":{"listChanged":false},
|
"tools":{"listChanged":false},
|
||||||
"resources":{"subscribe":false,"listChanged":false}
|
"resources":{"subscribe":false,"listChanged":false},
|
||||||
|
"experimental":{"opencadstudio.build":bridge_identity()}
|
||||||
},
|
},
|
||||||
"serverInfo":server_info(),
|
"serverInfo":server_info(),
|
||||||
"instructions":INSTRUCTIONS
|
"instructions":INSTRUCTIONS
|
||||||
@@ -2628,6 +2631,68 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn initialize_is_spec_pure_with_experimental_build() {
|
||||||
|
// serverInfo carries only the spec'd name/title/version; build
|
||||||
|
// extras live canonically in capabilities.experimental (single
|
||||||
|
// source: bridge_identity), never copied.
|
||||||
|
let initialized = handle_message(
|
||||||
|
json!({"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25"}}),
|
||||||
|
&mut HashMap::new(),
|
||||||
|
&mut TaskStore::default(),
|
||||||
|
&mut ResourceStore::default(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let info = &initialized["result"]["serverInfo"];
|
||||||
|
assert_eq!(
|
||||||
|
info.as_object().unwrap().keys().collect::<Vec<_>>(),
|
||||||
|
["name", "title", "version"]
|
||||||
|
);
|
||||||
|
assert_eq!(info["name"], "OpenCADStudio");
|
||||||
|
let build = &initialized["result"]["capabilities"]["experimental"]["opencadstudio.build"];
|
||||||
|
assert_eq!(build["tool_schema"], Value::String(tool_schema_digest()));
|
||||||
|
assert!(build["build_rev"].as_str().is_some());
|
||||||
|
assert!(build["build_profile"].as_str().is_some());
|
||||||
|
// Unknown versions fall back to our newest instead of erroring.
|
||||||
|
let fallback = handle_message(
|
||||||
|
json!({"jsonrpc":"2.0","id":2,"method":"initialize","params":{"protocolVersion":"2099-01-01"}}),
|
||||||
|
&mut HashMap::new(),
|
||||||
|
&mut TaskStore::default(),
|
||||||
|
&mut ResourceStore::default(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(fallback["result"]["protocolVersion"], PROTOCOL_VERSION);
|
||||||
|
assert!(fallback.get("error").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn malformed_requests_get_jsonrpc_errors() {
|
||||||
|
let unknown = handle_message(
|
||||||
|
json!({"jsonrpc":"2.0","id":1,"method":"frobnicate"}),
|
||||||
|
&mut HashMap::new(),
|
||||||
|
&mut TaskStore::default(),
|
||||||
|
&mut ResourceStore::default(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(unknown["error"]["code"], -32601);
|
||||||
|
let nameless = handle_message(
|
||||||
|
json!({"jsonrpc":"2.0","id":2,"method":"tools/call","params":{}}),
|
||||||
|
&mut HashMap::new(),
|
||||||
|
&mut TaskStore::default(),
|
||||||
|
&mut ResourceStore::default(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(nameless["error"]["code"], -32602);
|
||||||
|
let uriless = handle_message(
|
||||||
|
json!({"jsonrpc":"2.0","id":3,"method":"resources/read","params":{}}),
|
||||||
|
&mut HashMap::new(),
|
||||||
|
&mut TaskStore::default(),
|
||||||
|
&mut ResourceStore::default(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(uriless["error"]["code"], -32602);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn read_tool_rejects_mutations_before_connecting() {
|
fn read_tool_rejects_mutations_before_connecting() {
|
||||||
let mut clients = HashMap::new();
|
let mut clients = HashMap::new();
|
||||||
|
|||||||
Reference in new issue
Block a user