use rmcp::handler::server::ServerHandler;
use rmcp::model::{
CallToolRequestParams, CallToolResult, ErrorData, ListToolsResult, PaginatedRequestParams,
ServerInfo, Tool,
};
use rmcp::service::RequestContext;
use rmcp::RoleServer;
use crate::mcp::get_mcp_tools;
use crate::mcp::handler::{value_to_json_object_arc, McpToolInstance};
#[derive(Clone)]
pub struct SdForgeMcpServer {
pub(crate) tools: Vec<McpToolInstance>,
pub(crate) server_name: String,
pub(crate) server_version: String,
}
impl Default for SdForgeMcpServer {
fn default() -> Self {
Self::new()
}
}
impl SdForgeMcpServer {
pub fn new() -> Self {
Self::with_server_info("sdforge-mcp".to_string(), "0.2.0".to_string())
}
pub fn with_server_info(server_name: String, server_version: String) -> Self {
let tools = get_mcp_tools();
let (name, version) = tools
.first()
.map(|t| {
(
t.metadata().name().to_string(),
t.metadata().version().to_string(),
)
})
.unwrap_or((server_name, server_version));
Self {
tools,
server_name: name,
server_version: version,
}
}
pub fn empty() -> Self {
Self {
tools: Vec::new(),
server_name: "sdforge-mcp".to_string(),
server_version: "0.2.0".to_string(),
}
}
pub fn with_tools(tools: Vec<McpToolInstance>) -> Self {
let (name, version) = tools
.first()
.map(|t| {
(
t.metadata().name().to_string(),
t.metadata().version().to_string(),
)
})
.unwrap_or(("sdforge-mcp".to_string(), "0.2.0".to_string()));
Self {
tools,
server_name: name,
server_version: version,
}
}
pub fn tool_count(&self) -> usize {
self.tools.len()
}
pub fn find_tool(&self, name: &str) -> Option<&McpToolInstance> {
self.tools.iter().find(|t| t.tool().name() == name)
}
pub(crate) fn build_tool_model(&self, instance: &McpToolInstance) -> Tool {
let tool = instance.tool();
let input_schema = value_to_json_object_arc(tool.input_schema());
let mut model = Tool::default();
model.name = tool.name().to_string().into();
model.description = Some(tool.description().to_string().into());
model.input_schema = input_schema;
model
}
pub fn get_all_tools(&self) -> Vec<Tool> {
self.tools
.iter()
.map(|instance| self.build_tool_model(instance))
.collect()
}
pub fn call_tool_internal(
&self,
name: &str,
arguments: Option<serde_json::Value>,
) -> Result<CallToolResult, ErrorData> {
let instance = self.find_tool(name).ok_or_else(|| {
ErrorData::invalid_params(
format!(
"Tool '{}' not found. Registered tools: {}",
name,
self.tools
.iter()
.map(|t| t.tool().name())
.collect::<Vec<_>>()
.join(", ")
),
None,
)
})?;
instance.tool().call(arguments)
}
}
impl ServerHandler for SdForgeMcpServer {
fn get_info(&self) -> ServerInfo {
use rmcp::model::{Implementation, ServerCapabilities};
let mut server_info = Implementation::default();
server_info.name = self.server_name.clone();
server_info.version = self.server_version.clone();
ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
.with_server_info(server_info)
}
async fn list_tools(
&self,
_request: Option<PaginatedRequestParams>,
_context: RequestContext<RoleServer>,
) -> Result<ListToolsResult, ErrorData> {
let tools = self.get_all_tools();
Ok(ListToolsResult {
meta: None,
next_cursor: None,
tools,
})
}
async fn call_tool(
&self,
request: CallToolRequestParams,
_context: RequestContext<RoleServer>,
) -> Result<CallToolResult, ErrorData> {
let name: &str = request.name.as_ref();
let arguments = request.arguments.map(serde_json::Value::Object);
self.call_tool_internal(name, arguments)
}
}
#[cfg(test)]
mod tests {
use super::*;
use rmcp::model::{NumberOrString, TaskMetadata};
use rmcp::service::serve_directly;
#[derive(Debug)]
struct DummyTransportError;
impl std::fmt::Display for DummyTransportError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "dummy transport error")
}
}
impl std::error::Error for DummyTransportError {}
struct DummyTransport;
#[allow(clippy::manual_async_fn)] impl rmcp::transport::Transport<rmcp::RoleServer> for DummyTransport {
type Error = DummyTransportError;
fn send(
&mut self,
_item: rmcp::service::TxJsonRpcMessage<rmcp::RoleServer>,
) -> impl std::future::Future<Output = Result<(), Self::Error>> + Send + 'static {
async { Ok(()) }
}
fn receive(
&mut self,
) -> impl std::future::Future<
Output = Option<rmcp::service::RxJsonRpcMessage<rmcp::RoleServer>>,
> + Send {
async { None }
}
fn close(&mut self) -> impl std::future::Future<Output = Result<(), Self::Error>> + Send {
async { Ok(()) }
}
}
fn make_test_context() -> RequestContext<RoleServer> {
let server = SdForgeMcpServer::new();
let running = serve_directly(server, DummyTransport, None);
let peer = running.peer().clone();
RequestContext::new(NumberOrString::Number(0), peer)
}
#[tokio::test]
async fn test_server_handler_list_tools_returns_all_tools() {
let server = SdForgeMcpServer::new();
let expected_count = server.tool_count();
let context = make_test_context();
let result = server.list_tools(None, context).await;
assert!(result.is_ok(), "list_tools should succeed");
let tools_result = result.unwrap();
assert_eq!(
tools_result.tools.len(),
expected_count,
"list_tools should return all registered tools"
);
assert!(
tools_result.next_cursor.is_none(),
"next_cursor should be None when all tools fit in one page"
);
assert!(
tools_result.meta.is_none(),
"meta should be None for a plain list_tools response"
);
for tool in &tools_result.tools {
assert!(
!tool.name.as_ref().is_empty(),
"Each tool should have a non-empty name"
);
assert!(
tool.description.is_some(),
"Each tool should have a description"
);
}
}
#[tokio::test]
async fn test_server_handler_list_tools_with_pagination_params() {
let server = SdForgeMcpServer::new();
let context = make_test_context();
let mut params = PaginatedRequestParams::default();
params.cursor = Some("cursor_abc".to_string());
let result = server.list_tools(Some(params), context).await;
assert!(
result.is_ok(),
"list_tools with pagination params should succeed"
);
let tools_result = result.unwrap();
assert!(
!tools_result.tools.is_empty(),
"Should return tools even with a cursor"
);
}
#[tokio::test]
async fn test_server_handler_call_tool_valid_no_args() {
let server = SdForgeMcpServer::new();
let context = make_test_context();
let request = CallToolRequestParams::new("coverage_test_tool");
let result = server.call_tool(request, context).await;
assert!(result.is_ok(), "call_tool should succeed for valid tool");
let tool_result = result.unwrap();
assert!(
tool_result.content.is_empty(),
"coverage_test_tool should return empty content"
);
assert!(
tool_result.is_error.is_none(),
"is_error should be None for successful call"
);
}
#[tokio::test]
async fn test_server_handler_call_tool_valid_with_args() {
let server = SdForgeMcpServer::new();
let context = make_test_context();
let mut args = serde_json::Map::new();
args.insert(
"param1".to_string(),
serde_json::Value::String("value1".to_string()),
);
args.insert(
"param2".to_string(),
serde_json::Value::Number(serde_json::Number::from(42)),
);
let mut request = CallToolRequestParams::new("coverage_test_tool");
request.arguments = Some(args);
let result = server.call_tool(request, context).await;
assert!(
result.is_ok(),
"call_tool with arguments should succeed for coverage_test_tool"
);
}
#[tokio::test]
async fn test_server_handler_call_tool_nonexistent() {
let server = SdForgeMcpServer::new();
let context = make_test_context();
let request = CallToolRequestParams::new("does_not_exist");
let result = server.call_tool(request, context).await;
assert!(
result.is_err(),
"call_tool with nonexistent tool should return error"
);
let error = result.unwrap_err();
assert!(
error.message.contains("not found"),
"Error message should contain 'not found', got: {}",
error.message
);
assert!(
error.message.contains("coverage_test_tool"),
"Error message should list registered tools, got: {}",
error.message
);
}
#[tokio::test]
async fn test_server_handler_call_tool_empty_name() {
let server = SdForgeMcpServer::new();
let context = make_test_context();
let request = CallToolRequestParams::new("");
let result = server.call_tool(request, context).await;
assert!(
result.is_err(),
"call_tool with empty name should return error"
);
}
#[tokio::test]
async fn test_server_handler_call_tool_with_task_metadata() {
let server = SdForgeMcpServer::new();
let context = make_test_context();
let request = {
let mut params = CallToolRequestParams::new("coverage_test_tool");
params.task = Some(TaskMetadata::new());
params
};
let result = server.call_tool(request, context).await;
assert!(
result.is_ok(),
"call_tool with task metadata should succeed for coverage_test_tool"
);
}
}