claude-mpm 5.6.1__py3-none-any.whl → 5.6.76__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/PM_INSTRUCTIONS.md +8 -3
- claude_mpm/auth/__init__.py +35 -0
- claude_mpm/auth/callback_server.py +328 -0
- claude_mpm/auth/models.py +104 -0
- claude_mpm/auth/oauth_manager.py +266 -0
- claude_mpm/auth/providers/__init__.py +12 -0
- claude_mpm/auth/providers/base.py +165 -0
- claude_mpm/auth/providers/google.py +261 -0
- claude_mpm/auth/token_storage.py +252 -0
- claude_mpm/cli/commands/commander.py +174 -4
- claude_mpm/cli/commands/mcp.py +29 -17
- claude_mpm/cli/commands/mcp_command_router.py +39 -0
- claude_mpm/cli/commands/mcp_service_commands.py +304 -0
- claude_mpm/cli/commands/oauth.py +481 -0
- claude_mpm/cli/commands/skill_source.py +51 -2
- claude_mpm/cli/commands/skills.py +5 -3
- claude_mpm/cli/executor.py +9 -0
- claude_mpm/cli/helpers.py +1 -1
- claude_mpm/cli/parsers/base_parser.py +13 -0
- claude_mpm/cli/parsers/commander_parser.py +43 -10
- claude_mpm/cli/parsers/mcp_parser.py +79 -0
- claude_mpm/cli/parsers/oauth_parser.py +165 -0
- claude_mpm/cli/parsers/skill_source_parser.py +4 -0
- claude_mpm/cli/parsers/skills_parser.py +5 -0
- claude_mpm/cli/startup.py +300 -33
- claude_mpm/cli/startup_display.py +4 -2
- claude_mpm/cli/startup_migrations.py +236 -0
- claude_mpm/commander/__init__.py +6 -0
- claude_mpm/commander/adapters/__init__.py +32 -3
- claude_mpm/commander/adapters/auggie.py +260 -0
- claude_mpm/commander/adapters/base.py +98 -1
- claude_mpm/commander/adapters/claude_code.py +32 -1
- claude_mpm/commander/adapters/codex.py +237 -0
- claude_mpm/commander/adapters/example_usage.py +310 -0
- claude_mpm/commander/adapters/mpm.py +389 -0
- claude_mpm/commander/adapters/registry.py +204 -0
- claude_mpm/commander/api/app.py +32 -16
- claude_mpm/commander/api/errors.py +21 -0
- claude_mpm/commander/api/routes/messages.py +11 -11
- claude_mpm/commander/api/routes/projects.py +20 -20
- claude_mpm/commander/api/routes/sessions.py +37 -26
- claude_mpm/commander/api/routes/work.py +86 -50
- claude_mpm/commander/api/schemas.py +4 -0
- claude_mpm/commander/chat/cli.py +47 -5
- claude_mpm/commander/chat/commands.py +44 -16
- claude_mpm/commander/chat/repl.py +1729 -82
- claude_mpm/commander/config.py +5 -3
- claude_mpm/commander/core/__init__.py +10 -0
- claude_mpm/commander/core/block_manager.py +325 -0
- claude_mpm/commander/core/response_manager.py +323 -0
- claude_mpm/commander/daemon.py +215 -10
- claude_mpm/commander/env_loader.py +59 -0
- claude_mpm/commander/events/manager.py +61 -1
- claude_mpm/commander/frameworks/base.py +91 -1
- claude_mpm/commander/frameworks/mpm.py +9 -14
- claude_mpm/commander/git/__init__.py +5 -0
- claude_mpm/commander/git/worktree_manager.py +212 -0
- claude_mpm/commander/instance_manager.py +546 -15
- claude_mpm/commander/memory/__init__.py +45 -0
- claude_mpm/commander/memory/compression.py +347 -0
- claude_mpm/commander/memory/embeddings.py +230 -0
- claude_mpm/commander/memory/entities.py +310 -0
- claude_mpm/commander/memory/example_usage.py +290 -0
- claude_mpm/commander/memory/integration.py +325 -0
- claude_mpm/commander/memory/search.py +381 -0
- claude_mpm/commander/memory/store.py +657 -0
- claude_mpm/commander/models/events.py +6 -0
- claude_mpm/commander/persistence/state_store.py +95 -1
- claude_mpm/commander/registry.py +10 -4
- claude_mpm/commander/runtime/monitor.py +32 -2
- claude_mpm/commander/tmux_orchestrator.py +3 -2
- claude_mpm/commander/work/executor.py +38 -20
- claude_mpm/commander/workflow/event_handler.py +25 -3
- claude_mpm/config/skill_sources.py +16 -0
- claude_mpm/constants.py +5 -0
- claude_mpm/core/claude_runner.py +152 -0
- claude_mpm/core/config.py +30 -22
- claude_mpm/core/config_constants.py +74 -9
- claude_mpm/core/constants.py +56 -12
- claude_mpm/core/hook_manager.py +2 -1
- claude_mpm/core/interactive_session.py +5 -4
- claude_mpm/core/logger.py +16 -2
- claude_mpm/core/logging_utils.py +40 -16
- claude_mpm/core/network_config.py +148 -0
- claude_mpm/core/oneshot_session.py +7 -6
- claude_mpm/core/output_style_manager.py +37 -7
- claude_mpm/core/socketio_pool.py +47 -15
- claude_mpm/core/unified_paths.py +68 -80
- claude_mpm/hooks/claude_hooks/auto_pause_handler.py +30 -31
- claude_mpm/hooks/claude_hooks/event_handlers.py +285 -194
- claude_mpm/hooks/claude_hooks/hook_handler.py +115 -32
- claude_mpm/hooks/claude_hooks/installer.py +222 -54
- claude_mpm/hooks/claude_hooks/memory_integration.py +52 -32
- claude_mpm/hooks/claude_hooks/response_tracking.py +40 -59
- claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +25 -30
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +24 -28
- claude_mpm/hooks/claude_hooks/services/container.py +326 -0
- claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
- claude_mpm/hooks/claude_hooks/services/state_manager.py +25 -38
- claude_mpm/hooks/claude_hooks/services/subagent_processor.py +49 -75
- claude_mpm/hooks/session_resume_hook.py +22 -18
- claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
- claude_mpm/hooks/templates/pre_tool_use_template.py +16 -8
- claude_mpm/init.py +21 -14
- claude_mpm/mcp/__init__.py +9 -0
- claude_mpm/mcp/google_workspace_server.py +610 -0
- claude_mpm/scripts/claude-hook-handler.sh +10 -9
- claude_mpm/services/agents/agent_selection_service.py +2 -2
- claude_mpm/services/agents/single_tier_deployment_service.py +4 -4
- claude_mpm/services/command_deployment_service.py +44 -26
- claude_mpm/services/hook_installer_service.py +77 -8
- claude_mpm/services/mcp_config_manager.py +99 -19
- claude_mpm/services/mcp_service_registry.py +294 -0
- claude_mpm/services/monitor/server.py +6 -1
- claude_mpm/services/pm_skills_deployer.py +5 -3
- claude_mpm/services/skills/git_skill_source_manager.py +79 -8
- claude_mpm/services/skills/selective_skill_deployer.py +28 -0
- claude_mpm/services/skills/skill_discovery_service.py +17 -1
- claude_mpm/services/skills_deployer.py +31 -5
- claude_mpm/skills/__init__.py +2 -1
- claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
- claude_mpm/skills/registry.py +295 -90
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/METADATA +28 -3
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/RECORD +131 -93
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/WHEEL +1 -1
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/entry_points.txt +2 -0
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/top_level.txt +0 -0
claude_mpm/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.6.
|
|
1
|
+
5.6.76
|
|
@@ -294,6 +294,8 @@ If you're about to run ANY other command, stop and delegate instead.
|
|
|
294
294
|
- Grep (>1), Glob (investigation) → Delegate to research
|
|
295
295
|
- `mcp__mcp-ticketer__*` → Delegate to ticketing
|
|
296
296
|
- `mcp__chrome-devtools__*` → Delegate to web-qa
|
|
297
|
+
- `mcp__claude-in-chrome__*` → Delegate to web-qa
|
|
298
|
+
- `mcp__playwright__*` → Delegate to web-qa
|
|
297
299
|
|
|
298
300
|
## Agent Deployment Architecture
|
|
299
301
|
|
|
@@ -358,7 +360,7 @@ These are EXAMPLES of routing, not an exhaustive list. **Default to delegation f
|
|
|
358
360
|
| **Research** | Understanding codebase, investigating approaches, analyzing files | Grep, Glob, Read multiple files, WebSearch | Investigation tools |
|
|
359
361
|
| **Engineer** | Writing/modifying code, implementing features, refactoring | Edit, Write, codebase knowledge, testing workflows | - |
|
|
360
362
|
| **Ops** (local-ops) | Deploying apps, managing infrastructure, starting servers, port/process management | Environment config, deployment procedures | Use `local-ops` for localhost/PM2/docker |
|
|
361
|
-
| **QA** (web-qa, api-qa) | Testing implementations, verifying deployments, regression tests, browser testing | Playwright (web), fetch (APIs), verification protocols | For browser: use **web-qa** (never use chrome-devtools directly) |
|
|
363
|
+
| **QA** (web-qa, api-qa) | Testing implementations, verifying deployments, regression tests, browser testing | Playwright (web), fetch (APIs), verification protocols | For browser: use **web-qa** (never use chrome-devtools, claude-in-chrome, or playwright directly) |
|
|
362
364
|
| **Documentation** | Creating/updating docs, README, API docs, guides | Style consistency, organization standards | - |
|
|
363
365
|
| **Ticketing** | ALL ticket operations (CRUD, search, hierarchy, comments) | Direct mcp-ticketer access | PM never uses `mcp__mcp-ticketer__*` directly |
|
|
364
366
|
| **Version Control** | Creating PRs, managing branches, complex git ops | PR workflows, branch management | Check git user for main branch access (bobmatnyc@users.noreply.github.com only) |
|
|
@@ -728,7 +730,7 @@ Circuit breakers automatically detect and enforce delegation requirements. All c
|
|
|
728
730
|
| 3 | Unverified Assertions | PM claiming status without agent evidence | Require verification evidence | [Details](#circuit-breaker-3-unverified-assertions) |
|
|
729
731
|
| 4 | File Tracking | PM marking task complete without tracking new files | Run git tracking sequence | [Details](#circuit-breaker-4-file-tracking-enforcement) |
|
|
730
732
|
| 5 | Delegation Chain | PM claiming completion without full workflow delegation | Execute missing phases | [Details](#circuit-breaker-5-delegation-chain) |
|
|
731
|
-
| 6 | Forbidden Tool Usage | PM using ticketing/browser MCP tools directly | Delegate to specialist agent | [Details](#circuit-breaker-6-forbidden-tool-usage) |
|
|
733
|
+
| 6 | Forbidden Tool Usage | PM using ticketing/browser MCP tools (ticketer, chrome-devtools, claude-in-chrome, playwright) directly | Delegate to specialist agent | [Details](#circuit-breaker-6-forbidden-tool-usage) |
|
|
732
734
|
| 7 | Verification Commands | PM using curl/lsof/ps/wget/nc | Delegate to local-ops or QA | [Details](#circuit-breaker-7-verification-command-detection) |
|
|
733
735
|
| 8 | QA Verification Gate | PM claiming work complete without QA delegation | BLOCK - Delegate to QA now | [Details](#circuit-breaker-8-qa-verification-gate) |
|
|
734
736
|
| 9 | User Delegation | PM instructing user to run commands | Delegate to appropriate agent | [Details](#circuit-breaker-9-user-delegation-detection) |
|
|
@@ -747,6 +749,9 @@ Circuit breakers automatically detect and enforce delegation requirements. All c
|
|
|
747
749
|
- "It works" / "It's deployed" → Circuit Breaker #3
|
|
748
750
|
- Marks todo complete without `git status` → Circuit Breaker #4
|
|
749
751
|
- Uses `mcp__mcp-ticketer__*` → Circuit Breaker #6
|
|
752
|
+
- Uses `mcp__chrome-devtools__*` → Circuit Breaker #6
|
|
753
|
+
- Uses `mcp__claude-in-chrome__*` → Circuit Breaker #6
|
|
754
|
+
- Uses `mcp__playwright__*` → Circuit Breaker #6
|
|
750
755
|
- Uses curl/lsof directly → Circuit Breaker #7
|
|
751
756
|
- Claims complete without QA → Circuit Breaker #8
|
|
752
757
|
- "You'll need to run..." → Circuit Breaker #9
|
|
@@ -782,7 +787,7 @@ When the user says "just do it" or "handle it", delegate to the full workflow pi
|
|
|
782
787
|
|
|
783
788
|
When the user says "verify", "check", or "test", delegate to the QA agent with specific verification criteria.
|
|
784
789
|
|
|
785
|
-
When the user mentions "browser", "screenshot", "click", "navigate", "DOM", "console errors", delegate to web-qa agent for browser testing (NEVER use chrome-devtools tools directly).
|
|
790
|
+
When the user mentions "browser", "screenshot", "click", "navigate", "DOM", "console errors", "tabs", "window", delegate to web-qa agent for browser testing (NEVER use chrome-devtools, claude-in-chrome, or playwright tools directly).
|
|
786
791
|
|
|
787
792
|
When the user mentions "localhost", "local server", or "PM2", delegate to **local-ops** as the primary choice for local development operations.
|
|
788
793
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""OAuth authentication module for MCP services.
|
|
2
|
+
|
|
3
|
+
This module provides secure OAuth token management and callback handling
|
|
4
|
+
for authenticating with MCP services that require OAuth2 flows.
|
|
5
|
+
|
|
6
|
+
Core Components:
|
|
7
|
+
- OAuthToken: Token data model with expiration handling
|
|
8
|
+
- TokenStorage: Secure encrypted token persistence
|
|
9
|
+
- OAuthCallbackServer: Local HTTP server for OAuth callbacks
|
|
10
|
+
- OAuthProvider: Abstract base class for OAuth providers
|
|
11
|
+
- GoogleOAuthProvider: Google OAuth2 implementation
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from claude_mpm.auth.callback_server import OAuthCallbackServer
|
|
15
|
+
from claude_mpm.auth.models import (
|
|
16
|
+
OAuthToken,
|
|
17
|
+
StoredToken,
|
|
18
|
+
TokenMetadata,
|
|
19
|
+
TokenStatus,
|
|
20
|
+
)
|
|
21
|
+
from claude_mpm.auth.oauth_manager import OAuthManager
|
|
22
|
+
from claude_mpm.auth.providers import GoogleOAuthProvider, OAuthProvider
|
|
23
|
+
from claude_mpm.auth.token_storage import TokenStorage
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"GoogleOAuthProvider",
|
|
27
|
+
"OAuthCallbackServer",
|
|
28
|
+
"OAuthManager",
|
|
29
|
+
"OAuthProvider",
|
|
30
|
+
"OAuthToken",
|
|
31
|
+
"StoredToken",
|
|
32
|
+
"TokenMetadata",
|
|
33
|
+
"TokenStatus",
|
|
34
|
+
"TokenStorage",
|
|
35
|
+
]
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""OAuth callback server for handling authorization redirects.
|
|
2
|
+
|
|
3
|
+
This module provides a local HTTP server that handles OAuth2 callback
|
|
4
|
+
redirects, capturing authorization codes and tokens from OAuth providers.
|
|
5
|
+
|
|
6
|
+
Security Features:
|
|
7
|
+
- Binds only to localhost (127.0.0.1)
|
|
8
|
+
- CSRF protection via state parameter validation
|
|
9
|
+
- Automatic server shutdown after callback received
|
|
10
|
+
- Configurable timeout for callback wait
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import secrets
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
from aiohttp import web
|
|
19
|
+
|
|
20
|
+
# Default port for OAuth callback server
|
|
21
|
+
DEFAULT_PORT = 8789
|
|
22
|
+
|
|
23
|
+
# HTML response templates
|
|
24
|
+
SUCCESS_HTML = """
|
|
25
|
+
<!DOCTYPE html>
|
|
26
|
+
<html>
|
|
27
|
+
<head>
|
|
28
|
+
<title>Authorization Successful</title>
|
|
29
|
+
<style>
|
|
30
|
+
body {
|
|
31
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
32
|
+
display: flex;
|
|
33
|
+
justify-content: center;
|
|
34
|
+
align-items: center;
|
|
35
|
+
height: 100vh;
|
|
36
|
+
margin: 0;
|
|
37
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
38
|
+
}
|
|
39
|
+
.container {
|
|
40
|
+
background: white;
|
|
41
|
+
padding: 40px 60px;
|
|
42
|
+
border-radius: 12px;
|
|
43
|
+
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
|
44
|
+
text-align: center;
|
|
45
|
+
}
|
|
46
|
+
.success-icon {
|
|
47
|
+
font-size: 64px;
|
|
48
|
+
margin-bottom: 20px;
|
|
49
|
+
}
|
|
50
|
+
h1 { color: #22c55e; margin: 0 0 10px 0; }
|
|
51
|
+
p { color: #666; margin: 0; }
|
|
52
|
+
</style>
|
|
53
|
+
</head>
|
|
54
|
+
<body>
|
|
55
|
+
<div class="container">
|
|
56
|
+
<div class="success-icon">✔</div>
|
|
57
|
+
<h1>Authorization Successful</h1>
|
|
58
|
+
<p>You can close this window and return to Claude.</p>
|
|
59
|
+
</div>
|
|
60
|
+
</body>
|
|
61
|
+
</html>
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
ERROR_HTML = """
|
|
65
|
+
<!DOCTYPE html>
|
|
66
|
+
<html>
|
|
67
|
+
<head>
|
|
68
|
+
<title>Authorization Failed</title>
|
|
69
|
+
<style>
|
|
70
|
+
body {
|
|
71
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
72
|
+
display: flex;
|
|
73
|
+
justify-content: center;
|
|
74
|
+
align-items: center;
|
|
75
|
+
height: 100vh;
|
|
76
|
+
margin: 0;
|
|
77
|
+
background: linear-gradient(135deg, #f87171 0%, #dc2626 100%);
|
|
78
|
+
}
|
|
79
|
+
.container {
|
|
80
|
+
background: white;
|
|
81
|
+
padding: 40px 60px;
|
|
82
|
+
border-radius: 12px;
|
|
83
|
+
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
|
84
|
+
text-align: center;
|
|
85
|
+
}
|
|
86
|
+
.error-icon {
|
|
87
|
+
font-size: 64px;
|
|
88
|
+
margin-bottom: 20px;
|
|
89
|
+
}
|
|
90
|
+
h1 { color: #dc2626; margin: 0 0 10px 0; }
|
|
91
|
+
p { color: #666; margin: 0; }
|
|
92
|
+
.error-detail {
|
|
93
|
+
color: #999;
|
|
94
|
+
font-size: 14px;
|
|
95
|
+
margin-top: 15px;
|
|
96
|
+
padding: 10px;
|
|
97
|
+
background: #f5f5f5;
|
|
98
|
+
border-radius: 6px;
|
|
99
|
+
}
|
|
100
|
+
</style>
|
|
101
|
+
</head>
|
|
102
|
+
<body>
|
|
103
|
+
<div class="container">
|
|
104
|
+
<div class="error-icon">✖</div>
|
|
105
|
+
<h1>Authorization Failed</h1>
|
|
106
|
+
<p>An error occurred during authorization.</p>
|
|
107
|
+
<div class="error-detail">{error}</div>
|
|
108
|
+
</div>
|
|
109
|
+
</body>
|
|
110
|
+
</html>
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass
|
|
115
|
+
class CallbackResult:
|
|
116
|
+
"""Result from an OAuth callback.
|
|
117
|
+
|
|
118
|
+
Attributes:
|
|
119
|
+
success: Whether the callback was successful.
|
|
120
|
+
code: Authorization code if successful.
|
|
121
|
+
state: State parameter from the callback.
|
|
122
|
+
error: Error message if unsuccessful.
|
|
123
|
+
error_description: Detailed error description from provider.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
success: bool
|
|
127
|
+
code: Optional[str] = None
|
|
128
|
+
state: Optional[str] = None
|
|
129
|
+
error: Optional[str] = None
|
|
130
|
+
error_description: Optional[str] = None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass
|
|
134
|
+
class OAuthCallbackServer:
|
|
135
|
+
"""Local HTTP server for OAuth2 callback handling.
|
|
136
|
+
|
|
137
|
+
This server listens on localhost for OAuth redirect callbacks,
|
|
138
|
+
captures the authorization code or token, and provides it to
|
|
139
|
+
the calling code.
|
|
140
|
+
|
|
141
|
+
The server implements CSRF protection by generating a unique
|
|
142
|
+
state parameter that must be validated in the callback.
|
|
143
|
+
|
|
144
|
+
Attributes:
|
|
145
|
+
port: Port to listen on, defaults to 8789.
|
|
146
|
+
host: Host to bind to, always 127.0.0.1 for security.
|
|
147
|
+
|
|
148
|
+
Example:
|
|
149
|
+
```python
|
|
150
|
+
server = OAuthCallbackServer()
|
|
151
|
+
state = server.generate_state()
|
|
152
|
+
|
|
153
|
+
# Use server.callback_url and state in OAuth authorization URL
|
|
154
|
+
auth_url = f"https://provider.com/oauth/authorize?redirect_uri={server.callback_url}&state={state}"
|
|
155
|
+
|
|
156
|
+
# Wait for callback (user completes auth in browser)
|
|
157
|
+
result = await server.wait_for_callback(expected_state=state, timeout=300)
|
|
158
|
+
|
|
159
|
+
if result.success:
|
|
160
|
+
print(f"Got authorization code: {result.code}")
|
|
161
|
+
else:
|
|
162
|
+
print(f"Error: {result.error}")
|
|
163
|
+
```
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
port: int = DEFAULT_PORT
|
|
167
|
+
host: str = field(default="127.0.0.1", init=False)
|
|
168
|
+
_state: Optional[str] = field(default=None, init=False, repr=False)
|
|
169
|
+
_result: Optional[CallbackResult] = field(default=None, init=False, repr=False)
|
|
170
|
+
_callback_received: asyncio.Event = field(
|
|
171
|
+
default_factory=asyncio.Event, init=False, repr=False
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def callback_url(self) -> str:
|
|
176
|
+
"""Get the callback URL for OAuth configuration.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
The full callback URL including host and port.
|
|
180
|
+
"""
|
|
181
|
+
return f"http://{self.host}:{self.port}/callback"
|
|
182
|
+
|
|
183
|
+
def generate_state(self) -> str:
|
|
184
|
+
"""Generate a cryptographically secure state parameter.
|
|
185
|
+
|
|
186
|
+
The state parameter is used for CSRF protection in the OAuth flow.
|
|
187
|
+
It should be included in the authorization request and validated
|
|
188
|
+
when the callback is received.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
A 32-character URL-safe random string.
|
|
192
|
+
"""
|
|
193
|
+
self._state = secrets.token_urlsafe(24)
|
|
194
|
+
return self._state
|
|
195
|
+
|
|
196
|
+
async def _handle_callback(self, request: web.Request) -> web.Response:
|
|
197
|
+
"""Handle the OAuth callback request.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
request: The incoming HTTP request.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
HTML response indicating success or failure.
|
|
204
|
+
"""
|
|
205
|
+
# Extract query parameters
|
|
206
|
+
code = request.query.get("code")
|
|
207
|
+
state = request.query.get("state")
|
|
208
|
+
error = request.query.get("error")
|
|
209
|
+
error_description = request.query.get("error_description", "")
|
|
210
|
+
|
|
211
|
+
# Check for error from provider
|
|
212
|
+
if error:
|
|
213
|
+
self._result = CallbackResult(
|
|
214
|
+
success=False,
|
|
215
|
+
state=state,
|
|
216
|
+
error=error,
|
|
217
|
+
error_description=error_description,
|
|
218
|
+
)
|
|
219
|
+
self._callback_received.set()
|
|
220
|
+
return web.Response(
|
|
221
|
+
text=ERROR_HTML.format(error=f"{error}: {error_description}"),
|
|
222
|
+
content_type="text/html",
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# Validate state parameter (CSRF protection)
|
|
226
|
+
if self._state and state != self._state:
|
|
227
|
+
self._result = CallbackResult(
|
|
228
|
+
success=False,
|
|
229
|
+
state=state,
|
|
230
|
+
error="state_mismatch",
|
|
231
|
+
error_description="State parameter does not match. Possible CSRF attack.",
|
|
232
|
+
)
|
|
233
|
+
self._callback_received.set()
|
|
234
|
+
return web.Response(
|
|
235
|
+
text=ERROR_HTML.format(error="State mismatch - possible CSRF attack"),
|
|
236
|
+
content_type="text/html",
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Check for authorization code
|
|
240
|
+
if not code:
|
|
241
|
+
self._result = CallbackResult(
|
|
242
|
+
success=False,
|
|
243
|
+
state=state,
|
|
244
|
+
error="missing_code",
|
|
245
|
+
error_description="No authorization code received.",
|
|
246
|
+
)
|
|
247
|
+
self._callback_received.set()
|
|
248
|
+
return web.Response(
|
|
249
|
+
text=ERROR_HTML.format(error="No authorization code received"),
|
|
250
|
+
content_type="text/html",
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Success
|
|
254
|
+
self._result = CallbackResult(
|
|
255
|
+
success=True,
|
|
256
|
+
code=code,
|
|
257
|
+
state=state,
|
|
258
|
+
)
|
|
259
|
+
self._callback_received.set()
|
|
260
|
+
return web.Response(text=SUCCESS_HTML, content_type="text/html")
|
|
261
|
+
|
|
262
|
+
async def wait_for_callback(
|
|
263
|
+
self,
|
|
264
|
+
expected_state: Optional[str] = None,
|
|
265
|
+
timeout: float = 300.0,
|
|
266
|
+
) -> CallbackResult:
|
|
267
|
+
"""Start the server and wait for an OAuth callback.
|
|
268
|
+
|
|
269
|
+
This method starts the HTTP server, waits for a callback request,
|
|
270
|
+
validates the state parameter, and returns the result.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
expected_state: State parameter to validate against.
|
|
274
|
+
If not provided, uses the last generated state.
|
|
275
|
+
timeout: Maximum time to wait for callback in seconds.
|
|
276
|
+
Defaults to 300 seconds (5 minutes).
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
CallbackResult containing the authorization code or error.
|
|
280
|
+
|
|
281
|
+
Raises:
|
|
282
|
+
asyncio.TimeoutError: If no callback received within timeout.
|
|
283
|
+
"""
|
|
284
|
+
# Set expected state
|
|
285
|
+
if expected_state:
|
|
286
|
+
self._state = expected_state
|
|
287
|
+
|
|
288
|
+
# Reset state for new wait
|
|
289
|
+
self._result = None
|
|
290
|
+
self._callback_received.clear()
|
|
291
|
+
|
|
292
|
+
# Create aiohttp app and routes
|
|
293
|
+
app = web.Application()
|
|
294
|
+
app.router.add_get("/callback", self._handle_callback)
|
|
295
|
+
|
|
296
|
+
# Create and start runner
|
|
297
|
+
runner = web.AppRunner(app)
|
|
298
|
+
await runner.setup()
|
|
299
|
+
|
|
300
|
+
site = web.TCPSite(runner, self.host, self.port)
|
|
301
|
+
await site.start()
|
|
302
|
+
|
|
303
|
+
try:
|
|
304
|
+
# Wait for callback with timeout
|
|
305
|
+
await asyncio.wait_for(
|
|
306
|
+
self._callback_received.wait(),
|
|
307
|
+
timeout=timeout,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
if self._result is None:
|
|
311
|
+
return CallbackResult(
|
|
312
|
+
success=False,
|
|
313
|
+
error="unknown_error",
|
|
314
|
+
error_description="Callback received but no result set.",
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
return self._result
|
|
318
|
+
|
|
319
|
+
except asyncio.TimeoutError:
|
|
320
|
+
return CallbackResult(
|
|
321
|
+
success=False,
|
|
322
|
+
error="timeout",
|
|
323
|
+
error_description=f"No callback received within {timeout} seconds.",
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
finally:
|
|
327
|
+
# Clean up server
|
|
328
|
+
await runner.cleanup()
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""OAuth data models for token management.
|
|
2
|
+
|
|
3
|
+
This module defines Pydantic models for OAuth tokens and their metadata,
|
|
4
|
+
providing type-safe token handling with automatic validation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TokenStatus(str, Enum):
|
|
15
|
+
"""Status of an OAuth token."""
|
|
16
|
+
|
|
17
|
+
VALID = "valid"
|
|
18
|
+
EXPIRED = "expired"
|
|
19
|
+
MISSING = "missing"
|
|
20
|
+
INVALID = "invalid"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class OAuthToken(BaseModel):
|
|
24
|
+
"""OAuth2 token data.
|
|
25
|
+
|
|
26
|
+
Represents the token response from an OAuth provider with
|
|
27
|
+
expiration tracking and scope management.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
access_token: The access token string for API authentication.
|
|
31
|
+
refresh_token: Optional refresh token for token renewal.
|
|
32
|
+
expires_at: UTC timestamp when the access token expires.
|
|
33
|
+
scopes: List of granted OAuth scopes.
|
|
34
|
+
token_type: Token type, typically "Bearer".
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
access_token: str = Field(..., description="OAuth access token")
|
|
38
|
+
refresh_token: Optional[str] = Field(
|
|
39
|
+
default=None, description="OAuth refresh token for renewal"
|
|
40
|
+
)
|
|
41
|
+
expires_at: datetime = Field(..., description="Token expiration timestamp (UTC)")
|
|
42
|
+
scopes: list[str] = Field(default_factory=list, description="Granted OAuth scopes")
|
|
43
|
+
token_type: str = Field(default="Bearer", description="Token type")
|
|
44
|
+
|
|
45
|
+
def is_expired(self, buffer_seconds: int = 60) -> bool:
|
|
46
|
+
"""Check if the token is expired or about to expire.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
buffer_seconds: Number of seconds before actual expiration
|
|
50
|
+
to consider the token expired. Defaults to 60 seconds
|
|
51
|
+
to allow time for token refresh.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
True if the token is expired or will expire within the buffer period.
|
|
55
|
+
"""
|
|
56
|
+
now = datetime.now(timezone.utc)
|
|
57
|
+
# Ensure expires_at is timezone-aware
|
|
58
|
+
expires_at = self.expires_at
|
|
59
|
+
if expires_at.tzinfo is None:
|
|
60
|
+
expires_at = expires_at.replace(tzinfo=timezone.utc)
|
|
61
|
+
|
|
62
|
+
from datetime import timedelta
|
|
63
|
+
|
|
64
|
+
return now >= (expires_at - timedelta(seconds=buffer_seconds))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TokenMetadata(BaseModel):
|
|
68
|
+
"""Metadata about a stored OAuth token.
|
|
69
|
+
|
|
70
|
+
Tracks service information and timestamps for token lifecycle management.
|
|
71
|
+
|
|
72
|
+
Attributes:
|
|
73
|
+
service_name: Name of the MCP service this token authenticates.
|
|
74
|
+
provider: OAuth provider identifier (e.g., "github", "google").
|
|
75
|
+
created_at: When the token was first stored.
|
|
76
|
+
last_refreshed: When the token was last refreshed, if applicable.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
service_name: str = Field(..., description="MCP service name")
|
|
80
|
+
provider: str = Field(..., description="OAuth provider identifier")
|
|
81
|
+
created_at: datetime = Field(
|
|
82
|
+
default_factory=lambda: datetime.now(timezone.utc),
|
|
83
|
+
description="Token creation timestamp",
|
|
84
|
+
)
|
|
85
|
+
last_refreshed: Optional[datetime] = Field(
|
|
86
|
+
default=None, description="Last token refresh timestamp"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class StoredToken(BaseModel):
|
|
91
|
+
"""Complete stored token with metadata and versioning.
|
|
92
|
+
|
|
93
|
+
This is the top-level structure persisted to storage, containing
|
|
94
|
+
both the token data and metadata needed for management.
|
|
95
|
+
|
|
96
|
+
Attributes:
|
|
97
|
+
version: Schema version for future migration support.
|
|
98
|
+
metadata: Token metadata including service and provider info.
|
|
99
|
+
token: The actual OAuth token data.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
version: int = Field(default=1, description="Schema version for migrations")
|
|
103
|
+
metadata: TokenMetadata = Field(..., description="Token metadata")
|
|
104
|
+
token: OAuthToken = Field(..., description="OAuth token data")
|