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}