use std::sync::Arc;
use futures::future::BoxFuture;
use reson_mcp::server::McpServerBuilder;
use reson_mcp::{CallToolResult, Content, ErrorData};
use serde_json::Value;
use crate::error::{Error, Result};
pub use reson_mcp::server::ServerTransport;
#[cfg(feature = "mcp-apps")]
pub use reson_mcp::apps::{
ui_uri, DisplayMode, UiPermissions, UiResource, UiResourceCsp, UiResourceMeta, Visibility,
};
pub struct McpServer {
builder: McpServerBuilder,
}
impl McpServer {
pub fn new(name: impl Into<String>) -> Self {
Self {
builder: reson_mcp::server::McpServer::builder(name),
}
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.builder = self.builder.with_description(description);
self
}
pub fn version(mut self, version: impl Into<String>) -> Self {
self.builder = self.builder.with_version(version);
self
}
pub fn agent<F>(self, name: &str, description: &str, schema: Value, handler: F) -> Self
where
F: Fn(Value) -> BoxFuture<'static, Result<String>> + Send + Sync + 'static,
{
self.register_agent(name, description, schema, handler)
}
pub fn agents<F>(mut self, agents: Vec<(&str, &str, Value, F)>) -> Self
where
F: Fn(Value) -> BoxFuture<'static, Result<String>> + Send + Sync + 'static,
{
for (name, description, schema, handler) in agents {
self = self.register_agent(name, description, schema, handler);
}
self
}
pub fn tool<F>(mut self, name: &str, description: &str, schema: Value, handler: F) -> Self
where
F: Fn(
String,
Option<serde_json::Map<String, Value>>,
) -> BoxFuture<'static, std::result::Result<CallToolResult, ErrorData>>
+ Send
+ Sync
+ 'static,
{
self.builder = self.builder.with_tool(name, description, schema, handler);
self
}
pub fn tools<F>(mut self, tools: Vec<(&str, &str, Value, F)>) -> Self
where
F: Fn(
String,
Option<serde_json::Map<String, Value>>,
) -> BoxFuture<'static, std::result::Result<CallToolResult, ErrorData>>
+ Send
+ Sync
+ 'static,
{
for (name, description, schema, handler) in tools {
self.builder = self.builder.with_tool(name, description, schema, handler);
}
self
}
#[cfg(feature = "mcp-apps")]
pub fn with_ui(mut self, resource: UiResource) -> Self {
self.builder = self.builder.with_ui(resource);
self
}
#[cfg(feature = "mcp-apps")]
pub fn visibility(mut self, visibility: Vec<reson_mcp::apps::Visibility>) -> Self {
self.builder = self.builder.visibility(visibility);
self
}
pub async fn serve(self, transport: ServerTransport) -> Result<()> {
self.builder
.build()
.serve(transport)
.await
.map_err(|e| Error::NonRetryable(format!("MCP server error: {}", e)))
}
fn register_agent<F>(mut self, name: &str, description: &str, schema: Value, handler: F) -> Self
where
F: Fn(Value) -> BoxFuture<'static, Result<String>> + Send + Sync + 'static,
{
let handler = Arc::new(handler);
self.builder = self
.builder
.with_tool(name, description, schema, move |_name, args| {
let handler = handler.clone();
Box::pin(async move {
let args_value = match args {
Some(map) => Value::Object(map),
None => Value::Object(serde_json::Map::new()),
};
match handler(args_value).await {
Ok(result) => Ok(CallToolResult::success(vec![Content::text(result)])),
Err(e) => Ok(CallToolResult::error(vec![Content::text(e.to_string())])),
}
})
});
self
}
}
pub(crate) async fn connect_and_register(
runtime: &crate::runtime::Runtime,
uri: &str,
label: Option<&str>,
) -> Result<Arc<reson_mcp::client::McpClient>> {
let client = if uri.starts_with("http://") || uri.starts_with("https://") {
reson_mcp::client::McpClient::http(uri).await
} else if uri.starts_with("ws://") || uri.starts_with("wss://") {
reson_mcp::client::McpClient::websocket(uri).await
} else {
reson_mcp::client::McpClient::stdio(uri).await
}
.map_err(|e| {
Error::NonRetryable(format!("Failed to connect to MCP server '{}': {}", uri, e))
})?;
let client = Arc::new(client);
let tools_result = client
.list_tools()
.await
.map_err(|e| Error::NonRetryable(format!("Failed to list MCP tools: {}", e)))?;
for tool in &tools_result.tools {
if is_app_only_tool(tool) {
continue;
}
let remote_name = tool.name.to_string();
let registered_name = match label {
Some(prefix) => format!("{}_{}", prefix, remote_name),
None => remote_name.clone(),
};
let tool_description = tool.description.clone().unwrap_or_default();
let schema = serde_json::to_value(&tool.input_schema)
.unwrap_or(Value::Object(serde_json::Map::new()));
let client_ref = client.clone();
let name_for_closure = remote_name;
let handler = crate::runtime::ToolFunction::Async(Box::new(move |args: Value| {
let client = client_ref.clone();
let name = name_for_closure.clone();
Box::pin(async move {
let result = client.call_tool(&name, args).await.map_err(|e| {
Error::NonRetryable(format!("MCP tool '{}' failed: {}", name, e))
})?;
let text: String = result
.content
.iter()
.filter_map(|c| c.as_text().map(|t| t.text.as_str()))
.collect::<Vec<_>>()
.join("\n");
Ok(text)
}) as BoxFuture<'static, Result<String>>
}));
runtime
.register_tool_with_schema(registered_name, tool_description, schema, handler)
.await?;
}
Ok(client)
}
fn is_app_only_tool(tool: &reson_mcp::McpTool) -> bool {
let Some(meta) = &tool.meta else { return false };
let Some(ui) = meta.0.get("ui") else {
return false;
};
let Some(visibility) = ui.get("visibility") else {
return false;
};
let Some(arr) = visibility.as_array() else {
return false;
};
!arr.iter().any(|v| v.as_str() == Some("model"))
}