descry-tool-core 0.3.1

Core traits and types for descry-tool framework
Documentation
//! MCP (Model Context Protocol) adapter
//!
//! Converts tools to MCP format for use with MCP-compatible LLM platforms.

use serde::{Deserialize, Serialize};

use crate::adapters::ToolAdapter;
use crate::ToolMeta;

/// MCP tool specification
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpToolSpec {
    /// Tool name
    pub name: String,
    /// Tool description
    pub description: String,
    /// Input schema (JSON Schema)
    pub input_schema: serde_json::Value,
    /// Optional input examples
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub examples: Vec<McpExample>,
}

/// MCP example
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpExample {
    /// Example name
    pub name: String,
    /// Example input (JSON object)
    pub input: serde_json::Value,
}

/// MCP call request
#[derive(Debug, Clone, Deserialize)]
pub struct McpCallRequest {
    /// Tool name
    pub name: String,
    /// Tool arguments
    pub arguments: serde_json::Value,
}

/// MCP call response
#[derive(Debug, Clone, Serialize)]
pub struct McpCallResponse {
    /// Tool output
    pub content: Vec<McpContent>,
    /// Whether the tool call resulted in an error
    #[serde(skip_serializing_if = "Option::is_none")]
    pub is_error: Option<bool>,
}

fn is_false() -> bool {
    false
}

/// MCP content block
#[derive(Debug, Clone, Serialize)]
pub struct McpContent {
    /// Content type
    #[serde(rename = "type")]
    pub content_type: String,
    /// Text content
    pub text: String,
}

/// MCP adapter
pub struct McpAdapter;

impl ToolAdapter for McpAdapter {
    type ToolSpec = McpToolSpec;
    type CallRequest = McpCallRequest;
    type CallResponse = McpCallResponse;

    fn to_spec(meta: &ToolMeta) -> McpToolSpec {
        let examples: Vec<McpExample> = (meta.examples)()
            .iter()
            .map(|(name, json_str)| {
                McpExample {
                    name: name.to_string(),
                    input: serde_json::from_str(json_str).unwrap_or(serde_json::json!({})),
                }
            })
            .collect();

        McpToolSpec {
            name: meta.name.to_string(),
            description: meta.description.to_string(),
            input_schema: (meta.schema)().clone(),
            examples,
        }
    }

    fn from_request(req: McpCallRequest) -> Result<(String, serde_json::Value), crate::ToolError> {
        Ok((req.name, req.arguments))
    }

    fn to_response(output: serde_json::Value) -> McpCallResponse {
        McpCallResponse {
            content: vec![McpContent {
                content_type: "text".to_string(),
                text: serde_json::to_string(&output).unwrap_or_else(|_| output.to_string()),
            }],
            is_error: None,
        }
    }
}

impl McpAdapter {
    /// Create error response
    pub fn error_response(error_message: String) -> McpCallResponse {
        McpCallResponse {
            content: vec![McpContent {
                content_type: "text".to_string(),
                text: error_message,
            }],
            is_error: Some(true),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{Tool, ToolContext, ToolError};
    use schemars::JsonSchema;
    use serde::{Deserialize, Serialize};
    use std::sync::Arc;

    #[derive(Deserialize, JsonSchema)]
    struct TestParams {
        value: i32,
    }

    #[derive(Serialize, JsonSchema)]
    struct TestOutput {
        result: i32,
    }

    struct TestTool;

    impl Tool for TestTool {
        type Params = TestParams;
        type Output = TestOutput;
        const NAME: &'static str = "test";
        const DESCRIPTION: &'static str = "Test tool";

        async fn call(
            _ctx: Arc<ToolContext>,
            params: Self::Params,
        ) -> Result<Self::Output, ToolError> {
            Ok(TestOutput {
                result: params.value * 2,
            })
        }
    }

    inventory::submit! {
        crate::ToolMeta {
            name: TestTool::NAME,
            description: TestTool::DESCRIPTION,
            call: |ctx, params| {
                Box::pin(async move {
                    let params: TestParams = serde_json::from_value(params)?;
                    let result = <TestTool as Tool>::call(ctx, params).await?;
                    Ok(serde_json::to_value(result)?)
                })
            },
            schema: || <TestTool as Tool>::schema(),
            examples: || <TestTool as Tool>::EXAMPLES,
        }
    }

    #[test]
    fn test_to_spec() {
        let meta = crate::find_tool("test").unwrap();
        let spec = McpAdapter::to_spec(meta);
        
        assert_eq!(spec.name, "test");
        assert_eq!(spec.description, "Test tool");
        assert!(spec.input_schema.is_object());
    }

    #[test]
    fn test_from_request() {
        let req = McpCallRequest {
            name: "test".to_string(),
            arguments: serde_json::json!({"value": 5}),
        };
        
        let (name, params) = McpAdapter::from_request(req).unwrap();
        assert_eq!(name, "test");
        assert_eq!(params["value"], 5);
    }

    #[test]
    fn test_to_response() {
        let output = serde_json::json!({"result": 10});
        let response = McpAdapter::to_response(output);
        
        assert_eq!(response.content.len(), 1);
        assert_eq!(response.content[0].content_type, "text");
        assert_eq!(response.is_error, None);
    }

    #[test]
    fn test_error_response() {
        let response = McpAdapter::error_response("Test error".to_string());
        assert_eq!(response.is_error, Some(true));
        assert_eq!(response.content[0].text, "Test error");
    }
}