quickcall-integrations 0.1.7__py3-none-any.whl → 0.2.0__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/__init__.py +1 -1
- mcp_server/api_clients/github_client.py +220 -62
- mcp_server/auth/__init__.py +8 -0
- mcp_server/auth/credentials.py +350 -42
- mcp_server/tools/auth_tools.py +194 -27
- mcp_server/tools/git_tools.py +174 -0
- mcp_server/tools/github_tools.py +215 -48
- {quickcall_integrations-0.1.7.dist-info → quickcall_integrations-0.2.0.dist-info}/METADATA +82 -67
- quickcall_integrations-0.2.0.dist-info/RECORD +20 -0
- quickcall_integrations-0.1.7.dist-info/RECORD +0 -20
- {quickcall_integrations-0.1.7.dist-info → quickcall_integrations-0.2.0.dist-info}/WHEEL +0 -0
- {quickcall_integrations-0.1.7.dist-info → quickcall_integrations-0.2.0.dist-info}/entry_points.txt +0 -0
mcp_server/__init__.py
CHANGED
|
@@ -78,6 +78,10 @@ class GitHubClient:
|
|
|
78
78
|
|
|
79
79
|
Provides simplified interface for GitHub operations.
|
|
80
80
|
Focuses on PRs and commits.
|
|
81
|
+
|
|
82
|
+
Supports both:
|
|
83
|
+
- GitHub App installation tokens (via QuickCall)
|
|
84
|
+
- Personal Access Tokens (PAT fallback)
|
|
81
85
|
"""
|
|
82
86
|
|
|
83
87
|
def __init__(
|
|
@@ -91,16 +95,19 @@ class GitHubClient:
|
|
|
91
95
|
Initialize GitHub API client.
|
|
92
96
|
|
|
93
97
|
Args:
|
|
94
|
-
token: GitHub installation
|
|
98
|
+
token: GitHub access token (installation token or PAT)
|
|
95
99
|
default_owner: Default repository owner (optional)
|
|
96
100
|
default_repo: Default repository name (optional)
|
|
97
|
-
installation_id: GitHub App installation ID (for
|
|
101
|
+
installation_id: GitHub App installation ID (None for PAT mode)
|
|
98
102
|
"""
|
|
99
103
|
self.token = token
|
|
100
104
|
self.default_owner = default_owner
|
|
101
105
|
self.default_repo = default_repo
|
|
102
106
|
self.installation_id = installation_id
|
|
103
107
|
|
|
108
|
+
# Detect if this is a PAT (no installation_id means PAT mode)
|
|
109
|
+
self._is_pat_mode = installation_id is None
|
|
110
|
+
|
|
104
111
|
# Initialize PyGithub client
|
|
105
112
|
auth = Auth.Token(token)
|
|
106
113
|
self.gh = Github(auth=auth)
|
|
@@ -108,6 +115,11 @@ class GitHubClient:
|
|
|
108
115
|
# Cache for repo objects
|
|
109
116
|
self._repo_cache: Dict[str, Any] = {}
|
|
110
117
|
|
|
118
|
+
@property
|
|
119
|
+
def is_pat_mode(self) -> bool:
|
|
120
|
+
"""Check if client is using PAT authentication."""
|
|
121
|
+
return self._is_pat_mode
|
|
122
|
+
|
|
111
123
|
def _get_repo(self, owner: Optional[str] = None, repo: Optional[str] = None):
|
|
112
124
|
"""Get PyGithub repo object, using defaults if not specified."""
|
|
113
125
|
owner = owner or self.default_owner
|
|
@@ -127,36 +139,50 @@ class GitHubClient:
|
|
|
127
139
|
def health_check(self) -> bool:
|
|
128
140
|
"""Check if GitHub API is accessible with the token."""
|
|
129
141
|
try:
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
+
if self._is_pat_mode:
|
|
143
|
+
# For PAT, use /user endpoint
|
|
144
|
+
user = self.gh.get_user()
|
|
145
|
+
_ = user.login # This will trigger the API call
|
|
146
|
+
return True
|
|
147
|
+
else:
|
|
148
|
+
# For installation tokens, use /installation/repositories endpoint
|
|
149
|
+
with httpx.Client() as client:
|
|
150
|
+
response = client.get(
|
|
151
|
+
"https://api.github.com/installation/repositories",
|
|
152
|
+
headers={
|
|
153
|
+
"Authorization": f"Bearer {self.token}",
|
|
154
|
+
"Accept": "application/vnd.github+json",
|
|
155
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
156
|
+
},
|
|
157
|
+
params={"per_page": 1},
|
|
158
|
+
)
|
|
159
|
+
return response.status_code == 200
|
|
142
160
|
except Exception:
|
|
143
161
|
return False
|
|
144
162
|
|
|
145
163
|
def get_authenticated_user(self) -> str:
|
|
146
164
|
"""
|
|
147
|
-
Get the GitHub username
|
|
165
|
+
Get the GitHub username for the authenticated user/installation.
|
|
148
166
|
|
|
149
|
-
|
|
150
|
-
|
|
167
|
+
For PAT: Returns the user's login
|
|
168
|
+
For GitHub App: Returns the installation owner
|
|
151
169
|
"""
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
170
|
+
if self._is_pat_mode:
|
|
171
|
+
try:
|
|
172
|
+
user = self.gh.get_user()
|
|
173
|
+
return user.login
|
|
174
|
+
except Exception:
|
|
175
|
+
return self.default_owner or "unknown"
|
|
176
|
+
else:
|
|
177
|
+
# GitHub App installation tokens can't access /user endpoint
|
|
178
|
+
# Try to get from first repo's owner
|
|
179
|
+
try:
|
|
180
|
+
repos = self.list_repos(limit=1)
|
|
181
|
+
if repos:
|
|
182
|
+
return repos[0].owner
|
|
183
|
+
except Exception:
|
|
184
|
+
pass
|
|
185
|
+
return self.default_owner or "GitHub App"
|
|
160
186
|
|
|
161
187
|
def close(self):
|
|
162
188
|
"""Close GitHub API client."""
|
|
@@ -168,7 +194,10 @@ class GitHubClient:
|
|
|
168
194
|
|
|
169
195
|
def list_repos(self, limit: int = 20) -> List[Repository]:
|
|
170
196
|
"""
|
|
171
|
-
List repositories accessible to the
|
|
197
|
+
List repositories accessible to the authenticated user/installation.
|
|
198
|
+
|
|
199
|
+
For PAT mode: Lists user's repositories
|
|
200
|
+
For GitHub App: Lists installation repositories
|
|
172
201
|
|
|
173
202
|
Args:
|
|
174
203
|
limit: Maximum repositories to return
|
|
@@ -177,43 +206,70 @@ class GitHubClient:
|
|
|
177
206
|
List of repositories
|
|
178
207
|
"""
|
|
179
208
|
repos = []
|
|
180
|
-
try:
|
|
181
|
-
# Installation tokens can't use PyGithub's user.get_repos() endpoint
|
|
182
|
-
# Must use /installation/repositories endpoint directly (same as backend)
|
|
183
|
-
# https://docs.github.com/en/rest/apps/installations#list-repositories-accessible-to-the-app-installation
|
|
184
|
-
with httpx.Client() as client:
|
|
185
|
-
response = client.get(
|
|
186
|
-
"https://api.github.com/installation/repositories",
|
|
187
|
-
headers={
|
|
188
|
-
"Authorization": f"Bearer {self.token}",
|
|
189
|
-
"Accept": "application/vnd.github+json",
|
|
190
|
-
"X-GitHub-Api-Version": "2022-11-28",
|
|
191
|
-
},
|
|
192
|
-
params={"per_page": limit},
|
|
193
|
-
)
|
|
194
|
-
response.raise_for_status()
|
|
195
|
-
data = response.json()
|
|
196
209
|
|
|
197
|
-
|
|
210
|
+
if self._is_pat_mode:
|
|
211
|
+
# PAT mode: Use PyGithub's user.get_repos()
|
|
212
|
+
try:
|
|
213
|
+
user = self.gh.get_user()
|
|
214
|
+
for i, gh_repo in enumerate(user.get_repos(sort="updated")):
|
|
215
|
+
if i >= limit:
|
|
216
|
+
break
|
|
198
217
|
repos.append(
|
|
199
218
|
Repository(
|
|
200
|
-
name=
|
|
201
|
-
owner=
|
|
202
|
-
full_name=
|
|
203
|
-
html_url=
|
|
204
|
-
description=
|
|
205
|
-
default_branch=
|
|
206
|
-
private=
|
|
219
|
+
name=gh_repo.name,
|
|
220
|
+
owner=gh_repo.owner.login,
|
|
221
|
+
full_name=gh_repo.full_name,
|
|
222
|
+
html_url=gh_repo.html_url,
|
|
223
|
+
description=gh_repo.description or "",
|
|
224
|
+
default_branch=gh_repo.default_branch,
|
|
225
|
+
private=gh_repo.private,
|
|
207
226
|
)
|
|
208
227
|
)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
228
|
+
except GithubException as e:
|
|
229
|
+
logger.error(f"Failed to list user repos: {e}")
|
|
230
|
+
raise
|
|
231
|
+
except Exception as e:
|
|
232
|
+
logger.error(f"Failed to list user repos: {e}")
|
|
233
|
+
raise
|
|
234
|
+
else:
|
|
235
|
+
# GitHub App mode: Use /installation/repositories endpoint
|
|
236
|
+
try:
|
|
237
|
+
# Installation tokens can't use PyGithub's user.get_repos() endpoint
|
|
238
|
+
# Must use /installation/repositories endpoint directly (same as backend)
|
|
239
|
+
# https://docs.github.com/en/rest/apps/installations#list-repositories-accessible-to-the-app-installation
|
|
240
|
+
with httpx.Client() as client:
|
|
241
|
+
response = client.get(
|
|
242
|
+
"https://api.github.com/installation/repositories",
|
|
243
|
+
headers={
|
|
244
|
+
"Authorization": f"Bearer {self.token}",
|
|
245
|
+
"Accept": "application/vnd.github+json",
|
|
246
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
247
|
+
},
|
|
248
|
+
params={"per_page": limit},
|
|
249
|
+
)
|
|
250
|
+
response.raise_for_status()
|
|
251
|
+
data = response.json()
|
|
252
|
+
|
|
253
|
+
for repo_data in data.get("repositories", [])[:limit]:
|
|
254
|
+
repos.append(
|
|
255
|
+
Repository(
|
|
256
|
+
name=repo_data["name"],
|
|
257
|
+
owner=repo_data["owner"]["login"],
|
|
258
|
+
full_name=repo_data["full_name"],
|
|
259
|
+
html_url=repo_data["html_url"],
|
|
260
|
+
description=repo_data.get("description") or "",
|
|
261
|
+
default_branch=repo_data.get("default_branch", "main"),
|
|
262
|
+
private=repo_data.get("private", False),
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
except httpx.HTTPStatusError as e:
|
|
266
|
+
logger.error(
|
|
267
|
+
f"Failed to list installation repos: HTTP {e.response.status_code}"
|
|
268
|
+
)
|
|
269
|
+
raise GithubException(e.response.status_code, e.response.json())
|
|
270
|
+
except Exception as e:
|
|
271
|
+
logger.error(f"Failed to list installation repos: {e}")
|
|
272
|
+
raise
|
|
217
273
|
|
|
218
274
|
return repos
|
|
219
275
|
|
|
@@ -258,10 +314,15 @@ class GitHubClient:
|
|
|
258
314
|
gh_repo = self._get_repo(owner, repo)
|
|
259
315
|
prs = []
|
|
260
316
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
317
|
+
try:
|
|
318
|
+
pulls = gh_repo.get_pulls(state=state, sort="updated", direction="desc")
|
|
319
|
+
for i, pr in enumerate(pulls):
|
|
320
|
+
if i >= limit:
|
|
321
|
+
break
|
|
322
|
+
prs.append(self._convert_pr(pr))
|
|
323
|
+
except IndexError:
|
|
324
|
+
# Empty repo or no PRs - return empty list
|
|
325
|
+
pass
|
|
265
326
|
|
|
266
327
|
return prs
|
|
267
328
|
|
|
@@ -460,3 +521,100 @@ class GitHubClient:
|
|
|
460
521
|
)
|
|
461
522
|
|
|
462
523
|
return branches
|
|
524
|
+
|
|
525
|
+
# ========================================================================
|
|
526
|
+
# Search Operations (for Appraisals)
|
|
527
|
+
# ========================================================================
|
|
528
|
+
|
|
529
|
+
def search_merged_prs(
|
|
530
|
+
self,
|
|
531
|
+
author: Optional[str] = None,
|
|
532
|
+
since_date: Optional[str] = None,
|
|
533
|
+
org: Optional[str] = None,
|
|
534
|
+
repo: Optional[str] = None,
|
|
535
|
+
limit: int = 100,
|
|
536
|
+
) -> List[Dict[str, Any]]:
|
|
537
|
+
"""
|
|
538
|
+
Search for merged pull requests using GitHub Search API.
|
|
539
|
+
|
|
540
|
+
Ideal for gathering contribution data for appraisals/reviews.
|
|
541
|
+
|
|
542
|
+
Args:
|
|
543
|
+
author: GitHub username to filter by
|
|
544
|
+
since_date: ISO date string (YYYY-MM-DD) - only PRs merged after this date
|
|
545
|
+
org: GitHub org to search within
|
|
546
|
+
repo: Specific repo in "owner/repo" format (overrides org if specified)
|
|
547
|
+
limit: Maximum PRs to return (max 100 per page)
|
|
548
|
+
|
|
549
|
+
Returns:
|
|
550
|
+
List of merged PR dicts with: number, title, body, merged_at,
|
|
551
|
+
labels, repo, owner, html_url
|
|
552
|
+
"""
|
|
553
|
+
# Build search query
|
|
554
|
+
query_parts = ["is:pr", "is:merged"]
|
|
555
|
+
|
|
556
|
+
if author:
|
|
557
|
+
query_parts.append(f"author:{author}")
|
|
558
|
+
|
|
559
|
+
if since_date:
|
|
560
|
+
query_parts.append(f"merged:>={since_date}")
|
|
561
|
+
|
|
562
|
+
# repo takes precedence over org
|
|
563
|
+
if repo:
|
|
564
|
+
query_parts.append(f"repo:{repo}")
|
|
565
|
+
elif org:
|
|
566
|
+
query_parts.append(f"org:{org}")
|
|
567
|
+
|
|
568
|
+
query = " ".join(query_parts)
|
|
569
|
+
|
|
570
|
+
try:
|
|
571
|
+
with httpx.Client() as client:
|
|
572
|
+
response = client.get(
|
|
573
|
+
"https://api.github.com/search/issues",
|
|
574
|
+
headers={
|
|
575
|
+
"Authorization": f"Bearer {self.token}",
|
|
576
|
+
"Accept": "application/vnd.github+json",
|
|
577
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
578
|
+
},
|
|
579
|
+
params={
|
|
580
|
+
"q": query,
|
|
581
|
+
"sort": "updated",
|
|
582
|
+
"order": "desc",
|
|
583
|
+
"per_page": min(limit, 100),
|
|
584
|
+
},
|
|
585
|
+
timeout=30.0,
|
|
586
|
+
)
|
|
587
|
+
response.raise_for_status()
|
|
588
|
+
data = response.json()
|
|
589
|
+
|
|
590
|
+
# Convert to simplified format
|
|
591
|
+
prs = []
|
|
592
|
+
for item in data.get("items", [])[:limit]:
|
|
593
|
+
# Extract repo info from repository_url
|
|
594
|
+
# Format: https://api.github.com/repos/owner/repo
|
|
595
|
+
repo_url_parts = item.get("repository_url", "").split("/")
|
|
596
|
+
repo_owner = repo_url_parts[-2] if len(repo_url_parts) >= 2 else ""
|
|
597
|
+
repo_name = repo_url_parts[-1] if len(repo_url_parts) >= 1 else ""
|
|
598
|
+
|
|
599
|
+
prs.append(
|
|
600
|
+
{
|
|
601
|
+
"number": item["number"],
|
|
602
|
+
"title": item["title"],
|
|
603
|
+
"body": item.get("body") or "",
|
|
604
|
+
"merged_at": item.get("pull_request", {}).get("merged_at"),
|
|
605
|
+
"html_url": item["html_url"],
|
|
606
|
+
"labels": [label["name"] for label in item.get("labels", [])],
|
|
607
|
+
"repo": repo_name,
|
|
608
|
+
"owner": repo_owner,
|
|
609
|
+
"author": item.get("user", {}).get("login", "unknown"),
|
|
610
|
+
}
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
return prs
|
|
614
|
+
|
|
615
|
+
except httpx.HTTPStatusError as e:
|
|
616
|
+
logger.error(f"Failed to search PRs: HTTP {e.response.status_code}")
|
|
617
|
+
raise GithubException(e.response.status_code, e.response.json())
|
|
618
|
+
except Exception as e:
|
|
619
|
+
logger.error(f"Failed to search PRs: {e}")
|
|
620
|
+
raise
|
mcp_server/auth/__init__.py
CHANGED
|
@@ -3,22 +3,30 @@ QuickCall Authentication Module
|
|
|
3
3
|
|
|
4
4
|
Handles OAuth device flow authentication for CLI/MCP clients.
|
|
5
5
|
Stores credentials locally and fetches fresh tokens from quickcall.dev API.
|
|
6
|
+
|
|
7
|
+
Also supports GitHub PAT fallback for users without GitHub App access.
|
|
6
8
|
"""
|
|
7
9
|
|
|
8
10
|
from mcp_server.auth.credentials import (
|
|
9
11
|
CredentialStore,
|
|
12
|
+
GitHubPATCredentials,
|
|
10
13
|
get_credential_store,
|
|
11
14
|
is_authenticated,
|
|
12
15
|
get_credentials,
|
|
13
16
|
clear_credentials,
|
|
17
|
+
get_github_pat,
|
|
18
|
+
get_github_pat_username,
|
|
14
19
|
)
|
|
15
20
|
from mcp_server.auth.device_flow import DeviceFlowAuth
|
|
16
21
|
|
|
17
22
|
__all__ = [
|
|
18
23
|
"CredentialStore",
|
|
24
|
+
"GitHubPATCredentials",
|
|
19
25
|
"get_credential_store",
|
|
20
26
|
"is_authenticated",
|
|
21
27
|
"get_credentials",
|
|
22
28
|
"clear_credentials",
|
|
29
|
+
"get_github_pat",
|
|
30
|
+
"get_github_pat_username",
|
|
23
31
|
"DeviceFlowAuth",
|
|
24
32
|
]
|