Unlucky Launcher v0.1.0
CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
单提交快照:main 只保留已发布版本的代码树,开发史在 dev。
This commit is contained in:
commit
5f8382565e
570 files changed
+104468
No files matched your search
@@ -0,0 +1,106 @@
|
||||
//! 双投影的 **MCP 侧**:`ToolDefinition` → rmcp `Tool`(`ToolRoute::new_dyn` 的属性入参)。
|
||||
//!
|
||||
//! rig 侧在 [`crate::brain::rig`](`ToolDefinition → RigToolDefinition`)。两侧都只吃裸
|
||||
//! `serde_json::Value` schema,所以「一致性」是可断言的:`tests/mcp_contract.rs` 遍历注册表,
|
||||
//! 断言 MCP 侧的 `(name, description, input_schema)` 与注册表逐项相等——这就是
|
||||
//! research/02 §6.2 建议进 CI 的那条契约。
|
||||
//!
|
||||
//! 额外一位信息:`annotations.read_only_hint`。MCP 客户端拿它决定「这个工具能不能
|
||||
//! 静默调用」,我们用它把 [`Access`] 语义透出去(只读提示,不是权限——权限在编排层)。
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use rmcp::model::{JsonObject, Tool, ToolAnnotations};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::registry::{Access, ToolDefinition, ToolRegistry};
|
||||
|
||||
/// 一个 `ToolDefinition` → rmcp `Tool`。
|
||||
///
|
||||
/// 注册表在注册阶段已保证 `input_schema` 根是 `object`,所以这里的兜底分支
|
||||
/// (非对象 → 空 schema)只是为了不 panic:真走到它说明注册校验被绕过了。
|
||||
#[must_use]
|
||||
pub fn tool_attr(definition: &ToolDefinition, access: Access) -> Tool {
|
||||
let schema: JsonObject = match &definition.input_schema {
|
||||
Value::Object(map) => map.clone(),
|
||||
_ => JsonObject::new(),
|
||||
};
|
||||
let mut attr = Tool::new(
|
||||
definition.name.clone(),
|
||||
definition.description.clone(),
|
||||
Arc::new(schema),
|
||||
);
|
||||
let mut annotations = ToolAnnotations::default();
|
||||
annotations.read_only_hint = Some(access == Access::ReadOnly);
|
||||
annotations.destructive_hint = Some(access == Access::Mutating);
|
||||
attr.annotations = Some(annotations);
|
||||
attr
|
||||
}
|
||||
|
||||
/// 注册表 → rmcp `Tool` 列表(顺序同注册顺序,便于两侧比对)。
|
||||
#[must_use]
|
||||
pub fn tool_attrs(registry: &ToolRegistry) -> Vec<Tool> {
|
||||
registry
|
||||
.specs()
|
||||
.iter()
|
||||
.map(|spec| tool_attr(&spec.definition(), spec.access))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::testing::mock_ports;
|
||||
use crate::tools::build_v1_registry;
|
||||
use serde_json::json;
|
||||
|
||||
fn registry() -> ToolRegistry {
|
||||
let (ports, _settings) = mock_ports();
|
||||
build_v1_registry(&ports).expect("注册表应装配成功")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projection_keeps_name_description_and_schema() {
|
||||
let registry = registry();
|
||||
let attrs = tool_attrs(®istry);
|
||||
assert_eq!(attrs.len(), registry.len());
|
||||
|
||||
for (attr, spec) in attrs.iter().zip(registry.specs()) {
|
||||
let definition = spec.definition();
|
||||
assert_eq!(attr.name.as_ref(), definition.name);
|
||||
assert_eq!(
|
||||
attr.description.as_deref(),
|
||||
Some(definition.description.as_str()),
|
||||
"{} 的说明必须逐字相等",
|
||||
definition.name
|
||||
);
|
||||
let projected = Value::Object((*attr.input_schema).clone());
|
||||
assert_eq!(
|
||||
projected, definition.input_schema,
|
||||
"{} 的 JSON Schema 必须逐字相等",
|
||||
definition.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_only_hint_matches_access() {
|
||||
let registry = registry();
|
||||
for (attr, spec) in tool_attrs(®istry).iter().zip(registry.specs()) {
|
||||
let hint = attr
|
||||
.annotations
|
||||
.as_ref()
|
||||
.and_then(|a| a.read_only_hint)
|
||||
.unwrap_or(false);
|
||||
assert_eq!(hint, spec.access == Access::ReadOnly, "{}", attr.name);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_root_stays_an_object_on_the_wire() {
|
||||
for attr in tool_attrs(®istry()) {
|
||||
let value = Value::Object((*attr.input_schema).clone());
|
||||
assert_eq!(value["type"], json!("object"), "{}", attr.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user