Skip to main content

pmcp_code_mode/
code_executor.rs

1//! High-level code execution trait for MCP servers.
2//!
3//! This module provides the [`CodeExecutor`] trait, which is the primary public API
4//! for implementing code execution in MCP servers. It replaces the internal
5//! `HttpExecutor`, `SdkExecutor`, and `McpExecutor` traits for external server
6//! developers (those traits remain available for advanced use behind the
7//! `js-runtime` feature flag).
8
9use crate::types::ExecutionError;
10
11/// High-level trait for executing validated code.
12///
13/// Implementations handle the execution of code that has already passed
14/// validation and token verification. This is the primary public API
15/// for code execution -- it replaces the internal `HttpExecutor`,
16/// `SdkExecutor`, and `McpExecutor` traits for external server developers.
17///
18/// # Execution Patterns
19///
20/// The four supported patterns (all implemented via this single trait):
21/// - **Pattern A (SQL):** Direct SQL execution, no JS runtime
22/// - **Pattern B (JS+HTTP):** JavaScript plan compiled and executed via HTTP calls
23/// - **Pattern C (JS+SDK):** JavaScript plan executed via AWS SDK calls
24/// - **Pattern D (JS+MCP):** JavaScript plan executed via MCP tool calls
25///
26/// # API Stability Note
27///
28/// **\[Addresses divergent review concern: CodeExecutor trait surface area\]**
29/// This v0.1.0 API uses a simple `(code, variables)` signature per D-04.
30/// A future v0.2.0 may add an `ExecutionContext` parameter carrying timeout,
31/// cancellation token, and request metadata. The `(code, variables)` signature
32/// will be preserved as a default-method wrapper for backward compatibility.
33///
34/// # Example
35///
36/// ```rust,ignore
37/// use pmcp_code_mode::{CodeExecutor, ExecutionError};
38/// use serde_json::Value;
39///
40/// struct MyExecutor { /* database pool, http client, etc. */ }
41///
42/// #[pmcp_code_mode::async_trait]
43/// impl CodeExecutor for MyExecutor {
44///     async fn execute(
45///         &self,
46///         code: &str,
47///         variables: Option<&Value>,
48///     ) -> Result<Value, ExecutionError> {
49///         // Execute validated code against your backend
50///         todo!()
51///     }
52/// }
53/// ```
54#[async_trait::async_trait]
55pub trait CodeExecutor: Send + Sync {
56    /// Execute validated code and return the result.
57    ///
58    /// `code` has already passed validation and token verification.
59    /// `variables` are optional user-provided parameters (e.g., GraphQL variables).
60    ///
61    /// Implementations should NOT re-verify the token -- that is handled
62    /// by the Code Mode framework before calling this method.
63    async fn execute(
64        &self,
65        code: &str,
66        variables: Option<&serde_json::Value>,
67    ) -> Result<serde_json::Value, ExecutionError>;
68}
69
70// ---------------------------------------------------------------------------
71// Standard adapters: bridge low-level executor traits to CodeExecutor
72// ---------------------------------------------------------------------------
73//
74// These adapters solve the &mut self vs &self mismatch: PlanExecutor::execute
75// requires &mut self, but CodeExecutor::execute takes &self. Each adapter
76// creates a fresh PlanCompiler + PlanExecutor per call (cheap — both are small
77// structs, and the caller's HttpExecutor/SdkExecutor holds Arc'd state).
78
79/// Compile JavaScript code and execute the plan, returning the result value.
80///
81/// Shared implementation for all three adapters. The `setup` closure configures
82/// the `PlanExecutor` with the appropriate backend (HTTP, SDK, or MCP) before
83/// execution begins.
84#[cfg(feature = "js-runtime")]
85async fn compile_and_execute<H: crate::executor::HttpExecutor + 'static>(
86    config: &crate::executor::ExecutionConfig,
87    http: H,
88    code: &str,
89    variables: Option<&serde_json::Value>,
90    setup: impl FnOnce(&mut crate::executor::PlanExecutor<H>),
91    adapter: &str,
92) -> Result<serde_json::Value, ExecutionError> {
93    let mut compiler = crate::executor::PlanCompiler::with_config(config);
94    let plan = compiler
95        .compile_code(code)
96        .map_err(|e| ExecutionError::RuntimeError {
97            message: format!("Compilation failed: {e}"),
98        })?;
99    let mut executor = crate::executor::PlanExecutor::new(http, config.clone());
100    if let Some(vars) = variables {
101        executor.set_variable("args", vars.clone());
102    }
103    setup(&mut executor);
104    let result = executor.execute(&plan).await?;
105    tracing::debug!(
106        adapter,
107        api_calls = result.api_calls.len(),
108        execution_time_ms = result.execution_time_ms,
109        "plan executed"
110    );
111    Ok(result.value)
112}
113
114/// Adapter bridging [`HttpExecutor`] to [`CodeExecutor`] for JavaScript/OpenAPI
115/// servers (Pattern B: JS+HTTP).
116///
117/// Compiles JavaScript code into an execution plan, then runs it against an
118/// HTTP backend. The executor holds its own [`ExecutionConfig`] for limits
119/// (`max_api_calls`, `timeout_seconds`, `max_loop_iterations`).
120///
121/// # Example
122///
123/// ```rust,ignore
124/// use pmcp_code_mode::{JsCodeExecutor, ExecutionConfig};
125///
126/// let http = CostExplorerHttpExecutor::new(clients.clone());
127/// let config = ExecutionConfig::default();
128/// let executor = Arc::new(JsCodeExecutor::new(http, config));
129/// // Pass executor to #[derive(CodeMode)] struct as code_executor field
130/// ```
131#[cfg(feature = "js-runtime")]
132pub struct JsCodeExecutor<H> {
133    http: H,
134    config: crate::executor::ExecutionConfig,
135}
136
137#[cfg(feature = "js-runtime")]
138impl<H: crate::executor::HttpExecutor + Clone> JsCodeExecutor<H> {
139    /// Create a new JS code executor with the given HTTP backend and config.
140    pub fn new(http: H, config: crate::executor::ExecutionConfig) -> Self {
141        Self { http, config }
142    }
143}
144
145#[cfg(feature = "js-runtime")]
146#[async_trait::async_trait]
147impl<H: crate::executor::HttpExecutor + Clone + 'static> CodeExecutor for JsCodeExecutor<H> {
148    async fn execute(
149        &self,
150        code: &str,
151        variables: Option<&serde_json::Value>,
152    ) -> Result<serde_json::Value, ExecutionError> {
153        compile_and_execute(
154            &self.config,
155            self.http.clone(),
156            code,
157            variables,
158            |_| {},
159            "js",
160        )
161        .await
162    }
163}
164
165/// Adapter bridging [`SdkExecutor`] to [`CodeExecutor`] for SDK-backed servers
166/// (Pattern C: JS+SDK).
167///
168/// Uses a no-op HTTP executor stub since SDK plans route through
169/// `PlanExecutor::set_sdk_executor` instead of HTTP calls.
170///
171/// # Example
172///
173/// ```rust,ignore
174/// use pmcp_code_mode::{SdkCodeExecutor, ExecutionConfig};
175///
176/// let sdk = MyCostExplorerSdk::new(credentials);
177/// let config = ExecutionConfig::default();
178/// let executor = Arc::new(SdkCodeExecutor::new(sdk, config));
179/// ```
180#[cfg(feature = "js-runtime")]
181pub struct SdkCodeExecutor<S> {
182    sdk: S,
183    config: crate::executor::ExecutionConfig,
184}
185
186#[cfg(feature = "js-runtime")]
187impl<S: crate::executor::SdkExecutor + Clone + 'static> SdkCodeExecutor<S> {
188    /// Create a new SDK code executor with the given SDK backend and config.
189    pub fn new(sdk: S, config: crate::executor::ExecutionConfig) -> Self {
190        Self { sdk, config }
191    }
192}
193
194#[cfg(feature = "js-runtime")]
195#[async_trait::async_trait]
196impl<S: crate::executor::SdkExecutor + Clone + 'static> CodeExecutor for SdkCodeExecutor<S> {
197    async fn execute(
198        &self,
199        code: &str,
200        variables: Option<&serde_json::Value>,
201    ) -> Result<serde_json::Value, ExecutionError> {
202        let sdk = self.sdk.clone();
203        compile_and_execute(
204            &self.config,
205            NoopHttpExecutor,
206            code,
207            variables,
208            move |ex| {
209                ex.set_sdk_executor(sdk);
210            },
211            "sdk",
212        )
213        .await
214    }
215}
216
217/// Adapter bridging [`McpExecutor`] to [`CodeExecutor`] for MCP composition
218/// servers (Pattern D: JS+MCP).
219///
220/// Uses a no-op HTTP executor stub since MCP plans route through
221/// `PlanExecutor::set_mcp_executor` instead of HTTP calls.
222///
223/// # Example
224///
225/// ```rust,ignore
226/// use pmcp_code_mode::{McpCodeExecutor, ExecutionConfig};
227///
228/// let mcp = MyMcpRouter::new(foundation_servers);
229/// let config = ExecutionConfig::default();
230/// let executor = Arc::new(McpCodeExecutor::new(mcp, config));
231/// ```
232///
233/// Note: `mcp-code-mode` feature implies `js-runtime` in Cargo.toml,
234/// which provides `PlanCompiler`, `PlanExecutor`, and `NoopHttpExecutor`.
235#[cfg(feature = "mcp-code-mode")]
236pub struct McpCodeExecutor<M> {
237    mcp: M,
238    config: crate::executor::ExecutionConfig,
239}
240
241#[cfg(feature = "mcp-code-mode")]
242impl<M: crate::executor::McpExecutor + Clone + 'static> McpCodeExecutor<M> {
243    /// Create a new MCP composition code executor.
244    pub fn new(mcp: M, config: crate::executor::ExecutionConfig) -> Self {
245        Self { mcp, config }
246    }
247}
248
249#[cfg(feature = "mcp-code-mode")]
250#[async_trait::async_trait]
251impl<M: crate::executor::McpExecutor + Clone + 'static> CodeExecutor for McpCodeExecutor<M> {
252    async fn execute(
253        &self,
254        code: &str,
255        variables: Option<&serde_json::Value>,
256    ) -> Result<serde_json::Value, ExecutionError> {
257        let mcp = self.mcp.clone();
258        compile_and_execute(
259            &self.config,
260            NoopHttpExecutor,
261            code,
262            variables,
263            move |ex| {
264                ex.set_mcp_executor(mcp);
265            },
266            "mcp",
267        )
268        .await
269    }
270}
271
272/// No-op HTTP executor for SDK and MCP adapters that don't use HTTP calls.
273/// Gated on `js-runtime`; `mcp-code-mode` implies `js-runtime` in Cargo.toml.
274#[cfg(feature = "js-runtime")]
275#[derive(Clone)]
276struct NoopHttpExecutor;
277
278#[cfg(feature = "js-runtime")]
279#[async_trait::async_trait]
280impl crate::executor::HttpExecutor for NoopHttpExecutor {
281    async fn execute_request(
282        &self,
283        method: &str,
284        path: &str,
285        _body: Option<serde_json::Value>,
286    ) -> Result<serde_json::Value, ExecutionError> {
287        Err(ExecutionError::RuntimeError {
288            message: format!(
289                "HTTP calls not supported in this executor mode (attempted {method} {path}). \
290                 Use JsCodeExecutor for HTTP-based execution."
291            ),
292        })
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299    use serde_json::json;
300
301    struct EchoExecutor;
302
303    #[async_trait::async_trait]
304    impl CodeExecutor for EchoExecutor {
305        async fn execute(
306            &self,
307            code: &str,
308            variables: Option<&serde_json::Value>,
309        ) -> Result<serde_json::Value, ExecutionError> {
310            Ok(json!({
311                "code": code,
312                "variables": variables,
313            }))
314        }
315    }
316
317    #[tokio::test]
318    async fn code_executor_echo() {
319        let executor = EchoExecutor;
320        let result = executor.execute("SELECT 1", None).await.unwrap();
321        assert_eq!(result["code"], "SELECT 1");
322    }
323
324    #[tokio::test]
325    async fn code_executor_with_variables() {
326        let executor = EchoExecutor;
327        let vars = json!({"limit": 10});
328        let result = executor
329            .execute("query { users }", Some(&vars))
330            .await
331            .unwrap();
332        assert_eq!(result["variables"]["limit"], 10);
333    }
334
335    #[tokio::test]
336    async fn code_executor_returns_error() {
337        struct FailingExecutor;
338
339        #[async_trait::async_trait]
340        impl CodeExecutor for FailingExecutor {
341            async fn execute(
342                &self,
343                _code: &str,
344                _variables: Option<&serde_json::Value>,
345            ) -> Result<serde_json::Value, ExecutionError> {
346                Err(ExecutionError::BackendError(
347                    "database unavailable".to_string(),
348                ))
349            }
350        }
351
352        let executor = FailingExecutor;
353        let result = executor.execute("SELECT 1", None).await;
354        assert!(result.is_err());
355        let err = result.unwrap_err();
356        assert!(err.to_string().contains("database unavailable"));
357    }
358
359    // Compile-time test: CodeExecutor requires Send + Sync
360    fn _assert_send_sync<T: Send + Sync>() {}
361    fn _code_executor_is_send_sync() {
362        _assert_send_sync::<EchoExecutor>();
363    }
364}