@treedy/lsp-mcp 0.1.8 → 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.
- package/dist/bundled/python/src/rope_mcp/config.py +50 -14
- package/dist/bundled/python/src/rope_mcp/lsp/client.py +243 -4
- package/dist/bundled/python/src/rope_mcp/lsp/types.py +1 -0
- package/dist/bundled/python/src/rope_mcp/server.py +331 -77
- package/dist/bundled/python/src/rope_mcp/tools/__init__.py +0 -2
- package/dist/bundled/typescript/dist/index.js +6129 -5891
- package/dist/bundled/typescript/dist/index.js.map +5 -5
- package/dist/bundled/vue/dist/index.js +136 -71
- package/dist/bundled/vue/dist/vue-service.d.ts +16 -0
- package/dist/index.js +567 -314
- package/dist/index.js.map +6 -6
- package/package.json +1 -1
- 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/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/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
|
@@ -24,7 +24,7 @@ from .config import (
|
|
|
24
24
|
get_python_path_status,
|
|
25
25
|
set_active_workspace,
|
|
26
26
|
get_active_workspace,
|
|
27
|
-
|
|
27
|
+
resolve_file_path,
|
|
28
28
|
)
|
|
29
29
|
from .rope_client import get_client as get_rope_client
|
|
30
30
|
from .lsp import get_lsp_client, close_all_clients, close_all_clients as close_lsp_clients
|
|
@@ -38,7 +38,6 @@ from .tools import (
|
|
|
38
38
|
get_hover as rope_hover,
|
|
39
39
|
get_references as rope_references,
|
|
40
40
|
get_symbols as rope_symbols,
|
|
41
|
-
get_diagnostics,
|
|
42
41
|
get_search,
|
|
43
42
|
)
|
|
44
43
|
|
|
@@ -120,7 +119,7 @@ def hover(
|
|
|
120
119
|
"""Get documentation for the symbol at the given position.
|
|
121
120
|
|
|
122
121
|
Args:
|
|
123
|
-
file:
|
|
122
|
+
file: Path to the Python file (absolute or relative to active workspace)
|
|
124
123
|
line: 1-based line number
|
|
125
124
|
column: 1-based column number
|
|
126
125
|
backend: Backend to use (rope/pyright). Default: from config or 'rope'
|
|
@@ -128,8 +127,8 @@ def hover(
|
|
|
128
127
|
Returns:
|
|
129
128
|
JSON string with documentation or error message
|
|
130
129
|
"""
|
|
131
|
-
#
|
|
132
|
-
error =
|
|
130
|
+
# Resolve and validate path
|
|
131
|
+
abs_file, error = resolve_file_path(file)
|
|
133
132
|
if error:
|
|
134
133
|
return json.dumps(error, indent=2)
|
|
135
134
|
|
|
@@ -137,9 +136,9 @@ def hover(
|
|
|
137
136
|
|
|
138
137
|
if effective_backend == Backend.PYRIGHT:
|
|
139
138
|
try:
|
|
140
|
-
workspace = _find_workspace(
|
|
139
|
+
workspace = _find_workspace(abs_file)
|
|
141
140
|
client = get_lsp_client(workspace)
|
|
142
|
-
result = client.hover(
|
|
141
|
+
result = client.hover(abs_file, line, column)
|
|
143
142
|
if result:
|
|
144
143
|
return json.dumps(
|
|
145
144
|
{"contents": result.get("contents", ""), "backend": "pyright"},
|
|
@@ -152,7 +151,7 @@ def hover(
|
|
|
152
151
|
except Exception as e:
|
|
153
152
|
return json.dumps({"error": str(e), "backend": "pyright"}, indent=2)
|
|
154
153
|
else:
|
|
155
|
-
result = rope_hover(
|
|
154
|
+
result = rope_hover(abs_file, line, column)
|
|
156
155
|
result["backend"] = "rope"
|
|
157
156
|
return json.dumps(result, indent=2)
|
|
158
157
|
|
|
@@ -167,7 +166,7 @@ def definition(
|
|
|
167
166
|
"""Get the definition location for the symbol at the given position.
|
|
168
167
|
|
|
169
168
|
Args:
|
|
170
|
-
file:
|
|
169
|
+
file: Path to the Python file (absolute or relative to active workspace)
|
|
171
170
|
line: 1-based line number
|
|
172
171
|
column: 1-based column number
|
|
173
172
|
backend: Backend to use (rope/pyright). Default: from config or 'rope'
|
|
@@ -175,8 +174,8 @@ def definition(
|
|
|
175
174
|
Returns:
|
|
176
175
|
JSON string with definition location or error message
|
|
177
176
|
"""
|
|
178
|
-
#
|
|
179
|
-
error =
|
|
177
|
+
# Resolve and validate path
|
|
178
|
+
abs_file, error = resolve_file_path(file)
|
|
180
179
|
if error:
|
|
181
180
|
return json.dumps(error, indent=2)
|
|
182
181
|
|
|
@@ -184,9 +183,9 @@ def definition(
|
|
|
184
183
|
|
|
185
184
|
if effective_backend == Backend.PYRIGHT:
|
|
186
185
|
try:
|
|
187
|
-
workspace = _find_workspace(
|
|
186
|
+
workspace = _find_workspace(abs_file)
|
|
188
187
|
client = get_lsp_client(workspace)
|
|
189
|
-
locations = client.definition(
|
|
188
|
+
locations = client.definition(abs_file, line, column)
|
|
190
189
|
if locations:
|
|
191
190
|
result = locations[0]
|
|
192
191
|
result["backend"] = "pyright"
|
|
@@ -198,7 +197,7 @@ def definition(
|
|
|
198
197
|
except Exception as e:
|
|
199
198
|
return json.dumps({"error": str(e), "backend": "pyright"}, indent=2)
|
|
200
199
|
else:
|
|
201
|
-
result = rope_definition(
|
|
200
|
+
result = rope_definition(abs_file, line, column)
|
|
202
201
|
result["backend"] = "rope"
|
|
203
202
|
return json.dumps(result, indent=2)
|
|
204
203
|
|
|
@@ -213,7 +212,7 @@ def references(
|
|
|
213
212
|
"""Find all references to the symbol at the given position.
|
|
214
213
|
|
|
215
214
|
Args:
|
|
216
|
-
file:
|
|
215
|
+
file: Path to the Python file (absolute or relative to active workspace)
|
|
217
216
|
line: 1-based line number
|
|
218
217
|
column: 1-based column number
|
|
219
218
|
backend: Backend to use (rope/pyright). Default: from config or 'rope'
|
|
@@ -221,8 +220,8 @@ def references(
|
|
|
221
220
|
Returns:
|
|
222
221
|
JSON string with list of references or error message
|
|
223
222
|
"""
|
|
224
|
-
#
|
|
225
|
-
error =
|
|
223
|
+
# Resolve and validate path
|
|
224
|
+
abs_file, error = resolve_file_path(file)
|
|
226
225
|
if error:
|
|
227
226
|
return json.dumps(error, indent=2)
|
|
228
227
|
|
|
@@ -230,16 +229,16 @@ def references(
|
|
|
230
229
|
|
|
231
230
|
if effective_backend == Backend.PYRIGHT:
|
|
232
231
|
try:
|
|
233
|
-
workspace = _find_workspace(
|
|
232
|
+
workspace = _find_workspace(abs_file)
|
|
234
233
|
client = get_lsp_client(workspace)
|
|
235
|
-
refs = client.references(
|
|
234
|
+
refs = client.references(abs_file, line, column)
|
|
236
235
|
return json.dumps(
|
|
237
236
|
{"references": refs, "count": len(refs), "backend": "pyright"}, indent=2
|
|
238
237
|
)
|
|
239
238
|
except Exception as e:
|
|
240
239
|
return json.dumps({"error": str(e), "backend": "pyright"}, indent=2)
|
|
241
240
|
else:
|
|
242
|
-
result = rope_references(
|
|
241
|
+
result = rope_references(abs_file, line, column)
|
|
243
242
|
result["backend"] = "rope"
|
|
244
243
|
return json.dumps(result, indent=2)
|
|
245
244
|
|
|
@@ -254,7 +253,7 @@ def completions(
|
|
|
254
253
|
"""Get code completion suggestions at the given position.
|
|
255
254
|
|
|
256
255
|
Args:
|
|
257
|
-
file:
|
|
256
|
+
file: Path to the Python file (absolute or relative to active workspace)
|
|
258
257
|
line: 1-based line number
|
|
259
258
|
column: 1-based column number
|
|
260
259
|
backend: Backend to use (rope/pyright). Default: from config or 'rope'
|
|
@@ -262,8 +261,8 @@ def completions(
|
|
|
262
261
|
Returns:
|
|
263
262
|
JSON string with completion items or error message
|
|
264
263
|
"""
|
|
265
|
-
#
|
|
266
|
-
error =
|
|
264
|
+
# Resolve and validate path
|
|
265
|
+
abs_file, error = resolve_file_path(file)
|
|
267
266
|
if error:
|
|
268
267
|
return json.dumps(error, indent=2)
|
|
269
268
|
|
|
@@ -271,9 +270,9 @@ def completions(
|
|
|
271
270
|
|
|
272
271
|
if effective_backend == Backend.PYRIGHT:
|
|
273
272
|
try:
|
|
274
|
-
workspace = _find_workspace(
|
|
273
|
+
workspace = _find_workspace(abs_file)
|
|
275
274
|
client = get_lsp_client(workspace)
|
|
276
|
-
items = client.completions(
|
|
275
|
+
items = client.completions(abs_file, line, column)
|
|
277
276
|
return json.dumps(
|
|
278
277
|
{"completions": items, "count": len(items), "backend": "pyright"},
|
|
279
278
|
indent=2,
|
|
@@ -281,7 +280,7 @@ def completions(
|
|
|
281
280
|
except Exception as e:
|
|
282
281
|
return json.dumps({"error": str(e), "backend": "pyright"}, indent=2)
|
|
283
282
|
else:
|
|
284
|
-
result = rope_completions(
|
|
283
|
+
result = rope_completions(abs_file, line, column)
|
|
285
284
|
result["backend"] = "rope"
|
|
286
285
|
return json.dumps(result, indent=2)
|
|
287
286
|
|
|
@@ -295,15 +294,15 @@ def symbols(
|
|
|
295
294
|
"""Get symbols from a Python file.
|
|
296
295
|
|
|
297
296
|
Args:
|
|
298
|
-
file:
|
|
297
|
+
file: Path to the Python file (absolute or relative to active workspace)
|
|
299
298
|
query: Optional filter query for symbol names
|
|
300
299
|
backend: Backend to use (rope/pyright). Default: from config or 'rope'
|
|
301
300
|
|
|
302
301
|
Returns:
|
|
303
302
|
JSON string with list of symbols or error message
|
|
304
303
|
"""
|
|
305
|
-
#
|
|
306
|
-
error =
|
|
304
|
+
# Resolve and validate path
|
|
305
|
+
abs_file, error = resolve_file_path(file)
|
|
307
306
|
if error:
|
|
308
307
|
return json.dumps(error, indent=2)
|
|
309
308
|
|
|
@@ -311,9 +310,9 @@ def symbols(
|
|
|
311
310
|
|
|
312
311
|
if effective_backend == Backend.PYRIGHT:
|
|
313
312
|
try:
|
|
314
|
-
workspace = _find_workspace(
|
|
313
|
+
workspace = _find_workspace(abs_file)
|
|
315
314
|
client = get_lsp_client(workspace)
|
|
316
|
-
syms = client.document_symbols(
|
|
315
|
+
syms = client.document_symbols(abs_file)
|
|
317
316
|
# Filter by query if provided
|
|
318
317
|
if query:
|
|
319
318
|
query_lower = query.lower()
|
|
@@ -322,7 +321,7 @@ def symbols(
|
|
|
322
321
|
{
|
|
323
322
|
"symbols": syms,
|
|
324
323
|
"count": len(syms),
|
|
325
|
-
"file":
|
|
324
|
+
"file": abs_file,
|
|
326
325
|
"backend": "pyright",
|
|
327
326
|
},
|
|
328
327
|
indent=2,
|
|
@@ -330,7 +329,7 @@ def symbols(
|
|
|
330
329
|
except Exception as e:
|
|
331
330
|
return json.dumps({"error": str(e), "backend": "pyright"}, indent=2)
|
|
332
331
|
else:
|
|
333
|
-
result = rope_symbols(
|
|
332
|
+
result = rope_symbols(abs_file, query)
|
|
334
333
|
result["backend"] = "rope"
|
|
335
334
|
return json.dumps(result, indent=2)
|
|
336
335
|
|
|
@@ -340,10 +339,9 @@ def rename(file: str, line: int, column: int, new_name: str) -> str:
|
|
|
340
339
|
"""Rename the symbol at the given position.
|
|
341
340
|
|
|
342
341
|
This will modify files on disk to rename all occurrences of the symbol.
|
|
343
|
-
|
|
344
|
-
|
|
342
|
+
|
|
345
343
|
Args:
|
|
346
|
-
file:
|
|
344
|
+
file: Path to the Python file (absolute or relative to active workspace)
|
|
347
345
|
line: 1-based line number
|
|
348
346
|
column: 1-based column number
|
|
349
347
|
new_name: The new name for the symbol
|
|
@@ -351,14 +349,44 @@ def rename(file: str, line: int, column: int, new_name: str) -> str:
|
|
|
351
349
|
Returns:
|
|
352
350
|
JSON string with changes made or error message
|
|
353
351
|
"""
|
|
354
|
-
#
|
|
355
|
-
error =
|
|
352
|
+
# Resolve and validate path
|
|
353
|
+
abs_file, error = resolve_file_path(file)
|
|
356
354
|
if error:
|
|
357
355
|
return json.dumps(error, indent=2)
|
|
358
356
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
357
|
+
# Determine backend (Pyright is now supported for rename)
|
|
358
|
+
# Since we moved rename to SHARED_TOOLS, we can use config logic
|
|
359
|
+
effective_backend = get_config().get_backend_for("rename")
|
|
360
|
+
|
|
361
|
+
if effective_backend == Backend.PYRIGHT:
|
|
362
|
+
try:
|
|
363
|
+
workspace = _find_workspace(abs_file)
|
|
364
|
+
client = get_lsp_client(workspace)
|
|
365
|
+
edit = client.rename(abs_file, line, column, new_name)
|
|
366
|
+
|
|
367
|
+
if not edit:
|
|
368
|
+
return json.dumps({
|
|
369
|
+
"error": "Rename returned no changes. Symbol might not be renamable or Pyright failed.",
|
|
370
|
+
"backend": "pyright"
|
|
371
|
+
}, indent=2)
|
|
372
|
+
|
|
373
|
+
# Reuse the helper from run_code_action
|
|
374
|
+
_apply_workspace_edit(edit)
|
|
375
|
+
|
|
376
|
+
return json.dumps({
|
|
377
|
+
"success": True,
|
|
378
|
+
"message": f"Renamed symbol to '{new_name}'",
|
|
379
|
+
"backend": "pyright",
|
|
380
|
+
"changes": edit.get("changes") or edit.get("documentChanges")
|
|
381
|
+
}, indent=2)
|
|
382
|
+
|
|
383
|
+
except Exception as e:
|
|
384
|
+
return json.dumps({"error": str(e), "backend": "pyright"}, indent=2)
|
|
385
|
+
else:
|
|
386
|
+
# Rope fallback
|
|
387
|
+
result = do_rename(abs_file, line, column, new_name)
|
|
388
|
+
result["backend"] = "rope"
|
|
389
|
+
return json.dumps(result, indent=2)
|
|
362
390
|
|
|
363
391
|
|
|
364
392
|
@mcp.tool()
|
|
@@ -375,7 +403,7 @@ def move(
|
|
|
375
403
|
Uses Rope backend for refactoring.
|
|
376
404
|
|
|
377
405
|
Args:
|
|
378
|
-
file:
|
|
406
|
+
file: Path to the Python file containing the symbol
|
|
379
407
|
line: 1-based line number of the symbol to move
|
|
380
408
|
column: 1-based column number of the symbol
|
|
381
409
|
destination: Destination module path (e.g., "mypackage.utils" or "utils.py")
|
|
@@ -384,12 +412,12 @@ def move(
|
|
|
384
412
|
Returns:
|
|
385
413
|
JSON string with changes made or error message
|
|
386
414
|
"""
|
|
387
|
-
#
|
|
388
|
-
error =
|
|
415
|
+
# Resolve and validate path
|
|
416
|
+
abs_file, error = resolve_file_path(file)
|
|
389
417
|
if error:
|
|
390
418
|
return json.dumps(error, indent=2)
|
|
391
419
|
|
|
392
|
-
result = do_move(
|
|
420
|
+
result = do_move(abs_file, line, column, destination, resources_only=preview)
|
|
393
421
|
result["backend"] = "rope"
|
|
394
422
|
return json.dumps(result, indent=2)
|
|
395
423
|
|
|
@@ -412,7 +440,7 @@ def change_signature(
|
|
|
412
440
|
Uses Rope backend for refactoring.
|
|
413
441
|
|
|
414
442
|
Args:
|
|
415
|
-
file:
|
|
443
|
+
file: Path to the Python file (absolute or relative to active workspace)
|
|
416
444
|
line: 1-based line number of the function
|
|
417
445
|
column: 1-based column number of the function
|
|
418
446
|
new_params: New parameter order, e.g. ["self", "b", "a"] to reorder
|
|
@@ -435,8 +463,8 @@ def change_signature(
|
|
|
435
463
|
# Remove param: def foo(a, b) -> def foo(a)
|
|
436
464
|
change_signature(file, line, col, remove_param="b")
|
|
437
465
|
"""
|
|
438
|
-
#
|
|
439
|
-
error =
|
|
466
|
+
# Resolve and validate path
|
|
467
|
+
abs_file, error = resolve_file_path(file)
|
|
440
468
|
if error:
|
|
441
469
|
return json.dumps(error, indent=2)
|
|
442
470
|
|
|
@@ -450,7 +478,7 @@ def change_signature(
|
|
|
450
478
|
}
|
|
451
479
|
|
|
452
480
|
result = do_change_signature(
|
|
453
|
-
|
|
481
|
+
abs_file,
|
|
454
482
|
line,
|
|
455
483
|
column,
|
|
456
484
|
new_params=new_params,
|
|
@@ -469,43 +497,63 @@ def function_signature(file: str, line: int, column: int) -> str:
|
|
|
469
497
|
Useful for inspecting function parameters before changing the signature.
|
|
470
498
|
|
|
471
499
|
Args:
|
|
472
|
-
file:
|
|
500
|
+
file: Path to the Python file (absolute or relative to active workspace)
|
|
473
501
|
line: 1-based line number of the function
|
|
474
502
|
column: 1-based column number of the function
|
|
475
503
|
|
|
476
504
|
Returns:
|
|
477
505
|
JSON string with function signature info
|
|
478
506
|
"""
|
|
479
|
-
#
|
|
480
|
-
error =
|
|
507
|
+
# Resolve and validate path
|
|
508
|
+
abs_file, error = resolve_file_path(file)
|
|
481
509
|
if error:
|
|
482
510
|
return json.dumps(error, indent=2)
|
|
483
511
|
|
|
484
|
-
result = get_function_signature(
|
|
512
|
+
result = get_function_signature(abs_file, line, column)
|
|
485
513
|
result["backend"] = "rope"
|
|
486
514
|
return json.dumps(result, indent=2)
|
|
487
515
|
|
|
488
516
|
|
|
489
517
|
@mcp.tool()
|
|
490
518
|
def diagnostics(path: str) -> str:
|
|
491
|
-
"""Get type errors and warnings for a Python file
|
|
519
|
+
"""Get type errors and warnings for a Python file.
|
|
492
520
|
|
|
493
|
-
Uses Pyright for type checking.
|
|
521
|
+
Uses Pyright LSP for type checking.
|
|
494
522
|
|
|
495
523
|
Args:
|
|
496
|
-
path:
|
|
524
|
+
path: Path to a Python file (absolute or relative to active workspace)
|
|
497
525
|
|
|
498
526
|
Returns:
|
|
499
527
|
JSON string with diagnostics or error message
|
|
500
528
|
"""
|
|
501
|
-
#
|
|
502
|
-
error =
|
|
529
|
+
# Resolve and validate path
|
|
530
|
+
abs_path, error = resolve_file_path(path)
|
|
503
531
|
if error:
|
|
504
532
|
return json.dumps(error, indent=2)
|
|
505
533
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
534
|
+
try:
|
|
535
|
+
workspace = _find_workspace(abs_path)
|
|
536
|
+
client = get_lsp_client(workspace)
|
|
537
|
+
result = client.get_diagnostics(abs_path)
|
|
538
|
+
|
|
539
|
+
# Check for config file to warn about permissive defaults
|
|
540
|
+
has_config = False
|
|
541
|
+
for cfg in ["pyrightconfig.json", "pyproject.toml"]:
|
|
542
|
+
if os.path.exists(os.path.join(workspace, cfg)):
|
|
543
|
+
has_config = True
|
|
544
|
+
break
|
|
545
|
+
|
|
546
|
+
if not has_config:
|
|
547
|
+
if "summary" not in result:
|
|
548
|
+
result["summary"] = {}
|
|
549
|
+
result["summary"]["note"] = (
|
|
550
|
+
"No 'pyrightconfig.json' or 'pyproject.toml' found. "
|
|
551
|
+
"Using permissive defaults. Create a config file to enable stricter checks (e.g. unused imports)."
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
return json.dumps(result, indent=2)
|
|
555
|
+
except Exception as e:
|
|
556
|
+
return json.dumps({"error": str(e), "backend": "pyright-lsp"}, indent=2)
|
|
509
557
|
|
|
510
558
|
|
|
511
559
|
@mcp.tool()
|
|
@@ -515,22 +563,22 @@ def signature_help(file: str, line: int, column: int) -> str:
|
|
|
515
563
|
Uses Pyright backend for accurate signature information.
|
|
516
564
|
|
|
517
565
|
Args:
|
|
518
|
-
file:
|
|
566
|
+
file: Path to the Python file (absolute or relative to active workspace)
|
|
519
567
|
line: 1-based line number
|
|
520
568
|
column: 1-based column number
|
|
521
569
|
|
|
522
570
|
Returns:
|
|
523
571
|
JSON string with signature help or error message
|
|
524
572
|
"""
|
|
525
|
-
#
|
|
526
|
-
error =
|
|
573
|
+
# Resolve and validate path
|
|
574
|
+
abs_file, error = resolve_file_path(file)
|
|
527
575
|
if error:
|
|
528
576
|
return json.dumps(error, indent=2)
|
|
529
577
|
|
|
530
578
|
try:
|
|
531
|
-
workspace = _find_workspace(
|
|
579
|
+
workspace = _find_workspace(abs_file)
|
|
532
580
|
client = get_lsp_client(workspace)
|
|
533
|
-
result = client.signature_help(
|
|
581
|
+
result = client.signature_help(abs_file, line, column)
|
|
534
582
|
if result:
|
|
535
583
|
result["backend"] = "pyright"
|
|
536
584
|
return json.dumps(result, indent=2)
|
|
@@ -549,29 +597,227 @@ def update_document(file: str, content: str) -> str:
|
|
|
549
597
|
Uses Pyright backend for incremental updates.
|
|
550
598
|
|
|
551
599
|
Args:
|
|
552
|
-
file:
|
|
600
|
+
file: Path to the Python file (absolute or relative to active workspace)
|
|
553
601
|
content: New file content
|
|
554
602
|
|
|
555
603
|
Returns:
|
|
556
604
|
JSON string with confirmation
|
|
557
605
|
"""
|
|
558
|
-
#
|
|
559
|
-
error =
|
|
606
|
+
# Resolve and validate path
|
|
607
|
+
abs_file, error = resolve_file_path(file)
|
|
560
608
|
if error:
|
|
561
609
|
return json.dumps(error, indent=2)
|
|
562
610
|
|
|
563
611
|
try:
|
|
564
|
-
workspace = _find_workspace(
|
|
612
|
+
workspace = _find_workspace(abs_file)
|
|
565
613
|
client = get_lsp_client(workspace)
|
|
566
|
-
client.update_document(
|
|
614
|
+
client.update_document(abs_file, content)
|
|
567
615
|
return json.dumps(
|
|
568
|
-
{"success": True, "file":
|
|
616
|
+
{"success": True, "file": abs_file, "backend": "pyright"}, indent=2
|
|
569
617
|
)
|
|
570
618
|
except Exception as e:
|
|
571
619
|
return json.dumps({"error": str(e), "backend": "pyright"}, indent=2)
|
|
572
620
|
|
|
573
621
|
|
|
574
622
|
|
|
623
|
+
@mcp.tool()
|
|
624
|
+
def inlay_hints(file: str) -> str:
|
|
625
|
+
"""Get inlay hints (type annotations, parameter names) for a file.
|
|
626
|
+
|
|
627
|
+
Args:
|
|
628
|
+
file: Path to the Python file
|
|
629
|
+
|
|
630
|
+
Returns:
|
|
631
|
+
JSON string with list of hints
|
|
632
|
+
"""
|
|
633
|
+
# Resolve and validate path
|
|
634
|
+
abs_file, error = resolve_file_path(file)
|
|
635
|
+
if error:
|
|
636
|
+
return json.dumps(error, indent=2)
|
|
637
|
+
|
|
638
|
+
try:
|
|
639
|
+
workspace = _find_workspace(abs_file)
|
|
640
|
+
client = get_lsp_client(workspace)
|
|
641
|
+
|
|
642
|
+
# Read file to get line count
|
|
643
|
+
with open(abs_file, "r", encoding="utf-8") as f:
|
|
644
|
+
lines = f.readlines()
|
|
645
|
+
line_count = len(lines)
|
|
646
|
+
last_col = len(lines[-1]) if lines else 0
|
|
647
|
+
|
|
648
|
+
hints = client.inlay_hint(abs_file, 1, 1, line_count + 1, last_col + 1)
|
|
649
|
+
|
|
650
|
+
return json.dumps({"hints": hints, "count": len(hints)}, indent=2)
|
|
651
|
+
|
|
652
|
+
except Exception as e:
|
|
653
|
+
return json.dumps({"error": str(e), "backend": "pyright"}, indent=2)
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
@mcp.tool()
|
|
657
|
+
def code_action(file: str, line: int, column: int) -> str:
|
|
658
|
+
"""Get available code actions (Quick Fixes and Refactorings).
|
|
659
|
+
|
|
660
|
+
Args:
|
|
661
|
+
file: Path to the Python file
|
|
662
|
+
line: 1-based line number
|
|
663
|
+
column: 1-based column number
|
|
664
|
+
|
|
665
|
+
Returns:
|
|
666
|
+
JSON string with list of actions
|
|
667
|
+
"""
|
|
668
|
+
# Resolve and validate path
|
|
669
|
+
abs_file, error = resolve_file_path(file)
|
|
670
|
+
if error:
|
|
671
|
+
return json.dumps(error, indent=2)
|
|
672
|
+
|
|
673
|
+
try:
|
|
674
|
+
workspace = _find_workspace(abs_file)
|
|
675
|
+
client = get_lsp_client(workspace)
|
|
676
|
+
|
|
677
|
+
# 1. Get diagnostics to provide context
|
|
678
|
+
# In a real IDE, we'd pass active diagnostics. Here we fetch them.
|
|
679
|
+
# Note: This might be slow if we do full check.
|
|
680
|
+
# For Pyright, we can pass empty diagnostics context and it might still return some actions,
|
|
681
|
+
# but usually it needs context.
|
|
682
|
+
# Let's try to get diagnostics for this file first.
|
|
683
|
+
# We can't easily get diagnostics without running the full check which is slow.
|
|
684
|
+
# For now, let's pass empty list and see what Pyright gives (e.g. Organize Imports usually works).
|
|
685
|
+
|
|
686
|
+
# Actually, let's try to fetch diagnostics if possible, or just pass context.
|
|
687
|
+
diagnostics = [] # Placeholder
|
|
688
|
+
|
|
689
|
+
actions = client.code_action(abs_file, line, column, line, column, diagnostics)
|
|
690
|
+
|
|
691
|
+
# Format for MCP
|
|
692
|
+
formatted = []
|
|
693
|
+
for action in actions:
|
|
694
|
+
formatted.append({
|
|
695
|
+
"title": action.get("title", "Unknown Action"),
|
|
696
|
+
"kind": action.get("kind", "quickfix"),
|
|
697
|
+
"command": action.get("command"),
|
|
698
|
+
"edit": action.get("edit"),
|
|
699
|
+
# We need to store enough info to run it
|
|
700
|
+
"data": action
|
|
701
|
+
})
|
|
702
|
+
|
|
703
|
+
return json.dumps({"actions": formatted, "count": len(formatted)}, indent=2)
|
|
704
|
+
|
|
705
|
+
except Exception as e:
|
|
706
|
+
return json.dumps({"error": str(e), "backend": "pyright"}, indent=2)
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
@mcp.tool()
|
|
710
|
+
def run_code_action(
|
|
711
|
+
file: str,
|
|
712
|
+
line: int,
|
|
713
|
+
column: int,
|
|
714
|
+
title: str
|
|
715
|
+
) -> str:
|
|
716
|
+
"""Run a specific code action.
|
|
717
|
+
|
|
718
|
+
Args:
|
|
719
|
+
file: Path to the Python file
|
|
720
|
+
line: 1-based line number
|
|
721
|
+
column: 1-based column number
|
|
722
|
+
title: The title of the action to run (must match one from code_action)
|
|
723
|
+
|
|
724
|
+
Returns:
|
|
725
|
+
JSON string with result
|
|
726
|
+
"""
|
|
727
|
+
# Resolve and validate path
|
|
728
|
+
abs_file, error = resolve_file_path(file)
|
|
729
|
+
if error:
|
|
730
|
+
return json.dumps(error, indent=2)
|
|
731
|
+
|
|
732
|
+
try:
|
|
733
|
+
workspace = _find_workspace(abs_file)
|
|
734
|
+
client = get_lsp_client(workspace)
|
|
735
|
+
|
|
736
|
+
# We need to fetch actions again to find the matching one and its edit/command
|
|
737
|
+
actions = client.code_action(abs_file, line, column, line, column, [])
|
|
738
|
+
|
|
739
|
+
target_action = next((a for a in actions if a.get("title") == title), None)
|
|
740
|
+
if not target_action:
|
|
741
|
+
return json.dumps({"error": f"Action '{title}' not found. It may have expired."}, indent=2)
|
|
742
|
+
|
|
743
|
+
# Handle WorkspaceEdit
|
|
744
|
+
if "edit" in target_action:
|
|
745
|
+
_apply_workspace_edit(target_action["edit"])
|
|
746
|
+
return json.dumps({"success": True, "message": "Applied workspace edit"}, indent=2)
|
|
747
|
+
|
|
748
|
+
# Handle Command
|
|
749
|
+
if "command" in target_action:
|
|
750
|
+
# If command has arguments, we might need to execute it via LSP workspace/executeCommand
|
|
751
|
+
cmd = target_action["command"]
|
|
752
|
+
if isinstance(cmd, dict): # Command object
|
|
753
|
+
command_name = cmd["command"]
|
|
754
|
+
arguments = cmd.get("arguments", [])
|
|
755
|
+
|
|
756
|
+
# Execute command via LSP
|
|
757
|
+
# We need to add execute_command to LspClient if not exists
|
|
758
|
+
# client.execute_command(command_name, arguments)
|
|
759
|
+
# For now, let's just say we don't support custom commands yet unless we add that method.
|
|
760
|
+
# Pyright's organize imports is a command.
|
|
761
|
+
return json.dumps({"error": "Command execution not yet supported", "command": command_name}, indent=2)
|
|
762
|
+
|
|
763
|
+
return json.dumps({"success": True, "message": "Action executed (no-op?)"}, indent=2)
|
|
764
|
+
|
|
765
|
+
except Exception as e:
|
|
766
|
+
return json.dumps({"error": str(e), "backend": "pyright"}, indent=2)
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def _apply_workspace_edit(edit: dict) -> None:
|
|
770
|
+
"""Apply a WorkspaceEdit to files on disk."""
|
|
771
|
+
changes = edit.get("changes")
|
|
772
|
+
document_changes = edit.get("documentChanges")
|
|
773
|
+
|
|
774
|
+
# Normalize to dict of uri -> edits
|
|
775
|
+
all_edits = {}
|
|
776
|
+
|
|
777
|
+
if changes:
|
|
778
|
+
all_edits.update(changes)
|
|
779
|
+
|
|
780
|
+
if document_changes:
|
|
781
|
+
for change in document_changes:
|
|
782
|
+
# Check if it's TextDocumentEdit (has textDocument and edits)
|
|
783
|
+
if "textDocument" in change and "edits" in change:
|
|
784
|
+
uri = change["textDocument"]["uri"]
|
|
785
|
+
if uri not in all_edits:
|
|
786
|
+
all_edits[uri] = []
|
|
787
|
+
all_edits[uri].extend(change["edits"])
|
|
788
|
+
# TODO: Handle CreateFile, RenameFile, DeleteFile if needed
|
|
789
|
+
|
|
790
|
+
for uri, text_edits in all_edits.items():
|
|
791
|
+
file_path = uri.replace("file://", "")
|
|
792
|
+
if sys.platform == "win32" and file_path.startswith("/"):
|
|
793
|
+
file_path = file_path[1:]
|
|
794
|
+
|
|
795
|
+
if not os.path.exists(file_path):
|
|
796
|
+
continue
|
|
797
|
+
|
|
798
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
799
|
+
content = f.read()
|
|
800
|
+
|
|
801
|
+
# Apply edits in reverse order
|
|
802
|
+
# Sort edits by start position descending
|
|
803
|
+
text_edits.sort(key=lambda e: (e["range"]["start"]["line"], e["range"]["start"]["character"]), reverse=True)
|
|
804
|
+
|
|
805
|
+
client = get_rope_client() # Use rope client for offset conversion helper
|
|
806
|
+
|
|
807
|
+
for text_edit in text_edits:
|
|
808
|
+
start = text_edit["range"]["start"]
|
|
809
|
+
end = text_edit["range"]["end"]
|
|
810
|
+
new_text = text_edit["newText"]
|
|
811
|
+
|
|
812
|
+
start_offset = client.position_to_offset(content, start["line"] + 1, start["character"] + 1)
|
|
813
|
+
end_offset = client.position_to_offset(content, end["line"] + 1, end["character"] + 1)
|
|
814
|
+
|
|
815
|
+
content = content[:start_offset] + new_text + content[end_offset:]
|
|
816
|
+
|
|
817
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
818
|
+
f.write(content)
|
|
819
|
+
|
|
820
|
+
|
|
575
821
|
@mcp.tool()
|
|
576
822
|
def search(
|
|
577
823
|
pattern: str,
|
|
@@ -584,7 +830,7 @@ def search(
|
|
|
584
830
|
|
|
585
831
|
Args:
|
|
586
832
|
pattern: The regex pattern to search for
|
|
587
|
-
path: Directory or file to search in (defaults to current working directory)
|
|
833
|
+
path: Directory or file to search in (defaults to current working directory). Can be relative to active workspace.
|
|
588
834
|
glob: Glob pattern to filter files (e.g., "*.py", "**/*.ts")
|
|
589
835
|
case_sensitive: Whether the search is case sensitive
|
|
590
836
|
max_results: Maximum number of results to return
|
|
@@ -592,15 +838,23 @@ def search(
|
|
|
592
838
|
Returns:
|
|
593
839
|
JSON string with search results or error message
|
|
594
840
|
"""
|
|
595
|
-
#
|
|
596
|
-
|
|
597
|
-
|
|
841
|
+
# Resolve and validate path if provided
|
|
842
|
+
search_path = path
|
|
843
|
+
if search_path:
|
|
844
|
+
abs_path, error = resolve_file_path(search_path)
|
|
598
845
|
if error:
|
|
599
846
|
return json.dumps(error, indent=2)
|
|
847
|
+
search_path = abs_path
|
|
848
|
+
|
|
849
|
+
# If no path provided, use active workspace if available
|
|
850
|
+
if not search_path:
|
|
851
|
+
active = get_active_workspace()
|
|
852
|
+
if active:
|
|
853
|
+
search_path = active
|
|
600
854
|
|
|
601
855
|
result = get_search(
|
|
602
856
|
pattern=pattern,
|
|
603
|
-
path=
|
|
857
|
+
path=search_path,
|
|
604
858
|
glob=glob,
|
|
605
859
|
case_sensitive=case_sensitive,
|
|
606
860
|
max_results=max_results,
|