From 9f947fee3ba916fd67706131c07f1fd70341fdf5 Mon Sep 17 00:00:00 2001 From: neil Date: Thu, 13 Aug 2026 10:24:52 +0800 Subject: [PATCH 1/4] fix(cli): remove MCP schema format annotations --- flicknote-cli/src/main_tests/mcp.rs | 22 +++++++++++++++ flicknote-cli/src/mcp/server.rs | 42 ++++++++++++++++++++++++++++- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/flicknote-cli/src/main_tests/mcp.rs b/flicknote-cli/src/main_tests/mcp.rs index 82c2b48..de28d36 100644 --- a/flicknote-cli/src/main_tests/mcp.rs +++ b/flicknote-cli/src/main_tests/mcp.rs @@ -474,6 +474,26 @@ fn assert_no_boolean_schema_terms(schema: &serde_json::Value, tool: &str) { } } +fn assert_no_schema_formats(schema: &serde_json::Value, tool: &str) { + match schema { + serde_json::Value::Object(map) => { + assert!( + !map.contains_key("format"), + "tool {tool}: schema contains an implementation-specific format: {schema}" + ); + for value in map.values() { + assert_no_schema_formats(value, tool); + } + } + serde_json::Value::Array(values) => { + for value in values { + assert_no_schema_formats(value, tool); + } + } + _ => {} + } +} + #[tokio::test] async fn mcp_tool_output_schemas_are_strict_client_compatible() { let mut harness = McpHarness::start().await; @@ -487,6 +507,8 @@ async fn mcp_tool_output_schemas_are_strict_client_compatible() { "tool {name}: outputSchema root type must be object" ); assert_no_boolean_schema_terms(output, name); + assert_no_schema_formats(output, name); + assert_no_schema_formats(&tool["inputSchema"], name); } let list = tools diff --git a/flicknote-cli/src/mcp/server.rs b/flicknote-cli/src/mcp/server.rs index 8402058..84c7a83 100644 --- a/flicknote-cli/src/mcp/server.rs +++ b/flicknote-cli/src/mcp/server.rs @@ -75,10 +75,21 @@ impl FlickNoteMcp { pub(crate) fn new(config: Arc) -> Self { Self { config, - tool_router: Self::tool_router(), + tool_router: Self::normalized_tool_router(), } } + fn normalized_tool_router() -> ToolRouter { + let mut router = Self::tool_router(); + for route in router.map.values_mut() { + normalize_schema_formats(Arc::make_mut(&mut route.attr.input_schema)); + if let Some(output_schema) = &mut route.attr.output_schema { + normalize_schema_formats(Arc::make_mut(output_schema)); + } + } + router + } + async fn call(&self, request: AppRequest) -> Result { DaemonClient::new(&self.config).call(request).await } @@ -100,6 +111,35 @@ impl FlickNoteMcp { } } +/// Remove generator-specific format labels from advertised MCP schemas. +/// +/// JSON Schema's `format` is annotation-only and Schemars emits Rust integer +/// implementation details such as `uint` and `uint64`. MCP clients need only +/// the existing `integer` type and numeric bounds; omitting every format avoids +/// client-specific format registries without changing serialization or validation. +fn normalize_schema_formats(schema: &mut serde_json::Map) { + fn visit(value: &mut serde_json::Value) { + match value { + serde_json::Value::Object(object) => { + object.remove("format"); + for value in object.values_mut() { + visit(value); + } + } + serde_json::Value::Array(values) => { + for value in values { + visit(value); + } + } + _ => {} + } + } + + for value in schema.values_mut() { + visit(value); + } +} + fn structured(result: Result) -> Result, CallToolResult> { result.map(Json).map_err(|error| tool_error(&error)) } From 2ddf099ffa44ee78b7d2c6057ab3a03ff18951d3 Mon Sep 17 00:00:00 2001 From: neil Date: Thu, 13 Aug 2026 10:37:14 +0800 Subject: [PATCH 2/4] fix(cli): preserve semantic MCP schema formats --- flicknote-cli/src/main_tests/mcp.rs | 21 ++++++++++++++------- flicknote-cli/src/mcp/server.rs | 25 +++++++++++++++++++------ 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/flicknote-cli/src/main_tests/mcp.rs b/flicknote-cli/src/main_tests/mcp.rs index de28d36..170b017 100644 --- a/flicknote-cli/src/main_tests/mcp.rs +++ b/flicknote-cli/src/main_tests/mcp.rs @@ -474,20 +474,27 @@ fn assert_no_boolean_schema_terms(schema: &serde_json::Value, tool: &str) { } } -fn assert_no_schema_formats(schema: &serde_json::Value, tool: &str) { +fn assert_no_unsigned_integer_formats(schema: &serde_json::Value, tool: &str) { match schema { serde_json::Value::Object(map) => { assert!( - !map.contains_key("format"), - "tool {tool}: schema contains an implementation-specific format: {schema}" + !map.get("format") + .and_then(serde_json::Value::as_str) + .is_some_and(|format| { + matches!( + format, + "uint" | "uint8" | "uint16" | "uint32" | "uint64" | "uint128" + ) + }), + "tool {tool}: schema contains a nonportable unsigned-integer format: {schema}" ); for value in map.values() { - assert_no_schema_formats(value, tool); + assert_no_unsigned_integer_formats(value, tool); } } serde_json::Value::Array(values) => { for value in values { - assert_no_schema_formats(value, tool); + assert_no_unsigned_integer_formats(value, tool); } } _ => {} @@ -507,8 +514,8 @@ async fn mcp_tool_output_schemas_are_strict_client_compatible() { "tool {name}: outputSchema root type must be object" ); assert_no_boolean_schema_terms(output, name); - assert_no_schema_formats(output, name); - assert_no_schema_formats(&tool["inputSchema"], name); + assert_no_unsigned_integer_formats(output, name); + assert_no_unsigned_integer_formats(&tool["inputSchema"], name); } let list = tools diff --git a/flicknote-cli/src/mcp/server.rs b/flicknote-cli/src/mcp/server.rs index 84c7a83..3edced0 100644 --- a/flicknote-cli/src/mcp/server.rs +++ b/flicknote-cli/src/mcp/server.rs @@ -111,17 +111,30 @@ impl FlickNoteMcp { } } -/// Remove generator-specific format labels from advertised MCP schemas. +/// Return whether `format` is one of Schemars' unsigned Rust primitive labels. +fn is_schemars_unsigned_integer_format(format: &str) -> bool { + matches!( + format, + "uint" | "uint8" | "uint16" | "uint32" | "uint64" | "uint128" + ) +} + +/// Remove Schemars' nonportable unsigned-integer labels from MCP schemas. /// -/// JSON Schema's `format` is annotation-only and Schemars emits Rust integer -/// implementation details such as `uint` and `uint64`. MCP clients need only -/// the existing `integer` type and numeric bounds; omitting every format avoids -/// client-specific format registries without changing serialization or validation. +/// These Rust implementation details are not JSON Schema-defined formats and +/// strict clients may reject them. Keep semantic formats such as `uuid`, `uri`, +/// and `date-time`, which clients can use as annotations. fn normalize_schema_formats(schema: &mut serde_json::Map) { fn visit(value: &mut serde_json::Value) { match value { serde_json::Value::Object(object) => { - object.remove("format"); + if object + .get("format") + .and_then(serde_json::Value::as_str) + .is_some_and(is_schemars_unsigned_integer_format) + { + object.remove("format"); + } for value in object.values_mut() { visit(value); } From e30fc1f97d591ab07eaf331353b66655e0999747 Mon Sep 17 00:00:00 2001 From: neil Date: Thu, 13 Aug 2026 11:23:20 +0800 Subject: [PATCH 3/4] fix(cli): remove MCP schema format annotations --- flicknote-cli/src/main_tests/mcp.rs | 21 +++++++-------------- flicknote-cli/src/mcp/server.rs | 23 ++++------------------- 2 files changed, 11 insertions(+), 33 deletions(-) diff --git a/flicknote-cli/src/main_tests/mcp.rs b/flicknote-cli/src/main_tests/mcp.rs index 170b017..3d40ac2 100644 --- a/flicknote-cli/src/main_tests/mcp.rs +++ b/flicknote-cli/src/main_tests/mcp.rs @@ -474,27 +474,20 @@ fn assert_no_boolean_schema_terms(schema: &serde_json::Value, tool: &str) { } } -fn assert_no_unsigned_integer_formats(schema: &serde_json::Value, tool: &str) { +fn assert_no_schema_formats(schema: &serde_json::Value, tool: &str) { match schema { serde_json::Value::Object(map) => { assert!( - !map.get("format") - .and_then(serde_json::Value::as_str) - .is_some_and(|format| { - matches!( - format, - "uint" | "uint8" | "uint16" | "uint32" | "uint64" | "uint128" - ) - }), - "tool {tool}: schema contains a nonportable unsigned-integer format: {schema}" + !map.contains_key("format"), + "tool {tool}: schema contains a format annotation: {schema}" ); for value in map.values() { - assert_no_unsigned_integer_formats(value, tool); + assert_no_schema_formats(value, tool); } } serde_json::Value::Array(values) => { for value in values { - assert_no_unsigned_integer_formats(value, tool); + assert_no_schema_formats(value, tool); } } _ => {} @@ -514,8 +507,8 @@ async fn mcp_tool_output_schemas_are_strict_client_compatible() { "tool {name}: outputSchema root type must be object" ); assert_no_boolean_schema_terms(output, name); - assert_no_unsigned_integer_formats(output, name); - assert_no_unsigned_integer_formats(&tool["inputSchema"], name); + assert_no_schema_formats(output, name); + assert_no_schema_formats(&tool["inputSchema"], name); } let list = tools diff --git a/flicknote-cli/src/mcp/server.rs b/flicknote-cli/src/mcp/server.rs index 3edced0..f63e6d4 100644 --- a/flicknote-cli/src/mcp/server.rs +++ b/flicknote-cli/src/mcp/server.rs @@ -111,30 +111,15 @@ impl FlickNoteMcp { } } -/// Return whether `format` is one of Schemars' unsigned Rust primitive labels. -fn is_schemars_unsigned_integer_format(format: &str) -> bool { - matches!( - format, - "uint" | "uint8" | "uint16" | "uint32" | "uint64" | "uint128" - ) -} - -/// Remove Schemars' nonportable unsigned-integer labels from MCP schemas. +/// Remove generator-specific format annotations from advertised MCP schemas. /// -/// These Rust implementation details are not JSON Schema-defined formats and -/// strict clients may reject them. Keep semantic formats such as `uuid`, `uri`, -/// and `date-time`, which clients can use as annotations. +/// The server remains the authority for value validation; MCP clients do not +/// provide a useful user-facing behavior for these annotations. fn normalize_schema_formats(schema: &mut serde_json::Map) { fn visit(value: &mut serde_json::Value) { match value { serde_json::Value::Object(object) => { - if object - .get("format") - .and_then(serde_json::Value::as_str) - .is_some_and(is_schemars_unsigned_integer_format) - { - object.remove("format"); - } + object.remove("format"); for value in object.values_mut() { visit(value); } From 549fc1108aebf48b9a7e3b377130a0ba82cc3a44 Mon Sep 17 00:00:00 2001 From: neil Date: Thu, 13 Aug 2026 11:31:35 +0800 Subject: [PATCH 4/4] chore(cli): clarify MCP schema format policy --- AGENTS.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 3681e0a..1a17210 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,9 +25,13 @@ for human and operational workflows; content and section mutations are not CLI commands. Every MCP structured result must have an object root, and each advertised output -schema must be precise and derived from its boundary DTO. Arbitrary JSON schema -terms must use object form rather than bare boolean terms. Every MCP change must -pass the repository-wide strict-client output-schema contract test. +schema must be precise and derived from its boundary DTO's serialized JSON +contract: fields, requiredness, JSON types, value and structural constraints, +and references. `format` annotations are intentionally omitted: client support +is nonportable, so they do not establish a client-facing validation or UI +contract; server-side validation is authoritative. Arbitrary JSON schema terms +must use object form rather than bare boolean terms. Every MCP change must pass +the repository-wide strict-client output-schema contract test. ## Build & Test