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