Skip to content
This repository was archived by the owner on May 20, 2026. It is now read-only.

Commit ace1f0c

Browse files
committed
lsp query tool for CCLI
1 parent 2532830 commit ace1f0c

2 files changed

Lines changed: 146 additions & 5 deletions

File tree

src/extension/chatSessions/copilotcli/vscode-node/tools/index.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,17 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
7-
import { registerOpenDiffTool } from './openDiff';
7+
import { ILogger } from '../../../../../platform/log/common/logService';
8+
import { ICopilotCLISessionTracker } from '../copilotCLISessionTracker';
9+
import { DiffStateManager } from '../diffState';
10+
import { ReadonlyContentProvider } from '../readonlyContentProvider';
811
import { registerCloseDiffTool } from './closeDiff';
912
import { registerGetDiagnosticsTool } from './getDiagnostics';
1013
import { registerGetSelectionTool, SelectionState } from './getSelection';
1114
import { registerGetVscodeInfoTool } from './getVscodeInfo';
15+
import { registerOpenDiffTool } from './openDiff';
16+
import { registerRunLspQueryTool } from './runLspQuery';
1217
import { registerUpdateSessionNameTool } from './updateSessionName';
13-
import { ILogger } from '../../../../../platform/log/common/logService';
14-
import { DiffStateManager } from '../diffState';
15-
import { ReadonlyContentProvider } from '../readonlyContentProvider';
16-
import { ICopilotCLISessionTracker } from '../copilotCLISessionTracker';
1718

