Skip to main content

soma_application/
ports.rs

1use std::sync::Arc;
2
3use async_trait::async_trait;
4use serde_json::Value;
5
6use crate::{
7    CodeModeExecuteRequest, ExecutionContext, GatewayExecuteRequest, GatewayPromptRoute,
8    GatewayReloadRequest, GatewayResourceRoute, GatewayRouteScope, GatewayToolRoute,
9    OpenApiExecuteRequest,
10};
11
12/// Error returned by an engine port when an operation cannot be completed.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct PortError {
15    /// Stable, machine-readable error code.
16    pub code: String,
17    /// Human-readable description of the failure.
18    pub message: String,
19    /// Whether retrying the operation might succeed.
20    pub retryable: bool,
21    /// Suggested remediation the caller can act on.
22    pub remediation: String,
23}
24
25impl PortError {
26    /// Builds a `PortError` from a code and message, defaulting to
27    /// non-retryable with a generic remediation hint.
28    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
29        Self {
30            code: code.into(),
31            message: message.into(),
32            retryable: false,
33            remediation: "Check the engine configuration and retry.".to_owned(),
34        }
35    }
36}
37
38/// Port to the MCP gateway engine: status, reload, execution, and
39/// tool/resource/prompt routing.
40#[async_trait]
41pub trait GatewayPort: Send + Sync {
42    /// Returns the gateway's current status snapshot.
43    async fn status(&self, context: &ExecutionContext) -> Result<Value, PortError>;
44    /// Reloads the gateway configuration.
45    async fn reload(
46        &self,
47        request: GatewayReloadRequest,
48        context: &ExecutionContext,
49    ) -> Result<Value, PortError>;
50    /// Executes a gateway operation.
51    async fn execute(
52        &self,
53        request: GatewayExecuteRequest,
54        context: &ExecutionContext,
55    ) -> Result<Value, PortError>;
56
57    /// Lists MCP tool routes exposed through the gateway, optionally scoped.
58    async fn list_mcp_tools(
59        &self,
60        scope: Option<&GatewayRouteScope>,
61        context: &ExecutionContext,
62    ) -> Result<Vec<GatewayToolRoute>, PortError>;
63
64    /// Calls an MCP tool by name through the gateway.
65    async fn call_mcp_tool(
66        &self,
67        name: &str,
68        params: Value,
69        scope: Option<&GatewayRouteScope>,
70        context: &ExecutionContext,
71    ) -> Result<Option<Value>, PortError>;
72
73    /// Lists MCP resource routes exposed through the gateway, optionally scoped.
74    async fn list_mcp_resources(
75        &self,
76        scope: Option<&GatewayRouteScope>,
77        context: &ExecutionContext,
78    ) -> Result<Vec<GatewayResourceRoute>, PortError>;
79
80    /// Reads an MCP resource by URI through the gateway.
81    async fn read_mcp_resource(
82        &self,
83        uri: &str,
84        scope: Option<&GatewayRouteScope>,
85        context: &ExecutionContext,
86    ) -> Result<Option<Value>, PortError>;
87
88    /// Lists MCP prompt routes exposed through the gateway, optionally scoped.
89    async fn list_mcp_prompts(
90        &self,
91        scope: Option<&GatewayRouteScope>,
92        context: &ExecutionContext,
93    ) -> Result<Vec<GatewayPromptRoute>, PortError>;
94
95    /// Gets an MCP prompt by name, with optional arguments, through the gateway.
96    async fn get_mcp_prompt(
97        &self,
98        name: &str,
99        arguments: Option<serde_json::Map<String, Value>>,
100        scope: Option<&GatewayRouteScope>,
101        context: &ExecutionContext,
102    ) -> Result<Option<Value>, PortError>;
103}
104
105/// Port to the Code Mode engine that runs sandboxed JavaScript against the
106/// tool catalog.
107#[async_trait]
108pub trait CodeModePort: Send + Sync {
109    /// Executes a Code Mode request.
110    async fn execute(
111        &self,
112        request: CodeModeExecuteRequest,
113        context: &ExecutionContext,
114    ) -> Result<Value, PortError>;
115}
116
117/// Port to the OpenAPI engine that dispatches requests against described APIs.
118#[async_trait]
119pub trait OpenApiPort: Send + Sync {
120    /// Executes an OpenAPI request.
121    async fn execute(
122        &self,
123        request: OpenApiExecuteRequest,
124        context: &ExecutionContext,
125    ) -> Result<Value, PortError>;
126}
127
128/// Bundle of the engine ports the application depends on.
129pub struct ApplicationPorts {
130    /// MCP gateway engine port.
131    pub gateway: Arc<dyn GatewayPort>,
132    /// Code Mode engine port.
133    pub codemode: Arc<dyn CodeModePort>,
134    /// OpenAPI engine port.
135    pub openapi: Arc<dyn OpenApiPort>,
136}
137
138impl ApplicationPorts {
139    /// Builds a port bundle where every engine reports itself as unavailable.
140    pub fn unavailable() -> Self {
141        let port = Arc::new(UnavailableEnginePort);
142        Self {
143            gateway: port.clone(),
144            codemode: port.clone(),
145            openapi: port,
146        }
147    }
148
149    /// Replaces the gateway port and returns the updated bundle.
150    pub fn with_gateway(mut self, gateway: Arc<dyn GatewayPort>) -> Self {
151        self.gateway = gateway;
152        self
153    }
154
155    /// Replaces the Code Mode port and returns the updated bundle.
156    pub fn with_codemode(mut self, codemode: Arc<dyn CodeModePort>) -> Self {
157        self.codemode = codemode;
158        self
159    }
160
161    /// Replaces the OpenAPI port and returns the updated bundle.
162    pub fn with_openapi(mut self, openapi: Arc<dyn OpenApiPort>) -> Self {
163        self.openapi = openapi;
164        self
165    }
166}
167
168struct UnavailableEnginePort;
169
170impl UnavailableEnginePort {
171    fn error(engine: &str) -> PortError {
172        PortError::new(
173            "engine_unavailable",
174            format!("{engine} is not configured for this application instance"),
175        )
176    }
177}
178
179#[async_trait]
180impl GatewayPort for UnavailableEnginePort {
181    async fn status(&self, _context: &ExecutionContext) -> Result<Value, PortError> {
182        Err(Self::error("gateway"))
183    }
184
185    async fn reload(
186        &self,
187        _request: GatewayReloadRequest,
188        _context: &ExecutionContext,
189    ) -> Result<Value, PortError> {
190        Err(Self::error("gateway"))
191    }
192
193    async fn execute(
194        &self,
195        _request: GatewayExecuteRequest,
196        _context: &ExecutionContext,
197    ) -> Result<Value, PortError> {
198        Err(Self::error("gateway"))
199    }
200
201    async fn list_mcp_tools(
202        &self,
203        _scope: Option<&GatewayRouteScope>,
204        _context: &ExecutionContext,
205    ) -> Result<Vec<GatewayToolRoute>, PortError> {
206        Err(Self::error("gateway"))
207    }
208
209    async fn call_mcp_tool(
210        &self,
211        _name: &str,
212        _params: Value,
213        _scope: Option<&GatewayRouteScope>,
214        _context: &ExecutionContext,
215    ) -> Result<Option<Value>, PortError> {
216        Err(Self::error("gateway"))
217    }
218
219    async fn list_mcp_resources(
220        &self,
221        _scope: Option<&GatewayRouteScope>,
222        _context: &ExecutionContext,
223    ) -> Result<Vec<GatewayResourceRoute>, PortError> {
224        Err(Self::error("gateway"))
225    }
226
227    async fn read_mcp_resource(
228        &self,
229        _uri: &str,
230        _scope: Option<&GatewayRouteScope>,
231        _context: &ExecutionContext,
232    ) -> Result<Option<Value>, PortError> {
233        Err(Self::error("gateway"))
234    }
235
236    async fn list_mcp_prompts(
237        &self,
238        _scope: Option<&GatewayRouteScope>,
239        _context: &ExecutionContext,
240    ) -> Result<Vec<GatewayPromptRoute>, PortError> {
241        Err(Self::error("gateway"))
242    }
243
244    async fn get_mcp_prompt(
245        &self,
246        _name: &str,
247        _arguments: Option<serde_json::Map<String, Value>>,
248        _scope: Option<&GatewayRouteScope>,
249        _context: &ExecutionContext,
250    ) -> Result<Option<Value>, PortError> {
251        Err(Self::error("gateway"))
252    }
253}
254
255#[async_trait]
256impl CodeModePort for UnavailableEnginePort {
257    async fn execute(
258        &self,
259        _request: CodeModeExecuteRequest,
260        _context: &ExecutionContext,
261    ) -> Result<Value, PortError> {
262        Err(Self::error("Code Mode"))
263    }
264}
265
266#[async_trait]
267impl OpenApiPort for UnavailableEnginePort {
268    async fn execute(
269        &self,
270        _request: OpenApiExecuteRequest,
271        _context: &ExecutionContext,
272    ) -> Result<Value, PortError> {
273        Err(Self::error("OpenAPI"))
274    }
275}