@treedy/lsp-mcp 0.1.7 → 0.1.9

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 (54) 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/config.py +444 -0
  26. package/dist/bundled/python/src/rope_mcp/lsp/__init__.py +15 -0
  27. package/dist/bundled/python/src/rope_mcp/lsp/client.py +863 -0
  28. package/dist/bundled/python/src/rope_mcp/lsp/types.py +83 -0
  29. package/dist/bundled/python/src/rope_mcp/pyright_client.py +147 -0
  30. package/dist/bundled/python/src/rope_mcp/rope_client.py +198 -0
  31. package/dist/bundled/python/src/rope_mcp/server.py +1217 -0
  32. package/dist/bundled/python/src/rope_mcp/tools/__init__.py +24 -0
  33. package/dist/bundled/python/src/rope_mcp/tools/change_signature.py +184 -0
  34. package/dist/bundled/python/src/rope_mcp/tools/completions.py +84 -0
  35. package/dist/bundled/python/src/rope_mcp/tools/definition.py +51 -0
  36. package/dist/bundled/python/src/rope_mcp/tools/diagnostics.py +18 -0
  37. package/dist/bundled/python/src/rope_mcp/tools/hover.py +49 -0
  38. package/dist/bundled/python/src/rope_mcp/tools/move.py +81 -0
  39. package/dist/bundled/python/src/rope_mcp/tools/references.py +60 -0
  40. package/dist/bundled/python/src/rope_mcp/tools/rename.py +61 -0
  41. package/dist/bundled/python/src/rope_mcp/tools/search.py +128 -0
  42. package/dist/bundled/python/src/rope_mcp/tools/symbols.py +118 -0
  43. package/dist/bundled/python/uv.lock +979 -0
  44. package/dist/bundled/typescript/dist/index.js +29772 -0
  45. package/dist/bundled/typescript/dist/index.js.map +211 -0
  46. package/dist/bundled/typescript/package.json +46 -0
  47. package/dist/bundled/vue/dist/index.d.ts +8 -0
  48. package/dist/bundled/vue/dist/index.js +21176 -0
  49. package/dist/bundled/vue/dist/ts-vue-service.d.ts +67 -0
  50. package/dist/bundled/vue/dist/vue-service.d.ts +160 -0
  51. package/dist/bundled/vue/package.json +45 -0
  52. package/dist/index.js +695 -352
  53. package/dist/index.js.map +6 -6
  54. package/package.json +1 -1
