zjcode 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deepagents_code/__init__.py +42 -0
- deepagents_code/__main__.py +6 -0
- deepagents_code/_ask_user_types.py +90 -0
- deepagents_code/_cli_context.py +96 -0
- deepagents_code/_constants.py +41 -0
- deepagents_code/_debug.py +148 -0
- deepagents_code/_debug_buffer.py +204 -0
- deepagents_code/_env_vars.py +411 -0
- deepagents_code/_fake_models.py +66 -0
- deepagents_code/_git.py +521 -0
- deepagents_code/_paths.py +69 -0
- deepagents_code/_server_config.py +576 -0
- deepagents_code/_session_stats.py +235 -0
- deepagents_code/_startup_error.py +45 -0
- deepagents_code/_testing_models.py +305 -0
- deepagents_code/_textual_patches.py +420 -0
- deepagents_code/_tool_stream.py +694 -0
- deepagents_code/_version.py +46 -0
- deepagents_code/agent.py +1976 -0
- deepagents_code/app.py +19239 -0
- deepagents_code/app.tcss +438 -0
- deepagents_code/approval_mode.py +131 -0
- deepagents_code/ask_user.py +306 -0
- deepagents_code/auth_display.py +185 -0
- deepagents_code/auth_store.py +545 -0
- deepagents_code/built_in_skills/__init__.py +5 -0
- deepagents_code/built_in_skills/remember/SKILL.md +118 -0
- deepagents_code/built_in_skills/skill-creator/SKILL.md +383 -0
- deepagents_code/built_in_skills/skill-creator/scripts/init_skill.py +366 -0
- deepagents_code/built_in_skills/skill-creator/scripts/quick_validate.py +158 -0
- deepagents_code/client/__init__.py +1 -0
- deepagents_code/client/commands/__init__.py +1 -0
- deepagents_code/client/commands/auth.py +541 -0
- deepagents_code/client/commands/config.py +714 -0
- deepagents_code/client/commands/mcp.py +250 -0
- deepagents_code/client/commands/tools.py +416 -0
- deepagents_code/client/launch/__init__.py +1 -0
- deepagents_code/client/launch/server.py +978 -0
- deepagents_code/client/launch/server_manager.py +540 -0
- deepagents_code/client/non_interactive.py +1758 -0
- deepagents_code/client/remote_client.py +794 -0
- deepagents_code/clipboard.py +217 -0
- deepagents_code/command_registry.py +450 -0
- deepagents_code/config.py +4829 -0
- deepagents_code/config_manifest.py +1451 -0
- deepagents_code/configurable_model.py +577 -0
- deepagents_code/default_agent_prompt.md +12 -0
- deepagents_code/doctor.py +559 -0
- deepagents_code/editor.py +142 -0
- deepagents_code/event_bus.py +411 -0
- deepagents_code/extras_info.py +661 -0
- deepagents_code/file_ops.py +576 -0
- deepagents_code/formatting.py +135 -0
- deepagents_code/goal_rubric.py +497 -0
- deepagents_code/goal_tools.py +459 -0
- deepagents_code/hooks.py +348 -0
- deepagents_code/input.py +1041 -0
- deepagents_code/integrations/__init__.py +1 -0
- deepagents_code/integrations/openai_codex.py +551 -0
- deepagents_code/integrations/sandbox_config.py +198 -0
- deepagents_code/integrations/sandbox_factory.py +1124 -0
- deepagents_code/integrations/sandbox_provider.py +137 -0
- deepagents_code/integrations/sandbox_registry.py +350 -0
- deepagents_code/iterm_cursor_guide.py +176 -0
- deepagents_code/local_context.py +926 -0
- deepagents_code/main.py +3985 -0
- deepagents_code/managed_tools.py +642 -0
- deepagents_code/mcp_auth.py +1950 -0
- deepagents_code/mcp_config.py +176 -0
- deepagents_code/mcp_disabled.py +212 -0
- deepagents_code/mcp_login_service.py +281 -0
- deepagents_code/mcp_oauth_ui.py +199 -0
- deepagents_code/mcp_providers/__init__.py +23 -0
- deepagents_code/mcp_providers/_registry.py +39 -0
- deepagents_code/mcp_providers/base.py +133 -0
- deepagents_code/mcp_providers/github.py +102 -0
- deepagents_code/mcp_providers/slack.py +175 -0
- deepagents_code/mcp_tools.py +2427 -0
- deepagents_code/mcp_trust.py +207 -0
- deepagents_code/media_utils.py +826 -0
- deepagents_code/memory_guard.py +474 -0
- deepagents_code/model_config.py +4156 -0
- deepagents_code/notifications.py +247 -0
- deepagents_code/offload.py +402 -0
- deepagents_code/onboarding.py +223 -0
- deepagents_code/output.py +69 -0
- deepagents_code/paste_collapse.py +103 -0
- deepagents_code/project_utils.py +231 -0
- deepagents_code/py.typed +0 -0
- deepagents_code/reasoning_effort.py +641 -0
- deepagents_code/reliable_rubric.py +97 -0
- deepagents_code/resume_state.py +237 -0
- deepagents_code/server_graph.py +310 -0
- deepagents_code/session_end_summary.py +329 -0
- deepagents_code/sessions.py +1716 -0
- deepagents_code/skills/__init__.py +18 -0
- deepagents_code/skills/commands.py +1252 -0
- deepagents_code/skills/invocation.py +112 -0
- deepagents_code/skills/load.py +196 -0
- deepagents_code/skills/trust.py +547 -0
- deepagents_code/state_migration.py +136 -0
- deepagents_code/subagents.py +278 -0
- deepagents_code/system_prompt.md +204 -0
- deepagents_code/terminal_capabilities.py +115 -0
- deepagents_code/terminal_escape.py +287 -0
- deepagents_code/theme.py +891 -0
- deepagents_code/todo_list_prompt.md +12 -0
- deepagents_code/tool_catalog.py +509 -0
- deepagents_code/tool_display.py +367 -0
- deepagents_code/tools.py +516 -0
- deepagents_code/tui/__init__.py +1 -0
- deepagents_code/tui/textual_adapter.py +2553 -0
- deepagents_code/tui/widgets/__init__.py +9 -0
- deepagents_code/tui/widgets/_js_eval_display.py +139 -0
- deepagents_code/tui/widgets/_links.py +261 -0
- deepagents_code/tui/widgets/agent_selector.py +420 -0
- deepagents_code/tui/widgets/approval.py +602 -0
- deepagents_code/tui/widgets/ask_user.py +515 -0
- deepagents_code/tui/widgets/auth.py +1997 -0
- deepagents_code/tui/widgets/autocomplete.py +890 -0
- deepagents_code/tui/widgets/chat_input.py +3181 -0
- deepagents_code/tui/widgets/codex_auth.py +452 -0
- deepagents_code/tui/widgets/cwd_switch.py +242 -0
- deepagents_code/tui/widgets/debug_console.py +868 -0
- deepagents_code/tui/widgets/diff.py +252 -0
- deepagents_code/tui/widgets/effort_selector.py +189 -0
- deepagents_code/tui/widgets/goal_review.py +390 -0
- deepagents_code/tui/widgets/goal_status.py +50 -0
- deepagents_code/tui/widgets/history.py +194 -0
- deepagents_code/tui/widgets/install_confirm.py +248 -0
- deepagents_code/tui/widgets/launch_init.py +482 -0
- deepagents_code/tui/widgets/loading.py +227 -0
- deepagents_code/tui/widgets/mcp_login.py +539 -0
- deepagents_code/tui/widgets/mcp_reconnect.py +212 -0
- deepagents_code/tui/widgets/mcp_viewer.py +1634 -0
- deepagents_code/tui/widgets/message_store.py +999 -0
- deepagents_code/tui/widgets/messages.py +3846 -0
- deepagents_code/tui/widgets/model_selector.py +2343 -0
- deepagents_code/tui/widgets/notification_center.py +456 -0
- deepagents_code/tui/widgets/notification_detail.py +257 -0
- deepagents_code/tui/widgets/notification_settings.py +170 -0
- deepagents_code/tui/widgets/restart_prompt.py +158 -0
- deepagents_code/tui/widgets/skill_trust.py +131 -0
- deepagents_code/tui/widgets/startup_tip.py +91 -0
- deepagents_code/tui/widgets/status.py +781 -0
- deepagents_code/tui/widgets/subagent_panel.py +969 -0
- deepagents_code/tui/widgets/theme_selector.py +401 -0
- deepagents_code/tui/widgets/thread_selector.py +2564 -0
- deepagents_code/tui/widgets/tool_renderers.py +190 -0
- deepagents_code/tui/widgets/tool_widgets.py +304 -0
- deepagents_code/tui/widgets/update_available.py +311 -0
- deepagents_code/tui/widgets/update_confirm.py +184 -0
- deepagents_code/tui/widgets/update_progress.py +327 -0
- deepagents_code/tui/widgets/welcome.py +508 -0
- deepagents_code/turn_end_summary.py +522 -0
- deepagents_code/ui.py +858 -0
- deepagents_code/unicode_security.py +563 -0
- deepagents_code/update_check.py +3124 -0
- zjcode-0.0.1.data/data/deepagents_code/default_agent_prompt.md +12 -0
- zjcode-0.0.1.dist-info/METADATA +220 -0
- zjcode-0.0.1.dist-info/RECORD +163 -0
- zjcode-0.0.1.dist-info/WHEEL +4 -0
- zjcode-0.0.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""UI-agnostic interaction interface for MCP OAuth login.
|
|
2
|
+
|
|
3
|
+
The OAuth login flow needs to ask the user a few things during the
|
|
4
|
+
handshake — open or display the authorize URL, accept a pasted callback
|
|
5
|
+
URL when the provider has no loopback redirect, show RFC 8628 device-code
|
|
6
|
+
instructions, and report success or failure. The CLI uses `print` and
|
|
7
|
+
`input`; a TUI surface needs in-app widgets instead. `OAuthInteraction` is
|
|
8
|
+
the small Protocol both implementations satisfy, and `CliOAuthInteraction`
|
|
9
|
+
is the existing CLI behavior preserved as one implementation of that
|
|
10
|
+
interface.
|
|
11
|
+
|
|
12
|
+
Important: implementations must never embed access or refresh tokens
|
|
13
|
+
in user-facing messages. The interaction surface only ever sees
|
|
14
|
+
authorize URLs, callback URLs, device codes, and short status strings,
|
|
15
|
+
so leaks come from misuse, not from this interface's shape.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import Protocol
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class OAuthInteraction(Protocol):
|
|
24
|
+
"""User-facing OAuth interaction surface shared by CLI and TUI."""
|
|
25
|
+
|
|
26
|
+
async def show_authorize_url(self, url: str, *, opened_in_browser: bool) -> None:
|
|
27
|
+
"""Tell the user about the authorize URL.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
url: Final authorize URL with provider-specific extras applied.
|
|
31
|
+
opened_in_browser: `True` when the caller already launched the
|
|
32
|
+
URL via `webbrowser.open`; `False` when the caller needs the
|
|
33
|
+
user to open it manually.
|
|
34
|
+
"""
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
async def request_callback_url(self) -> str:
|
|
38
|
+
"""Wait for the user to paste back the full provider callback URL.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
The raw pasted URL (the caller parses `code`/`state`/`error`).
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
RuntimeError: When the user interaction cannot complete (for
|
|
45
|
+
example, the input surface is unavailable or was dismissed).
|
|
46
|
+
"""
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
async def show_device_code(
|
|
50
|
+
self,
|
|
51
|
+
*,
|
|
52
|
+
verification_uri: str,
|
|
53
|
+
user_code: str,
|
|
54
|
+
expires_in: int,
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Show RFC 8628 device-code instructions to the user.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
verification_uri: Provider URL the user visits in a browser.
|
|
60
|
+
user_code: Short code the user enters on `verification_uri`.
|
|
61
|
+
expires_in: Lifetime of the device code in seconds.
|
|
62
|
+
"""
|
|
63
|
+
...
|
|
64
|
+
|
|
65
|
+
async def show_success(self, message: str) -> None:
|
|
66
|
+
"""Report a successful login step.
|
|
67
|
+
|
|
68
|
+
Implementations must not embed token material in `message`; the
|
|
69
|
+
login code only passes structural facts ("logged in", token file
|
|
70
|
+
path) here.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
message: Plain-text status line.
|
|
74
|
+
"""
|
|
75
|
+
...
|
|
76
|
+
|
|
77
|
+
async def show_notice(self, message: str) -> None:
|
|
78
|
+
"""Report a non-fatal progress notice (e.g. fallback path taken).
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
message: Plain-text notice.
|
|
82
|
+
"""
|
|
83
|
+
...
|
|
84
|
+
|
|
85
|
+
async def show_error(self, message: str) -> None:
|
|
86
|
+
"""Report a fatal (flow-ending) error.
|
|
87
|
+
|
|
88
|
+
"Fatal" rather than "terminal" because this is a TUI codebase
|
|
89
|
+
where "terminal" reads ambiguously.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
message: Plain-text error description.
|
|
93
|
+
"""
|
|
94
|
+
...
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class CliOAuthInteraction:
|
|
98
|
+
"""Default `OAuthInteraction` that drives the flow via stdin/stdout.
|
|
99
|
+
|
|
100
|
+
Preserves the previous `dcode mcp login` behavior — paste-back input,
|
|
101
|
+
plain-text prompts, success messages printed to stdout.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
async def show_authorize_url( # noqa: PLR6301
|
|
105
|
+
self,
|
|
106
|
+
url: str,
|
|
107
|
+
*,
|
|
108
|
+
opened_in_browser: bool,
|
|
109
|
+
) -> None:
|
|
110
|
+
"""Print the full authorize instruction block to stdout.
|
|
111
|
+
|
|
112
|
+
Uses browser-opened wording when `opened_in_browser` is `True`;
|
|
113
|
+
otherwise instructs the user to open the URL and paste back the callback.
|
|
114
|
+
"""
|
|
115
|
+
if opened_in_browser:
|
|
116
|
+
print( # noqa: T201
|
|
117
|
+
"\nOpened your browser to approve MCP access. "
|
|
118
|
+
"If it did not open, visit this URL:\n"
|
|
119
|
+
f"\n {url}\n",
|
|
120
|
+
)
|
|
121
|
+
else:
|
|
122
|
+
print( # noqa: T201
|
|
123
|
+
"\nOpen this URL in a browser, approve access, then paste the full "
|
|
124
|
+
"callback URL back here:\n"
|
|
125
|
+
f"\n {url}\n",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
async def request_callback_url(self) -> str: # noqa: PLR6301
|
|
129
|
+
"""Read a trimmed callback URL from stdin via a worker thread.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
The trimmed callback URL string.
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
RuntimeError: If stdin is closed before the user replies.
|
|
136
|
+
"""
|
|
137
|
+
import asyncio
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
raw = await asyncio.to_thread(input, "Callback URL: ")
|
|
141
|
+
except EOFError as exc:
|
|
142
|
+
msg = (
|
|
143
|
+
"No callback URL received (stdin closed). "
|
|
144
|
+
"Re-run `dcode mcp login <server>` and paste the URL."
|
|
145
|
+
)
|
|
146
|
+
raise RuntimeError(msg) from exc
|
|
147
|
+
return raw.strip()
|
|
148
|
+
|
|
149
|
+
async def show_device_code( # noqa: PLR6301
|
|
150
|
+
self,
|
|
151
|
+
*,
|
|
152
|
+
verification_uri: str,
|
|
153
|
+
user_code: str,
|
|
154
|
+
expires_in: int,
|
|
155
|
+
) -> None:
|
|
156
|
+
"""Print RFC 8628 device-code instructions to stdout."""
|
|
157
|
+
print( # noqa: T201
|
|
158
|
+
f"\nVisit {verification_uri} and enter code: "
|
|
159
|
+
f"{user_code}\n(code expires in {expires_in}s)\n",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
async def prompt_slack_team_id(self) -> str | None: # noqa: PLR6301
|
|
163
|
+
"""Ask for a Slack team ID via `input()` on a worker thread.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
The entered Slack team ID, or `None` if the prompt was blank or
|
|
167
|
+
stdin was closed.
|
|
168
|
+
"""
|
|
169
|
+
import asyncio
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
raw = await asyncio.to_thread(
|
|
173
|
+
input,
|
|
174
|
+
"Slack team ID to install the app into "
|
|
175
|
+
"(e.g. T01234567 — leave blank to pick on Slack's page): ",
|
|
176
|
+
)
|
|
177
|
+
except EOFError:
|
|
178
|
+
return None
|
|
179
|
+
return raw.strip() or None
|
|
180
|
+
|
|
181
|
+
async def show_success(self, message: str) -> None: # noqa: PLR6301
|
|
182
|
+
"""Print success message to stdout."""
|
|
183
|
+
print(message) # noqa: T201
|
|
184
|
+
|
|
185
|
+
async def show_notice(self, message: str) -> None: # noqa: PLR6301
|
|
186
|
+
"""Print progress notice to stdout."""
|
|
187
|
+
print(message) # noqa: T201
|
|
188
|
+
|
|
189
|
+
async def show_error(self, message: str) -> None: # noqa: PLR6301
|
|
190
|
+
"""Print error message to stderr."""
|
|
191
|
+
import sys
|
|
192
|
+
|
|
193
|
+
print(message, file=sys.stderr) # noqa: T201
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
__all__ = [
|
|
197
|
+
"CliOAuthInteraction",
|
|
198
|
+
"OAuthInteraction",
|
|
199
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Provider-specific MCP OAuth dispatch.
|
|
2
|
+
|
|
3
|
+
`resolve_provider(url)` returns the registered policy whose `matches`
|
|
4
|
+
predicate fires for `url`, with `GenericProvider` as the fallback.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from deepagents_code.mcp_providers._registry import resolve_provider
|
|
8
|
+
from deepagents_code.mcp_providers.base import (
|
|
9
|
+
GenericProvider,
|
|
10
|
+
LoginResult,
|
|
11
|
+
OAuthProvider,
|
|
12
|
+
)
|
|
13
|
+
from deepagents_code.mcp_providers.github import GitHubProvider
|
|
14
|
+
from deepagents_code.mcp_providers.slack import SlackProvider
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"GenericProvider",
|
|
18
|
+
"GitHubProvider",
|
|
19
|
+
"LoginResult",
|
|
20
|
+
"OAuthProvider",
|
|
21
|
+
"SlackProvider",
|
|
22
|
+
"resolve_provider",
|
|
23
|
+
]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Ordered provider registry for MCP OAuth dispatch.
|
|
2
|
+
|
|
3
|
+
`resolve_provider` walks `_REGISTRY` in order and returns the first
|
|
4
|
+
provider whose `matches(url)` is `True`. `GenericProvider` sits last so
|
|
5
|
+
spec-compliant servers always resolve to a usable policy.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from deepagents_code.mcp_providers.base import GenericProvider, OAuthProvider
|
|
11
|
+
from deepagents_code.mcp_providers.github import GitHubProvider
|
|
12
|
+
from deepagents_code.mcp_providers.slack import SlackProvider
|
|
13
|
+
|
|
14
|
+
_REGISTRY: tuple[OAuthProvider, ...] = (
|
|
15
|
+
SlackProvider(),
|
|
16
|
+
GitHubProvider(),
|
|
17
|
+
GenericProvider(),
|
|
18
|
+
)
|
|
19
|
+
"""Ordered provider list; `GenericProvider` must stay last as the fallback."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def resolve_provider(server_url: str) -> OAuthProvider:
|
|
23
|
+
"""Return the provider policy that owns `server_url`.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
server_url: Remote MCP endpoint URL.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
The first matching `OAuthProvider`; falls back to `GenericProvider`.
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
RuntimeError: If no provider matches (unreachable in practice since
|
|
33
|
+
`GenericProvider.matches` always returns `True`).
|
|
34
|
+
"""
|
|
35
|
+
for provider in _REGISTRY:
|
|
36
|
+
if provider.matches(server_url):
|
|
37
|
+
return provider
|
|
38
|
+
msg = f"No MCP OAuth provider matched {server_url!r}"
|
|
39
|
+
raise RuntimeError(msg)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Policy interface for provider-specific MCP OAuth quirks.
|
|
2
|
+
|
|
3
|
+
Each concrete provider module (e.g. `slack`, `github`) subclasses
|
|
4
|
+
`OAuthProvider` to encode its own URL match rule, client metadata, and
|
|
5
|
+
any pre-handshake login steps (preseeding client info, running a
|
|
6
|
+
device flow, prompting for workspace IDs). `mcp_auth` dispatches to
|
|
7
|
+
the first matching provider via `resolve_provider`, so adding a new
|
|
8
|
+
provider is one new module plus one registry entry — no edits to
|
|
9
|
+
`build_oauth_provider` or `login`.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from abc import ABC, abstractmethod
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
from mcp.shared.auth import AnyUrl, OAuthClientMetadata
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from deepagents_code.mcp_auth import FileTokenStorage
|
|
22
|
+
from deepagents_code.mcp_oauth_ui import OAuthInteraction
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class LoginResult:
|
|
27
|
+
"""Outcome of a provider's pre-handshake `run_login` step."""
|
|
28
|
+
|
|
29
|
+
completed: bool = False
|
|
30
|
+
"""`True` means tokens are persisted and `login` should skip the handshake."""
|
|
31
|
+
|
|
32
|
+
extra_auth_params: dict[str, str] = field(default_factory=dict)
|
|
33
|
+
"""Extra query params to thread into the interactive auth URL."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class OAuthProvider(ABC):
|
|
37
|
+
"""Base class for provider-specific OAuth dispatch.
|
|
38
|
+
|
|
39
|
+
Subclasses override `matches` plus whichever of `client_metadata`
|
|
40
|
+
and `run_login` they customize. The default implementations cover
|
|
41
|
+
the spec-compliant Authorization Code + PKCE + Dynamic Client
|
|
42
|
+
Registration path.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def matches(self, server_url: str) -> bool:
|
|
47
|
+
"""Return `True` when this provider owns `server_url`."""
|
|
48
|
+
|
|
49
|
+
def supports_loopback_callback(self) -> bool: # noqa: PLR6301 # subclass hook
|
|
50
|
+
"""Return whether this provider can use a runtime loopback redirect URI.
|
|
51
|
+
|
|
52
|
+
When `False`, `client_metadata()` ignores the `redirect_uri` argument
|
|
53
|
+
and uses the provider's own pre-registered static URI instead.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
`True` when the provider accepts dynamically registered redirect URIs.
|
|
57
|
+
"""
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
def loopback_port(self) -> int | None: # noqa: PLR6301 # subclass hook
|
|
61
|
+
"""Return a fixed loopback port, or `None` for a random ephemeral port.
|
|
62
|
+
|
|
63
|
+
Override when the provider's app registration requires a specific
|
|
64
|
+
pre-registered port (e.g. Slack registers `http://localhost:3118/callback`).
|
|
65
|
+
Ignored when `supports_loopback_callback()` is `False`.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
A fixed TCP port, or `None` to pick a random ephemeral port.
|
|
69
|
+
"""
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
def client_metadata( # noqa: PLR6301 # subclass hook
|
|
73
|
+
self,
|
|
74
|
+
*,
|
|
75
|
+
redirect_uri: str | None = None,
|
|
76
|
+
) -> OAuthClientMetadata:
|
|
77
|
+
"""Return the `OAuthClientMetadata` used to build the auth provider.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
redirect_uri: Optional runtime redirect URI for CLI loopback auth.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Metadata for the spec-compliant Authorization Code + PKCE +
|
|
84
|
+
Dynamic Client Registration flow.
|
|
85
|
+
"""
|
|
86
|
+
return OAuthClientMetadata(
|
|
87
|
+
client_name="deepagents-code",
|
|
88
|
+
redirect_uris=[AnyUrl(redirect_uri or "http://localhost/callback")],
|
|
89
|
+
grant_types=["authorization_code", "refresh_token"],
|
|
90
|
+
response_types=["code"],
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
async def run_login( # noqa: PLR6301 # subclass hook
|
|
94
|
+
self,
|
|
95
|
+
*,
|
|
96
|
+
server_name: str,
|
|
97
|
+
server_url: str,
|
|
98
|
+
storage: FileTokenStorage,
|
|
99
|
+
ui: OAuthInteraction,
|
|
100
|
+
) -> LoginResult:
|
|
101
|
+
"""Perform any provider-specific pre-handshake work.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
server_name: MCP server name from `mcpServers`.
|
|
105
|
+
server_url: Remote MCP endpoint URL.
|
|
106
|
+
storage: File-backed token storage for this server identity.
|
|
107
|
+
ui: Interaction surface used for any provider-specific
|
|
108
|
+
prompts (e.g. device-code instructions, workspace IDs).
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
`LoginResult.completed=True` if the provider finished the
|
|
112
|
+
login itself (e.g. device flow). Otherwise the caller drives
|
|
113
|
+
the standard Authorization Code handshake and passes any
|
|
114
|
+
returned `extra_auth_params` to the redirect URL.
|
|
115
|
+
"""
|
|
116
|
+
del server_name, server_url, storage, ui
|
|
117
|
+
return LoginResult()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class GenericProvider(OAuthProvider):
|
|
121
|
+
"""Fallback provider for spec-compliant MCP servers with no quirks."""
|
|
122
|
+
|
|
123
|
+
def matches(self, server_url: str) -> bool: # noqa: PLR6301 # subclass hook
|
|
124
|
+
"""Match any URL — the registry places this provider last.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
server_url: Remote MCP endpoint URL (unused).
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Always `True`.
|
|
131
|
+
"""
|
|
132
|
+
del server_url
|
|
133
|
+
return True
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""GitHub-hosted MCP OAuth provider.
|
|
2
|
+
|
|
3
|
+
GitHub's remote MCP at `api.githubcopilot.com` authenticates via RFC
|
|
4
|
+
8628 Device Authorization Grant — the app runs the device flow,
|
|
5
|
+
persists the resulting token plus a stub client-info record, and skips
|
|
6
|
+
the standard Authorization Code handshake entirely.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
from urllib.parse import urlparse
|
|
13
|
+
|
|
14
|
+
from mcp.shared.auth import AnyUrl, OAuthClientInformationFull
|
|
15
|
+
|
|
16
|
+
from deepagents_code.mcp_auth import _run_device_flow
|
|
17
|
+
from deepagents_code.mcp_providers.base import LoginResult, OAuthProvider
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from deepagents_code.mcp_auth import FileTokenStorage
|
|
21
|
+
from deepagents_code.mcp_oauth_ui import OAuthInteraction
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_GITHUB_MCP_CLIENT_ID = "Iv23libxz8qOApH0WQL3"
|
|
25
|
+
"""Public OAuth client ID for the GitHub App backing GitHub's remote MCP."""
|
|
26
|
+
|
|
27
|
+
_GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code"
|
|
28
|
+
"""GitHub Device Authorization Grant endpoint that issues the user/device code pair."""
|
|
29
|
+
|
|
30
|
+
_GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token" # noqa: S105
|
|
31
|
+
"""GitHub OAuth token endpoint polled while the user completes the device flow."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _is_github_mcp_url(url: str) -> bool:
|
|
35
|
+
"""Return `True` when `url` points at GitHub's remote MCP endpoint."""
|
|
36
|
+
return (urlparse(url).hostname or "") == "api.githubcopilot.com"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async def _preseed_github_auth(
|
|
40
|
+
storage: FileTokenStorage, *, ui: OAuthInteraction
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Run GitHub Device Flow and persist the token and stub client info.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
storage: File-backed token storage for this server identity.
|
|
46
|
+
ui: Interaction surface that renders the device-code prompt.
|
|
47
|
+
"""
|
|
48
|
+
token = await _run_device_flow(
|
|
49
|
+
device_code_url=_GITHUB_DEVICE_CODE_URL,
|
|
50
|
+
token_url=_GITHUB_TOKEN_URL,
|
|
51
|
+
client_id=_GITHUB_MCP_CLIENT_ID,
|
|
52
|
+
ui=ui,
|
|
53
|
+
)
|
|
54
|
+
await storage.set_tokens_and_client_info(
|
|
55
|
+
token,
|
|
56
|
+
OAuthClientInformationFull(
|
|
57
|
+
client_id=_GITHUB_MCP_CLIENT_ID,
|
|
58
|
+
redirect_uris=[AnyUrl("http://localhost/callback")],
|
|
59
|
+
grant_types=["urn:ietf:params:oauth:grant-type:device_code"],
|
|
60
|
+
response_types=["code"],
|
|
61
|
+
token_endpoint_auth_method="none", # noqa: S106
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class GitHubProvider(OAuthProvider):
|
|
67
|
+
"""GitHub-hosted MCP: RFC 8628 Device Authorization Grant."""
|
|
68
|
+
|
|
69
|
+
def matches(self, server_url: str) -> bool: # noqa: PLR6301 # subclass hook
|
|
70
|
+
"""Match `api.githubcopilot.com`.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
server_url: Remote MCP endpoint URL.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
`True` when `server_url`'s host is GitHub's MCP endpoint.
|
|
77
|
+
"""
|
|
78
|
+
return _is_github_mcp_url(server_url)
|
|
79
|
+
|
|
80
|
+
async def run_login( # noqa: PLR6301 # subclass hook
|
|
81
|
+
self,
|
|
82
|
+
*,
|
|
83
|
+
server_name: str,
|
|
84
|
+
server_url: str,
|
|
85
|
+
storage: FileTokenStorage,
|
|
86
|
+
ui: OAuthInteraction,
|
|
87
|
+
) -> LoginResult:
|
|
88
|
+
"""Run the device flow and short-circuit the Authorization Code handshake.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
server_name: MCP server name (unused).
|
|
92
|
+
server_url: Remote MCP endpoint URL (unused).
|
|
93
|
+
storage: File-backed token storage for this server identity.
|
|
94
|
+
ui: Interaction surface that renders the device-code prompt.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
`LoginResult(completed=True)` — tokens are already persisted so
|
|
98
|
+
the caller must skip the handshake step.
|
|
99
|
+
"""
|
|
100
|
+
del server_name, server_url
|
|
101
|
+
await _preseed_github_auth(storage, ui=ui)
|
|
102
|
+
return LoginResult(completed=True)
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Slack-hosted MCP OAuth provider.
|
|
2
|
+
|
|
3
|
+
Slack's hosted MCP endpoint uses the Authorization Code flow with a
|
|
4
|
+
hardcoded public client ID and a fixed pre-registered loopback redirect
|
|
5
|
+
URI (`http://localhost:3118/callback`). The local callback server listens
|
|
6
|
+
on that port so the browser redirect completes automatically. An optional
|
|
7
|
+
`team` query parameter selects the workspace to install the app into.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
13
|
+
from urllib.parse import urlparse
|
|
14
|
+
|
|
15
|
+
from mcp.shared.auth import (
|
|
16
|
+
AnyUrl,
|
|
17
|
+
OAuthClientInformationFull,
|
|
18
|
+
OAuthClientMetadata,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from deepagents_code.mcp_providers.base import LoginResult, OAuthProvider
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from deepagents_code.mcp_auth import FileTokenStorage
|
|
25
|
+
from deepagents_code.mcp_oauth_ui import OAuthInteraction
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@runtime_checkable
|
|
29
|
+
class _SupportsSlackTeamPrompt(Protocol):
|
|
30
|
+
"""Optional interaction-surface capability: prompt for a Slack team ID.
|
|
31
|
+
|
|
32
|
+
`OAuthInteraction` deliberately omits this method — only the CLI
|
|
33
|
+
surface implements it. The TUI lets Slack's browser page handle
|
|
34
|
+
workspace selection. Marked `runtime_checkable` so `isinstance`
|
|
35
|
+
can replace `getattr`-style structural probes at the call site.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
async def prompt_slack_team_id(self) -> str | None: ...
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Public OAuth client ID — safe to check in. No secret is associated;
|
|
42
|
+
# Slack treats this as a browser-style public client where the security
|
|
43
|
+
# boundary is the redirect URI rather than client secrecy.
|
|
44
|
+
_SLACK_MCP_CLIENT_ID = "4518649543379.10944517634130"
|
|
45
|
+
"""Public OAuth client ID registered with Slack for the hosted MCP endpoint."""
|
|
46
|
+
|
|
47
|
+
_SLACK_LOOPBACK_PORT = 3118
|
|
48
|
+
"""Fixed TCP port the local callback server binds to for Slack OAuth.
|
|
49
|
+
|
|
50
|
+
Slack validates the redirect URI against the app's registered allowlist, so
|
|
51
|
+
the port must be pre-registered in the Slack app dashboard. Only one Slack
|
|
52
|
+
login can proceed at a time per machine (port conflicts are surfaced as an
|
|
53
|
+
OSError from the loopback server's `start()` call).
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
_SLACK_REDIRECT_URI = f"http://localhost:{_SLACK_LOOPBACK_PORT}/callback"
|
|
57
|
+
"""Pre-registered loopback redirect URI for the Slack MCP OAuth app."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _is_slack_mcp_url(url: str) -> bool:
|
|
61
|
+
"""Return `True` when `url` points at a Slack-hosted MCP endpoint."""
|
|
62
|
+
host = urlparse(url).hostname or ""
|
|
63
|
+
return host == "slack.com" or host.endswith(".slack.com")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
async def _prompt_slack_team(ui: OAuthInteraction) -> str | None:
|
|
67
|
+
"""Return a Slack team ID when the interaction surface supports prompting.
|
|
68
|
+
|
|
69
|
+
`prompt_slack_team_id` is **not** a member of the `OAuthInteraction`
|
|
70
|
+
Protocol — only the CLI surface implements it. The TUI omits it so
|
|
71
|
+
Slack's browser page handles workspace selection instead. Detection
|
|
72
|
+
uses `isinstance` against the `runtime_checkable`
|
|
73
|
+
`_SupportsSlackTeamPrompt` capability protocol.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
ui: Interaction surface to use.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
The entered Slack team ID, or `None` when the surface lacks the
|
|
80
|
+
optional prompt method or the user declined to specify one.
|
|
81
|
+
"""
|
|
82
|
+
if not isinstance(ui, _SupportsSlackTeamPrompt):
|
|
83
|
+
return None
|
|
84
|
+
return await ui.prompt_slack_team_id()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
async def _preseed_slack_client_info(storage: FileTokenStorage) -> None:
|
|
88
|
+
"""Write the hardcoded Slack `client_info` to `storage` if not current."""
|
|
89
|
+
existing = await storage.get_client_info()
|
|
90
|
+
redirect_uris = existing.redirect_uris if existing is not None else None
|
|
91
|
+
current_redirect = str(redirect_uris[0]) if redirect_uris else None
|
|
92
|
+
if (
|
|
93
|
+
existing is not None
|
|
94
|
+
and existing.client_id == _SLACK_MCP_CLIENT_ID
|
|
95
|
+
and current_redirect == _SLACK_REDIRECT_URI
|
|
96
|
+
):
|
|
97
|
+
return
|
|
98
|
+
await storage.set_client_info(
|
|
99
|
+
OAuthClientInformationFull(
|
|
100
|
+
client_id=_SLACK_MCP_CLIENT_ID,
|
|
101
|
+
redirect_uris=[AnyUrl(_SLACK_REDIRECT_URI)],
|
|
102
|
+
grant_types=["authorization_code", "refresh_token"],
|
|
103
|
+
response_types=["code"],
|
|
104
|
+
token_endpoint_auth_method="none", # noqa: S106
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class SlackProvider(OAuthProvider):
|
|
110
|
+
"""Slack-hosted MCP: loopback Authorization Code with a public client."""
|
|
111
|
+
|
|
112
|
+
def matches(self, server_url: str) -> bool: # noqa: PLR6301 # subclass hook
|
|
113
|
+
"""Match `slack.com` and any `*.slack.com` subdomain.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
server_url: Remote MCP endpoint URL.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
`True` when `server_url`'s host is Slack.
|
|
120
|
+
"""
|
|
121
|
+
return _is_slack_mcp_url(server_url)
|
|
122
|
+
|
|
123
|
+
def loopback_port(self) -> int: # noqa: PLR6301 # subclass hook
|
|
124
|
+
"""Return the fixed loopback port registered in the Slack OAuth app.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
`_SLACK_LOOPBACK_PORT` (3118).
|
|
128
|
+
"""
|
|
129
|
+
return _SLACK_LOOPBACK_PORT
|
|
130
|
+
|
|
131
|
+
def client_metadata( # noqa: PLR6301 # subclass hook
|
|
132
|
+
self, *, redirect_uri: str | None = None
|
|
133
|
+
) -> OAuthClientMetadata:
|
|
134
|
+
"""Return public-client metadata with Slack's pre-registered loopback URI.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
redirect_uri: Ignored; Slack requires its pre-registered loopback URI.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Metadata configured for Slack's public OAuth client (no token secret).
|
|
141
|
+
"""
|
|
142
|
+
del redirect_uri
|
|
143
|
+
return OAuthClientMetadata(
|
|
144
|
+
client_name="deepagents-code",
|
|
145
|
+
redirect_uris=[AnyUrl(_SLACK_REDIRECT_URI)],
|
|
146
|
+
grant_types=["authorization_code", "refresh_token"],
|
|
147
|
+
response_types=["code"],
|
|
148
|
+
token_endpoint_auth_method="none", # noqa: S106
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
async def run_login( # noqa: PLR6301 # subclass hook
|
|
152
|
+
self,
|
|
153
|
+
*,
|
|
154
|
+
server_name: str,
|
|
155
|
+
server_url: str,
|
|
156
|
+
storage: FileTokenStorage,
|
|
157
|
+
ui: OAuthInteraction,
|
|
158
|
+
) -> LoginResult:
|
|
159
|
+
"""Preseed client info and optionally thread the team ID into auth URL.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
server_name: MCP server name (unused).
|
|
163
|
+
server_url: Remote MCP endpoint URL (unused).
|
|
164
|
+
storage: File-backed token storage for this server identity.
|
|
165
|
+
ui: Interaction surface used to prompt for the Slack team ID.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
A `LoginResult` carrying the optional `team=<id>` extra param
|
|
169
|
+
so the Slack authorize URL installs into the chosen workspace.
|
|
170
|
+
"""
|
|
171
|
+
del server_name, server_url
|
|
172
|
+
await _preseed_slack_client_info(storage)
|
|
173
|
+
team_id = await _prompt_slack_team(ui)
|
|
174
|
+
extras = {"team": team_id} if team_id else {}
|
|
175
|
+
return LoginResult(extra_auth_params=extras)
|