superqode 0.1.5__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.
Files changed (288) hide show
  1. superqode/__init__.py +33 -0
  2. superqode/acp/__init__.py +23 -0
  3. superqode/acp/client.py +913 -0
  4. superqode/acp/permission_screen.py +457 -0
  5. superqode/acp/types.py +480 -0
  6. superqode/acp_discovery.py +856 -0
  7. superqode/agent/__init__.py +22 -0
  8. superqode/agent/edit_strategies.py +334 -0
  9. superqode/agent/loop.py +892 -0
  10. superqode/agent/qe_report_templates.py +39 -0
  11. superqode/agent/system_prompts.py +353 -0
  12. superqode/agent_output.py +721 -0
  13. superqode/agent_stream.py +953 -0
  14. superqode/agents/__init__.py +59 -0
  15. superqode/agents/acp_registry.py +305 -0
  16. superqode/agents/client.py +249 -0
  17. superqode/agents/data/augmentcode.com.toml +51 -0
  18. superqode/agents/data/cagent.dev.toml +51 -0
  19. superqode/agents/data/claude.com.toml +60 -0
  20. superqode/agents/data/codeassistant.dev.toml +51 -0
  21. superqode/agents/data/codex.openai.com.toml +57 -0
  22. superqode/agents/data/fastagent.ai.toml +66 -0
  23. superqode/agents/data/geminicli.com.toml +77 -0
  24. superqode/agents/data/goose.block.xyz.toml +54 -0
  25. superqode/agents/data/junie.jetbrains.com.toml +56 -0
  26. superqode/agents/data/kimi.moonshot.cn.toml +57 -0
  27. superqode/agents/data/llmlingagent.dev.toml +51 -0
  28. superqode/agents/data/molt.bot.toml +49 -0
  29. superqode/agents/data/opencode.ai.toml +60 -0
  30. superqode/agents/data/stakpak.dev.toml +51 -0
  31. superqode/agents/data/vtcode.dev.toml +51 -0
  32. superqode/agents/discovery.py +266 -0
  33. superqode/agents/messaging.py +160 -0
  34. superqode/agents/persona.py +166 -0
  35. superqode/agents/registry.py +421 -0
  36. superqode/agents/schema.py +72 -0
  37. superqode/agents/unified.py +367 -0
  38. superqode/app/__init__.py +111 -0
  39. superqode/app/constants.py +314 -0
  40. superqode/app/css.py +366 -0
  41. superqode/app/models.py +118 -0
  42. superqode/app/suggester.py +125 -0
  43. superqode/app/widgets.py +1591 -0
  44. superqode/app_enhanced.py +399 -0
  45. superqode/app_main.py +17187 -0
  46. superqode/approval.py +312 -0
  47. superqode/atomic.py +296 -0
  48. superqode/commands/__init__.py +1 -0
  49. superqode/commands/acp.py +965 -0
  50. superqode/commands/agents.py +180 -0
  51. superqode/commands/auth.py +278 -0
  52. superqode/commands/config.py +374 -0
  53. superqode/commands/init.py +826 -0
  54. superqode/commands/providers.py +819 -0
  55. superqode/commands/qe.py +1145 -0
  56. superqode/commands/roles.py +380 -0
  57. superqode/commands/serve.py +172 -0
  58. superqode/commands/suggestions.py +127 -0
  59. superqode/commands/superqe.py +460 -0
  60. superqode/config/__init__.py +51 -0
  61. superqode/config/loader.py +812 -0
  62. superqode/config/schema.py +498 -0
  63. superqode/core/__init__.py +111 -0
  64. superqode/core/roles.py +281 -0
  65. superqode/danger.py +386 -0
  66. superqode/data/superqode-template.yaml +1522 -0
  67. superqode/design_system.py +1080 -0
  68. superqode/dialogs/__init__.py +6 -0
  69. superqode/dialogs/base.py +39 -0
  70. superqode/dialogs/model.py +130 -0
  71. superqode/dialogs/provider.py +870 -0
  72. superqode/diff_view.py +919 -0
  73. superqode/enterprise.py +21 -0
  74. superqode/evaluation/__init__.py +25 -0
  75. superqode/evaluation/adapters.py +93 -0
  76. superqode/evaluation/behaviors.py +89 -0
  77. superqode/evaluation/engine.py +209 -0
  78. superqode/evaluation/scenarios.py +96 -0
  79. superqode/execution/__init__.py +36 -0
  80. superqode/execution/linter.py +538 -0
  81. superqode/execution/modes.py +347 -0
  82. superqode/execution/resolver.py +283 -0
  83. superqode/execution/runner.py +642 -0
  84. superqode/file_explorer.py +811 -0
  85. superqode/file_viewer.py +471 -0
  86. superqode/flash.py +183 -0
  87. superqode/guidance/__init__.py +58 -0
  88. superqode/guidance/config.py +203 -0
  89. superqode/guidance/prompts.py +71 -0
  90. superqode/harness/__init__.py +54 -0
  91. superqode/harness/accelerator.py +291 -0
  92. superqode/harness/config.py +319 -0
  93. superqode/harness/validator.py +147 -0
  94. superqode/history.py +279 -0
  95. superqode/integrations/superopt_runner.py +124 -0
  96. superqode/logging/__init__.py +49 -0
  97. superqode/logging/adapters.py +219 -0
  98. superqode/logging/formatter.py +923 -0
  99. superqode/logging/integration.py +341 -0
  100. superqode/logging/sinks.py +170 -0
  101. superqode/logging/unified_log.py +417 -0
  102. superqode/lsp/__init__.py +26 -0
  103. superqode/lsp/client.py +544 -0
  104. superqode/main.py +1069 -0
  105. superqode/mcp/__init__.py +89 -0
  106. superqode/mcp/auth_storage.py +380 -0
  107. superqode/mcp/client.py +1236 -0
  108. superqode/mcp/config.py +319 -0
  109. superqode/mcp/integration.py +337 -0
  110. superqode/mcp/oauth.py +436 -0
  111. superqode/mcp/oauth_callback.py +385 -0
  112. superqode/mcp/types.py +290 -0
  113. superqode/memory/__init__.py +31 -0
  114. superqode/memory/feedback.py +342 -0
  115. superqode/memory/store.py +522 -0
  116. superqode/notifications.py +369 -0
  117. superqode/optimization/__init__.py +5 -0
  118. superqode/optimization/config.py +33 -0
  119. superqode/permissions/__init__.py +25 -0
  120. superqode/permissions/rules.py +488 -0
  121. superqode/plan.py +323 -0
  122. superqode/providers/__init__.py +33 -0
  123. superqode/providers/gateway/__init__.py +165 -0
  124. superqode/providers/gateway/base.py +228 -0
  125. superqode/providers/gateway/litellm_gateway.py +1170 -0
  126. superqode/providers/gateway/openresponses_gateway.py +436 -0
  127. superqode/providers/health.py +297 -0
  128. superqode/providers/huggingface/__init__.py +74 -0
  129. superqode/providers/huggingface/downloader.py +472 -0
  130. superqode/providers/huggingface/endpoints.py +442 -0
  131. superqode/providers/huggingface/hub.py +531 -0
  132. superqode/providers/huggingface/inference.py +394 -0
  133. superqode/providers/huggingface/transformers_runner.py +516 -0
  134. superqode/providers/local/__init__.py +100 -0
  135. superqode/providers/local/base.py +438 -0
  136. superqode/providers/local/discovery.py +418 -0
  137. superqode/providers/local/lmstudio.py +256 -0
  138. superqode/providers/local/mlx.py +457 -0
  139. superqode/providers/local/ollama.py +486 -0
  140. superqode/providers/local/sglang.py +268 -0
  141. superqode/providers/local/tgi.py +260 -0
  142. superqode/providers/local/tool_support.py +477 -0
  143. superqode/providers/local/vllm.py +258 -0
  144. superqode/providers/manager.py +1338 -0
  145. superqode/providers/models.py +1016 -0
  146. superqode/providers/models_dev.py +578 -0
  147. superqode/providers/openresponses/__init__.py +87 -0
  148. superqode/providers/openresponses/converters/__init__.py +17 -0
  149. superqode/providers/openresponses/converters/messages.py +343 -0
  150. superqode/providers/openresponses/converters/tools.py +268 -0
  151. superqode/providers/openresponses/schema/__init__.py +56 -0
  152. superqode/providers/openresponses/schema/models.py +585 -0
  153. superqode/providers/openresponses/streaming/__init__.py +5 -0
  154. superqode/providers/openresponses/streaming/parser.py +338 -0
  155. superqode/providers/openresponses/tools/__init__.py +21 -0
  156. superqode/providers/openresponses/tools/apply_patch.py +352 -0
  157. superqode/providers/openresponses/tools/code_interpreter.py +290 -0
  158. superqode/providers/openresponses/tools/file_search.py +333 -0
  159. superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
  160. superqode/providers/registry.py +716 -0
  161. superqode/providers/usage.py +332 -0
  162. superqode/pure_mode.py +384 -0
  163. superqode/qr/__init__.py +23 -0
  164. superqode/qr/dashboard.py +781 -0
  165. superqode/qr/generator.py +1018 -0
  166. superqode/qr/templates.py +135 -0
  167. superqode/safety/__init__.py +41 -0
  168. superqode/safety/sandbox.py +413 -0
  169. superqode/safety/warnings.py +256 -0
  170. superqode/server/__init__.py +33 -0
  171. superqode/server/lsp_server.py +775 -0
  172. superqode/server/web.py +250 -0
  173. superqode/session/__init__.py +25 -0
  174. superqode/session/persistence.py +580 -0
  175. superqode/session/sharing.py +477 -0
  176. superqode/session.py +475 -0
  177. superqode/sidebar.py +2991 -0
  178. superqode/stream_view.py +648 -0
  179. superqode/styles/__init__.py +3 -0
  180. superqode/superqe/__init__.py +184 -0
  181. superqode/superqe/acp_runner.py +1064 -0
  182. superqode/superqe/constitution/__init__.py +62 -0
  183. superqode/superqe/constitution/evaluator.py +308 -0
  184. superqode/superqe/constitution/loader.py +432 -0
  185. superqode/superqe/constitution/schema.py +250 -0
  186. superqode/superqe/events.py +591 -0
  187. superqode/superqe/frameworks/__init__.py +65 -0
  188. superqode/superqe/frameworks/base.py +234 -0
  189. superqode/superqe/frameworks/e2e.py +263 -0
  190. superqode/superqe/frameworks/executor.py +237 -0
  191. superqode/superqe/frameworks/javascript.py +409 -0
  192. superqode/superqe/frameworks/python.py +373 -0
  193. superqode/superqe/frameworks/registry.py +92 -0
  194. superqode/superqe/mcp_tools/__init__.py +47 -0
  195. superqode/superqe/mcp_tools/core_tools.py +418 -0
  196. superqode/superqe/mcp_tools/registry.py +230 -0
  197. superqode/superqe/mcp_tools/testing_tools.py +167 -0
  198. superqode/superqe/noise.py +89 -0
  199. superqode/superqe/orchestrator.py +778 -0
  200. superqode/superqe/roles.py +609 -0
  201. superqode/superqe/session.py +713 -0
  202. superqode/superqe/skills/__init__.py +57 -0
  203. superqode/superqe/skills/base.py +106 -0
  204. superqode/superqe/skills/core_skills.py +899 -0
  205. superqode/superqe/skills/registry.py +90 -0
  206. superqode/superqe/verifier.py +101 -0
  207. superqode/superqe_cli.py +76 -0
  208. superqode/tool_call.py +358 -0
  209. superqode/tools/__init__.py +93 -0
  210. superqode/tools/agent_tools.py +496 -0
  211. superqode/tools/base.py +324 -0
  212. superqode/tools/batch_tool.py +133 -0
  213. superqode/tools/diagnostics.py +311 -0
  214. superqode/tools/edit_tools.py +653 -0
  215. superqode/tools/enhanced_base.py +515 -0
  216. superqode/tools/file_tools.py +269 -0
  217. superqode/tools/file_tracking.py +45 -0
  218. superqode/tools/lsp_tools.py +610 -0
  219. superqode/tools/network_tools.py +350 -0
  220. superqode/tools/permissions.py +400 -0
  221. superqode/tools/question_tool.py +324 -0
  222. superqode/tools/search_tools.py +598 -0
  223. superqode/tools/shell_tools.py +259 -0
  224. superqode/tools/todo_tools.py +121 -0
  225. superqode/tools/validation.py +80 -0
  226. superqode/tools/web_tools.py +639 -0
  227. superqode/tui.py +1152 -0
  228. superqode/tui_integration.py +875 -0
  229. superqode/tui_widgets/__init__.py +27 -0
  230. superqode/tui_widgets/widgets/__init__.py +18 -0
  231. superqode/tui_widgets/widgets/progress.py +185 -0
  232. superqode/tui_widgets/widgets/tool_display.py +188 -0
  233. superqode/undo_manager.py +574 -0
  234. superqode/utils/__init__.py +5 -0
  235. superqode/utils/error_handling.py +323 -0
  236. superqode/utils/fuzzy.py +257 -0
  237. superqode/widgets/__init__.py +477 -0
  238. superqode/widgets/agent_collab.py +390 -0
  239. superqode/widgets/agent_store.py +936 -0
  240. superqode/widgets/agent_switcher.py +395 -0
  241. superqode/widgets/animation_manager.py +284 -0
  242. superqode/widgets/code_context.py +356 -0
  243. superqode/widgets/command_palette.py +412 -0
  244. superqode/widgets/connection_status.py +537 -0
  245. superqode/widgets/conversation_history.py +470 -0
  246. superqode/widgets/diff_indicator.py +155 -0
  247. superqode/widgets/enhanced_status_bar.py +385 -0
  248. superqode/widgets/enhanced_toast.py +476 -0
  249. superqode/widgets/file_browser.py +809 -0
  250. superqode/widgets/file_reference.py +585 -0
  251. superqode/widgets/issue_timeline.py +340 -0
  252. superqode/widgets/leader_key.py +264 -0
  253. superqode/widgets/mode_switcher.py +445 -0
  254. superqode/widgets/model_picker.py +234 -0
  255. superqode/widgets/permission_preview.py +1205 -0
  256. superqode/widgets/prompt.py +358 -0
  257. superqode/widgets/provider_connect.py +725 -0
  258. superqode/widgets/pty_shell.py +587 -0
  259. superqode/widgets/qe_dashboard.py +321 -0
  260. superqode/widgets/resizable_sidebar.py +377 -0
  261. superqode/widgets/response_changes.py +218 -0
  262. superqode/widgets/response_display.py +528 -0
  263. superqode/widgets/rich_tool_display.py +613 -0
  264. superqode/widgets/sidebar_panels.py +1180 -0
  265. superqode/widgets/slash_complete.py +356 -0
  266. superqode/widgets/split_view.py +612 -0
  267. superqode/widgets/status_bar.py +273 -0
  268. superqode/widgets/superqode_display.py +786 -0
  269. superqode/widgets/thinking_display.py +815 -0
  270. superqode/widgets/throbber.py +87 -0
  271. superqode/widgets/toast.py +206 -0
  272. superqode/widgets/unified_output.py +1073 -0
  273. superqode/workspace/__init__.py +75 -0
  274. superqode/workspace/artifacts.py +472 -0
  275. superqode/workspace/coordinator.py +353 -0
  276. superqode/workspace/diff_tracker.py +429 -0
  277. superqode/workspace/git_guard.py +373 -0
  278. superqode/workspace/git_snapshot.py +526 -0
  279. superqode/workspace/manager.py +750 -0
  280. superqode/workspace/snapshot.py +357 -0
  281. superqode/workspace/watcher.py +535 -0
  282. superqode/workspace/worktree.py +440 -0
  283. superqode-0.1.5.dist-info/METADATA +204 -0
  284. superqode-0.1.5.dist-info/RECORD +288 -0
  285. superqode-0.1.5.dist-info/WHEEL +5 -0
  286. superqode-0.1.5.dist-info/entry_points.txt +3 -0
  287. superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
  288. superqode-0.1.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,89 @@
