quickcall-integrations 0.1.8__py3-none-any.whl → 0.3.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/api_clients/github_client.py +366 -77
- 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 +416 -45
- {quickcall_integrations-0.1.8.dist-info → quickcall_integrations-0.3.0.dist-info}/METADATA +71 -61
- {quickcall_integrations-0.1.8.dist-info → quickcall_integrations-0.3.0.dist-info}/RECORD +10 -10
- {quickcall_integrations-0.1.8.dist-info → quickcall_integrations-0.3.0.dist-info}/WHEEL +0 -0
- {quickcall_integrations-0.1.8.dist-info → quickcall_integrations-0.3.0.dist-info}/entry_points.txt +0 -0
mcp_server/tools/github_tools.py
CHANGED
|
@@ -1,54 +1,110 @@
|
|
|
1
1
|
"""
|
|
2
2
|
GitHub Tools - Pull requests and commits via GitHub API.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
Authentication (in priority order):
|
|
5
|
+
1. QuickCall GitHub App (preferred) - connect via connect_quickcall
|
|
6
|
+
2. Personal Access Token (PAT) - set GITHUB_TOKEN env var or use .quickcall.env file
|
|
7
|
+
|
|
8
|
+
PAT fallback is useful for:
|
|
9
|
+
- Users at organizations that can't install the GitHub App
|
|
10
|
+
- Personal repositories without app installation
|
|
11
|
+
- Testing and development
|
|
6
12
|
"""
|
|
7
13
|
|
|
8
|
-
from typing import Optional
|
|
14
|
+
from typing import List, Optional, Tuple
|
|
9
15
|
import logging
|
|
10
16
|
|
|
11
17
|
from fastmcp import FastMCP
|
|
12
18
|
from fastmcp.exceptions import ToolError
|
|
13
19
|
from pydantic import Field
|
|
14
20
|
|
|
15
|
-
from mcp_server.auth import
|
|
21
|
+
from mcp_server.auth import (
|
|
22
|
+
get_credential_store,
|
|
23
|
+
get_github_pat,
|
|
24
|
+
get_github_pat_username,
|
|
25
|
+
)
|
|
16
26
|
from mcp_server.api_clients.github_client import GitHubClient
|
|
17
27
|
|
|
18
28
|
logger = logging.getLogger(__name__)
|
|
19
29
|
|
|
20
30
|
|
|
31
|
+
# Track whether we're using PAT mode for status reporting
|
|
32
|
+
_using_pat_mode: bool = False
|
|
33
|
+
_pat_source: Optional[str] = None
|
|
34
|
+
|
|
35
|
+
|
|
21
36
|
def _get_client() -> GitHubClient:
|
|
22
|
-
"""
|
|
37
|
+
"""
|
|
38
|
+
Get the GitHub client using the best available authentication method.
|
|
39
|
+
|
|
40
|
+
Authentication priority:
|
|
41
|
+
1. QuickCall GitHub App (if connected and working)
|
|
42
|
+
2. Personal Access Token from environment/config file
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
ToolError: If no authentication method is available
|
|
46
|
+
"""
|
|
47
|
+
global _using_pat_mode, _pat_source
|
|
48
|
+
|
|
23
49
|
store = get_credential_store()
|
|
24
50
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
51
|
+
# Try QuickCall GitHub App first (preferred)
|
|
52
|
+
if store.is_authenticated():
|
|
53
|
+
creds = store.get_api_credentials()
|
|
54
|
+
if creds and creds.github_connected and creds.github_token:
|
|
55
|
+
_using_pat_mode = False
|
|
56
|
+
_pat_source = None
|
|
57
|
+
return GitHubClient(
|
|
58
|
+
token=creds.github_token,
|
|
59
|
+
default_owner=creds.github_username,
|
|
60
|
+
installation_id=creds.github_installation_id,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Try PAT fallback
|
|
64
|
+
pat_token, pat_source = get_github_pat()
|
|
65
|
+
if pat_token:
|
|
66
|
+
_using_pat_mode = True
|
|
67
|
+
_pat_source = pat_source
|
|
68
|
+
pat_username = get_github_pat_username()
|
|
69
|
+
logger.info(f"Using GitHub PAT from {pat_source}")
|
|
70
|
+
return GitHubClient(
|
|
71
|
+
token=pat_token,
|
|
72
|
+
default_owner=pat_username,
|
|
73
|
+
installation_id=None, # No installation ID for PAT
|
|
29
74
|
)
|
|
30
75
|
|
|
31
|
-
#
|
|
32
|
-
|
|
76
|
+
# No authentication available - provide helpful error message
|
|
77
|
+
_using_pat_mode = False
|
|
78
|
+
_pat_source = None
|
|
33
79
|
|
|
34
|
-
if
|
|
80
|
+
if store.is_authenticated():
|
|
81
|
+
# Connected to QuickCall but GitHub not connected
|
|
35
82
|
raise ToolError(
|
|
36
|
-
"GitHub not connected. "
|
|
37
|
-
"Connect GitHub at quickcall.dev/assistant
|
|
83
|
+
"GitHub not connected. Options:\n"
|
|
84
|
+
"1. Connect GitHub App at quickcall.dev/assistant (recommended)\n"
|
|
85
|
+
"2. Run connect_github_via_pat with your Personal Access Token\n"
|
|
86
|
+
"3. Set GITHUB_TOKEN environment variable"
|
|
38
87
|
)
|
|
39
|
-
|
|
40
|
-
|
|
88
|
+
else:
|
|
89
|
+
# Not connected to QuickCall at all
|
|
41
90
|
raise ToolError(
|
|
42
|
-
"
|
|
43
|
-
"
|
|
91
|
+
"GitHub authentication required. Options:\n"
|
|
92
|
+
"1. Run connect_quickcall to use QuickCall (full access to GitHub + Slack)\n"
|
|
93
|
+
"2. Run connect_github_via_pat with a Personal Access Token (GitHub only)\n"
|
|
94
|
+
"3. Set GITHUB_TOKEN environment variable\n\n"
|
|
95
|
+
"For PAT: Create token at https://github.com/settings/tokens\n"
|
|
96
|
+
"Required scopes: repo (private) or public_repo (public only)"
|
|
44
97
|
)
|
|
45
98
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
99
|
+
|
|
100
|
+
def is_using_pat_mode() -> Tuple[bool, Optional[str]]:
|
|
101
|
+
"""
|
|
102
|
+
Check if GitHub tools are using PAT mode.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Tuple of (is_using_pat, source) where source is where the PAT was loaded from.
|
|
106
|
+
"""
|
|
107
|
+
return (_using_pat_mode, _pat_source)
|
|
52
108
|
|
|
53
109
|
|
|
54
110
|
def create_github_tools(mcp: FastMCP) -> None:
|
|
@@ -98,19 +154,34 @@ def create_github_tools(mcp: FastMCP) -> None:
|
|
|
98
154
|
default=20,
|
|
99
155
|
description="Maximum number of PRs to return (default: 20)",
|
|
100
156
|
),
|
|
157
|
+
detail_level: str = Field(
|
|
158
|
+
default="summary",
|
|
159
|
+
description="'summary' for minimal fields (~200 bytes/PR: number, title, state, author, merged_at, html_url), "
|
|
160
|
+
"'full' for all fields (~2KB/PR). Use 'summary' for large result sets, 'full' for detailed analysis.",
|
|
161
|
+
),
|
|
101
162
|
) -> dict:
|
|
102
163
|
"""
|
|
103
164
|
List pull requests for a GitHub repository.
|
|
104
165
|
|
|
105
166
|
Returns PRs sorted by last updated.
|
|
106
167
|
Requires QuickCall authentication with GitHub connected.
|
|
168
|
+
|
|
169
|
+
Use detail_level='summary' (default) to avoid context overflow with large result sets.
|
|
170
|
+
Use get_pr(number) to get full details for specific PRs when needed.
|
|
107
171
|
"""
|
|
108
172
|
try:
|
|
109
173
|
client = _get_client()
|
|
110
|
-
prs = client.list_prs(
|
|
174
|
+
prs = client.list_prs(
|
|
175
|
+
owner=owner,
|
|
176
|
+
repo=repo,
|
|
177
|
+
state=state,
|
|
178
|
+
limit=limit,
|
|
179
|
+
detail_level=detail_level,
|
|
180
|
+
)
|
|
111
181
|
|
|
112
182
|
return {
|
|
113
183
|
"count": len(prs),
|
|
184
|
+
"detail_level": detail_level,
|
|
114
185
|
"prs": [pr.model_dump() for pr in prs],
|
|
115
186
|
}
|
|
116
187
|
except ToolError:
|
|
@@ -185,12 +256,20 @@ def create_github_tools(mcp: FastMCP) -> None:
|
|
|
185
256
|
default=20,
|
|
186
257
|
description="Maximum number of commits to return (default: 20)",
|
|
187
258
|
),
|
|
259
|
+
detail_level: str = Field(
|
|
260
|
+
default="summary",
|
|
261
|
+
description="'summary' for minimal fields (short sha, message title, author, date, url), "
|
|
262
|
+
"'full' for all fields including full commit message. Use 'summary' for large result sets.",
|
|
263
|
+
),
|
|
188
264
|
) -> dict:
|
|
189
265
|
"""
|
|
190
266
|
List commits for a GitHub repository.
|
|
191
267
|
|
|
192
268
|
Returns commits sorted by date (newest first).
|
|
193
269
|
Requires QuickCall authentication with GitHub connected.
|
|
270
|
+
|
|
271
|
+
Use detail_level='summary' (default) to avoid context overflow with large result sets.
|
|
272
|
+
Use get_commit(sha) to get full details for specific commits when needed.
|
|
194
273
|
"""
|
|
195
274
|
try:
|
|
196
275
|
client = _get_client()
|
|
@@ -201,10 +280,12 @@ def create_github_tools(mcp: FastMCP) -> None:
|
|
|
201
280
|
author=author,
|
|
202
281
|
since=since,
|
|
203
282
|
limit=limit,
|
|
283
|
+
detail_level=detail_level,
|
|
204
284
|
)
|
|
205
285
|
|
|
206
286
|
return {
|
|
207
287
|
"count": len(commits),
|
|
288
|
+
"detail_level": detail_level,
|
|
208
289
|
"commits": [commit.model_dump() for commit in commits],
|
|
209
290
|
}
|
|
210
291
|
except ToolError:
|
|
@@ -292,44 +373,334 @@ def create_github_tools(mcp: FastMCP) -> None:
|
|
|
292
373
|
except Exception as e:
|
|
293
374
|
raise ToolError(f"Failed to list branches: {str(e)}")
|
|
294
375
|
|
|
295
|
-
@mcp.tool(tags={"github", "
|
|
296
|
-
def
|
|
376
|
+
@mcp.tool(tags={"github", "prs", "appraisal"})
|
|
377
|
+
def search_merged_prs(
|
|
378
|
+
author: Optional[str] = Field(
|
|
379
|
+
default=None,
|
|
380
|
+
description="GitHub username to filter by. Defaults to authenticated user if not specified.",
|
|
381
|
+
),
|
|
382
|
+
days: int = Field(
|
|
383
|
+
default=180,
|
|
384
|
+
description="Number of days to look back (default: 180 for ~6 months)",
|
|
385
|
+
),
|
|
386
|
+
org: Optional[str] = Field(
|
|
387
|
+
default=None,
|
|
388
|
+
description="GitHub org to search within. If not specified, searches all accessible repos.",
|
|
389
|
+
),
|
|
390
|
+
repo: Optional[str] = Field(
|
|
391
|
+
default=None,
|
|
392
|
+
description="Specific repo in 'owner/repo' format (e.g., 'revolving-org/supabase'). Overrides org if specified.",
|
|
393
|
+
),
|
|
394
|
+
limit: int = Field(
|
|
395
|
+
default=100,
|
|
396
|
+
description="Maximum PRs to return (default: 100)",
|
|
397
|
+
),
|
|
398
|
+
detail_level: str = Field(
|
|
399
|
+
default="summary",
|
|
400
|
+
description="'summary' for minimal fields (number, title, merged_at, repo, owner, html_url, author), "
|
|
401
|
+
"'full' adds body and labels. Use 'summary' for large result sets.",
|
|
402
|
+
),
|
|
403
|
+
) -> dict:
|
|
297
404
|
"""
|
|
298
|
-
|
|
405
|
+
Search for merged pull requests by author within a time period.
|
|
299
406
|
|
|
300
|
-
|
|
301
|
-
|
|
407
|
+
USE FOR APPRAISALS: This tool is ideal for gathering contribution data
|
|
408
|
+
for performance reviews. Returns basic PR info - use get_pr for full
|
|
409
|
+
details (additions, deletions, files) on specific PRs.
|
|
410
|
+
|
|
411
|
+
Claude should analyze the returned PRs to:
|
|
412
|
+
|
|
413
|
+
1. CATEGORIZE by type (look at PR title/labels):
|
|
414
|
+
- Features: "feat:", "add:", "implement", "new", "create"
|
|
415
|
+
- Enhancements: "improve:", "update:", "perf:", "optimize", "enhance"
|
|
416
|
+
- Bug fixes: "fix:", "bugfix:", "hotfix:", "resolve", "patch"
|
|
417
|
+
- Chores: "chore:", "docs:", "test:", "ci:", "refactor:", "bump"
|
|
418
|
+
|
|
419
|
+
2. IDENTIFY top PRs worth highlighting (call get_pr for detailed metrics)
|
|
420
|
+
|
|
421
|
+
3. SUMMARIZE for appraisal with accomplishments grouped by category
|
|
422
|
+
|
|
423
|
+
Use detail_level='summary' (default) to avoid context overflow with large result sets.
|
|
424
|
+
Use get_pr(number) to get full details for specific PRs when needed.
|
|
425
|
+
|
|
426
|
+
Requires QuickCall authentication with GitHub connected.
|
|
302
427
|
"""
|
|
303
|
-
|
|
428
|
+
try:
|
|
429
|
+
client = _get_client()
|
|
430
|
+
|
|
431
|
+
# Calculate since_date from days
|
|
432
|
+
from datetime import datetime, timedelta, timezone
|
|
433
|
+
|
|
434
|
+
since_date = (datetime.now(timezone.utc) - timedelta(days=days)).strftime(
|
|
435
|
+
"%Y-%m-%d"
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
# Use authenticated user if author not specified
|
|
439
|
+
if not author:
|
|
440
|
+
creds = get_credential_store().get_api_credentials()
|
|
441
|
+
if creds and creds.github_username:
|
|
442
|
+
author = creds.github_username
|
|
443
|
+
|
|
444
|
+
prs = client.search_merged_prs(
|
|
445
|
+
author=author,
|
|
446
|
+
since_date=since_date,
|
|
447
|
+
org=org,
|
|
448
|
+
repo=repo,
|
|
449
|
+
limit=limit,
|
|
450
|
+
detail_level=detail_level,
|
|
451
|
+
)
|
|
304
452
|
|
|
305
|
-
if not store.is_authenticated():
|
|
306
453
|
return {
|
|
307
|
-
"
|
|
308
|
-
"
|
|
454
|
+
"count": len(prs),
|
|
455
|
+
"detail_level": detail_level,
|
|
456
|
+
"period": f"Last {days} days",
|
|
457
|
+
"author": author,
|
|
458
|
+
"org": org,
|
|
459
|
+
"repo": repo,
|
|
460
|
+
"prs": prs,
|
|
309
461
|
}
|
|
462
|
+
except ToolError:
|
|
463
|
+
raise
|
|
464
|
+
except Exception as e:
|
|
465
|
+
raise ToolError(f"Failed to search merged PRs: {str(e)}")
|
|
310
466
|
|
|
311
|
-
|
|
467
|
+
@mcp.tool(tags={"github", "prs", "appraisal"})
|
|
468
|
+
def prepare_appraisal_data(
|
|
469
|
+
author: Optional[str] = Field(
|
|
470
|
+
default=None,
|
|
471
|
+
description="GitHub username. Defaults to authenticated user.",
|
|
472
|
+
),
|
|
473
|
+
days: int = Field(
|
|
474
|
+
default=180,
|
|
475
|
+
description="Number of days to look back (default: 180 for ~6 months)",
|
|
476
|
+
),
|
|
477
|
+
org: Optional[str] = Field(
|
|
478
|
+
default=None,
|
|
479
|
+
description="GitHub org to search within.",
|
|
480
|
+
),
|
|
481
|
+
repo: Optional[str] = Field(
|
|
482
|
+
default=None,
|
|
483
|
+
description="Specific repo in 'owner/repo' format.",
|
|
484
|
+
),
|
|
485
|
+
) -> dict:
|
|
486
|
+
"""
|
|
487
|
+
Prepare appraisal data by fetching ALL merged PRs with full details.
|
|
488
|
+
|
|
489
|
+
This tool:
|
|
490
|
+
1. Searches for all merged PRs by the author
|
|
491
|
+
2. Fetches FULL details (additions, deletions, files) for each PR IN PARALLEL
|
|
492
|
+
3. Dumps everything to a JSON file for later queries
|
|
493
|
+
4. Returns the file path + list of PR titles for Claude to review
|
|
494
|
+
|
|
495
|
+
USE THIS for appraisals instead of search_merged_prs + multiple get_pr calls.
|
|
496
|
+
After calling this, use get_appraisal_pr_details to get full info for specific PRs.
|
|
497
|
+
"""
|
|
498
|
+
import json
|
|
499
|
+
import tempfile
|
|
500
|
+
from datetime import datetime, timedelta, timezone
|
|
501
|
+
|
|
502
|
+
try:
|
|
503
|
+
client = _get_client()
|
|
504
|
+
|
|
505
|
+
since_date = (datetime.now(timezone.utc) - timedelta(days=days)).strftime(
|
|
506
|
+
"%Y-%m-%d"
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
# Use authenticated user if author not specified
|
|
510
|
+
if not author:
|
|
511
|
+
creds = get_credential_store().get_api_credentials()
|
|
512
|
+
if creds and creds.github_username:
|
|
513
|
+
author = creds.github_username
|
|
514
|
+
|
|
515
|
+
# Step 1: Get list of merged PRs
|
|
516
|
+
pr_list = client.search_merged_prs(
|
|
517
|
+
author=author,
|
|
518
|
+
since_date=since_date,
|
|
519
|
+
org=org,
|
|
520
|
+
repo=repo,
|
|
521
|
+
limit=100,
|
|
522
|
+
detail_level="full", # Get body/labels from search
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
if not pr_list:
|
|
526
|
+
return {
|
|
527
|
+
"count": 0,
|
|
528
|
+
"message": "No merged PRs found for the specified criteria",
|
|
529
|
+
"author": author,
|
|
530
|
+
"period": f"Last {days} days",
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
# Step 2: Prepare refs for parallel fetch
|
|
534
|
+
pr_refs = [
|
|
535
|
+
{"owner": pr["owner"], "repo": pr["repo"], "number": pr["number"]}
|
|
536
|
+
for pr in pr_list
|
|
537
|
+
]
|
|
538
|
+
|
|
539
|
+
# Step 3: Fetch full details in parallel
|
|
540
|
+
full_prs = client.fetch_prs_parallel(pr_refs, max_workers=10)
|
|
541
|
+
|
|
542
|
+
# Step 4: Merge search data with full PR data
|
|
543
|
+
# (search has body/labels, full PR has additions/deletions/files)
|
|
544
|
+
pr_lookup = {(pr["owner"], pr["repo"], pr["number"]): pr for pr in pr_list}
|
|
545
|
+
for pr in full_prs:
|
|
546
|
+
key = (pr["owner"], pr["repo"], pr["number"])
|
|
547
|
+
if key in pr_lookup:
|
|
548
|
+
# Add labels from search (not in PyGithub response for some reason)
|
|
549
|
+
search_pr = pr_lookup[key]
|
|
550
|
+
if "labels" in search_pr:
|
|
551
|
+
pr["labels"] = search_pr["labels"]
|
|
552
|
+
|
|
553
|
+
# Step 5: Dump to file
|
|
554
|
+
dump_data = {
|
|
555
|
+
"author": author,
|
|
556
|
+
"period": f"Last {days} days",
|
|
557
|
+
"org": org,
|
|
558
|
+
"repo": repo,
|
|
559
|
+
"fetched_at": datetime.now(timezone.utc).isoformat(),
|
|
560
|
+
"count": len(full_prs),
|
|
561
|
+
"prs": full_prs,
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
# Create temp file that persists
|
|
565
|
+
fd, file_path = tempfile.mkstemp(suffix=".json", prefix="appraisal_")
|
|
566
|
+
with open(file_path, "w") as f:
|
|
567
|
+
json.dump(dump_data, f, indent=2, default=str)
|
|
568
|
+
|
|
569
|
+
# Step 6: Return file path + just titles for Claude to scan
|
|
570
|
+
pr_titles = [
|
|
571
|
+
{
|
|
572
|
+
"number": pr["number"],
|
|
573
|
+
"title": pr["title"],
|
|
574
|
+
"repo": f"{pr['owner']}/{pr['repo']}",
|
|
575
|
+
}
|
|
576
|
+
for pr in full_prs
|
|
577
|
+
]
|
|
312
578
|
|
|
313
|
-
if not creds:
|
|
314
579
|
return {
|
|
315
|
-
"
|
|
316
|
-
"
|
|
580
|
+
"file_path": file_path,
|
|
581
|
+
"count": len(full_prs),
|
|
582
|
+
"author": author,
|
|
583
|
+
"period": f"Last {days} days",
|
|
584
|
+
"pr_titles": pr_titles,
|
|
585
|
+
"next_step": "Review titles above, then call "
|
|
586
|
+
"get_appraisal_pr_details(file_path, pr_numbers) for full details on selected PRs.",
|
|
317
587
|
}
|
|
318
588
|
|
|
319
|
-
|
|
589
|
+
except ToolError:
|
|
590
|
+
raise
|
|
591
|
+
except Exception as e:
|
|
592
|
+
raise ToolError(f"Failed to prepare appraisal data: {str(e)}")
|
|
593
|
+
|
|
594
|
+
@mcp.tool(tags={"github", "prs", "appraisal"})
|
|
595
|
+
def get_appraisal_pr_details(
|
|
596
|
+
file_path: str = Field(
|
|
597
|
+
...,
|
|
598
|
+
description="Path to the appraisal data file from prepare_appraisal_data",
|
|
599
|
+
),
|
|
600
|
+
pr_numbers: List[int] = Field(
|
|
601
|
+
..., description="List of PR numbers to get full details for"
|
|
602
|
+
),
|
|
603
|
+
) -> dict:
|
|
604
|
+
"""
|
|
605
|
+
Get full PR details from the appraisal data dump.
|
|
606
|
+
|
|
607
|
+
This reads from the local file created by prepare_appraisal_data.
|
|
608
|
+
NO API CALLS are made - all data comes from the cached dump.
|
|
609
|
+
|
|
610
|
+
Use this after prepare_appraisal_data to get full details for specific PRs
|
|
611
|
+
that Claude has identified as important for the appraisal.
|
|
612
|
+
"""
|
|
613
|
+
import json
|
|
614
|
+
|
|
615
|
+
try:
|
|
616
|
+
with open(file_path) as f:
|
|
617
|
+
data = json.load(f)
|
|
618
|
+
|
|
619
|
+
pr_numbers_set = set(pr_numbers)
|
|
620
|
+
selected_prs = [
|
|
621
|
+
pr for pr in data.get("prs", []) if pr["number"] in pr_numbers_set
|
|
622
|
+
]
|
|
623
|
+
|
|
320
624
|
return {
|
|
321
|
-
"
|
|
322
|
-
"
|
|
625
|
+
"count": len(selected_prs),
|
|
626
|
+
"requested": len(pr_numbers),
|
|
627
|
+
"prs": selected_prs,
|
|
323
628
|
}
|
|
324
629
|
|
|
630
|
+
except FileNotFoundError:
|
|
631
|
+
raise ToolError(f"Appraisal data file not found: {file_path}")
|
|
632
|
+
except json.JSONDecodeError:
|
|
633
|
+
raise ToolError(f"Invalid JSON in appraisal data file: {file_path}")
|
|
634
|
+
except Exception as e:
|
|
635
|
+
raise ToolError(f"Failed to read appraisal data: {str(e)}")
|
|
636
|
+
|
|
637
|
+
@mcp.tool(tags={"github", "status"})
|
|
638
|
+
def check_github_connection() -> dict:
|
|
639
|
+
"""
|
|
640
|
+
Check if GitHub is connected and working.
|
|
641
|
+
|
|
642
|
+
Tests the GitHub connection by fetching your account info.
|
|
643
|
+
Shows whether using QuickCall GitHub App or PAT fallback.
|
|
644
|
+
Use this to verify your GitHub integration is working.
|
|
645
|
+
"""
|
|
646
|
+
store = get_credential_store()
|
|
647
|
+
|
|
648
|
+
# First, try to get a working client (this handles both QuickCall and PAT)
|
|
325
649
|
try:
|
|
326
650
|
client = _get_client()
|
|
327
|
-
|
|
651
|
+
using_pat, pat_source = is_using_pat_mode()
|
|
652
|
+
|
|
653
|
+
# Try to get username to verify connection works
|
|
654
|
+
try:
|
|
655
|
+
username = client.get_authenticated_user()
|
|
656
|
+
except Exception:
|
|
657
|
+
username = None
|
|
658
|
+
|
|
659
|
+
if using_pat:
|
|
660
|
+
return {
|
|
661
|
+
"connected": True,
|
|
662
|
+
"mode": "pat",
|
|
663
|
+
"pat_source": pat_source,
|
|
664
|
+
"username": username or get_github_pat_username(),
|
|
665
|
+
"note": "Using Personal Access Token (PAT) mode. "
|
|
666
|
+
"Some features like list_repos may have limited access.",
|
|
667
|
+
}
|
|
668
|
+
else:
|
|
669
|
+
creds = store.get_api_credentials()
|
|
670
|
+
return {
|
|
671
|
+
"connected": True,
|
|
672
|
+
"mode": "github_app",
|
|
673
|
+
"username": username or (creds.github_username if creds else None),
|
|
674
|
+
"installation_id": creds.github_installation_id if creds else None,
|
|
675
|
+
}
|
|
676
|
+
except ToolError as e:
|
|
677
|
+
# No authentication available
|
|
678
|
+
pat_token, _ = get_github_pat()
|
|
679
|
+
if pat_token:
|
|
680
|
+
# PAT exists but failed to work
|
|
681
|
+
return {
|
|
682
|
+
"connected": False,
|
|
683
|
+
"error": "PAT authentication failed. Token may be invalid or expired.",
|
|
684
|
+
"suggestion": "Check your GITHUB_TOKEN or .quickcall.env file.",
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
# Check QuickCall status for helpful error
|
|
688
|
+
if store.is_authenticated():
|
|
689
|
+
creds = store.get_api_credentials()
|
|
690
|
+
if creds and not creds.github_connected:
|
|
691
|
+
return {
|
|
692
|
+
"connected": False,
|
|
693
|
+
"error": "GitHub not connected via QuickCall.",
|
|
694
|
+
"suggestions": [
|
|
695
|
+
"Connect GitHub App at quickcall.dev/assistant",
|
|
696
|
+
"Or set GITHUB_TOKEN environment variable",
|
|
697
|
+
"Or create .quickcall.env with GITHUB_TOKEN=ghp_xxx",
|
|
698
|
+
],
|
|
699
|
+
}
|
|
328
700
|
|
|
329
701
|
return {
|
|
330
|
-
"connected":
|
|
331
|
-
"
|
|
332
|
-
"installation_id": creds.github_installation_id,
|
|
702
|
+
"connected": False,
|
|
703
|
+
"error": str(e),
|
|
333
704
|
}
|
|
334
705
|
except Exception as e:
|
|
335
706
|
return {
|