soma_gateway/gateway/manager/
mcp_routes.rs1use serde_json::Value;
2pub use soma_mcp_proxy::{
3 parse_upstream_resource_uri, upstream_resource_uri, McpPromptRoute as GatewayPromptRoute,
4 McpResourceRoute as GatewayResourceRoute, McpToolRoute as GatewayToolRoute,
5};
6
7use crate::upstream::{PromptDescriptor, ResourceDescriptor, ToolDescriptor, UpstreamHealth};
8
9use super::{GatewayManager, GatewayManagerError};
10
11impl GatewayManager {
12 pub async fn tool_routes(&self) -> Result<Vec<GatewayToolRoute>, GatewayManagerError> {
13 self.tool_routes_for_subject(None).await
14 }
15
16 pub async fn tool_routes_for_subject(
17 &self,
18 subject: Option<&str>,
19 ) -> Result<Vec<GatewayToolRoute>, GatewayManagerError> {
20 let pool = self.ready_pool()?;
21 let snapshots = discover_snapshots(&pool, subject).await?;
22 let mut candidates = Vec::new();
23 for snapshot in snapshots {
24 if !matches!(snapshot.health, UpstreamHealth::Connected) {
25 continue;
26 }
27 for descriptor in exposed_tools(&pool, &snapshot.name, subject).await? {
28 candidates.push((snapshot.name.clone(), descriptor));
29 }
30 }
31 Ok(soma_mcp_proxy::tool_routes_from_candidates(
32 candidates,
33 std::iter::empty::<&str>(),
34 ))
35 }
36
37 pub async fn call_mcp_tool(
38 &self,
39 name: &str,
40 params: Value,
41 ) -> Result<Option<Value>, GatewayManagerError> {
42 self.call_mcp_tool_for_subject(name, params, None).await
43 }
44
45 pub async fn call_mcp_tool_for_subject(
46 &self,
47 name: &str,
48 params: Value,
49 subject: Option<&str>,
50 ) -> Result<Option<Value>, GatewayManagerError> {
51 let Some(route) = self
52 .tool_routes_for_subject(subject)
53 .await?
54 .into_iter()
55 .find(|route| route.name == name)
56 else {
57 return Ok(None);
58 };
59 let pool = self.ready_pool()?;
60 call_tool(&pool, route, params, subject)
61 .await
62 .map(Some)
63 .map_err(Into::into)
64 }
65
66 pub async fn resource_routes(&self) -> Result<Vec<GatewayResourceRoute>, GatewayManagerError> {
67 self.resource_routes_for_subject(None).await
68 }
69
70 pub async fn resource_routes_for_subject(
71 &self,
72 subject: Option<&str>,
73 ) -> Result<Vec<GatewayResourceRoute>, GatewayManagerError> {
74 let pool = self.ready_pool()?;
75 let snapshots = discover_snapshots(&pool, subject).await?;
76 let mut routes = Vec::new();
77 for snapshot in snapshots {
78 if !matches!(snapshot.health, UpstreamHealth::Connected) {
79 continue;
80 }
81 for descriptor in list_resources(&pool, &snapshot.name, subject).await? {
82 routes.push(soma_mcp_proxy::resource_route(&snapshot.name, descriptor));
83 }
84 }
85 Ok(routes)
86 }
87
88 pub async fn read_mcp_resource(&self, uri: &str) -> Result<Option<Value>, GatewayManagerError> {
89 self.read_mcp_resource_for_subject(uri, None).await
90 }
91
92 pub async fn read_mcp_resource_for_subject(
93 &self,
94 uri: &str,
95 subject: Option<&str>,
96 ) -> Result<Option<Value>, GatewayManagerError> {
97 let Some((upstream, native_uri)) = parse_upstream_resource_uri(uri) else {
98 return Ok(None);
99 };
100 let pool = self.ready_pool()?;
101 read_resource(&pool, &upstream, &native_uri, subject)
102 .await
103 .map(Some)
104 .map_err(Into::into)
105 }
106
107 pub async fn prompt_routes(&self) -> Result<Vec<GatewayPromptRoute>, GatewayManagerError> {
108 self.prompt_routes_for_subject(None).await
109 }
110
111 pub async fn prompt_routes_for_subject(
112 &self,
113 subject: Option<&str>,
114 ) -> Result<Vec<GatewayPromptRoute>, GatewayManagerError> {
115 let pool = self.ready_pool()?;
116 let snapshots = discover_snapshots(&pool, subject).await?;
117 let mut candidates = Vec::new();
118 for snapshot in snapshots {
119 if !matches!(snapshot.health, UpstreamHealth::Connected) {
120 continue;
121 }
122 for descriptor in list_prompts(&pool, &snapshot.name, subject).await? {
123 candidates.push((snapshot.name.clone(), descriptor));
124 }
125 }
126 Ok(soma_mcp_proxy::prompt_routes_from_candidates(candidates))
127 }
128
129 pub async fn get_mcp_prompt(
130 &self,
131 name: &str,
132 arguments: Option<serde_json::Map<String, Value>>,
133 ) -> Result<Option<Value>, GatewayManagerError> {
134 self.get_mcp_prompt_for_subject(name, arguments, None).await
135 }
136
137 pub async fn get_mcp_prompt_for_subject(
138 &self,
139 name: &str,
140 arguments: Option<serde_json::Map<String, Value>>,
141 subject: Option<&str>,
142 ) -> Result<Option<Value>, GatewayManagerError> {
143 let Some(route) = self
144 .prompt_routes_for_subject(subject)
145 .await?
146 .into_iter()
147 .find(|route| route.name == name)
148 else {
149 return Ok(None);
150 };
151 let pool = self.ready_pool()?;
152 get_prompt(
153 &pool,
154 &route.upstream,
155 &route.native_name,
156 arguments,
157 subject,
158 )
159 .await
160 .map(Some)
161 .map_err(Into::into)
162 }
163
164 fn ready_pool(
165 &self,
166 ) -> Result<std::sync::Arc<crate::upstream::pool::UpstreamPool>, GatewayManagerError> {
167 self.ensure_ready()?;
168 Ok(self.pool.read().expect("gateway pool poisoned").clone())
169 }
170}
171
172async fn discover_snapshots(
173 pool: &crate::upstream::pool::UpstreamPool,
174 subject: Option<&str>,
175) -> Result<Vec<crate::upstream::UpstreamSnapshot>, crate::upstream::UpstreamError> {
176 let _ = subject;
177 #[cfg(feature = "oauth")]
178 if subject.is_some() {
179 return pool.discover_for_subject(subject).await;
180 }
181 pool.discover().await
182}
183
184async fn exposed_tools(
185 pool: &crate::upstream::pool::UpstreamPool,
186 upstream: &str,
187 subject: Option<&str>,
188) -> Result<Vec<ToolDescriptor>, crate::upstream::UpstreamError> {
189 let _ = subject;
190 #[cfg(feature = "oauth")]
191 if subject.is_some() {
192 return pool.exposed_tools_for_subject(upstream, subject).await;
193 }
194 pool.exposed_tools(upstream)
195}
196
197async fn call_tool(
198 pool: &crate::upstream::pool::UpstreamPool,
199 route: GatewayToolRoute,
200 params: Value,
201 subject: Option<&str>,
202) -> Result<Value, crate::upstream::UpstreamError> {
203 let _ = subject;
204 let call = crate::upstream::pool::ToolCall {
205 upstream: route.upstream,
206 tool: route.native_name,
207 params,
208 };
209 #[cfg(feature = "oauth")]
210 if subject.is_some() {
211 return pool.call_tool_for_subject(call, subject).await;
212 }
213 pool.call_tool(call).await
214}
215
216async fn list_resources(
217 pool: &crate::upstream::pool::UpstreamPool,
218 upstream: &str,
219 subject: Option<&str>,
220) -> Result<Vec<ResourceDescriptor>, crate::upstream::UpstreamError> {
221 let _ = subject;
222 #[cfg(feature = "oauth")]
223 if subject.is_some() {
224 return pool.list_resources_for_subject(upstream, subject).await;
225 }
226 pool.list_resources(upstream).await
227}
228
229async fn read_resource(
230 pool: &crate::upstream::pool::UpstreamPool,
231 upstream: &str,
232 uri: &str,
233 subject: Option<&str>,
234) -> Result<Value, crate::upstream::UpstreamError> {
235 let _ = subject;
236 #[cfg(feature = "oauth")]
237 if subject.is_some() {
238 return pool.read_resource_for_subject(upstream, uri, subject).await;
239 }
240 pool.read_resource(upstream, uri).await
241}
242
243async fn list_prompts(
244 pool: &crate::upstream::pool::UpstreamPool,
245 upstream: &str,
246 subject: Option<&str>,
247) -> Result<Vec<PromptDescriptor>, crate::upstream::UpstreamError> {
248 let _ = subject;
249 #[cfg(feature = "oauth")]
250 if subject.is_some() {
251 return pool.list_prompts_for_subject(upstream, subject).await;
252 }
253 pool.list_prompts(upstream).await
254}
255
256async fn get_prompt(
257 pool: &crate::upstream::pool::UpstreamPool,
258 upstream: &str,
259 name: &str,
260 arguments: Option<serde_json::Map<String, Value>>,
261 subject: Option<&str>,
262) -> Result<Value, crate::upstream::UpstreamError> {
263 let _ = subject;
264 #[cfg(feature = "oauth")]
265 if subject.is_some() {
266 return pool
267 .get_prompt_for_subject(upstream, name, arguments, subject)
268 .await;
269 }
270 pool.get_prompt(upstream, name, arguments).await
271}
272
273#[cfg(test)]
274#[path = "mcp_routes_tests.rs"]
275mod tests;