crtx-mcp 0.1.2

MCP stdio JSON-RPC 2.0 server for Cortex — tool dispatch, ToolHandler trait, gate wiring (ADR 0045).
Documentation
//! [`ToolRegistry`] — owns the list of registered [`ToolHandler`]s and
//! dispatches incoming JSON-RPC method calls to the correct handler.
//!
//! ## Gate-wiring validation (DA-3)
//!
//! [`ToolRegistry::register`] asserts at registration time — before the stdio
//! loop starts — that every handler declares at least one gate in its
//! [`gate_set`](crate::tool_handler::ToolHandler::gate_set). An empty
//! `gate_set` is a programming error: it means the tool has been wired with no
//! admission control, which silently violates the gate-equivalence requirement
//! of ADR 0045 §3.
//!
//! The assertion fires a `panic!` during startup, causing the process to exit
//! before accepting any request. The CLI maps startup panics to
//! `Exit::Internal`.

use crate::tool_handler::{ToolError, ToolHandler};

/// Owns all registered [`ToolHandler`]s and routes JSON-RPC calls to them.
///
/// Build the registry with [`ToolRegistry::new`], populate it with
/// [`ToolRegistry::register`], then pass it to
/// [`crate::serve::run_stdio_server`].
#[derive(Default)]
pub struct ToolRegistry {
    handlers: Vec<Box<dyn ToolHandler>>,
}

impl std::fmt::Debug for ToolRegistry {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let names: Vec<&str> = self.handlers.iter().map(|h| h.name()).collect();
        f.debug_struct("ToolRegistry")
            .field("tools", &names)
            .finish()
    }
}

impl ToolRegistry {
    /// Create an empty registry.
    pub fn new() -> Self {
        Self::default()
    }

    /// Register a handler.
    ///
    /// # Panics
    ///
    /// Panics if `handler.gate_set()` is empty. An empty gate set means the
    /// tool has no admission gates declared — a fatal wiring error (DA-3).
    pub fn register(&mut self, handler: Box<dyn ToolHandler>) {
        assert!(
            !handler.gate_set().is_empty(),
            "tool '{}' declares no gates — gate wiring validation failed (DA-3)",
            handler.name()
        );
        tracing::debug!(tool = handler.name(), "registered tool handler");
        self.handlers.push(handler);
    }

    /// Dispatch a JSON-RPC method call to the matching handler.
    ///
    /// Returns:
    /// - `None` if no handler is registered for `method` (caller should return
    ///   a `-32601` method-not-found error).
    /// - `Some(Ok(value))` on success.
    /// - `Some(Err(e))` if the handler returned a [`ToolError`].
    pub fn dispatch(
        &self,
        method: &str,
        params: serde_json::Value,
    ) -> Option<Result<serde_json::Value, ToolError>> {
        self.handlers
            .iter()
            .find(|h| h.name() == method)
            .map(|h| h.call(params))
    }
}