@treedy/lsp-mcp 0.1.7 → 0.1.8
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/bundled/pyright/dist/index.d.ts +2 -0
- package/dist/bundled/pyright/dist/index.js +1620 -0
- package/dist/bundled/pyright/dist/index.js.map +26 -0
- package/dist/bundled/pyright/dist/lsp/connection.d.ts +71 -0
- package/dist/bundled/pyright/dist/lsp/document-manager.d.ts +67 -0
- package/dist/bundled/pyright/dist/lsp/index.d.ts +3 -0
- package/dist/bundled/pyright/dist/lsp/types.d.ts +55 -0
- package/dist/bundled/pyright/dist/lsp-client.d.ts +55 -0
- package/dist/bundled/pyright/dist/tools/completions.d.ts +18 -0
- package/dist/bundled/pyright/dist/tools/definition.d.ts +16 -0
- package/dist/bundled/pyright/dist/tools/diagnostics.d.ts +12 -0
- package/dist/bundled/pyright/dist/tools/hover.d.ts +16 -0
- package/dist/bundled/pyright/dist/tools/references.d.ts +16 -0
- package/dist/bundled/pyright/dist/tools/rename.d.ts +18 -0
- package/dist/bundled/pyright/dist/tools/search.d.ts +20 -0
- package/dist/bundled/pyright/dist/tools/signature-help.d.ts +16 -0
- package/dist/bundled/pyright/dist/tools/status.d.ts +14 -0
- package/dist/bundled/pyright/dist/tools/symbols.d.ts +17 -0
- package/dist/bundled/pyright/dist/tools/update-document.d.ts +14 -0
- package/dist/bundled/pyright/dist/utils/position.d.ts +33 -0
- package/dist/bundled/pyright/package.json +54 -0
- package/dist/bundled/python/README.md +230 -0
- package/dist/bundled/python/pyproject.toml +61 -0
- package/dist/bundled/python/src/rope_mcp/__init__.py +3 -0
- package/dist/bundled/python/src/rope_mcp/__pycache__/__init__.cpython-312.pyc +0 -0
- package/dist/bundled/python/src/rope_mcp/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/bundled/python/src/rope_mcp/__pycache__/config.cpython-312.pyc +0 -0
- package/dist/bundled/python/src/rope_mcp/__pycache__/config.cpython-313.pyc +0 -0
- package/dist/bundled/python/src/rope_mcp/__pycache__/pyright_client.cpython-313.pyc +0 -0
- package/dist/bundled/python/src/rope_mcp/__pycache__/rope_client.cpython-313.pyc +0 -0
- package/dist/bundled/python/src/rope_mcp/__pycache__/server.cpython-312.pyc +0 -0
- package/dist/bundled/python/src/rope_mcp/__pycache__/server.cpython-313.pyc +0 -0
- package/dist/bundled/python/src/rope_mcp/config.py +408 -0
- package/dist/bundled/python/src/rope_mcp/lsp/__init__.py +15 -0
- package/dist/bundled/python/src/rope_mcp/lsp/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/bundled/python/src/rope_mcp/lsp/__pycache__/client.cpython-313.pyc +0 -0
- package/dist/bundled/python/src/rope_mcp/lsp/__pycache__/types.cpython-313.pyc +0 -0
- package/dist/bundled/python/src/rope_mcp/lsp/client.py +624 -0
- package/dist/bundled/python/src/rope_mcp/lsp/types.py +82 -0
- package/dist/bundled/python/src/rope_mcp/pyright_client.py +147 -0
- package/dist/bundled/python/src/rope_mcp/rope_client.py +198 -0
- package/dist/bundled/python/src/rope_mcp/server.py +963 -0
- package/dist/bundled/python/src/rope_mcp/tools/__init__.py +26 -0
- package/dist/bundled/python/src/rope_mcp/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/bundled/python/src/rope_mcp/tools/__pycache__/change_signature.cpython-313.pyc +0 -0
- package/dist/bundled/python/src/rope_mcp/tools/__pycache__/completions.cpython-313.pyc +0 -0
- package/dist/bundled/python/src/rope_mcp/tools/__pycache__/definition.cpython-313.pyc +0 -0
- package/dist/bundled/python/src/rope_mcp/tools/__pycache__/diagnostics.cpython-313.pyc +0 -0
- package/dist/bundled/python/src/rope_mcp/tools/__pycache__/hover.cpython-313.pyc +0 -0
- package/dist/bundled/python/src/rope_mcp/tools/__pycache__/move.cpython-313.pyc +0 -0
- package/dist/bundled/python/src/rope_mcp/tools/__pycache__/references.cpython-313.pyc +0 -0
- package/dist/bundled/python/src/rope_mcp/tools/__pycache__/rename.cpython-313.pyc +0 -0
- package/dist/bundled/python/src/rope_mcp/tools/__pycache__/search.cpython-313.pyc +0 -0
- package/dist/bundled/python/src/rope_mcp/tools/__pycache__/symbols.cpython-313.pyc +0 -0
- package/dist/bundled/python/src/rope_mcp/tools/change_signature.py +184 -0
- package/dist/bundled/python/src/rope_mcp/tools/completions.py +84 -0
- package/dist/bundled/python/src/rope_mcp/tools/definition.py +51 -0
- package/dist/bundled/python/src/rope_mcp/tools/diagnostics.py +18 -0
- package/dist/bundled/python/src/rope_mcp/tools/hover.py +49 -0
- package/dist/bundled/python/src/rope_mcp/tools/move.py +81 -0
- package/dist/bundled/python/src/rope_mcp/tools/references.py +60 -0
- package/dist/bundled/python/src/rope_mcp/tools/rename.py +61 -0
- package/dist/bundled/python/src/rope_mcp/tools/search.py +128 -0
- package/dist/bundled/python/src/rope_mcp/tools/symbols.py +118 -0
- package/dist/bundled/python/uv.lock +979 -0
- package/dist/bundled/typescript/dist/index.js +29534 -0
- package/dist/bundled/typescript/dist/index.js.map +211 -0
- package/dist/bundled/typescript/package.json +46 -0
- package/dist/bundled/vue/dist/index.d.ts +8 -0
- package/dist/bundled/vue/dist/index.js +21111 -0
- package/dist/bundled/vue/dist/ts-vue-service.d.ts +67 -0
- package/dist/bundled/vue/dist/vue-service.d.ts +144 -0
- package/dist/bundled/vue/package.json +45 -0
- package/dist/index.js +148 -58
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Pyright client wrapper for running Pyright CLI commands."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PyrightClient:
|
|
10
|
+
"""Runs Pyright CLI for diagnostics and analysis."""
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self._pyright_path: Optional[str] = None
|
|
14
|
+
self._check_pyright()
|
|
15
|
+
|
|
16
|
+
def _check_pyright(self) -> None:
|
|
17
|
+
"""Check if Pyright is available."""
|
|
18
|
+
try:
|
|
19
|
+
result = subprocess.run(
|
|
20
|
+
["pyright", "--version"],
|
|
21
|
+
capture_output=True,
|
|
22
|
+
text=True,
|
|
23
|
+
timeout=10,
|
|
24
|
+
)
|
|
25
|
+
if result.returncode == 0:
|
|
26
|
+
self._pyright_path = "pyright"
|
|
27
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
28
|
+
# Try npx
|
|
29
|
+
try:
|
|
30
|
+
result = subprocess.run(
|
|
31
|
+
["npx", "pyright", "--version"],
|
|
32
|
+
capture_output=True,
|
|
33
|
+
text=True,
|
|
34
|
+
timeout=30,
|
|
35
|
+
)
|
|
36
|
+
if result.returncode == 0:
|
|
37
|
+
self._pyright_path = "npx pyright"
|
|
38
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def is_available(self) -> bool:
|
|
43
|
+
"""Check if Pyright is available."""
|
|
44
|
+
return self._pyright_path is not None
|
|
45
|
+
|
|
46
|
+
def get_version(self) -> Optional[str]:
|
|
47
|
+
"""Get Pyright version."""
|
|
48
|
+
if not self.is_available or self._pyright_path is None:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
cmd = self._pyright_path.split() + ["--version"]
|
|
53
|
+
result = subprocess.run(
|
|
54
|
+
cmd,
|
|
55
|
+
capture_output=True,
|
|
56
|
+
text=True,
|
|
57
|
+
timeout=10,
|
|
58
|
+
)
|
|
59
|
+
return result.stdout.strip()
|
|
60
|
+
except Exception:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
def get_diagnostics(self, path: str) -> dict:
|
|
64
|
+
"""Get diagnostics for a file or directory.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
path: Path to file or directory
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Dict containing diagnostics or error
|
|
71
|
+
"""
|
|
72
|
+
if not self.is_available or self._pyright_path is None:
|
|
73
|
+
return {
|
|
74
|
+
"error": "Pyright is not installed. Install with: npm install -g pyright",
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
cmd = self._pyright_path.split() + ["--outputjson", path]
|
|
79
|
+
result = subprocess.run(
|
|
80
|
+
cmd,
|
|
81
|
+
capture_output=True,
|
|
82
|
+
text=True,
|
|
83
|
+
timeout=120,
|
|
84
|
+
cwd=str(Path(path).parent) if Path(path).is_file() else path,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Parse JSON output
|
|
88
|
+
try:
|
|
89
|
+
data = json.loads(result.stdout)
|
|
90
|
+
except json.JSONDecodeError:
|
|
91
|
+
# Pyright might output non-JSON errors
|
|
92
|
+
return {
|
|
93
|
+
"error": f"Failed to parse Pyright output: {result.stderr or result.stdout}",
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# Format diagnostics
|
|
97
|
+
diagnostics = []
|
|
98
|
+
for diag in data.get("generalDiagnostics", []):
|
|
99
|
+
diagnostics.append(
|
|
100
|
+
{
|
|
101
|
+
"file": diag.get("file", ""),
|
|
102
|
+
"line": diag.get("range", {}).get("start", {}).get("line", 0)
|
|
103
|
+
+ 1,
|
|
104
|
+
"column": diag.get("range", {})
|
|
105
|
+
.get("start", {})
|
|
106
|
+
.get("character", 0)
|
|
107
|
+
+ 1,
|
|
108
|
+
"end_line": diag.get("range", {}).get("end", {}).get("line", 0)
|
|
109
|
+
+ 1,
|
|
110
|
+
"end_column": diag.get("range", {})
|
|
111
|
+
.get("end", {})
|
|
112
|
+
.get("character", 0)
|
|
113
|
+
+ 1,
|
|
114
|
+
"severity": diag.get("severity", "error"),
|
|
115
|
+
"message": diag.get("message", ""),
|
|
116
|
+
"rule": diag.get("rule", ""),
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
summary = data.get("summary", {})
|
|
121
|
+
return {
|
|
122
|
+
"diagnostics": diagnostics,
|
|
123
|
+
"summary": {
|
|
124
|
+
"files_analyzed": summary.get("filesAnalyzed", 0),
|
|
125
|
+
"errors": summary.get("errorCount", 0),
|
|
126
|
+
"warnings": summary.get("warningCount", 0),
|
|
127
|
+
"informations": summary.get("informationCount", 0),
|
|
128
|
+
},
|
|
129
|
+
"version": data.get("version", ""),
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
except subprocess.TimeoutExpired:
|
|
133
|
+
return {"error": "Pyright analysis timed out"}
|
|
134
|
+
except Exception as e:
|
|
135
|
+
return {"error": str(e)}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# Global client instance
|
|
139
|
+
_client: Optional[PyrightClient] = None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def get_pyright_client() -> PyrightClient:
|
|
143
|
+
"""Get the global PyrightClient instance."""
|
|
144
|
+
global _client
|
|
145
|
+
if _client is None:
|
|
146
|
+
_client = PyrightClient()
|
|
147
|
+
return _client
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Rope client wrapper for managing projects and providing code analysis."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional, cast
|
|
7
|
+
|
|
8
|
+
import rope.base.project
|
|
9
|
+
from rope.base.resources import File
|
|
10
|
+
|
|
11
|
+
from .config import get_python_path_for_workspace
|
|
12
|
+
|
|
13
|
+
# Environment variable to disable Rope caching (creates .ropeproject in each project)
|
|
14
|
+
# Set to "1" or "true" to disable caching
|
|
15
|
+
DISABLE_CACHE = os.environ.get("PYTHON_LSP_MCP_NO_CACHE", "").lower() in ("1", "true")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _get_site_packages(python_executable: str) -> list[str]:
|
|
19
|
+
"""Get site-packages paths from a Python interpreter.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
python_executable: Path to Python interpreter
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
List of site-packages paths
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
result = subprocess.run(
|
|
29
|
+
[
|
|
30
|
+
python_executable,
|
|
31
|
+
"-c",
|
|
32
|
+
"import site; print('\\n'.join(site.getsitepackages()))",
|
|
33
|
+
],
|
|
34
|
+
capture_output=True,
|
|
35
|
+
text=True,
|
|
36
|
+
timeout=5,
|
|
37
|
+
)
|
|
38
|
+
if result.returncode == 0:
|
|
39
|
+
paths = [p.strip() for p in result.stdout.strip().split("\n") if p.strip()]
|
|
40
|
+
return paths
|
|
41
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
42
|
+
pass
|
|
43
|
+
return []
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class RopeClient:
|
|
47
|
+
"""Manages Rope projects and provides code analysis operations."""
|
|
48
|
+
|
|
49
|
+
def __init__(self):
|
|
50
|
+
self._projects: dict[str, rope.base.project.Project] = {}
|
|
51
|
+
self._project_python_paths: dict[str, str] = {}
|
|
52
|
+
|
|
53
|
+
def get_project(self, workspace: str) -> rope.base.project.Project:
|
|
54
|
+
"""Get or create a Rope project for the given workspace."""
|
|
55
|
+
workspace = os.path.abspath(workspace)
|
|
56
|
+
|
|
57
|
+
# Get the Python interpreter path for this workspace
|
|
58
|
+
python_executable = get_python_path_for_workspace(workspace)
|
|
59
|
+
|
|
60
|
+
# Check if we need to recreate the project (Python path changed)
|
|
61
|
+
if workspace in self._projects:
|
|
62
|
+
if self._project_python_paths.get(workspace) != python_executable:
|
|
63
|
+
# Python path changed, close old project
|
|
64
|
+
self._projects[workspace].close()
|
|
65
|
+
del self._projects[workspace]
|
|
66
|
+
|
|
67
|
+
if workspace not in self._projects:
|
|
68
|
+
# ropefolder=None disables caching, default ".ropeproject" enables it
|
|
69
|
+
ropefolder = None if DISABLE_CACHE else ".ropeproject"
|
|
70
|
+
project = rope.base.project.Project(
|
|
71
|
+
workspace,
|
|
72
|
+
ropefolder=ropefolder, # type: ignore[arg-type]
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Get site-packages from the Python interpreter and add to python_path
|
|
76
|
+
site_packages = _get_site_packages(python_executable)
|
|
77
|
+
if site_packages:
|
|
78
|
+
project.prefs.set("python_path", site_packages)
|
|
79
|
+
|
|
80
|
+
self._projects[workspace] = project
|
|
81
|
+
self._project_python_paths[workspace] = python_executable
|
|
82
|
+
|
|
83
|
+
return self._projects[workspace]
|
|
84
|
+
|
|
85
|
+
def get_python_path(self, workspace: str) -> str:
|
|
86
|
+
"""Get the Python path being used for a workspace."""
|
|
87
|
+
workspace = os.path.abspath(workspace)
|
|
88
|
+
return self._project_python_paths.get(
|
|
89
|
+
workspace, get_python_path_for_workspace(workspace)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def get_resource(self, project: rope.base.project.Project, file_path: str) -> File:
|
|
93
|
+
"""Get a Rope resource for a file path."""
|
|
94
|
+
abs_path = os.path.abspath(file_path)
|
|
95
|
+
project_root = project.root.real_path
|
|
96
|
+
|
|
97
|
+
if abs_path.startswith(project_root):
|
|
98
|
+
rel_path = os.path.relpath(abs_path, project_root)
|
|
99
|
+
else:
|
|
100
|
+
rel_path = abs_path
|
|
101
|
+
|
|
102
|
+
return cast(File, project.get_resource(rel_path))
|
|
103
|
+
|
|
104
|
+
def position_to_offset(self, source: str, line: int, column: int) -> int:
|
|
105
|
+
"""Convert (line, column) to byte offset.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
source: The source code string
|
|
109
|
+
line: 1-based line number
|
|
110
|
+
column: 1-based column number
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
0-based byte offset
|
|
114
|
+
"""
|
|
115
|
+
lines = source.splitlines(keepends=True)
|
|
116
|
+
offset = 0
|
|
117
|
+
for i in range(min(line - 1, len(lines))):
|
|
118
|
+
offset += len(lines[i])
|
|
119
|
+
offset += column - 1
|
|
120
|
+
return offset
|
|
121
|
+
|
|
122
|
+
def offset_to_position(self, source: str, offset: int) -> tuple[int, int]:
|
|
123
|
+
"""Convert byte offset to (line, column).
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
source: The source code string
|
|
127
|
+
offset: 0-based byte offset
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Tuple of (1-based line, 1-based column)
|
|
131
|
+
"""
|
|
132
|
+
lines = source.splitlines(keepends=True)
|
|
133
|
+
current_offset = 0
|
|
134
|
+
for i, line_text in enumerate(lines):
|
|
135
|
+
if current_offset + len(line_text) > offset:
|
|
136
|
+
return (i + 1, offset - current_offset + 1)
|
|
137
|
+
current_offset += len(line_text)
|
|
138
|
+
# Offset is at the end
|
|
139
|
+
return (len(lines), len(lines[-1]) + 1 if lines else 1)
|
|
140
|
+
|
|
141
|
+
def find_workspace_for_file(self, file_path: str) -> str:
|
|
142
|
+
"""Find the workspace root for a given file.
|
|
143
|
+
|
|
144
|
+
Looks for common project markers like pyproject.toml, setup.py, .git, etc.
|
|
145
|
+
Falls back to the file's parent directory.
|
|
146
|
+
"""
|
|
147
|
+
path = Path(file_path).resolve()
|
|
148
|
+
markers = [
|
|
149
|
+
"pyproject.toml",
|
|
150
|
+
"setup.py",
|
|
151
|
+
"setup.cfg",
|
|
152
|
+
".git",
|
|
153
|
+
"requirements.txt",
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
current = path.parent
|
|
157
|
+
while current != current.parent:
|
|
158
|
+
for marker in markers:
|
|
159
|
+
if (current / marker).exists():
|
|
160
|
+
return str(current)
|
|
161
|
+
current = current.parent
|
|
162
|
+
|
|
163
|
+
return str(path.parent)
|
|
164
|
+
|
|
165
|
+
def close_project(self, workspace: str) -> None:
|
|
166
|
+
"""Close and remove a project from the cache."""
|
|
167
|
+
workspace = os.path.abspath(workspace)
|
|
168
|
+
if workspace in self._projects:
|
|
169
|
+
self._projects[workspace].close()
|
|
170
|
+
del self._projects[workspace]
|
|
171
|
+
|
|
172
|
+
def close_all(self) -> None:
|
|
173
|
+
"""Close all cached projects."""
|
|
174
|
+
for project in self._projects.values():
|
|
175
|
+
project.close()
|
|
176
|
+
self._projects.clear()
|
|
177
|
+
|
|
178
|
+
def get_status(self) -> dict:
|
|
179
|
+
"""Get status information about the Rope client."""
|
|
180
|
+
return {
|
|
181
|
+
"active_projects": list(self._projects.keys()),
|
|
182
|
+
"project_count": len(self._projects),
|
|
183
|
+
"project_python_paths": dict(self._project_python_paths),
|
|
184
|
+
"caching_enabled": not DISABLE_CACHE,
|
|
185
|
+
"cache_folder": ".ropeproject" if not DISABLE_CACHE else None,
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# Global client instance
|
|
190
|
+
_client: Optional[RopeClient] = None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def get_client() -> RopeClient:
|
|
194
|
+
"""Get the global RopeClient instance."""
|
|
195
|
+
global _client
|
|
196
|
+
if _client is None:
|
|
197
|
+
_client = RopeClient()
|
|
198
|
+
return _client
|