@@ -0,0 +1,444 @@
1
+ """Configuration for Python LSP MCP Server."""
2
+
3
+ import json
4
+ import sys
5
+ from dataclasses import dataclass, field
6
+ from enum import Enum
7
+ from pathlib import Path
8
+ from typing import Optional
9
+ import os
10
+
11
+ try:
12
+ import tomllib # type: ignore[import-not-found]
13
+ except ImportError:
14
+ import tomli as tomllib # type: ignore[import-not-found,import-untyped]
15
+
16
+
17
+ class Backend(Enum):
18
+ """Available backends for code analysis."""
19
+
20
+ ROPE = "rope"
21
+ PYRIGHT = "pyright"
22
+
23
+
24
+ # Tools that support both backends
25
+ SHARED_TOOLS = {"hover", "definition", "references", "completions", "symbols", "rename"}
26
+
27
+ # Tools exclusive to each backend
28
+ ROPE_ONLY_TOOLS = set() # Rope has better refactoring (move, change_signature)
29
+ PYRIGHT_ONLY_TOOLS = {"diagnostics", "signature_help"} # Pyright exclusive
30
+
31
+ # Environment variable prefix (supports both old and new names)
32
+ ENV_PREFIXES = ["PYTHON_LSP_MCP_", "ROPE_MCP_"]
33
+
34
+
35
+ @dataclass
36
+ class ServerConfig:
37
+ """Configuration for the MCP server."""
38
+
39
+ # Default backend for shared features
40
+ # Pyright is generally more robust for read operations as it doesn't require
41
+ # perfect environment setup (like running imports).
42
+ default_backend: Backend = Backend.PYRIGHT
43
+
44
+ # Per-tool backend overrides (None = use default)
45
+ tool_backends: dict[str, Backend] = field(default_factory=dict)
46
+
47
+ @classmethod
48
+ def from_env(cls) -> "ServerConfig":
49
+ """Create config from environment variables.
50
+
51
+ Environment variables (PYTHON_LSP_MCP_ or ROPE_MCP_ prefix):
52
+ *_BACKEND: Default backend (rope/pyright), default: pyright
53
+ *_HOVER_BACKEND: Backend for hover (rope/pyright)
54
+ *_DEFINITION_BACKEND: Backend for definition
55
+ *_REFERENCES_BACKEND: Backend for references
56
+ *_COMPLETIONS_BACKEND: Backend for completions
57
+ *_SYMBOLS_BACKEND: Backend for symbols
58
+ """
59
+
60
+ def get_env(suffix: str) -> Optional[str]:
61
+ """Get environment variable with any prefix."""
62
+ for prefix in ENV_PREFIXES:
63
+ val = os.environ.get(f"{prefix}{suffix}", "").lower()
64
+ if val:
65
+ return val
66
+ return None
67
+
68
+ default = get_env("BACKEND") or "pyright"
69
+ default_backend = (
70
+ Backend(default) if default in ["rope", "pyright"] else Backend.PYRIGHT
71
+ )
72
+
73
+ tool_backends = {}
74
+ for tool in SHARED_TOOLS:
75
+ val = get_env(f"{tool.upper()}_BACKEND")
76
+ if val in ["rope", "pyright"]:
77
+ tool_backends[tool] = Backend(val)
78
+
79
+ return cls(
80
+ default_backend=default_backend,
81
+ tool_backends=tool_backends,
82
+ )
83
+
84
+ def get_backend_for(self, tool: str) -> Backend:
85
+ """Get the backend to use for a specific tool."""
86
+ if tool in ROPE_ONLY_TOOLS:
87
+ return Backend.ROPE
88
+ if tool in PYRIGHT_ONLY_TOOLS:
89
+ return Backend.PYRIGHT
90
+
91
+ if tool in self.tool_backends:
92
+ return self.tool_backends[tool]
93
+ return self.default_backend
94
+
95
+ def set_backend(self, backend: Backend, tool: Optional[str] = None) -> None:
96
+ """Set the backend for a tool or as default.
97
+
98
+ Args:
99
+ backend: The backend to use
100
+ tool: Optional tool name. If None, sets the default backend.
101
+ """
102
+ if tool is None:
103
+ self.default_backend = backend
104
+ elif tool in SHARED_TOOLS:
105
+ self.tool_backends[tool] = backend
106
+
107
+ def set_all_backends(self, backend: Backend) -> None:
108
+ """Set all shared tools to use the same backend."""
109
+ self.default_backend = backend
110
+ self.tool_backends.clear()
111
+
112
+
113
+ # Global config instance
114
+ _config: Optional[ServerConfig] = None
115
+
116
+
117
+ def get_config() -> ServerConfig:
118
+ """Get the global server configuration."""
119
+ global _config
120
+ if _config is None:
121
+ _config = ServerConfig.from_env()
122
+ return _config
123
+
124
+
125
+ def set_config(config: ServerConfig) -> None:
126
+ """Set the global server configuration."""
127
+ global _config
128
+ _config = config
129
+
130
+
131
+ # ============================================================================
132
+ # Python Interpreter Path Configuration
133
+ # ============================================================================
134
+
135
+
136
+ def find_pyright_python_path(workspace: str) -> Optional[str]:
137
+ """Find Python path from Pyright configuration.
138
+
139
+ Checks in order:
140
+ 1. pyrightconfig.json
141
+ 2. pyproject.toml [tool.pyright]
142
+
143
+ Args:
144
+ workspace: The workspace root directory
145
+
146
+ Returns:
147
+ Python interpreter path if found, None otherwise
148
+ """
149
+ workspace_path = Path(workspace)
150
+
151
+ # Check pyrightconfig.json
152
+ pyright_config = workspace_path / "pyrightconfig.json"
153
+ if pyright_config.exists():
154
+ try:
155
+ with open(pyright_config, "r", encoding="utf-8") as f:
156
+ config = json.load(f)
157
+ python_path = _extract_python_path_from_pyright(config, workspace_path)
158
+ if python_path:
159
+ return python_path
160
+ except (json.JSONDecodeError, OSError):
161
+ pass
162
+
163
+ # Check pyproject.toml
164
+ pyproject = workspace_path / "pyproject.toml"
165
+ if pyproject.exists():
166
+ try:
167
+ with open(pyproject, "rb") as f:
168
+ config = tomllib.load(f)
169
+ pyright_config = config.get("tool", {}).get("pyright", {})
170
+ python_path = _extract_python_path_from_pyright(
171
+ pyright_config, workspace_path
172
+ )
173
+ if python_path:
174
+ return python_path
175
+ except (tomllib.TOMLDecodeError, OSError):
176
+ pass
177
+
178
+ return None
179
+
180
+
181
+ def _extract_python_path_from_pyright(
182
+ config: dict, workspace_path: Path
183
+ ) -> Optional[str]:
184
+ """Extract Python path from Pyright config dict.
185
+
186
+ Supports:
187
+ - pythonPath: direct path to Python interpreter
188
+ - venvPath + venv: virtual environment path
189
+ """
190
+ # Direct pythonPath
191
+ if "pythonPath" in config:
192
+ python_path = Path(config["pythonPath"])
193
+ if not python_path.is_absolute():
194
+ python_path = workspace_path / python_path
195
+ if python_path.exists():
196
+ return str(python_path)
197
+
198
+ # venvPath + venv combination
199
+ venv_path = config.get("venvPath")
200
+ venv_name = config.get("venv")
201
+ if venv_path and venv_name:
202
+ venv_dir = Path(venv_path)
203
+ if not venv_dir.is_absolute():
204
+ venv_dir = workspace_path / venv_dir
205
+ venv_dir = venv_dir / venv_name
206
+
207
+ # Check for Python executable
208
+ if sys.platform == "win32":
209
+ python_exe = venv_dir / "Scripts" / "python.exe"
210
+ else:
211
+ python_exe = venv_dir / "bin" / "python"
212
+
213
+ if python_exe.exists():
214
+ return str(python_exe)
215
+
216
+ return None
217
+
218
+
219
+ def find_venv_python_path(workspace: str) -> Optional[str]:
220
+ """Find Python path from common virtual environment locations.
221
+
222
+ Checks:
223
+ - .venv/bin/python
224
+ - venv/bin/python
225
+ - env/bin/python
226
+
227
+ Args:
228
+ workspace: The workspace root directory
229
+
230
+ Returns:
231
+ Python interpreter path if found, None otherwise
232
+ """
233
+ workspace_path = Path(workspace)
234
+ venv_dirs = [".venv", "venv", "env"]
235
+
236
+ for venv_dir in venv_dirs:
237
+ if sys.platform == "win32":
238
+ python_exe = workspace_path / venv_dir / "Scripts" / "python.exe"
239
+ else:
240
+ python_exe = workspace_path / venv_dir / "bin" / "python"
241
+
242
+ if python_exe.exists():
243
+ return str(python_exe)
244
+
245
+ return None
246
+
247
+
248
+ def get_python_path_for_workspace(workspace: str) -> str:
249
+ """Get the Python interpreter path for a workspace.
250
+
251
+ Priority:
252
+ 1. Manually set path (via set_python_path tool)
253
+ 2. Pyright configuration (pyrightconfig.json or pyproject.toml)
254
+ 3. Virtual environment in workspace (.venv, venv, env)
255
+ 4. Current Python interpreter (sys.executable)
256
+
257
+ Args:
258
+ workspace: The workspace root directory
259
+
260
+ Returns:
261
+ Path to Python interpreter
262
+ """
263
+ workspace = os.path.abspath(workspace)
264
+
265
+ # Check manually set path
266
+ if workspace in _python_paths:
267
+ return _python_paths[workspace]
268
+
269
+ # Check global override
270
+ if _global_python_path:
271
+ return _global_python_path
272
+
273
+ # Check Pyright config
274
+ pyright_path = find_pyright_python_path(workspace)
275
+ if pyright_path:
276
+ return pyright_path
277
+
278
+ # Check virtual environment
279
+ venv_path = find_venv_python_path(workspace)
280
+ if venv_path:
281
+ return venv_path
282
+
283
+ # Fall back to current interpreter
284
+ return sys.executable
285
+
286
+
287
+ # Per-workspace Python paths (set via tool)
288
+ _python_paths: dict[str, str] = {}
289
+
290
+ # Global Python path override
291
+ _global_python_path: Optional[str] = None
292
+
293
+ # Active workspace for single-project mode
294
+ _active_workspace: Optional[str] = None
295
+
296
+
297
+ def set_active_workspace(workspace: str) -> str:
298
+ """Set the active workspace.
299
+
300
+ Args:
301
+ workspace: Path to the workspace root
302
+
303
+ Returns:
304
+ The absolute path to the active workspace
305
+ """
306
+ global _active_workspace
307
+ _active_workspace = os.path.abspath(workspace)
308
+ return _active_workspace
309
+
310
+
311
+ def get_active_workspace() -> Optional[str]:
312
+ """Get the currently active workspace."""
313
+ return _active_workspace
314
+
315
+
316
+ def is_file_in_workspace(file_path: str) -> bool:
317
+ """Check if a file belongs to the active workspace.
318
+
319
+ Args:
320
+ file_path: Path to the file
321
+
322
+ Returns:
323
+ True if the file is within the active workspace, False otherwise.
324
+ Returns True if no active workspace is set (backward compatibility).
325
+ """
326
+ if not _active_workspace:
327
+ return True
328
+
329
+ abs_file = os.path.abspath(file_path)
330
+ return abs_file.startswith(_active_workspace)
331
+
332
+
333
+ def resolve_file_path(file_path: str) -> tuple[Optional[str], Optional[dict]]:
334
+ """Resolve a file path and validate it against the active workspace.
335
+
336
+ Supports:
337
+ 1. Absolute paths (must be within active workspace)
338
+ 2. Relative paths (resolved against active workspace)
339
+
340
+ Args:
341
+ file_path: Path to the file (absolute or relative)
342
+
343
+ Returns:
344
+ Tuple of (absolute_path, error_dict).
345
+ If success, absolute_path is set and error_dict is None.
346
+ If failure, absolute_path is None and error_dict contains the error.
347
+ """
348
+ path_obj = Path(file_path)
349
+
350
+ # Handle relative paths
351
+ if not path_obj.is_absolute():
352
+ if _active_workspace:
353
+ abs_path = os.path.join(_active_workspace, file_path)
354
+ else:
355
+ # If no workspace set, we can't resolve relative paths reliably
356
+ # But for backward compatibility with "no workspace" mode, we might assume cwd
357
+ abs_path = os.path.abspath(file_path)
358
+ else:
359
+ abs_path = str(path_obj)
360
+
361
+ # Normalize path
362
+ abs_path = os.path.abspath(abs_path)
363
+
364
+ # Validate against workspace
365
+ if _active_workspace and not abs_path.startswith(_active_workspace):
366
+ return None, {
367
+ "error": "Context Mismatch",
368
+ "message": (
369
+ f"The file '{file_path}' resolves to '{abs_path}', which is outside the active workspace '{_active_workspace}'.\n\n"
370
+ "Current Logic:\n"
371
+ "1. I only analyze files from the active project to ensure accuracy and save resources.\n"
372
+ "2. You must explicitly switch the workspace if you want to work on a different project.\n\n"
373
+ "Action Required:\n"
374
+ "Please call 'switch_workspace(path=\"...\")' with the new project root before retrying."
375
+ ),
376
+ "current_workspace": _active_workspace,
377
+ "resolved_path": abs_path
378
+ }
379
+
380
+ return abs_path, None
381
+
382
+
383
+ def validate_file_workspace(file_path: str) -> Optional[dict]:
384
+ """Validate that a file is within the active workspace.
385
+
386
+ Deprecated: Use resolve_file_path instead.
387
+ """
388
+ _, error = resolve_file_path(file_path)
389
+ return error
390
+
391
+
392
+ def set_python_path(python_path: str, workspace: Optional[str] = None) -> dict:
393
+ """Set the Python interpreter path.
394
+
395
+ Args:
396
+ python_path: Path to Python interpreter
397
+ workspace: Optional workspace to set path for.
398
+ If None, sets global default.
399
+
400
+ Returns:
401
+ Dict with success status and message
402
+ """
403
+ global _global_python_path
404
+
405
+ # Validate the path exists
406
+ path = Path(python_path)
407
+ if not path.exists():
408
+ return {
409
+ "success": False,
410
+ "error": f"Python path does not exist: {python_path}",
411
+ }
412
+
413
+ # Check if it's executable
414
+ if not os.access(python_path, os.X_OK):
415
+ return {
416
+ "success": False,
417
+ "error": f"Python path is not executable: {python_path}",
418
+ }
419
+
420
+ if workspace:
421
+ workspace = os.path.abspath(workspace)
422
+ _python_paths[workspace] = python_path
423
+ return {
424
+ "success": True,
425
+ "message": f"Python path set for workspace: {workspace}",
426
+ "python_path": python_path,
427
+ "workspace": workspace,
428
+ }
429
+ else:
430
+ _global_python_path = python_path
431
+ return {
432
+ "success": True,
433
+ "message": "Global Python path set",
434
+ "python_path": python_path,
435
+ }
436
+
437
+
438
+ def get_python_path_status() -> dict:
439
+ """Get the current Python path configuration status."""
440
+ return {
441
+ "global_python_path": _global_python_path,
442
+ "workspace_python_paths": dict(_python_paths),
443
+ "current_interpreter": sys.executable,
444
+ }
@@ -0,0 +1,15 @@
1
+ """LSP client for Pyright language server."""
2
+
3
+ from .client import LspClient, get_lsp_client, close_all_clients, refresh_lsp_documents
4
+ from .types import Position, Range, Location, TextDocumentIdentifier
5
+
6
+ __all__ = [
7
+ "LspClient",
8
+ "get_lsp_client",
9
+ "close_all_clients",
10
+ "refresh_lsp_documents",
11
+ "Position",
12
+ "Range",
13
+ "Location",
14
+ "TextDocumentIdentifier",
15
+ ]