feat(infrastructure,backend,app-tauri,frontend): tools MCP workspace natifs read-only idea_workspace_map/list/read/search, activés par défaut et configurables dans les permissions MCP et les templates — #341 (QA verte)

This commit is contained in:
2026-09-12 19:17:09 +02:00
parent ea42d95f2e
commit 7b712f2b62
16 changed files with 1503 additions and 80 deletions

View File

@ -1,15 +1,14 @@
//! [`McpServer`] — the IdeA MCP **driving adapter** (cadrage Décision 4).
//!
//! This is the **pair of [`FsOrchestratorWatcher`](super::super::FsOrchestratorWatcher)**:
//! another entry door onto the *same* [`OrchestratorService::dispatch`]. Where the
//! watcher reads a JSON file, this server reads a JSON-RPC `tools/call`; both build
//! an [`OrchestratorCommand`] and return its [`OrchestratorOutcome`] unchanged. It
//! invents **no semantics** and **never re-routes** replies.
//! another entry door onto the *same* [`OrchestratorService::dispatch`] for
//! orchestration commands. Capability-specific tools are handled by injected
//! providers without being disguised as [`OrchestratorCommand`] variants.
//!
//! It implements the strict MCP minimum over JSON-RPC 2.0:
//! - `initialize` → advertise protocol version + tool capability,
//! - `tools/list` → the [`tools::catalogue`],
//! - `tools/call` → map → `dispatch` → MCP tool result,
//! - `tools/call` → dedicated provider or orchestration dispatch → MCP tool result,
//! - notifications (e.g. `notifications/initialized`) → accepted, no reply.
//!
//! It receives `Arc<OrchestratorService>` and the target [`Project`] by injection
@ -40,13 +39,14 @@ use super::policy::ToolPolicyRegistry;
use super::templates::TemplateToolProvider;
use super::tickets::TicketToolProvider;
use super::tools::{self, ToolMapError};
use super::workspace::WorkspaceToolProvider;
/// The MCP protocol version this server speaks (advertised on `initialize`).
const MCP_PROTOCOL_VERSION: &str = "2024-11-05";
/// Compact MCP instructions advertised at initialization. The first 512 chars are
/// intentionally self-contained because some clients truncate this field.
const MCP_INSTRUCTIONS: &str = "Use IdeA tools by intent: ticket work -> idea_ticket_*; ask/delegate to one agent -> idea_ask_agent, several -> idea_ask_agents; durable project facts -> idea_memory_*; project/agent context -> idea_context_*; recurring project maintenance -> idea_scheduled_task_* (never system cron). Then use idea_skill_* for assigned workflows, idea_template_* for agent templates, idea_workstate_* for live status, idea_run_in_background/background tools for long commands, agent lifecycle tools to launch/stop/resume/swap agents, and sprint tools for planning.";
const MCP_INSTRUCTIONS: &str = "Use IdeA tools by intent: explore project files -> idea_workspace_*; ticket work -> idea_ticket_*; ask/delegate to one agent -> idea_ask_agent, several -> idea_ask_agents; durable project facts -> idea_memory_*; project/agent context -> idea_context_*; recurring project maintenance -> idea_scheduled_task_* (never system cron). Then use idea_skill_* for assigned workflows, idea_template_* for agent templates, idea_workstate_* for live status, idea_run_in_background/background tools for long commands, agent lifecycle tools to launch/stop/resume/swap agents, and sprint tools for planning.";
/// The IdeA MCP server: an entry adapter over [`OrchestratorService::dispatch`].
///
@ -87,6 +87,8 @@ pub struct McpServer {
managed_temp_tools: Option<Arc<dyn ManagedTempToolProvider>>,
/// Optional public plugin administration provider.
plugin_tools: Option<Arc<dyn PluginToolProvider>>,
/// Optional read-only project workspace provider.
workspace_tools: Option<Arc<dyn WorkspaceToolProvider>>,
/// Optional per-requester MCP tool policy registry for constrained sessions.
tool_policies: Option<Arc<ToolPolicyRegistry>>,
/// Optional durable MCP tool permission store. When wired, absence of project or
@ -110,6 +112,7 @@ impl McpServer {
template_tools: None,
managed_temp_tools: None,
plugin_tools: None,
workspace_tools: None,
tool_policies: None,
mcp_tool_permissions: None,
}
@ -167,6 +170,13 @@ impl McpServer {
self
}
/// Attaches the read-only project workspace provider.
#[must_use]
pub fn with_workspace_tools(mut self, workspace_tools: Arc<dyn WorkspaceToolProvider>) -> Self {
self.workspace_tools = Some(workspace_tools);
self
}
/// Attaches the requester-scoped MCP tool policy registry.
#[must_use]
pub fn with_tool_policies(mut self, tool_policies: Arc<ToolPolicyRegistry>) -> Self {
@ -204,6 +214,7 @@ impl McpServer {
template_tools: self.template_tools.clone(),
managed_temp_tools: self.managed_temp_tools.clone(),
plugin_tools: self.plugin_tools.clone(),
workspace_tools: self.workspace_tools.clone(),
tool_policies: self.tool_policies.clone(),
mcp_tool_permissions: self.mcp_tool_permissions.clone(),
}
@ -454,9 +465,8 @@ impl McpServer {
))
}
/// `tools/call`: map the tool to an [`OrchestratorCommand`], `dispatch` it, and
/// fold the [`OrchestratorOutcome`](application::OrchestratorOutcome) into an MCP
/// tool result.
/// `tools/call`: route provider-backed tools directly, or map orchestration
/// tools to an [`OrchestratorCommand`] and dispatch them.
async fn tools_call(&self, params: Value) -> Result<Value, JsonRpcError> {
let name = params
.get("name")
@ -682,6 +692,32 @@ impl McpServer {
};
}
if tools::is_workspace_tool(&name) {
let result = match &self.workspace_tools {
Some(provider) => {
provider
.handle_workspace_tool(&self.project, &self.requester, &name, arguments)
.await
}
None => Err(super::workspace::WorkspaceToolError::new(
"notConfigured",
"workspace tools are not configured",
)),
};
self.publish_processed(&name, result.is_ok());
return match result {
Ok(value) => {
let text = serde_json::to_string(&value).unwrap_or_else(|_| "null".to_owned());
Ok(tool_result_text(&text, false))
}
Err(err) => {
let text =
serde_json::to_string(&err.to_value()).unwrap_or_else(|_| err.to_string());
Ok(tool_result_text(&text, true))
}
};
}
// The handshake-provided requester is still passed to the mapper for tools that
// need peer identity.
let command = match tools::map_tool_call(&name, &arguments, &self.requester) {