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.
- superqode/__init__.py +33 -0
- superqode/acp/__init__.py +23 -0
- superqode/acp/client.py +913 -0
- superqode/acp/permission_screen.py +457 -0
- superqode/acp/types.py +480 -0
- superqode/acp_discovery.py +856 -0
- superqode/agent/__init__.py +22 -0
- superqode/agent/edit_strategies.py +334 -0
- superqode/agent/loop.py +892 -0
- superqode/agent/qe_report_templates.py +39 -0
- superqode/agent/system_prompts.py +353 -0
- superqode/agent_output.py +721 -0
- superqode/agent_stream.py +953 -0
- superqode/agents/__init__.py +59 -0
- superqode/agents/acp_registry.py +305 -0
- superqode/agents/client.py +249 -0
- superqode/agents/data/augmentcode.com.toml +51 -0
- superqode/agents/data/cagent.dev.toml +51 -0
- superqode/agents/data/claude.com.toml +60 -0
- superqode/agents/data/codeassistant.dev.toml +51 -0
- superqode/agents/data/codex.openai.com.toml +57 -0
- superqode/agents/data/fastagent.ai.toml +66 -0
- superqode/agents/data/geminicli.com.toml +77 -0
- superqode/agents/data/goose.block.xyz.toml +54 -0
- superqode/agents/data/junie.jetbrains.com.toml +56 -0
- superqode/agents/data/kimi.moonshot.cn.toml +57 -0
- superqode/agents/data/llmlingagent.dev.toml +51 -0
- superqode/agents/data/molt.bot.toml +49 -0
- superqode/agents/data/opencode.ai.toml +60 -0
- superqode/agents/data/stakpak.dev.toml +51 -0
- superqode/agents/data/vtcode.dev.toml +51 -0
- superqode/agents/discovery.py +266 -0
- superqode/agents/messaging.py +160 -0
- superqode/agents/persona.py +166 -0
- superqode/agents/registry.py +421 -0
- superqode/agents/schema.py +72 -0
- superqode/agents/unified.py +367 -0
- superqode/app/__init__.py +111 -0
- superqode/app/constants.py +314 -0
- superqode/app/css.py +366 -0
- superqode/app/models.py +118 -0
- superqode/app/suggester.py +125 -0
- superqode/app/widgets.py +1591 -0
- superqode/app_enhanced.py +399 -0
- superqode/app_main.py +17187 -0
- superqode/approval.py +312 -0
- superqode/atomic.py +296 -0
- superqode/commands/__init__.py +1 -0
- superqode/commands/acp.py +965 -0
- superqode/commands/agents.py +180 -0
- superqode/commands/auth.py +278 -0
- superqode/commands/config.py +374 -0
- superqode/commands/init.py +826 -0
- superqode/commands/providers.py +819 -0
- superqode/commands/qe.py +1145 -0
- superqode/commands/roles.py +380 -0
- superqode/commands/serve.py +172 -0
- superqode/commands/suggestions.py +127 -0
- superqode/commands/superqe.py +460 -0
- superqode/config/__init__.py +51 -0
- superqode/config/loader.py +812 -0
- superqode/config/schema.py +498 -0
- superqode/core/__init__.py +111 -0
- superqode/core/roles.py +281 -0
- superqode/danger.py +386 -0
- superqode/data/superqode-template.yaml +1522 -0
- superqode/design_system.py +1080 -0
- superqode/dialogs/__init__.py +6 -0
- superqode/dialogs/base.py +39 -0
- superqode/dialogs/model.py +130 -0
- superqode/dialogs/provider.py +870 -0
- superqode/diff_view.py +919 -0
- superqode/enterprise.py +21 -0
- superqode/evaluation/__init__.py +25 -0
- superqode/evaluation/adapters.py +93 -0
- superqode/evaluation/behaviors.py +89 -0
- superqode/evaluation/engine.py +209 -0
- superqode/evaluation/scenarios.py +96 -0
- superqode/execution/__init__.py +36 -0
- superqode/execution/linter.py +538 -0
- superqode/execution/modes.py +347 -0
- superqode/execution/resolver.py +283 -0
- superqode/execution/runner.py +642 -0
- superqode/file_explorer.py +811 -0
- superqode/file_viewer.py +471 -0
- superqode/flash.py +183 -0
- superqode/guidance/__init__.py +58 -0
- superqode/guidance/config.py +203 -0
- superqode/guidance/prompts.py +71 -0
- superqode/harness/__init__.py +54 -0
- superqode/harness/accelerator.py +291 -0
- superqode/harness/config.py +319 -0
- superqode/harness/validator.py +147 -0
- superqode/history.py +279 -0
- superqode/integrations/superopt_runner.py +124 -0
- superqode/logging/__init__.py +49 -0
- superqode/logging/adapters.py +219 -0
- superqode/logging/formatter.py +923 -0
- superqode/logging/integration.py +341 -0
- superqode/logging/sinks.py +170 -0
- superqode/logging/unified_log.py +417 -0
- superqode/lsp/__init__.py +26 -0
- superqode/lsp/client.py +544 -0
- superqode/main.py +1069 -0
- superqode/mcp/__init__.py +89 -0
- superqode/mcp/auth_storage.py +380 -0
- superqode/mcp/client.py +1236 -0
- superqode/mcp/config.py +319 -0
- superqode/mcp/integration.py +337 -0
- superqode/mcp/oauth.py +436 -0
- superqode/mcp/oauth_callback.py +385 -0
- superqode/mcp/types.py +290 -0
- superqode/memory/__init__.py +31 -0
- superqode/memory/feedback.py +342 -0
- superqode/memory/store.py +522 -0
- superqode/notifications.py +369 -0
- superqode/optimization/__init__.py +5 -0
- superqode/optimization/config.py +33 -0
- superqode/permissions/__init__.py +25 -0
- superqode/permissions/rules.py +488 -0
- superqode/plan.py +323 -0
- superqode/providers/__init__.py +33 -0
- superqode/providers/gateway/__init__.py +165 -0
- superqode/providers/gateway/base.py +228 -0
- superqode/providers/gateway/litellm_gateway.py +1170 -0
- superqode/providers/gateway/openresponses_gateway.py +436 -0
- superqode/providers/health.py +297 -0
- superqode/providers/huggingface/__init__.py +74 -0
- superqode/providers/huggingface/downloader.py +472 -0
- superqode/providers/huggingface/endpoints.py +442 -0
- superqode/providers/huggingface/hub.py +531 -0
- superqode/providers/huggingface/inference.py +394 -0
- superqode/providers/huggingface/transformers_runner.py +516 -0
- superqode/providers/local/__init__.py +100 -0
- superqode/providers/local/base.py +438 -0
- superqode/providers/local/discovery.py +418 -0
- superqode/providers/local/lmstudio.py +256 -0
- superqode/providers/local/mlx.py +457 -0
- superqode/providers/local/ollama.py +486 -0
- superqode/providers/local/sglang.py +268 -0
- superqode/providers/local/tgi.py +260 -0
- superqode/providers/local/tool_support.py +477 -0
- superqode/providers/local/vllm.py +258 -0
- superqode/providers/manager.py +1338 -0
- superqode/providers/models.py +1016 -0
- superqode/providers/models_dev.py +578 -0
- superqode/providers/openresponses/__init__.py +87 -0
- superqode/providers/openresponses/converters/__init__.py +17 -0
- superqode/providers/openresponses/converters/messages.py +343 -0
- superqode/providers/openresponses/converters/tools.py +268 -0
- superqode/providers/openresponses/schema/__init__.py +56 -0
- superqode/providers/openresponses/schema/models.py +585 -0
- superqode/providers/openresponses/streaming/__init__.py +5 -0
- superqode/providers/openresponses/streaming/parser.py +338 -0
- superqode/providers/openresponses/tools/__init__.py +21 -0
- superqode/providers/openresponses/tools/apply_patch.py +352 -0
- superqode/providers/openresponses/tools/code_interpreter.py +290 -0
- superqode/providers/openresponses/tools/file_search.py +333 -0
- superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
- superqode/providers/registry.py +716 -0
- superqode/providers/usage.py +332 -0
- superqode/pure_mode.py +384 -0
- superqode/qr/__init__.py +23 -0
- superqode/qr/dashboard.py +781 -0
- superqode/qr/generator.py +1018 -0
- superqode/qr/templates.py +135 -0
- superqode/safety/__init__.py +41 -0
- superqode/safety/sandbox.py +413 -0
- superqode/safety/warnings.py +256 -0
- superqode/server/__init__.py +33 -0
- superqode/server/lsp_server.py +775 -0
- superqode/server/web.py +250 -0
- superqode/session/__init__.py +25 -0
- superqode/session/persistence.py +580 -0
- superqode/session/sharing.py +477 -0
- superqode/session.py +475 -0
- superqode/sidebar.py +2991 -0
- superqode/stream_view.py +648 -0
- superqode/styles/__init__.py +3 -0
- superqode/superqe/__init__.py +184 -0
- superqode/superqe/acp_runner.py +1064 -0
- superqode/superqe/constitution/__init__.py +62 -0
- superqode/superqe/constitution/evaluator.py +308 -0
- superqode/superqe/constitution/loader.py +432 -0
- superqode/superqe/constitution/schema.py +250 -0
- superqode/superqe/events.py +591 -0
- superqode/superqe/frameworks/__init__.py +65 -0
- superqode/superqe/frameworks/base.py +234 -0
- superqode/superqe/frameworks/e2e.py +263 -0
- superqode/superqe/frameworks/executor.py +237 -0
- superqode/superqe/frameworks/javascript.py +409 -0
- superqode/superqe/frameworks/python.py +373 -0
- superqode/superqe/frameworks/registry.py +92 -0
- superqode/superqe/mcp_tools/__init__.py +47 -0
- superqode/superqe/mcp_tools/core_tools.py +418 -0
- superqode/superqe/mcp_tools/registry.py +230 -0
- superqode/superqe/mcp_tools/testing_tools.py +167 -0
- superqode/superqe/noise.py +89 -0
- superqode/superqe/orchestrator.py +778 -0
- superqode/superqe/roles.py +609 -0
- superqode/superqe/session.py +713 -0
- superqode/superqe/skills/__init__.py +57 -0
- superqode/superqe/skills/base.py +106 -0
- superqode/superqe/skills/core_skills.py +899 -0
- superqode/superqe/skills/registry.py +90 -0
- superqode/superqe/verifier.py +101 -0
- superqode/superqe_cli.py +76 -0
- superqode/tool_call.py +358 -0
- superqode/tools/__init__.py +93 -0
- superqode/tools/agent_tools.py +496 -0
- superqode/tools/base.py +324 -0
- superqode/tools/batch_tool.py +133 -0
- superqode/tools/diagnostics.py +311 -0
- superqode/tools/edit_tools.py +653 -0
- superqode/tools/enhanced_base.py +515 -0
- superqode/tools/file_tools.py +269 -0
- superqode/tools/file_tracking.py +45 -0
- superqode/tools/lsp_tools.py +610 -0
- superqode/tools/network_tools.py +350 -0
- superqode/tools/permissions.py +400 -0
- superqode/tools/question_tool.py +324 -0
- superqode/tools/search_tools.py +598 -0
- superqode/tools/shell_tools.py +259 -0
- superqode/tools/todo_tools.py +121 -0
- superqode/tools/validation.py +80 -0
- superqode/tools/web_tools.py +639 -0
- superqode/tui.py +1152 -0
- superqode/tui_integration.py +875 -0
- superqode/tui_widgets/__init__.py +27 -0
- superqode/tui_widgets/widgets/__init__.py +18 -0
- superqode/tui_widgets/widgets/progress.py +185 -0
- superqode/tui_widgets/widgets/tool_display.py +188 -0
- superqode/undo_manager.py +574 -0
- superqode/utils/__init__.py +5 -0
- superqode/utils/error_handling.py +323 -0
- superqode/utils/fuzzy.py +257 -0
- superqode/widgets/__init__.py +477 -0
- superqode/widgets/agent_collab.py +390 -0
- superqode/widgets/agent_store.py +936 -0
- superqode/widgets/agent_switcher.py +395 -0
- superqode/widgets/animation_manager.py +284 -0
- superqode/widgets/code_context.py +356 -0
- superqode/widgets/command_palette.py +412 -0
- superqode/widgets/connection_status.py +537 -0
- superqode/widgets/conversation_history.py +470 -0
- superqode/widgets/diff_indicator.py +155 -0
- superqode/widgets/enhanced_status_bar.py +385 -0
- superqode/widgets/enhanced_toast.py +476 -0
- superqode/widgets/file_browser.py +809 -0
- superqode/widgets/file_reference.py +585 -0
- superqode/widgets/issue_timeline.py +340 -0
- superqode/widgets/leader_key.py +264 -0
- superqode/widgets/mode_switcher.py +445 -0
- superqode/widgets/model_picker.py +234 -0
- superqode/widgets/permission_preview.py +1205 -0
- superqode/widgets/prompt.py +358 -0
- superqode/widgets/provider_connect.py +725 -0
- superqode/widgets/pty_shell.py +587 -0
- superqode/widgets/qe_dashboard.py +321 -0
- superqode/widgets/resizable_sidebar.py +377 -0
- superqode/widgets/response_changes.py +218 -0
- superqode/widgets/response_display.py +528 -0
- superqode/widgets/rich_tool_display.py +613 -0
- superqode/widgets/sidebar_panels.py +1180 -0
- superqode/widgets/slash_complete.py +356 -0
- superqode/widgets/split_view.py +612 -0
- superqode/widgets/status_bar.py +273 -0
- superqode/widgets/superqode_display.py +786 -0
- superqode/widgets/thinking_display.py +815 -0
- superqode/widgets/throbber.py +87 -0
- superqode/widgets/toast.py +206 -0
- superqode/widgets/unified_output.py +1073 -0
- superqode/workspace/__init__.py +75 -0
- superqode/workspace/artifacts.py +472 -0
- superqode/workspace/coordinator.py +353 -0
- superqode/workspace/diff_tracker.py +429 -0
- superqode/workspace/git_guard.py +373 -0
- superqode/workspace/git_snapshot.py +526 -0
- superqode/workspace/manager.py +750 -0
- superqode/workspace/snapshot.py +357 -0
- superqode/workspace/watcher.py +535 -0
- superqode/workspace/worktree.py +440 -0
- superqode-0.1.5.dist-info/METADATA +204 -0
- superqode-0.1.5.dist-info/RECORD +288 -0
- superqode-0.1.5.dist-info/WHEEL +5 -0
- superqode-0.1.5.dist-info/entry_points.txt +3 -0
- superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
- 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
|