Skip to main content

chant/mcp/
mod.rs

1//! Model Context Protocol (MCP) server implementation.
2//!
3//! # Doc Audit
4//! - audited: 2026-01-25
5//! - docs: reference/mcp.md
6//! - ignore: false
7
8pub mod handlers;
9pub mod protocol;
10pub mod server;
11pub mod tools;
12
13pub use handlers::{handle_method, handle_notification};
14pub use server::run_server;
15
16#[cfg(test)]
17mod tests {
18    use super::handlers::{handle_method, handle_notification};
19    use super::protocol::{JsonRpcResponse, PROTOCOL_VERSION, SERVER_NAME};
20    use serde_json::json;
21
22    #[test]
23    fn test_handle_initialize() {
24        let result = handle_method("initialize", None).unwrap();
25        assert_eq!(result["protocolVersion"], PROTOCOL_VERSION);
26        assert_eq!(result["serverInfo"]["name"], SERVER_NAME);
27    }
28
29    #[test]
30    fn test_handle_tools_list() {
31        let result = handle_method("tools/list", None).unwrap();
32        let tools = result["tools"].as_array().unwrap();
33        assert_eq!(tools.len(), 24); // Added chant_resume as deprecated alias
34                                     // Query tools (8)
35        assert_eq!(tools[0]["name"], "chant_spec_list");
36        assert_eq!(tools[1]["name"], "chant_spec_get");
37        assert_eq!(tools[2]["name"], "chant_ready");
38        assert_eq!(tools[3]["name"], "chant_status");
39        assert_eq!(tools[4]["name"], "chant_log");
40        assert_eq!(tools[5]["name"], "chant_search");
41        assert_eq!(tools[6]["name"], "chant_diagnose");
42        assert_eq!(tools[7]["name"], "chant_lint");
43        // Mutating tools (12)
44        assert_eq!(tools[8]["name"], "chant_spec_update");
45        assert_eq!(tools[9]["name"], "chant_add");
46        assert_eq!(tools[10]["name"], "chant_finalize");
47        assert_eq!(tools[11]["name"], "chant_reset");
48        assert_eq!(tools[12]["name"], "chant_resume"); // deprecated alias
49        assert_eq!(tools[13]["name"], "chant_cancel");
50        assert_eq!(tools[14]["name"], "chant_archive");
51        assert_eq!(tools[15]["name"], "chant_verify");
52        assert_eq!(tools[16]["name"], "chant_work_start");
53        assert_eq!(tools[17]["name"], "chant_work_list");
54        assert_eq!(tools[18]["name"], "chant_pause");
55        assert_eq!(tools[19]["name"], "chant_takeover");
56    }
57
58    #[test]
59    fn test_json_rpc_response_success() {
60        let resp = JsonRpcResponse::success(json!(1), json!({"test": true}));
61        assert_eq!(resp.jsonrpc, "2.0");
62        assert!(resp.result.is_some());
63        assert!(resp.error.is_none());
64    }
65
66    #[test]
67    fn test_json_rpc_response_error() {
68        let resp = JsonRpcResponse::error(json!(1), -32600, "Invalid request");
69        assert_eq!(resp.jsonrpc, "2.0");
70        assert!(resp.result.is_none());
71        assert!(resp.error.is_some());
72        assert_eq!(resp.error.as_ref().unwrap().code, -32600);
73    }
74
75    #[test]
76    fn test_chant_status_schema_has_brief_and_activity() {
77        let result = handle_method("tools/list", None).unwrap();
78        let tools = result["tools"].as_array().unwrap();
79        let status_tool = tools.iter().find(|t| t["name"] == "chant_status").unwrap();
80
81        let props = &status_tool["inputSchema"]["properties"];
82        assert!(
83            props.get("brief").is_some(),
84            "chant_status should have 'brief' property"
85        );
86        assert!(
87            props.get("include_activity").is_some(),
88            "chant_status should have 'include_activity' property"
89        );
90
91        // Check descriptions
92        assert!(props["brief"]["description"]
93            .as_str()
94            .unwrap()
95            .contains("single-line"));
96        assert!(props["include_activity"]["description"]
97            .as_str()
98            .unwrap()
99            .contains("activity"));
100    }
101
102    #[test]
103    fn test_chant_ready_has_limit_param() {
104        let result = handle_method("tools/list", None).unwrap();
105        let tools = result["tools"].as_array().unwrap();
106        let ready_tool = tools.iter().find(|t| t["name"] == "chant_ready").unwrap();
107
108        let props = &ready_tool["inputSchema"]["properties"];
109        assert!(
110            props.get("limit").is_some(),
111            "chant_ready should have 'limit' property"
112        );
113        assert_eq!(props["limit"]["type"], "integer");
114        assert!(props["limit"]["description"]
115            .as_str()
116            .unwrap()
117            .contains("50"));
118    }
119
120    #[test]
121    fn test_chant_spec_list_has_limit_param() {
122        let result = handle_method("tools/list", None).unwrap();
123        let tools = result["tools"].as_array().unwrap();
124        let list_tool = tools
125            .iter()
126            .find(|t| t["name"] == "chant_spec_list")
127            .unwrap();
128
129        let props = &list_tool["inputSchema"]["properties"];
130        assert!(
131            props.get("limit").is_some(),
132            "chant_spec_list should have 'limit' property"
133        );
134        assert_eq!(props["limit"]["type"], "integer");
135        assert!(props["limit"]["description"]
136            .as_str()
137            .unwrap()
138            .contains("50"));
139    }
140
141    #[test]
142    fn test_handle_notification() {
143        // Should not panic
144        handle_notification("notifications/initialized", None);
145        handle_notification("unknown_notification", None);
146    }
147}