Skip to main content

lattice_ai/mcp/
protocol.rs

1//! MCP envelope payloads carried inside the JSON-RPC frames (the
2//! `lattice_protocol::jsonrpc` types are the envelope; these are the
3//! method-specific bodies).
4//!
5//! The wire contract must match VS Code's so the stock `claude` CLI talks
6//! to lattice unchanged: MCP protocol version `2024-11-05`, a `tools`
7//! capability with `listChanged`, and the tool catalog enumerated by
8//! `tools/list`.
9
10use serde::{Deserialize, Serialize};
11use serde_json::{Value, json};
12
13/// MCP protocol version lattice advertises. Must match the VS Code
14/// contract the `claude` CLI expects.
15pub const MCP_PROTOCOL_VERSION: &str = "2024-11-05";
16
17/// Server name reported in the `initialize` result.
18pub const SERVER_NAME: &str = "lattice";
19
20/// One MCP tool descriptor as enumerated by `tools/list`.
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct ToolDescriptor {
23    /// Tool name the agent invokes via `tools/call`.
24    pub name: String,
25    /// Human-readable description shown by the agent.
26    pub description: String,
27    /// JSON Schema for the tool's arguments.
28    #[serde(rename = "inputSchema")]
29    pub input_schema: Value,
30}
31
32impl ToolDescriptor {
33    fn new(name: &str, description: &str, input_schema: Value) -> Self {
34        Self {
35            name: name.to_string(),
36            description: description.to_string(),
37            input_schema,
38        }
39    }
40}
41
42/// An object schema with no required properties — the placeholder used by
43/// the parameterless read tools until I2 fills in real schemas.
44fn empty_object_schema() -> Value {
45    json!({ "type": "object", "properties": {} })
46}
47
48/// The full tool catalog lattice advertises. The tools are **stubbed** in
49/// I1 (`tools/call` returns "not implemented"); reads land in I2, writes
50/// in I3, `openDiff` in I4. Enumerating them now lets a real `claude` CLI
51/// validate the catalog against the VS Code contract during the I0–I2
52/// walking-skeleton phase.
53pub fn tool_catalog() -> Vec<ToolDescriptor> {
54    vec![
55        // Reads (I2).
56        ToolDescriptor::new(
57            "getCurrentSelection",
58            "Return the active buffer's current selection.",
59            empty_object_schema(),
60        ),
61        ToolDescriptor::new(
62            "getOpenEditors",
63            "List the open editor buffers.",
64            empty_object_schema(),
65        ),
66        ToolDescriptor::new(
67            "getWorkspaceFolders",
68            "List the open workspace folders.",
69            empty_object_schema(),
70        ),
71        ToolDescriptor::new(
72            "getDiagnostics",
73            "Return diagnostics for the workspace or a given file.",
74            json!({
75                "type": "object",
76                "properties": { "uri": { "type": "string" } }
77            }),
78        ),
79        ToolDescriptor::new(
80            "checkDocumentDirty",
81            "Report whether a document has unsaved changes.",
82            json!({
83                "type": "object",
84                "properties": { "filePath": { "type": "string" } },
85                "required": ["filePath"]
86            }),
87        ),
88        // Writes (I3).
89        ToolDescriptor::new(
90            "openFile",
91            "Open a file in the editor, optionally selecting a range.",
92            json!({
93                "type": "object",
94                "properties": { "filePath": { "type": "string" } },
95                "required": ["filePath"]
96            }),
97        ),
98        ToolDescriptor::new(
99            "saveDocument",
100            "Save a document to disk.",
101            json!({
102                "type": "object",
103                "properties": { "filePath": { "type": "string" } },
104                "required": ["filePath"]
105            }),
106        ),
107        ToolDescriptor::new(
108            "close_tab",
109            "Close an open tab by name.",
110            json!({
111                "type": "object",
112                "properties": { "tab_name": { "type": "string" } },
113                "required": ["tab_name"]
114            }),
115        ),
116        // D-fix.6: close every diff this session opened (used when the agent
117        // abandons a proposed edit). Scoped to the calling connection.
118        ToolDescriptor::new(
119            "closeAllDiffTabs",
120            "Close all diff tabs opened by this session.",
121            json!({ "type": "object", "properties": {} }),
122        ),
123        // Blocking (I4).
124        ToolDescriptor::new(
125            "openDiff",
126            "Open an interactive diff the user Keeps or Rejects; blocks until resolved.",
127            json!({
128                "type": "object",
129                "properties": {
130                    "old_file_path": { "type": "string" },
131                    "new_file_path": { "type": "string" },
132                    "new_file_contents": { "type": "string" },
133                    "tab_name": { "type": "string" }
134                }
135            }),
136        ),
137    ]
138}
139
140/// The `initialize` result body advertised to the agent.
141pub fn initialize_result() -> Value {
142    json!({
143        "protocolVersion": MCP_PROTOCOL_VERSION,
144        "capabilities": { "tools": { "listChanged": true } },
145        "serverInfo": { "name": SERVER_NAME, "version": env!("CARGO_PKG_VERSION") }
146    })
147}
148
149/// The `tools/list` result body (the enumerated catalog).
150pub fn tools_list_result() -> Value {
151    json!({ "tools": tool_catalog() })
152}
153
154/// The `prompts/list` result body — empty; lattice exposes no MCP prompts.
155pub fn prompts_list_result() -> Value {
156    json!({ "prompts": [] })
157}