crtx-mcp 0.1.1

MCP stdio JSON-RPC 2.0 server for Cortex — tool dispatch, ToolHandler trait, gate wiring (ADR 0045).
Documentation
//! `ToolHandler` trait and associated types for MCP tool dispatch.
//!
//! Every tool in `cortex-mcp` must implement [`ToolHandler`] and declare a
//! non-empty [`gate_set`](ToolHandler::gate_set). The [`ToolRegistry`] asserts
//! gate wiring at registration time (DA-3): a missing gate is a fatal startup
//! error, not a per-call error.
//!
//! [`ToolRegistry`]: crate::tool_registry::ToolRegistry

use std::fmt;

/// Logical gate IDs that a tool activates when called.
///
/// These map 1-to-1 with the gate equivalents enforced by the CLI. A tool
/// that reads FTS data must declare [`GateId::FtsRead`]; a tool that writes
/// session state must declare [`GateId::SessionWrite`] and
/// [`GateId::CommitWrite`].
///
/// The full set declared here reflects the gates needed by the five stable
/// tools defined in ADR 0045 §2. Future tools must add gate IDs here before
/// implementing.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum GateId {
    /// Gate for FTS5 full-text search reads (`cortex_search`).
    FtsRead,
    /// Gate for embedding-index reads (`cortex_search` with `semantic: true`).
    EmbeddingRead,
    /// Gate for context-pack reads (`cortex_context`).
    ContextRead,
    /// Gate for memory-health count reads (`cortex_memory_health`).
    HealthRead,
    /// Gate for session-event write path (`cortex_session_close`).
    SessionWrite,
    /// Gate for ledger/commit write path (`cortex_session_close`).
    CommitWrite,
}

/// Errors returned from [`ToolHandler::call`].
///
/// Each variant maps to a JSON-RPC `-32000` application-error response.
/// The [`serve`](crate::serve) loop converts these to the wire format.
#[derive(Debug)]
pub enum ToolError {
    /// The caller supplied parameters that do not match the tool schema.
    InvalidParams(String),
    /// A policy gate returned `Reject`, `Quarantine`, or `BreakGlass`.
    ///
    /// Per ADR 0045 §3, a BreakGlass composed outcome is treated as Reject at
    /// the MCP boundary. No write occurs when this variant is returned.
    PolicyRejected(String),
    /// The incoming payload exceeds the server-side size limit (RT-5).
    SizeLimitExceeded(String),
    /// An unexpected internal error that is not one of the above categories.
    Internal(String),
}

impl fmt::Display for ToolError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ToolError::InvalidParams(msg) => write!(f, "invalid params: {msg}"),
            ToolError::PolicyRejected(msg) => write!(f, "policy rejected: {msg}"),
            ToolError::SizeLimitExceeded(msg) => write!(f, "size limit exceeded: {msg}"),
            ToolError::Internal(msg) => write!(f, "internal error: {msg}"),
        }
    }
}

/// A single MCP tool that can be dispatched by the [`ToolRegistry`].
///
/// Implementors must be `Send + Sync` because the registry may be held across
/// thread boundaries in future async-capable transports.
///
/// [`ToolRegistry`]: crate::tool_registry::ToolRegistry
pub trait ToolHandler: Send + Sync {
    /// The JSON-RPC method name this handler responds to.
    ///
    /// Must be unique across all handlers registered in the same
    /// [`ToolRegistry`]. The convention is `cortex_<verb>`.
    fn name(&self) -> &'static str;

    /// Gate IDs this tool activates.
    ///
    /// Must be non-empty. [`ToolRegistry::register`] asserts this at
    /// registration time (DA-3).
    ///
    /// [`ToolRegistry::register`]: crate::tool_registry::ToolRegistry::register
    fn gate_set(&self) -> &'static [GateId];

    /// Execute the tool with the given JSON params, returning a JSON result.
    ///
    /// The `params` value is the raw `"params"` field from the JSON-RPC
    /// request. If the request omitted `"params"`, the serve loop passes
    /// `serde_json::Value::Null`.
    fn call(&self, params: serde_json::Value) -> Result<serde_json::Value, ToolError>;
}