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
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)