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}