quickcall-integrations 0.1.3__py3-none-any.whl → 0.1.4__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.
- mcp_server/api_clients/__init__.py +6 -0
- mcp_server/api_clients/github_client.py +440 -0
- mcp_server/api_clients/slack_client.py +359 -0
- mcp_server/auth/__init__.py +24 -0
- mcp_server/auth/credentials.py +278 -0
- mcp_server/auth/device_flow.py +253 -0
- mcp_server/server.py +54 -2
- mcp_server/tools/__init__.py +12 -0
- mcp_server/tools/auth_tools.py +411 -0
- mcp_server/tools/git_tools.py +42 -18
- mcp_server/tools/github_tools.py +338 -0
- mcp_server/tools/slack_tools.py +203 -0
- {quickcall_integrations-0.1.3.dist-info → quickcall_integrations-0.1.4.dist-info}/METADATA +20 -5
- quickcall_integrations-0.1.4.dist-info/RECORD +18 -0
- mcp_server/config.py +0 -10
- quickcall_integrations-0.1.3.dist-info/RECORD +0 -10
- {quickcall_integrations-0.1.3.dist-info → quickcall_integrations-0.1.4.dist-info}/WHEEL +0 -0
- {quickcall_integrations-0.1.3.dist-info → quickcall_integrations-0.1.4.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OAuth Device Flow Authentication for QuickCall MCP.
|
|
3
|
+
|
|
4
|
+
Implements RFC 8628 device authorization flow:
|
|
5
|
+
1. CLI calls init to get device_code + user_code
|
|
6
|
+
2. User visits quickcall.dev/cli/setup?code={user_code}
|
|
7
|
+
3. User signs in with Google (Clerk)
|
|
8
|
+
4. CLI polls until complete, receives device_token
|
|
9
|
+
5. Device token stored locally for future API calls
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import time
|
|
14
|
+
import logging
|
|
15
|
+
import webbrowser
|
|
16
|
+
from typing import Optional, Tuple
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
|
|
19
|
+
import httpx
|
|
20
|
+
|
|
21
|
+
from mcp_server.auth.credentials import (
|
|
22
|
+
CredentialStore,
|
|
23
|
+
StoredCredentials,
|
|
24
|
+
get_credential_store,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
# QuickCall URLs - configurable via environment for local testing
|
|
30
|
+
# Set QUICKCALL_API_URL=http://localhost:8000 for local backend
|
|
31
|
+
# Set QUICKCALL_WEB_URL=http://localhost:3000 for local frontend
|
|
32
|
+
QUICKCALL_API_URL = os.getenv("QUICKCALL_API_URL", "https://api.quickcall.dev")
|
|
33
|
+
QUICKCALL_WEB_URL = os.getenv("QUICKCALL_WEB_URL", "https://quickcall.dev")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DeviceFlowAuth:
|
|
37
|
+
"""
|
|
38
|
+
Handles OAuth device flow authentication.
|
|
39
|
+
|
|
40
|
+
Usage:
|
|
41
|
+
auth = DeviceFlowAuth()
|
|
42
|
+
|
|
43
|
+
# Start flow (opens browser)
|
|
44
|
+
success = auth.authenticate()
|
|
45
|
+
|
|
46
|
+
if success:
|
|
47
|
+
print("Authenticated!")
|
|
48
|
+
# Credentials are now stored in ~/.quickcall/credentials.json
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
api_url: Optional[str] = None,
|
|
54
|
+
web_url: Optional[str] = None,
|
|
55
|
+
credential_store: Optional[CredentialStore] = None,
|
|
56
|
+
):
|
|
57
|
+
"""
|
|
58
|
+
Initialize device flow authentication.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
api_url: QuickCall API URL (defaults to production)
|
|
62
|
+
web_url: QuickCall web URL (defaults to production)
|
|
63
|
+
credential_store: Credential store instance (defaults to global)
|
|
64
|
+
"""
|
|
65
|
+
self.api_url = api_url or QUICKCALL_API_URL
|
|
66
|
+
self.web_url = web_url or QUICKCALL_WEB_URL
|
|
67
|
+
self.credential_store = credential_store or get_credential_store()
|
|
68
|
+
|
|
69
|
+
def init_flow(self) -> Tuple[str, str, str, int, int]:
|
|
70
|
+
"""
|
|
71
|
+
Initialize device authorization flow.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Tuple of (device_code, user_code, verification_url, expires_in, interval)
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
Exception if initialization fails
|
|
78
|
+
"""
|
|
79
|
+
with httpx.Client(timeout=30.0) as client:
|
|
80
|
+
response = client.post(f"{self.api_url}/api/device/init")
|
|
81
|
+
response.raise_for_status()
|
|
82
|
+
data = response.json()
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
data["device_code"],
|
|
86
|
+
data["user_code"],
|
|
87
|
+
data["verification_url"],
|
|
88
|
+
data["expires_in"],
|
|
89
|
+
data["interval"],
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def poll_for_completion(
|
|
93
|
+
self,
|
|
94
|
+
device_code: str,
|
|
95
|
+
interval: int = 5,
|
|
96
|
+
timeout: int = 900,
|
|
97
|
+
on_poll: Optional[callable] = None,
|
|
98
|
+
) -> Optional[StoredCredentials]:
|
|
99
|
+
"""
|
|
100
|
+
Poll for device authorization completion.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
device_code: Device code from init_flow
|
|
104
|
+
interval: Polling interval in seconds
|
|
105
|
+
timeout: Maximum time to wait in seconds
|
|
106
|
+
on_poll: Optional callback called on each poll (for progress indication)
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
StoredCredentials if successful, None if expired/cancelled
|
|
110
|
+
"""
|
|
111
|
+
start_time = time.time()
|
|
112
|
+
|
|
113
|
+
with httpx.Client(timeout=30.0) as client:
|
|
114
|
+
while time.time() - start_time < timeout:
|
|
115
|
+
if on_poll:
|
|
116
|
+
on_poll()
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
response = client.get(
|
|
120
|
+
f"{self.api_url}/api/device/status",
|
|
121
|
+
params={"device_code": device_code},
|
|
122
|
+
)
|
|
123
|
+
response.raise_for_status()
|
|
124
|
+
data = response.json()
|
|
125
|
+
|
|
126
|
+
status = data["status"]
|
|
127
|
+
|
|
128
|
+
if status == "complete":
|
|
129
|
+
# Success! Save credentials
|
|
130
|
+
credentials = StoredCredentials(
|
|
131
|
+
device_token=data["device_token"],
|
|
132
|
+
user_id=data["user_id"],
|
|
133
|
+
authenticated_at=datetime.now(timezone.utc)
|
|
134
|
+
.isoformat()
|
|
135
|
+
.replace("+00:00", "Z"),
|
|
136
|
+
)
|
|
137
|
+
self.credential_store.save(credentials)
|
|
138
|
+
return credentials
|
|
139
|
+
|
|
140
|
+
elif status == "expired":
|
|
141
|
+
logger.warning("Authorization code expired")
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
elif status == "revoked":
|
|
145
|
+
logger.warning("Authorization was revoked")
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
# Status is "pending", continue polling
|
|
149
|
+
time.sleep(interval)
|
|
150
|
+
|
|
151
|
+
except httpx.HTTPStatusError as e:
|
|
152
|
+
if e.response.status_code == 404:
|
|
153
|
+
logger.error("Device code not found")
|
|
154
|
+
return None
|
|
155
|
+
raise
|
|
156
|
+
|
|
157
|
+
logger.warning("Polling timeout exceeded")
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
def authenticate(
|
|
161
|
+
self,
|
|
162
|
+
open_browser: bool = True,
|
|
163
|
+
print_instructions: bool = True,
|
|
164
|
+
) -> bool:
|
|
165
|
+
"""
|
|
166
|
+
Run the full device flow authentication.
|
|
167
|
+
|
|
168
|
+
This is the main entry point for CLI authentication:
|
|
169
|
+
1. Initializes the flow
|
|
170
|
+
2. Opens browser (or prints URL)
|
|
171
|
+
3. Polls until complete
|
|
172
|
+
4. Saves credentials
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
open_browser: Whether to automatically open browser
|
|
176
|
+
print_instructions: Whether to print user instructions
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
True if authentication successful, False otherwise
|
|
180
|
+
"""
|
|
181
|
+
try:
|
|
182
|
+
# Initialize flow
|
|
183
|
+
device_code, user_code, verification_url, expires_in, interval = (
|
|
184
|
+
self.init_flow()
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Build URL with code
|
|
188
|
+
auth_url = f"{verification_url}?code={user_code}"
|
|
189
|
+
|
|
190
|
+
if print_instructions:
|
|
191
|
+
print("\n" + "=" * 50)
|
|
192
|
+
print("QuickCall Authentication")
|
|
193
|
+
print("=" * 50)
|
|
194
|
+
print(f"\nYour code: {user_code}")
|
|
195
|
+
print(f"\nVisit: {auth_url}")
|
|
196
|
+
print("\nSign in with Google to connect your CLI.")
|
|
197
|
+
print(f"This code expires in {expires_in // 60} minutes.")
|
|
198
|
+
print("=" * 50 + "\n")
|
|
199
|
+
|
|
200
|
+
# Open browser
|
|
201
|
+
if open_browser:
|
|
202
|
+
try:
|
|
203
|
+
webbrowser.open(auth_url)
|
|
204
|
+
if print_instructions:
|
|
205
|
+
print("Browser opened. Waiting for authorization...")
|
|
206
|
+
except Exception as e:
|
|
207
|
+
logger.warning(f"Could not open browser: {e}")
|
|
208
|
+
if print_instructions:
|
|
209
|
+
print("Could not open browser. Please visit the URL manually.")
|
|
210
|
+
|
|
211
|
+
# Poll for completion
|
|
212
|
+
def on_poll():
|
|
213
|
+
if print_instructions:
|
|
214
|
+
print(".", end="", flush=True)
|
|
215
|
+
|
|
216
|
+
credentials = self.poll_for_completion(
|
|
217
|
+
device_code=device_code,
|
|
218
|
+
interval=interval,
|
|
219
|
+
on_poll=on_poll,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
if credentials:
|
|
223
|
+
if print_instructions:
|
|
224
|
+
print("\n\nAuthentication successful!")
|
|
225
|
+
print(f"Connected as: {credentials.email or credentials.user_id}")
|
|
226
|
+
return True
|
|
227
|
+
else:
|
|
228
|
+
if print_instructions:
|
|
229
|
+
print("\n\nAuthentication failed or timed out.")
|
|
230
|
+
return False
|
|
231
|
+
|
|
232
|
+
except Exception as e:
|
|
233
|
+
logger.error(f"Authentication error: {e}")
|
|
234
|
+
if print_instructions:
|
|
235
|
+
print(f"\nAuthentication error: {e}")
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
def disconnect(self) -> bool:
|
|
239
|
+
"""
|
|
240
|
+
Disconnect the current device (logout).
|
|
241
|
+
|
|
242
|
+
This clears local credentials. The device token remains valid
|
|
243
|
+
on the server until explicitly revoked from the web UI.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
True if cleared successfully
|
|
247
|
+
"""
|
|
248
|
+
try:
|
|
249
|
+
self.credential_store.clear()
|
|
250
|
+
return True
|
|
251
|
+
except Exception as e:
|
|
252
|
+
logger.error(f"Failed to disconnect: {e}")
|
|
253
|
+
return False
|
mcp_server/server.py
CHANGED
|
@@ -1,24 +1,76 @@
|
|
|
1
1
|
"""
|
|
2
2
|
QuickCall Integrations MCP Server
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
Developer integrations for Claude Code and Cursor:
|
|
5
|
+
- Local git tools (always available)
|
|
6
|
+
- GitHub API tools (requires QuickCall authentication + GitHub connected)
|
|
7
|
+
- Slack tools (requires QuickCall authentication + Slack connected)
|
|
8
|
+
|
|
9
|
+
Authentication:
|
|
10
|
+
- Run connect_quickcall to authenticate via OAuth
|
|
11
|
+
- Credentials stored locally in ~/.quickcall/credentials.json
|
|
12
|
+
- GitHub and Slack tokens fetched from quickcall.dev API
|
|
5
13
|
"""
|
|
6
14
|
|
|
7
15
|
import os
|
|
16
|
+
import logging
|
|
8
17
|
|
|
9
18
|
from fastmcp import FastMCP
|
|
10
19
|
|
|
20
|
+
from mcp_server.auth import get_credential_store
|
|
11
21
|
from mcp_server.tools.git_tools import create_git_tools
|
|
12
22
|
from mcp_server.tools.utility_tools import create_utility_tools
|
|
23
|
+
from mcp_server.tools.github_tools import create_github_tools
|
|
24
|
+
from mcp_server.tools.slack_tools import create_slack_tools
|
|
25
|
+
from mcp_server.tools.auth_tools import create_auth_tools
|
|
26
|
+
|
|
27
|
+
# Configure logging
|
|
28
|
+
logging.basicConfig(
|
|
29
|
+
level=logging.INFO,
|
|
30
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
31
|
+
)
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
13
33
|
|
|
14
34
|
|
|
15
35
|
def create_server() -> FastMCP:
|
|
16
|
-
"""Create and configure the MCP server."""
|
|
36
|
+
"""Create and configure the MCP server with graceful degradation."""
|
|
17
37
|
mcp = FastMCP("quickcall-integrations")
|
|
18
38
|
|
|
39
|
+
# Check authentication status
|
|
40
|
+
store = get_credential_store()
|
|
41
|
+
is_authenticated = store.is_authenticated()
|
|
42
|
+
|
|
43
|
+
# Always register local git tools (no credentials needed)
|
|
19
44
|
create_git_tools(mcp)
|
|
20
45
|
create_utility_tools(mcp)
|
|
21
46
|
|
|
47
|
+
# Register authentication tools (always available)
|
|
48
|
+
create_auth_tools(mcp)
|
|
49
|
+
logger.info(
|
|
50
|
+
"Auth tools: enabled (connect_quickcall, check_quickcall_status, disconnect_quickcall)"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Register GitHub and Slack tools (check credentials at runtime)
|
|
54
|
+
create_github_tools(mcp)
|
|
55
|
+
create_slack_tools(mcp)
|
|
56
|
+
|
|
57
|
+
# Log current status
|
|
58
|
+
if is_authenticated:
|
|
59
|
+
logger.info("QuickCall: authenticated")
|
|
60
|
+
creds = store.get_api_credentials()
|
|
61
|
+
if creds:
|
|
62
|
+
logger.info(
|
|
63
|
+
f"GitHub: {'connected' if creds.github_connected else 'not connected'}"
|
|
64
|
+
)
|
|
65
|
+
logger.info(
|
|
66
|
+
f"Slack: {'connected' if creds.slack_connected else 'not connected'}"
|
|
67
|
+
)
|
|
68
|
+
else:
|
|
69
|
+
logger.info("QuickCall: not authenticated")
|
|
70
|
+
logger.info(
|
|
71
|
+
"Run connect_quickcall to authenticate and enable GitHub/Slack tools"
|
|
72
|
+
)
|
|
73
|
+
|
|
22
74
|
return mcp
|
|
23
75
|
|
|
24
76
|
|
mcp_server/tools/__init__.py
CHANGED
|
@@ -1 +1,13 @@
|
|
|
1
1
|
"""MCP tools for external integrations"""
|
|
2
|
+
|
|
3
|
+
from mcp_server.tools.git_tools import create_git_tools
|
|
4
|
+
from mcp_server.tools.utility_tools import create_utility_tools
|
|
5
|
+
from mcp_server.tools.github_tools import create_github_tools
|
|
6
|
+
from mcp_server.tools.slack_tools import create_slack_tools
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"create_git_tools",
|
|
10
|
+
"create_utility_tools",
|
|
11
|
+
"create_github_tools",
|
|
12
|
+
"create_slack_tools",
|
|
13
|
+
]
|