use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(into = "i32", try_from = "i32")]
pub enum McpErrorCode {
ParseError,
InvalidRequest,
MethodNotFound,
InvalidParams,
InternalError,
ServerError(i32),
Unknown(i32),
}
impl McpErrorCode {
pub fn from_code(code: i32) -> Self {
match code {
-32700 => Self::ParseError,
-32600 => Self::InvalidRequest,
-32601 => Self::MethodNotFound,
-32602 => Self::InvalidParams,
-32603 => Self::InternalError,
c if (-32099..=-32000).contains(&c) => Self::ServerError(c),
c => Self::Unknown(c),
}
}
pub fn code(&self) -> i32 {
match self {
Self::ParseError => -32700,
Self::InvalidRequest => -32600,
Self::MethodNotFound => -32601,
Self::InvalidParams => -32602,
Self::InternalError => -32603,
Self::ServerError(c) | Self::Unknown(c) => *c,
}
}
pub fn is_client_error(&self) -> bool {
matches!(
self,
Self::ParseError | Self::InvalidRequest | Self::InvalidParams
)
}
pub fn is_server_error(&self) -> bool {
matches!(
self,
Self::InternalError | Self::MethodNotFound | Self::ServerError(_)
)
}
pub fn description(&self) -> &'static str {
match self {
Self::ParseError => "Invalid JSON was received",
Self::InvalidRequest => "The JSON sent is not a valid Request object",
Self::MethodNotFound => "The method does not exist or is not available",
Self::InvalidParams => "Invalid method parameter(s)",
Self::InternalError => "Internal JSON-RPC error",
Self::ServerError(_) => "Server error",
Self::Unknown(_) => "Unknown error",
}
}
}
impl std::fmt::Display for McpErrorCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} ({})", self.description(), self.code())
}
}
impl From<McpErrorCode> for i32 {
fn from(code: McpErrorCode) -> Self {
code.code()
}
}
impl From<i32> for McpErrorCode {
fn from(code: i32) -> Self {
Self::from_code(code)
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct McpConfig {
#[serde(skip)]
pub name: String,
pub command: String,
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub env: FxHashMap<String, String>,
#[serde(default)]
pub cwd: Option<String>,
}
impl McpConfig {
pub fn new(name: impl Into<String>, command: impl Into<String>) -> Self {
Self {
name: name.into(),
command: command.into(),
args: Vec::new(),
env: FxHashMap::default(),
cwd: None,
}
}
pub fn with_arg(mut self, arg: impl Into<String>) -> Self {
self.args.push(arg.into());
self
}
pub fn with_args(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.args.extend(args.into_iter().map(Into::into));
self
}
pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.env.insert(key.into(), value.into());
self
}
pub fn with_cwd(mut self, cwd: impl Into<String>) -> Self {
self.cwd = Some(cwd.into());
self
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct ToolCallRequest {
pub name: String,
#[serde(default)]
pub arguments: serde_json::Value,
}
impl ToolCallRequest {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
arguments: serde_json::Value::Object(serde_json::Map::new()),
}
}
pub fn with_arguments(mut self, args: serde_json::Value) -> Self {
self.arguments = args;
self
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct ToolCallResult {
pub content: Vec<ContentBlock>,
#[serde(default)]
pub is_error: bool,
}
impl ToolCallResult {
pub fn success(content: Vec<ContentBlock>) -> Self {
Self {
content,
is_error: false,
}
}
pub fn error(message: impl Into<String>) -> Self {
Self {
content: vec![ContentBlock::text(message)],
is_error: true,
}
}
pub fn text(&self) -> String {
self.content
.iter()
.filter_map(|block| block.text.as_deref())
.collect::<Vec<_>>()
.join("\n")
}
pub fn first_text(&self) -> Option<&str> {
self.content.iter().find_map(|block| block.text.as_deref())
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct ContentBlock {
#[serde(rename = "type")]
pub content_type: String,
#[serde(default)]
pub text: Option<String>,
#[serde(default)]
pub data: Option<String>,
#[serde(default)]
pub mime_type: Option<String>,
#[serde(default)]
pub resource: Option<ResourceContent>,
}
impl ContentBlock {
pub fn text(text: impl Into<String>) -> Self {
Self {
content_type: "text".to_string(),
text: Some(text.into()),
data: None,
mime_type: None,
resource: None,
}
}
pub fn image(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
Self {
content_type: "image".to_string(),
text: None,
data: Some(data.into()),
mime_type: Some(mime_type.into()),
resource: None,
}
}
pub fn resource(resource: ResourceContent) -> Self {
Self {
content_type: "resource".to_string(),
text: None,
data: None,
mime_type: None,
resource: Some(resource),
}
}
pub fn is_text(&self) -> bool {
self.content_type == "text"
}
pub fn is_image(&self) -> bool {
self.content_type == "image"
}
pub fn is_resource(&self) -> bool {
self.content_type == "resource"
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct ResourceContent {
pub uri: String,
#[serde(default, rename = "mimeType")]
pub mime_type: Option<String>,
#[serde(default)]
pub text: Option<String>,
#[serde(default)]
pub blob: Option<String>,
}
impl ResourceContent {
pub fn new(uri: impl Into<String>) -> Self {
Self {
uri: uri.into(),
mime_type: None,
text: None,
blob: None,
}
}
pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
self.mime_type = Some(mime_type.into());
self
}
pub fn with_text(mut self, text: impl Into<String>) -> Self {
self.text = Some(text.into());
self
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct ToolDefinition {
pub name: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default, rename = "inputSchema")]
pub input_schema: Option<serde_json::Value>,
}
impl ToolDefinition {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
description: None,
input_schema: None,
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn with_input_schema(mut self, schema: serde_json::Value) -> Self {
self.input_schema = Some(schema);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::serde_yaml;
use pretty_assertions::assert_eq;
#[test]
fn test_mcp_config_deserialize() {
let yaml = r#"
command: "npx"
args:
- "-y"
- "@novanet/mcp-server"
env:
NEO4J_URI: "bolt://localhost:7687"
NEO4J_USER: "neo4j"
cwd: "/home/user/project"
"#;
let mut config: McpConfig = serde_yaml::from_str(yaml).unwrap();
config.name = "novanet".to_string();
assert_eq!(config.name, "novanet");
assert_eq!(config.command, "npx");
assert_eq!(config.args, vec!["-y", "@novanet/mcp-server"]);
assert_eq!(
config.env.get("NEO4J_URI"),
Some(&"bolt://localhost:7687".to_string())
);
assert_eq!(config.env.get("NEO4J_USER"), Some(&"neo4j".to_string()));
assert_eq!(config.cwd, Some("/home/user/project".to_string()));
}
#[test]
fn test_mcp_config_deserialize_minimal() {
let yaml = r#"
command: "node"
"#;
let config: McpConfig = serde_yaml::from_str(yaml).unwrap();
assert_eq!(config.command, "node");
assert!(config.args.is_empty());
assert!(config.env.is_empty());
assert!(config.cwd.is_none());
}
#[test]
fn test_mcp_config_builder() {
let config = McpConfig::new("test", "npx")
.with_args(["-y", "@test/server"])
.with_env("API_KEY", "secret")
.with_cwd("/tmp");
assert_eq!(config.name, "test");
assert_eq!(config.command, "npx");
assert_eq!(config.args, vec!["-y", "@test/server"]);
assert_eq!(config.env.get("API_KEY"), Some(&"secret".to_string()));
assert_eq!(config.cwd, Some("/tmp".to_string()));
}
#[test]
fn test_mcp_config_serialize_roundtrip() {
let config = McpConfig::new("test", "python")
.with_arg("server.py")
.with_env("DEBUG", "true");
let json = serde_json::to_string(&config).unwrap();
let parsed: McpConfig = serde_json::from_str(&json).unwrap();
assert_eq!(config.command, parsed.command);
assert_eq!(config.args, parsed.args);
assert_eq!(config.env, parsed.env);
}
#[test]
fn test_tool_call_request_new() {
let request = ToolCallRequest::new("novanet_generate");
assert_eq!(request.name, "novanet_generate");
assert!(request.arguments.is_object());
assert!(request.arguments.as_object().unwrap().is_empty());
}
#[test]
fn test_tool_call_request_with_arguments() {
let args = serde_json::json!({
"entity": "qr-code",
"locale": "fr-FR"
});
let request = ToolCallRequest::new("novanet_generate").with_arguments(args.clone());
assert_eq!(request.name, "novanet_generate");
assert_eq!(request.arguments, args);
}
#[test]
fn test_tool_call_request_deserialize() {
let json = r#"{
"name": "read_file",
"arguments": {
"path": "/tmp/test.txt"
}
}"#;
let request: ToolCallRequest = serde_json::from_str(json).unwrap();
assert_eq!(request.name, "read_file");
assert_eq!(request.arguments["path"], "/tmp/test.txt");
}
#[test]
fn test_tool_result_text_extraction() {
let result = ToolCallResult::success(vec![
ContentBlock::text("First line"),
ContentBlock::image("base64data", "image/png"),
ContentBlock::text("Second line"),
]);
assert_eq!(result.text(), "First line\nSecond line");
assert_eq!(result.first_text(), Some("First line"));
assert!(!result.is_error);
}
#[test]
fn test_tool_result_text_extraction_empty() {
let result = ToolCallResult::success(vec![ContentBlock::image("data", "image/png")]);
assert_eq!(result.text(), "");
assert_eq!(result.first_text(), None);
}
#[test]
fn test_tool_result_error() {
let result = ToolCallResult::error("Something went wrong");
assert!(result.is_error);
assert_eq!(result.text(), "Something went wrong");
}
#[test]
fn test_tool_result_deserialize() {
let json = r#"{
"content": [
{"type": "text", "text": "Hello, world!"}
],
"is_error": false
}"#;
let result: ToolCallResult = serde_json::from_str(json).unwrap();
assert!(!result.is_error);
assert_eq!(result.content.len(), 1);
assert_eq!(result.first_text(), Some("Hello, world!"));
}
#[test]
fn test_content_block_text() {
let block = ContentBlock::text("Hello");
assert!(block.is_text());
assert!(!block.is_image());
assert!(!block.is_resource());
assert_eq!(block.text, Some("Hello".to_string()));
}
#[test]
fn test_content_block_image() {
let block = ContentBlock::image("SGVsbG8=", "image/png");
assert!(block.is_image());
assert!(!block.is_text());
assert_eq!(block.data, Some("SGVsbG8=".to_string()));
assert_eq!(block.mime_type, Some("image/png".to_string()));
}
#[test]
fn test_content_block_resource() {
let resource = ResourceContent::new("file:///tmp/test.txt").with_text("File content");
let block = ContentBlock::resource(resource);
assert!(block.is_resource());
assert!(!block.is_text());
assert!(block.resource.is_some());
assert_eq!(block.resource.unwrap().uri, "file:///tmp/test.txt");
}
#[test]
fn test_content_block_deserialize() {
let json = r#"{
"type": "text",
"text": "Hello from MCP"
}"#;
let block: ContentBlock = serde_json::from_str(json).unwrap();
assert_eq!(block.content_type, "text");
assert_eq!(block.text, Some("Hello from MCP".to_string()));
}
#[test]
fn test_resource_content_builder() {
let resource = ResourceContent::new("neo4j://entity/qr-code")
.with_mime_type("application/json")
.with_text(r#"{"name": "QR Code"}"#);
assert_eq!(resource.uri, "neo4j://entity/qr-code");
assert_eq!(resource.mime_type, Some("application/json".to_string()));
assert_eq!(resource.text, Some(r#"{"name": "QR Code"}"#.to_string()));
}
#[test]
fn test_resource_content_deserialize() {
let json = r#"{
"uri": "file:///tmp/data.json",
"mimeType": "application/json",
"text": "{\"key\": \"value\"}"
}"#;
let resource: ResourceContent = serde_json::from_str(json).unwrap();
assert_eq!(resource.uri, "file:///tmp/data.json");
assert_eq!(resource.mime_type, Some("application/json".to_string()));
}
#[test]
fn test_tool_definition_builder() {
let schema = serde_json::json!({
"type": "object",
"properties": {
"entity": {"type": "string"},
"locale": {"type": "string"}
},
"required": ["entity"]
});
let tool = ToolDefinition::new("novanet_generate")
.with_description("Generate native content for an entity")
.with_input_schema(schema.clone());
assert_eq!(tool.name, "novanet_generate");
assert_eq!(
tool.description,
Some("Generate native content for an entity".to_string())
);
assert_eq!(tool.input_schema, Some(schema));
}
#[test]
fn test_tool_definition_deserialize() {
let json = r#"{
"name": "read_resource",
"description": "Read a resource from the server",
"inputSchema": {
"type": "object",
"properties": {
"uri": {"type": "string"}
}
}
}"#;
let tool: ToolDefinition = serde_json::from_str(json).unwrap();
assert_eq!(tool.name, "read_resource");
assert_eq!(
tool.description,
Some("Read a resource from the server".to_string())
);
assert!(tool.input_schema.is_some());
}
#[test]
fn test_tool_definition_minimal() {
let json = r#"{"name": "ping"}"#;
let tool: ToolDefinition = serde_json::from_str(json).unwrap();
assert_eq!(tool.name, "ping");
assert!(tool.description.is_none());
assert!(tool.input_schema.is_none());
}
#[test]
fn test_mcp_error_code_standard_codes() {
assert_eq!(McpErrorCode::from_code(-32700), McpErrorCode::ParseError);
assert_eq!(
McpErrorCode::from_code(-32600),
McpErrorCode::InvalidRequest
);
assert_eq!(
McpErrorCode::from_code(-32601),
McpErrorCode::MethodNotFound
);
assert_eq!(McpErrorCode::from_code(-32602), McpErrorCode::InvalidParams);
assert_eq!(McpErrorCode::from_code(-32603), McpErrorCode::InternalError);
}
#[test]
fn test_mcp_error_code_server_error_range() {
let code = McpErrorCode::from_code(-32050);
assert!(matches!(code, McpErrorCode::ServerError(-32050)));
assert!(code.is_server_error());
assert!(!code.is_client_error());
}
#[test]
fn test_mcp_error_code_unknown() {
let code = McpErrorCode::from_code(42);
assert!(matches!(code, McpErrorCode::Unknown(42)));
assert!(!code.is_server_error());
assert!(!code.is_client_error());
}
#[test]
fn test_mcp_error_code_client_errors() {
assert!(McpErrorCode::ParseError.is_client_error());
assert!(McpErrorCode::InvalidRequest.is_client_error());
assert!(McpErrorCode::InvalidParams.is_client_error());
assert!(!McpErrorCode::MethodNotFound.is_client_error());
assert!(!McpErrorCode::InternalError.is_client_error());
}
#[test]
fn test_mcp_error_code_server_errors() {
assert!(McpErrorCode::MethodNotFound.is_server_error());
assert!(McpErrorCode::InternalError.is_server_error());
assert!(McpErrorCode::ServerError(-32050).is_server_error());
assert!(!McpErrorCode::ParseError.is_server_error());
}
#[test]
fn test_mcp_error_code_display() {
let code = McpErrorCode::InvalidParams;
let display = format!("{}", code);
assert!(display.contains("-32602"));
assert!(display.contains("Invalid method parameter"));
}
#[test]
fn test_mcp_error_code_serde_roundtrip() {
let original = McpErrorCode::InvalidParams;
let json = serde_json::to_string(&original).unwrap();
assert_eq!(json, "-32602");
let parsed: McpErrorCode = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, original);
}
#[test]
fn test_mcp_error_code_into_i32() {
let code = McpErrorCode::ParseError;
let num: i32 = code.into();
assert_eq!(num, -32700);
}
}