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: 7 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions flicknote-cli/src/main_tests/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 a format annotation: {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;
Expand All @@ -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
Expand Down
40 changes: 39 additions & 1 deletion flicknote-cli/src/mcp/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,21 @@ impl FlickNoteMcp {
pub(crate) fn new(config: Arc<Config>) -> Self {
Self {
config,
tool_router: Self::tool_router(),
tool_router: Self::normalized_tool_router(),
}
}

fn normalized_tool_router() -> ToolRouter<Self> {
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<T: AppResult>(&self, request: AppRequest) -> Result<T, ServiceError> {
DaemonClient::new(&self.config).call(request).await
}
Expand All @@ -100,6 +111,33 @@ impl FlickNoteMcp {
}
}

/// Remove generator-specific format annotations from advertised MCP schemas.
///
/// 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<String, serde_json::Value>) {
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<T>(result: Result<T, ServiceError>) -> Result<Json<T>, CallToolResult> {
result.map(Json).map_err(|error| tool_error(&error))
}
Expand Down