@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.
Files changed (76) hide show
  1. package/dist/bundled/pyright/dist/index.d.ts +2 -0
  2. package/dist/bundled/pyright/dist/index.js +1620 -0
  3. package/dist/bundled/pyright/dist/index.js.map +26 -0
  4. package/dist/bundled/pyright/dist/lsp/connection.d.ts +71 -0
  5. package/dist/bundled/pyright/dist/lsp/document-manager.d.ts +67 -0
  6. package/dist/bundled/pyright/dist/lsp/index.d.ts +3 -0
  7. package/dist/bundled/pyright/dist/lsp/types.d.ts +55 -0
  8. package/dist/bundled/pyright/dist/lsp-client.d.ts +55 -0
  9. package/dist/bundled/pyright/dist/tools/completions.d.ts +18 -0
  10. package/dist/bundled/pyright/dist/tools/definition.d.ts +16 -0
  11. package/dist/bundled/pyright/dist/tools/diagnostics.d.ts +12 -0
  12. package/dist/bundled/pyright/dist/tools/hover.d.ts +16 -0
  13. package/dist/bundled/pyright/dist/tools/references.d.ts +16 -0
  14. package/dist/bundled/pyright/dist/tools/rename.d.ts +18 -0
  15. package/dist/bundled/pyright/dist/tools/search.d.ts +20 -0
  16. package/dist/bundled/pyright/dist/tools/signature-help.d.ts +16 -0
  17. package/dist/bundled/pyright/dist/tools/status.d.ts +14 -0
  18. package/dist/bundled/pyright/dist/tools/symbols.d.ts +17 -0
  19. package/dist/bundled/pyright/dist/tools/update-document.d.ts +14 -0
  20. package/dist/bundled/pyright/dist/utils/position.d.ts +33 -0
  21. package/dist/bundled/pyright/package.json +54 -0
  22. package/dist/bundled/python/README.md +230 -0
  23. package/dist/bundled/python/pyproject.toml +61 -0
  24. package/dist/bundled/python/src/rope_mcp/__init__.py +3 -0
  25. package/dist/bundled/python/src/rope_mcp/__pycache__/__init__.cpython-312.pyc +0 -0
  26. package/dist/bundled/python/src/rope_mcp/__pycache__/__init__.cpython-313.pyc +0 -0
  27. package/dist/bundled/python/src/rope_mcp/__pycache__/config.cpython-312.pyc +0 -0
  28. package/dist/bundled/python/src/rope_mcp/__pycache__/config.cpython-313.pyc +0 -0
  29. package/dist/bundled/python/src/rope_mcp/__pycache__/pyright_client.cpython-313.pyc +0 -0
  30. package/dist/bundled/python/src/rope_mcp/__pycache__/rope_client.cpython-313.pyc +0 -0
  31. package/dist/bundled/python/src/rope_mcp/__pycache__/server.cpython-312.pyc +0 -0
  32. package/dist/bundled/python/src/rope_mcp/__pycache__/server.cpython-313.pyc +0 -0
  33. package/dist/bundled/python/src/rope_mcp/config.py +408 -0
  34. package/dist/bundled/python/src/rope_mcp/lsp/__init__.py +15 -0
  35. package/dist/bundled/python/src/rope_mcp/lsp/__pycache__/__init__.cpython-313.pyc +0 -0
  36. package/dist/bundled/python/src/rope_mcp/lsp/__pycache__/client.cpython-313.pyc +0 -0
  37. package/dist/bundled/python/src/rope_mcp/lsp/__pycache__/types.cpython-313.pyc +0 -0
  38. package/dist/bundled/python/src/rope_mcp/lsp/client.py +624 -0
  39. package/dist/bundled/python/src/rope_mcp/lsp/types.py +82 -0
  40. package/dist/bundled/python/src/rope_mcp/pyright_client.py +147 -0
  41. package/dist/bundled/python/src/rope_mcp/rope_client.py +198 -0
  42. package/dist/bundled/python/src/rope_mcp/server.py +963 -0
  43. package/dist/bundled/python/src/rope_mcp/tools/__init__.py +26 -0
  44. package/dist/bundled/python/src/rope_mcp/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  45. package/dist/bundled/python/src/rope_mcp/tools/__pycache__/change_signature.cpython-313.pyc +0 -0
  46. package/dist/bundled/python/src/rope_mcp/tools/__pycache__/completions.cpython-313.pyc +0 -0
  47. package/dist/bundled/python/src/rope_mcp/tools/__pycache__/definition.cpython-313.pyc +0 -0
  48. package/dist/bundled/python/src/rope_mcp/tools/__pycache__/diagnostics.cpython-313.pyc +0 -0
  49. package/dist/bundled/python/src/rope_mcp/tools/__pycache__/hover.cpython-313.pyc +0 -0
  50. package/dist/bundled/python/src/rope_mcp/tools/__pycache__/move.cpython-313.pyc +0 -0
  51. package/dist/bundled/python/src/rope_mcp/tools/__pycache__/references.cpython-313.pyc +0 -0
  52. package/dist/bundled/python/src/rope_mcp/tools/__pycache__/rename.cpython-313.pyc +0 -0
  53. package/dist/bundled/python/src/rope_mcp/tools/__pycache__/search.cpython-313.pyc +0 -0
  54. package/dist/bundled/python/src/rope_mcp/tools/__pycache__/symbols.cpython-313.pyc +0 -0
  55. package/dist/bundled/python/src/rope_mcp/tools/change_signature.py +184 -0
  56. package/dist/bundled/python/src/rope_mcp/tools/completions.py +84 -0
  57. package/dist/bundled/python/src/rope_mcp/tools/definition.py +51 -0
  58. package/dist/bundled/python/src/rope_mcp/tools/diagnostics.py +18 -0
  59. package/dist/bundled/python/src/rope_mcp/tools/hover.py +49 -0
  60. package/dist/bundled/python/src/rope_mcp/tools/move.py +81 -0
  61. package/dist/bundled/python/src/rope_mcp/tools/references.py +60 -0
  62. package/dist/bundled/python/src/rope_mcp/tools/rename.py +61 -0
  63. package/dist/bundled/python/src/rope_mcp/tools/search.py +128 -0
  64. package/dist/bundled/python/src/rope_mcp/tools/symbols.py +118 -0
  65. package/dist/bundled/python/uv.lock +979 -0
  66. package/dist/bundled/typescript/dist/index.js +29534 -0
  67. package/dist/bundled/typescript/dist/index.js.map +211 -0
  68. package/dist/bundled/typescript/package.json +46 -0
  69. package/dist/bundled/vue/dist/index.d.ts +8 -0
  70. package/dist/bundled/vue/dist/index.js +21111 -0
  71. package/dist/bundled/vue/dist/ts-vue-service.d.ts +67 -0
  72. package/dist/bundled/vue/dist/vue-service.d.ts +144 -0
  73. package/dist/bundled/vue/package.json +45 -0
  74. package/dist/index.js +148 -58
  75. package/dist/index.js.map +4 -4
  76. 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