@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,963 @@
1
+ """MCP Server for Python code analysis using Rope and Pyright."""
2
+
3
+ import atexit
4
+ import json
5
+ import os
6
+ import sys
7
+ import threading
8
+ from importlib.metadata import version as pkg_version
9
+ from typing import Literal, Optional
10
+
11
+ from mcp.server.fastmcp import FastMCP
12
+
13
+ # Get package version at runtime
14
+ try:
15
+ __version__ = pkg_version("python-lsp-mcp")
16
+ except Exception:
17
+ __version__ = "0.3.0" # Fallback
18
+
19
+ from .config import (
20
+ Backend,
21
+ get_config,
22
+ SHARED_TOOLS,
23
+ set_python_path as config_set_python_path,
24
+ get_python_path_status,
25
+ set_active_workspace,
26
+ get_active_workspace,
27
+ validate_file_workspace,
28
+ )
29
+ from .rope_client import get_client as get_rope_client
30
+ from .lsp import get_lsp_client, close_all_clients, close_all_clients as close_lsp_clients
31
+ from .tools import (
32
+ do_rename,
33
+ do_move,
34
+ do_change_signature,
35
+ get_function_signature,
36
+ get_completions as rope_completions,
37
+ get_definition as rope_definition,
38
+ get_hover as rope_hover,
39
+ get_references as rope_references,
40
+ get_symbols as rope_symbols,
41
+ get_diagnostics,
42
+ get_search,
43
+ )
44
+
45
+ # Create the MCP server
46
+ mcp = FastMCP("python-lsp-mcp")
47
+
48
+ # Register cleanup on exit
49
+ atexit.register(close_all_clients)
50
+
51
+
52
+ @mcp.tool()
53
+ def switch_workspace(path: str) -> str:
54
+ """Switch the active workspace to a new project directory.
55
+
56
+ This will:
57
+ 1. Set the new active workspace path.
58
+ 2. Close all existing language server instances to save resources.
59
+ 3. The next tool call will start a new language server for the new workspace.
60
+
61
+ Args:
62
+ path: Absolute path to the new project root directory.
63
+
64
+ Returns:
65
+ JSON string with confirmation of the switch.
66
+ """
67
+ abs_path = os.path.abspath(path)
68
+ if not os.path.isdir(abs_path):
69
+ return json.dumps(
70
+ {"error": "Invalid Path", "message": f"'{path}' is not a directory."},
71
+ indent=2,
72
+ )
73
+
74
+ # Close all existing clients (LSP and Rope)
75
+ close_lsp_clients()
76
+ get_rope_client().close_all()
77
+
78
+ # Set new active workspace
79
+ new_workspace = set_active_workspace(abs_path)
80
+
81
+ return json.dumps(
82
+ {
83
+ "success": True,
84
+ "message": f"Switched active workspace to: {new_workspace}",
85
+ "workspace": new_workspace,
86
+ "info": "All previous language server instances have been closed.",
87
+ },
88
+ indent=2,
89
+ )
90
+
91
+
92
+ def _find_workspace(file_path: str) -> str:
93
+ """Find workspace root for a file."""
94
+ # If we have an active workspace, always use it
95
+ active = get_active_workspace()
96
+ if active:
97
+ return active
98
+
99
+ client = get_rope_client()
100
+ return client.find_workspace_for_file(file_path)
101
+
102
+
103
+ def _get_effective_backend(tool: str, backend: Optional[str]) -> Backend:
104
+ """Get the effective backend for a tool."""
105
+ if backend:
106
+ try:
107
+ return Backend(backend.lower())
108
+ except ValueError:
109
+ pass
110
+ return get_config().get_backend_for(tool)
111
+
112
+
113
+ @mcp.tool()
114
+ def hover(
115
+ file: str,
116
+ line: int,
117
+ column: int,
118
+ backend: Optional[Literal["rope", "pyright"]] = None,
119
+ ) -> str:
120
+ """Get documentation for the symbol at the given position.
121
+
122
+ Args:
123
+ file: Absolute path to the Python file
124
+ line: 1-based line number
125
+ column: 1-based column number
126
+ backend: Backend to use (rope/pyright). Default: from config or 'rope'
127
+
128
+ Returns:
129
+ JSON string with documentation or error message
130
+ """
131
+ # Guard: check workspace
132
+ error = validate_file_workspace(file)
133
+ if error:
134
+ return json.dumps(error, indent=2)
135
+
136
+ effective_backend = _get_effective_backend("hover", backend)
137
+
138
+ if effective_backend == Backend.PYRIGHT:
139
+ try:
140
+ workspace = _find_workspace(file)
141
+ client = get_lsp_client(workspace)
142
+ result = client.hover(file, line, column)
143
+ if result:
144
+ return json.dumps(
145
+ {"contents": result.get("contents", ""), "backend": "pyright"},
146
+ indent=2,
147
+ )
148
+ return json.dumps(
149
+ {"contents": None, "message": "No hover info", "backend": "pyright"},
150
+ indent=2,
151
+ )
152
+ except Exception as e:
153
+ return json.dumps({"error": str(e), "backend": "pyright"}, indent=2)
154
+ else:
155
+ result = rope_hover(file, line, column)
156
+ result["backend"] = "rope"
157
+ return json.dumps(result, indent=2)
158
+
159
+
160
+ @mcp.tool()
161
+ def definition(
162
+ file: str,
163
+ line: int,
164
+ column: int,
165
+ backend: Optional[Literal["rope", "pyright"]] = None,
166
+ ) -> str:
167
+ """Get the definition location for the symbol at the given position.
168
+
169
+ Args:
170
+ file: Absolute path to the Python file
171
+ line: 1-based line number
172
+ column: 1-based column number
173
+ backend: Backend to use (rope/pyright). Default: from config or 'rope'
174
+
175
+ Returns:
176
+ JSON string with definition location or error message
177
+ """
178
+ # Guard: check workspace
179
+ error = validate_file_workspace(file)
180
+ if error:
181
+ return json.dumps(error, indent=2)
182
+
183
+ effective_backend = _get_effective_backend("definition", backend)
184
+
185
+ if effective_backend == Backend.PYRIGHT:
186
+ try:
187
+ workspace = _find_workspace(file)
188
+ client = get_lsp_client(workspace)
189
+ locations = client.definition(file, line, column)
190
+ if locations:
191
+ result = locations[0]
192
+ result["backend"] = "pyright"
193
+ return json.dumps(result, indent=2)
194
+ return json.dumps(
195
+ {"file": None, "message": "No definition found", "backend": "pyright"},
196
+ indent=2,
197
+ )
198
+ except Exception as e:
199
+ return json.dumps({"error": str(e), "backend": "pyright"}, indent=2)
200
+ else:
201
+ result = rope_definition(file, line, column)
202
+ result["backend"] = "rope"
203
+ return json.dumps(result, indent=2)
204
+
205
+
206
+ @mcp.tool()
207
+ def references(
208
+ file: str,
209
+ line: int,
210
+ column: int,
211
+ backend: Optional[Literal["rope", "pyright"]] = None,
212
+ ) -> str:
213
+ """Find all references to the symbol at the given position.
214
+
215
+ Args:
216
+ file: Absolute path to the Python file
217
+ line: 1-based line number
218
+ column: 1-based column number
219
+ backend: Backend to use (rope/pyright). Default: from config or 'rope'
220
+
221
+ Returns:
222
+ JSON string with list of references or error message
223
+ """
224
+ # Guard: check workspace
225
+ error = validate_file_workspace(file)
226
+ if error:
227
+ return json.dumps(error, indent=2)
228
+
229
+ effective_backend = _get_effective_backend("references", backend)
230
+
231
+ if effective_backend == Backend.PYRIGHT:
232
+ try:
233
+ workspace = _find_workspace(file)
234
+ client = get_lsp_client(workspace)
235
+ refs = client.references(file, line, column)
236
+ return json.dumps(
237
+ {"references": refs, "count": len(refs), "backend": "pyright"}, indent=2
238
+ )
239
+ except Exception as e:
240
+ return json.dumps({"error": str(e), "backend": "pyright"}, indent=2)
241
+ else:
242
+ result = rope_references(file, line, column)
243
+ result["backend"] = "rope"
244
+ return json.dumps(result, indent=2)
245
+
246
+
247
+ @mcp.tool()
248
+ def completions(
249
+ file: str,
250
+ line: int,
251
+ column: int,
252
+ backend: Optional[Literal["rope", "pyright"]] = None,
253
+ ) -> str:
254
+ """Get code completion suggestions at the given position.
255
+
256
+ Args:
257
+ file: Absolute path to the Python file
258
+ line: 1-based line number
259
+ column: 1-based column number
260
+ backend: Backend to use (rope/pyright). Default: from config or 'rope'
261
+
262
+ Returns:
263
+ JSON string with completion items or error message
264
+ """
265
+ # Guard: check workspace
266
+ error = validate_file_workspace(file)
267
+ if error:
268
+ return json.dumps(error, indent=2)
269
+
270
+ effective_backend = _get_effective_backend("completions", backend)
271
+
272
+ if effective_backend == Backend.PYRIGHT:
273
+ try:
274
+ workspace = _find_workspace(file)
275
+ client = get_lsp_client(workspace)
276
+ items = client.completions(file, line, column)
277
+ return json.dumps(
278
+ {"completions": items, "count": len(items), "backend": "pyright"},
279
+ indent=2,
280
+ )
281
+ except Exception as e:
282
+ return json.dumps({"error": str(e), "backend": "pyright"}, indent=2)
283
+ else:
284
+ result = rope_completions(file, line, column)
285
+ result["backend"] = "rope"
286
+ return json.dumps(result, indent=2)
287
+
288
+
289
+ @mcp.tool()
290
+ def symbols(
291
+ file: str,
292
+ query: Optional[str] = None,
293
+ backend: Optional[Literal["rope", "pyright"]] = None,
294
+ ) -> str:
295
+ """Get symbols from a Python file.
296
+
297
+ Args:
298
+ file: Absolute path to the Python file
299
+ query: Optional filter query for symbol names
300
+ backend: Backend to use (rope/pyright). Default: from config or 'rope'
301
+
302
+ Returns:
303
+ JSON string with list of symbols or error message
304
+ """
305
+ # Guard: check workspace
306
+ error = validate_file_workspace(file)
307
+ if error:
308
+ return json.dumps(error, indent=2)
309
+
310
+ effective_backend = _get_effective_backend("symbols", backend)
311
+
312
+ if effective_backend == Backend.PYRIGHT:
313
+ try:
314
+ workspace = _find_workspace(file)
315
+ client = get_lsp_client(workspace)
316
+ syms = client.document_symbols(file)
317
+ # Filter by query if provided
318
+ if query:
319
+ query_lower = query.lower()
320
+ syms = [s for s in syms if query_lower in s["name"].lower()]
321
+ return json.dumps(
322
+ {
323
+ "symbols": syms,
324
+ "count": len(syms),
325
+ "file": file,
326
+ "backend": "pyright",
327
+ },
328
+ indent=2,
329
+ )
330
+ except Exception as e:
331
+ return json.dumps({"error": str(e), "backend": "pyright"}, indent=2)
332
+ else:
333
+ result = rope_symbols(file, query)
334
+ result["backend"] = "rope"
335
+ return json.dumps(result, indent=2)
336
+
337
+
338
+ @mcp.tool()
339
+ def rename(file: str, line: int, column: int, new_name: str) -> str:
340
+ """Rename the symbol at the given position.
341
+
342
+ This will modify files on disk to rename all occurrences of the symbol.
343
+ Uses Rope backend for best refactoring support.
344
+
345
+ Args:
346
+ file: Absolute path to the Python file
347
+ line: 1-based line number
348
+ column: 1-based column number
349
+ new_name: The new name for the symbol
350
+
351
+ Returns:
352
+ JSON string with changes made or error message
353
+ """
354
+ # Guard: check workspace
355
+ error = validate_file_workspace(file)
356
+ if error:
357
+ return json.dumps(error, indent=2)
358
+
359
+ result = do_rename(file, line, column, new_name)
360
+ result["backend"] = "rope"
361
+ return json.dumps(result, indent=2)
362
+
363
+
364
+ @mcp.tool()
365
+ def move(
366
+ file: str,
367
+ line: int,
368
+ column: int,
369
+ destination: str,
370
+ preview: bool = False,
371
+ ) -> str:
372
+ """Move a function or class to another module.
373
+
374
+ This will modify files on disk to move the symbol and update all imports.
375
+ Uses Rope backend for refactoring.
376
+
377
+ Args:
378
+ file: Absolute path to the Python file containing the symbol
379
+ line: 1-based line number of the symbol to move
380
+ column: 1-based column number of the symbol
381
+ destination: Destination module path (e.g., "mypackage.utils" or "utils.py")
382
+ preview: If True, only show what would change without applying
383
+
384
+ Returns:
385
+ JSON string with changes made or error message
386
+ """
387
+ # Guard: check workspace
388
+ error = validate_file_workspace(file)
389
+ if error:
390
+ return json.dumps(error, indent=2)
391
+
392
+ result = do_move(file, line, column, destination, resources_only=preview)
393
+ result["backend"] = "rope"
394
+ return json.dumps(result, indent=2)
395
+
396
+
397
+ @mcp.tool()
398
+ def change_signature(
399
+ file: str,
400
+ line: int,
401
+ column: int,
402
+ new_params: Optional[list[str]] = None,
403
+ add_param: Optional[str] = None,
404
+ add_param_default: Optional[str] = None,
405
+ add_param_index: Optional[int] = None,
406
+ remove_param: Optional[str] = None,
407
+ preview: bool = False,
408
+ ) -> str:
409
+ """Change the signature of a function.
410
+
411
+ This will modify files on disk to update the function and all call sites.
412
+ Uses Rope backend for refactoring.
413
+
414
+ Args:
415
+ file: Absolute path to the Python file
416
+ line: 1-based line number of the function
417
+ column: 1-based column number of the function
418
+ new_params: New parameter order, e.g. ["self", "b", "a"] to reorder
419
+ add_param: Name of parameter to add
420
+ add_param_default: Default value for added parameter
421
+ add_param_index: Index where to insert new param (None = append)
422
+ remove_param: Name of parameter to remove
423
+ preview: If True, only show what would change without applying
424
+
425
+ Returns:
426
+ JSON string with changes made or error message
427
+
428
+ Examples:
429
+ # Reorder: def foo(a, b) -> def foo(b, a)
430
+ change_signature(file, line, col, new_params=["self", "b", "a"])
431
+
432
+ # Add param: def foo(a) -> def foo(a, b=None)
433
+ change_signature(file, line, col, add_param="b", add_param_default="None")
434
+
435
+ # Remove param: def foo(a, b) -> def foo(a)
436
+ change_signature(file, line, col, remove_param="b")
437
+ """
438
+ # Guard: check workspace
439
+ error = validate_file_workspace(file)
440
+ if error:
441
+ return json.dumps(error, indent=2)
442
+
443
+ # Build add_param dict if specified
444
+ add_param_dict = None
445
+ if add_param:
446
+ add_param_dict = {
447
+ "name": add_param,
448
+ "default": add_param_default,
449
+ "index": add_param_index,
450
+ }
451
+
452
+ result = do_change_signature(
453
+ file,
454
+ line,
455
+ column,
456
+ new_params=new_params,
457
+ add_param=add_param_dict,
458
+ remove_param=remove_param,
459
+ resources_only=preview,
460
+ )
461
+ result["backend"] = "rope"
462
+ return json.dumps(result, indent=2)
463
+
464
+
465
+ @mcp.tool()
466
+ def function_signature(file: str, line: int, column: int) -> str:
467
+ """Get the current signature of a function.
468
+
469
+ Useful for inspecting function parameters before changing the signature.
470
+
471
+ Args:
472
+ file: Absolute path to the Python file
473
+ line: 1-based line number of the function
474
+ column: 1-based column number of the function
475
+
476
+ Returns:
477
+ JSON string with function signature info
478
+ """
479
+ # Guard: check workspace
480
+ error = validate_file_workspace(file)
481
+ if error:
482
+ return json.dumps(error, indent=2)
483
+
484
+ result = get_function_signature(file, line, column)
485
+ result["backend"] = "rope"
486
+ return json.dumps(result, indent=2)
487
+
488
+
489
+ @mcp.tool()
490
+ def diagnostics(path: str) -> str:
491
+ """Get type errors and warnings for a Python file or directory.
492
+
493
+ Uses Pyright for type checking. Requires Pyright to be installed.
494
+
495
+ Args:
496
+ path: Absolute path to a Python file or directory
497
+
498
+ Returns:
499
+ JSON string with diagnostics or error message
500
+ """
501
+ # Guard: check workspace
502
+ error = validate_file_workspace(path)
503
+ if error:
504
+ return json.dumps(error, indent=2)
505
+
506
+ result = get_diagnostics(path)
507
+ result["backend"] = "pyright"
508
+ return json.dumps(result, indent=2)
509
+
510
+
511
+ @mcp.tool()
512
+ def signature_help(file: str, line: int, column: int) -> str:
513
+ """Get function signature information at the given position.
514
+
515
+ Uses Pyright backend for accurate signature information.
516
+
517
+ Args:
518
+ file: Absolute path to the Python file
519
+ line: 1-based line number
520
+ column: 1-based column number
521
+
522
+ Returns:
523
+ JSON string with signature help or error message
524
+ """
525
+ # Guard: check workspace
526
+ error = validate_file_workspace(file)
527
+ if error:
528
+ return json.dumps(error, indent=2)
529
+
530
+ try:
531
+ workspace = _find_workspace(file)
532
+ client = get_lsp_client(workspace)
533
+ result = client.signature_help(file, line, column)
534
+ if result:
535
+ result["backend"] = "pyright"
536
+ return json.dumps(result, indent=2)
537
+ return json.dumps(
538
+ {"message": "No signature help available", "backend": "pyright"}, indent=2
539
+ )
540
+ except Exception as e:
541
+ return json.dumps({"error": str(e), "backend": "pyright"}, indent=2)
542
+
543
+
544
+ @mcp.tool()
545
+ def update_document(file: str, content: str) -> str:
546
+ """Update file content for incremental analysis without writing to disk.
547
+
548
+ Useful for testing code changes before saving.
549
+ Uses Pyright backend for incremental updates.
550
+
551
+ Args:
552
+ file: Absolute path to the Python file
553
+ content: New file content
554
+
555
+ Returns:
556
+ JSON string with confirmation
557
+ """
558
+ # Guard: check workspace
559
+ error = validate_file_workspace(file)
560
+ if error:
561
+ return json.dumps(error, indent=2)
562
+
563
+ try:
564
+ workspace = _find_workspace(file)
565
+ client = get_lsp_client(workspace)
566
+ client.update_document(file, content)
567
+ return json.dumps(
568
+ {"success": True, "file": file, "backend": "pyright"}, indent=2
569
+ )
570
+ except Exception as e:
571
+ return json.dumps({"error": str(e), "backend": "pyright"}, indent=2)
572
+
573
+
574
+
575
+ @mcp.tool()
576
+ def search(
577
+ pattern: str,
578
+ path: Optional[str] = None,
579
+ glob: Optional[str] = None,
580
+ case_sensitive: bool = True,
581
+ max_results: int = 50,
582
+ ) -> str:
583
+ """Search for a regex pattern in files using ripgrep.
584
+
585
+ Args:
586
+ pattern: The regex pattern to search for
587
+ path: Directory or file to search in (defaults to current working directory)
588
+ glob: Glob pattern to filter files (e.g., "*.py", "**/*.ts")
589
+ case_sensitive: Whether the search is case sensitive
590
+ max_results: Maximum number of results to return
591
+
592
+ Returns:
593
+ JSON string with search results or error message
594
+ """
595
+ # Guard: check workspace if path is provided
596
+ if path:
597
+ error = validate_file_workspace(path)
598
+ if error:
599
+ return json.dumps(error, indent=2)
600
+
601
+ result = get_search(
602
+ pattern=pattern,
603
+ path=path,
604
+ glob=glob,
605
+ case_sensitive=case_sensitive,
606
+ max_results=max_results,
607
+ )
608
+ result["backend"] = "ripgrep"
609
+ return json.dumps(result, indent=2)
610
+
611
+
612
+ @mcp.tool()
613
+ def set_backend(
614
+ backend: Literal["rope", "pyright"],
615
+ tool: Optional[str] = None,
616
+ ) -> str:
617
+ """Set the backend for code analysis tools.
618
+
619
+ Args:
620
+ backend: The backend to use ('rope' or 'pyright')
621
+ tool: Optional tool name (hover/definition/references/completions/symbols).
622
+ If not provided, sets the default backend for all shared tools.
623
+
624
+ Returns:
625
+ JSON string with the updated configuration
626
+ """
627
+ config = get_config()
628
+
629
+ try:
630
+ backend_enum = Backend(backend.lower())
631
+ except ValueError:
632
+ return json.dumps(
633
+ {
634
+ "error": f"Invalid backend: {backend}. Must be 'rope' or 'pyright'.",
635
+ },
636
+ indent=2,
637
+ )
638
+
639
+ if tool:
640
+ if tool not in SHARED_TOOLS:
641
+ return json.dumps(
642
+ {
643
+ "error": f"Invalid tool: {tool}. Must be one of: {', '.join(SHARED_TOOLS)}",
644
+ },
645
+ indent=2,
646
+ )
647
+ config.set_backend(backend_enum, tool)
648
+ return json.dumps(
649
+ {
650
+ "success": True,
651
+ "message": f"Backend for '{tool}' set to '{backend}'",
652
+ "tool": tool,
653
+ "backend": backend,
654
+ },
655
+ indent=2,
656
+ )
657
+ else:
658
+ config.set_all_backends(backend_enum)
659
+ return json.dumps(
660
+ {
661
+ "success": True,
662
+ "message": f"Default backend set to '{backend}' for all shared tools",
663
+ "backend": backend,
664
+ "affected_tools": list(SHARED_TOOLS),
665
+ },
666
+ indent=2,
667
+ )
668
+
669
+
670
+ @mcp.tool()
671
+ def set_python_path(
672
+ python_path: str,
673
+ workspace: Optional[str] = None,
674
+ ) -> str:
675
+ """Set the Python interpreter path for code analysis.
676
+
677
+ This affects how Rope resolves imports and analyzes code.
678
+ The path is auto-detected from Pyright config or virtual environments,
679
+ but can be manually overridden using this tool.
680
+
681
+ Args:
682
+ python_path: Absolute path to the Python interpreter
683
+ workspace: Optional workspace to set the path for.
684
+ If not provided, sets the global default.
685
+
686
+ Returns:
687
+ JSON string with success status
688
+ """
689
+ result = config_set_python_path(python_path, workspace)
690
+ return json.dumps(result, indent=2)
691
+
692
+
693
+ @mcp.tool()
694
+ def status() -> str:
695
+ """Get the status of the MCP server.
696
+
697
+ Returns:
698
+ JSON string with server status information
699
+ """
700
+ rope_client = get_rope_client()
701
+ rope_status = rope_client.get_status()
702
+
703
+ config = get_config()
704
+ python_status = get_python_path_status()
705
+
706
+ # Get Python paths for active projects
707
+ active_projects = rope_status.get("active_projects", [])
708
+ project_python_paths = {}
709
+ for project in active_projects:
710
+ project_python_paths[project] = rope_client.get_python_path(project)
711
+
712
+ status_info = {
713
+ "server": "python-lsp-mcp",
714
+ "version": __version__,
715
+ "backends": {
716
+ "rope": {
717
+ "available": True,
718
+ "description": "Fast, Python-native code analysis",
719
+ "active_projects": active_projects,
720
+ "project_python_paths": project_python_paths,
721
+ "caching_enabled": rope_status.get("caching_enabled", False),
722
+ "cache_folder": rope_status.get("cache_folder"),
723
+ },
724
+ "pyright": {
725
+ "available": True,
726
+ "description": "Full-featured type checking via LSP",
727
+ },
728
+ "ripgrep": {
729
+ "available": True,
730
+ "description": "Fast regex search",
731
+ },
732
+ },
733
+ "config": {
734
+ "default_backend": config.default_backend.value,
735
+ "shared_tools": list(SHARED_TOOLS),
736
+ "tool_backends": {
737
+ tool: config.get_backend_for(tool).value for tool in SHARED_TOOLS
738
+ },
739
+ "rope_only_tools": ["rename"],
740
+ "pyright_only_tools": ["diagnostics", "signature_help"],
741
+ },
742
+ "python_interpreter": {
743
+ "current": python_status["current_interpreter"],
744
+ "global_override": python_status["global_python_path"],
745
+ "workspace_overrides": python_status["workspace_python_paths"],
746
+ },
747
+ }
748
+ return json.dumps(status_info, indent=2)
749
+
750
+
751
+ @mcp.tool()
752
+ def reload_modules() -> str:
753
+ """Reload all tool modules (development only).
754
+
755
+ This reloads the Python modules so code changes take effect without
756
+ restarting the server. Use this during development/debugging.
757
+
758
+ Returns:
759
+ JSON string with reload status
760
+ """
761
+ import importlib
762
+ from . import tools
763
+ from .tools import (
764
+ hover,
765
+ definition,
766
+ references,
767
+ completions,
768
+ symbols,
769
+ rename,
770
+ move,
771
+ change_signature,
772
+ diagnostics,
773
+ search,
774
+ )
775
+ from . import rope_client
776
+ from . import config
777
+ from .lsp import client as lsp_client
778
+
779
+ reloaded = []
780
+ errors = []
781
+
782
+ # Reload in dependency order
783
+ modules_to_reload = [
784
+ ("config", config),
785
+ ("rope_client", rope_client),
786
+ ("lsp.client", lsp_client),
787
+ ("tools.hover", hover),
788
+ ("tools.definition", definition),
789
+ ("tools.references", references),
790
+ ("tools.completions", completions),
791
+ ("tools.symbols", symbols),
792
+ ("tools.rename", rename),
793
+ ("tools.move", move),
794
+ ("tools.change_signature", change_signature),
795
+ ("tools.diagnostics", diagnostics),
796
+ ("tools.search", search),
797
+ ("tools", tools),
798
+ ]
799
+
800
+ for name, module in modules_to_reload:
801
+ try:
802
+ importlib.reload(module)
803
+ reloaded.append(name)
804
+ except Exception as e:
805
+ errors.append({"module": name, "error": str(e)})
806
+
807
+ # Re-import the functions we use
808
+ global do_rename, do_move, do_change_signature, get_function_signature
809
+ global rope_completions, rope_definition, rope_hover, rope_references, rope_symbols
810
+ global get_diagnostics, get_search
811
+
812
+ from .tools import (
813
+ do_rename,
814
+ do_move,
815
+ do_change_signature,
816
+ get_function_signature,
817
+ get_completions as rope_completions,
818
+ get_definition as rope_definition,
819
+ get_hover as rope_hover,
820
+ get_references as rope_references,
821
+ get_symbols as rope_symbols,
822
+ get_diagnostics,
823
+ get_search,
824
+ )
825
+
826
+ return json.dumps(
827
+ {
828
+ "success": len(errors) == 0,
829
+ "reloaded": reloaded,
830
+ "errors": errors if errors else None,
831
+ },
832
+ indent=2,
833
+ )
834
+
835
+
836
+ def _do_reload():
837
+ """Perform module reload."""
838
+ import importlib
839
+ from . import tools
840
+ from .tools import (
841
+ hover,
842
+ definition,
843
+ references,
844
+ completions,
845
+ symbols,
846
+ rename,
847
+ move,
848
+ change_signature,
849
+ diagnostics,
850
+ search,
851
+ )
852
+ from . import rope_client
853
+ from . import config
854
+ from .lsp import client as lsp_client
855
+
856
+ # Reload in dependency order
857
+ modules = [
858
+ config,
859
+ rope_client,
860
+ lsp_client,
861
+ hover,
862
+ definition,
863
+ references,
864
+ completions,
865
+ symbols,
866
+ rename,
867
+ move,
868
+ change_signature,
869
+ diagnostics,
870
+ search,
871
+ tools,
872
+ ]
873
+
874
+ for module in modules:
875
+ try:
876
+ importlib.reload(module)
877
+ except Exception as e:
878
+ print(f"Error reloading {module.__name__}: {e}", file=sys.stderr)
879
+
880
+ # Re-import the functions
881
+ global do_rename, do_move, do_change_signature, get_function_signature
882
+ global rope_completions, rope_definition, rope_hover, rope_references, rope_symbols
883
+ global get_diagnostics, get_search
884
+
885
+ from .tools import (
886
+ do_rename,
887
+ do_move,
888
+ do_change_signature,
889
+ get_function_signature,
890
+ get_completions as rope_completions,
891
+ get_definition as rope_definition,
892
+ get_hover as rope_hover,
893
+ get_references as rope_references,
894
+ get_symbols as rope_symbols,
895
+ get_diagnostics,
896
+ get_search,
897
+ )
898
+
899
+ print("Modules reloaded", file=sys.stderr)
900
+
901
+
902
+ def _start_file_watcher(src_path: str):
903
+ """Start watching for file changes and reload modules."""
904
+ try:
905
+ from watchdog.observers import Observer
906
+ from watchdog.events import FileSystemEventHandler, FileModifiedEvent
907
+ except ImportError:
908
+ print(
909
+ "watchdog not installed. Run: uv pip install watchdog",
910
+ file=sys.stderr,
911
+ )
912
+ return None
913
+
914
+ class ReloadHandler(FileSystemEventHandler):
915
+ def __init__(self):
916
+ self._debounce_timer = None
917
+ self._lock = threading.Lock()
918
+
919
+ def _schedule_reload(self):
920
+ with self._lock:
921
+ if self._debounce_timer:
922
+ self._debounce_timer.cancel()
923
+ self._debounce_timer = threading.Timer(0.5, self._do_reload)
924
+ self._debounce_timer.start()
925
+
926
+ def _do_reload(self):
927
+ try:
928
+ _do_reload()
929
+ except Exception as e:
930
+ print(f"Reload error: {e}", file=sys.stderr)
931
+
932
+ def on_modified(self, event):
933
+ if isinstance(event, FileModifiedEvent) and event.src_path.endswith(".py"):
934
+ print(f"File changed: {event.src_path}", file=sys.stderr)
935
+ self._schedule_reload()
936
+
937
+ observer = Observer()
938
+ observer.schedule(ReloadHandler(), src_path, recursive=True)
939
+ observer.start()
940
+ print(f"Watching for changes in: {src_path}", file=sys.stderr)
941
+ return observer
942
+
943
+
944
+ def main():
945
+ """Run the MCP server."""
946
+ reload_mode = "--reload" in sys.argv or os.environ.get("MCP_RELOAD") == "1"
947
+
948
+ observer = None
949
+ if reload_mode:
950
+ # Get the source path
951
+ src_path = os.path.dirname(os.path.abspath(__file__))
952
+ observer = _start_file_watcher(src_path)
953
+
954
+ try:
955
+ mcp.run()
956
+ finally:
957
+ if observer:
958
+ observer.stop()
959
+ observer.join()
960
+
961
+
962
+ if __name__ == "__main__":
963
+ main()