feat(cross-project): découverte et contact inter-projets — tools MCP idea_project_{list,read_context,list_agents,ask_agent,cancel_agent}, permission contactable par agent + default_contactable persistée dans les templates, routage orchestrator par projectId+agentId — #260 (tests ciblés verts, QA en attente)
This commit is contained in:
@ -45,6 +45,9 @@ pub enum McpToolAccess {
|
||||
|
||||
/// Canonical read-only MCP tools allowed by default.
|
||||
pub const READ_ONLY_TOOLS: &[&str] = &[
|
||||
"idea_project_list",
|
||||
"idea_project_read_context",
|
||||
"idea_project_list_agents",
|
||||
"idea_list_agents",
|
||||
"idea_context_read",
|
||||
"idea_memory_read",
|
||||
@ -62,6 +65,8 @@ pub const READ_ONLY_TOOLS: &[&str] = &[
|
||||
|
||||
/// Canonical write/action MCP tools denied by default.
|
||||
pub const WRITE_ACTION_TOOLS: &[&str] = &[
|
||||
"idea_project_ask_agent",
|
||||
"idea_project_cancel_agent",
|
||||
"idea_ask_agent",
|
||||
"idea_ask_agents",
|
||||
"idea_run_in_background",
|
||||
@ -136,7 +141,12 @@ pub enum ToolMapError {
|
||||
pub fn tool_returns_reply(tool: &str) -> bool {
|
||||
matches!(
|
||||
tool,
|
||||
"idea_ask_agent"
|
||||
"idea_project_list"
|
||||
| "idea_project_read_context"
|
||||
| "idea_project_list_agents"
|
||||
| "idea_project_ask_agent"
|
||||
| "idea_project_cancel_agent"
|
||||
| "idea_ask_agent"
|
||||
| "idea_ask_agents"
|
||||
| "idea_list_agents"
|
||||
| "idea_context_read"
|
||||
@ -174,6 +184,68 @@ pub fn is_managed_temp_tool(tool: &str) -> bool {
|
||||
#[must_use]
|
||||
pub fn catalogue() -> Vec<ToolDef> {
|
||||
let mut tools = vec![
|
||||
ToolDef {
|
||||
name: "idea_project_list",
|
||||
description: "List known IdeA projects for cross-project agent discovery.",
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
},
|
||||
ToolDef {
|
||||
name: "idea_project_read_context",
|
||||
description: "Read a project's global context by stable project id.",
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"projectId": { "type": "string", "format": "uuid" }
|
||||
},
|
||||
"required": ["projectId"],
|
||||
"additionalProperties": false
|
||||
}),
|
||||
},
|
||||
ToolDef {
|
||||
name: "idea_project_list_agents",
|
||||
description: "List cross-project contactable agents for a project by stable project id.",
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"projectId": { "type": "string", "format": "uuid" }
|
||||
},
|
||||
"required": ["projectId"],
|
||||
"additionalProperties": false
|
||||
}),
|
||||
},
|
||||
ToolDef {
|
||||
name: "idea_project_ask_agent",
|
||||
description: "Ask a cross-project contactable agent by projectId and agentId and wait for its final answer.",
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"projectId": { "type": "string", "format": "uuid" },
|
||||
"agentId": { "type": "string", "format": "uuid" },
|
||||
"correlationId": { "type": "string" },
|
||||
"task": { "type": "string" }
|
||||
},
|
||||
"required": ["projectId", "agentId", "correlationId", "task"],
|
||||
"additionalProperties": false
|
||||
}),
|
||||
},
|
||||
ToolDef {
|
||||
name: "idea_project_cancel_agent",
|
||||
description: "Cancel/stop a cross-project contactable agent by projectId, agentId and correlationId.",
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"projectId": { "type": "string", "format": "uuid" },
|
||||
"agentId": { "type": "string", "format": "uuid" },
|
||||
"correlationId": { "type": "string" }
|
||||
},
|
||||
"required": ["projectId", "agentId", "correlationId"],
|
||||
"additionalProperties": false
|
||||
}),
|
||||
},
|
||||
ToolDef {
|
||||
name: "idea_list_agents",
|
||||
description: "List the IdeA agents declared in the project's manifest. Returns the \
|
||||
@ -762,6 +834,63 @@ mod tests {
|
||||
assert!(tool_returns_reply("idea_ask_agents"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_tools_are_classified_and_return_inline_replies() {
|
||||
for tool in [
|
||||
"idea_project_list",
|
||||
"idea_project_read_context",
|
||||
"idea_project_list_agents",
|
||||
] {
|
||||
assert_eq!(tool_access(tool), Some(McpToolAccess::Read));
|
||||
assert!(
|
||||
tool_returns_reply(tool),
|
||||
"{tool} must return inline JSON/text"
|
||||
);
|
||||
}
|
||||
for tool in ["idea_project_ask_agent", "idea_project_cancel_agent"] {
|
||||
assert_eq!(tool_access(tool), Some(McpToolAccess::WriteAction));
|
||||
assert!(
|
||||
tool_returns_reply(tool),
|
||||
"{tool} must return an inline result"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn catalogue_advertises_project_tools_with_stable_identity_fields() {
|
||||
let cat = catalogue();
|
||||
let by_name = |name: &str| {
|
||||
cat.iter()
|
||||
.find(|tool| tool.name == name)
|
||||
.unwrap_or_else(|| panic!("catalogue must contain {name}"))
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
by_name("idea_project_read_context").input_schema["required"],
|
||||
json!(["projectId"])
|
||||
);
|
||||
assert_eq!(
|
||||
by_name("idea_project_list_agents").input_schema["required"],
|
||||
json!(["projectId"])
|
||||
);
|
||||
assert_eq!(
|
||||
by_name("idea_project_ask_agent").input_schema["required"],
|
||||
json!(["projectId", "agentId", "correlationId", "task"])
|
||||
);
|
||||
assert_eq!(
|
||||
by_name("idea_project_cancel_agent").input_schema["required"],
|
||||
json!(["projectId", "agentId", "correlationId"])
|
||||
);
|
||||
assert_eq!(
|
||||
by_name("idea_project_ask_agent").input_schema["properties"]["projectId"]["format"],
|
||||
json!("uuid")
|
||||
);
|
||||
assert_eq!(
|
||||
by_name("idea_project_ask_agent").input_schema["properties"]["agentId"]["format"],
|
||||
json!("uuid")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_in_background_maps_to_background_command_with_handshake_owner() {
|
||||
let cmd = map_tool_call(
|
||||
|
||||
Reference in New Issue
Block a user