feat(agent): bind transport S-MCP — outils idea_* vivants de bout en bout (M5a-e) — §14.3.1
Dernier kilomètre de l'orchestration native : une CLI MCP réellement lancée
joint le serveur MCP du projet et ses outils idea_* aboutissent au vrai dispatch.
- M5a endpoint loopback par projet (interprocess UDS/named pipe, source unique
mcp_endpoint, cleanup au close) — zéro port réseau, AppImage/SSH-safe.
- M5b sous-commande `idea mcp-server` : pont stdio↔loopback headless (avant init
Tauri), handshake {"project","requester"}, endpoint-absent borné, EOF propre.
- M5c McpServerHandle boucle accept + serve_peer par pair ; requester réel
propagé jusqu'à OrchestratorRequestProcessed (fin du "mcp" figé) ; isolation
des pairs ; terminaison propre.
- M5d apply_mcp_config écrit la déclaration réelle (current_exe + --endpoint
mcp_endpoint + --project simple-uuid + --requester) ; McpRuntime injecté comme
donnée (application ne dépend pas de app-tauri).
- M5e smoke e2e sur vrai loopback : list/ask inline, cible PTY → erreur typée,
JSON malformé → pas de panic, requester propagé.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -47,6 +47,12 @@ pub struct McpServer {
|
||||
/// delegation arrived through the MCP door. `None` ⇒ no publication (the server
|
||||
/// still works), so existing call sites stay valid.
|
||||
events: Option<Arc<dyn Fn(DomainEvent) + Send + Sync>>,
|
||||
/// Identity of the connected peer — the real agent id carried in the loopback
|
||||
/// handshake (cadrage v5 §1.4). When non-empty it becomes
|
||||
/// [`DomainEvent::OrchestratorRequestProcessed`]'s `requester_id` instead of the
|
||||
/// frozen `"mcp"` placeholder; empty ⇒ the legacy `"mcp"` label (back-compat for
|
||||
/// M2 callers and connections that arrive without a requester).
|
||||
requester: String,
|
||||
}
|
||||
|
||||
impl McpServer {
|
||||
@ -59,6 +65,7 @@ impl McpServer {
|
||||
service,
|
||||
project,
|
||||
events: None,
|
||||
requester: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,6 +79,35 @@ impl McpServer {
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns a per-connection clone of this server tagged with the connected
|
||||
/// peer's `requester` id (the loopback handshake's `requester`, cadrage v5 §1.4).
|
||||
///
|
||||
/// The base server (one per project, M3) is shared by every connection; each
|
||||
/// accepted peer derives its own contextualized server so concurrent peers never
|
||||
/// share a requester. An empty `requester` keeps the legacy `"mcp"` label.
|
||||
///
|
||||
/// Cheap: only `Arc`s, a `Project` clone, and the requester `String` are copied.
|
||||
#[must_use]
|
||||
pub fn for_requester(&self, requester: impl Into<String>) -> Self {
|
||||
Self {
|
||||
service: Arc::clone(&self.service),
|
||||
project: self.project.clone(),
|
||||
events: self.events.clone(),
|
||||
requester: requester.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Serves JSON-RPC messages from `transport`, tagging every processed
|
||||
/// `tools/call` with `requester` as the delegating agent (cadrage v5 §1.4).
|
||||
///
|
||||
/// A thin wrapper over [`serve`](Self::serve) on a per-connection
|
||||
/// [`for_requester`](Self::for_requester) clone: the caller (the per-peer accept
|
||||
/// loop) need not mutate the shared project server. An empty `requester` yields
|
||||
/// the legacy `"mcp"` label.
|
||||
pub async fn serve_as<T: Transport>(&self, requester: impl Into<String>, transport: &mut T) {
|
||||
self.for_requester(requester).serve(transport).await;
|
||||
}
|
||||
|
||||
/// Serves JSON-RPC messages from `transport` until it closes (clean EOF).
|
||||
///
|
||||
/// Every inbound line is handled in isolation: a malformed line or an unknown
|
||||
@ -211,12 +247,18 @@ impl McpServer {
|
||||
|
||||
/// Publishes [`DomainEvent::OrchestratorRequestProcessed`] for a handled
|
||||
/// `tools/call`, tagged [`OrchestrationSource::Mcp`]. The requester id is the
|
||||
/// stable `"mcp"` label until the per-session transport bind (open point S-MCP)
|
||||
/// carries the connected agent's identity. No-op without an event sink.
|
||||
/// connected peer's real agent id (carried in the loopback handshake, cadrage v5
|
||||
/// §1.4), falling back to the legacy `"mcp"` label when no identity was supplied.
|
||||
/// No-op without an event sink.
|
||||
fn publish_processed(&self, action: &str, ok: bool) {
|
||||
if let Some(publish) = &self.events {
|
||||
let requester_id = if self.requester.is_empty() {
|
||||
"mcp".to_owned()
|
||||
} else {
|
||||
self.requester.clone()
|
||||
};
|
||||
publish(DomainEvent::OrchestratorRequestProcessed {
|
||||
requester_id: "mcp".to_owned(),
|
||||
requester_id,
|
||||
action: action.to_owned(),
|
||||
ok,
|
||||
source: OrchestrationSource::Mcp,
|
||||
|
||||
Reference in New Issue
Block a user