use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use std::collections::HashMap;
use crate::error::{Error, Result};
use fusabi_host::Value;
pub const PROTOCOL_VERSION: &str = "2024-11-05";
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "method")]
pub enum McpMessage {
#[serde(rename = "initialize")]
Initialize(InitializeParams),
#[serde(rename = "tools/list")]
ListTools,
#[serde(rename = "tools/call")]
CallTool(CallToolParams),
#[serde(rename = "resources/list")]
ListResources,
#[serde(rename = "resources/read")]
ReadResource(ReadResourceParams),
#[serde(rename = "prompts/list")]
ListPrompts,
#[serde(rename = "prompts/get")]
GetPrompt(GetPromptParams),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InitializeParams {
#[serde(rename = "protocolVersion")]
pub protocol_version: String,
pub capabilities: ClientCapabilities,
#[serde(rename = "clientInfo")]
pub client_info: ClientInfo,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ClientCapabilities {
#[serde(default)]
pub sampling: Option<JsonValue>,
#[serde(default)]
pub roots: Option<JsonValue>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientInfo {
pub name: String,
pub version: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ServerCapabilities {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tools: Option<ToolCapabilities>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resources: Option<ResourceCapabilities>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prompts: Option<PromptCapabilities>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ToolCapabilities {
#[serde(default, rename = "listChanged")]
pub list_changed: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ResourceCapabilities {
#[serde(default)]
pub subscribe: bool,
#[serde(default, rename = "listChanged")]
pub list_changed: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PromptCapabilities {
#[serde(default, rename = "listChanged")]
pub list_changed: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerInfo {
pub name: String,
pub version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CallToolParams {
pub name: String,
#[serde(default)]
pub arguments: HashMap<String, JsonValue>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReadResourceParams {
pub uri: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetPromptParams {
pub name: String,
#[serde(default)]
pub arguments: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolDefinition {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(rename = "inputSchema")]
pub input_schema: JsonValue,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceDefinition {
pub uri: String,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, rename = "mimeType", skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PromptDefinition {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub arguments: Vec<PromptArgument>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PromptArgument {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default)]
pub required: bool,
}
pub fn fusabi_to_json(value: &Value) -> JsonValue {
match value {
Value::Null => JsonValue::Null,
Value::Bool(b) => JsonValue::Bool(*b),
Value::Int(n) => JsonValue::Number((*n).into()),
Value::Float(f) => {
if let Some(n) = serde_json::Number::from_f64(*f) {
JsonValue::Number(n)
} else {
JsonValue::Null
}
}
Value::String(s) => JsonValue::String(s.clone()),
Value::List(items) => JsonValue::Array(items.iter().map(fusabi_to_json).collect()),
Value::Map(map) => {
let obj: serde_json::Map<String, JsonValue> = map
.iter()
.map(|(k, v)| (k.clone(), fusabi_to_json(v)))
.collect();
JsonValue::Object(obj)
}
Value::Bytes(b) => {
let hex: String = b.iter().map(|byte| format!("{:02x}", byte)).collect();
JsonValue::String(hex)
}
Value::Function(_) => JsonValue::String("<function>".to_string()),
Value::Error(e) => JsonValue::Object({
let mut obj = serde_json::Map::new();
obj.insert("error".to_string(), JsonValue::String(e.clone()));
obj
}),
}
}
pub fn json_to_fusabi(value: &JsonValue) -> Value {
match value {
JsonValue::Null => Value::Null,
JsonValue::Bool(b) => Value::Bool(*b),
JsonValue::Number(n) => {
if let Some(i) = n.as_i64() {
Value::Int(i)
} else if let Some(f) = n.as_f64() {
Value::Float(f)
} else {
Value::Null
}
}
JsonValue::String(s) => Value::String(s.clone()),
JsonValue::Array(items) => Value::List(items.iter().map(json_to_fusabi).collect()),
JsonValue::Object(map) => {
let converted: HashMap<String, Value> = map
.iter()
.map(|(k, v)| (k.clone(), json_to_fusabi(v)))
.collect();
Value::Map(converted)
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpServerConfig {
pub name: String,
pub endpoint: String,
#[serde(default)]
pub inject: Vec<String>,
}
impl McpServerConfig {
pub fn new(name: impl Into<String>, endpoint: impl Into<String>) -> Self {
Self {
name: name.into(),
endpoint: endpoint.into(),
inject: Vec::new(),
}
}
pub fn with_inject(mut self, inject: Vec<String>) -> Self {
self.inject = inject;
self
}
pub fn to_fusabi_value(&self) -> Value {
let mut map = HashMap::new();
map.insert("name".to_string(), Value::String(self.name.clone()));
map.insert("endpoint".to_string(), Value::String(self.endpoint.clone()));
map.insert(
"inject".to_string(),
Value::List(
self.inject
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
Value::Map(map)
}
pub fn from_fusabi_value(value: &Value) -> Result<Self> {
match value {
Value::Map(map) => {
let name = map
.get("name")
.and_then(|v| match v {
Value::String(s) => Some(s.clone()),
_ => None,
})
.ok_or_else(|| Error::InvalidValue("MCP config missing 'name' field".into()))?;
let endpoint = map
.get("endpoint")
.and_then(|v| match v {
Value::String(s) => Some(s.clone()),
_ => None,
})
.ok_or_else(|| {
Error::InvalidValue("MCP config missing 'endpoint' field".into())
})?;
let inject = map
.get("inject")
.and_then(|v| match v {
Value::List(items) => Some(
items
.iter()
.filter_map(|item| match item {
Value::String(s) => Some(s.clone()),
_ => None,
})
.collect(),
),
_ => None,
})
.unwrap_or_default();
Ok(McpServerConfig {
name,
endpoint,
inject,
})
}
_ => Err(Error::InvalidValue(
"Expected Map for MCP server config".into(),
)),
}
}
pub fn to_json(&self) -> Result<String> {
serde_json::to_string_pretty(self).map_err(|e| Error::Serialization(e.to_string()))
}
}
pub fn mcp_server_new(name: &Value, endpoint: &Value) -> Result<Value> {
let name_str = match name {
Value::String(s) => s.clone(),
_ => return Err(Error::InvalidValue("name must be a string".into())),
};
let endpoint_str = match endpoint {
Value::String(s) => s.clone(),
_ => return Err(Error::InvalidValue("endpoint must be a string".into())),
};
Ok(McpServerConfig::new(name_str, endpoint_str).to_fusabi_value())
}
pub fn mcp_server_with_inject(server: &Value, inject: &Value) -> Result<Value> {
let mut config = McpServerConfig::from_fusabi_value(server)?;
let inject_items = match inject {
Value::List(items) => items
.iter()
.filter_map(|v| match v {
Value::String(s) => Some(s.clone()),
_ => None,
})
.collect(),
_ => {
return Err(Error::InvalidValue(
"inject must be a list of strings".into(),
))
}
};
config.inject = inject_items;
Ok(config.to_fusabi_value())
}
pub fn mcp_server_to_json(server: &Value) -> Result<Value> {
let config = McpServerConfig::from_fusabi_value(server)?;
let json = config.to_json()?;
Ok(Value::String(json))
}
pub fn mcp_server_get_name(server: &Value) -> Result<Value> {
let config = McpServerConfig::from_fusabi_value(server)?;
Ok(Value::String(config.name))
}
pub fn mcp_server_get_endpoint(server: &Value) -> Result<Value> {
let config = McpServerConfig::from_fusabi_value(server)?;
Ok(Value::String(config.endpoint))
}
pub fn mcp_server_get_inject(server: &Value) -> Result<Value> {
let config = McpServerConfig::from_fusabi_value(server)?;
Ok(Value::List(
config.inject.into_iter().map(Value::String).collect(),
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fusabi_to_json_roundtrip() {
let original = Value::Map(HashMap::from([
("name".to_string(), Value::String("test".to_string())),
("count".to_string(), Value::Int(42)),
(
"items".to_string(),
Value::List(vec![Value::Int(1), Value::Int(2)]),
),
]));
let json = fusabi_to_json(&original);
let back = json_to_fusabi(&json);
if let Value::Map(orig_map) = &original {
if let Value::Map(back_map) = &back {
assert_eq!(orig_map.len(), back_map.len());
for (k, v) in orig_map {
assert_eq!(
back_map.get(k).map(|v| format!("{:?}", v)),
Some(format!("{:?}", v))
);
}
} else {
panic!("Expected Map value");
}
}
}
#[test]
fn test_tool_definition_serialize() {
let tool = ToolDefinition {
name: "test-tool".to_string(),
description: Some("A test tool".to_string()),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"input": { "type": "string" }
}
}),
};
let json = serde_json::to_string(&tool).unwrap();
assert!(json.contains("test-tool"));
}
#[test]
fn test_mcp_server_config_new() {
let config = McpServerConfig::new("test-server", "http://localhost:3000");
assert_eq!(config.name, "test-server");
assert_eq!(config.endpoint, "http://localhost:3000");
assert!(config.inject.is_empty());
}
#[test]
fn test_mcp_server_config_with_inject() {
let config = McpServerConfig::new("test", "http://localhost")
.with_inject(vec!["tasks".into(), "context".into()]);
assert_eq!(config.inject.len(), 2);
assert_eq!(config.inject[0], "tasks");
}
#[test]
fn test_mcp_server_config_roundtrip() {
let original = McpServerConfig {
name: "roundtrip".into(),
endpoint: "http://example.com".into(),
inject: vec!["a".into(), "b".into()],
};
let value = original.to_fusabi_value();
let recovered = McpServerConfig::from_fusabi_value(&value).unwrap();
assert_eq!(recovered.name, original.name);
assert_eq!(recovered.endpoint, original.endpoint);
assert_eq!(recovered.inject, original.inject);
}
#[test]
fn test_mcp_server_new_function() {
let name = Value::String("my-server".into());
let endpoint = Value::String("http://localhost:8080".into());
let result = mcp_server_new(&name, &endpoint).unwrap();
let config = McpServerConfig::from_fusabi_value(&result).unwrap();
assert_eq!(config.name, "my-server");
assert_eq!(config.endpoint, "http://localhost:8080");
}
#[test]
fn test_mcp_server_with_inject_function() {
let server = McpServerConfig::new("test", "http://localhost").to_fusabi_value();
let inject = Value::List(vec![
Value::String("item1".into()),
Value::String("item2".into()),
]);
let result = mcp_server_with_inject(&server, &inject).unwrap();
let config = McpServerConfig::from_fusabi_value(&result).unwrap();
assert_eq!(config.inject, vec!["item1", "item2"]);
}
#[test]
fn test_mcp_server_to_json_function() {
let server = McpServerConfig::new("json-test", "http://test.com")
.with_inject(vec!["data".into()])
.to_fusabi_value();
let result = mcp_server_to_json(&server).unwrap();
if let Value::String(json) = result {
assert!(json.contains("json-test"));
assert!(json.contains("http://test.com"));
assert!(json.contains("data"));
} else {
panic!("Expected String value");
}
}
}