klieo-tools-mcp 3.5.0

MCP (Model Context Protocol) adapter for klieo-core's Tool trait.
Documentation
//! In-tree MCP server fixture for integration tests.
//!
//! The advertised protocol version is pinned to `MCP_PROTOCOL_VERSION`
//! so the conformance test can assert the negotiated version the client
//! observes matches the single crate constant.
//!
//! The rmcp server stack is gated behind the `test-utils` feature so the
//! production CLIENT crate never pulls it into a default build; the binary
//! still compiles without the feature (as an unusable stub) so downstream
//! crates can list `klieo-tools-mcp` as a plain dev-dependency.
//!
//! [`TokioChildProcess`]: rmcp::transport::TokioChildProcess

#[cfg(feature = "test-utils")]
use klieo_tools_mcp::MCP_PROTOCOL_VERSION;
#[cfg(feature = "test-utils")]
use rmcp::handler::server::ServerHandler;
#[cfg(feature = "test-utils")]
use rmcp::model::{
    CallToolRequestParams, CallToolResult, Content, Implementation, InitializeResult,
    ListToolsResult, ProtocolVersion, ServerCapabilities, ServerInfo, Tool,
};
#[cfg(feature = "test-utils")]
use rmcp::service::{RequestContext, RoleServer};
#[cfg(feature = "test-utils")]
use rmcp::{ErrorData as McpError, ServiceExt};
#[cfg(feature = "test-utils")]
use std::borrow::Cow;
#[cfg(feature = "test-utils")]
use std::sync::Arc;

#[cfg(feature = "test-utils")]
const ECHO_TOOL_NAME: &str = "echo";

/// Fixed payload the `echo` tool returns, regardless of arguments. Tests
/// assert this exact string to prove the client→server call round-trips.
#[cfg(feature = "test-utils")]
const ECHO_REPLY: &str = "hello-mcp";

#[cfg(feature = "test-utils")]
#[derive(Clone)]
struct EchoServer;

#[cfg(feature = "test-utils")]
impl ServerHandler for EchoServer {
    fn get_info(&self) -> ServerInfo {
        InitializeResult::new(ServerCapabilities::builder().enable_tools().build())
            .with_protocol_version(pinned_protocol_version())
            .with_server_info(Implementation::new(
                "mcp-echo-server",
                env!("CARGO_PKG_VERSION"),
            ))
    }

    async fn list_tools(
        &self,
        _request: Option<rmcp::model::PaginatedRequestParams>,
        _context: RequestContext<RoleServer>,
    ) -> Result<ListToolsResult, McpError> {
        Ok(ListToolsResult::with_all_items(vec![echo_tool()]))
    }

    async fn call_tool(
        &self,
        request: CallToolRequestParams,
        _context: RequestContext<RoleServer>,
    ) -> Result<CallToolResult, McpError> {
        if request.name != ECHO_TOOL_NAME {
            return Err(McpError::invalid_params(
                format!("unknown tool: {}", request.name),
                None,
            ));
        }
        Ok(CallToolResult::success(vec![Content::text(ECHO_REPLY)]))
    }
}

/// Tool definition for `echo`. Schema accepts an optional `message`
/// string; the reply is fixed regardless of input.
#[cfg(feature = "test-utils")]
fn echo_tool() -> Tool {
    let schema = serde_json::json!({
        "type": "object",
        "properties": { "message": { "type": "string" } },
        "additionalProperties": false
    });
    let schema = schema.as_object().cloned().unwrap_or_default();
    Tool::new(
        Cow::Borrowed(ECHO_TOOL_NAME),
        Cow::Borrowed("Returns a fixed greeting as a text content block"),
        Arc::new(schema),
    )
}

/// The crate constant is the single source of truth; this binary panics
/// at startup if it ever drifts to a value rmcp cannot represent, which
/// surfaces in the test subprocess as a spawn/handshake failure.
#[cfg(feature = "test-utils")]
fn pinned_protocol_version() -> ProtocolVersion {
    serde_json::from_value(serde_json::Value::String(MCP_PROTOCOL_VERSION.to_string()))
        .expect("MCP_PROTOCOL_VERSION must be a protocol version rmcp recognises")
}

#[cfg(feature = "test-utils")]
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let transport = rmcp::transport::io::stdio();
    let service = EchoServer.serve(transport).await?;
    service.waiting().await?;
    Ok(())
}

/// Stub entry point compiled when the `test-utils` feature is off. The
/// fixture's rmcp server stack is unavailable, so the binary refuses to
/// run rather than pretend to serve.
#[cfg(not(feature = "test-utils"))]
fn main() {
    eprintln!("mcp-echo-server is a test fixture; rebuild with --features test-utils to run it");
    std::process::exit(1);
}