优化: 前端体验批次(Knowledge持久化+流式缓存失效 + dfnodes节点注册 + delta 50ms合批 + MCP schema必填校验+bind_directory对齐)
This commit is contained in:
@@ -215,7 +215,19 @@ pub(crate) async fn dispatch(ctx: &Ctx, read_only: bool, id: Option<Value>, meth
|
||||
let r = CallToolResult::error(msg);
|
||||
return Response::ok(id, serde_json::to_value(r).unwrap_or(Value::Null));
|
||||
}
|
||||
// Low / Medium:执行
|
||||
// Low / Medium:执行前先校验 schema 必填参数(缺必填直接拒,
|
||||
// 防 handler 内 arg_str_or 静默兜底把缺参当空串/默认值写入)
|
||||
let missing: Vec<String> = required_names(&spec.tool.input_schema)
|
||||
.into_iter()
|
||||
.filter(|name| arguments.get(name).map_or(true, |v| v.is_null()))
|
||||
.collect();
|
||||
if !missing.is_empty() {
|
||||
let r = CallToolResult::error(format!(
|
||||
"缺少必填参数: {}",
|
||||
missing.join(", ")
|
||||
));
|
||||
return Response::ok(id, serde_json::to_value(r).unwrap_or(Value::Null));
|
||||
}
|
||||
let result = (spec.handler)(ctx, arguments).await;
|
||||
Response::ok(id, serde_json::to_value(result).unwrap_or(Value::Null))
|
||||
}
|
||||
@@ -235,6 +247,30 @@ fn should_execute(read_only: bool, risk: RiskLevel) -> bool {
|
||||
!(read_only && risk != RiskLevel::Low) && risk != RiskLevel::High
|
||||
}
|
||||
|
||||
/// 从工具 inputSchema 提取必填参数名列表。
|
||||
///
|
||||
/// 兼容 MCP schema required 的两种形态:array 显式列出必填属性;bool true 表示全部
|
||||
/// properties 必填;false/缺失表示无必填。供 tools/call 执行前校验参数完整性,
|
||||
/// 缺必填直接拒绝,杜绝 handler 内 arg_str_or 静默兜底把缺参当空串/默认值写入。
|
||||
fn required_names(schema: &Value) -> Vec<String> {
|
||||
match schema.get("required") {
|
||||
Some(Value::Array(items)) => items
|
||||
.iter()
|
||||
.filter_map(|v| v.as_str().map(|s| s.to_owned()))
|
||||
.collect(),
|
||||
Some(Value::Bool(true)) => {
|
||||
let mut names: Vec<String> = schema
|
||||
.get("properties")
|
||||
.and_then(|p| p.as_object())
|
||||
.map(|props| props.keys().cloned().collect())
|
||||
.unwrap_or_default();
|
||||
names.sort();
|
||||
names
|
||||
}
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 触发写操作回调(若有)。仅当工具为写操作(risk != Low)且未被 read-only/High 拒绝时触发,
|
||||
/// 与 dispatch 的执行判定一致。回调仅作通知(如 GUI 刷新),不承载返回结果。
|
||||
fn fire_write_hook(config: &ServerConfig, name: &str) {
|
||||
@@ -415,6 +451,54 @@ mod tests {
|
||||
assert_eq!(list_v["projects"][0]["name"], "McpProj");
|
||||
}
|
||||
|
||||
// ── schema required 校验(dispatch 层缺必填直接拒)────────────────
|
||||
|
||||
#[tokio::test]
|
||||
async fn tools_call_missing_required_arg_returns_error() {
|
||||
// create_project schema required 含 name:缺 name 应被 dispatch 层拒绝
|
||||
// (而非 handler 内 arg_str_or 静默兜底把 description 当空串)
|
||||
let input =
|
||||
r#"{"jsonrpc":"2.0","id":9,"method":"tools/call","params":{"name":"create_project","arguments":{"description":"缺 name"}}}"#;
|
||||
let out = run_io_lines(&[input], false).await;
|
||||
let v: Value = serde_json::from_str(&out[0]).unwrap();
|
||||
assert_eq!(v["result"]["isError"], true);
|
||||
let text = v["result"]["content"][0]["text"].as_str().unwrap();
|
||||
assert!(text.contains("缺少必填参数: name"), "实际: {text}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn required_names_from_array() {
|
||||
let schema = json!({
|
||||
"type": "object",
|
||||
"properties": { "id": {}, "name": {} },
|
||||
"required": ["id", "name"]
|
||||
});
|
||||
assert_eq!(required_names(&schema), vec!["id".to_string(), "name".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn required_names_from_bool_true_means_all_properties() {
|
||||
let schema = json!({
|
||||
"type": "object",
|
||||
"properties": { "id": {}, "name": {}, "desc": {} },
|
||||
"required": true
|
||||
});
|
||||
// bool true = 全部 properties 必填,返回全部属性名(排序保证确定性)
|
||||
assert_eq!(
|
||||
required_names(&schema),
|
||||
vec!["desc".to_string(), "id".to_string(), "name".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn required_names_false_or_missing_is_empty() {
|
||||
assert_eq!(required_names(&json!({ "type": "object" })), Vec::<String>::new());
|
||||
assert_eq!(
|
||||
required_names(&json!({ "type": "object", "required": false })),
|
||||
Vec::<String>::new()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn visible_predicate() {
|
||||
assert!(visible(false, RiskLevel::Low));
|
||||
|
||||
Reference in New Issue
Block a user