1819
export { getSelectionInfo, SelectionState } from './getSelection';
1920
export type { SelectionInfo } from './getSelection';
@@ -26,5 +27,6 @@ export function registerTools(server: McpServer, logger: ILogger, diffState: Dif
2627
registerCloseDiffTool(server, logger, diffState);
2728
registerGetDiagnosticsTool(server, logger);
2829
registerUpdateSessionNameTool(server, logger, sessionTracker, sessionId);
30+
registerRunLspQueryTool(server, logger);
2931
logger.debug('All MCP tools registered');
3032
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
7+
import * as fs from 'fs/promises';
8+
import * as os from 'os';
9+
import * as path from 'path';
10+
import * as vscode from 'vscode';
11+
import { z } from 'zod';
12+
import { ILogger } from '../../../../../platform/log/common/logService';
13+
import { makeTextResult } from './utils';
14+
15+
export function registerRunLspQueryTool(server: McpServer, logger: ILogger): void {
16+
const schema = {
17+
command: z.enum([
18+
'vscode.executeDocumentHighlights',
19+
'vscode.executeDocumentSymbolProvider',
20+
'vscode.executeFormatDocumentProvider',
21+
'vscode.executeFormatRangeProvider',
22+
'vscode.executeFormatOnTypeProvider',
23+
'vscode.executeDefinitionProvider',
24+
'vscode.executeTypeDefinitionProvider',
25+
'vscode.executeDeclarationProvider',
26+
'vscode.executeImplementationProvider',
27+
'vscode.executeReferenceProvider',
28+
'vscode.executeHoverProvider',
29+
'vscode.executeSelectionRangeProvider',
30+
'vscode.executeWorkspaceSymbolProvider',
31+
'vscode.prepareCallHierarchy',
32+
'vscode.provideIncomingCalls',
33+
'vscode.provideOutgoingCalls',
34+
'vscode.prepareRename',
35+
'vscode.executeDocumentRenameProvider',
36+
'vscode.executeLinkProvider',
37+
'vscode.provideDocumentSemanticTokensLegend',
38+
'vscode.provideDocumentSemanticTokens',
39+
'vscode.provideDocumentRangeSemanticTokensLegend'
40+
]).describe('The VS Code LSP command to execute'),
41+
uri: z.string().describe('The URI of the document'),
42+
position: z.object({
43+
line: z.number(),
44+
character: z.number()
45+
}).optional().describe('The position in the document (if required by the command)'),
46+
query: z.string().optional().describe('A string query (for workspace symbols)'),
47+
};
48+
server.registerTool(
49+
'run_lsp_query',
50+
{
51+
description: 'Execute a precise LSP query (e.g. definitions, references, hover) using built-in VS Code commands. Pass the command name and the appropriate arguments.',
52+
inputSchema: schema,
53+
},
54+
// @ts-ignore - TS2589: zod type instantiation too deep for server.tool() generics
55+
async (args: { command: string; uri: string; position?: { line: number; character: number }; query?: string }) => {
56+
const { command, uri, position, query } = args;
57+
logger.debug(`Executing LSP query: ${command} on ${uri}`);
58+
try {
59+
const decodedUri = decodeURIComponent(uri);
60+
const documentUri = decodedUri.startsWith('file:') ? vscode.Uri.parse(decodedUri) : vscode.Uri.file(decodedUri);
61+
const pos = position ? new vscode.Position(position.line, position.character) : undefined;
62+
let result: any;
63+
64+
if (command === 'vscode.executeWorkspaceSymbolProvider' && query !== undefined) {
65+
result = await vscode.commands.executeCommand(command, query);
66+
} else if (pos) {
67+
result = await vscode.commands.executeCommand(command, documentUri, pos);
68+
} else {
69+
result = await vscode.commands.executeCommand(command, documentUri);
70+
}
71+
72+
if (Array.isArray(result) && result.length > 0) {
73+
const grouped: Record<string, any[]> = {};
74+
const ungrouped: any[] = [];
75+
let hasUri = false;
76+
77+
for (const item of result) {
78+
const uriObj = item?.uri || item?.targetUri || item?.location?.uri;
79+
if (uriObj && (uriObj.fsPath || typeof uriObj.toString === 'function')) {
80+
hasUri = true;
81+
const uriStr = uriObj.fsPath || uriObj.toString();
82+
if (!grouped[uriStr]) {
83+
grouped[uriStr] = [];
84+
}
85+
const compactItem = { ...item };
86+
delete compactItem.uri;
87+
delete compactItem.targetUri;
88+
if (compactItem.location?.uri) {
89+
compactItem.location = { ...compactItem.location };
90+
delete compactItem.location.uri;
91+
}
92+
grouped[uriStr].push(compactItem);
93+
} else {
94+
ungrouped.push(item);
95+
}
96+
}
97+
98+
if (hasUri) {
99+
result = ungrouped.length > 0 ? { grouped, ungrouped } : grouped;
100+
}
101+
}
102+
103+
// Safe stringify handling circular references or overwhelming output
104+
const replacer = () => {
105+
const seen = new WeakSet();
106+
return (key: string, value: any) => {
107+
if (typeof value === 'object' && value !== null) {
108+
if (seen.has(value)) {
109+
return '[Circular]';
110+
}
111+
seen.add(value);
112+
}
113+
// Limit arrays
114+
if (Array.isArray(value) && value.length > 50) {
115+
return [...value.slice(0, 50), `... ${value.length - 50} more items`];
116+
}
117+
return value;
118+
};
119+
};
120+
121+
const resultString = JSON.stringify(result, replacer(), 2) ?? 'No results found.';
122+
const explanation = `These are the results of executing ${command} on ${uri}${position ? ` at line ${position.line}, character ${position.character}` : ''}${query ? ` with query "${query}"` : ''}.`;
123+
124+
if (resultString.length > 50000) {
125+
const tmpFile = path.join(os.tmpdir(), `lsp_query_result_${Date.now()}.json`);
126+
await fs.writeFile(tmpFile, resultString);
127+
logger.trace(`Returning saved file path for LSP query ${command} due to size`);
128+
return makeTextResult(`${explanation}\n\nThe result is very long and has been saved to: ${tmpFile}`);
129+
}
130+
131+
logger.trace(`Returning result for LSP query ${command}`);
132+
return makeTextResult(`${explanation}\n\n${resultString}`);
133+
} catch (error) {
134+
logger.error(`Error executing LSP query ${command}: ${error}`);
135+
return makeTextResult(`Error executing ${command}: ${error instanceof Error ? error.message : String(error)}`);
136+
}
137+
}
138+
);
139+
}

0 commit comments

Comments
 (0)