fix(mcp): publish properties in tool input schemas

This commit is contained in:
lxk955
2026-10-17 15:14:03 +08:00
committed by GitHub
parent c7004df282
commit dee61595f6
+73
View File
@@ -55,6 +55,7 @@ enum DbxResourceRequest {
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
#[schemars(extend("properties" = {}))]
pub struct ListConnectionsRequest {}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
@@ -3371,6 +3372,9 @@ impl DbxMcpServer {
if self.plugin_tools_mode.advertise_flat() {
tools.extend(self.plugin_tools_view(policy.as_ref()).await);
}
for tool in &mut tools {
normalize_tool_input_schema(tool);
}
tools
}
@@ -4003,6 +4007,23 @@ impl ServerHandler for DbxMcpServer {
}
}
/// Ensure object-typed input schemas publish a `properties` object.
/// OpenAI-compatible gateways (e.g. DashScope / 通义千问, DeepSeek Harness)
/// strictly validate tool schemas and reject all tool calls if any tool
/// with `type: "object"` lacks a `properties` map (issue #12464).
fn normalize_tool_input_schema(tool: &mut rmcp::model::Tool) {
let is_object = match tool.input_schema.get("type") {
Some(serde_json::Value::String(t)) => t == "object",
None => true,
_ => false,
};
if is_object && !tool.input_schema.contains_key("properties") {
let mut object = (*tool.input_schema).clone();
object.insert("properties".to_string(), serde_json::json!({}));
tool.input_schema = std::sync::Arc::new(object);
}
}
fn parse_dbx_resource_uri(uri: &str) -> Result<DbxResourceRequest, ErrorData> {
let parsed = Url::parse(uri).map_err(|_| ErrorData::invalid_params("Invalid DBX resource URI.", None))?;
if parsed.scheme() != "dbx"
@@ -6628,6 +6649,58 @@ mod tests {
.is_some_and(|required| required.iter().any(|field| field == "tables")));
}
#[test]
fn all_advertised_tools_publish_properties_in_input_schema() {
let server = DbxMcpServer::with_runtime_options(Arc::new(FakeBackend::default()), McpScope::default(), false);
let tools = server.tool_router.list_all();
for tool in &tools {
let is_object = match tool.input_schema.get("type") {
Some(serde_json::Value::String(t)) => t == "object",
None => true,
_ => false,
};
if is_object {
assert!(
tool.input_schema.get("properties").and_then(serde_json::Value::as_object).is_some(),
"tool {} inputSchema must publish a 'properties' object for OpenAI compatibility (issue #12464)",
tool.name
);
}
}
let list_connections = tools
.iter()
.find(|tool| tool.name == "dbx_list_connections")
.expect("dbx_list_connections should be registered");
assert_eq!(
list_connections.input_schema.get("properties"),
Some(&serde_json::json!({})),
"dbx_list_connections must have empty properties object"
);
}
#[tokio::test]
async fn policy_filtered_tools_guarantee_properties_for_all_object_schemas() {
let server = DbxMcpServer::with_runtime_options(Arc::new(FakeBackend::default()), McpScope::default(), false);
let tools = server.policy_filtered_tools().await;
for tool in &tools {
let is_object = match tool.input_schema.get("type") {
Some(serde_json::Value::String(t)) => t == "object",
None => true,
_ => false,
};
if is_object {
assert!(
tool.input_schema.get("properties").and_then(serde_json::Value::as_object).is_some(),
"advertised tool {} inputSchema must publish 'properties'",
tool.name
);
}
}
}
#[test]
fn connection_selector_schema_uses_optional_strings() {
let server = DbxMcpServer::with_runtime_options(Arc::new(FakeBackend::default()), McpScope::default(), false);