1use rmcp::model::{
8 CallToolRequestParams, Content, GetPromptRequestParams, GetPromptResult, RawContent,
9 ReadResourceRequestParams, ResourceContents,
10};
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13
14use crate::compression::engine::{CompressionEngine, Tool};
15use crate::compression::CompressionLevel;
16use crate::config::topology::MCPConfig;
17use crate::server::backend::BackendServerConfig;
18use crate::server::connect::{connect_backend, ConnectedBackend};
19use crate::Error;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum ProxyTransformMode {
24 CompressedTools,
26 Cli,
28 JustBash,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum BackendConfigSource {
35 Command,
37 SingleServerJsonConfig,
39 MultiServerJsonConfig,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct CompressedServerConfig {
46 pub level: CompressionLevel,
47 pub server_name: Option<String>,
48 pub include_tools: Vec<String>,
49 pub exclude_tools: Vec<String>,
50 pub toonify: bool,
51 pub transform_mode: ProxyTransformMode,
52 pub config_source: BackendConfigSource,
53}
54
55impl Default for CompressedServerConfig {
56 fn default() -> Self {
57 Self {
58 level: CompressionLevel::default(),
59 server_name: None,
60 include_tools: Vec::new(),
61 exclude_tools: Vec::new(),
62 toonify: false,
63 transform_mode: ProxyTransformMode::CompressedTools,
64 config_source: BackendConfigSource::Command,
65 }
66 }
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70pub struct JustBashProviderSpec {
71 pub provider_name: String,
72 pub help_tool_name: String,
73 pub tools: Vec<JustBashCommandSpec>,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
77pub struct JustBashCommandSpec {
78 pub command_name: String,
79 pub backend_tool_name: String,
80 pub description: Option<String>,
81 pub input_schema: Value,
82 pub invoke_tool_name: String,
83}
84
85#[derive(Debug)]
87pub struct CompressedServer {
88 config: CompressedServerConfig,
89 backends: Vec<ConnectedBackend>,
90}
91
92impl CompressedServer {
93 pub async fn connect_stdio(
95 config: CompressedServerConfig,
96 backend: BackendServerConfig,
97 ) -> Result<Self, Error> {
98 let public_name = config
99 .server_name
100 .clone()
101 .unwrap_or_else(|| backend.name.clone());
102 let backend = connect_backend(
103 backend,
104 public_name,
105 &config.include_tools,
106 &config.exclude_tools,
107 )
108 .await?;
109 Ok(Self {
110 config,
111 backends: vec![backend],
112 })
113 }
114
115 pub async fn connect_multi_stdio(
117 config: CompressedServerConfig,
118 backends: Vec<BackendServerConfig>,
119 ) -> Result<Self, Error> {
120 let suite_prefix = config.server_name.clone();
121 let mut connected = Vec::with_capacity(backends.len());
122 for backend in backends {
123 let public_name = match &suite_prefix {
124 Some(prefix) => format!("{prefix}_{}", backend.name),
125 None => backend.name.clone(),
126 };
127 connected.push(
128 connect_backend(
129 backend,
130 public_name,
131 &config.include_tools,
132 &config.exclude_tools,
133 )
134 .await?,
135 );
136 }
137 Ok(Self {
138 config,
139 backends: connected,
140 })
141 }
142
143 pub async fn connect_mcp_config_json(
145 config: CompressedServerConfig,
146 mcp_config_json: &str,
147 ) -> Result<Self, Error> {
148 let mcp_config = MCPConfig::from_json(mcp_config_json)?;
149 let mut backends = Vec::new();
150 for name in mcp_config.server_names() {
151 let server = mcp_config
152 .server(&name)
153 .ok_or_else(|| Error::Config(format!("server not found: {name}")))?;
154 backends.push(
155 BackendServerConfig::new(name, server.command.clone(), server.args.clone())
156 .with_env(server.env.clone()),
157 );
158 }
159
160 if backends.len() == 1 {
161 let backend = backends.into_iter().next().expect("one backend exists");
162 let public_name = config.server_name.clone().unwrap_or_default();
163 let backend = connect_backend(
164 backend,
165 public_name,
166 &config.include_tools,
167 &config.exclude_tools,
168 )
169 .await?;
170 Ok(Self {
171 config,
172 backends: vec![backend],
173 })
174 } else {
175 Self::connect_multi_stdio(config, backends).await
176 }
177 }
178
179 pub async fn list_frontend_tools(&self) -> Result<Vec<Tool>, Error> {
181 if self.config.transform_mode == ProxyTransformMode::JustBash {
182 return Ok(self.just_bash_tools());
183 }
184 if self.config.transform_mode == ProxyTransformMode::Cli {
185 return Ok(self.cli_help_tools());
186 }
187 let mut tools = Vec::new();
188 for backend in &self.backends {
189 let prefix = self.wrapper_prefix(backend);
190 tools.push(wrapper_tool(
191 format!("{prefix}get_tool_schema"),
192 &self.get_tool_schema_description(backend),
193 ));
194 tools.push(wrapper_tool(
195 format!("{prefix}invoke_tool"),
196 &self.invoke_tool_description(backend),
197 ));
198 if self.config.level == CompressionLevel::Max {
199 tools.push(wrapper_tool(
200 format!("{prefix}list_tools"),
201 "List compressed backend tools.",
202 ));
203 }
204 }
205 Ok(tools)
206 }
207
208 fn get_tool_schema_description(&self, backend: &ConnectedBackend) -> String {
209 format!(
210 "Get the input schema for a specific tool from the {} toolset.\n\nAvailable tools are:\n{}",
211 backend.public_name,
212 self.frontend_tool_listing(backend)
213 )
214 }
215
216 fn invoke_tool_description(&self, backend: &ConnectedBackend) -> String {
217 format!(
218 "Invoke a tool from the {} toolset. Use get_tool_schema first when you need the full input schema.",
219 backend.public_name
220 )
221 }
222
223 fn frontend_tool_listing(&self, backend: &ConnectedBackend) -> String {
224 let listing = self.engine().format_listing(&backend.tools);
225 if listing.is_empty() {
226 backend
227 .tools
228 .iter()
229 .map(|tool| format!("<tool>{}</tool>", tool.name))
230 .collect::<Vec<_>>()
231 .join("\n")
232 } else {
233 listing
234 }
235 }
236
237 fn engine(&self) -> crate::compression::engine::CompressionEngine {
238 crate::compression::engine::CompressionEngine::new(self.config.level.clone())
239 }
240
241 pub fn compression_level(&self) -> &CompressionLevel {
243 &self.config.level
244 }
245
246 pub fn default_server_name(&self) -> Option<&str> {
247 self.config.server_name.as_deref().or_else(|| {
248 if self.backends.len() == 1 {
249 Some(self.backends[0].public_name.as_str())
250 } else {
251 None
252 }
253 })
254 }
255
256 pub fn backend_tools(&self) -> Vec<Tool> {
258 self.backends
259 .iter()
260 .flat_map(|backend| backend.tools.iter().cloned())
261 .collect()
262 }
263
264 pub fn backend_tools_by_server(&self) -> Vec<(String, Tool)> {
266 self.backends
267 .iter()
268 .flat_map(|backend| {
269 backend
270 .tools
271 .iter()
272 .cloned()
273 .map(|tool| (backend.public_name.clone(), tool))
274 })
275 .collect()
276 }
277
278 pub async fn get_tool_schema(
280 &self,
281 _wrapper_tool_name: &str,
282 backend_tool_name: &str,
283 ) -> Result<String, Error> {
284 let backend = self.backend_for_wrapper(_wrapper_tool_name)?;
285 let tool = backend
286 .tools
287 .iter()
288 .find(|tool| tool.name == backend_tool_name)
289 .ok_or_else(|| Error::ToolNotFound(backend_tool_name.to_string()))?;
290 Ok(CompressionEngine::format_schema_response(tool))
291 }
292
293 pub async fn list_backend_tools(&self, wrapper_tool_name: &str) -> Result<String, Error> {
295 let backend = self.backend_for_wrapper(wrapper_tool_name)?;
296 let engine = CompressionEngine::new(CompressionLevel::High);
297 Ok(engine
298 .format_listing(&backend.tools)
299 .lines()
300 .collect::<Vec<_>>()
301 .join("\n"))
302 }
303
304 pub async fn invoke_tool(
306 &self,
307 _wrapper_tool_name: &str,
308 backend_tool_name: &str,
309 tool_input: Value,
310 ) -> Result<String, Error> {
311 let backend = self.backend_for_wrapper(_wrapper_tool_name)?;
312 self.invoke_backend(backend, backend_tool_name, tool_input)
313 .await
314 }
315
316 pub async fn list_resources(&self) -> Result<Vec<String>, Error> {
319 let mut resources = Vec::new();
320 for backend in &self.backends {
321 resources.extend(backend.resources.clone());
322 resources.push(format!(
323 "compressor://{}/uncompressed-tools",
324 backend.public_name
325 ));
326 }
327 Ok(resources)
328 }
329
330 pub async fn read_resource(&self, uri: &str) -> Result<String, Error> {
332 for backend in &self.backends {
333 if uri == format!("compressor://{}/uncompressed-tools", backend.public_name) {
334 return serde_json::to_string_pretty(&backend.tools).map_err(Error::from);
335 }
336 }
337 let backend = self
338 .backends
339 .iter()
340 .find(|backend| backend.resources.iter().any(|resource| resource == uri))
341 .ok_or_else(|| Error::ToolNotFound(uri.to_string()))?;
342 let result = backend
343 .client
344 .read_resource(ReadResourceRequestParams::new(uri))
345 .await
346 .map_err(|error| Error::Config(error.to_string()))?;
347 Ok(resource_contents_to_string(result.contents))
348 }
349
350 pub async fn list_prompts(&self) -> Result<Vec<String>, Error> {
352 Ok(self
353 .backends
354 .iter()
355 .flat_map(|backend| backend.prompts.iter().map(|prompt| prompt.name.clone()))
356 .collect())
357 }
358
359 pub async fn get_prompt(
361 &self,
362 name: &str,
363 arguments: Option<serde_json::Map<String, Value>>,
364 ) -> Result<GetPromptResult, Error> {
365 let backend = self
366 .backends
367 .iter()
368 .find(|backend| backend.prompts.iter().any(|prompt| prompt.name == name))
369 .ok_or_else(|| Error::ToolNotFound(name.to_string()))?;
370 let mut request = GetPromptRequestParams::new(name);
371 if let Some(arguments) = arguments {
372 request = request.with_arguments(arguments);
373 }
374 backend
375 .client
376 .get_prompt(request)
377 .await
378 .map_err(|error| Error::Config(error.to_string()))
379 }
380
381 pub fn single_backend_tools(&self) -> Result<Vec<Tool>, Error> {
383 self.backends
384 .first()
385 .filter(|_| self.backends.len() == 1)
386 .map(|backend| backend.tools.clone())
387 .ok_or_else(|| Error::Config("expected exactly one backend".to_string()))
388 }
389
390 pub fn just_bash_provider_specs(&self) -> Vec<JustBashProviderSpec> {
395 self.backends
396 .iter()
397 .map(|backend| {
398 let invoke_tool_name = format!("{}invoke_tool", self.wrapper_prefix(backend));
399 JustBashProviderSpec {
400 provider_name: backend.public_name.clone(),
401 help_tool_name: format!("{}_help", backend.public_name),
402 tools: backend
403 .tools
404 .iter()
405 .map(|tool| JustBashCommandSpec {
406 command_name: crate::cli::mapping::tool_name_to_subcommand(&tool.name),
407 backend_tool_name: tool.name.clone(),
408 description: tool.description.clone(),
409 input_schema: tool.input_schema.clone(),
410 invoke_tool_name: invoke_tool_name.clone(),
411 })
412 .collect(),
413 }
414 })
415 .collect()
416 }
417
418 pub async fn invoke_single_backend_tool(
419 &self,
420 backend_tool_name: &str,
421 tool_input: Value,
422 ) -> Result<String, Error> {
423 let backend = self
424 .backends
425 .first()
426 .filter(|_| self.backends.len() == 1)
427 .ok_or_else(|| Error::ToolNotFound(backend_tool_name.to_string()))?;
428 self.invoke_backend(backend, backend_tool_name, tool_input)
429 .await
430 }
431
432 async fn invoke_backend(
433 &self,
434 backend: &ConnectedBackend,
435 backend_tool_name: &str,
436 tool_input: Value,
437 ) -> Result<String, Error> {
438 if !backend
439 .tools
440 .iter()
441 .any(|tool| tool.name == backend_tool_name)
442 {
443 return Err(Error::ToolNotFound(backend_tool_name.to_string()));
444 }
445 let arguments = match tool_input {
446 Value::Object(map) => Some(map),
447 _ => None,
448 };
449 let mut params = CallToolRequestParams::new(backend_tool_name.to_string());
450 if let Some(arguments) = arguments {
451 params = params.with_arguments(arguments);
452 }
453 let result = backend
454 .client
455 .call_tool(params)
456 .await
457 .map_err(|error| Error::Config(error.to_string()))?;
458 let output = call_tool_result_to_string(result);
459 Ok(self.maybe_toonify_output(&output))
460 }
461
462 fn maybe_toonify_output(&self, output: &str) -> String {
463 if !self.config.toonify {
464 return output.to_string();
465 }
466 let Ok(value) = serde_json::from_str::<Value>(output) else {
467 return output.to_string();
468 };
469 toon_format::encode(&value, &toon_format::EncodeOptions::default())
470 .unwrap_or_else(|_| output.to_string())
471 }
472
473 fn cli_help_tools(&self) -> Vec<Tool> {
474 self.backends
475 .iter()
476 .map(|backend| {
477 Tool::new(
478 format!("{}_help", backend.public_name),
479 Some(format_backend_help(backend)),
480 serde_json::json!({"type": "object", "properties": {}}),
481 )
482 })
483 .collect()
484 }
485
486 fn just_bash_tools(&self) -> Vec<Tool> {
487 let mut tools = Vec::new();
488 let names = self
489 .backends
490 .iter()
491 .map(|backend| backend.public_name.as_str())
492 .collect::<Vec<_>>()
493 .join(", ");
494 tools.push(Tool::new(
495 "bash_tool",
496 Some(format!(
497 "Register backend MCP tools as custom commands in a language-hosted just-bash instance. Providers: {names}. When relevant, prefer TOON output for compact representation."
498 )),
499 serde_json::json!({
500 "type": "object",
501 "properties": {
502 "command": {"type": "string", "description": "Command text interpreted by the host language's just-bash implementation"}
503 },
504 "required": ["command"]
505 }),
506 ));
507 tools.extend(self.cli_help_tools());
508 tools
509 }
510
511 fn wrapper_prefix(&self, backend: &ConnectedBackend) -> String {
512 if backend.public_name.is_empty() {
513 String::new()
514 } else {
515 format!("{}_", backend.public_name)
516 }
517 }
518
519 fn backend_for_wrapper(&self, wrapper_tool_name: &str) -> Result<&ConnectedBackend, Error> {
520 if self.backends.len() == 1 && self.backends[0].public_name.is_empty() {
521 return Ok(&self.backends[0]);
522 }
523 self.backends
524 .iter()
525 .find(|backend| wrapper_tool_name.starts_with(&self.wrapper_prefix(backend)))
526 .ok_or_else(|| Error::ToolNotFound(wrapper_tool_name.to_string()))
527 }
528}
529
530fn format_backend_help(backend: &ConnectedBackend) -> String {
531 let mut lines = vec![format!(
532 "{} - the {} toolset",
533 backend.public_name, backend.public_name
534 )];
535 lines.push(String::new());
536 lines.push("When relevant, outputs from this CLI will prefer using the TOON format for more efficient representation of data.".to_string());
537 lines.push(String::new());
538 lines.push("SUBCOMMANDS:".to_string());
539 for tool in &backend.tools {
540 let subcommand = crate::cli::mapping::tool_name_to_subcommand(&tool.name);
541 let description = tool.description.as_deref().unwrap_or_default();
542 lines.push(format!(" {subcommand:<35} {description}"));
543 }
544 lines.join("\n")
545}
546
547fn wrapper_tool(name: String, description: &str) -> Tool {
548 Tool::new(
549 name,
550 Some(description.to_string()),
551 serde_json::json!({
552 "type": "object",
553 "properties": {}
554 }),
555 )
556}
557
558fn call_tool_result_to_string(result: rmcp::model::CallToolResult) -> String {
559 if let Some(structured) = result.structured_content {
560 return value_to_string(&structured);
561 }
562
563 result
564 .content
565 .into_iter()
566 .map(content_to_string)
567 .collect::<Vec<_>>()
568 .join("\n")
569}
570
571fn content_to_string(content: Content) -> String {
572 match content.raw {
573 RawContent::Text(text) => text.text,
574 RawContent::Image(image) => image.data,
575 RawContent::Resource(resource) => resource_contents_to_string(vec![resource.resource]),
576 RawContent::Audio(audio) => audio.data,
577 RawContent::ResourceLink(resource) => resource.uri,
578 }
579}
580
581fn resource_contents_to_string(contents: Vec<ResourceContents>) -> String {
582 contents
583 .into_iter()
584 .map(|content| match content {
585 ResourceContents::TextResourceContents { text, .. } => text,
586 ResourceContents::BlobResourceContents { blob, .. } => blob,
587 })
588 .collect::<Vec<_>>()
589 .join("\n")
590}
591
592fn value_to_string(value: &Value) -> String {
593 match value {
594 Value::String(value) => value.clone(),
595 Value::Object(map) if map.len() == 1 && map.contains_key("result") => {
596 value_to_string(&map["result"])
597 }
598 _ => value.to_string(),
599 }
600}