@treedy/pyright-mcp 1.1.2 → 1.1.4

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.
@@ -1,80 +0,0 @@
1
- import { execSync } from 'child_process';
2
- import { fileURLToPath } from 'url';
3
- import { dirname, resolve } from 'path';
4
- import { findProjectRoot } from './utils/position.js';
5
- const __filename = fileURLToPath(import.meta.url);
6
- const __dirname = dirname(__filename);
7
- const workerScript = resolve(__dirname, 'pyright-worker.mjs');
8
- function log(message) {
9
- console.error(`[LSP] ${message}`);
10
- }
11
- export class LspClient {
12
- constructor() {
13
- // No longer stores workspace root - it's determined per-file
14
- }
15
- async start() {
16
- // No-op, worker starts fresh for each request
17
- }
18
- async stop() {
19
- // No-op
20
- }
21
- runWorker(method, filePath, args) {
22
- // Automatically find project root from file path
23
- const workspaceRoot = findProjectRoot(filePath);
24
- log(`Workspace root: ${workspaceRoot}`);
25
- const params = JSON.stringify({
26
- workspaceRoot,
27
- method,
28
- filePath,
29
- ...args,
30
- });
31
- log(`Running worker: ${method}`);
32
- try {
33
- const result = execSync(`node "${workerScript}" '${params.replace(/'/g, "'\\''")}'`, {
34
- encoding: 'utf-8',
35
- timeout: 30000,
36
- maxBuffer: 10 * 1024 * 1024,
37
- });
38
- const parsed = JSON.parse(result.trim());
39
- if (parsed.success) {
40
- log(`Worker success: ${method}`);
41
- return parsed.result;
42
- }
43
- else {
44
- throw new Error(parsed.error);
45
- }
46
- }
47
- catch (error) {
48
- log(`Worker error: ${error}`);
49
- throw error;
50
- }
51
- }
52
- async hover(filePath, position) {
53
- return this.runWorker('hover', filePath, { position });
54
- }
55
- async definition(filePath, position) {
56
- return this.runWorker('definition', filePath, { position });
57
- }
58
- async references(filePath, position, includeDeclaration = true) {
59
- return this.runWorker('references', filePath, { position, includeDeclaration });
60
- }
61
- async completions(filePath, position) {
62
- return this.runWorker('completions', filePath, { position });
63
- }
64
- async signatureHelp(filePath, position) {
65
- return this.runWorker('signatureHelp', filePath, { position });
66
- }
67
- async rename(filePath, position, newName) {
68
- return this.runWorker('rename', filePath, { position, newName });
69
- }
70
- async getDiagnostics(filePath) {
71
- return this.runWorker('diagnostics', filePath, {});
72
- }
73
- }
74
- let client = null;
75
- export function getLspClient() {
76
- if (!client) {
77
- client = new LspClient();
78
- }
79
- return client;
80
- }
@@ -1,148 +0,0 @@
1
- #!/usr/bin/env node
2
- // Standalone worker script that communicates with pyright-langserver
3
- // Uses vscode-jsonrpc for reliable LSP communication
4
-
5
- import { spawn } from 'child_process';
6
- import * as rpc from 'vscode-jsonrpc/node.js';
7
- import * as fs from 'fs';
8
-
9
- const args = JSON.parse(process.argv[2]);
10
- const { workspaceRoot, method, filePath, position, newName, includeDeclaration } = args;
11
-
12
- function log(msg) {
13
- console.error(`[worker] ${msg}`);
14
- }
15
-
16
- function pathToUri(p) {
17
- return p.startsWith('file://') ? p : `file://${p}`;
18
- }
19
-
20
- // Create process and connection at module level like the working test script
21
- const proc = spawn('pyright-langserver', ['--stdio'], {
22
- stdio: ['pipe', 'pipe', 'pipe'],
23
- });
24
-
25
- proc.stderr.on('data', (data) => {
26
- log(`stderr: ${data.toString().trim()}`);
27
- });
28
-
29
- const connection = rpc.createMessageConnection(
30
- new rpc.StreamMessageReader(proc.stdout),
31
- new rpc.StreamMessageWriter(proc.stdin)
32
- );
33
-
34
- connection.onError((err) => log(`Connection error: ${err}`));
35
- connection.onClose(() => log('Connection closed'));
36
- connection.listen();
37
-
38
- async function main() {
39
- try {
40
- // Initialize with simpler capabilities like the working test
41
- log('Sending initialize...');
42
- await connection.sendRequest('initialize', {
43
- processId: process.pid,
44
- rootUri: pathToUri(workspaceRoot),
45
- capabilities: {
46
- textDocument: {
47
- hover: { contentFormat: ['markdown', 'plaintext'] },
48
- },
49
- },
50
- });
51
- log('Initialize done');
52
-
53
- await connection.sendNotification('initialized', {});
54
- log('Sent initialized notification');
55
-
56
- // Open document
57
- let uri = null;
58
- if (filePath) {
59
- const content = fs.readFileSync(filePath, 'utf-8');
60
- uri = pathToUri(filePath);
61
-
62
- log(`Opening document: ${uri}`);
63
- await connection.sendNotification('textDocument/didOpen', {
64
- textDocument: {
65
- uri,
66
- languageId: 'python',
67
- version: 1,
68
- text: content,
69
- },
70
- });
71
-
72
- // Wait for analysis
73
- log('Waiting for analysis...');
74
- await new Promise((r) => setTimeout(r, 2000));
75
- }
76
-
77
- let result;
78
-
79
- switch (method) {
80
- case 'hover':
81
- log(`Sending hover request at ${JSON.stringify(position)}`);
82
- result = await connection.sendRequest('textDocument/hover', {
83
- textDocument: { uri },
84
- position,
85
- });
86
- break;
87
-
88
- case 'definition':
89
- log('Sending definition request');
90
- result = await connection.sendRequest('textDocument/definition', {
91
- textDocument: { uri },
92
- position,
93
- });
94
- break;
95
-
96
- case 'references':
97
- log('Sending references request');
98
- result = await connection.sendRequest('textDocument/references', {
99
- textDocument: { uri },
100
- position,
101
- context: { includeDeclaration: includeDeclaration !== false },
102
- });
103
- break;
104
-
105
- case 'completions':
106
- log('Sending completions request');
107
- result = await connection.sendRequest('textDocument/completion', {
108
- textDocument: { uri },
109
- position,
110
- });
111
- break;
112
-
113
- case 'signatureHelp':
114
- log('Sending signatureHelp request');
115
- result = await connection.sendRequest('textDocument/signatureHelp', {
116
- textDocument: { uri },
117
- position,
118
- });
119
- break;
120
-
121
- case 'rename':
122
- log('Sending rename request');
123
- result = await connection.sendRequest('textDocument/rename', {
124
- textDocument: { uri },
125
- position,
126
- newName,
127
- });
128
- break;
129
-
130
- case 'diagnostics':
131
- result = [];
132
- break;
133
-
134
- default:
135
- throw new Error(`Unknown method: ${method}`);
136
- }
137
-
138
- console.log(JSON.stringify({ success: true, result }));
139
- } catch (error) {
140
- console.log(JSON.stringify({ success: false, error: error.message }));
141
- } finally {
142
- connection.dispose();
143
- proc.kill();
144
- process.exit(0);
145
- }
146
- }
147
-
148
- main();
@@ -1,71 +0,0 @@
1
- import { z } from 'zod';
2
- import { getLspClient } from '../lsp-client.js';
3
- import { toPosition } from '../utils/position.js';
4
- import { CompletionItemKind } from 'vscode-languageserver-protocol';
5
- export const completionsSchema = {
6
- file: z.string().describe('Absolute path to the Python file'),
7
- line: z.number().int().positive().describe('Line number (1-based)'),
8
- column: z.number().int().positive().describe('Column number (1-based)'),
9
- limit: z.number().int().positive().optional().default(20).describe('Maximum number of completions to return'),
10
- };
11
- const kindNames = {
12
- [CompletionItemKind.Text]: 'Text',
13
- [CompletionItemKind.Method]: 'Method',
14
- [CompletionItemKind.Function]: 'Function',
15
- [CompletionItemKind.Constructor]: 'Constructor',
16
- [CompletionItemKind.Field]: 'Field',
17
- [CompletionItemKind.Variable]: 'Variable',
18
- [CompletionItemKind.Class]: 'Class',
19
- [CompletionItemKind.Interface]: 'Interface',
20
- [CompletionItemKind.Module]: 'Module',
21
- [CompletionItemKind.Property]: 'Property',
22
- [CompletionItemKind.Unit]: 'Unit',
23
- [CompletionItemKind.Value]: 'Value',
24
- [CompletionItemKind.Enum]: 'Enum',
25
- [CompletionItemKind.Keyword]: 'Keyword',
26
- [CompletionItemKind.Snippet]: 'Snippet',
27
- [CompletionItemKind.Color]: 'Color',
28
- [CompletionItemKind.File]: 'File',
29
- [CompletionItemKind.Reference]: 'Reference',
30
- [CompletionItemKind.Folder]: 'Folder',
31
- [CompletionItemKind.EnumMember]: 'EnumMember',
32
- [CompletionItemKind.Constant]: 'Constant',
33
- [CompletionItemKind.Struct]: 'Struct',
34
- [CompletionItemKind.Event]: 'Event',
35
- [CompletionItemKind.Operator]: 'Operator',
36
- [CompletionItemKind.TypeParameter]: 'TypeParameter',
37
- };
38
- export async function completions(args) {
39
- const client = getLspClient();
40
- const position = toPosition(args.line, args.column);
41
- const limit = args.limit ?? 20;
42
- const result = await client.completions(args.file, position);
43
- if (!result) {
44
- return {
45
- content: [{ type: 'text', text: 'No completions available at this position.' }],
46
- };
47
- }
48
- let items;
49
- if (Array.isArray(result)) {
50
- items = result;
51
- }
52
- else {
53
- items = result.items;
54
- }
55
- if (items.length === 0) {
56
- return {
57
- content: [{ type: 'text', text: 'No completions available at this position.' }],
58
- };
59
- }
60
- const limitedItems = items.slice(0, limit);
61
- let output = `**Completions** at ${args.file}:${args.line}:${args.column}\n\n`;
62
- output += `Showing ${limitedItems.length} of ${items.length} completion(s):\n\n`;
63
- for (const item of limitedItems) {
64
- const kind = item.kind ? kindNames[item.kind] || 'Unknown' : 'Unknown';
65
- const detail = item.detail ? ` - ${item.detail}` : '';
66
- output += `- **${item.label}** (${kind})${detail}\n`;
67
- }
68
- return {
69
- content: [{ type: 'text', text: output }],
70
- };
71
- }
@@ -1,65 +0,0 @@
1
- import { z } from 'zod';
2
- import { getLspClient } from '../lsp-client.js';
3
- import { toPosition, fromPosition, uriToPath } from '../utils/position.js';
4
- export const definitionSchema = {
5
- file: z.string().describe('Absolute path to the Python file'),
6
- line: z.number().int().positive().describe('Line number (1-based)'),
7
- column: z.number().int().positive().describe('Column number (1-based)'),
8
- };
9
- export async function definition(args) {
10
- const client = getLspClient();
11
- const position = toPosition(args.line, args.column);
12
- const result = await client.definition(args.file, position);
13
- if (!result) {
14
- return {
15
- content: [{ type: 'text', text: 'No definition found at this position.' }],
16
- };
17
- }
18
- const locations = [];
19
- if (Array.isArray(result)) {
20
- for (const item of result) {
21
- if ('targetUri' in item) {
22
- // LocationLink
23
- const link = item;
24
- const pos = fromPosition(link.targetSelectionRange.start);
25
- locations.push({
26
- file: uriToPath(link.targetUri),
27
- line: pos.line,
28
- column: pos.column,
29
- });
30
- }
31
- else {
32
- // Location
33
- const loc = item;
34
- const pos = fromPosition(loc.range.start);
35
- locations.push({
36
- file: uriToPath(loc.uri),
37
- line: pos.line,
38
- column: pos.column,
39
- });
40
- }
41
- }
42
- }
43
- else {
44
- // Single Location
45
- const loc = result;
46
- const pos = fromPosition(loc.range.start);
47
- locations.push({
48
- file: uriToPath(loc.uri),
49
- line: pos.line,
50
- column: pos.column,
51
- });
52
- }
53
- if (locations.length === 0) {
54
- return {
55
- content: [{ type: 'text', text: 'No definition found at this position.' }],
56
- };
57
- }
58
- let output = `**Definition(s)** for symbol at ${args.file}:${args.line}:${args.column}\n\n`;
59
- for (const loc of locations) {
60
- output += `- ${loc.file}:${loc.line}:${loc.column}\n`;
61
- }
62
- return {
63
- content: [{ type: 'text', text: output }],
64
- };
65
- }
@@ -1,93 +0,0 @@
1
- import { z } from 'zod';
2
- import { execSync } from 'child_process';
3
- import { findProjectRoot } from '../utils/position.js';
4
- export const diagnosticsSchema = {
5
- path: z
6
- .string()
7
- .describe('Path to a Python file or directory to check'),
8
- };
9
- export async function diagnostics(args) {
10
- const { path } = args;
11
- // Determine project root from path
12
- const projectRoot = findProjectRoot(path);
13
- // Build pyright command - path can be file or directory
14
- const target = path;
15
- const cmd = `pyright "${target}" --outputjson`;
16
- let output;
17
- try {
18
- const result = execSync(cmd, {
19
- encoding: 'utf-8',
20
- cwd: projectRoot,
21
- timeout: 60000,
22
- maxBuffer: 10 * 1024 * 1024,
23
- });
24
- output = JSON.parse(result);
25
- }
26
- catch (e) {
27
- const error = e;
28
- // pyright returns non-zero exit code if there are errors
29
- if (error.stdout) {
30
- try {
31
- output = JSON.parse(error.stdout);
32
- }
33
- catch {
34
- return {
35
- content: [
36
- {
37
- type: 'text',
38
- text: `Error running pyright: ${error.message || 'Unknown error'}`,
39
- },
40
- ],
41
- };
42
- }
43
- }
44
- else {
45
- return {
46
- content: [
47
- {
48
- type: 'text',
49
- text: `Error running pyright: ${error.message || 'Unknown error'}\n\nMake sure pyright is installed: npm install -g pyright`,
50
- },
51
- ],
52
- };
53
- }
54
- }
55
- const diags = output.generalDiagnostics || [];
56
- const summary = output.summary;
57
- if (diags.length === 0) {
58
- let text = `**No issues found**\n\n`;
59
- text += `- Files analyzed: ${summary.filesAnalyzed}\n`;
60
- text += `- Time: ${summary.timeInSec}s`;
61
- return {
62
- content: [{ type: 'text', text }],
63
- };
64
- }
65
- // Group by file
66
- const byFile = new Map();
67
- for (const diag of diags) {
68
- const list = byFile.get(diag.file) || [];
69
- list.push(diag);
70
- byFile.set(diag.file, list);
71
- }
72
- let text = `**Diagnostics Summary**\n\n`;
73
- text += `- Errors: ${summary.errorCount}\n`;
74
- text += `- Warnings: ${summary.warningCount}\n`;
75
- text += `- Information: ${summary.informationCount}\n`;
76
- text += `- Files analyzed: ${summary.filesAnalyzed}\n`;
77
- text += `- Time: ${summary.timeInSec}s\n\n`;
78
- text += `---\n\n`;
79
- for (const [filePath, fileDiags] of byFile) {
80
- text += `### ${filePath}\n\n`;
81
- for (const diag of fileDiags) {
82
- const line = diag.range.start.line + 1;
83
- const col = diag.range.start.character + 1;
84
- const rule = diag.rule ? ` (${diag.rule})` : '';
85
- const icon = diag.severity === 'error' ? '❌' : diag.severity === 'warning' ? '⚠️' : 'ℹ️';
86
- text += `- ${icon} **${diag.severity}** at ${line}:${col}${rule}\n`;
87
- text += ` ${diag.message}\n\n`;
88
- }
89
- }
90
- return {
91
- content: [{ type: 'text', text }],
92
- };
93
- }
@@ -1,44 +0,0 @@
1
- import { z } from 'zod';
2
- import { getLspClient } from '../lsp-client.js';
3
- import { toPosition, formatRange } from '../utils/position.js';
4
- export const hoverSchema = {
5
- file: z.string().describe('Absolute path to the Python file'),
6
- line: z.number().int().positive().describe('Line number (1-based)'),
7
- column: z.number().int().positive().describe('Column number (1-based)'),
8
- };
9
- export async function hover(args) {
10
- console.error(`[hover] Starting hover for ${args.file}:${args.line}:${args.column}`);
11
- const client = getLspClient();
12
- const position = toPosition(args.line, args.column);
13
- console.error(`[hover] Calling LSP hover...`);
14
- const result = await client.hover(args.file, position);
15
- console.error(`[hover] Got result: ${result ? 'yes' : 'no'}`);
16
- if (!result) {
17
- return {
18
- content: [{ type: 'text', text: 'No hover information available at this position.' }],
19
- };
20
- }
21
- let hoverText = '';
22
- if (typeof result.contents === 'string') {
23
- hoverText = result.contents;
24
- }
25
- else if (Array.isArray(result.contents)) {
26
- hoverText = result.contents
27
- .map((c) => (typeof c === 'string' ? c : c.value))
28
- .join('\n\n');
29
- }
30
- else if ('kind' in result.contents) {
31
- hoverText = result.contents.value;
32
- }
33
- else if ('value' in result.contents) {
34
- hoverText = result.contents.value;
35
- }
36
- let output = `**Hover Info** at ${args.file}:${args.line}:${args.column}\n\n`;
37
- output += hoverText;
38
- if (result.range) {
39
- output += `\n\n**Range:** ${formatRange(result.range)}`;
40
- }
41
- return {
42
- content: [{ type: 'text', text: output }],
43
- };
44
- }
@@ -1,162 +0,0 @@
1
- import { z } from 'zod';
2
- import { execSync } from 'child_process';
3
- import { readFileSync } from 'fs';
4
- import { findProjectRoot } from '../utils/position.js';
5
- export const referencesSchema = {
6
- file: z.string().describe('Absolute path to the Python file'),
7
- line: z.number().int().positive().describe('Line number (1-based)'),
8
- column: z.number().int().positive().describe('Column number (1-based)'),
9
- };
10
- function getSymbolAtPosition(filePath, line, column) {
11
- try {
12
- const content = readFileSync(filePath, 'utf-8');
13
- const lines = content.split('\n');
14
- const targetLine = lines[line - 1];
15
- if (!targetLine)
16
- return null;
17
- // Find word at position
18
- const col = column - 1;
19
- let start = col;
20
- let end = col;
21
- // Expand left
22
- while (start > 0 && /[\w_]/.test(targetLine[start - 1])) {
23
- start--;
24
- }
25
- // Expand right
26
- while (end < targetLine.length && /[\w_]/.test(targetLine[end])) {
27
- end++;
28
- }
29
- const symbol = targetLine.slice(start, end);
30
- return symbol.length > 0 ? symbol : null;
31
- }
32
- catch {
33
- return null;
34
- }
35
- }
36
- export async function references(args) {
37
- const { file, line, column } = args;
38
- // Get symbol name at position
39
- const symbol = getSymbolAtPosition(file, line, column);
40
- if (!symbol) {
41
- return {
42
- content: [{ type: 'text', text: 'Could not identify symbol at this position.' }],
43
- };
44
- }
45
- // Find project root
46
- const projectRoot = findProjectRoot(file);
47
- // Use ripgrep to find all references in Python files
48
- // Match word boundaries to avoid partial matches
49
- const pattern = `\\b${symbol}\\b`;
50
- let rgOutput;
51
- try {
52
- // Try ripgrep first
53
- rgOutput = execSync(`rg --no-heading --line-number --column --type py "${pattern}" "${projectRoot}"`, {
54
- encoding: 'utf-8',
55
- maxBuffer: 10 * 1024 * 1024,
56
- timeout: 30000,
57
- });
58
- }
59
- catch (e) {
60
- const error = e;
61
- if (error.status === 1) {
62
- // No matches found
63
- return {
64
- content: [
65
- {
66
- type: 'text',
67
- text: `No references found for symbol \`${symbol}\``,
68
- },
69
- ],
70
- };
71
- }
72
- // Try grep as fallback
73
- try {
74
- rgOutput = execSync(`grep -rn --include="*.py" -w "${symbol}" "${projectRoot}"`, {
75
- encoding: 'utf-8',
76
- maxBuffer: 10 * 1024 * 1024,
77
- timeout: 30000,
78
- });
79
- }
80
- catch {
81
- return {
82
- content: [
83
- {
84
- type: 'text',
85
- text: `No references found for symbol \`${symbol}\``,
86
- },
87
- ],
88
- };
89
- }
90
- }
91
- // Parse ripgrep output: file:line:column:text
92
- const refs = [];
93
- const lines = rgOutput.trim().split('\n').filter(Boolean);
94
- for (const outputLine of lines) {
95
- // Format: /path/to/file.py:10:5: some code here
96
- const match = outputLine.match(/^(.+?):(\d+):(\d+):(.*)$/);
97
- if (match) {
98
- const [, filePath, lineNum, colNum, text] = match;
99
- refs.push({
100
- file: filePath,
101
- line: parseInt(lineNum, 10),
102
- column: parseInt(colNum, 10),
103
- text: text.trim(),
104
- isDefinition: filePath === file && parseInt(lineNum, 10) === line,
105
- });
106
- }
107
- else {
108
- // Fallback for grep format: /path/to/file.py:10: some code here
109
- const grepMatch = outputLine.match(/^(.+?):(\d+):(.*)$/);
110
- if (grepMatch) {
111
- const [, filePath, lineNum, text] = grepMatch;
112
- refs.push({
113
- file: filePath,
114
- line: parseInt(lineNum, 10),
115
- column: 1,
116
- text: text.trim(),
117
- isDefinition: filePath === file && parseInt(lineNum, 10) === line,
118
- });
119
- }
120
- }
121
- }
122
- if (refs.length === 0) {
123
- return {
124
- content: [
125
- {
126
- type: 'text',
127
- text: `No references found for symbol \`${symbol}\``,
128
- },
129
- ],
130
- };
131
- }
132
- // Sort: definition first, then by file and line
133
- refs.sort((a, b) => {
134
- if (a.isDefinition && !b.isDefinition)
135
- return -1;
136
- if (!a.isDefinition && b.isDefinition)
137
- return 1;
138
- if (a.file !== b.file)
139
- return a.file.localeCompare(b.file);
140
- return a.line - b.line;
141
- });
142
- // Group by file
143
- const byFile = new Map();
144
- for (const ref of refs) {
145
- const list = byFile.get(ref.file) || [];
146
- list.push(ref);
147
- byFile.set(ref.file, list);
148
- }
149
- let output = `**References** for \`${symbol}\`\n\n`;
150
- output += `Found ${refs.length} reference(s) in ${byFile.size} file(s):\n\n`;
151
- for (const [filePath, fileRefs] of byFile) {
152
- output += `### ${filePath}\n\n`;
153
- for (const ref of fileRefs) {
154
- const marker = ref.isDefinition ? ' (definition)' : '';
155
- output += `- **${ref.line}:${ref.column}**${marker}: \`${ref.text}\`\n`;
156
- }
157
- output += '\n';
158
- }
159
- return {
160
- content: [{ type: 'text', text: output }],
161
- };
162
- }