Skip to content

Commit 76cb6fb

Browse files
authored
feat: complete MCP tools compliance with VS Code extension specs (#57)
* feat: implement complete MCP tools compliance with VS Code extension specs Complete implementation of MCP (Model Context Protocol) tool compliance by: - Adding 2 missing tools from VS Code extension - Converting all tool outputs to MCP-compliant format - Exposing internal tools via MCP while preserving existing functionality - Comprehensive test suite updates with robust JSON handling - Updated documentation to reflect 100% VS Code compatibility - **getLatestSelection**: Get most recent text selection (different from getCurrentSelection) - **closeAllDiffTabs**: Close all diff-related tabs/windows with VS Code-compatible output format All tools now return MCP-compliant format: `{content: [{type: "text", text: "JSON-stringified-data"}]}` - **checkDocumentDirty**: Added schema for MCP exposure, success/failure JSON responses - **saveDocument**: Added schema for MCP exposure, detailed success information - **getWorkspaceFolders**: Added schema for MCP exposure, VS Code-compatible structure - **getOpenEditors**: Restructured from `{editors: [...]}` to `{tabs: [...]}` with all VS Code fields - **getCurrentSelection**: Enhanced with proper fallback behavior and MCP format - **openFile**: Added missing parameters (preview, selectToEndOfLine, makeFrontmost, text selection) - **closeTab**: Updated format while keeping internal-only (per Claude Code requirement) - **Text Selection in openFile**: Full implementation of startText/endText pattern matching - **Conditional Output**: openFile returns simple vs detailed responses based on makeFrontmost - **Language Detection**: getOpenEditors includes proper languageId field mapping - **Error Handling**: Comprehensive JSON-RPC error responses throughout - **Robust JSON encoder/decoder**: Custom implementation supporting nested objects and special keys - **Comprehensive test coverage**: All new tools with unit tests - **Updated test expectations**: All existing tests adapted to MCP format - **Format validation**: Tests verify exact VS Code extension compatibility - **CLAUDE.md**: Complete rewrite of MCP tools section with 100% compliance status - **Development Guidelines**: Added MCP tool development patterns and troubleshooting - **Quality Standards**: Updated to reflect 320+ tests with 100% success rate - **Protocol Compliance**: New section documenting VS Code extension feature parity - **Backward compatibility**: No breaking changes to existing API - **VS Code alignment**: Output formats match official VS Code extension exactly - **Internal tools preserved**: close_tab remains internal as required by Claude Code architecture - ✅ 320 tests passing (0 failures, 0 errors) - ✅ All linting checks passing (0 warnings, 0 errors) - ✅ Full MCP protocol compliance - ✅ VS Code extension feature parity achieved Change-Id: Ic1bd33aadb7fa45d64d4aba208acf37b2c9779cb Signed-off-by: Thomas Kosiewski <tk@coder.com> * fix: address PR review comments for MCP tools compliance - Fix closeAllDiffTabs potential duplicate window closing by using set-based approach instead of array - Add comprehensive test coverage for openFile parameters (makeFrontmost, preview mode, line/text selection) - Update CLAUDE.md documentation to mention endLine parameter for openFile tool - Enhance JSON encoder/decoder in test suite with proper escape sequence handling - Add missing vim API mocks for complex openFile functionality testing All 325 tests now pass with complete coverage of new MCP tool features. Change-Id: I15bceb2bb44552205ea63c5ef1cb83722f7b5893 Signed-off-by: Thomas Kosiewski <tk@coder.com> * fix: correct off-by-one indexing error in openFile text pattern search Address Copilot review comment: when searching for endText pattern, line_idx is already the correct 1-based index for the lines array, so accessing lines[line_idx + 1] was incorrect. Changed to lines[line_idx] to access the current line directly. Change-Id: I05853ff183ef8f3e5df2863d2184a0cb58cb7e65 Signed-off-by: Thomas Kosiewski <tk@coder.com> * feat: enhance MCP tools with VS Code compatibility and success fields Change-Id: Iddb033256d1c8093e871f3a303a95e4df35ef9aa Signed-off-by: Thomas Kosiewski <tk@coder.com> --------- Signed-off-by: Thomas Kosiewski <tk@coder.com>
1 parent fa8d64f commit 76cb6fb

22 files changed

+1762
-142
lines changed

CLAUDE.md

Lines changed: 151 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,28 @@ The WebSocket server implements secure authentication using:
6565
- **Lock File Discovery**: Tokens stored in `~/.claude/ide/[port].lock` for Claude CLI
6666
- **MCP Compliance**: Follows official Claude Code IDE authentication protocol
6767

68-
### MCP Tools Architecture
68+
### MCP Tools Architecture (✅ FULLY COMPLIANT)
6969

70-
Tools are registered with JSON schemas and handlers. MCP-exposed tools include:
70+
**Complete VS Code Extension Compatibility**: All tools now implement identical behavior and output formats as the official VS Code extension.
7171

72-
- `openFile` - Opens files with optional line/text selection
73-
- `getCurrentSelection` - Gets current text selection
74-
- `getOpenEditors` - Lists currently open files
72+
**MCP-Exposed Tools** (with JSON schemas):
73+
74+
- `openFile` - Opens files with optional line/text selection (startLine/endLine), preview mode, text pattern matching, and makeFrontmost flag
75+
- `getCurrentSelection` - Gets current text selection from active editor
76+
- `getLatestSelection` - Gets most recent text selection (even from inactive editors)
77+
- `getOpenEditors` - Lists currently open files with VS Code-compatible `tabs` structure
7578
- `openDiff` - Opens native Neovim diff views
79+
- `checkDocumentDirty` - Checks if document has unsaved changes
80+
- `saveDocument` - Saves document with detailed success/failure reporting
81+
- `getWorkspaceFolders` - Gets workspace folder information
82+
- `closeAllDiffTabs` - Closes all diff-related tabs and windows
83+
- `getDiagnostics` - Gets language diagnostics (errors, warnings) from the editor
84+
85+
**Internal Tools** (not exposed via MCP):
86+
87+
- `close_tab` - Internal-only tool for tab management (hardcoded in Claude Code)
88+
89+
**Format Compliance**: All tools return MCP-compliant format: `{content: [{type: "text", text: "JSON-stringified-data"}]}`
7690

7791
### Key File Locations
7892

@@ -81,6 +95,33 @@ Tools are registered with JSON schemas and handlers. MCP-exposed tools include:
8195
- `plugin/claudecode.lua` - Plugin loader with version checks
8296
- `tests/` - Comprehensive test suite with unit, component, and integration tests
8397

98+
## MCP Protocol Compliance
99+
100+
### Protocol Implementation Status
101+
102+
-**WebSocket Server**: RFC 6455 compliant with MCP message format
103+
-**Tool Registration**: JSON Schema-based tool definitions
104+
-**Authentication**: UUID v4 token-based secure handshake
105+
-**Message Format**: JSON-RPC 2.0 with MCP content structure
106+
-**Error Handling**: Comprehensive JSON-RPC error responses
107+
108+
### VS Code Extension Compatibility
109+
110+
claudecode.nvim implements **100% feature parity** with Anthropic's official VS Code extension:
111+
112+
- **Identical Tool Set**: All 10 VS Code tools implemented
113+
- **Compatible Formats**: Output structures match VS Code extension exactly
114+
- **Behavioral Consistency**: Same parameter handling and response patterns
115+
- **Error Compatibility**: Matching error codes and messages
116+
117+
### Protocol Validation
118+
119+
Run `make test` to verify MCP compliance:
120+
121+
- **Tool Format Validation**: All tools return proper MCP structure
122+
- **Schema Compliance**: JSON schemas validated against VS Code specs
123+
- **Integration Testing**: End-to-end MCP message flow verification
124+
84125
## Testing Architecture
85126

86127
Tests are organized in three layers:
@@ -91,6 +132,33 @@ Tests are organized in three layers:
91132

92133
Test files follow the pattern `*_spec.lua` or `*_test.lua` and use the busted framework.
93134

135+
### Test Infrastructure
136+
137+
**JSON Handling**: Custom JSON encoder/decoder with support for:
138+
139+
- Nested objects and arrays
140+
- Special Lua keywords as object keys (`["end"]`)
141+
- MCP message format validation
142+
- VS Code extension output compatibility
143+
144+
**Test Pattern**: Run specific test files during development:
145+
146+
```bash
147+
# Run specific tool tests with proper LUA_PATH
148+
export LUA_PATH="./lua/?.lua;./lua/?/init.lua;./?.lua;./?/init.lua;$LUA_PATH"
149+
busted tests/unit/tools/specific_tool_spec.lua --verbose
150+
151+
# Or use make for full validation
152+
make test # Recommended for complete validation
153+
```
154+
155+
**Coverage Metrics**:
156+
157+
- **320+ tests** covering all MCP tools and core functionality
158+
- **Unit Tests**: Individual tool behavior and error cases
159+
- **Integration Tests**: End-to-end MCP protocol flow
160+
- **Format Tests**: MCP compliance and VS Code compatibility
161+
94162
### Test Organization Principles
95163

96164
- **Isolation**: Each test should be independent and not rely on external state
@@ -274,9 +342,86 @@ rg "0\.1\.0" . # Should only show CHANGELOG.md historical entries
274342
4. **Document Changes**: Update relevant documentation (this file, PROTOCOL.md, etc.)
275343
5. **Commit**: Only commit after successful `make` execution
276344

345+
### MCP Tool Development Guidelines
346+
347+
**Adding New Tools**:
348+
349+
1. **Study Existing Patterns**: Review `lua/claudecode/tools/` for consistent structure
350+
2. **Implement Handler**: Return MCP format: `{content: [{type: "text", text: JSON}]}`
351+
3. **Add JSON Schema**: Define parameters and expose via MCP (if needed)
352+
4. **Create Tests**: Both unit tests and integration tests required
353+
5. **Update Documentation**: Add to this file's MCP tools list
354+
355+
**Tool Testing Pattern**:
356+
357+
```lua
358+
-- All tools should return MCP-compliant format
359+
local result = tool_handler(params)
360+
expect(result).to_be_table()
361+
expect(result.content).to_be_table()
362+
expect(result.content[1].type).to_be("text")
363+
local parsed = json_decode(result.content[1].text)
364+
-- Validate parsed structure matches VS Code extension
365+
```
366+
367+
**Error Handling Standard**:
368+
369+
```lua
370+
-- Use consistent JSON-RPC error format
371+
error({
372+
code = -32602, -- Invalid params
373+
message = "Description of the issue",
374+
data = "Additional context"
375+
})
376+
```
377+
277378
### Code Quality Standards
278379

279-
- **Test Coverage**: Maintain comprehensive test coverage (currently 314+ tests)
380+
- **Test Coverage**: Maintain comprehensive test coverage (currently **320+ tests**, 100% success rate)
280381
- **Zero Warnings**: All code must pass luacheck with 0 warnings/errors
382+
- **MCP Compliance**: All tools must return proper MCP format with JSON-stringified content
383+
- **VS Code Compatibility**: New tools must match VS Code extension behavior exactly
281384
- **Consistent Formatting**: Use `nix fmt` or `stylua` for consistent code style
282385
- **Documentation**: Update CLAUDE.md for architectural changes, PROTOCOL.md for protocol changes
386+
387+
### Development Quality Gates
388+
389+
1. **`make check`** - Syntax and linting (0 warnings required)
390+
2. **`make test`** - All tests passing (320/320 success rate required)
391+
3. **`make format`** - Consistent code formatting
392+
4. **MCP Validation** - Tools return proper format structure
393+
5. **Integration Test** - End-to-end protocol flow verification
394+
395+
## Development Troubleshooting
396+
397+
### Common Issues
398+
399+
**Test Failures with LUA_PATH**:
400+
401+
```bash
402+
# Tests can't find modules - use proper LUA_PATH
403+
export LUA_PATH="./lua/?.lua;./lua/?/init.lua;./?.lua;./?/init.lua;$LUA_PATH"
404+
busted tests/unit/specific_test.lua
405+
```
406+
407+
**JSON Format Issues**:
408+
409+
- Ensure all tools return: `{content: [{type: "text", text: "JSON-string"}]}`
410+
- Use `vim.json.encode()` for proper JSON stringification
411+
- Test JSON parsing with custom test decoder in `tests/busted_setup.lua`
412+
413+
**MCP Tool Registration**:
414+
415+
- Tools with `schema = nil` are internal-only
416+
- Tools with schema are exposed via MCP
417+
- Check `lua/claudecode/tools/init.lua` for registration patterns
418+
419+
**Authentication Testing**:
420+
421+
```bash
422+
# Verify auth token generation
423+
cat ~/.claude/ide/*.lock | jq .authToken
424+
425+
# Test WebSocket connection
426+
websocat ws://localhost:PORT --header "x-claude-code-ide-authorization: $(cat ~/.claude/ide/*.lock | jq -r .authToken)"
427+
```
Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
--- Tool implementation for checking if a document is dirty.
22

3+
local schema = {
4+
description = "Check if a document has unsaved changes (is dirty)",
5+
inputSchema = {
6+
type = "object",
7+
properties = {
8+
filePath = {
9+
type = "string",
10+
description = "Path to the file to check",
11+
},
12+
},
13+
required = { "filePath" },
14+
additionalProperties = false,
15+
["$schema"] = "http://json-schema.org/draft-07/schema#",
16+
},
17+
}
18+
319
--- Handles the checkDocumentDirty tool invocation.
420
-- Checks if the specified file (buffer) has unsaved changes.
521
-- @param params table The input parameters for the tool.
@@ -14,22 +30,41 @@ local function handler(params)
1430
local bufnr = vim.fn.bufnr(params.filePath)
1531

1632
if bufnr == -1 then
17-
-- It's debatable if this is an "error" or if it should return { isDirty = false }
18-
-- For now, treating as an operational error as the file isn't actively managed by a buffer.
19-
error({
20-
code = -32000,
21-
message = "File operation error",
22-
data = "File not open in editor: " .. params.filePath,
23-
})
33+
-- Return success: false when document not open, matching VS Code behavior
34+
return {
35+
content = {
36+
{
37+
type = "text",
38+
text = vim.json.encode({
39+
success = false,
40+
message = "Document not open: " .. params.filePath,
41+
}, { indent = 2 }),
42+
},
43+
},
44+
}
2445
end
2546

2647
local is_dirty = vim.api.nvim_buf_get_option(bufnr, "modified")
48+
local is_untitled = vim.api.nvim_buf_get_name(bufnr) == ""
2749

28-
return { isDirty = is_dirty }
50+
-- Return MCP-compliant format with JSON-stringified result
51+
return {
52+
content = {
53+
{
54+
type = "text",
55+
text = vim.json.encode({
56+
success = true,
57+
filePath = params.filePath,
58+
isDirty = is_dirty,
59+
isUntitled = is_untitled,
60+
}, { indent = 2 }),
61+
},
62+
},
63+
}
2964
end
3065

3166
return {
3267
name = "checkDocumentDirty",
33-
schema = nil, -- Internal tool
68+
schema = schema,
3469
handler = handler,
3570
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
--- Tool implementation for closing all diff tabs.
2+
3+
local schema = {
4+
description = "Close all diff tabs in the editor",
5+
inputSchema = {
6+
type = "object",
7+
additionalProperties = false,
8+
["$schema"] = "http://json-schema.org/draft-07/schema#",
9+
},
10+
}
11+
12+
--- Handles the closeAllDiffTabs tool invocation.
13+
-- Closes all diff tabs/windows in the editor.
14+
-- @param _params table The input parameters for the tool (currently unused).
15+
-- @return table MCP-compliant response with content array indicating number of closed tabs.
16+
-- @error table A table with code, message, and data for JSON-RPC error if failed.
17+
local function handler(_params) -- Prefix unused params with underscore
18+
local closed_count = 0
19+
20+
-- Get all windows
21+
local windows = vim.api.nvim_list_wins()
22+
local windows_to_close = {} -- Use set to avoid duplicates
23+
24+
for _, win in ipairs(windows) do
25+
local buf = vim.api.nvim_win_get_buf(win)
26+
local buftype = vim.api.nvim_buf_get_option(buf, "buftype")
27+
local diff_mode = vim.api.nvim_win_get_option(win, "diff")
28+
local should_close = false
29+
30+
-- Check if this is a diff window
31+
if diff_mode then
32+
should_close = true
33+
end
34+
35+
-- Also check for diff-related buffer names or types
36+
local buf_name = vim.api.nvim_buf_get_name(buf)
37+
if buf_name:match("%.diff$") or buf_name:match("diff://") then
38+
should_close = true
39+
end
40+
41+
-- Check for special diff buffer types
42+
if buftype == "nofile" and buf_name:match("^fugitive://") then
43+
should_close = true
44+
end
45+
46+
-- Add to close set only once (prevents duplicates)
47+
if should_close then
48+
windows_to_close[win] = true
49+
end
50+
end
51+
52+
-- Close the identified diff windows
53+
for win, _ in pairs(windows_to_close) do
54+
if vim.api.nvim_win_is_valid(win) then
55+
local success = pcall(vim.api.nvim_win_close, win, false)
56+
if success then
57+
closed_count = closed_count + 1
58+
end
59+
end
60+
end
61+
62+
-- Also check for buffers that might be diff-related but not currently in windows
63+
local buffers = vim.api.nvim_list_bufs()
64+
for _, buf in ipairs(buffers) do
65+
if vim.api.nvim_buf_is_loaded(buf) then
66+
local buf_name = vim.api.nvim_buf_get_name(buf)
67+
local buftype = vim.api.nvim_buf_get_option(buf, "buftype")
68+
69+
-- Check for diff-related buffers
70+
if
71+
buf_name:match("%.diff$")
72+
or buf_name:match("diff://")
73+
or (buftype == "nofile" and buf_name:match("^fugitive://"))
74+
then
75+
-- Delete the buffer if it's not in any window
76+
local buf_windows = vim.fn.win_findbuf(buf)
77+
if #buf_windows == 0 then
78+
local success = pcall(vim.api.nvim_buf_delete, buf, { force = true })
79+
if success then
80+
closed_count = closed_count + 1
81+
end
82+
end
83+
end
84+
end
85+
end
86+
87+
-- Return MCP-compliant format matching VS Code extension
88+
return {
89+
content = {
90+
{
91+
type = "text",
92+
text = "CLOSED_" .. closed_count .. "_DIFF_TABS",
93+
},
94+
},
95+
}
96+
end
97+
98+
return {
99+
name = "closeAllDiffTabs",
100+
schema = schema,
101+
handler = handler,
102+
}

0 commit comments

Comments
 (0)