hanzo-mcp 0.8.1__py3-none-any.whl → 0.8.3__py3-none-any.whl

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.

Potentially problematic release.


This version of hanzo-mcp might be problematic. Click here for more details.

@@ -0,0 +1,544 @@
1
+ """CLI tool implementations for direct batch execution.
2
+
3
+ This module provides CLI tool wrappers that can be used directly in batch operations,
4
+ including claude (cc), codex, gemini, grok, openhands (oh), hanzo-dev, cline, and aider.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import asyncio
11
+ from typing import Any, Optional, Dict, List, Unpack, Annotated, TypedDict, override, final
12
+ from pathlib import Path
13
+
14
+ from pydantic import Field
15
+ from mcp.server import FastMCP
16
+ from mcp.server.fastmcp import Context
17
+
18
+ from hanzo_mcp.tools.common.base import BaseTool
19
+ from hanzo_mcp.tools.common.context import create_tool_context
20
+ from hanzo_mcp.tools.common.permissions import PermissionManager
21
+
22
+
23
+ # Parameter types for CLI tools
24
+ Prompt = Annotated[
25
+ str,
26
+ Field(
27
+ description="The prompt or command to send to the CLI tool",
28
+ min_length=1,
29
+ ),
30
+ ]
31
+
32
+ Model = Annotated[
33
+ Optional[str],
34
+ Field(
35
+ description="Optional model override for the CLI tool",
36
+ default=None,
37
+ ),
38
+ ]
39
+
40
+ WorkingDir = Annotated[
41
+ Optional[str],
42
+ Field(
43
+ description="Working directory for the command",
44
+ default=None,
45
+ ),
46
+ ]
47
+
48
+ Timeout = Annotated[
49
+ Optional[int],
50
+ Field(
51
+ description="Timeout in seconds for the command",
52
+ default=300, # 5 minutes default
53
+ ),
54
+ ]
55
+
56
+
57
+ class CLIToolParams(TypedDict, total=False):
58
+ """Common parameters for CLI tools."""
59
+ prompt: str
60
+ model: Optional[str]
61
+ working_dir: Optional[str]
62
+ timeout: Optional[int]
63
+
64
+
65
+ class BaseCLITool(BaseTool):
66
+ """Base class for CLI tool implementations."""
67
+
68
+ def __init__(
69
+ self,
70
+ permission_manager: Optional[PermissionManager] = None,
71
+ default_model: Optional[str] = None,
72
+ api_key_env: Optional[str] = None,
73
+ ):
74
+ """Initialize CLI tool.
75
+
76
+ Args:
77
+ permission_manager: Permission manager for access control
78
+ default_model: Default model to use
79
+ api_key_env: Environment variable name for API key
80
+ """
81
+ self.permission_manager = permission_manager
82
+ self.default_model = default_model
83
+ self.api_key_env = api_key_env
84
+
85
+ def get_auth_env(self) -> dict[str, str]:
86
+ """Get authentication environment variables."""
87
+ env = os.environ.copy()
88
+
89
+ # Add API key if configured
90
+ if self.api_key_env and self.api_key_env in os.environ:
91
+ env[self.api_key_env] = os.environ[self.api_key_env]
92
+
93
+ # Add Hanzo API key for unified auth
94
+ if "HANZO_API_KEY" in os.environ:
95
+ env["HANZO_API_KEY"] = os.environ["HANZO_API_KEY"]
96
+
97
+ return env
98
+
99
+ async def execute_cli(
100
+ self,
101
+ command: list[str],
102
+ input_text: Optional[str] = None,
103
+ working_dir: Optional[str] = None,
104
+ timeout: int = 300,
105
+ ) -> str:
106
+ """Execute CLI command with proper error handling.
107
+
108
+ Args:
109
+ command: Command and arguments
110
+ input_text: Optional stdin input
111
+ working_dir: Working directory
112
+ timeout: Timeout in seconds
113
+
114
+ Returns:
115
+ Command output
116
+ """
117
+ try:
118
+ # Set up environment with auth
119
+ env = self.get_auth_env()
120
+
121
+ # Execute command
122
+ process = await asyncio.create_subprocess_exec(
123
+ *command,
124
+ stdin=asyncio.subprocess.PIPE if input_text else None,
125
+ stdout=asyncio.subprocess.PIPE,
126
+ stderr=asyncio.subprocess.PIPE,
127
+ cwd=working_dir,
128
+ env=env,
129
+ )
130
+
131
+ # Send input and get output
132
+ stdout, stderr = await asyncio.wait_for(
133
+ process.communicate(input_text.encode() if input_text else None),
134
+ timeout=timeout,
135
+ )
136
+
137
+ # Check for errors
138
+ if process.returncode != 0:
139
+ error_msg = stderr.decode() if stderr else "Unknown error"
140
+ return f"Error: {error_msg}"
141
+
142
+ return stdout.decode()
143
+
144
+ except asyncio.TimeoutError:
145
+ return f"Error: Command timed out after {timeout} seconds"
146
+ except Exception as e:
147
+ return f"Error executing command: {str(e)}"
148
+
149
+ def register(self, mcp_server: FastMCP) -> None:
150
+ """Register this tool with the MCP server.
151
+
152
+ Args:
153
+ mcp_server: The FastMCP server instance
154
+ """
155
+ tool_self = self # Create a reference to self for use in the closure
156
+
157
+ @mcp_server.tool(name=self.name, description=self.description)
158
+ async def tool_wrapper(
159
+ prompt: str,
160
+ ctx: Context[Any, Any, Any],
161
+ model: Optional[str] = None,
162
+ working_dir: Optional[str] = None,
163
+ timeout: int = 300,
164
+ ) -> str:
165
+ result: str = await tool_self.call(
166
+ ctx, prompt=prompt, model=model, working_dir=working_dir, timeout=timeout
167
+ )
168
+ return result
169
+
170
+
171
+ class ClaudeCLITool(BaseCLITool):
172
+ """Claude CLI tool (also available as 'cc' alias)."""
173
+
174
+ def __init__(self, permission_manager: Optional[PermissionManager] = None):
175
+ super().__init__(
176
+ permission_manager=permission_manager,
177
+ default_model="claude-3-5-sonnet-20241022",
178
+ api_key_env="ANTHROPIC_API_KEY",
179
+ )
180
+
181
+ @property
182
+ def name(self) -> str:
183
+ return "claude"
184
+
185
+ @property
186
+ def description(self) -> str:
187
+ return "Execute Claude CLI for AI assistance using Anthropic's models"
188
+
189
+ async def call(self, ctx: Context[Any, Any, Any], **params: Any) -> str:
190
+ prompt: str = params.get("prompt", "")
191
+ model: Optional[str] = params.get("model") or self.default_model
192
+ working_dir: Optional[str] = params.get("working_dir")
193
+ timeout: int = params.get("timeout", 300)
194
+
195
+ # Build command
196
+ command: list[str] = ["claude"]
197
+ if model:
198
+ command.extend(["--model", model])
199
+
200
+ # Execute
201
+ return await self.execute_cli(
202
+ command,
203
+ input_text=prompt,
204
+ working_dir=working_dir,
205
+ timeout=timeout,
206
+ )
207
+
208
+
209
+ class ClaudeCodeCLITool(ClaudeCLITool):
210
+ """Claude Code CLI tool (cc alias)."""
211
+
212
+ @property
213
+ def name(self) -> str:
214
+ return "cc"
215
+
216
+ @property
217
+ def description(self) -> str:
218
+ return "Claude Code CLI (alias for claude)"
219
+
220
+
221
+ class CodexCLITool(BaseCLITool):
222
+ """OpenAI Codex/GPT-4 CLI tool."""
223
+
224
+ def __init__(self, permission_manager: Optional[PermissionManager] = None):
225
+ super().__init__(
226
+ permission_manager=permission_manager,
227
+ default_model="gpt-4-turbo",
228
+ api_key_env="OPENAI_API_KEY",
229
+ )
230
+
231
+ @property
232
+ def name(self) -> str:
233
+ return "codex"
234
+
235
+ @property
236
+ def description(self) -> str:
237
+ return "Execute OpenAI Codex/GPT-4 CLI for code generation and AI assistance"
238
+
239
+ async def call(self, ctx: Context[Any, Any, Any], **params: Any) -> str:
240
+ prompt: str = params.get("prompt", "")
241
+ model: Optional[str] = params.get("model") or self.default_model
242
+ working_dir: Optional[str] = params.get("working_dir")
243
+ timeout: int = params.get("timeout", 300)
244
+
245
+ # Build command (using openai CLI or custom wrapper)
246
+ command: list[str] = ["openai", "api", "chat.completions.create"]
247
+ if model:
248
+ command.extend(["-m", model])
249
+ command.extend(["-g", "user", prompt])
250
+
251
+ # Execute
252
+ return await self.execute_cli(
253
+ command,
254
+ working_dir=working_dir,
255
+ timeout=timeout,
256
+ )
257
+
258
+
259
+ class GeminiCLITool(BaseCLITool):
260
+ """Google Gemini CLI tool."""
261
+
262
+ def __init__(self, permission_manager: Optional[PermissionManager] = None):
263
+ super().__init__(
264
+ permission_manager=permission_manager,
265
+ default_model="gemini-1.5-pro",
266
+ api_key_env="GEMINI_API_KEY",
267
+ )
268
+
269
+ @property
270
+ def name(self) -> str:
271
+ return "gemini"
272
+
273
+ @property
274
+ def description(self) -> str:
275
+ return "Execute Google Gemini CLI for multimodal AI assistance"
276
+
277
+ async def call(self, ctx: Context[Any, Any, Any], **params: Any) -> str:
278
+ prompt: str = params.get("prompt", "")
279
+ model: Optional[str] = params.get("model") or self.default_model
280
+ working_dir: Optional[str] = params.get("working_dir")
281
+ timeout: int = params.get("timeout", 300)
282
+
283
+ # Build command
284
+ command: list[str] = ["gemini"]
285
+ if model:
286
+ command.extend(["--model", model])
287
+ command.append(prompt)
288
+
289
+ # Execute
290
+ return await self.execute_cli(
291
+ command,
292
+ working_dir=working_dir,
293
+ timeout=timeout,
294
+ )
295
+
296
+
297
+ class GrokCLITool(BaseCLITool):
298
+ """xAI Grok CLI tool."""
299
+
300
+ def __init__(self, permission_manager: Optional[PermissionManager] = None):
301
+ super().__init__(
302
+ permission_manager=permission_manager,
303
+ default_model="grok-4",
304
+ api_key_env="XAI_API_KEY",
305
+ )
306
+
307
+ @property
308
+ def name(self) -> str:
309
+ return "grok"
310
+
311
+ @property
312
+ def description(self) -> str:
313
+ return "Execute xAI Grok CLI for real-time AI assistance"
314
+
315
+ async def call(self, ctx: Context[Any, Any, Any], **params: Any) -> str:
316
+ prompt: str = params.get("prompt", "")
317
+ model: Optional[str] = params.get("model") or self.default_model
318
+ working_dir: Optional[str] = params.get("working_dir")
319
+ timeout: int = params.get("timeout", 300)
320
+
321
+ # Build command
322
+ command: list[str] = ["grok"]
323
+ if model:
324
+ command.extend(["--model", model])
325
+ command.append(prompt)
326
+
327
+ # Execute
328
+ return await self.execute_cli(
329
+ command,
330
+ working_dir=working_dir,
331
+ timeout=timeout,
332
+ )
333
+
334
+
335
+ class OpenHandsCLITool(BaseCLITool):
336
+ """OpenHands (OpenDevin) CLI tool."""
337
+
338
+ def __init__(self, permission_manager: Optional[PermissionManager] = None):
339
+ super().__init__(
340
+ permission_manager=permission_manager,
341
+ default_model="claude-3-5-sonnet-20241022",
342
+ api_key_env="OPENAI_API_KEY",
343
+ )
344
+
345
+ @property
346
+ def name(self) -> str:
347
+ return "openhands"
348
+
349
+ @property
350
+ def description(self) -> str:
351
+ return "Execute OpenHands (OpenDevin) for autonomous coding assistance"
352
+
353
+ async def call(self, ctx: Context[Any, Any, Any], **params: Any) -> str:
354
+ prompt = params.get("prompt", "")
355
+ model = params.get("model") or self.default_model
356
+ working_dir: str = params.get("working_dir") or os.getcwd()
357
+ timeout: int = params.get("timeout", 600) # 10 minutes for OpenHands
358
+
359
+ # Build command
360
+ command: list[str] = ["openhands", "run", prompt]
361
+ if model:
362
+ command.extend(["--model", model])
363
+ command.extend(["--workspace", working_dir])
364
+
365
+ # Execute
366
+ return await self.execute_cli(
367
+ command,
368
+ working_dir=working_dir,
369
+ timeout=timeout,
370
+ )
371
+
372
+
373
+ class OpenHandsShortCLITool(OpenHandsCLITool):
374
+ """OpenHands CLI tool (oh alias)."""
375
+
376
+ @property
377
+ def name(self) -> str:
378
+ return "oh"
379
+
380
+ @property
381
+ def description(self) -> str:
382
+ return "OpenHands CLI (alias for openhands)"
383
+
384
+
385
+ class HanzoDevCLITool(BaseCLITool):
386
+ """Hanzo Dev AI coding assistant."""
387
+
388
+ def __init__(self, permission_manager: Optional[PermissionManager] = None):
389
+ super().__init__(
390
+ permission_manager=permission_manager,
391
+ default_model="claude-3-5-sonnet-20241022",
392
+ api_key_env="HANZO_API_KEY",
393
+ )
394
+
395
+ @property
396
+ def name(self) -> str:
397
+ return "hanzo_dev"
398
+
399
+ @property
400
+ def description(self) -> str:
401
+ return "Execute Hanzo Dev for AI-powered code editing and development"
402
+
403
+ async def call(self, ctx: Context[Any, Any, Any], **params: Any) -> str:
404
+ prompt = params.get("prompt", "")
405
+ model = params.get("model") or self.default_model
406
+ working_dir: str = params.get("working_dir") or os.getcwd()
407
+ timeout: int = params.get("timeout", 600)
408
+
409
+ # Build command
410
+ command: list[str] = ["hanzo", "dev"]
411
+ if model:
412
+ command.extend(["--model", model])
413
+ command.extend(["--prompt", prompt])
414
+
415
+ # Execute
416
+ return await self.execute_cli(
417
+ command,
418
+ working_dir=working_dir,
419
+ timeout=timeout,
420
+ )
421
+
422
+
423
+ class ClineCLITool(BaseCLITool):
424
+ """Cline (formerly Claude Engineer) CLI tool."""
425
+
426
+ def __init__(self, permission_manager: Optional[PermissionManager] = None):
427
+ super().__init__(
428
+ permission_manager=permission_manager,
429
+ default_model="claude-3-5-sonnet-20241022",
430
+ api_key_env="ANTHROPIC_API_KEY",
431
+ )
432
+
433
+ @property
434
+ def name(self) -> str:
435
+ return "cline"
436
+
437
+ @property
438
+ def description(self) -> str:
439
+ return "Execute Cline for autonomous coding with Claude"
440
+
441
+ async def call(self, ctx: Context[Any, Any, Any], **params: Any) -> str:
442
+ prompt = params.get("prompt", "")
443
+ working_dir: str = params.get("working_dir") or os.getcwd()
444
+ timeout: int = params.get("timeout", 600)
445
+
446
+ # Build command
447
+ command: list[str] = ["cline", prompt]
448
+ command.extend(["--no-interactive"]) # Non-interactive mode for batch
449
+
450
+ # Execute
451
+ return await self.execute_cli(
452
+ command,
453
+ working_dir=working_dir,
454
+ timeout=timeout,
455
+ )
456
+
457
+
458
+ class AiderCLITool(BaseCLITool):
459
+ """Aider AI pair programming tool."""
460
+
461
+ def __init__(self, permission_manager: Optional[PermissionManager] = None):
462
+ super().__init__(
463
+ permission_manager=permission_manager,
464
+ default_model="gpt-4-turbo",
465
+ api_key_env="OPENAI_API_KEY",
466
+ )
467
+
468
+ @property
469
+ def name(self) -> str:
470
+ return "aider"
471
+
472
+ @property
473
+ def description(self) -> str:
474
+ return "Execute Aider for AI pair programming"
475
+
476
+ async def call(self, ctx: Context[Any, Any, Any], **params: Any) -> str:
477
+ prompt = params.get("prompt", "")
478
+ model = params.get("model") or self.default_model
479
+ working_dir: str = params.get("working_dir") or os.getcwd()
480
+ timeout: int = params.get("timeout", 600)
481
+
482
+ # Build command
483
+ command: list[str] = ["aider"]
484
+ if model:
485
+ command.extend(["--model", model])
486
+ command.extend(["--message", prompt])
487
+ command.extend(["--yes"]) # Auto-approve changes
488
+ command.extend(["--no-stream"]) # No streaming for batch
489
+
490
+ # Execute
491
+ return await self.execute_cli(
492
+ command,
493
+ working_dir=working_dir,
494
+ timeout=timeout,
495
+ )
496
+
497
+
498
+ def register_cli_tools(
499
+ mcp_server: FastMCP,
500
+ permission_manager: Optional[PermissionManager] = None,
501
+ ) -> list[BaseTool]:
502
+ """Register all CLI tools with the MCP server.
503
+
504
+ Args:
505
+ mcp_server: The FastMCP server instance
506
+ permission_manager: Permission manager for access control
507
+
508
+ Returns:
509
+ List of registered CLI tools
510
+ """
511
+ tools: list[BaseTool] = [
512
+ ClaudeCLITool(permission_manager),
513
+ ClaudeCodeCLITool(permission_manager), # cc alias
514
+ CodexCLITool(permission_manager),
515
+ GeminiCLITool(permission_manager),
516
+ GrokCLITool(permission_manager),
517
+ OpenHandsCLITool(permission_manager),
518
+ OpenHandsShortCLITool(permission_manager), # oh alias
519
+ HanzoDevCLITool(permission_manager),
520
+ ClineCLITool(permission_manager),
521
+ AiderCLITool(permission_manager),
522
+ ]
523
+
524
+ # Register each tool
525
+ for tool in tools:
526
+ tool.register(mcp_server)
527
+
528
+ return tools
529
+
530
+
531
+ # Export all CLI tool classes
532
+ __all__ = [
533
+ "ClaudeCLITool",
534
+ "ClaudeCodeCLITool",
535
+ "CodexCLITool",
536
+ "GeminiCLITool",
537
+ "GrokCLITool",
538
+ "OpenHandsCLITool",
539
+ "OpenHandsShortCLITool",
540
+ "HanzoDevCLITool",
541
+ "ClineCLITool",
542
+ "AiderCLITool",
543
+ "register_cli_tools",
544
+ ]