sarveshpatel commited on
Commit
ba6de18
·
verified ·
1 Parent(s): 1a6c037

Create app/mcp_handler.py

Browse files
Files changed (1) hide show
  1. app/mcp_handler.py +369 -0
app/mcp_handler.py ADDED
@@ -0,0 +1,369 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """MCP protocol handler - maps MCP tool calls to actual operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import Any
7
+
8
+ from app.environment import configure_environment, get_environment_config
9
+ from app.executor import get_executor
10
+ from app.file_manager import FileManager
11
+ from app.models import MCPToolResult
12
+ from app.package_manager import get_package_manager
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Tool definitions for MCP list_tools
17
+ TOOL_DEFINITIONS = [
18
+ {
19
+ "name": "execute_code",
20
+ "description": "Executes Python code in the configured environment. Best for short code snippets.",
21
+ "inputSchema": {
22
+ "type": "object",
23
+ "properties": {
24
+ "code": {
25
+ "type": "string",
26
+ "description": "Python code to execute",
27
+ },
28
+ "filename": {
29
+ "type": "string",
30
+ "description": "Optional filename (without extension)",
31
+ },
32
+ },
33
+ "required": ["code"],
34
+ },
35
+ },
36
+ {
37
+ "name": "install_dependencies",
38
+ "description": "Installs Python packages in the environment.",
39
+ "inputSchema": {
40
+ "type": "object",
41
+ "properties": {
42
+ "packages": {
43
+ "type": "array",
44
+ "items": {"type": "string"},
45
+ "description": "List of package names to install",
46
+ },
47
+ },
48
+ "required": ["packages"],
49
+ },
50
+ },
51
+ {
52
+ "name": "check_installed_packages",
53
+ "description": "Checks if packages are already installed in the environment.",
54
+ "inputSchema": {
55
+ "type": "object",
56
+ "properties": {
57
+ "packages": {
58
+ "type": "array",
59
+ "items": {"type": "string"},
60
+ "description": "List of package names to check",
61
+ },
62
+ },
63
+ "required": ["packages"],
64
+ },
65
+ },
66
+ {
67
+ "name": "configure_environment",
68
+ "description": "Dynamically changes the environment configuration.",
69
+ "inputSchema": {
70
+ "type": "object",
71
+ "properties": {
72
+ "type": {
73
+ "type": "string",
74
+ "enum": ["conda", "venv", "venv-uv"],
75
+ "description": "Environment type",
76
+ },
77
+ "conda_name": {
78
+ "type": "string",
79
+ "description": "Conda environment name (required for conda type)",
80
+ },
81
+ "venv_path": {
82
+ "type": "string",
83
+ "description": "Virtualenv path (required for venv type)",
84
+ },
85
+ "uv_venv_path": {
86
+ "type": "string",
87
+ "description": "UV virtualenv path (required for venv-uv type)",
88
+ },
89
+ },
90
+ "required": ["type"],
91
+ },
92
+ },
93
+ {
94
+ "name": "get_environment_config",
95
+ "description": "Gets the current environment configuration.",
96
+ "inputSchema": {
97
+ "type": "object",
98
+ "properties": {},
99
+ },
100
+ },
101
+ {
102
+ "name": "initialize_code_file",
103
+ "description": "Creates a new Python file with initial content. Use this as the first step for longer code that may exceed token limits.",
104
+ "inputSchema": {
105
+ "type": "object",
106
+ "properties": {
107
+ "content": {
108
+ "type": "string",
109
+ "description": "Initial file content",
110
+ },
111
+ "filename": {
112
+ "type": "string",
113
+ "description": "Optional filename (without extension)",
114
+ },
115
+ },
116
+ "required": ["content"],
117
+ },
118
+ },
119
+ {
120
+ "name": "append_to_code_file",
121
+ "description": "Appends content to an existing Python code file.",
122
+ "inputSchema": {
123
+ "type": "object",
124
+ "properties": {
125
+ "file_path": {
126
+ "type": "string",
127
+ "description": "Path to the existing code file",
128
+ },
129
+ "content": {
130
+ "type": "string",
131
+ "description": "Content to append",
132
+ },
133
+ },
134
+ "required": ["file_path", "content"],
135
+ },
136
+ },
137
+ {
138
+ "name": "execute_code_file",
139
+ "description": "Executes an existing Python file.",
140
+ "inputSchema": {
141
+ "type": "object",
142
+ "properties": {
143
+ "file_path": {
144
+ "type": "string",
145
+ "description": "Path to the code file to execute",
146
+ },
147
+ },
148
+ "required": ["file_path"],
149
+ },
150
+ },
151
+ {
152
+ "name": "read_code_file",
153
+ "description": "Reads the content of an existing Python code file.",
154
+ "inputSchema": {
155
+ "type": "object",
156
+ "properties": {
157
+ "file_path": {
158
+ "type": "string",
159
+ "description": "Path to the code file to read",
160
+ },
161
+ },
162
+ "required": ["file_path"],
163
+ },
164
+ },
165
+ ]
166
+
167
+
168
+ async def handle_tool_call(name: str, arguments: dict[str, Any]) -> MCPToolResult:
169
+ """Route an MCP tool call to the appropriate handler."""
170
+ try:
171
+ if name == "execute_code":
172
+ return await _handle_execute_code(arguments)
173
+ elif name == "install_dependencies":
174
+ return await _handle_install_dependencies(arguments)
175
+ elif name == "check_installed_packages":
176
+ return await _handle_check_packages(arguments)
177
+ elif name == "configure_environment":
178
+ return _handle_configure_environment(arguments)
179
+ elif name == "get_environment_config":
180
+ return _handle_get_environment_config()
181
+ elif name == "initialize_code_file":
182
+ return _handle_initialize_code_file(arguments)
183
+ elif name == "append_to_code_file":
184
+ return _handle_append_to_code_file(arguments)
185
+ elif name == "execute_code_file":
186
+ return await _handle_execute_code_file(arguments)
187
+ elif name == "read_code_file":
188
+ return _handle_read_code_file(arguments)
189
+ else:
190
+ return MCPToolResult(
191
+ content=[{"type": "text", "text": f"Unknown tool: {name}"}],
192
+ isError=True,
193
+ )
194
+ except Exception as e:
195
+ logger.exception("Error handling tool call: %s", name)
196
+ return MCPToolResult(
197
+ content=[{"type": "text", "text": f"Error: {str(e)}"}],
198
+ isError=True,
199
+ )
200
+
201
+
202
+ async def _handle_execute_code(args: dict) -> MCPToolResult:
203
+ executor = get_executor()
204
+ result = await executor.execute_code(args["code"], args.get("filename"))
205
+
206
+ output_parts = []
207
+ if result.stdout:
208
+ output_parts.append(f"STDOUT:\n{result.stdout}")
209
+ if result.stderr:
210
+ output_parts.append(f"STDERR:\n{result.stderr}")
211
+ output_parts.append(f"Return code: {result.return_code}")
212
+ output_parts.append(f"Execution time: {result.execution_time:.2f}s")
213
+ output_parts.append(f"File: {result.file_path}")
214
+
215
+ return MCPToolResult(
216
+ content=[{"type": "text", "text": "\n".join(output_parts)}],
217
+ isError=not result.success,
218
+ )
219
+
220
+
221
+ async def _handle_install_dependencies(args: dict) -> MCPToolResult:
222
+ pm = get_package_manager()
223
+ result = await pm.install_packages(args["packages"])
224
+
225
+ text = f"Installation {'succeeded' if result['success'] else 'failed'}\n"
226
+ if result.get("stdout"):
227
+ text += f"Output:\n{result['stdout']}\n"
228
+ if result.get("stderr"):
229
+ text += f"Errors:\n{result['stderr']}\n"
230
+
231
+ return MCPToolResult(
232
+ content=[{"type": "text", "text": text}],
233
+ isError=not result["success"],
234
+ )
235
+
236
+
237
+ async def _handle_check_packages(args: dict) -> MCPToolResult:
238
+ pm = get_package_manager()
239
+ results = await pm.check_packages(args["packages"])
240
+
241
+ lines = []
242
+ for r in results:
243
+ status = "✓ installed" if r.installed else "✗ not installed"
244
+ version = f" (v{r.version})" if r.version and r.version != "unknown" else ""
245
+ lines.append(f" {r.package}: {status}{version}")
246
+
247
+ return MCPToolResult(
248
+ content=[{"type": "text", "text": "Package status:\n" + "\n".join(lines)}],
249
+ isError=False,
250
+ )
251
+
252
+
253
+ def _handle_configure_environment(args: dict) -> MCPToolResult:
254
+ try:
255
+ config = configure_environment(
256
+ env_type=args["type"],
257
+ conda_name=args.get("conda_name"),
258
+ venv_path=args.get("venv_path"),
259
+ uv_venv_path=args.get("uv_venv_path"),
260
+ )
261
+ return MCPToolResult(
262
+ content=[
263
+ {
264
+ "type": "text",
265
+ "text": f"Environment configured:\n"
266
+ f" Type: {config.env_type}\n"
267
+ f" Python: {config.python_executable}\n"
268
+ f" Storage: {config.code_storage_dir}",
269
+ }
270
+ ],
271
+ isError=False,
272
+ )
273
+ except ValueError as e:
274
+ return MCPToolResult(
275
+ content=[{"type": "text", "text": f"Configuration error: {str(e)}"}],
276
+ isError=True,
277
+ )
278
+
279
+
280
+ def _handle_get_environment_config() -> MCPToolResult:
281
+ config = get_environment_config()
282
+ return MCPToolResult(
283
+ content=[
284
+ {
285
+ "type": "text",
286
+ "text": f"Current environment:\n"
287
+ f" Type: {config.env_type}\n"
288
+ f" Python: {config.python_executable}\n"
289
+ f" Storage: {config.code_storage_dir}\n"
290
+ f" Conda env: {config.conda_env_name or 'N/A'}\n"
291
+ f" Venv path: {config.venv_path or 'N/A'}\n"
292
+ f" UV venv path: {config.uv_venv_path or 'N/A'}",
293
+ }
294
+ ],
295
+ isError=False,
296
+ )
297
+
298
+
299
+ def _handle_initialize_code_file(args: dict) -> MCPToolResult:
300
+ fm = FileManager()
301
+ result = fm.create_file(args["content"], args.get("filename"))
302
+
303
+ if result.success:
304
+ return MCPToolResult(
305
+ content=[
306
+ {
307
+ "type": "text",
308
+ "text": f"File created: {result.file_path}\n{result.message}",
309
+ }
310
+ ],
311
+ isError=False,
312
+ )
313
+ return MCPToolResult(
314
+ content=[{"type": "text", "text": f"Error: {result.message}"}],
315
+ isError=True,
316
+ )
317
+
318
+
319
+ def _handle_append_to_code_file(args: dict) -> MCPToolResult:
320
+ fm = FileManager()
321
+ result = fm.append_to_file(args["file_path"], args["content"])
322
+
323
+ return MCPToolResult(
324
+ content=[
325
+ {
326
+ "type": "text",
327
+ "text": result.message + (f"\nFile: {result.file_path}" if result.success else ""),
328
+ }
329
+ ],
330
+ isError=not result.success,
331
+ )
332
+
333
+
334
+ async def _handle_execute_code_file(args: dict) -> MCPToolResult:
335
+ executor = get_executor()
336
+ result = await executor.execute_file(args["file_path"])
337
+
338
+ output_parts = []
339
+ if result.stdout:
340
+ output_parts.append(f"STDOUT:\n{result.stdout}")
341
+ if result.stderr:
342
+ output_parts.append(f"STDERR:\n{result.stderr}")
343
+ output_parts.append(f"Return code: {result.return_code}")
344
+ output_parts.append(f"Execution time: {result.execution_time:.2f}s")
345
+
346
+ return MCPToolResult(
347
+ content=[{"type": "text", "text": "\n".join(output_parts)}],
348
+ isError=not result.success,
349
+ )
350
+
351
+
352
+ def _handle_read_code_file(args: dict) -> MCPToolResult:
353
+ fm = FileManager()
354
+ result = fm.read_file(args["file_path"])
355
+
356
+ if result.success:
357
+ return MCPToolResult(
358
+ content=[
359
+ {
360
+ "type": "text",
361
+ "text": f"File: {result.file_path}\n\n{result.content}",
362
+ }
363
+ ],
364
+ isError=False,
365
+ )
366
+ return MCPToolResult(
367
+ content=[{"type": "text", "text": f"Error: {result.message}"}],
368
+ isError=True,
369
+ )