@treedy/lsp-mcp 0.1.6 → 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.
- package/dist/bundled/pyright/dist/index.d.ts +2 -0
- package/dist/bundled/pyright/dist/index.js +1620 -0
- package/dist/bundled/pyright/dist/index.js.map +26 -0
- package/dist/bundled/pyright/dist/lsp/connection.d.ts +71 -0
- package/dist/bundled/pyright/dist/lsp/document-manager.d.ts +67 -0
- package/dist/bundled/pyright/dist/lsp/index.d.ts +3 -0
- package/dist/bundled/pyright/dist/lsp/types.d.ts +55 -0
- package/dist/bundled/pyright/dist/lsp-client.d.ts +55 -0
- package/dist/bundled/pyright/dist/tools/completions.d.ts +18 -0
- package/dist/bundled/pyright/dist/tools/definition.d.ts +16 -0
- package/dist/bundled/pyright/dist/tools/diagnostics.d.ts +12 -0
- package/dist/bundled/pyright/dist/tools/hover.d.ts +16 -0
- package/dist/bundled/pyright/dist/tools/references.d.ts +16 -0
- package/dist/bundled/pyright/dist/tools/rename.d.ts +18 -0
- package/dist/bundled/pyright/dist/tools/search.d.ts +20 -0
- package/dist/bundled/pyright/dist/tools/signature-help.d.ts +16 -0
- package/dist/bundled/pyright/dist/tools/status.d.ts +14 -0
- package/dist/bundled/pyright/dist/tools/symbols.d.ts +17 -0
- package/dist/bundled/pyright/dist/tools/update-document.d.ts +14 -0
- package/dist/bundled/pyright/dist/utils/position.d.ts +33 -0
- package/dist/bundled/pyright/package.json +54 -0
- package/dist/bundled/python/README.md +230 -0
- package/dist/bundled/python/pyproject.toml +61 -0
- package/dist/bundled/python/src/rope_mcp/__init__.py +3 -0
- 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/config.py +408 -0
- package/dist/bundled/python/src/rope_mcp/lsp/__init__.py +15 -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/lsp/client.py +624 -0
- package/dist/bundled/python/src/rope_mcp/lsp/types.py +82 -0
- package/dist/bundled/python/src/rope_mcp/pyright_client.py +147 -0
- package/dist/bundled/python/src/rope_mcp/rope_client.py +198 -0
- package/dist/bundled/python/src/rope_mcp/server.py +963 -0
- package/dist/bundled/python/src/rope_mcp/tools/__init__.py +26 -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
- package/dist/bundled/python/src/rope_mcp/tools/change_signature.py +184 -0
- package/dist/bundled/python/src/rope_mcp/tools/completions.py +84 -0
- package/dist/bundled/python/src/rope_mcp/tools/definition.py +51 -0
- package/dist/bundled/python/src/rope_mcp/tools/diagnostics.py +18 -0
- package/dist/bundled/python/src/rope_mcp/tools/hover.py +49 -0
- package/dist/bundled/python/src/rope_mcp/tools/move.py +81 -0
- package/dist/bundled/python/src/rope_mcp/tools/references.py +60 -0
- package/dist/bundled/python/src/rope_mcp/tools/rename.py +61 -0
- package/dist/bundled/python/src/rope_mcp/tools/search.py +128 -0
- package/dist/bundled/python/src/rope_mcp/tools/symbols.py +118 -0
- package/dist/bundled/python/uv.lock +979 -0
- package/dist/bundled/typescript/dist/index.js +29534 -0
- package/dist/bundled/typescript/dist/index.js.map +211 -0
- package/dist/bundled/typescript/package.json +46 -0
- package/dist/bundled/vue/dist/index.d.ts +8 -0
- package/dist/bundled/vue/dist/index.js +21111 -0
- package/dist/bundled/vue/dist/ts-vue-service.d.ts +67 -0
- package/dist/bundled/vue/dist/vue-service.d.ts +144 -0
- package/dist/bundled/vue/package.json +45 -0
- package/dist/index.js +268 -112
- package/dist/index.js.map +5 -5
- 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()
|