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
superqode/mcp/oauth.py
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP OAuth Provider - OAuth 2.0 Authentication for MCP Servers.
|
|
3
|
+
|
|
4
|
+
Implements OAuth 2.0 with PKCE (Proof Key for Code Exchange) for
|
|
5
|
+
secure authentication with MCP servers that require it.
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- PKCE flow for public clients
|
|
9
|
+
- Dynamic client registration
|
|
10
|
+
- Token refresh
|
|
11
|
+
- Secure state management
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import base64
|
|
18
|
+
import hashlib
|
|
19
|
+
import json
|
|
20
|
+
import logging
|
|
21
|
+
import os
|
|
22
|
+
import secrets
|
|
23
|
+
import ssl
|
|
24
|
+
import urllib.error
|
|
25
|
+
import urllib.parse
|
|
26
|
+
import urllib.request
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from datetime import datetime, timedelta
|
|
29
|
+
from typing import Any, Dict, Optional
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class OAuthConfig:
|
|
36
|
+
"""OAuth configuration for an MCP server."""
|
|
37
|
+
|
|
38
|
+
client_id: Optional[str] = None
|
|
39
|
+
client_secret: Optional[str] = None
|
|
40
|
+
scope: str = "mcp"
|
|
41
|
+
redirect_uri: str = "http://localhost:19876/mcp/oauth/callback"
|
|
42
|
+
# PKCE settings
|
|
43
|
+
use_pkce: bool = True
|
|
44
|
+
code_challenge_method: str = "S256"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class OAuthTokens:
|
|
49
|
+
"""OAuth tokens from authentication."""
|
|
50
|
+
|
|
51
|
+
access_token: str
|
|
52
|
+
refresh_token: Optional[str] = None
|
|
53
|
+
expires_at: Optional[datetime] = None
|
|
54
|
+
token_type: str = "Bearer"
|
|
55
|
+
scope: str = ""
|
|
56
|
+
|
|
57
|
+
def is_expired(self) -> bool:
|
|
58
|
+
"""Check if the access token is expired."""
|
|
59
|
+
if self.expires_at is None:
|
|
60
|
+
return False
|
|
61
|
+
# Consider expired 5 minutes before actual expiry
|
|
62
|
+
buffer = timedelta(minutes=5)
|
|
63
|
+
return datetime.now() >= (self.expires_at - buffer)
|
|
64
|
+
|
|
65
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
66
|
+
"""Convert to dictionary for storage."""
|
|
67
|
+
return {
|
|
68
|
+
"access_token": self.access_token,
|
|
69
|
+
"refresh_token": self.refresh_token,
|
|
70
|
+
"expires_at": self.expires_at.isoformat() if self.expires_at else None,
|
|
71
|
+
"token_type": self.token_type,
|
|
72
|
+
"scope": self.scope,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def from_dict(cls, data: Dict[str, Any]) -> "OAuthTokens":
|
|
77
|
+
"""Create from dictionary."""
|
|
78
|
+
expires_at = None
|
|
79
|
+
if data.get("expires_at"):
|
|
80
|
+
expires_at = datetime.fromisoformat(data["expires_at"])
|
|
81
|
+
|
|
82
|
+
return cls(
|
|
83
|
+
access_token=data["access_token"],
|
|
84
|
+
refresh_token=data.get("refresh_token"),
|
|
85
|
+
expires_at=expires_at,
|
|
86
|
+
token_type=data.get("token_type", "Bearer"),
|
|
87
|
+
scope=data.get("scope", ""),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class OAuthState:
|
|
93
|
+
"""State for an OAuth flow in progress."""
|
|
94
|
+
|
|
95
|
+
state: str
|
|
96
|
+
code_verifier: str # For PKCE
|
|
97
|
+
server_url: str
|
|
98
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
99
|
+
|
|
100
|
+
def is_expired(self) -> bool:
|
|
101
|
+
"""Check if the state has expired (10 minute timeout)."""
|
|
102
|
+
return datetime.now() > (self.created_at + timedelta(minutes=10))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class MCPOAuthProvider:
|
|
106
|
+
"""
|
|
107
|
+
OAuth 2.0 provider for MCP server authentication.
|
|
108
|
+
|
|
109
|
+
Implements the OAuth 2.0 Authorization Code flow with PKCE
|
|
110
|
+
for secure authentication with MCP servers.
|
|
111
|
+
|
|
112
|
+
Usage:
|
|
113
|
+
provider = MCPOAuthProvider(config)
|
|
114
|
+
|
|
115
|
+
# Get authorization URL
|
|
116
|
+
auth_url = await provider.start_auth_flow(server_url, metadata)
|
|
117
|
+
|
|
118
|
+
# Open browser for user authentication
|
|
119
|
+
# ... wait for callback ...
|
|
120
|
+
|
|
121
|
+
# Exchange code for tokens
|
|
122
|
+
tokens = await provider.handle_callback(code, state)
|
|
123
|
+
|
|
124
|
+
# Use tokens
|
|
125
|
+
headers = {"Authorization": f"Bearer {tokens.access_token}"}
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
def __init__(self, config: Optional[OAuthConfig] = None):
|
|
129
|
+
self.config = config or OAuthConfig()
|
|
130
|
+
self._pending_flows: Dict[str, OAuthState] = {}
|
|
131
|
+
self._metadata_cache: Dict[str, Dict[str, Any]] = {}
|
|
132
|
+
|
|
133
|
+
async def discover_oauth_metadata(self, server_url: str) -> Dict[str, Any]:
|
|
134
|
+
"""
|
|
135
|
+
Discover OAuth metadata from server.
|
|
136
|
+
|
|
137
|
+
Looks for .well-known/oauth-authorization-server endpoint.
|
|
138
|
+
"""
|
|
139
|
+
if server_url in self._metadata_cache:
|
|
140
|
+
return self._metadata_cache[server_url]
|
|
141
|
+
|
|
142
|
+
# Try standard OAuth discovery endpoint
|
|
143
|
+
parsed = urllib.parse.urlparse(server_url)
|
|
144
|
+
base_url = f"{parsed.scheme}://{parsed.netloc}"
|
|
145
|
+
|
|
146
|
+
metadata_url = f"{base_url}/.well-known/oauth-authorization-server"
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
loop = asyncio.get_event_loop()
|
|
150
|
+
metadata = await loop.run_in_executor(None, lambda: self._fetch_metadata(metadata_url))
|
|
151
|
+
|
|
152
|
+
if metadata:
|
|
153
|
+
self._metadata_cache[server_url] = metadata
|
|
154
|
+
return metadata
|
|
155
|
+
|
|
156
|
+
except Exception as e:
|
|
157
|
+
logger.debug(f"OAuth discovery failed for {server_url}: {e}")
|
|
158
|
+
|
|
159
|
+
# Return empty if discovery fails
|
|
160
|
+
return {}
|
|
161
|
+
|
|
162
|
+
def _fetch_metadata(self, url: str) -> Optional[Dict[str, Any]]:
|
|
163
|
+
"""Fetch OAuth metadata synchronously."""
|
|
164
|
+
try:
|
|
165
|
+
req = urllib.request.Request(url)
|
|
166
|
+
req.add_header("Accept", "application/json")
|
|
167
|
+
|
|
168
|
+
ctx = ssl.create_default_context()
|
|
169
|
+
|
|
170
|
+
with urllib.request.urlopen(req, timeout=10, context=ctx) as response:
|
|
171
|
+
return json.loads(response.read().decode("utf-8"))
|
|
172
|
+
|
|
173
|
+
except (urllib.error.HTTPError, urllib.error.URLError):
|
|
174
|
+
return None
|
|
175
|
+
except json.JSONDecodeError:
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
def _generate_pkce_pair(self) -> tuple[str, str]:
|
|
179
|
+
"""
|
|
180
|
+
Generate PKCE code verifier and challenge.
|
|
181
|
+
|
|
182
|
+
Returns (code_verifier, code_challenge).
|
|
183
|
+
"""
|
|
184
|
+
# Generate random code verifier (43-128 characters)
|
|
185
|
+
code_verifier = secrets.token_urlsafe(64)
|
|
186
|
+
|
|
187
|
+
# Generate code challenge using S256 method
|
|
188
|
+
if self.config.code_challenge_method == "S256":
|
|
189
|
+
digest = hashlib.sha256(code_verifier.encode()).digest()
|
|
190
|
+
code_challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode()
|
|
191
|
+
else:
|
|
192
|
+
# Plain method (not recommended)
|
|
193
|
+
code_challenge = code_verifier
|
|
194
|
+
|
|
195
|
+
return code_verifier, code_challenge
|
|
196
|
+
|
|
197
|
+
async def start_auth_flow(
|
|
198
|
+
self,
|
|
199
|
+
server_url: str,
|
|
200
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
201
|
+
) -> str:
|
|
202
|
+
"""
|
|
203
|
+
Start the OAuth authorization flow.
|
|
204
|
+
|
|
205
|
+
Returns the authorization URL to open in the browser.
|
|
206
|
+
"""
|
|
207
|
+
# Get OAuth metadata if not provided
|
|
208
|
+
if metadata is None:
|
|
209
|
+
metadata = await self.discover_oauth_metadata(server_url)
|
|
210
|
+
|
|
211
|
+
# Get authorization endpoint
|
|
212
|
+
auth_endpoint = metadata.get("authorization_endpoint", f"{server_url}/oauth/authorize")
|
|
213
|
+
|
|
214
|
+
# Generate state for CSRF protection
|
|
215
|
+
state = secrets.token_urlsafe(32)
|
|
216
|
+
|
|
217
|
+
# Generate PKCE pair
|
|
218
|
+
code_verifier, code_challenge = self._generate_pkce_pair()
|
|
219
|
+
|
|
220
|
+
# Store state for verification
|
|
221
|
+
self._pending_flows[state] = OAuthState(
|
|
222
|
+
state=state,
|
|
223
|
+
code_verifier=code_verifier,
|
|
224
|
+
server_url=server_url,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Build authorization URL
|
|
228
|
+
params = {
|
|
229
|
+
"response_type": "code",
|
|
230
|
+
"client_id": self.config.client_id or "superqode",
|
|
231
|
+
"redirect_uri": self.config.redirect_uri,
|
|
232
|
+
"scope": self.config.scope,
|
|
233
|
+
"state": state,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
# Add PKCE parameters
|
|
237
|
+
if self.config.use_pkce:
|
|
238
|
+
params["code_challenge"] = code_challenge
|
|
239
|
+
params["code_challenge_method"] = self.config.code_challenge_method
|
|
240
|
+
|
|
241
|
+
auth_url = f"{auth_endpoint}?{urllib.parse.urlencode(params)}"
|
|
242
|
+
return auth_url
|
|
243
|
+
|
|
244
|
+
async def handle_callback(
|
|
245
|
+
self,
|
|
246
|
+
code: str,
|
|
247
|
+
state: str,
|
|
248
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
249
|
+
) -> OAuthTokens:
|
|
250
|
+
"""
|
|
251
|
+
Handle the OAuth callback and exchange code for tokens.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
code: Authorization code from callback
|
|
255
|
+
state: State parameter from callback
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
OAuthTokens with access and refresh tokens
|
|
259
|
+
"""
|
|
260
|
+
# Verify state
|
|
261
|
+
if state not in self._pending_flows:
|
|
262
|
+
raise ValueError("Invalid or expired state parameter")
|
|
263
|
+
|
|
264
|
+
flow_state = self._pending_flows.pop(state)
|
|
265
|
+
|
|
266
|
+
if flow_state.is_expired():
|
|
267
|
+
raise ValueError("OAuth flow has expired")
|
|
268
|
+
|
|
269
|
+
# Get OAuth metadata if not provided
|
|
270
|
+
if metadata is None:
|
|
271
|
+
metadata = await self.discover_oauth_metadata(flow_state.server_url)
|
|
272
|
+
|
|
273
|
+
# Get token endpoint
|
|
274
|
+
token_endpoint = metadata.get("token_endpoint", f"{flow_state.server_url}/oauth/token")
|
|
275
|
+
|
|
276
|
+
# Build token request
|
|
277
|
+
token_data = {
|
|
278
|
+
"grant_type": "authorization_code",
|
|
279
|
+
"code": code,
|
|
280
|
+
"redirect_uri": self.config.redirect_uri,
|
|
281
|
+
"client_id": self.config.client_id or "superqode",
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
# Add PKCE verifier
|
|
285
|
+
if self.config.use_pkce:
|
|
286
|
+
token_data["code_verifier"] = flow_state.code_verifier
|
|
287
|
+
|
|
288
|
+
# Add client secret if available
|
|
289
|
+
if self.config.client_secret:
|
|
290
|
+
token_data["client_secret"] = self.config.client_secret
|
|
291
|
+
|
|
292
|
+
# Exchange code for tokens
|
|
293
|
+
loop = asyncio.get_event_loop()
|
|
294
|
+
token_response = await loop.run_in_executor(
|
|
295
|
+
None, lambda: self._request_tokens(token_endpoint, token_data)
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
return self._parse_token_response(token_response)
|
|
299
|
+
|
|
300
|
+
async def refresh_tokens(
|
|
301
|
+
self,
|
|
302
|
+
refresh_token: str,
|
|
303
|
+
server_url: str,
|
|
304
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
305
|
+
) -> OAuthTokens:
|
|
306
|
+
"""
|
|
307
|
+
Refresh expired access token using refresh token.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
refresh_token: The refresh token
|
|
311
|
+
server_url: Server URL for token endpoint
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
New OAuthTokens with refreshed access token
|
|
315
|
+
"""
|
|
316
|
+
if metadata is None:
|
|
317
|
+
metadata = await self.discover_oauth_metadata(server_url)
|
|
318
|
+
|
|
319
|
+
token_endpoint = metadata.get("token_endpoint", f"{server_url}/oauth/token")
|
|
320
|
+
|
|
321
|
+
token_data = {
|
|
322
|
+
"grant_type": "refresh_token",
|
|
323
|
+
"refresh_token": refresh_token,
|
|
324
|
+
"client_id": self.config.client_id or "superqode",
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if self.config.client_secret:
|
|
328
|
+
token_data["client_secret"] = self.config.client_secret
|
|
329
|
+
|
|
330
|
+
loop = asyncio.get_event_loop()
|
|
331
|
+
token_response = await loop.run_in_executor(
|
|
332
|
+
None, lambda: self._request_tokens(token_endpoint, token_data)
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
return self._parse_token_response(token_response)
|
|
336
|
+
|
|
337
|
+
def _request_tokens(self, token_endpoint: str, data: Dict[str, str]) -> Dict[str, Any]:
|
|
338
|
+
"""Make token request synchronously."""
|
|
339
|
+
encoded_data = urllib.parse.urlencode(data).encode("utf-8")
|
|
340
|
+
|
|
341
|
+
req = urllib.request.Request(
|
|
342
|
+
token_endpoint,
|
|
343
|
+
data=encoded_data,
|
|
344
|
+
method="POST",
|
|
345
|
+
)
|
|
346
|
+
req.add_header("Content-Type", "application/x-www-form-urlencoded")
|
|
347
|
+
req.add_header("Accept", "application/json")
|
|
348
|
+
|
|
349
|
+
ctx = ssl.create_default_context()
|
|
350
|
+
|
|
351
|
+
try:
|
|
352
|
+
with urllib.request.urlopen(req, timeout=30, context=ctx) as response:
|
|
353
|
+
return json.loads(response.read().decode("utf-8"))
|
|
354
|
+
except urllib.error.HTTPError as e:
|
|
355
|
+
error_body = e.read().decode("utf-8") if e.fp else ""
|
|
356
|
+
raise ValueError(f"Token request failed: {e.code} - {error_body}")
|
|
357
|
+
|
|
358
|
+
def _parse_token_response(self, response: Dict[str, Any]) -> OAuthTokens:
|
|
359
|
+
"""Parse token response into OAuthTokens."""
|
|
360
|
+
if "error" in response:
|
|
361
|
+
raise ValueError(f"OAuth error: {response['error']}")
|
|
362
|
+
|
|
363
|
+
access_token = response.get("access_token")
|
|
364
|
+
if not access_token:
|
|
365
|
+
raise ValueError("No access token in response")
|
|
366
|
+
|
|
367
|
+
# Calculate expiry time
|
|
368
|
+
expires_at = None
|
|
369
|
+
if "expires_in" in response:
|
|
370
|
+
expires_at = datetime.now() + timedelta(seconds=response["expires_in"])
|
|
371
|
+
|
|
372
|
+
return OAuthTokens(
|
|
373
|
+
access_token=access_token,
|
|
374
|
+
refresh_token=response.get("refresh_token"),
|
|
375
|
+
expires_at=expires_at,
|
|
376
|
+
token_type=response.get("token_type", "Bearer"),
|
|
377
|
+
scope=response.get("scope", ""),
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
def cleanup_expired_flows(self) -> None:
|
|
381
|
+
"""Clean up expired OAuth flows."""
|
|
382
|
+
expired = [state for state, flow in self._pending_flows.items() if flow.is_expired()]
|
|
383
|
+
for state in expired:
|
|
384
|
+
del self._pending_flows[state]
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
async def dynamic_client_registration(
|
|
388
|
+
server_url: str,
|
|
389
|
+
client_name: str = "SuperQode",
|
|
390
|
+
redirect_uris: Optional[list[str]] = None,
|
|
391
|
+
) -> Dict[str, Any]:
|
|
392
|
+
"""
|
|
393
|
+
Perform dynamic client registration with an OAuth server.
|
|
394
|
+
|
|
395
|
+
Some OAuth servers support RFC 7591 dynamic client registration,
|
|
396
|
+
which allows clients to register themselves.
|
|
397
|
+
|
|
398
|
+
Returns the registration response including client_id and optionally
|
|
399
|
+
client_secret.
|
|
400
|
+
"""
|
|
401
|
+
if redirect_uris is None:
|
|
402
|
+
redirect_uris = ["http://localhost:19876/mcp/oauth/callback"]
|
|
403
|
+
|
|
404
|
+
# Try to discover registration endpoint
|
|
405
|
+
provider = MCPOAuthProvider()
|
|
406
|
+
metadata = await provider.discover_oauth_metadata(server_url)
|
|
407
|
+
|
|
408
|
+
registration_endpoint = metadata.get("registration_endpoint")
|
|
409
|
+
if not registration_endpoint:
|
|
410
|
+
raise ValueError("Server does not support dynamic client registration")
|
|
411
|
+
|
|
412
|
+
registration_data = {
|
|
413
|
+
"client_name": client_name,
|
|
414
|
+
"redirect_uris": redirect_uris,
|
|
415
|
+
"grant_types": ["authorization_code", "refresh_token"],
|
|
416
|
+
"response_types": ["code"],
|
|
417
|
+
"token_endpoint_auth_method": "none", # Public client
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
loop = asyncio.get_event_loop()
|
|
421
|
+
|
|
422
|
+
def _register() -> Dict[str, Any]:
|
|
423
|
+
req = urllib.request.Request(
|
|
424
|
+
registration_endpoint,
|
|
425
|
+
data=json.dumps(registration_data).encode("utf-8"),
|
|
426
|
+
method="POST",
|
|
427
|
+
)
|
|
428
|
+
req.add_header("Content-Type", "application/json")
|
|
429
|
+
req.add_header("Accept", "application/json")
|
|
430
|
+
|
|
431
|
+
ctx = ssl.create_default_context()
|
|
432
|
+
|
|
433
|
+
with urllib.request.urlopen(req, timeout=30, context=ctx) as response:
|
|
434
|
+
return json.loads(response.read().decode("utf-8"))
|
|
435
|
+
|
|
436
|
+
return await loop.run_in_executor(None, _register)
|