Skip to main content

perspt_agent/tools/handlers/
external.rs

1//! The external (MCP) dispatcher — the handler registry's fallback.
2//!
3//! Namespaced external names (`mcp.<server>.<tool>`) are discovered at
4//! runtime, so they dispatch through one fallback handler instead of exact
5//! registrations. Admission already happened (`admit_external_tool` against
6//! the session's derived grant surface); the loop brackets every call in
7//! the external-effect log because admitted entries are `durable`; and the
8//! result enters the conversation as an untrusted observation. Replay never
9//! re-invokes the server: recorded outputs come back from the ledger.
10
11use std::sync::Arc;
12
13use anyhow::Result;
14
15use crate::candidate::CandidateWorkspace;
16use crate::external_tools::ExternalToolRuntime;
17use crate::toolloop::EffectOutcome;
18
19use super::CandidateToolHandler;
20
21pub struct ExternalDispatcher {
22    runtime: Arc<tokio::sync::Mutex<ExternalToolRuntime>>,
23}
24
25impl ExternalDispatcher {
26    pub fn new(runtime: Arc<tokio::sync::Mutex<ExternalToolRuntime>>) -> Self {
27        Self { runtime }
28    }
29}
30
31#[async_trait::async_trait]
32impl CandidateToolHandler for ExternalDispatcher {
33    async fn apply(
34        &self,
35        _workspace: &CandidateWorkspace,
36        call: &perspt_sdk::ProviderToolCall,
37        _entry: &perspt_sdk::ToolEntry,
38    ) -> Result<EffectOutcome> {
39        let mut runtime = self.runtime.lock().await;
40        match runtime.call(&call.name, call.arguments.clone()).await {
41            Ok(result) => Ok(EffectOutcome {
42                output: format!(
43                    "[untrusted MCP result{}] {}",
44                    if result.is_error { ", tool error" } else { "" },
45                    result.content
46                ),
47                mutated: false,
48                completed: true,
49            }),
50            // Uncertain completion is evidence for the model, not a loop
51            // abort: the bracket stays open in the ledger for reconciliation.
52            Err(error) => Ok(EffectOutcome {
53                output: format!("tool failed: {error:#}"),
54                mutated: false,
55                completed: false,
56            }),
57        }
58    }
59}