@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.
@@ -1,153 +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 renameSchema = {
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
- newName: z.string().describe('New name for the symbol'),
10
- };
11
- function getSymbolAtPosition(filePath, line, column) {
12
- try {
13
- const content = readFileSync(filePath, 'utf-8');
14
- const lines = content.split('\n');
15
- const targetLine = lines[line - 1];
16
- if (!targetLine)
17
- return null;
18
- // Find word at position
19
- const col = column - 1;
20
- let start = col;
21
- let end = col;
22
- // Expand left
23
- while (start > 0 && /[\w_]/.test(targetLine[start - 1])) {
24
- start--;
25
- }
26
- // Expand right
27
- while (end < targetLine.length && /[\w_]/.test(targetLine[end])) {
28
- end++;
29
- }
30
- const symbol = targetLine.slice(start, end);
31
- return symbol.length > 0 ? { symbol, start: start + 1, end: end + 1 } : null;
32
- }
33
- catch {
34
- return null;
35
- }
36
- }
37
- export async function rename(args) {
38
- const { file, line, column, newName } = args;
39
- // Get symbol name at position
40
- const symbolInfo = getSymbolAtPosition(file, line, column);
41
- if (!symbolInfo) {
42
- return {
43
- content: [{ type: 'text', text: 'Could not identify symbol at this position.' }],
44
- };
45
- }
46
- const { symbol: oldName } = symbolInfo;
47
- if (oldName === newName) {
48
- return {
49
- content: [{ type: 'text', text: 'New name is the same as the old name.' }],
50
- };
51
- }
52
- // Find project root
53
- const projectRoot = findProjectRoot(file);
54
- // Use ripgrep to find all references in Python files
55
- const pattern = `\\b${oldName}\\b`;
56
- let rgOutput;
57
- try {
58
- rgOutput = execSync(`rg --no-heading --line-number --column --type py "${pattern}" "${projectRoot}"`, {
59
- encoding: 'utf-8',
60
- maxBuffer: 10 * 1024 * 1024,
61
- timeout: 30000,
62
- });
63
- }
64
- catch (e) {
65
- const error = e;
66
- if (error.status === 1) {
67
- return {
68
- content: [
69
- {
70
- type: 'text',
71
- text: `No references found for symbol \`${oldName}\``,
72
- },
73
- ],
74
- };
75
- }
76
- // Try grep as fallback
77
- try {
78
- rgOutput = execSync(`grep -rn --include="*.py" -w "${oldName}" "${projectRoot}"`, {
79
- encoding: 'utf-8',
80
- maxBuffer: 10 * 1024 * 1024,
81
- timeout: 30000,
82
- });
83
- }
84
- catch {
85
- return {
86
- content: [
87
- {
88
- type: 'text',
89
- text: `No references found for symbol \`${oldName}\``,
90
- },
91
- ],
92
- };
93
- }
94
- }
95
- // Parse output and create edits
96
- const edits = [];
97
- const outputLines = rgOutput.trim().split('\n').filter(Boolean);
98
- for (const outputLine of outputLines) {
99
- // Format: /path/to/file.py:10:5: some code here
100
- const match = outputLine.match(/^(.+?):(\d+):(\d+):(.*)$/);
101
- if (match) {
102
- const [, filePath, lineNum, colNum, lineContent] = match;
103
- const col = parseInt(colNum, 10);
104
- edits.push({
105
- file: filePath,
106
- line: parseInt(lineNum, 10),
107
- column: col,
108
- endColumn: col + oldName.length,
109
- oldText: oldName,
110
- newText: newName,
111
- lineContent: lineContent.trim(),
112
- });
113
- }
114
- }
115
- if (edits.length === 0) {
116
- return {
117
- content: [
118
- {
119
- type: 'text',
120
- text: `No references found for symbol \`${oldName}\``,
121
- },
122
- ],
123
- };
124
- }
125
- // Group by file
126
- const byFile = new Map();
127
- for (const edit of edits) {
128
- const list = byFile.get(edit.file) || [];
129
- list.push(edit);
130
- byFile.set(edit.file, list);
131
- }
132
- let output = `**Rename Preview**\n\n`;
133
- output += `- Symbol: \`${oldName}\` → \`${newName}\`\n`;
134
- output += `- Found ${edits.length} occurrence(s) in ${byFile.size} file(s)\n\n`;
135
- output += `---\n\n`;
136
- for (const [filePath, fileEdits] of byFile) {
137
- output += `### ${filePath}\n\n`;
138
- for (const edit of fileEdits) {
139
- const preview = edit.lineContent.replace(new RegExp(`\\b${oldName}\\b`), `~~${oldName}~~ **${newName}**`);
140
- output += `- Line ${edit.line}: ${preview}\n`;
141
- }
142
- output += '\n';
143
- }
144
- output += `---\n\n`;
145
- output += `**Note:** This is a preview only. To apply the rename, use your editor's rename feature or run:\n`;
146
- output += `\`\`\`bash\n`;
147
- output += `# Using sed (backup recommended)\n`;
148
- output += `find "${projectRoot}" -name "*.py" -exec sed -i '' 's/\\b${oldName}\\b/${newName}/g' {} +\n`;
149
- output += `\`\`\``;
150
- return {
151
- content: [{ type: 'text', text: output }],
152
- };
153
- }
@@ -1,91 +0,0 @@
1
- import { z } from 'zod';
2
- import { execSync } from 'child_process';
3
- import * as path from 'path';
4
- export const searchSchema = {
5
- pattern: z.string().describe('The regex pattern to search for'),
6
- path: z.string().optional().describe('Directory or file to search in (defaults to current working directory)'),
7
- glob: z.string().optional().describe('Glob pattern to filter files (e.g., "*.py", "**/*.ts")'),
8
- caseSensitive: z.boolean().optional().default(true).describe('Whether the search is case sensitive'),
9
- maxResults: z.number().int().positive().optional().default(50).describe('Maximum number of results to return'),
10
- };
11
- export async function search(args) {
12
- const searchPath = args.path || process.cwd();
13
- const caseSensitive = args.caseSensitive ?? true;
14
- const maxResults = args.maxResults ?? 50;
15
- // Build rg command
16
- const rgArgs = [
17
- '--json',
18
- '--line-number',
19
- '--column',
20
- ];
21
- if (!caseSensitive) {
22
- rgArgs.push('--ignore-case');
23
- }
24
- if (args.glob) {
25
- rgArgs.push('--glob', args.glob);
26
- }
27
- rgArgs.push('--', args.pattern, searchPath);
28
- try {
29
- const result = execSync(`rg ${rgArgs.map(a => `'${a}'`).join(' ')}`, {
30
- encoding: 'utf-8',
31
- maxBuffer: 10 * 1024 * 1024, // 10MB
32
- });
33
- const results = [];
34
- const lines = result.split('\n').filter(Boolean);
35
- for (const line of lines) {
36
- if (results.length >= maxResults)
37
- break;
38
- try {
39
- const json = JSON.parse(line);
40
- if (json.type === 'match') {
41
- const data = json.data;
42
- const filePath = data.path.text;
43
- const lineNumber = data.line_number;
44
- const lineText = data.lines.text.trimEnd();
45
- // Get all matches in this line
46
- for (const submatch of data.submatches) {
47
- if (results.length >= maxResults)
48
- break;
49
- results.push({
50
- file: path.resolve(filePath),
51
- line: lineNumber,
52
- column: submatch.start + 1, // Convert to 1-based
53
- text: lineText,
54
- match: submatch.match.text,
55
- });
56
- }
57
- }
58
- }
59
- catch {
60
- // Skip non-JSON lines
61
- }
62
- }
63
- if (results.length === 0) {
64
- return {
65
- content: [{ type: 'text', text: `No matches found for pattern: ${args.pattern}` }],
66
- };
67
- }
68
- let output = `**Search Results** for \`${args.pattern}\`\n\n`;
69
- output += `Found ${results.length} match(es)${results.length >= maxResults ? ` (limited to ${maxResults})` : ''}:\n\n`;
70
- for (const r of results) {
71
- output += `**${r.file}:${r.line}:${r.column}**\n`;
72
- output += ` \`${r.text}\`\n`;
73
- output += ` Match: \`${r.match}\`\n\n`;
74
- }
75
- return {
76
- content: [{ type: 'text', text: output }],
77
- };
78
- }
79
- catch (err) {
80
- const error = err;
81
- if (error.status === 1) {
82
- // rg returns 1 when no matches found
83
- return {
84
- content: [{ type: 'text', text: `No matches found for pattern: ${args.pattern}` }],
85
- };
86
- }
87
- return {
88
- content: [{ type: 'text', text: `Search error: ${error.message || 'Unknown error'}` }],
89
- };
90
- }
91
- }
@@ -1,54 +0,0 @@
1
- import { z } from 'zod';
2
- import { getLspClient } from '../lsp-client.js';
3
- import { toPosition } from '../utils/position.js';
4
- export const signatureHelpSchema = {
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 signatureHelp(args) {
10
- const client = getLspClient();
11
- const position = toPosition(args.line, args.column);
12
- const result = await client.signatureHelp(args.file, position);
13
- if (!result || result.signatures.length === 0) {
14
- return {
15
- content: [{ type: 'text', text: 'No signature help available at this position.' }],
16
- };
17
- }
18
- let output = `**Signature Help** at ${args.file}:${args.line}:${args.column}\n\n`;
19
- const activeIndex = result.activeSignature ?? 0;
20
- const activeParam = result.activeParameter ?? 0;
21
- for (let i = 0; i < result.signatures.length; i++) {
22
- const sig = result.signatures[i];
23
- const isActive = i === activeIndex;
24
- output += `${isActive ? '→ ' : ' '}**${sig.label}**\n`;
25
- if (sig.documentation) {
26
- const doc = typeof sig.documentation === 'string'
27
- ? sig.documentation
28
- : sig.documentation.value;
29
- output += ` ${doc}\n`;
30
- }
31
- if (sig.parameters && sig.parameters.length > 0) {
32
- output += `\n Parameters:\n`;
33
- for (let j = 0; j < sig.parameters.length; j++) {
34
- const param = sig.parameters[j];
35
- const isActiveParam = isActive && j === activeParam;
36
- const label = typeof param.label === 'string'
37
- ? param.label
38
- : sig.label.slice(param.label[0], param.label[1]);
39
- output += ` ${isActiveParam ? '→ ' : ' '}${label}`;
40
- if (param.documentation) {
41
- const paramDoc = typeof param.documentation === 'string'
42
- ? param.documentation
43
- : param.documentation.value;
44
- output += ` - ${paramDoc}`;
45
- }
46
- output += '\n';
47
- }
48
- }
49
- output += '\n';
50
- }
51
- return {
52
- content: [{ type: 'text', text: output }],
53
- };
54
- }
@@ -1,147 +0,0 @@
1
- import { z } from 'zod';
2
- import { execSync } from 'child_process';
3
- import { existsSync, readFileSync } from 'fs';
4
- import { join } from 'path';
5
- import { findProjectRoot } from '../utils/position.js';
6
- export const statusSchema = {
7
- file: z.string().describe('A Python file path to check the project status for'),
8
- };
9
- export async function status(args) {
10
- const { file } = args;
11
- const lines = [];
12
- // Find project root
13
- const projectRoot = findProjectRoot(file);
14
- lines.push(`## Project Root`);
15
- lines.push(`\`${projectRoot}\``);
16
- lines.push('');
17
- // Check pyright installation
18
- lines.push(`## Pyright`);
19
- try {
20
- const pyrightVersion = execSync('pyright --version', { encoding: 'utf-8' }).trim();
21
- lines.push(`- Version: ${pyrightVersion}`);
22
- }
23
- catch {
24
- lines.push(`- ⚠️ **Not installed or not in PATH**`);
25
- lines.push(` Install with: \`npm install -g pyright\``);
26
- }
27
- lines.push('');
28
- // Check pyright config
29
- lines.push(`## Pyright Config`);
30
- const pyrightConfigPath = join(projectRoot, 'pyrightconfig.json');
31
- const pyprojectPath = join(projectRoot, 'pyproject.toml');
32
- if (existsSync(pyrightConfigPath)) {
33
- lines.push(`- Config file: \`pyrightconfig.json\``);
34
- try {
35
- const config = JSON.parse(readFileSync(pyrightConfigPath, 'utf-8'));
36
- if (config.pythonVersion) {
37
- lines.push(`- Python version: ${config.pythonVersion}`);
38
- }
39
- if (config.pythonPlatform) {
40
- lines.push(`- Platform: ${config.pythonPlatform}`);
41
- }
42
- if (config.venvPath) {
43
- lines.push(`- Venv path: ${config.venvPath}`);
44
- }
45
- if (config.venv) {
46
- lines.push(`- Venv: ${config.venv}`);
47
- }
48
- if (config.typeCheckingMode) {
49
- lines.push(`- Type checking mode: ${config.typeCheckingMode}`);
50
- }
51
- if (config.include) {
52
- lines.push(`- Include: ${JSON.stringify(config.include)}`);
53
- }
54
- if (config.exclude) {
55
- lines.push(`- Exclude: ${JSON.stringify(config.exclude)}`);
56
- }
57
- }
58
- catch (e) {
59
- lines.push(`- ⚠️ Failed to parse config: ${e}`);
60
- }
61
- }
62
- else if (existsSync(pyprojectPath)) {
63
- lines.push(`- Config file: \`pyproject.toml\` (may contain [tool.pyright] section)`);
64
- }
65
- else {
66
- lines.push(`- ⚠️ No pyrightconfig.json or pyproject.toml found`);
67
- lines.push(` Pyright will use default settings`);
68
- }
69
- lines.push('');
70
- // Check Python environment
71
- lines.push(`## Python Environment`);
72
- try {
73
- const pythonVersion = execSync('python3 --version', { encoding: 'utf-8' }).trim();
74
- lines.push(`- System Python: ${pythonVersion}`);
75
- }
76
- catch {
77
- try {
78
- const pythonVersion = execSync('python --version', { encoding: 'utf-8' }).trim();
79
- lines.push(`- System Python: ${pythonVersion}`);
80
- }
81
- catch {
82
- lines.push(`- ⚠️ Python not found in PATH`);
83
- }
84
- }
85
- // Check for virtual environment
86
- const venvPaths = ['.venv', 'venv', '.env', 'env'];
87
- for (const venv of venvPaths) {
88
- const venvPath = join(projectRoot, venv);
89
- if (existsSync(venvPath)) {
90
- lines.push(`- Virtual env found: \`${venv}/\``);
91
- // Try to get venv python version
92
- const venvPython = join(venvPath, 'bin', 'python');
93
- if (existsSync(venvPython)) {
94
- try {
95
- const venvVersion = execSync(`"${venvPython}" --version`, { encoding: 'utf-8' }).trim();
96
- lines.push(` - ${venvVersion}`);
97
- }
98
- catch {
99
- // ignore
100
- }
101
- }
102
- break;
103
- }
104
- }
105
- lines.push('');
106
- // Quick pyright check on the file
107
- lines.push(`## File Check`);
108
- lines.push(`- File: \`${file}\``);
109
- if (existsSync(file)) {
110
- lines.push(`- Exists: ✅`);
111
- try {
112
- const result = execSync(`pyright "${file}" --outputjson`, {
113
- encoding: 'utf-8',
114
- cwd: projectRoot,
115
- timeout: 30000,
116
- });
117
- const output = JSON.parse(result);
118
- const errors = output.generalDiagnostics?.filter((d) => d.severity === 'error')?.length || 0;
119
- const warnings = output.generalDiagnostics?.filter((d) => d.severity === 'warning')?.length || 0;
120
- lines.push(`- Diagnostics: ${errors} errors, ${warnings} warnings`);
121
- }
122
- catch (e) {
123
- // pyright returns non-zero exit code if there are errors
124
- const error = e;
125
- if (error.stdout) {
126
- try {
127
- const output = JSON.parse(error.stdout);
128
- const errors = output.generalDiagnostics?.filter((d) => d.severity === 'error')?.length || 0;
129
- const warnings = output.generalDiagnostics?.filter((d) => d.severity === 'warning')?.length || 0;
130
- lines.push(`- Diagnostics: ${errors} errors, ${warnings} warnings`);
131
- }
132
- catch {
133
- lines.push(`- ⚠️ Could not run pyright check`);
134
- }
135
- }
136
- else {
137
- lines.push(`- ⚠️ Could not run pyright check`);
138
- }
139
- }
140
- }
141
- else {
142
- lines.push(`- Exists: ❌ File not found`);
143
- }
144
- return {
145
- content: [{ type: 'text', text: lines.join('\n') }],
146
- };
147
- }
@@ -1,74 +0,0 @@
1
- import { Position } from 'vscode-languageserver-protocol';
2
- import { existsSync } from 'fs';
3
- import { dirname, join, resolve } from 'path';
4
- /**
5
- * Convert 1-based line/column (user input) to 0-based LSP Position
6
- */
7
- export function toPosition(line, column) {
8
- return Position.create(line - 1, column - 1);
9
- }
10
- /**
11
- * Convert 0-based LSP Position to 1-based line/column (user output)
12
- */
13
- export function fromPosition(pos) {
14
- return {
15
- line: pos.line + 1,
16
- column: pos.character + 1,
17
- };
18
- }
19
- /**
20
- * Format a Location for display
21
- */
22
- export function formatLocation(loc) {
23
- const start = fromPosition(loc.range.start);
24
- const end = fromPosition(loc.range.end);
25
- return `${loc.uri}:${start.line}:${start.column}-${end.line}:${end.column}`;
26
- }
27
- /**
28
- * Format a Range for display
29
- */
30
- export function formatRange(range) {
31
- const start = fromPosition(range.start);
32
- const end = fromPosition(range.end);
33
- return `${start.line}:${start.column}-${end.line}:${end.column}`;
34
- }
35
- /**
36
- * Convert file path to URI
37
- */
38
- export function pathToUri(filePath) {
39
- if (filePath.startsWith('file://')) {
40
- return filePath;
41
- }
42
- return `file://${filePath}`;
43
- }
44
- /**
45
- * Convert URI to file path
46
- */
47
- export function uriToPath(uri) {
48
- if (uri.startsWith('file://')) {
49
- return uri.slice(7);
50
- }
51
- return uri;
52
- }
53
- /**
54
- * Find project root by looking for pyrightconfig.json or pyproject.toml
55
- * starting from the given file path and walking up the directory tree
56
- */
57
- export function findProjectRoot(filePath) {
58
- const configFiles = ['pyrightconfig.json', 'pyproject.toml', '.git'];
59
- let dir = dirname(resolve(filePath));
60
- const root = '/';
61
- while (dir !== root) {
62
- for (const configFile of configFiles) {
63
- if (existsSync(join(dir, configFile))) {
64
- return dir;
65
- }
66
- }
67
- const parent = dirname(dir);
68
- if (parent === dir)
69
- break;
70
- dir = parent;
71
- }
72
- // Fallback to file's directory
73
- return dirname(resolve(filePath));
74
- }