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