@treedy/pyright-mcp 1.1.2 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,49 +1,1617 @@
1
1
  #!/usr/bin/env node
2
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import { hover, hoverSchema } from './tools/hover.js';
5
- import { definition, definitionSchema } from './tools/definition.js';
6
- import { references, referencesSchema } from './tools/references.js';
7
- import { completions, completionsSchema } from './tools/completions.js';
8
- import { diagnostics, diagnosticsSchema } from './tools/diagnostics.js';
9
- import { signatureHelp, signatureHelpSchema } from './tools/signature-help.js';
10
- import { rename, renameSchema } from './tools/rename.js';
11
- import { search, searchSchema } from './tools/search.js';
12
- import { status, statusSchema } from './tools/status.js';
13
- import { getLspClient } from './lsp-client.js';
14
- const server = new McpServer({
15
- name: 'pyright-mcp',
16
- version: '1.0.0',
2
+ import { createRequire } from "node:module";
3
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
+
5
+ // src/index.ts
6
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
+
9
+ // src/tools/hover.ts
10
+ import { z } from "zod";
11
+
12
+ // src/lsp-client.ts
13
+ import * as fs2 from "fs";
14
+
15
+ // src/utils/position.ts
16
+ import { Position } from "vscode-languageserver-protocol";
17
+ import { existsSync } from "fs";
18
+ import { dirname, join, resolve } from "path";
19
+ function toPosition(line, column) {
20
+ return Position.create(line - 1, column - 1);
21
+ }
22
+ function fromPosition(pos) {
23
+ return {
24
+ line: pos.line + 1,
25
+ column: pos.character + 1
26
+ };
27
+ }
28
+ function formatRange(range) {
29
+ const start = fromPosition(range.start);
30
+ const end = fromPosition(range.end);
31
+ return `${start.line}:${start.column}-${end.line}:${end.column}`;
32
+ }
33
+ function uriToPath(uri) {
34
+ if (uri.startsWith("file://")) {
35
+ return uri.slice(7);
36
+ }
37
+ return uri;
38
+ }
39
+ function findProjectRoot(filePath) {
40
+ const configFiles = ["pyrightconfig.json", "pyproject.toml", ".git"];
41
+ let dir = dirname(resolve(filePath));
42
+ const root = "/";
43
+ while (dir !== root) {
44
+ for (const configFile of configFiles) {
45
+ if (existsSync(join(dir, configFile))) {
46
+ return dir;
47
+ }
48
+ }
49
+ const parent = dirname(dir);
50
+ if (parent === dir)
51
+ break;
52
+ dir = parent;
53
+ }
54
+ return dirname(resolve(filePath));
55
+ }
56
+
57
+ // src/lsp/connection.ts
58
+ import { spawn } from "child_process";
59
+ import {
60
+ createMessageConnection,
61
+ StreamMessageReader,
62
+ StreamMessageWriter
63
+ } from "vscode-jsonrpc/node.js";
64
+
65
+ // src/lsp/document-manager.ts
66
+ import * as fs from "fs";
67
+ import * as path from "path";
68
+
69
+ class DocumentManager {
70
+ documents = new Map;
71
+ filePathToUri(filePath) {
72
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(filePath);
73
+ return `file://${absolutePath}`;
74
+ }
75
+ uriToFilePath(uri) {
76
+ if (uri.startsWith("file://")) {
77
+ return uri.slice(7);
78
+ }
79
+ return uri;
80
+ }
81
+ isDocumentOpen(filePath) {
82
+ const uri = this.filePathToUri(filePath);
83
+ return this.documents.has(uri);
84
+ }
85
+ getDocument(filePath) {
86
+ const uri = this.filePathToUri(filePath);
87
+ return this.documents.get(uri);
88
+ }
89
+ openDocument(filePath, content) {
90
+ const uri = this.filePathToUri(filePath);
91
+ const actualContent = content ?? fs.readFileSync(filePath, "utf-8");
92
+ const state = {
93
+ uri,
94
+ version: 1,
95
+ content: actualContent,
96
+ languageId: "python"
97
+ };
98
+ this.documents.set(uri, state);
99
+ return state;
100
+ }
101
+ updateDocument(filePath, newContent) {
102
+ const uri = this.filePathToUri(filePath);
103
+ const existing = this.documents.get(uri);
104
+ if (!existing) {
105
+ const state = this.openDocument(filePath, newContent);
106
+ return state.version;
107
+ }
108
+ existing.version += 1;
109
+ existing.content = newContent;
110
+ return existing.version;
111
+ }
112
+ closeDocument(filePath) {
113
+ const uri = this.filePathToUri(filePath);
114
+ return this.documents.delete(uri);
115
+ }
116
+ getAllOpenDocuments() {
117
+ return Array.from(this.documents.values());
118
+ }
119
+ closeAll() {
120
+ this.documents.clear();
121
+ }
122
+ getTextDocumentIdentifier(filePath) {
123
+ return { uri: this.filePathToUri(filePath) };
124
+ }
125
+ getTextDocumentItem(filePath) {
126
+ const state = this.getDocument(filePath);
127
+ if (!state) {
128
+ throw new Error(`Document not open: ${filePath}`);
129
+ }
130
+ return {
131
+ uri: state.uri,
132
+ languageId: state.languageId,
133
+ version: state.version,
134
+ text: state.content
135
+ };
136
+ }
137
+ getVersionedTextDocumentIdentifier(filePath) {
138
+ const state = this.getDocument(filePath);
139
+ if (!state) {
140
+ throw new Error(`Document not open: ${filePath}`);
141
+ }
142
+ return {
143
+ uri: state.uri,
144
+ version: state.version
145
+ };
146
+ }
147
+ }
148
+
149
+ // src/lsp/connection.ts
150
+ class LspConnectionManager {
151
+ connections = new Map;
152
+ documentManagers = new Map;
153
+ initializationPromises = new Map;
154
+ static DEFAULT_INIT_TIMEOUT = 1e4;
155
+ static DOCUMENT_READY_DELAY = 500;
156
+ async getConnection(options) {
157
+ const { workspaceRoot } = options;
158
+ const existing = this.connections.get(workspaceRoot);
159
+ if (existing && existing.initialized) {
160
+ existing.lastUsed = Date.now();
161
+ return existing;
162
+ }
163
+ const pendingInit = this.initializationPromises.get(workspaceRoot);
164
+ if (pendingInit) {
165
+ return pendingInit;
166
+ }
167
+ const initPromise = this.createConnection(options);
168
+ this.initializationPromises.set(workspaceRoot, initPromise);
169
+ try {
170
+ const connection = await initPromise;
171
+ this.connections.set(workspaceRoot, connection);
172
+ return connection;
173
+ } finally {
174
+ this.initializationPromises.delete(workspaceRoot);
175
+ }
176
+ }
177
+ getDocumentManager(workspaceRoot) {
178
+ let manager = this.documentManagers.get(workspaceRoot);
179
+ if (!manager) {
180
+ manager = new DocumentManager;
181
+ this.documentManagers.set(workspaceRoot, manager);
182
+ }
183
+ return manager;
184
+ }
185
+ async createConnection(options) {
186
+ const { workspaceRoot, initTimeout = LspConnectionManager.DEFAULT_INIT_TIMEOUT } = options;
187
+ const pyrightProcess = spawn("pyright-langserver", ["--stdio"], {
188
+ stdio: ["pipe", "pipe", "ignore"],
189
+ cwd: workspaceRoot
190
+ });
191
+ pyrightProcess.on("error", (err) => {
192
+ console.error(`[LSP] Process error for ${workspaceRoot}:`, err);
193
+ this.closeConnection(workspaceRoot);
194
+ });
195
+ pyrightProcess.on("exit", (code, signal) => {
196
+ console.error(`[LSP] Process exited for ${workspaceRoot}: code=${code}, signal=${signal}`);
197
+ this.connections.delete(workspaceRoot);
198
+ });
199
+ console.error(`[LSP] Spawned pyright-langserver for ${workspaceRoot}`);
200
+ const connection = createMessageConnection(new StreamMessageReader(pyrightProcess.stdout), new StreamMessageWriter(pyrightProcess.stdin));
201
+ connection.onError((error) => {
202
+ console.error(`[LSP] Connection error for ${workspaceRoot}:`, error);
203
+ });
204
+ connection.onClose(() => {
205
+ console.error(`[LSP] Connection closed for ${workspaceRoot}`);
206
+ });
207
+ connection.listen();
208
+ const connectionId = Math.random().toString(36).slice(2, 8);
209
+ console.error(`[LSP] Created connection ${connectionId}`);
210
+ const lspConnection = {
211
+ connection,
212
+ process: pyrightProcess,
213
+ workspaceRoot,
214
+ initialized: false,
215
+ lastUsed: Date.now(),
216
+ id: connectionId
217
+ };
218
+ await this.initializeConnection(lspConnection, initTimeout);
219
+ return lspConnection;
220
+ }
221
+ async initializeConnection(lspConnection, timeout) {
222
+ const { connection, workspaceRoot } = lspConnection;
223
+ const initPromise = (async () => {
224
+ console.error(`[LSP] Sending initialize request...`);
225
+ const initResult = await connection.sendRequest("initialize", {
226
+ processId: process.pid,
227
+ rootUri: `file://${workspaceRoot}`,
228
+ capabilities: {
229
+ textDocument: {
230
+ hover: { contentFormat: ["markdown", "plaintext"] },
231
+ completion: {
232
+ completionItem: {
233
+ snippetSupport: true,
234
+ documentationFormat: ["markdown", "plaintext"]
235
+ }
236
+ },
237
+ signatureHelp: {
238
+ signatureInformation: {
239
+ documentationFormat: ["markdown", "plaintext"]
240
+ }
241
+ },
242
+ definition: { linkSupport: true },
243
+ references: {},
244
+ rename: { prepareSupport: true },
245
+ documentSymbol: {
246
+ hierarchicalDocumentSymbolSupport: true
247
+ },
248
+ publishDiagnostics: {}
249
+ }
250
+ },
251
+ workspaceFolders: [
252
+ {
253
+ uri: `file://${workspaceRoot}`,
254
+ name: workspaceRoot.split("/").pop() || "workspace"
255
+ }
256
+ ]
257
+ });
258
+ console.error(`[LSP] Sending initialized notification...`);
259
+ await connection.sendNotification("initialized", {});
260
+ console.error(`[LSP] Connection initialized successfully`);
261
+ lspConnection.initialized = true;
262
+ })();
263
+ const timeoutPromise = new Promise((_, reject) => {
264
+ setTimeout(() => reject(new Error(`LSP initialization timed out after ${timeout}ms`)), timeout);
265
+ });
266
+ await Promise.race([initPromise, timeoutPromise]);
267
+ }
268
+ async openDocument(workspaceRoot, filePath, content) {
269
+ const lspConnection = this.connections.get(workspaceRoot);
270
+ if (!lspConnection || !lspConnection.initialized) {
271
+ throw new Error(`No initialized connection for workspace: ${workspaceRoot}`);
272
+ }
273
+ const docManager = this.getDocumentManager(workspaceRoot);
274
+ if (docManager.isDocumentOpen(filePath)) {
275
+ return;
276
+ }
277
+ const docState = docManager.openDocument(filePath, content);
278
+ console.error(`[LSP] Sending didOpen for ${filePath}...`);
279
+ await lspConnection.connection.sendNotification("textDocument/didOpen", {
280
+ textDocument: {
281
+ uri: docState.uri,
282
+ languageId: docState.languageId,
283
+ version: docState.version,
284
+ text: docState.content
285
+ }
286
+ });
287
+ console.error(`[LSP] Waiting for analysis...`);
288
+ await this.waitForAnalysis();
289
+ console.error(`[LSP] Document ready: ${filePath}`);
290
+ }
291
+ async updateDocument(workspaceRoot, filePath, newContent) {
292
+ const lspConnection = this.connections.get(workspaceRoot);
293
+ if (!lspConnection || !lspConnection.initialized) {
294
+ throw new Error(`No initialized connection for workspace: ${workspaceRoot}`);
295
+ }
296
+ const docManager = this.getDocumentManager(workspaceRoot);
297
+ if (!docManager.isDocumentOpen(filePath)) {
298
+ await this.openDocument(workspaceRoot, filePath, newContent);
299
+ return;
300
+ }
301
+ const newVersion = docManager.updateDocument(filePath, newContent);
302
+ const uri = docManager.filePathToUri(filePath);
303
+ await lspConnection.connection.sendNotification("textDocument/didChange", {
304
+ textDocument: {
305
+ uri,
306
+ version: newVersion
307
+ },
308
+ contentChanges: [{ text: newContent }]
309
+ });
310
+ await this.waitForAnalysis();
311
+ }
312
+ async closeDocument(workspaceRoot, filePath) {
313
+ const lspConnection = this.connections.get(workspaceRoot);
314
+ if (!lspConnection || !lspConnection.initialized) {
315
+ return;
316
+ }
317
+ const docManager = this.getDocumentManager(workspaceRoot);
318
+ if (!docManager.isDocumentOpen(filePath)) {
319
+ return;
320
+ }
321
+ const uri = docManager.filePathToUri(filePath);
322
+ await lspConnection.connection.sendNotification("textDocument/didClose", {
323
+ textDocument: { uri }
324
+ });
325
+ docManager.closeDocument(filePath);
326
+ }
327
+ async waitForAnalysis() {
328
+ await new Promise((resolve3) => setTimeout(resolve3, LspConnectionManager.DOCUMENT_READY_DELAY));
329
+ }
330
+ async sendRequest(workspaceRoot, method, params) {
331
+ const lspConnection = this.connections.get(workspaceRoot);
332
+ if (!lspConnection || !lspConnection.initialized) {
333
+ throw new Error(`No initialized connection for workspace: ${workspaceRoot}`);
334
+ }
335
+ lspConnection.lastUsed = Date.now();
336
+ console.error(`[LSP] sendRequest: ${method}`);
337
+ const result = await lspConnection.connection.sendRequest(method, params);
338
+ console.error(`[LSP] Request ${method} done`);
339
+ return result;
340
+ }
341
+ async closeConnection(workspaceRoot) {
342
+ const lspConnection = this.connections.get(workspaceRoot);
343
+ if (!lspConnection) {
344
+ return;
345
+ }
346
+ const docManager = this.documentManagers.get(workspaceRoot);
347
+ if (docManager) {
348
+ docManager.closeAll();
349
+ this.documentManagers.delete(workspaceRoot);
350
+ }
351
+ if (lspConnection.initialized) {
352
+ try {
353
+ await lspConnection.connection.sendRequest("shutdown");
354
+ await lspConnection.connection.sendNotification("exit");
355
+ } catch {}
356
+ }
357
+ lspConnection.connection.dispose();
358
+ if (!lspConnection.process.killed) {
359
+ lspConnection.process.kill();
360
+ }
361
+ this.connections.delete(workspaceRoot);
362
+ }
363
+ async closeAll() {
364
+ const workspaceRoots = Array.from(this.connections.keys());
365
+ await Promise.all(workspaceRoots.map((root) => this.closeConnection(root)));
366
+ }
367
+ getStatus() {
368
+ return Array.from(this.connections.entries()).map(([root, conn]) => ({
369
+ workspaceRoot: root,
370
+ initialized: conn.initialized,
371
+ lastUsed: new Date(conn.lastUsed)
372
+ }));
373
+ }
374
+ }
375
+ var connectionManagerInstance = null;
376
+ function getConnectionManager() {
377
+ if (!connectionManagerInstance) {
378
+ connectionManagerInstance = new LspConnectionManager;
379
+ }
380
+ return connectionManagerInstance;
381
+ }
382
+ // src/lsp/types.ts
383
+ var SymbolKindNames = {
384
+ 1: "File",
385
+ 2: "Module",
386
+ 3: "Namespace",
387
+ 4: "Package",
388
+ 5: "Class",
389
+ 6: "Method",
390
+ 7: "Property",
391
+ 8: "Field",
392
+ 9: "Constructor",
393
+ 10: "Enum",
394
+ 11: "Interface",
395
+ 12: "Function",
396
+ 13: "Variable",
397
+ 14: "Constant",
398
+ 15: "String",
399
+ 16: "Number",
400
+ 17: "Boolean",
401
+ 18: "Array",
402
+ 19: "Object",
403
+ 20: "Key",
404
+ 21: "Null",
405
+ 22: "EnumMember",
406
+ 23: "Struct",
407
+ 24: "Event",
408
+ 25: "Operator",
409
+ 26: "TypeParameter"
410
+ };
411
+ // src/lsp-client.ts
412
+ function log(message) {
413
+ console.error(`[LSP] ${message}`);
414
+ }
415
+
416
+ class LspClient {
417
+ connectionManager;
418
+ constructor() {
419
+ this.connectionManager = getConnectionManager();
420
+ }
421
+ async start() {}
422
+ async stop() {
423
+ await this.connectionManager.closeAll();
424
+ }
425
+ async ensureDocumentOpen(workspaceRoot, filePath) {
426
+ const docManager = this.connectionManager.getDocumentManager(workspaceRoot);
427
+ if (docManager.isDocumentOpen(filePath)) {
428
+ return;
429
+ }
430
+ const content = fs2.readFileSync(filePath, "utf-8");
431
+ await this.connectionManager.openDocument(workspaceRoot, filePath, content);
432
+ }
433
+ async prepareRequest(filePath) {
434
+ const workspaceRoot = findProjectRoot(filePath);
435
+ log(`Workspace root: ${workspaceRoot}`);
436
+ await this.connectionManager.getConnection({ workspaceRoot });
437
+ await this.ensureDocumentOpen(workspaceRoot, filePath);
438
+ return workspaceRoot;
439
+ }
440
+ getDocumentUri(workspaceRoot, filePath) {
441
+ return this.connectionManager.getDocumentManager(workspaceRoot).filePathToUri(filePath);
442
+ }
443
+ async hover(filePath, position) {
444
+ const workspaceRoot = await this.prepareRequest(filePath);
445
+ const uri = this.getDocumentUri(workspaceRoot, filePath);
446
+ log("Sending hover request");
447
+ return await this.connectionManager.sendRequest(workspaceRoot, "textDocument/hover", {
448
+ textDocument: { uri },
449
+ position
450
+ });
451
+ }
452
+ async definition(filePath, position) {
453
+ const workspaceRoot = await this.prepareRequest(filePath);
454
+ const uri = this.getDocumentUri(workspaceRoot, filePath);
455
+ log("Sending definition request");
456
+ return await this.connectionManager.sendRequest(workspaceRoot, "textDocument/definition", {
457
+ textDocument: { uri },
458
+ position
459
+ });
460
+ }
461
+ async references(filePath, position, includeDeclaration = true) {
462
+ const workspaceRoot = await this.prepareRequest(filePath);
463
+ const uri = this.getDocumentUri(workspaceRoot, filePath);
464
+ log("Sending references request");
465
+ return await this.connectionManager.sendRequest(workspaceRoot, "textDocument/references", {
466
+ textDocument: { uri },
467
+ position,
468
+ context: { includeDeclaration }
469
+ });
470
+ }
471
+ async completions(filePath, position) {
472
+ const workspaceRoot = await this.prepareRequest(filePath);
473
+ const uri = this.getDocumentUri(workspaceRoot, filePath);
474
+ log("Sending completions request");
475
+ return await this.connectionManager.sendRequest(workspaceRoot, "textDocument/completion", {
476
+ textDocument: { uri },
477
+ position
478
+ });
479
+ }
480
+ async signatureHelp(filePath, position) {
481
+ const workspaceRoot = await this.prepareRequest(filePath);
482
+ const uri = this.getDocumentUri(workspaceRoot, filePath);
483
+ log("Sending signatureHelp request");
484
+ return await this.connectionManager.sendRequest(workspaceRoot, "textDocument/signatureHelp", {
485
+ textDocument: { uri },
486
+ position
487
+ });
488
+ }
489
+ async rename(filePath, position, newName) {
490
+ const workspaceRoot = await this.prepareRequest(filePath);
491
+ const uri = this.getDocumentUri(workspaceRoot, filePath);
492
+ log("Sending rename request");
493
+ return await this.connectionManager.sendRequest(workspaceRoot, "textDocument/rename", {
494
+ textDocument: { uri },
495
+ position,
496
+ newName
497
+ });
498
+ }
499
+ async getDiagnostics(filePath) {
500
+ const workspaceRoot = await this.prepareRequest(filePath);
501
+ const uri = this.getDocumentUri(workspaceRoot, filePath);
502
+ const content = fs2.readFileSync(filePath, "utf-8");
503
+ const docManager = this.connectionManager.getDocumentManager(workspaceRoot);
504
+ if (docManager.isDocumentOpen(filePath)) {
505
+ await this.connectionManager.closeDocument(workspaceRoot, filePath);
506
+ }
507
+ await this.connectionManager.openDocument(workspaceRoot, filePath, content);
508
+ await new Promise((resolve3) => setTimeout(resolve3, 500));
509
+ log("Getting diagnostics via CLI");
510
+ return this.getDiagnosticsViaCli(filePath, workspaceRoot);
511
+ }
512
+ getDiagnosticsViaCli(filePath, workspaceRoot) {
513
+ const { execSync } = __require("child_process");
514
+ try {
515
+ const result = execSync(`pyright --outputjson "${filePath}"`, {
516
+ encoding: "utf-8",
517
+ cwd: workspaceRoot,
518
+ timeout: 30000,
519
+ maxBuffer: 10 * 1024 * 1024
520
+ });
521
+ const parsed = JSON.parse(result);
522
+ return this.convertPyrightDiagnostics(parsed);
523
+ } catch (error) {
524
+ if (error && typeof error === "object" && "stdout" in error) {
525
+ try {
526
+ const parsed = JSON.parse(error.stdout);
527
+ return this.convertPyrightDiagnostics(parsed);
528
+ } catch {}
529
+ }
530
+ return [];
531
+ }
532
+ }
533
+ convertPyrightDiagnostics(output) {
534
+ if (!output.generalDiagnostics) {
535
+ return [];
536
+ }
537
+ return output.generalDiagnostics.map((d) => ({
538
+ range: {
539
+ start: { line: d.range.start.line, character: d.range.start.character },
540
+ end: { line: d.range.end.line, character: d.range.end.character }
541
+ },
542
+ severity: d.severity,
543
+ message: d.message,
544
+ source: "pyright",
545
+ code: d.rule
546
+ }));
547
+ }
548
+ async documentSymbols(filePath) {
549
+ const workspaceRoot = await this.prepareRequest(filePath);
550
+ const uri = this.getDocumentUri(workspaceRoot, filePath);
551
+ log("Sending documentSymbol request");
552
+ return await this.connectionManager.sendRequest(workspaceRoot, "textDocument/documentSymbol", {
553
+ textDocument: { uri }
554
+ });
555
+ }
556
+ async updateDocument(filePath, content) {
557
+ const workspaceRoot = findProjectRoot(filePath);
558
+ await this.connectionManager.getConnection({ workspaceRoot });
559
+ await this.connectionManager.updateDocument(workspaceRoot, filePath, content);
560
+ log(`Updated document: ${filePath}`);
561
+ }
562
+ getStatus() {
563
+ return this.connectionManager.getStatus();
564
+ }
565
+ }
566
+ var client = null;
567
+ function getLspClient() {
568
+ if (!client) {
569
+ client = new LspClient;
570
+ }
571
+ return client;
572
+ }
573
+ async function shutdownLspClient() {
574
+ if (client) {
575
+ await client.stop();
576
+ client = null;
577
+ }
578
+ }
579
+
580
+ // src/tools/hover.ts
581
+ var hoverSchema = {
582
+ file: z.string().describe("Absolute path to the Python file"),
583
+ line: z.number().int().positive().describe("Line number (1-based)"),
584
+ column: z.number().int().positive().describe("Column number (1-based)")
585
+ };
586
+ async function hover(args) {
587
+ console.error(`[hover] Starting hover for ${args.file}:${args.line}:${args.column}`);
588
+ const client2 = getLspClient();
589
+ const position = toPosition(args.line, args.column);
590
+ console.error(`[hover] Calling LSP hover...`);
591
+ const result = await client2.hover(args.file, position);
592
+ console.error(`[hover] Got result: ${result ? "yes" : "no"}`);
593
+ if (!result) {
594
+ return {
595
+ content: [{ type: "text", text: "No hover information available at this position." }]
596
+ };
597
+ }
598
+ let hoverText = "";
599
+ if (typeof result.contents === "string") {
600
+ hoverText = result.contents;
601
+ } else if (Array.isArray(result.contents)) {
602
+ hoverText = result.contents.map((c) => typeof c === "string" ? c : c.value).join(`
603
+
604
+ `);
605
+ } else if ("kind" in result.contents) {
606
+ hoverText = result.contents.value;
607
+ } else if ("value" in result.contents) {
608
+ hoverText = result.contents.value;
609
+ }
610
+ let output = `**Hover Info** at ${args.file}:${args.line}:${args.column}
611
+
612
+ `;
613
+ output += hoverText;
614
+ if (result.range) {
615
+ output += `
616
+
617
+ **Range:** ${formatRange(result.range)}`;
618
+ }
619
+ return {
620
+ content: [{ type: "text", text: output }]
621
+ };
622
+ }
623
+
624
+ // src/tools/definition.ts
625
+ import { z as z2 } from "zod";
626
+ var definitionSchema = {
627
+ file: z2.string().describe("Absolute path to the Python file"),
628
+ line: z2.number().int().positive().describe("Line number (1-based)"),
629
+ column: z2.number().int().positive().describe("Column number (1-based)")
630
+ };
631
+ async function definition(args) {
632
+ const client2 = getLspClient();
633
+ const position = toPosition(args.line, args.column);
634
+ const result = await client2.definition(args.file, position);
635
+ if (!result) {
636
+ return {
637
+ content: [{ type: "text", text: "No definition found at this position." }]
638
+ };
639
+ }
640
+ const locations = [];
641
+ if (Array.isArray(result)) {
642
+ for (const item of result) {
643
+ if ("targetUri" in item) {
644
+ const link = item;
645
+ const pos = fromPosition(link.targetSelectionRange.start);
646
+ locations.push({
647
+ file: uriToPath(link.targetUri),
648
+ line: pos.line,
649
+ column: pos.column
650
+ });
651
+ } else {
652
+ const loc = item;
653
+ const pos = fromPosition(loc.range.start);
654
+ locations.push({
655
+ file: uriToPath(loc.uri),
656
+ line: pos.line,
657
+ column: pos.column
658
+ });
659
+ }
660
+ }
661
+ } else {
662
+ const loc = result;
663
+ const pos = fromPosition(loc.range.start);
664
+ locations.push({
665
+ file: uriToPath(loc.uri),
666
+ line: pos.line,
667
+ column: pos.column
668
+ });
669
+ }
670
+ if (locations.length === 0) {
671
+ return {
672
+ content: [{ type: "text", text: "No definition found at this position." }]
673
+ };
674
+ }
675
+ let output = `**Definition(s)** for symbol at ${args.file}:${args.line}:${args.column}
676
+
677
+ `;
678
+ for (const loc of locations) {
679
+ output += `- ${loc.file}:${loc.line}:${loc.column}
680
+ `;
681
+ }
682
+ return {
683
+ content: [{ type: "text", text: output }]
684
+ };
685
+ }
686
+
687
+ // src/tools/references.ts
688
+ import { z as z3 } from "zod";
689
+ import { execSync } from "child_process";
690
+ import { readFileSync as readFileSync3 } from "fs";
691
+ var referencesSchema = {
692
+ file: z3.string().describe("Absolute path to the Python file"),
693
+ line: z3.number().int().positive().describe("Line number (1-based)"),
694
+ column: z3.number().int().positive().describe("Column number (1-based)")
695
+ };
696
+ function getSymbolAtPosition(filePath, line, column) {
697
+ try {
698
+ const content = readFileSync3(filePath, "utf-8");
699
+ const lines = content.split(`
700
+ `);
701
+ const targetLine = lines[line - 1];
702
+ if (!targetLine)
703
+ return null;
704
+ const col = column - 1;
705
+ let start = col;
706
+ let end = col;
707
+ while (start > 0 && /[\w_]/.test(targetLine[start - 1])) {
708
+ start--;
709
+ }
710
+ while (end < targetLine.length && /[\w_]/.test(targetLine[end])) {
711
+ end++;
712
+ }
713
+ const symbol = targetLine.slice(start, end);
714
+ return symbol.length > 0 ? symbol : null;
715
+ } catch {
716
+ return null;
717
+ }
718
+ }
719
+ async function references(args) {
720
+ const { file, line, column } = args;
721
+ const symbol = getSymbolAtPosition(file, line, column);
722
+ if (!symbol) {
723
+ return {
724
+ content: [{ type: "text", text: "Could not identify symbol at this position." }]
725
+ };
726
+ }
727
+ const projectRoot = findProjectRoot(file);
728
+ const pattern = `\\b${symbol}\\b`;
729
+ let rgOutput;
730
+ try {
731
+ rgOutput = execSync(`rg --no-heading --line-number --column --type py "${pattern}" "${projectRoot}"`, {
732
+ encoding: "utf-8",
733
+ maxBuffer: 10 * 1024 * 1024,
734
+ timeout: 30000
735
+ });
736
+ } catch (e) {
737
+ const error = e;
738
+ if (error.status === 1) {
739
+ return {
740
+ content: [
741
+ {
742
+ type: "text",
743
+ text: `No references found for symbol \`${symbol}\``
744
+ }
745
+ ]
746
+ };
747
+ }
748
+ try {
749
+ rgOutput = execSync(`grep -rn --include="*.py" -w "${symbol}" "${projectRoot}"`, {
750
+ encoding: "utf-8",
751
+ maxBuffer: 10 * 1024 * 1024,
752
+ timeout: 30000
753
+ });
754
+ } catch {
755
+ return {
756
+ content: [
757
+ {
758
+ type: "text",
759
+ text: `No references found for symbol \`${symbol}\``
760
+ }
761
+ ]
762
+ };
763
+ }
764
+ }
765
+ const refs = [];
766
+ const lines = rgOutput.trim().split(`
767
+ `).filter(Boolean);
768
+ for (const outputLine of lines) {
769
+ const match = outputLine.match(/^(.+?):(\d+):(\d+):(.*)$/);
770
+ if (match) {
771
+ const [, filePath, lineNum, colNum, text] = match;
772
+ refs.push({
773
+ file: filePath,
774
+ line: parseInt(lineNum, 10),
775
+ column: parseInt(colNum, 10),
776
+ text: text.trim(),
777
+ isDefinition: filePath === file && parseInt(lineNum, 10) === line
778
+ });
779
+ } else {
780
+ const grepMatch = outputLine.match(/^(.+?):(\d+):(.*)$/);
781
+ if (grepMatch) {
782
+ const [, filePath, lineNum, text] = grepMatch;
783
+ refs.push({
784
+ file: filePath,
785
+ line: parseInt(lineNum, 10),
786
+ column: 1,
787
+ text: text.trim(),
788
+ isDefinition: filePath === file && parseInt(lineNum, 10) === line
789
+ });
790
+ }
791
+ }
792
+ }
793
+ if (refs.length === 0) {
794
+ return {
795
+ content: [
796
+ {
797
+ type: "text",
798
+ text: `No references found for symbol \`${symbol}\``
799
+ }
800
+ ]
801
+ };
802
+ }
803
+ refs.sort((a, b) => {
804
+ if (a.isDefinition && !b.isDefinition)
805
+ return -1;
806
+ if (!a.isDefinition && b.isDefinition)
807
+ return 1;
808
+ if (a.file !== b.file)
809
+ return a.file.localeCompare(b.file);
810
+ return a.line - b.line;
811
+ });
812
+ const byFile = new Map;
813
+ for (const ref of refs) {
814
+ const list = byFile.get(ref.file) || [];
815
+ list.push(ref);
816
+ byFile.set(ref.file, list);
817
+ }
818
+ let output = `**References** for \`${symbol}\`
819
+
820
+ `;
821
+ output += `Found ${refs.length} reference(s) in ${byFile.size} file(s):
822
+
823
+ `;
824
+ for (const [filePath, fileRefs] of byFile) {
825
+ output += `### ${filePath}
826
+
827
+ `;
828
+ for (const ref of fileRefs) {
829
+ const marker = ref.isDefinition ? " (definition)" : "";
830
+ output += `- **${ref.line}:${ref.column}**${marker}: \`${ref.text}\`
831
+ `;
832
+ }
833
+ output += `
834
+ `;
835
+ }
836
+ return {
837
+ content: [{ type: "text", text: output }]
838
+ };
839
+ }
840
+
841
+ // src/tools/completions.ts
842
+ import { z as z4 } from "zod";
843
+ import { CompletionItemKind } from "vscode-languageserver-protocol";
844
+ var completionsSchema = {
845
+ file: z4.string().describe("Absolute path to the Python file"),
846
+ line: z4.number().int().positive().describe("Line number (1-based)"),
847
+ column: z4.number().int().positive().describe("Column number (1-based)"),
848
+ limit: z4.number().int().positive().optional().default(20).describe("Maximum number of completions to return")
849
+ };
850
+ var kindNames = {
851
+ [CompletionItemKind.Text]: "Text",
852
+ [CompletionItemKind.Method]: "Method",
853
+ [CompletionItemKind.Function]: "Function",
854
+ [CompletionItemKind.Constructor]: "Constructor",
855
+ [CompletionItemKind.Field]: "Field",
856
+ [CompletionItemKind.Variable]: "Variable",
857
+ [CompletionItemKind.Class]: "Class",
858
+ [CompletionItemKind.Interface]: "Interface",
859
+ [CompletionItemKind.Module]: "Module",
860
+ [CompletionItemKind.Property]: "Property",
861
+ [CompletionItemKind.Unit]: "Unit",
862
+ [CompletionItemKind.Value]: "Value",
863
+ [CompletionItemKind.Enum]: "Enum",
864
+ [CompletionItemKind.Keyword]: "Keyword",
865
+ [CompletionItemKind.Snippet]: "Snippet",
866
+ [CompletionItemKind.Color]: "Color",
867
+ [CompletionItemKind.File]: "File",
868
+ [CompletionItemKind.Reference]: "Reference",
869
+ [CompletionItemKind.Folder]: "Folder",
870
+ [CompletionItemKind.EnumMember]: "EnumMember",
871
+ [CompletionItemKind.Constant]: "Constant",
872
+ [CompletionItemKind.Struct]: "Struct",
873
+ [CompletionItemKind.Event]: "Event",
874
+ [CompletionItemKind.Operator]: "Operator",
875
+ [CompletionItemKind.TypeParameter]: "TypeParameter"
876
+ };
877
+ async function completions(args) {
878
+ const client2 = getLspClient();
879
+ const position = toPosition(args.line, args.column);
880
+ const limit = args.limit ?? 20;
881
+ const result = await client2.completions(args.file, position);
882
+ if (!result) {
883
+ return {
884
+ content: [{ type: "text", text: "No completions available at this position." }]
885
+ };
886
+ }
887
+ let items;
888
+ if (Array.isArray(result)) {
889
+ items = result;
890
+ } else {
891
+ items = result.items;
892
+ }
893
+ if (items.length === 0) {
894
+ return {
895
+ content: [{ type: "text", text: "No completions available at this position." }]
896
+ };
897
+ }
898
+ const limitedItems = items.slice(0, limit);
899
+ let output = `**Completions** at ${args.file}:${args.line}:${args.column}
900
+
901
+ `;
902
+ output += `Showing ${limitedItems.length} of ${items.length} completion(s):
903
+
904
+ `;
905
+ for (const item of limitedItems) {
906
+ const kind = item.kind ? kindNames[item.kind] || "Unknown" : "Unknown";
907
+ const detail = item.detail ? ` - ${item.detail}` : "";
908
+ output += `- **${item.label}** (${kind})${detail}
909
+ `;
910
+ }
911
+ return {
912
+ content: [{ type: "text", text: output }]
913
+ };
914
+ }
915
+
916
+ // src/tools/diagnostics.ts
917
+ import { z as z5 } from "zod";
918
+ import { execSync as execSync2 } from "child_process";
919
+ var diagnosticsSchema = {
920
+ path: z5.string().describe("Path to a Python file or directory to check")
921
+ };
922
+ async function diagnostics(args) {
923
+ const { path: path2 } = args;
924
+ const projectRoot = findProjectRoot(path2);
925
+ const target = path2;
926
+ const cmd = `pyright "${target}" --outputjson`;
927
+ let output;
928
+ try {
929
+ const result = execSync2(cmd, {
930
+ encoding: "utf-8",
931
+ cwd: projectRoot,
932
+ timeout: 60000,
933
+ maxBuffer: 10 * 1024 * 1024
934
+ });
935
+ output = JSON.parse(result);
936
+ } catch (e) {
937
+ const error = e;
938
+ if (error.stdout) {
939
+ try {
940
+ output = JSON.parse(error.stdout);
941
+ } catch {
942
+ return {
943
+ content: [
944
+ {
945
+ type: "text",
946
+ text: `Error running pyright: ${error.message || "Unknown error"}`
947
+ }
948
+ ]
949
+ };
950
+ }
951
+ } else {
952
+ return {
953
+ content: [
954
+ {
955
+ type: "text",
956
+ text: `Error running pyright: ${error.message || "Unknown error"}
957
+
958
+ Make sure pyright is installed: npm install -g pyright`
959
+ }
960
+ ]
961
+ };
962
+ }
963
+ }
964
+ const diags = output.generalDiagnostics || [];
965
+ const summary = output.summary;
966
+ if (diags.length === 0) {
967
+ let text2 = `**No issues found**
968
+
969
+ `;
970
+ text2 += `- Files analyzed: ${summary.filesAnalyzed}
971
+ `;
972
+ text2 += `- Time: ${summary.timeInSec}s`;
973
+ return {
974
+ content: [{ type: "text", text: text2 }]
975
+ };
976
+ }
977
+ const byFile = new Map;
978
+ for (const diag of diags) {
979
+ const list = byFile.get(diag.file) || [];
980
+ list.push(diag);
981
+ byFile.set(diag.file, list);
982
+ }
983
+ let text = `**Diagnostics Summary**
984
+
985
+ `;
986
+ text += `- Errors: ${summary.errorCount}
987
+ `;
988
+ text += `- Warnings: ${summary.warningCount}
989
+ `;
990
+ text += `- Information: ${summary.informationCount}
991
+ `;
992
+ text += `- Files analyzed: ${summary.filesAnalyzed}
993
+ `;
994
+ text += `- Time: ${summary.timeInSec}s
995
+
996
+ `;
997
+ text += `---
998
+
999
+ `;
1000
+ for (const [filePath, fileDiags] of byFile) {
1001
+ text += `### ${filePath}
1002
+
1003
+ `;
1004
+ for (const diag of fileDiags) {
1005
+ const line = diag.range.start.line + 1;
1006
+ const col = diag.range.start.character + 1;
1007
+ const rule = diag.rule ? ` (${diag.rule})` : "";
1008
+ const icon = diag.severity === "error" ? "❌" : diag.severity === "warning" ? "⚠️" : "ℹ️";
1009
+ text += `- ${icon} **${diag.severity}** at ${line}:${col}${rule}
1010
+ `;
1011
+ text += ` ${diag.message}
1012
+
1013
+ `;
1014
+ }
1015
+ }
1016
+ return {
1017
+ content: [{ type: "text", text }]
1018
+ };
1019
+ }
1020
+
1021
+ // src/tools/signature-help.ts
1022
+ import { z as z6 } from "zod";
1023
+ var signatureHelpSchema = {
1024
+ file: z6.string().describe("Absolute path to the Python file"),
1025
+ line: z6.number().int().positive().describe("Line number (1-based)"),
1026
+ column: z6.number().int().positive().describe("Column number (1-based)")
1027
+ };
1028
+ async function signatureHelp(args) {
1029
+ const client2 = getLspClient();
1030
+ const position = toPosition(args.line, args.column);
1031
+ const result = await client2.signatureHelp(args.file, position);
1032
+ if (!result || result.signatures.length === 0) {
1033
+ return {
1034
+ content: [{ type: "text", text: "No signature help available at this position." }]
1035
+ };
1036
+ }
1037
+ let output = `**Signature Help** at ${args.file}:${args.line}:${args.column}
1038
+
1039
+ `;
1040
+ const activeIndex = result.activeSignature ?? 0;
1041
+ const activeParam = result.activeParameter ?? 0;
1042
+ for (let i = 0;i < result.signatures.length; i++) {
1043
+ const sig = result.signatures[i];
1044
+ const isActive = i === activeIndex;
1045
+ output += `${isActive ? "→ " : " "}**${sig.label}**
1046
+ `;
1047
+ if (sig.documentation) {
1048
+ const doc = typeof sig.documentation === "string" ? sig.documentation : sig.documentation.value;
1049
+ output += ` ${doc}
1050
+ `;
1051
+ }
1052
+ if (sig.parameters && sig.parameters.length > 0) {
1053
+ output += `
1054
+ Parameters:
1055
+ `;
1056
+ for (let j = 0;j < sig.parameters.length; j++) {
1057
+ const param = sig.parameters[j];
1058
+ const isActiveParam = isActive && j === activeParam;
1059
+ const label = typeof param.label === "string" ? param.label : sig.label.slice(param.label[0], param.label[1]);
1060
+ output += ` ${isActiveParam ? "→ " : " "}${label}`;
1061
+ if (param.documentation) {
1062
+ const paramDoc = typeof param.documentation === "string" ? param.documentation : param.documentation.value;
1063
+ output += ` - ${paramDoc}`;
1064
+ }
1065
+ output += `
1066
+ `;
1067
+ }
1068
+ }
1069
+ output += `
1070
+ `;
1071
+ }
1072
+ return {
1073
+ content: [{ type: "text", text: output }]
1074
+ };
1075
+ }
1076
+
1077
+ // src/tools/rename.ts
1078
+ import { z as z7 } from "zod";
1079
+ import { execSync as execSync3 } from "child_process";
1080
+ import { readFileSync as readFileSync4 } from "fs";
1081
+ var renameSchema = {
1082
+ file: z7.string().describe("Absolute path to the Python file"),
1083
+ line: z7.number().int().positive().describe("Line number (1-based)"),
1084
+ column: z7.number().int().positive().describe("Column number (1-based)"),
1085
+ newName: z7.string().describe("New name for the symbol")
1086
+ };
1087
+ function getSymbolAtPosition2(filePath, line, column) {
1088
+ try {
1089
+ const content = readFileSync4(filePath, "utf-8");
1090
+ const lines = content.split(`
1091
+ `);
1092
+ const targetLine = lines[line - 1];
1093
+ if (!targetLine)
1094
+ return null;
1095
+ const col = column - 1;
1096
+ let start = col;
1097
+ let end = col;
1098
+ while (start > 0 && /[\w_]/.test(targetLine[start - 1])) {
1099
+ start--;
1100
+ }
1101
+ while (end < targetLine.length && /[\w_]/.test(targetLine[end])) {
1102
+ end++;
1103
+ }
1104
+ const symbol = targetLine.slice(start, end);
1105
+ return symbol.length > 0 ? { symbol, start: start + 1, end: end + 1 } : null;
1106
+ } catch {
1107
+ return null;
1108
+ }
1109
+ }
1110
+ async function rename(args) {
1111
+ const { file, line, column, newName } = args;
1112
+ const symbolInfo = getSymbolAtPosition2(file, line, column);
1113
+ if (!symbolInfo) {
1114
+ return {
1115
+ content: [{ type: "text", text: "Could not identify symbol at this position." }]
1116
+ };
1117
+ }
1118
+ const { symbol: oldName } = symbolInfo;
1119
+ if (oldName === newName) {
1120
+ return {
1121
+ content: [{ type: "text", text: "New name is the same as the old name." }]
1122
+ };
1123
+ }
1124
+ const projectRoot = findProjectRoot(file);
1125
+ const pattern = `\\b${oldName}\\b`;
1126
+ let rgOutput;
1127
+ try {
1128
+ rgOutput = execSync3(`rg --no-heading --line-number --column --type py "${pattern}" "${projectRoot}"`, {
1129
+ encoding: "utf-8",
1130
+ maxBuffer: 10 * 1024 * 1024,
1131
+ timeout: 30000
1132
+ });
1133
+ } catch (e) {
1134
+ const error = e;
1135
+ if (error.status === 1) {
1136
+ return {
1137
+ content: [
1138
+ {
1139
+ type: "text",
1140
+ text: `No references found for symbol \`${oldName}\``
1141
+ }
1142
+ ]
1143
+ };
1144
+ }
1145
+ try {
1146
+ rgOutput = execSync3(`grep -rn --include="*.py" -w "${oldName}" "${projectRoot}"`, {
1147
+ encoding: "utf-8",
1148
+ maxBuffer: 10 * 1024 * 1024,
1149
+ timeout: 30000
1150
+ });
1151
+ } catch {
1152
+ return {
1153
+ content: [
1154
+ {
1155
+ type: "text",
1156
+ text: `No references found for symbol \`${oldName}\``
1157
+ }
1158
+ ]
1159
+ };
1160
+ }
1161
+ }
1162
+ const edits = [];
1163
+ const outputLines = rgOutput.trim().split(`
1164
+ `).filter(Boolean);
1165
+ for (const outputLine of outputLines) {
1166
+ const match = outputLine.match(/^(.+?):(\d+):(\d+):(.*)$/);
1167
+ if (match) {
1168
+ const [, filePath, lineNum, colNum, lineContent] = match;
1169
+ const col = parseInt(colNum, 10);
1170
+ edits.push({
1171
+ file: filePath,
1172
+ line: parseInt(lineNum, 10),
1173
+ column: col,
1174
+ endColumn: col + oldName.length,
1175
+ oldText: oldName,
1176
+ newText: newName,
1177
+ lineContent: lineContent.trim()
1178
+ });
1179
+ }
1180
+ }
1181
+ if (edits.length === 0) {
1182
+ return {
1183
+ content: [
1184
+ {
1185
+ type: "text",
1186
+ text: `No references found for symbol \`${oldName}\``
1187
+ }
1188
+ ]
1189
+ };
1190
+ }
1191
+ const byFile = new Map;
1192
+ for (const edit of edits) {
1193
+ const list = byFile.get(edit.file) || [];
1194
+ list.push(edit);
1195
+ byFile.set(edit.file, list);
1196
+ }
1197
+ let output = `**Rename Preview**
1198
+
1199
+ `;
1200
+ output += `- Symbol: \`${oldName}\` → \`${newName}\`
1201
+ `;
1202
+ output += `- Found ${edits.length} occurrence(s) in ${byFile.size} file(s)
1203
+
1204
+ `;
1205
+ output += `---
1206
+
1207
+ `;
1208
+ for (const [filePath, fileEdits] of byFile) {
1209
+ output += `### ${filePath}
1210
+
1211
+ `;
1212
+ for (const edit of fileEdits) {
1213
+ const preview = edit.lineContent.replace(new RegExp(`\\b${oldName}\\b`), `~~${oldName}~~ **${newName}**`);
1214
+ output += `- Line ${edit.line}: ${preview}
1215
+ `;
1216
+ }
1217
+ output += `
1218
+ `;
1219
+ }
1220
+ output += `---
1221
+
1222
+ `;
1223
+ output += `**Note:** This is a preview only. To apply the rename, use your editor's rename feature or run:
1224
+ `;
1225
+ output += `\`\`\`bash
1226
+ `;
1227
+ output += `# Using sed (backup recommended)
1228
+ `;
1229
+ output += `find "${projectRoot}" -name "*.py" -exec sed -i '' 's/\\b${oldName}\\b/${newName}/g' {} +
1230
+ `;
1231
+ output += `\`\`\``;
1232
+ return {
1233
+ content: [{ type: "text", text: output }]
1234
+ };
1235
+ }
1236
+
1237
+ // src/tools/search.ts
1238
+ import { z as z8 } from "zod";
1239
+ import { execSync as execSync4 } from "child_process";
1240
+ import * as path2 from "path";
1241
+ var searchSchema = {
1242
+ pattern: z8.string().describe("The regex pattern to search for"),
1243
+ path: z8.string().optional().describe("Directory or file to search in (defaults to current working directory)"),
1244
+ glob: z8.string().optional().describe('Glob pattern to filter files (e.g., "*.py", "**/*.ts")'),
1245
+ caseSensitive: z8.boolean().optional().default(true).describe("Whether the search is case sensitive"),
1246
+ maxResults: z8.number().int().positive().optional().default(50).describe("Maximum number of results to return")
1247
+ };
1248
+ async function search(args) {
1249
+ const searchPath = args.path || process.cwd();
1250
+ const caseSensitive = args.caseSensitive ?? true;
1251
+ const maxResults = args.maxResults ?? 50;
1252
+ const rgArgs = [
1253
+ "--json",
1254
+ "--line-number",
1255
+ "--column"
1256
+ ];
1257
+ if (!caseSensitive) {
1258
+ rgArgs.push("--ignore-case");
1259
+ }
1260
+ if (args.glob) {
1261
+ rgArgs.push("--glob", args.glob);
1262
+ }
1263
+ rgArgs.push("--", args.pattern, searchPath);
1264
+ try {
1265
+ const result = execSync4(`rg ${rgArgs.map((a) => `'${a}'`).join(" ")}`, {
1266
+ encoding: "utf-8",
1267
+ maxBuffer: 10 * 1024 * 1024
1268
+ });
1269
+ const results = [];
1270
+ const lines = result.split(`
1271
+ `).filter(Boolean);
1272
+ for (const line of lines) {
1273
+ if (results.length >= maxResults)
1274
+ break;
1275
+ try {
1276
+ const json = JSON.parse(line);
1277
+ if (json.type === "match") {
1278
+ const data = json.data;
1279
+ const filePath = data.path.text;
1280
+ const lineNumber = data.line_number;
1281
+ const lineText = data.lines.text.trimEnd();
1282
+ for (const submatch of data.submatches) {
1283
+ if (results.length >= maxResults)
1284
+ break;
1285
+ results.push({
1286
+ file: path2.resolve(filePath),
1287
+ line: lineNumber,
1288
+ column: submatch.start + 1,
1289
+ text: lineText,
1290
+ match: submatch.match.text
1291
+ });
1292
+ }
1293
+ }
1294
+ } catch {}
1295
+ }
1296
+ if (results.length === 0) {
1297
+ return {
1298
+ content: [{ type: "text", text: `No matches found for pattern: ${args.pattern}` }]
1299
+ };
1300
+ }
1301
+ let output = `**Search Results** for \`${args.pattern}\`
1302
+
1303
+ `;
1304
+ output += `Found ${results.length} match(es)${results.length >= maxResults ? ` (limited to ${maxResults})` : ""}:
1305
+
1306
+ `;
1307
+ for (const r of results) {
1308
+ output += `**${r.file}:${r.line}:${r.column}**
1309
+ `;
1310
+ output += ` \`${r.text}\`
1311
+ `;
1312
+ output += ` Match: \`${r.match}\`
1313
+
1314
+ `;
1315
+ }
1316
+ return {
1317
+ content: [{ type: "text", text: output }]
1318
+ };
1319
+ } catch (err) {
1320
+ const error = err;
1321
+ if (error.status === 1) {
1322
+ return {
1323
+ content: [{ type: "text", text: `No matches found for pattern: ${args.pattern}` }]
1324
+ };
1325
+ }
1326
+ return {
1327
+ content: [{ type: "text", text: `Search error: ${error.message || "Unknown error"}` }]
1328
+ };
1329
+ }
1330
+ }
1331
+
1332
+ // src/tools/status.ts
1333
+ import { z as z9 } from "zod";
1334
+ import { execSync as execSync5 } from "child_process";
1335
+ import { existsSync as existsSync2, readFileSync as readFileSync5 } from "fs";
1336
+ import { join as join2 } from "path";
1337
+ var statusSchema = {
1338
+ file: z9.string().describe("A Python file path to check the project status for")
1339
+ };
1340
+ async function status(args) {
1341
+ const { file } = args;
1342
+ const lines = [];
1343
+ const projectRoot = findProjectRoot(file);
1344
+ lines.push(`## Project Root`);
1345
+ lines.push(`\`${projectRoot}\``);
1346
+ lines.push("");
1347
+ lines.push(`## Pyright`);
1348
+ try {
1349
+ const pyrightVersion = execSync5("pyright --version", { encoding: "utf-8" }).trim();
1350
+ lines.push(`- Version: ${pyrightVersion}`);
1351
+ } catch {
1352
+ lines.push(`- ⚠️ **Not installed or not in PATH**`);
1353
+ lines.push(` Install with: \`npm install -g pyright\``);
1354
+ }
1355
+ lines.push("");
1356
+ lines.push(`## Pyright Config`);
1357
+ const pyrightConfigPath = join2(projectRoot, "pyrightconfig.json");
1358
+ const pyprojectPath = join2(projectRoot, "pyproject.toml");
1359
+ if (existsSync2(pyrightConfigPath)) {
1360
+ lines.push(`- Config file: \`pyrightconfig.json\``);
1361
+ try {
1362
+ const config = JSON.parse(readFileSync5(pyrightConfigPath, "utf-8"));
1363
+ if (config.pythonVersion) {
1364
+ lines.push(`- Python version: ${config.pythonVersion}`);
1365
+ }
1366
+ if (config.pythonPlatform) {
1367
+ lines.push(`- Platform: ${config.pythonPlatform}`);
1368
+ }
1369
+ if (config.venvPath) {
1370
+ lines.push(`- Venv path: ${config.venvPath}`);
1371
+ }
1372
+ if (config.venv) {
1373
+ lines.push(`- Venv: ${config.venv}`);
1374
+ }
1375
+ if (config.typeCheckingMode) {
1376
+ lines.push(`- Type checking mode: ${config.typeCheckingMode}`);
1377
+ }
1378
+ if (config.include) {
1379
+ lines.push(`- Include: ${JSON.stringify(config.include)}`);
1380
+ }
1381
+ if (config.exclude) {
1382
+ lines.push(`- Exclude: ${JSON.stringify(config.exclude)}`);
1383
+ }
1384
+ } catch (e) {
1385
+ lines.push(`- ⚠️ Failed to parse config: ${e}`);
1386
+ }
1387
+ } else if (existsSync2(pyprojectPath)) {
1388
+ lines.push(`- Config file: \`pyproject.toml\` (may contain [tool.pyright] section)`);
1389
+ } else {
1390
+ lines.push(`- ⚠️ No pyrightconfig.json or pyproject.toml found`);
1391
+ lines.push(` Pyright will use default settings`);
1392
+ }
1393
+ lines.push("");
1394
+ lines.push(`## Python Environment`);
1395
+ try {
1396
+ const pythonVersion = execSync5("python3 --version", { encoding: "utf-8" }).trim();
1397
+ lines.push(`- System Python: ${pythonVersion}`);
1398
+ } catch {
1399
+ try {
1400
+ const pythonVersion = execSync5("python --version", { encoding: "utf-8" }).trim();
1401
+ lines.push(`- System Python: ${pythonVersion}`);
1402
+ } catch {
1403
+ lines.push(`- ⚠️ Python not found in PATH`);
1404
+ }
1405
+ }
1406
+ const venvPaths = [".venv", "venv", ".env", "env"];
1407
+ for (const venv of venvPaths) {
1408
+ const venvPath = join2(projectRoot, venv);
1409
+ if (existsSync2(venvPath)) {
1410
+ lines.push(`- Virtual env found: \`${venv}/\``);
1411
+ const venvPython = join2(venvPath, "bin", "python");
1412
+ if (existsSync2(venvPython)) {
1413
+ try {
1414
+ const venvVersion = execSync5(`"${venvPython}" --version`, { encoding: "utf-8" }).trim();
1415
+ lines.push(` - ${venvVersion}`);
1416
+ } catch {}
1417
+ }
1418
+ break;
1419
+ }
1420
+ }
1421
+ lines.push("");
1422
+ lines.push(`## File Check`);
1423
+ lines.push(`- File: \`${file}\``);
1424
+ if (existsSync2(file)) {
1425
+ lines.push(`- Exists: ✅`);
1426
+ try {
1427
+ const result = execSync5(`pyright "${file}" --outputjson`, {
1428
+ encoding: "utf-8",
1429
+ cwd: projectRoot,
1430
+ timeout: 30000
1431
+ });
1432
+ const output = JSON.parse(result);
1433
+ const errors = output.generalDiagnostics?.filter((d) => d.severity === "error")?.length || 0;
1434
+ const warnings = output.generalDiagnostics?.filter((d) => d.severity === "warning")?.length || 0;
1435
+ lines.push(`- Diagnostics: ${errors} errors, ${warnings} warnings`);
1436
+ } catch (e) {
1437
+ const error = e;
1438
+ if (error.stdout) {
1439
+ try {
1440
+ const output = JSON.parse(error.stdout);
1441
+ const errors = output.generalDiagnostics?.filter((d) => d.severity === "error")?.length || 0;
1442
+ const warnings = output.generalDiagnostics?.filter((d) => d.severity === "warning")?.length || 0;
1443
+ lines.push(`- Diagnostics: ${errors} errors, ${warnings} warnings`);
1444
+ } catch {
1445
+ lines.push(`- ⚠️ Could not run pyright check`);
1446
+ }
1447
+ } else {
1448
+ lines.push(`- ⚠️ Could not run pyright check`);
1449
+ }
1450
+ }
1451
+ } else {
1452
+ lines.push(`- Exists: ❌ File not found`);
1453
+ }
1454
+ return {
1455
+ content: [{ type: "text", text: lines.join(`
1456
+ `) }]
1457
+ };
1458
+ }
1459
+
1460
+ // src/tools/symbols.ts
1461
+ import { z as z10 } from "zod";
1462
+ import { SymbolKind } from "vscode-languageserver-protocol";
1463
+ var symbolsSchema = {
1464
+ file: z10.string().describe("Absolute path to the Python file"),
1465
+ filter: z10.enum(["all", "classes", "functions", "methods", "variables"]).optional().default("all").describe("Filter symbols by type"),
1466
+ includeChildren: z10.boolean().optional().default(true).describe("Include nested symbols (e.g., methods inside classes)")
1467
+ };
1468
+ function getSymbolKindName(kind) {
1469
+ return SymbolKindNames[kind] || "Unknown";
1470
+ }
1471
+ function matchesFilter(kind, filter) {
1472
+ if (filter === "all") {
1473
+ return true;
1474
+ }
1475
+ switch (filter) {
1476
+ case "classes":
1477
+ return kind === SymbolKind.Class;
1478
+ case "functions":
1479
+ return kind === SymbolKind.Function;
1480
+ case "methods":
1481
+ return kind === SymbolKind.Method;
1482
+ case "variables":
1483
+ return kind === SymbolKind.Variable || kind === SymbolKind.Constant;
1484
+ default:
1485
+ return true;
1486
+ }
1487
+ }
1488
+ function formatSymbol(symbol, filter, includeChildren, indent = 0) {
1489
+ const lines = [];
1490
+ const indentStr = " ".repeat(indent);
1491
+ const kindName = getSymbolKindName(symbol.kind);
1492
+ const line = symbol.range.start.line + 1;
1493
+ const symbolMatches = matchesFilter(symbol.kind, filter);
1494
+ if (symbolMatches) {
1495
+ lines.push(`${indentStr}- **${symbol.name}** (${kindName}) at line ${line}`);
1496
+ }
1497
+ if (includeChildren && symbol.children && symbol.children.length > 0) {
1498
+ for (const child of symbol.children) {
1499
+ const childLines = formatSymbol(child, filter, includeChildren, symbolMatches ? indent + 1 : indent);
1500
+ lines.push(...childLines);
1501
+ }
1502
+ }
1503
+ return lines;
1504
+ }
1505
+ function isDocumentSymbolArray(result) {
1506
+ return Array.isArray(result) && result.length > 0 && "range" in result[0];
1507
+ }
1508
+ async function symbols(args) {
1509
+ const filter = args.filter || "all";
1510
+ const includeChildren = args.includeChildren !== false;
1511
+ console.error(`[symbols] Getting symbols for ${args.file} (filter: ${filter})`);
1512
+ const client2 = getLspClient();
1513
+ const result = await client2.documentSymbols(args.file);
1514
+ console.error(`[symbols] Got result: ${result ? `${result.length} symbols` : "no"}`);
1515
+ if (!result || Array.isArray(result) && result.length === 0) {
1516
+ return {
1517
+ content: [{ type: "text", text: `No symbols found in ${args.file}` }]
1518
+ };
1519
+ }
1520
+ let output = `**Symbols** in ${args.file}
1521
+
1522
+ `;
1523
+ if (isDocumentSymbolArray(result)) {
1524
+ const lines = [];
1525
+ for (const symbol of result) {
1526
+ lines.push(...formatSymbol(symbol, filter, includeChildren));
1527
+ }
1528
+ if (lines.length === 0) {
1529
+ output += `No ${filter} symbols found.`;
1530
+ } else {
1531
+ output += lines.join(`
1532
+ `);
1533
+ }
1534
+ } else {
1535
+ const filteredSymbols = result.filter((s) => matchesFilter(s.kind, filter));
1536
+ if (filteredSymbols.length === 0) {
1537
+ output += `No ${filter} symbols found.`;
1538
+ } else {
1539
+ for (const symbol of filteredSymbols) {
1540
+ const kindName = getSymbolKindName(symbol.kind);
1541
+ const line = symbol.location.range.start.line + 1;
1542
+ output += `- **${symbol.name}** (${kindName}) at line ${line}
1543
+ `;
1544
+ }
1545
+ }
1546
+ }
1547
+ return {
1548
+ content: [{ type: "text", text: output.trim() }]
1549
+ };
1550
+ }
1551
+
1552
+ // src/tools/update-document.ts
1553
+ import { z as z11 } from "zod";
1554
+ var updateDocumentSchema = {
1555
+ file: z11.string().describe("Absolute path to the Python file"),
1556
+ content: z11.string().describe("New content for the file")
1557
+ };
1558
+ async function updateDocument(args) {
1559
+ console.error(`[updateDocument] Updating ${args.file}`);
1560
+ const client2 = getLspClient();
1561
+ await client2.updateDocument(args.file, args.content);
1562
+ return {
1563
+ content: [
1564
+ {
1565
+ type: "text",
1566
+ text: `Document updated: ${args.file}`
1567
+ }
1568
+ ]
1569
+ };
1570
+ }
1571
+
1572
+ // src/index.ts
1573
+ var server = new McpServer({
1574
+ name: "pyright-mcp",
1575
+ version: "1.0.0"
17
1576
  });
18
- // Register hover tool
19
- server.tool('hover', 'Get type information and documentation at a specific position in a Python file', hoverSchema, async (args) => hover(args));
20
- // Register definition tool
21
- server.tool('definition', 'Go to definition of a symbol at a specific position in a Python file', definitionSchema, async (args) => definition(args));
22
- // Register references tool
23
- server.tool('references', 'Find all references to a symbol at a specific position in a Python file', referencesSchema, async (args) => references(args));
24
- // Register completions tool
25
- server.tool('completions', 'Get code completion suggestions at a specific position in a Python file', completionsSchema, async (args) => completions(args));
26
- // Register diagnostics tool
27
- server.tool('diagnostics', 'Get diagnostics (errors, warnings) for a Python file', diagnosticsSchema, async (args) => diagnostics(args));
28
- // Register signature help tool
29
- server.tool('signature_help', 'Get function signature help at a specific position in a Python file', signatureHelpSchema, async (args) => signatureHelp(args));
30
- // Register rename tool
31
- server.tool('rename', 'Preview renaming a symbol at a specific position in a Python file', renameSchema, async (args) => rename(args));
32
- // Register search tool
33
- server.tool('search', 'Search for a pattern in files and return file:line:column locations', searchSchema, async (args) => search(args));
34
- // Register status tool
35
- server.tool('status', 'Check Python/Pyright environment status for a project', statusSchema, async (args) => status(args));
36
- // Start the server
1577
+ server.tool("hover", "Get type information and documentation at a specific position in a Python file", hoverSchema, async (args) => hover(args));
1578
+ server.tool("definition", "Go to definition of a symbol at a specific position in a Python file", definitionSchema, async (args) => definition(args));
1579
+ server.tool("references", "Find all references to a symbol at a specific position in a Python file", referencesSchema, async (args) => references(args));
1580
+ server.tool("completions", "Get code completion suggestions at a specific position in a Python file", completionsSchema, async (args) => completions(args));
1581
+ server.tool("diagnostics", "Get diagnostics (errors, warnings) for a Python file", diagnosticsSchema, async (args) => diagnostics(args));
1582
+ server.tool("signature_help", "Get function signature help at a specific position in a Python file", signatureHelpSchema, async (args) => signatureHelp(args));
1583
+ server.tool("rename", "Preview renaming a symbol at a specific position in a Python file", renameSchema, async (args) => rename(args));
1584
+ server.tool("search", "Search for a pattern in files and return file:line:column locations", searchSchema, async (args) => search(args));
1585
+ server.tool("status", "Check Python/Pyright environment status for a project", statusSchema, async (args) => status(args));
1586
+ server.tool("symbols", "Extract symbols (classes, functions, methods, variables) from a Python file", symbolsSchema, async (args) => symbols(args));
1587
+ server.tool("update_document", "Update the content of an open Python file for incremental analysis", updateDocumentSchema, async (args) => updateDocument(args));
1588
+ async function gracefulShutdown(signal) {
1589
+ console.error(`
1590
+ [Server] Received ${signal}, shutting down gracefully...`);
1591
+ try {
1592
+ await shutdownLspClient();
1593
+ console.error("[Server] LSP connections closed");
1594
+ await server.close();
1595
+ console.error("[Server] MCP server closed");
1596
+ process.exit(0);
1597
+ } catch (error) {
1598
+ console.error("[Server] Error during shutdown:", error);
1599
+ process.exit(1);
1600
+ }
1601
+ }
1602
+ process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
1603
+ process.on("SIGINT", () => gracefulShutdown("SIGINT"));
37
1604
  async function main() {
38
- console.error(`Pyright MCP server`);
39
- console.error(` Workspace: auto-detected from file path`);
40
- // Initialize LSP client (lazy start on first request)
41
- getLspClient();
42
- const transport = new StdioServerTransport();
43
- await server.connect(transport);
44
- console.error(` Ready`);
1605
+ console.error(`Pyright MCP server`);
1606
+ console.error(` Workspace: auto-detected from file path`);
1607
+ getLspClient();
1608
+ const transport = new StdioServerTransport;
1609
+ await server.connect(transport);
1610
+ console.error(` Ready`);
45
1611
  }
46
1612
  main().catch((error) => {
47
- console.error('Failed to start server:', error);
48
- process.exit(1);
1613
+ console.error("Failed to start server:", error);
1614
+ process.exit(1);
49
1615
  });
1616
+
1617
+ //# debugId=6646DB737E47BEFC64756E2164756E21