hanzo-mcp 0.6.13__py3-none-any.whl → 0.7.1__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.

Files changed (62) hide show
  1. hanzo_mcp/analytics/__init__.py +5 -0
  2. hanzo_mcp/analytics/posthog_analytics.py +364 -0
  3. hanzo_mcp/cli.py +3 -3
  4. hanzo_mcp/cli_enhanced.py +3 -3
  5. hanzo_mcp/config/settings.py +1 -1
  6. hanzo_mcp/config/tool_config.py +18 -4
  7. hanzo_mcp/server.py +34 -1
  8. hanzo_mcp/tools/__init__.py +65 -2
  9. hanzo_mcp/tools/agent/__init__.py +84 -3
  10. hanzo_mcp/tools/agent/agent_tool.py +102 -4
  11. hanzo_mcp/tools/agent/agent_tool_v2.py +492 -0
  12. hanzo_mcp/tools/agent/clarification_protocol.py +220 -0
  13. hanzo_mcp/tools/agent/clarification_tool.py +68 -0
  14. hanzo_mcp/tools/agent/claude_cli_tool.py +125 -0
  15. hanzo_mcp/tools/agent/claude_desktop_auth.py +508 -0
  16. hanzo_mcp/tools/agent/cli_agent_base.py +191 -0
  17. hanzo_mcp/tools/agent/code_auth.py +436 -0
  18. hanzo_mcp/tools/agent/code_auth_tool.py +194 -0
  19. hanzo_mcp/tools/agent/codex_cli_tool.py +123 -0
  20. hanzo_mcp/tools/agent/critic_tool.py +376 -0
  21. hanzo_mcp/tools/agent/gemini_cli_tool.py +128 -0
  22. hanzo_mcp/tools/agent/grok_cli_tool.py +128 -0
  23. hanzo_mcp/tools/agent/iching_tool.py +380 -0
  24. hanzo_mcp/tools/agent/network_tool.py +273 -0
  25. hanzo_mcp/tools/agent/prompt.py +62 -20
  26. hanzo_mcp/tools/agent/review_tool.py +433 -0
  27. hanzo_mcp/tools/agent/swarm_tool.py +535 -0
  28. hanzo_mcp/tools/agent/swarm_tool_v2.py +654 -0
  29. hanzo_mcp/tools/common/base.py +1 -0
  30. hanzo_mcp/tools/common/batch_tool.py +102 -10
  31. hanzo_mcp/tools/common/fastmcp_pagination.py +369 -0
  32. hanzo_mcp/tools/common/forgiving_edit.py +243 -0
  33. hanzo_mcp/tools/common/paginated_base.py +230 -0
  34. hanzo_mcp/tools/common/paginated_response.py +307 -0
  35. hanzo_mcp/tools/common/pagination.py +226 -0
  36. hanzo_mcp/tools/common/tool_list.py +3 -0
  37. hanzo_mcp/tools/common/truncate.py +101 -0
  38. hanzo_mcp/tools/filesystem/__init__.py +29 -0
  39. hanzo_mcp/tools/filesystem/ast_multi_edit.py +562 -0
  40. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +338 -0
  41. hanzo_mcp/tools/lsp/__init__.py +5 -0
  42. hanzo_mcp/tools/lsp/lsp_tool.py +512 -0
  43. hanzo_mcp/tools/memory/__init__.py +76 -0
  44. hanzo_mcp/tools/memory/knowledge_tools.py +518 -0
  45. hanzo_mcp/tools/memory/memory_tools.py +456 -0
  46. hanzo_mcp/tools/search/__init__.py +6 -0
  47. hanzo_mcp/tools/search/find_tool.py +581 -0
  48. hanzo_mcp/tools/search/unified_search.py +953 -0
  49. hanzo_mcp/tools/shell/__init__.py +5 -0
  50. hanzo_mcp/tools/shell/auto_background.py +203 -0
  51. hanzo_mcp/tools/shell/base_process.py +53 -27
  52. hanzo_mcp/tools/shell/bash_tool.py +17 -33
  53. hanzo_mcp/tools/shell/npx_tool.py +15 -32
  54. hanzo_mcp/tools/shell/streaming_command.py +594 -0
  55. hanzo_mcp/tools/shell/uvx_tool.py +15 -32
  56. hanzo_mcp/types.py +23 -0
  57. {hanzo_mcp-0.6.13.dist-info → hanzo_mcp-0.7.1.dist-info}/METADATA +229 -71
  58. {hanzo_mcp-0.6.13.dist-info → hanzo_mcp-0.7.1.dist-info}/RECORD +61 -24
  59. hanzo_mcp-0.6.13.dist-info/licenses/LICENSE +0 -21
  60. {hanzo_mcp-0.6.13.dist-info → hanzo_mcp-0.7.1.dist-info}/WHEEL +0 -0
  61. {hanzo_mcp-0.6.13.dist-info → hanzo_mcp-0.7.1.dist-info}/entry_points.txt +0 -0
  62. {hanzo_mcp-0.6.13.dist-info → hanzo_mcp-0.7.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,436 @@
1
+ """Claude Code and OpenAI Codex authentication management.
2
+
3
+ This module provides tools to manage API keys and authentication for
4
+ Claude Code CLI and OpenAI Codex, allowing separate accounts for swarm agents.
5
+ """
6
+
7
+ import asyncio
8
+ import os
9
+ import subprocess
10
+ import json
11
+ import tempfile
12
+ from pathlib import Path
13
+ from typing import Optional, Tuple, Dict, Any, List
14
+ from dataclasses import dataclass
15
+ import keyring
16
+ import getpass
17
+
18
+
19
+ @dataclass
20
+ class APICredential:
21
+ """API credential information."""
22
+ provider: str
23
+ api_key: str
24
+ model: Optional[str] = None
25
+ base_url: Optional[str] = None
26
+ org_id: Optional[str] = None
27
+ description: Optional[str] = None
28
+
29
+
30
+ class CodeAuthManager:
31
+ """Manages authentication for Claude Code and other AI coding tools."""
32
+
33
+ # Configuration paths
34
+ CONFIG_DIR = Path.home() / ".hanzo" / "auth"
35
+ ACCOUNTS_FILE = CONFIG_DIR / "accounts.json"
36
+ ACTIVE_ACCOUNT_FILE = CONFIG_DIR / "active_account"
37
+
38
+ # Environment variable mappings
39
+ ENV_VARS = {
40
+ "claude": ["ANTHROPIC_API_KEY", "CLAUDE_API_KEY"],
41
+ "openai": ["OPENAI_API_KEY"],
42
+ "azure": ["AZURE_OPENAI_API_KEY", "AZURE_API_KEY"],
43
+ "deepseek": ["DEEPSEEK_API_KEY"],
44
+ "google": ["GOOGLE_API_KEY", "GEMINI_API_KEY"],
45
+ "groq": ["GROQ_API_KEY"],
46
+ }
47
+
48
+ # Default models
49
+ DEFAULT_MODELS = {
50
+ "claude": "claude-3-5-sonnet-20241022", # Latest Sonnet
51
+ "openai": "gpt-4o",
52
+ "azure": "gpt-4",
53
+ "deepseek": "deepseek-coder",
54
+ "google": "gemini-1.5-pro",
55
+ "groq": "llama3-70b-8192",
56
+ }
57
+
58
+ def __init__(self):
59
+ """Initialize auth manager."""
60
+ self.ensure_config_dir()
61
+ self._env_backup = {}
62
+
63
+ def ensure_config_dir(self):
64
+ """Ensure config directory exists."""
65
+ self.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
66
+
67
+ def get_active_account(self) -> Optional[str]:
68
+ """Get the currently active account."""
69
+ if self.ACTIVE_ACCOUNT_FILE.exists():
70
+ return self.ACTIVE_ACCOUNT_FILE.read_text().strip()
71
+ return "default"
72
+
73
+ def set_active_account(self, account: str):
74
+ """Set the active account."""
75
+ self.ACTIVE_ACCOUNT_FILE.write_text(account)
76
+
77
+ def _load_accounts(self) -> Dict[str, Dict[str, Any]]:
78
+ """Load all accounts."""
79
+ if not self.ACCOUNTS_FILE.exists():
80
+ return {}
81
+
82
+ try:
83
+ with open(self.ACCOUNTS_FILE, 'r') as f:
84
+ return json.load(f)
85
+ except:
86
+ return {}
87
+
88
+ def _save_accounts(self, accounts: Dict[str, Dict[str, Any]]):
89
+ """Save accounts."""
90
+ with open(self.ACCOUNTS_FILE, 'w') as f:
91
+ json.dump(accounts, f, indent=2)
92
+
93
+ def list_accounts(self) -> List[str]:
94
+ """List all available accounts."""
95
+ accounts = self._load_accounts()
96
+ return list(accounts.keys())
97
+
98
+ def get_account_info(self, account: str) -> Optional[Dict[str, Any]]:
99
+ """Get information about an account."""
100
+ accounts = self._load_accounts()
101
+ return accounts.get(account)
102
+
103
+ def create_account(
104
+ self,
105
+ account: str,
106
+ provider: str = "claude",
107
+ api_key: Optional[str] = None,
108
+ model: Optional[str] = None,
109
+ description: Optional[str] = None
110
+ ) -> Tuple[bool, str]:
111
+ """Create a new account.
112
+
113
+ Args:
114
+ account: Account name
115
+ provider: Provider (claude, openai, etc.)
116
+ api_key: API key (will prompt if not provided)
117
+ model: Model to use (defaults to provider default)
118
+ description: Account description
119
+
120
+ Returns:
121
+ Tuple of (success, message)
122
+ """
123
+ accounts = self._load_accounts()
124
+
125
+ if account in accounts:
126
+ return False, f"Account '{account}' already exists"
127
+
128
+ # Get API key if not provided
129
+ if not api_key:
130
+ api_key = self._prompt_for_api_key(provider)
131
+ if not api_key:
132
+ return False, "No API key provided"
133
+
134
+ # Use default model if not specified
135
+ if not model:
136
+ model = self.DEFAULT_MODELS.get(provider)
137
+
138
+ # Store in keyring for security
139
+ try:
140
+ keyring.set_password(f"hanzo-{provider}", account, api_key)
141
+ except:
142
+ # Fallback to file storage (less secure)
143
+ pass
144
+
145
+ # Save account info
146
+ accounts[account] = {
147
+ "provider": provider,
148
+ "model": model,
149
+ "description": description or f"{provider} account",
150
+ "created_at": os.path.getmtime(__file__),
151
+ "has_keyring": self._has_keyring_support()
152
+ }
153
+
154
+ self._save_accounts(accounts)
155
+ return True, f"Created account '{account}' for {provider}"
156
+
157
+ def _prompt_for_api_key(self, provider: str) -> Optional[str]:
158
+ """Prompt user for API key."""
159
+ prompt = f"Enter {provider.upper()} API key: "
160
+ try:
161
+ return getpass.getpass(prompt)
162
+ except KeyboardInterrupt:
163
+ return None
164
+
165
+ def _has_keyring_support(self) -> bool:
166
+ """Check if keyring is available."""
167
+ try:
168
+ keyring.get_keyring()
169
+ return True
170
+ except:
171
+ return False
172
+
173
+ def login(self, account: str = "default") -> Tuple[bool, str]:
174
+ """Login to an account by setting environment variables.
175
+
176
+ Args:
177
+ account: Account name to login to
178
+
179
+ Returns:
180
+ Tuple of (success, message)
181
+ """
182
+ accounts = self._load_accounts()
183
+
184
+ if account not in accounts:
185
+ return False, f"Account '{account}' not found"
186
+
187
+ account_info = accounts[account]
188
+ provider = account_info["provider"]
189
+
190
+ # Get API key from keyring or prompt
191
+ api_key = None
192
+ if account_info.get("has_keyring"):
193
+ try:
194
+ api_key = keyring.get_password(f"hanzo-{provider}", account)
195
+ except:
196
+ pass
197
+
198
+ if not api_key:
199
+ # Try environment variable
200
+ for env_var in self.ENV_VARS.get(provider, []):
201
+ if env_var in os.environ:
202
+ api_key = os.environ[env_var]
203
+ break
204
+
205
+ if not api_key:
206
+ api_key = self._prompt_for_api_key(provider)
207
+ if not api_key:
208
+ return False, "No API key available"
209
+
210
+ # Backup current environment
211
+ self._backup_environment(provider)
212
+
213
+ # Set environment variables
214
+ for env_var in self.ENV_VARS.get(provider, []):
215
+ os.environ[env_var] = api_key
216
+
217
+ # Set active account
218
+ self.set_active_account(account)
219
+
220
+ # Update shell if using claude command
221
+ self._update_claude_command(account_info)
222
+
223
+ return True, f"Logged in as '{account}' ({provider})"
224
+
225
+ def logout(self) -> Tuple[bool, str]:
226
+ """Logout by clearing environment variables."""
227
+ current = self.get_active_account()
228
+
229
+ if not current or current == "default":
230
+ return False, "No active session"
231
+
232
+ accounts = self._load_accounts()
233
+ if current not in accounts:
234
+ return False, f"Unknown account: {current}"
235
+
236
+ provider = accounts[current]["provider"]
237
+
238
+ # Clear environment variables
239
+ for env_var in self.ENV_VARS.get(provider, []):
240
+ if env_var in os.environ:
241
+ del os.environ[env_var]
242
+
243
+ # Restore backed up environment if any
244
+ self._restore_environment(provider)
245
+
246
+ # Clear active account
247
+ if self.ACTIVE_ACCOUNT_FILE.exists():
248
+ self.ACTIVE_ACCOUNT_FILE.unlink()
249
+
250
+ return True, f"Logged out from '{current}'"
251
+
252
+ def _backup_environment(self, provider: str):
253
+ """Backup current environment variables."""
254
+ for env_var in self.ENV_VARS.get(provider, []):
255
+ if env_var in os.environ:
256
+ self._env_backup[env_var] = os.environ[env_var]
257
+
258
+ def _restore_environment(self, provider: str):
259
+ """Restore backed up environment variables."""
260
+ for env_var in self.ENV_VARS.get(provider, []):
261
+ if env_var in self._env_backup:
262
+ os.environ[env_var] = self._env_backup[env_var]
263
+ del self._env_backup[env_var]
264
+
265
+ def _update_claude_command(self, account_info: Dict[str, Any]):
266
+ """Update claude command configuration if needed."""
267
+ # Check if claude command exists
268
+ try:
269
+ result = subprocess.run(
270
+ ["which", "claude"],
271
+ capture_output=True,
272
+ text=True
273
+ )
274
+ if result.returncode == 0:
275
+ # Claude command exists, update its config
276
+ claude_config = Path.home() / ".claude" / "config.json"
277
+ if claude_config.exists():
278
+ try:
279
+ with open(claude_config, 'r') as f:
280
+ config = json.load(f)
281
+
282
+ # Update model if specified
283
+ if account_info.get("model"):
284
+ config["default_model"] = account_info["model"]
285
+
286
+ with open(claude_config, 'w') as f:
287
+ json.dump(config, f, indent=2)
288
+ except:
289
+ pass
290
+ except:
291
+ pass
292
+
293
+ def switch_account(self, account: str) -> Tuple[bool, str]:
294
+ """Switch to a different account."""
295
+ # Logout current
296
+ self.logout()
297
+
298
+ # Login to new account
299
+ return self.login(account)
300
+
301
+ def create_agent_account(
302
+ self,
303
+ agent_id: str,
304
+ provider: str = "claude",
305
+ parent_account: Optional[str] = None
306
+ ) -> Tuple[bool, str]:
307
+ """Create an account for a swarm agent.
308
+
309
+ Args:
310
+ agent_id: Unique agent identifier
311
+ provider: AI provider
312
+ parent_account: Parent account to clone from
313
+
314
+ Returns:
315
+ Tuple of (success, account_name)
316
+ """
317
+ agent_account = f"agent_{agent_id}"
318
+
319
+ # If parent account specified, clone its credentials
320
+ if parent_account:
321
+ parent_info = self.get_account_info(parent_account)
322
+ if not parent_info:
323
+ return False, f"Parent account '{parent_account}' not found"
324
+
325
+ # Get parent API key
326
+ api_key = None
327
+ if parent_info.get("has_keyring"):
328
+ try:
329
+ api_key = keyring.get_password(
330
+ f"hanzo-{parent_info['provider']}",
331
+ parent_account
332
+ )
333
+ except:
334
+ pass
335
+
336
+ if api_key:
337
+ success, msg = self.create_account(
338
+ agent_account,
339
+ provider=parent_info["provider"],
340
+ api_key=api_key,
341
+ model=parent_info.get("model"),
342
+ description=f"Agent account (parent: {parent_account})"
343
+ )
344
+ if success:
345
+ return True, agent_account
346
+
347
+ # Create with current environment
348
+ for env_var in self.ENV_VARS.get(provider, []):
349
+ if env_var in os.environ:
350
+ success, msg = self.create_account(
351
+ agent_account,
352
+ provider=provider,
353
+ api_key=os.environ[env_var],
354
+ model=self.DEFAULT_MODELS.get(provider),
355
+ description=f"Agent account for {agent_id}"
356
+ )
357
+ if success:
358
+ return True, agent_account
359
+
360
+ return False, "No credentials available for agent"
361
+
362
+ def get_agent_credentials(self, agent_id: str) -> Optional[APICredential]:
363
+ """Get credentials for an agent.
364
+
365
+ Args:
366
+ agent_id: Agent identifier
367
+
368
+ Returns:
369
+ APICredential if found
370
+ """
371
+ agent_account = f"agent_{agent_id}"
372
+ account_info = self.get_account_info(agent_account)
373
+
374
+ if not account_info:
375
+ return None
376
+
377
+ # Get API key
378
+ api_key = None
379
+ provider = account_info["provider"]
380
+
381
+ if account_info.get("has_keyring"):
382
+ try:
383
+ api_key = keyring.get_password(f"hanzo-{provider}", agent_account)
384
+ except:
385
+ pass
386
+
387
+ if not api_key:
388
+ # Try current environment
389
+ for env_var in self.ENV_VARS.get(provider, []):
390
+ if env_var in os.environ:
391
+ api_key = os.environ[env_var]
392
+ break
393
+
394
+ if not api_key:
395
+ return None
396
+
397
+ return APICredential(
398
+ provider=provider,
399
+ api_key=api_key,
400
+ model=account_info.get("model"),
401
+ description=account_info.get("description")
402
+ )
403
+
404
+
405
+ # Update swarm tool to use latest Sonnet
406
+ def get_latest_claude_model() -> str:
407
+ """Get the latest Claude model identifier."""
408
+ # As of the knowledge cutoff, this is the latest Sonnet
409
+ # In production, this could query an API for the latest model
410
+ return "claude-3-5-sonnet-20241022"
411
+
412
+
413
+ # Token counting using tiktoken (same as current implementation)
414
+ def count_tokens_streaming(text_stream) -> int:
415
+ """Count tokens in a streaming fashion.
416
+
417
+ This uses the same tiktoken approach as the truncate module,
418
+ but processes text as it streams.
419
+ """
420
+ import tiktoken
421
+
422
+ try:
423
+ # Use cl100k_base encoding (Claude/GPT-4 compatible)
424
+ encoding = tiktoken.get_encoding("cl100k_base")
425
+ except:
426
+ # Fallback to simple estimation
427
+ return len(text_stream) // 4
428
+
429
+ total_tokens = 0
430
+ for chunk in text_stream:
431
+ if isinstance(chunk, str):
432
+ total_tokens += len(encoding.encode(chunk))
433
+ elif isinstance(chunk, bytes):
434
+ total_tokens += len(encoding.encode(chunk.decode('utf-8', errors='ignore')))
435
+
436
+ return total_tokens
@@ -0,0 +1,194 @@
1
+ """Claude Code authentication tool.
2
+
3
+ This tool manages API keys and accounts for Claude Code and other AI coding tools.
4
+ """
5
+
6
+ from typing import Annotated, Optional, TypedDict, Unpack, final, override
7
+ from mcp.server import FastMCP
8
+ from mcp.server.fastmcp import Context as MCPContext
9
+ from pydantic import Field
10
+
11
+ from hanzo_mcp.tools.common.base import BaseTool
12
+ from hanzo_mcp.tools.common.context import create_tool_context
13
+ from hanzo_mcp.tools.agent.code_auth import CodeAuthManager
14
+
15
+
16
+ class CodeAuthParams(TypedDict, total=False):
17
+ """Parameters for code auth tool."""
18
+ action: str
19
+ account: Optional[str]
20
+ provider: Optional[str]
21
+ api_key: Optional[str]
22
+ model: Optional[str]
23
+ description: Optional[str]
24
+ agent_id: Optional[str]
25
+ parent_account: Optional[str]
26
+
27
+
28
+ @final
29
+ class CodeAuthTool(BaseTool):
30
+ """Tool for managing Claude Code authentication and API keys."""
31
+
32
+ @property
33
+ @override
34
+ def name(self) -> str:
35
+ """Get the tool name."""
36
+ return "code_auth"
37
+
38
+ @property
39
+ @override
40
+ def description(self) -> str:
41
+ """Get the tool description."""
42
+ return """Manage Claude Code and AI provider authentication.
43
+
44
+ Actions:
45
+ - status: Show current login status
46
+ - list: List all accounts
47
+ - create: Create a new account
48
+ - login: Login to an account
49
+ - logout: Logout current account
50
+ - switch: Switch between accounts
51
+ - agent: Create/get agent account
52
+
53
+ Examples:
54
+ code_auth status
55
+ code_auth list
56
+ code_auth create --account work --provider claude
57
+ code_auth login --account work
58
+ code_auth logout
59
+ code_auth switch --account personal
60
+ code_auth agent --agent_id swarm_1 --parent_account work
61
+
62
+ Providers: claude, openai, azure, deepseek, google, groq"""
63
+
64
+ def __init__(self):
65
+ """Initialize the code auth tool."""
66
+ self.auth_manager = CodeAuthManager()
67
+
68
+ @override
69
+ async def call(
70
+ self,
71
+ ctx: MCPContext,
72
+ **params: Unpack[CodeAuthParams],
73
+ ) -> str:
74
+ """Execute the code auth tool.
75
+
76
+ Args:
77
+ ctx: MCP context
78
+ **params: Tool parameters
79
+
80
+ Returns:
81
+ Result message
82
+ """
83
+ tool_ctx = create_tool_context(ctx)
84
+ await tool_ctx.set_tool_info(self.name)
85
+
86
+ action = params.get("action", "status")
87
+
88
+ if action == "status":
89
+ current = self.auth_manager.get_active_account()
90
+ if current:
91
+ info = self.auth_manager.get_account_info(current)
92
+ if info:
93
+ return f"Logged in as: {current} ({info['provider']})"
94
+ return "Not logged in"
95
+
96
+ elif action == "list":
97
+ accounts = self.auth_manager.list_accounts()
98
+ if not accounts:
99
+ return "No accounts configured"
100
+
101
+ current = self.auth_manager.get_active_account()
102
+ lines = ["Configured accounts:"]
103
+ for account in accounts:
104
+ info = self.auth_manager.get_account_info(account)
105
+ marker = " (active)" if account == current else ""
106
+ lines.append(f" - {account}: {info['provider']}{marker}")
107
+ return "\n".join(lines)
108
+
109
+ elif action == "create":
110
+ account = params.get("account")
111
+ if not account:
112
+ return "Error: account name required"
113
+
114
+ provider = params.get("provider", "claude")
115
+ api_key = params.get("api_key")
116
+ model = params.get("model")
117
+ description = params.get("description")
118
+
119
+ success, msg = self.auth_manager.create_account(
120
+ account, provider, api_key, model, description
121
+ )
122
+ return msg
123
+
124
+ elif action == "login":
125
+ account = params.get("account", "default")
126
+ success, msg = self.auth_manager.login(account)
127
+ return msg
128
+
129
+ elif action == "logout":
130
+ success, msg = self.auth_manager.logout()
131
+ return msg
132
+
133
+ elif action == "switch":
134
+ account = params.get("account")
135
+ if not account:
136
+ return "Error: account name required"
137
+
138
+ success, msg = self.auth_manager.switch_account(account)
139
+ return msg
140
+
141
+ elif action == "agent":
142
+ agent_id = params.get("agent_id")
143
+ if not agent_id:
144
+ return "Error: agent_id required"
145
+
146
+ provider = params.get("provider", "claude")
147
+ parent_account = params.get("parent_account")
148
+
149
+ # Try to create agent account
150
+ success, result = self.auth_manager.create_agent_account(
151
+ agent_id, provider, parent_account
152
+ )
153
+
154
+ if success:
155
+ # Get credentials
156
+ creds = self.auth_manager.get_agent_credentials(agent_id)
157
+ if creds:
158
+ return f"Agent account ready: {result} ({creds.provider})"
159
+ else:
160
+ return f"Agent account created but no credentials: {result}"
161
+ else:
162
+ return f"Failed to create agent account: {result}"
163
+
164
+ else:
165
+ return f"Unknown action: {action}. Use: status, list, create, login, logout, switch, agent"
166
+
167
+ @override
168
+ def register(self, mcp_server: FastMCP) -> None:
169
+ """Register this tool with the MCP server."""
170
+ tool_self = self
171
+
172
+ @mcp_server.tool(name=self.name, description=self.description)
173
+ async def code_auth(
174
+ ctx: MCPContext,
175
+ action: str = "status",
176
+ account: Optional[str] = None,
177
+ provider: Optional[str] = None,
178
+ api_key: Optional[str] = None,
179
+ model: Optional[str] = None,
180
+ description: Optional[str] = None,
181
+ agent_id: Optional[str] = None,
182
+ parent_account: Optional[str] = None,
183
+ ) -> str:
184
+ return await tool_self.call(
185
+ ctx,
186
+ action=action,
187
+ account=account,
188
+ provider=provider,
189
+ api_key=api_key,
190
+ model=model,
191
+ description=description,
192
+ agent_id=agent_id,
193
+ parent_account=parent_account,
194
+ )