1
+ """MCP (Model Context Protocol) support for SuperQode.
2
+
3
+ This module provides full MCP client support, allowing SuperQode to connect
4
+ to local and remote MCP servers for tool execution, resource access, and
5
+ prompt management. Implements the MCP protocol aligned with Zed editor's
6
+ implementation.
7
+
8
+ Features:
9
+ - Connect to local MCP servers via stdio (subprocess)
10
+ - Connect to remote MCP servers via HTTP or SSE
11
+ - Automatic tool/resource/prompt discovery on connection
12
+ - Tool execution with result formatting for agents
13
+ - Resource reading and subscription support
14
+ - Prompt retrieval with argument completion
15
+ - Logging level control
16
+ - Connection state management with callbacks
17
+ - Multi-server support with tool aggregation
18
+ """
19
+
20
+ from superqode.mcp.config import (
21
+ MCPServerConfig,
22
+ MCPStdioConfig,
23
+ MCPHttpConfig,
24
+ MCPSSEConfig,
25
+ load_mcp_config,
26
+ save_mcp_config,
27
+ create_default_mcp_config,
28
+ )
29
+ from superqode.mcp.client import (
30
+ MCPClientManager,
31
+ MCPConnection,
32
+ MCPConnectionState,
33
+ LATEST_PROTOCOL_VERSION,
34
+ SUPPORTED_PROTOCOL_VERSIONS,
35
+ )
36
+ from superqode.mcp.types import (
37
+ MCPTool,
38
+ MCPResource,
39
+ MCPResourceTemplate,
40
+ MCPPrompt,
41
+ MCPPromptArgument,
42
+ MCPToolResult,
43
+ MCPResourceContent,
44
+ MCPPromptResult,
45
+ MCPPromptMessage,
46
+ MCPCompletionResult,
47
+ MCPServerCapabilities,
48
+ MCPServerInfo,
49
+ MCPProgress,
50
+ MCPRoot,
51
+ ServerCapability,
52
+ LoggingLevel,
53
+ ToolAnnotations,
54
+ )
55
+
56
+ __all__ = [
57
+ # Config
58
+ "MCPServerConfig",
59
+ "MCPStdioConfig",
60
+ "MCPHttpConfig",
61
+ "MCPSSEConfig",
62
+ "load_mcp_config",
63
+ "save_mcp_config",
64
+ "create_default_mcp_config",
65
+ # Client
66
+ "MCPClientManager",
67
+ "MCPConnection",
68
+ "MCPConnectionState",
69
+ "LATEST_PROTOCOL_VERSION",
70
+ "SUPPORTED_PROTOCOL_VERSIONS",
71
+ # Types
72
+ "MCPTool",
73
+ "MCPResource",
74
+ "MCPResourceTemplate",
75
+ "MCPPrompt",
76
+ "MCPPromptArgument",
77
+ "MCPToolResult",
78
+ "MCPResourceContent",
79
+ "MCPPromptResult",
80
+ "MCPPromptMessage",
81
+ "MCPCompletionResult",
82
+ "MCPServerCapabilities",
83
+ "MCPServerInfo",
84
+ "MCPProgress",
85
+ "MCPRoot",
86
+ "ServerCapability",
87
+ "LoggingLevel",
88
+ "ToolAnnotations",
89
+ ]
@@ -0,0 +1,380 @@
1
+ """
2
+ MCP Auth Storage - Secure Storage for OAuth Credentials.
3
+
4
+ Provides secure storage for MCP server authentication credentials
5
+ including OAuth tokens and API keys.
6
+
7
+ Security Features:
8
+ - File permissions set to 0o600 (owner read/write only)
9
+ - Credentials bound to server URL
10
+ - Token expiry tracking
11
+ - Automatic cleanup of expired tokens
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import hashlib
17
+ import json
18
+ import logging
19
+ import os
20
+ import stat
21
+ from dataclasses import dataclass
22
+ from datetime import datetime
23
+ from pathlib import Path
24
+ from typing import Any, Dict, Optional
25
+
26
+ from .oauth import OAuthTokens
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ @dataclass
32
+ class ServerCredentials:
33
+ """Credentials for an MCP server."""
34
+
35
+ server_url: str
36
+ server_url_hash: str # Hash for filename safety
37
+ oauth_tokens: Optional[OAuthTokens] = None
38
+ api_key: Optional[str] = None
39
+ bearer_token: Optional[str] = None
40
+ created_at: Optional[datetime] = None
41
+ updated_at: Optional[datetime] = None
42
+
43
+ def to_dict(self) -> Dict[str, Any]:
44
+ """Convert to dictionary for storage."""
45
+ result: Dict[str, Any] = {
46
+ "server_url": self.server_url,
47
+ "server_url_hash": self.server_url_hash,
48
+ "created_at": self.created_at.isoformat() if self.created_at else None,
49
+ "updated_at": self.updated_at.isoformat() if self.updated_at else None,
50
+ }
51
+
52
+ if self.oauth_tokens:
53
+ result["oauth_tokens"] = self.oauth_tokens.to_dict()
54
+
55
+ if self.api_key:
56
+ result["api_key"] = self.api_key
57
+
58
+ if self.bearer_token:
59
+ result["bearer_token"] = self.bearer_token
60
+
61
+ return result
62
+
63
+ @classmethod
64
+ def from_dict(cls, data: Dict[str, Any]) -> "ServerCredentials":
65
+ """Create from dictionary."""
66
+ oauth_tokens = None
67
+ if "oauth_tokens" in data:
68
+ oauth_tokens = OAuthTokens.from_dict(data["oauth_tokens"])
69
+
70
+ created_at = None
71
+ if data.get("created_at"):
72
+ created_at = datetime.fromisoformat(data["created_at"])
73
+
74
+ updated_at = None
75
+ if data.get("updated_at"):
76
+ updated_at = datetime.fromisoformat(data["updated_at"])
77
+
78
+ return cls(
79
+ server_url=data["server_url"],
80
+ server_url_hash=data["server_url_hash"],
81
+ oauth_tokens=oauth_tokens,
82
+ api_key=data.get("api_key"),
83
+ bearer_token=data.get("bearer_token"),
84
+ created_at=created_at,
85
+ updated_at=updated_at,
86
+ )
87
+
88
+
89
+ class MCPAuthStorage:
90
+ """
91
+ Secure storage for MCP OAuth credentials.
92
+
93
+ Stores credentials in ~/.superqode/mcp-auth/ with one file per server.
94
+ Files are created with restricted permissions (0o600).
95
+
96
+ Usage:
97
+ storage = MCPAuthStorage()
98
+
99
+ # Save tokens
100
+ storage.save_tokens(server_url, tokens)
101
+
102
+ # Load tokens
103
+ tokens = storage.load_tokens(server_url)
104
+
105
+ # Clear tokens
106
+ storage.clear_tokens(server_url)
107
+ """
108
+
109
+ DEFAULT_DIR = Path.home() / ".superqode" / "mcp-auth"
110
+
111
+ def __init__(self, storage_dir: Optional[Path] = None):
112
+ self.storage_dir = storage_dir or self.DEFAULT_DIR
113
+ self._ensure_storage_dir()
114
+
115
+ def _ensure_storage_dir(self) -> None:
116
+ """Ensure storage directory exists with proper permissions."""
117
+ if not self.storage_dir.exists():
118
+ self.storage_dir.mkdir(parents=True, mode=0o700)
119
+ else:
120
+ # Ensure permissions are correct
121
+ current_mode = self.storage_dir.stat().st_mode & 0o777
122
+ if current_mode != 0o700:
123
+ self.storage_dir.chmod(0o700)
124
+
125
+ def _url_hash(self, url: str) -> str:
126
+ """Generate a safe hash from a URL for use as filename."""
127
+ return hashlib.sha256(url.encode()).hexdigest()[:16]
128
+
129
+ def _get_credentials_path(self, server_url: str) -> Path:
130
+ """Get the path to the credentials file for a server."""
131
+ url_hash = self._url_hash(server_url)
132
+ return self.storage_dir / f"{url_hash}.json"
133
+
134
+ def save_tokens(
135
+ self,
136
+ server_url: str,
137
+ tokens: OAuthTokens,
138
+ ) -> None:
139
+ """
140
+ Save OAuth tokens for a server.
141
+
142
+ Args:
143
+ server_url: The MCP server URL
144
+ tokens: OAuth tokens to save
145
+ """
146
+ creds_path = self._get_credentials_path(server_url)
147
+
148
+ # Load existing credentials or create new
149
+ try:
150
+ credentials = self._load_credentials(server_url)
151
+ if credentials:
152
+ credentials.oauth_tokens = tokens
153
+ credentials.updated_at = datetime.now()
154
+ else:
155
+ credentials = ServerCredentials(
156
+ server_url=server_url,
157
+ server_url_hash=self._url_hash(server_url),
158
+ oauth_tokens=tokens,
159
+ created_at=datetime.now(),
160
+ updated_at=datetime.now(),
161
+ )
162
+ except Exception:
163
+ credentials = ServerCredentials(
164
+ server_url=server_url,
165
+ server_url_hash=self._url_hash(server_url),
166
+ oauth_tokens=tokens,
167
+ created_at=datetime.now(),
168
+ updated_at=datetime.now(),
169
+ )
170
+
171
+ # Write to file with secure permissions
172
+ self._save_credentials(credentials)
173
+ logger.debug(f"Saved OAuth tokens for {server_url}")
174
+
175
+ def load_tokens(self, server_url: str) -> Optional[OAuthTokens]:
176
+ """
177
+ Load OAuth tokens for a server.
178
+
179
+ Args:
180
+ server_url: The MCP server URL
181
+
182
+ Returns:
183
+ OAuthTokens if found and valid, None otherwise
184
+ """
185
+ credentials = self._load_credentials(server_url)
186
+ if credentials and credentials.oauth_tokens:
187
+ return credentials.oauth_tokens
188
+ return None
189
+
190
+ def clear_tokens(self, server_url: str) -> None:
191
+ """
192
+ Clear OAuth tokens for a server.
193
+
194
+ Args:
195
+ server_url: The MCP server URL
196
+ """
197
+ creds_path = self._get_credentials_path(server_url)
198
+
199
+ if creds_path.exists():
200
+ # Load credentials to check if we should keep other data
201
+ credentials = self._load_credentials(server_url)
202
+ if credentials:
203
+ credentials.oauth_tokens = None
204
+ credentials.updated_at = datetime.now()
205
+
206
+ # If no other credentials, delete the file
207
+ if not credentials.api_key and not credentials.bearer_token:
208
+ creds_path.unlink()
209
+ logger.debug(f"Deleted credentials file for {server_url}")
210
+ else:
211
+ self._save_credentials(credentials)
212
+ logger.debug(f"Cleared OAuth tokens for {server_url}")
213
+
214
+ def save_api_key(self, server_url: str, api_key: str) -> None:
215
+ """
216
+ Save an API key for a server.
217
+
218
+ Args:
219
+ server_url: The MCP server URL
220
+ api_key: The API key to save
221
+ """
222
+ credentials = self._load_credentials(server_url)
223
+ if credentials:
224
+ credentials.api_key = api_key
225
+ credentials.updated_at = datetime.now()
226
+ else:
227
+ credentials = ServerCredentials(
228
+ server_url=server_url,
229
+ server_url_hash=self._url_hash(server_url),
230
+ api_key=api_key,
231
+ created_at=datetime.now(),
232
+ updated_at=datetime.now(),
233
+ )
234
+
235
+ self._save_credentials(credentials)
236
+ logger.debug(f"Saved API key for {server_url}")
237
+
238
+ def load_api_key(self, server_url: str) -> Optional[str]:
239
+ """Load an API key for a server."""
240
+ credentials = self._load_credentials(server_url)
241
+ if credentials:
242
+ return credentials.api_key
243
+ return None
244
+
245
+ def save_bearer_token(self, server_url: str, token: str) -> None:
246
+ """
247
+ Save a bearer token for a server.
248
+
249
+ Args:
250
+ server_url: The MCP server URL
251
+ token: The bearer token to save
252
+ """
253
+ credentials = self._load_credentials(server_url)
254
+ if credentials:
255
+ credentials.bearer_token = token
256
+ credentials.updated_at = datetime.now()
257
+ else:
258
+ credentials = ServerCredentials(
259
+ server_url=server_url,
260
+ server_url_hash=self._url_hash(server_url),
261
+ bearer_token=token,
262
+ created_at=datetime.now(),
263
+ updated_at=datetime.now(),
264
+ )
265
+
266
+ self._save_credentials(credentials)
267
+ logger.debug(f"Saved bearer token for {server_url}")
268
+
269
+ def load_bearer_token(self, server_url: str) -> Optional[str]:
270
+ """Load a bearer token for a server."""
271
+ credentials = self._load_credentials(server_url)
272
+ if credentials:
273
+ return credentials.bearer_token
274
+ return None
275
+
276
+ def get_credentials(self, server_url: str) -> Optional[ServerCredentials]:
277
+ """Get all credentials for a server."""
278
+ return self._load_credentials(server_url)
279
+
280
+ def list_servers(self) -> list[str]:
281
+ """List all servers with stored credentials."""
282
+ servers = []
283
+ for creds_file in self.storage_dir.glob("*.json"):
284
+ try:
285
+ with open(creds_file, "r") as f:
286
+ data = json.load(f)
287
+ if "server_url" in data:
288
+ servers.append(data["server_url"])
289
+ except Exception:
290
+ pass
291
+ return servers
292
+
293
+ def clear_all(self) -> None:
294
+ """Clear all stored credentials."""
295
+ for creds_file in self.storage_dir.glob("*.json"):
296
+ try:
297
+ creds_file.unlink()
298
+ except Exception as e:
299
+ logger.warning(f"Failed to delete {creds_file}: {e}")
300
+ logger.info("Cleared all MCP credentials")
301
+
302
+ def cleanup_expired(self) -> int:
303
+ """
304
+ Clean up expired OAuth tokens.
305
+
306
+ Returns the number of expired tokens removed.
307
+ """
308
+ count = 0
309
+ for server_url in self.list_servers():
310
+ credentials = self._load_credentials(server_url)
311
+ if credentials and credentials.oauth_tokens:
312
+ tokens = credentials.oauth_tokens
313
+ if tokens.is_expired() and not tokens.refresh_token:
314
+ # Token is expired and can't be refreshed
315
+ self.clear_tokens(server_url)
316
+ count += 1
317
+ logger.debug(f"Cleaned up expired token for {server_url}")
318
+ return count
319
+
320
+ def _load_credentials(self, server_url: str) -> Optional[ServerCredentials]:
321
+ """Load credentials from file."""
322
+ creds_path = self._get_credentials_path(server_url)
323
+
324
+ if not creds_path.exists():
325
+ return None
326
+
327
+ try:
328
+ with open(creds_path, "r") as f:
329
+ data = json.load(f)
330
+
331
+ # Verify URL matches (security check)
332
+ if data.get("server_url") != server_url:
333
+ logger.warning(f"URL mismatch in credentials file: {creds_path}")
334
+ return None
335
+
336
+ return ServerCredentials.from_dict(data)
337
+
338
+ except (json.JSONDecodeError, KeyError) as e:
339
+ logger.warning(f"Invalid credentials file {creds_path}: {e}")
340
+ return None
341
+
342
+ def _save_credentials(self, credentials: ServerCredentials) -> None:
343
+ """Save credentials to file with secure permissions."""
344
+ creds_path = self._get_credentials_path(credentials.server_url)
345
+
346
+ # Write to temp file first
347
+ temp_path = creds_path.with_suffix(".tmp")
348
+
349
+ try:
350
+ # Create file with restricted permissions
351
+ fd = os.open(
352
+ temp_path,
353
+ os.O_WRONLY | os.O_CREAT | os.O_TRUNC,
354
+ 0o600,
355
+ )
356
+ with os.fdopen(fd, "w") as f:
357
+ json.dump(credentials.to_dict(), f, indent=2)
358
+
359
+ # Atomic rename
360
+ temp_path.rename(creds_path)
361
+
362
+ except Exception as e:
363
+ # Clean up temp file if exists
364
+ if temp_path.exists():
365
+ temp_path.unlink()
366
+ raise e
367
+
368
+
369
+ # Global storage instance
370
+ _global_storage: Optional[MCPAuthStorage] = None
371
+
372
+
373
+ def get_auth_storage() -> MCPAuthStorage:
374
+ """Get the global auth storage instance."""
375
+ global _global_storage
376
+
377
+ if _global_storage is None:
378
+ _global_storage = MCPAuthStorage()
379
+
380
+ return _global_storage