@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.
Files changed (34) hide show
  1. package/dist/bundled/python/src/rope_mcp/config.py +50 -14
  2. package/dist/bundled/python/src/rope_mcp/lsp/client.py +243 -4
  3. package/dist/bundled/python/src/rope_mcp/lsp/types.py +1 -0
  4. package/dist/bundled/python/src/rope_mcp/server.py +331 -77
  5. package/dist/bundled/python/src/rope_mcp/tools/__init__.py +0 -2
  6. package/dist/bundled/typescript/dist/index.js +6129 -5891
  7. package/dist/bundled/typescript/dist/index.js.map +5 -5
  8. package/dist/bundled/vue/dist/index.js +136 -71
  9. package/dist/bundled/vue/dist/vue-service.d.ts +16 -0
  10. package/dist/index.js +567 -314
  11. package/dist/index.js.map +6 -6
  12. package/package.json +1 -1
  13. package/dist/bundled/python/src/rope_mcp/__pycache__/__init__.cpython-312.pyc +0 -0
  14. package/dist/bundled/python/src/rope_mcp/__pycache__/__init__.cpython-313.pyc +0 -0
  15. package/dist/bundled/python/src/rope_mcp/__pycache__/config.cpython-312.pyc +0 -0
  16. package/dist/bundled/python/src/rope_mcp/__pycache__/config.cpython-313.pyc +0 -0
  17. package/dist/bundled/python/src/rope_mcp/__pycache__/pyright_client.cpython-313.pyc +0 -0
  18. package/dist/bundled/python/src/rope_mcp/__pycache__/rope_client.cpython-313.pyc +0 -0
  19. package/dist/bundled/python/src/rope_mcp/__pycache__/server.cpython-312.pyc +0 -0
  20. package/dist/bundled/python/src/rope_mcp/__pycache__/server.cpython-313.pyc +0 -0
  21. package/dist/bundled/python/src/rope_mcp/lsp/__pycache__/__init__.cpython-313.pyc +0 -0
  22. package/dist/bundled/python/src/rope_mcp/lsp/__pycache__/client.cpython-313.pyc +0 -0
  23. package/dist/bundled/python/src/rope_mcp/lsp/__pycache__/types.cpython-313.pyc +0 -0
  24. package/dist/bundled/python/src/rope_mcp/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  25. package/dist/bundled/python/src/rope_mcp/tools/__pycache__/change_signature.cpython-313.pyc +0 -0
  26. package/dist/bundled/python/src/rope_mcp/tools/__pycache__/completions.cpython-313.pyc +0 -0
  27. package/dist/bundled/python/src/rope_mcp/tools/__pycache__/definition.cpython-313.pyc +0 -0
  28. package/dist/bundled/python/src/rope_mcp/tools/__pycache__/diagnostics.cpython-313.pyc +0 -0
  29. package/dist/bundled/python/src/rope_mcp/tools/__pycache__/hover.cpython-313.pyc +0 -0
  30. package/dist/bundled/python/src/rope_mcp/tools/__pycache__/move.cpython-313.pyc +0 -0
  31. package/dist/bundled/python/src/rope_mcp/tools/__pycache__/references.cpython-313.pyc +0 -0
  32. package/dist/bundled/python/src/rope_mcp/tools/__pycache__/rename.cpython-313.pyc +0 -0
  33. package/dist/bundled/python/src/rope_mcp/tools/__pycache__/search.cpython-313.pyc +0 -0
  34. 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
- validate_file_workspace,
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: Absolute path to the Python 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
- # Guard: check workspace
132
- error = validate_file_workspace(file)
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(file)
139
+ workspace = _find_workspace(abs_file)
141
140
  client = get_lsp_client(workspace)
142
- result = client.hover(file, line, column)
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(file, line, column)
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: Absolute path to the Python 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
- # Guard: check workspace
179
- error = validate_file_workspace(file)
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(file)
186
+ workspace = _find_workspace(abs_file)
188
187
  client = get_lsp_client(workspace)
189
- locations = client.definition(file, line, column)
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(file, line, column)
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: Absolute path to the Python 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
- # Guard: check workspace
225
- error = validate_file_workspace(file)
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(file)
232
+ workspace = _find_workspace(abs_file)
234
233
  client = get_lsp_client(workspace)
235
- refs = client.references(file, line, column)
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(file, line, column)
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: Absolute path to the Python 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
- # Guard: check workspace
266
- error = validate_file_workspace(file)
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(file)
273
+ workspace = _find_workspace(abs_file)
275
274
  client = get_lsp_client(workspace)
276
- items = client.completions(file, line, column)
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(file, line, column)
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: Absolute path to the Python 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
- # Guard: check workspace
306
- error = validate_file_workspace(file)
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(file)
313
+ workspace = _find_workspace(abs_file)
315
314
  client = get_lsp_client(workspace)
316
- syms = client.document_symbols(file)
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": 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(file, query)
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
- Uses Rope backend for best refactoring support.
344
-
342
+
345
343
  Args:
346
- file: Absolute path to the Python 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
- # Guard: check workspace
355
- error = validate_file_workspace(file)
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
- result = do_rename(file, line, column, new_name)
360
- result["backend"] = "rope"
361
- return json.dumps(result, indent=2)
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: Absolute path to the Python file containing the symbol
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
- # Guard: check workspace
388
- error = validate_file_workspace(file)
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(file, line, column, destination, resources_only=preview)
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: Absolute path to the Python 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
- # Guard: check workspace
439
- error = validate_file_workspace(file)
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
- file,
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: Absolute path to the Python 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
- # Guard: check workspace
480
- error = validate_file_workspace(file)
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(file, line, column)
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 or directory.
519
+ """Get type errors and warnings for a Python file.
492
520
 
493
- Uses Pyright for type checking. Requires Pyright to be installed.
521
+ Uses Pyright LSP for type checking.
494
522
 
495
523
  Args:
496
- path: Absolute path to a Python file or directory
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
- # Guard: check workspace
502
- error = validate_file_workspace(path)
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
- result = get_diagnostics(path)
507
- result["backend"] = "pyright"
508
- return json.dumps(result, indent=2)
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: Absolute path to the Python 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
- # Guard: check workspace
526
- error = validate_file_workspace(file)
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(file)
579
+ workspace = _find_workspace(abs_file)
532
580
  client = get_lsp_client(workspace)
533
- result = client.signature_help(file, line, column)
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: Absolute path to the Python 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
- # Guard: check workspace
559
- error = validate_file_workspace(file)
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(file)
612
+ workspace = _find_workspace(abs_file)
565
613
  client = get_lsp_client(workspace)
566
- client.update_document(file, content)
614
+ client.update_document(abs_file, content)
567
615
  return json.dumps(
568
- {"success": True, "file": file, "backend": "pyright"}, indent=2
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
- # Guard: check workspace if path is provided
596
- if path:
597
- error = validate_file_workspace(path)
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=path,
857
+ path=search_path,
604
858
  glob=glob,
605
859
  case_sensitive=case_sensitive,
606
860
  max_results=max_results,