Skip to main content

cortex_mcp/
tool_handler.rs

1//! `ToolHandler` trait and associated types for MCP tool dispatch.
2//!
3//! Every tool in `cortex-mcp` must implement [`ToolHandler`] and declare a
4//! non-empty [`gate_set`](ToolHandler::gate_set). The [`ToolRegistry`] asserts
5//! gate wiring at registration time (DA-3): a missing gate is a fatal startup
6//! error, not a per-call error.
7//!
8//! [`ToolRegistry`]: crate::tool_registry::ToolRegistry
9
10use std::fmt;
11
12/// Logical gate IDs that a tool activates when called.
13///
14/// These map 1-to-1 with the gate equivalents enforced by the CLI. A tool
15/// that reads FTS data must declare [`GateId::FtsRead`]; a tool that writes
16/// session state must declare [`GateId::SessionWrite`] and
17/// [`GateId::CommitWrite`].
18///
19/// The full set declared here reflects the gates needed by the five stable
20/// tools defined in ADR 0045 §2. Future tools must add gate IDs here before
21/// implementing.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub enum GateId {
24    /// Gate for FTS5 full-text search reads (`cortex_search`).
25    FtsRead,
26    /// Gate for embedding-index reads (`cortex_search` with `semantic: true`).
27    EmbeddingRead,
28    /// Gate for context-pack reads (`cortex_context`).
29    ContextRead,
30    /// Gate for memory-health count reads (`cortex_memory_health`).
31    HealthRead,
32    /// Gate for session-event write path (`cortex_session_close`).
33    SessionWrite,
34    /// Gate for ledger/commit write path (`cortex_session_close`).
35    CommitWrite,
36}
37
38/// Errors returned from [`ToolHandler::call`].
39///
40/// Each variant maps to a JSON-RPC `-32000` application-error response.
41/// The [`serve`](crate::serve) loop converts these to the wire format.
42#[derive(Debug)]
43pub enum ToolError {
44    /// The caller supplied parameters that do not match the tool schema.
45    InvalidParams(String),
46    /// A policy gate returned `Reject`, `Quarantine`, or `BreakGlass`.
47    ///
48    /// Per ADR 0045 §3, a BreakGlass composed outcome is treated as Reject at
49    /// the MCP boundary. No write occurs when this variant is returned.
50    PolicyRejected(String),
51    /// The incoming payload exceeds the server-side size limit (RT-5).
52    SizeLimitExceeded(String),
53    /// An unexpected internal error that is not one of the above categories.
54    Internal(String),
55}
56
57impl fmt::Display for ToolError {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        match self {
60            ToolError::InvalidParams(msg) => write!(f, "invalid params: {msg}"),
61            ToolError::PolicyRejected(msg) => write!(f, "policy rejected: {msg}"),
62            ToolError::SizeLimitExceeded(msg) => write!(f, "size limit exceeded: {msg}"),
63            ToolError::Internal(msg) => write!(f, "internal error: {msg}"),
64        }
65    }
66}
67
68/// A single MCP tool that can be dispatched by the [`ToolRegistry`].
69///
70/// Implementors must be `Send + Sync` because the registry may be held across
71/// thread boundaries in future async-capable transports.
72///
73/// [`ToolRegistry`]: crate::tool_registry::ToolRegistry
74pub trait ToolHandler: Send + Sync {
75    /// The JSON-RPC method name this handler responds to.
76    ///
77    /// Must be unique across all handlers registered in the same
78    /// [`ToolRegistry`]. The convention is `cortex_<verb>`.
79    fn name(&self) -> &'static str;
80
81    /// Gate IDs this tool activates.
82    ///
83    /// Must be non-empty. [`ToolRegistry::register`] asserts this at
84    /// registration time (DA-3).
85    ///
86    /// [`ToolRegistry::register`]: crate::tool_registry::ToolRegistry::register
87    fn gate_set(&self) -> &'static [GateId];
88
89    /// Execute the tool with the given JSON params, returning a JSON result.
90    ///
91    /// The `params` value is the raw `"params"` field from the JSON-RPC
92    /// request. If the request omitted `"params"`, the serve loop passes
93    /// `serde_json::Value::Null`.
94    fn call(&self, params: serde_json::Value) -> Result<serde_json::Value, ToolError>;
95}