mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
Add tests and documentation for CodexLens LSP tool
- Introduced a new test script for the CodexLens LSP tool to validate core functionalities including symbol search, find definition, find references, and get hover. - Created comprehensive documentation for the MCP endpoint design, detailing the architecture, features, and integration with the CCW MCP Manager. - Developed a detailed implementation plan for transitioning to a real LSP server, outlining phases, architecture, and acceptance criteria.
This commit is contained in:
317
ccw/src/tools/vscode-lsp.ts
Normal file
317
ccw/src/tools/vscode-lsp.ts
Normal file
@@ -0,0 +1,317 @@
|
||||
/**
|
||||
* VSCode LSP Tool - Provides LSP-like code intelligence via VSCode Bridge Extension
|
||||
*
|
||||
* Features:
|
||||
* - get_definition: Find symbol definition
|
||||
* - get_references: Find all symbol references
|
||||
* - get_hover: Get hover information for symbols
|
||||
* - get_document_symbols: List all symbols in file
|
||||
*
|
||||
* Requirements:
|
||||
* - ccw-vscode-bridge extension must be running in VSCode
|
||||
* - File must be open/accessible in VSCode workspace
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { ToolSchema, ToolResult } from '../types/tool.js';
|
||||
|
||||
// VSCode Bridge configuration
|
||||
const BRIDGE_URL = 'http://127.0.0.1:3457';
|
||||
const REQUEST_TIMEOUT = 10000; // 10 seconds
|
||||
|
||||
// Define Zod schema for validation
|
||||
const ParamsSchema = z.object({
|
||||
action: z.enum(['get_definition', 'get_references', 'get_hover', 'get_document_symbols']),
|
||||
file_path: z.string().describe('Absolute path to the file'),
|
||||
line: z.number().optional().describe('Line number (1-based)'),
|
||||
character: z.number().optional().describe('Character position (1-based)'),
|
||||
});
|
||||
|
||||
type Params = z.infer<typeof ParamsSchema>;
|
||||
|
||||
/**
|
||||
* Result types from VSCode LSP
|
||||
*/
|
||||
interface Position {
|
||||
line: number;
|
||||
character: number;
|
||||
}
|
||||
|
||||
interface Range {
|
||||
start: Position;
|
||||
end: Position;
|
||||
}
|
||||
|
||||
interface Location {
|
||||
uri: string;
|
||||
range: Range;
|
||||
}
|
||||
|
||||
interface HoverContent {
|
||||
contents: string[];
|
||||
range?: Range;
|
||||
}
|
||||
|
||||
interface DocumentSymbol {
|
||||
name: string;
|
||||
kind: string;
|
||||
range: Range;
|
||||
selectionRange: Range;
|
||||
detail?: string;
|
||||
parent?: string;
|
||||
}
|
||||
|
||||
type LSPResult = {
|
||||
success: boolean;
|
||||
result?: Location | Location[] | HoverContent[] | DocumentSymbol[];
|
||||
error?: string;
|
||||
action: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute HTTP request to VSCode Bridge
|
||||
*/
|
||||
async function callVSCodeBridge(
|
||||
endpoint: string,
|
||||
payload: Record<string, unknown>
|
||||
): Promise<LSPResult> {
|
||||
const url = `${BRIDGE_URL}${endpoint}`;
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.json();
|
||||
return {
|
||||
success: false,
|
||||
error: errorBody.error || `HTTP ${response.status}: ${response.statusText}`,
|
||||
action: endpoint,
|
||||
};
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
success: data.success !== false,
|
||||
result: data.result,
|
||||
action: endpoint,
|
||||
};
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
|
||||
if (error.name === 'AbortError') {
|
||||
return {
|
||||
success: false,
|
||||
error: `Request timed out after ${REQUEST_TIMEOUT}ms`,
|
||||
action: endpoint,
|
||||
};
|
||||
}
|
||||
|
||||
if ((error as any).code === 'ECONNREFUSED') {
|
||||
return {
|
||||
success: false,
|
||||
error: `Could not connect to VSCode Bridge at ${BRIDGE_URL}. Is the ccw-vscode-bridge extension running in VSCode?`,
|
||||
action: endpoint,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: `Request failed: ${error.message}`,
|
||||
action: endpoint,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler: get_definition
|
||||
*/
|
||||
async function handleGetDefinition(params: Params): Promise<LSPResult> {
|
||||
if (params.line === undefined || params.character === undefined) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'line and character are required for get_definition',
|
||||
action: 'get_definition',
|
||||
};
|
||||
}
|
||||
|
||||
return callVSCodeBridge('/get_definition', {
|
||||
file_path: params.file_path,
|
||||
line: params.line,
|
||||
character: params.character,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler: get_references
|
||||
*/
|
||||
async function handleGetReferences(params: Params): Promise<LSPResult> {
|
||||
if (params.line === undefined || params.character === undefined) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'line and character are required for get_references',
|
||||
action: 'get_references',
|
||||
};
|
||||
}
|
||||
|
||||
return callVSCodeBridge('/get_references', {
|
||||
file_path: params.file_path,
|
||||
line: params.line,
|
||||
character: params.character,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler: get_hover
|
||||
*/
|
||||
async function handleGetHover(params: Params): Promise<LSPResult> {
|
||||
if (params.line === undefined || params.character === undefined) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'line and character are required for get_hover',
|
||||
action: 'get_hover',
|
||||
};
|
||||
}
|
||||
|
||||
return callVSCodeBridge('/get_hover', {
|
||||
file_path: params.file_path,
|
||||
line: params.line,
|
||||
character: params.character,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler: get_document_symbols
|
||||
*/
|
||||
async function handleGetDocumentSymbols(params: Params): Promise<LSPResult> {
|
||||
return callVSCodeBridge('/get_document_symbols', {
|
||||
file_path: params.file_path,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Main handler function
|
||||
*/
|
||||
export async function handler(params: Record<string, unknown>): Promise<ToolResult<LSPResult>> {
|
||||
try {
|
||||
// Validate parameters
|
||||
const validatedParams = ParamsSchema.parse(params);
|
||||
|
||||
// Route to appropriate handler based on action
|
||||
let result: LSPResult;
|
||||
switch (validatedParams.action) {
|
||||
case 'get_definition':
|
||||
result = await handleGetDefinition(validatedParams);
|
||||
break;
|
||||
case 'get_references':
|
||||
result = await handleGetReferences(validatedParams);
|
||||
break;
|
||||
case 'get_hover':
|
||||
result = await handleGetHover(validatedParams);
|
||||
break;
|
||||
case 'get_document_symbols':
|
||||
result = await handleGetDocumentSymbols(validatedParams);
|
||||
break;
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
error: `Unknown action: ${(validatedParams as any).action}`,
|
||||
result: null as any,
|
||||
};
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.error || 'Unknown error',
|
||||
result: null as any,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
result,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof z.ZodError) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Parameter validation failed: ${err.issues.map((e: z.ZodIssue) => `${e.path.join('.')}: ${e.message}`).join(', ')}`,
|
||||
result: null as any,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: `Execution failed: ${(err as Error).message}`,
|
||||
result: null as any,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool schema for MCP
|
||||
*/
|
||||
export const schema: ToolSchema = {
|
||||
name: 'vscode_lsp',
|
||||
description: `Access live VSCode LSP features via ccw-vscode-bridge extension.
|
||||
|
||||
**Actions:**
|
||||
- get_definition: Find the definition of a symbol at a given position
|
||||
- get_references: Find all references to a symbol at a given position
|
||||
- get_hover: Get hover information for a symbol at a given position
|
||||
- get_document_symbols: List all symbols in a given file
|
||||
|
||||
**Usage Examples:**
|
||||
|
||||
Find definition:
|
||||
vscode_lsp(action="get_definition", file_path="/path/to/file.ts", line=10, character=5)
|
||||
|
||||
Find references:
|
||||
vscode_lsp(action="get_references", file_path="/path/to/file.py", line=42, character=15)
|
||||
|
||||
Get hover info:
|
||||
vscode_lsp(action="get_hover", file_path="/path/to/file.go", line=100, character=20)
|
||||
|
||||
List document symbols:
|
||||
vscode_lsp(action="get_document_symbols", file_path="/path/to/file.rs")
|
||||
|
||||
**Requirements:**
|
||||
- ccw-vscode-bridge extension must be installed and active in VSCode
|
||||
- File must be part of the VSCode workspace
|
||||
- VSCode Bridge HTTP server running on http://127.0.0.1:3457`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
action: {
|
||||
type: 'string',
|
||||
enum: ['get_definition', 'get_references', 'get_hover', 'get_document_symbols'],
|
||||
description: 'LSP action to perform',
|
||||
},
|
||||
file_path: {
|
||||
type: 'string',
|
||||
description: 'Absolute path to the file (required)',
|
||||
},
|
||||
line: {
|
||||
type: 'number',
|
||||
description: 'Line number (1-based, required for definition/references/hover)',
|
||||
},
|
||||
character: {
|
||||
type: 'number',
|
||||
description: 'Character position (1-based, required for definition/references/hover)',
|
||||
},
|
||||
},
|
||||
required: ['action', 'file_path'],
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user