use std::collections::HashMap;
use crate::mcp::auth::BearerAuth;
use crate::mcp::schema::{ResourceDescription, ToolDescription};
pub type Handler =
Box<dyn Fn(serde_json::Value) -> crate::error::Result<serde_json::Value> + Send + Sync>;
pub struct McpServer {
server_name: String,
server_version: String,
tools: Vec<ToolDescription>,
resources: Vec<ResourceDescription>,
handlers: HashMap<String, Handler>,
resource_handlers: HashMap<String, Handler>,
auth: Option<BearerAuth>,
}
impl McpServer {
pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
Self {
server_name: name.into(),
server_version: version.into(),
tools: Vec::new(),
resources: Vec::new(),
handlers: HashMap::new(),
resource_handlers: HashMap::new(),
auth: None,
}
}
pub fn with_bearer_auth(mut self, token: impl Into<String>) -> Self {
self.auth = Some(BearerAuth::new(token));
self
}
pub fn with_generated_auth(mut self) -> (Self, String) {
let bearer = BearerAuth::generate();
let token = bearer.token().to_string();
self.auth = Some(bearer);
(self, token)
}
pub fn check_auth(&self, authorization_header: &str) -> bool {
match &self.auth {
None => true,
Some(bearer) => bearer.validate(authorization_header),
}
}
pub fn auth_enabled(&self) -> bool {
self.auth.is_some()
}
pub fn register_tool(&mut self, tool: ToolDescription) {
self.tools.push(tool);
}
pub fn register_resource(&mut self, resource: ResourceDescription) {
self.resources.push(resource);
}
pub fn set_handler(
&mut self,
tool_name: &str,
handler: impl Fn(serde_json::Value) -> crate::error::Result<serde_json::Value>
+ Send
+ Sync
+ 'static,
) {
self.handlers
.insert(tool_name.to_string(), Box::new(handler));
}
pub fn name(&self) -> &str {
&self.server_name
}
pub fn version(&self) -> &str {
&self.server_version
}
pub fn tools(&self) -> &[ToolDescription] {
&self.tools
}
pub fn resources(&self) -> &[ResourceDescription] {
&self.resources
}
pub fn set_resource_handler(
&mut self,
uri: &str,
handler: impl Fn(serde_json::Value) -> crate::error::Result<serde_json::Value>
+ Send
+ Sync
+ 'static,
) {
self.resource_handlers
.insert(uri.to_string(), Box::new(handler));
}
pub fn read_resource(
&self,
uri: &str,
params: serde_json::Value,
) -> crate::error::Result<serde_json::Value> {
let handler = self
.resource_handlers
.get(uri)
.ok_or_else(|| crate::error::KernelError::Config(format!("unknown resource: {uri}")))?;
handler(params)
}
pub fn call_tool(
&self,
name: &str,
params: serde_json::Value,
) -> crate::error::Result<serde_json::Value> {
let handler = self
.handlers
.get(name)
.ok_or_else(|| crate::error::KernelError::Config(format!("unknown tool: {name}")))?;
handler(params)
}
pub fn initialize_response(&self) -> serde_json::Value {
serde_json::json!({
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": { "listChanged": false },
"resources": { "subscribe": false, "listChanged": false },
},
"serverInfo": {
"name": self.server_name,
"version": self.server_version,
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn register_and_call_tool() {
let mut server = McpServer::new("test", "0.1.0");
server.register_tool(ToolDescription {
name: "echo".into(),
description: "Echo input".into(),
input_schema: serde_json::json!({"type": "object"}),
});
server.set_handler("echo", |params| Ok(params));
let result = server
.call_tool("echo", serde_json::json!({"msg": "hi"}))
.unwrap();
assert_eq!(result["msg"], "hi");
}
#[test]
fn unknown_tool_returns_error() {
let server = McpServer::new("test", "0.1.0");
let result = server.call_tool("missing", serde_json::json!(null));
assert!(result.is_err());
}
#[test]
fn initialize_response_shape() {
let server = McpServer::new("my-server", "2.0.0");
let resp = server.initialize_response();
assert_eq!(resp["serverInfo"]["name"], "my-server");
assert_eq!(resp["protocolVersion"], "2024-11-05");
}
#[test]
fn list_tools() {
let mut server = McpServer::new("test", "0.1.0");
server.register_tool(ToolDescription {
name: "a".into(),
description: "Tool A".into(),
input_schema: serde_json::json!({}),
});
server.register_tool(ToolDescription {
name: "b".into(),
description: "Tool B".into(),
input_schema: serde_json::json!({}),
});
assert_eq!(server.tools().len(), 2);
}
#[test]
fn read_resource() {
let mut server = McpServer::new("test", "0.1.0");
server.register_resource(ResourceDescription {
uri: "docs://readme".into(),
name: "README".into(),
description: Some("Project readme".into()),
mime_type: Some("text/markdown".into()),
});
server.set_resource_handler("docs://readme", |_params| {
Ok(serde_json::json!("# Hello World"))
});
let result = server
.read_resource("docs://readme", serde_json::json!({}))
.unwrap();
assert_eq!(result, serde_json::json!("# Hello World"));
}
#[test]
fn unknown_resource_returns_error() {
let server = McpServer::new("test", "0.1.0");
let result = server.read_resource("missing://uri", serde_json::json!({}));
assert!(result.is_err());
}
#[test]
fn no_auth_by_default() {
let server = McpServer::new("test", "0.1.0");
assert!(!server.auth_enabled());
assert!(server.check_auth(""));
assert!(server.check_auth("Bearer whatever"));
}
#[test]
fn with_bearer_auth_validates_correctly() {
let server = McpServer::new("test", "0.1.0").with_bearer_auth("my-token");
assert!(server.auth_enabled());
assert!(server.check_auth("Bearer my-token"));
assert!(!server.check_auth("Bearer wrong"));
assert!(!server.check_auth(""));
}
#[test]
fn with_generated_auth_returns_token() {
let (server, token) = McpServer::new("test", "0.1.0").with_generated_auth();
assert!(server.auth_enabled());
assert_eq!(token.len(), 32);
assert!(server.check_auth(&format!("Bearer {token}")));
assert!(!server.check_auth("Bearer bad"));
}
}