quickcall-integrations 0.2.0__py3-none-any.whl → 0.3.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.
- mcp_server/api_clients/github_client.py +162 -31
- mcp_server/tools/github_tools.py +256 -43
- {quickcall_integrations-0.2.0.dist-info → quickcall_integrations-0.3.1.dist-info}/METADATA +1 -1
- {quickcall_integrations-0.2.0.dist-info → quickcall_integrations-0.3.1.dist-info}/RECORD +6 -6
- {quickcall_integrations-0.2.0.dist-info → quickcall_integrations-0.3.1.dist-info}/WHEEL +0 -0
- {quickcall_integrations-0.2.0.dist-info → quickcall_integrations-0.3.1.dist-info}/entry_points.txt +0 -0
|
@@ -6,6 +6,7 @@ Focuses on PRs and commits for minimal implementation.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import logging
|
|
9
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
9
10
|
from typing import List, Optional, Dict, Any
|
|
10
11
|
from datetime import datetime
|
|
11
12
|
|
|
@@ -22,7 +23,7 @@ logger = logging.getLogger(__name__)
|
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
class Commit(BaseModel):
|
|
25
|
-
"""Represents a GitHub commit."""
|
|
26
|
+
"""Represents a GitHub commit (full details)."""
|
|
26
27
|
|
|
27
28
|
sha: str
|
|
28
29
|
message: str
|
|
@@ -31,8 +32,18 @@ class Commit(BaseModel):
|
|
|
31
32
|
html_url: str
|
|
32
33
|
|
|
33
34
|
|
|
35
|
+
class CommitSummary(BaseModel):
|
|
36
|
+
"""Represents a GitHub commit (summary - minimal fields for list operations)."""
|
|
37
|
+
|
|
38
|
+
sha: str
|
|
39
|
+
message_title: str # First line only
|
|
40
|
+
author: str
|
|
41
|
+
date: datetime
|
|
42
|
+
html_url: str
|
|
43
|
+
|
|
44
|
+
|
|
34
45
|
class PullRequest(BaseModel):
|
|
35
|
-
"""Represents a GitHub pull request."""
|
|
46
|
+
"""Represents a GitHub pull request (full details)."""
|
|
36
47
|
|
|
37
48
|
number: int
|
|
38
49
|
title: str
|
|
@@ -55,6 +66,18 @@ class PullRequest(BaseModel):
|
|
|
55
66
|
reviewers: List[str] = []
|
|
56
67
|
|
|
57
68
|
|
|
69
|
+
class PullRequestSummary(BaseModel):
|
|
70
|
+
"""Represents a GitHub pull request (summary - minimal fields for list operations)."""
|
|
71
|
+
|
|
72
|
+
number: int
|
|
73
|
+
title: str
|
|
74
|
+
state: str
|
|
75
|
+
author: str
|
|
76
|
+
created_at: datetime
|
|
77
|
+
merged_at: Optional[datetime] = None
|
|
78
|
+
html_url: str
|
|
79
|
+
|
|
80
|
+
|
|
58
81
|
class Repository(BaseModel):
|
|
59
82
|
"""Represents a GitHub repository."""
|
|
60
83
|
|
|
@@ -298,7 +321,8 @@ class GitHubClient:
|
|
|
298
321
|
repo: Optional[str] = None,
|
|
299
322
|
state: str = "open",
|
|
300
323
|
limit: int = 20,
|
|
301
|
-
|
|
324
|
+
detail_level: str = "summary",
|
|
325
|
+
) -> List[PullRequest] | List[PullRequestSummary]:
|
|
302
326
|
"""
|
|
303
327
|
List pull requests.
|
|
304
328
|
|
|
@@ -307,9 +331,11 @@ class GitHubClient:
|
|
|
307
331
|
repo: Repository name
|
|
308
332
|
state: PR state: 'open', 'closed', or 'all'
|
|
309
333
|
limit: Maximum PRs to return
|
|
334
|
+
detail_level: 'summary' for minimal fields (~200 bytes/PR),
|
|
335
|
+
'full' for all fields (~2KB/PR)
|
|
310
336
|
|
|
311
337
|
Returns:
|
|
312
|
-
List of pull requests
|
|
338
|
+
List of pull requests (summary or full based on detail_level)
|
|
313
339
|
"""
|
|
314
340
|
gh_repo = self._get_repo(owner, repo)
|
|
315
341
|
prs = []
|
|
@@ -319,7 +345,10 @@ class GitHubClient:
|
|
|
319
345
|
for i, pr in enumerate(pulls):
|
|
320
346
|
if i >= limit:
|
|
321
347
|
break
|
|
322
|
-
|
|
348
|
+
if detail_level == "full":
|
|
349
|
+
prs.append(self._convert_pr(pr))
|
|
350
|
+
else:
|
|
351
|
+
prs.append(self._convert_pr_summary(pr))
|
|
323
352
|
except IndexError:
|
|
324
353
|
# Empty repo or no PRs - return empty list
|
|
325
354
|
pass
|
|
@@ -353,7 +382,7 @@ class GitHubClient:
|
|
|
353
382
|
raise
|
|
354
383
|
|
|
355
384
|
def _convert_pr(self, pr) -> PullRequest:
|
|
356
|
-
"""Convert PyGithub PullRequest to Pydantic model."""
|
|
385
|
+
"""Convert PyGithub PullRequest to Pydantic model (full details)."""
|
|
357
386
|
return PullRequest(
|
|
358
387
|
number=pr.number,
|
|
359
388
|
title=pr.title,
|
|
@@ -376,6 +405,18 @@ class GitHubClient:
|
|
|
376
405
|
reviewers=[r.login for r in pr.requested_reviewers],
|
|
377
406
|
)
|
|
378
407
|
|
|
408
|
+
def _convert_pr_summary(self, pr) -> PullRequestSummary:
|
|
409
|
+
"""Convert PyGithub PullRequest to summary model (minimal fields)."""
|
|
410
|
+
return PullRequestSummary(
|
|
411
|
+
number=pr.number,
|
|
412
|
+
title=pr.title,
|
|
413
|
+
state=pr.state,
|
|
414
|
+
author=pr.user.login if pr.user else "unknown",
|
|
415
|
+
created_at=pr.created_at,
|
|
416
|
+
merged_at=pr.merged_at,
|
|
417
|
+
html_url=pr.html_url,
|
|
418
|
+
)
|
|
419
|
+
|
|
379
420
|
# ========================================================================
|
|
380
421
|
# Commit Operations
|
|
381
422
|
# ========================================================================
|
|
@@ -388,7 +429,8 @@ class GitHubClient:
|
|
|
388
429
|
author: Optional[str] = None,
|
|
389
430
|
since: Optional[str] = None,
|
|
390
431
|
limit: int = 20,
|
|
391
|
-
|
|
432
|
+
detail_level: str = "summary",
|
|
433
|
+
) -> List[Commit] | List[CommitSummary]:
|
|
392
434
|
"""
|
|
393
435
|
List commits.
|
|
394
436
|
|
|
@@ -399,9 +441,10 @@ class GitHubClient:
|
|
|
399
441
|
author: Filter by author username
|
|
400
442
|
since: ISO datetime - only commits after this date
|
|
401
443
|
limit: Maximum commits to return
|
|
444
|
+
detail_level: 'summary' for minimal fields, 'full' for all fields
|
|
402
445
|
|
|
403
446
|
Returns:
|
|
404
|
-
List of commits
|
|
447
|
+
List of commits (summary or full based on detail_level)
|
|
405
448
|
"""
|
|
406
449
|
gh_repo = self._get_repo(owner, repo)
|
|
407
450
|
|
|
@@ -427,15 +470,28 @@ class GitHubClient:
|
|
|
427
470
|
if author and author.lower() != commit_author.lower():
|
|
428
471
|
continue
|
|
429
472
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
473
|
+
if detail_level == "full":
|
|
474
|
+
commits.append(
|
|
475
|
+
Commit(
|
|
476
|
+
sha=commit.sha,
|
|
477
|
+
message=commit.commit.message,
|
|
478
|
+
author=commit_author,
|
|
479
|
+
date=commit.commit.author.date,
|
|
480
|
+
html_url=commit.html_url,
|
|
481
|
+
)
|
|
482
|
+
)
|
|
483
|
+
else:
|
|
484
|
+
# Summary: just first line of message
|
|
485
|
+
message_title = commit.commit.message.split("\n")[0][:100]
|
|
486
|
+
commits.append(
|
|
487
|
+
CommitSummary(
|
|
488
|
+
sha=commit.sha[:7], # Short SHA for summary
|
|
489
|
+
message_title=message_title,
|
|
490
|
+
author=commit_author,
|
|
491
|
+
date=commit.commit.author.date,
|
|
492
|
+
html_url=commit.html_url,
|
|
493
|
+
)
|
|
437
494
|
)
|
|
438
|
-
)
|
|
439
495
|
|
|
440
496
|
return commits
|
|
441
497
|
|
|
@@ -533,6 +589,7 @@ class GitHubClient:
|
|
|
533
589
|
org: Optional[str] = None,
|
|
534
590
|
repo: Optional[str] = None,
|
|
535
591
|
limit: int = 100,
|
|
592
|
+
detail_level: str = "summary",
|
|
536
593
|
) -> List[Dict[str, Any]]:
|
|
537
594
|
"""
|
|
538
595
|
Search for merged pull requests using GitHub Search API.
|
|
@@ -545,10 +602,11 @@ class GitHubClient:
|
|
|
545
602
|
org: GitHub org to search within
|
|
546
603
|
repo: Specific repo in "owner/repo" format (overrides org if specified)
|
|
547
604
|
limit: Maximum PRs to return (max 100 per page)
|
|
605
|
+
detail_level: 'summary' for minimal fields, 'full' for all fields
|
|
548
606
|
|
|
549
607
|
Returns:
|
|
550
|
-
List of merged PR dicts
|
|
551
|
-
|
|
608
|
+
List of merged PR dicts. Summary includes: number, title, merged_at,
|
|
609
|
+
repo, owner, html_url, author. Full adds: body, labels.
|
|
552
610
|
"""
|
|
553
611
|
# Build search query
|
|
554
612
|
query_parts = ["is:pr", "is:merged"]
|
|
@@ -596,19 +654,35 @@ class GitHubClient:
|
|
|
596
654
|
repo_owner = repo_url_parts[-2] if len(repo_url_parts) >= 2 else ""
|
|
597
655
|
repo_name = repo_url_parts[-1] if len(repo_url_parts) >= 1 else ""
|
|
598
656
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
657
|
+
if detail_level == "full":
|
|
658
|
+
prs.append(
|
|
659
|
+
{
|
|
660
|
+
"number": item["number"],
|
|
661
|
+
"title": item["title"],
|
|
662
|
+
"body": item.get("body") or "",
|
|
663
|
+
"merged_at": item.get("pull_request", {}).get("merged_at"),
|
|
664
|
+
"html_url": item["html_url"],
|
|
665
|
+
"labels": [
|
|
666
|
+
label["name"] for label in item.get("labels", [])
|
|
667
|
+
],
|
|
668
|
+
"repo": repo_name,
|
|
669
|
+
"owner": repo_owner,
|
|
670
|
+
"author": item.get("user", {}).get("login", "unknown"),
|
|
671
|
+
}
|
|
672
|
+
)
|
|
673
|
+
else:
|
|
674
|
+
# Summary: skip body and labels
|
|
675
|
+
prs.append(
|
|
676
|
+
{
|
|
677
|
+
"number": item["number"],
|
|
678
|
+
"title": item["title"],
|
|
679
|
+
"merged_at": item.get("pull_request", {}).get("merged_at"),
|
|
680
|
+
"html_url": item["html_url"],
|
|
681
|
+
"repo": repo_name,
|
|
682
|
+
"owner": repo_owner,
|
|
683
|
+
"author": item.get("user", {}).get("login", "unknown"),
|
|
684
|
+
}
|
|
685
|
+
)
|
|
612
686
|
|
|
613
687
|
return prs
|
|
614
688
|
|
|
@@ -618,3 +692,60 @@ class GitHubClient:
|
|
|
618
692
|
except Exception as e:
|
|
619
693
|
logger.error(f"Failed to search PRs: {e}")
|
|
620
694
|
raise
|
|
695
|
+
|
|
696
|
+
def fetch_prs_parallel(
|
|
697
|
+
self,
|
|
698
|
+
pr_refs: List[Dict[str, Any]],
|
|
699
|
+
max_workers: int = 10,
|
|
700
|
+
) -> List[Dict[str, Any]]:
|
|
701
|
+
"""
|
|
702
|
+
Fetch full PR details for multiple PRs in parallel.
|
|
703
|
+
|
|
704
|
+
Args:
|
|
705
|
+
pr_refs: List of dicts with 'owner', 'repo', 'number' keys
|
|
706
|
+
max_workers: Max concurrent requests (default: 10)
|
|
707
|
+
|
|
708
|
+
Returns:
|
|
709
|
+
List of full PR details with stats (additions, deletions, files)
|
|
710
|
+
"""
|
|
711
|
+
results = []
|
|
712
|
+
errors = []
|
|
713
|
+
|
|
714
|
+
def fetch_single_pr(pr_ref: Dict[str, Any]) -> Dict[str, Any] | None:
|
|
715
|
+
try:
|
|
716
|
+
owner = pr_ref["owner"]
|
|
717
|
+
repo = pr_ref["repo"]
|
|
718
|
+
number = pr_ref["number"]
|
|
719
|
+
pr = self.get_pr(number, owner=owner, repo=repo)
|
|
720
|
+
if pr:
|
|
721
|
+
pr_dict = pr.model_dump()
|
|
722
|
+
# Add owner/repo for context
|
|
723
|
+
pr_dict["owner"] = owner
|
|
724
|
+
pr_dict["repo"] = repo
|
|
725
|
+
return pr_dict
|
|
726
|
+
return None
|
|
727
|
+
except Exception as e:
|
|
728
|
+
logger.warning(f"Failed to fetch PR {pr_ref}: {e}")
|
|
729
|
+
return None
|
|
730
|
+
|
|
731
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
732
|
+
futures = {
|
|
733
|
+
executor.submit(fetch_single_pr, pr_ref): pr_ref for pr_ref in pr_refs
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
for future in as_completed(futures):
|
|
737
|
+
pr_ref = futures[future]
|
|
738
|
+
try:
|
|
739
|
+
result = future.result()
|
|
740
|
+
if result:
|
|
741
|
+
results.append(result)
|
|
742
|
+
else:
|
|
743
|
+
errors.append(pr_ref)
|
|
744
|
+
except Exception as e:
|
|
745
|
+
logger.error(f"Error fetching {pr_ref}: {e}")
|
|
746
|
+
errors.append(pr_ref)
|
|
747
|
+
|
|
748
|
+
if errors:
|
|
749
|
+
logger.warning(f"Failed to fetch {len(errors)} PRs: {errors[:5]}...")
|
|
750
|
+
|
|
751
|
+
return results
|
mcp_server/tools/github_tools.py
CHANGED
|
@@ -11,7 +11,7 @@ PAT fallback is useful for:
|
|
|
11
11
|
- Testing and development
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
from typing import Optional, Tuple
|
|
14
|
+
from typing import List, Optional, Tuple
|
|
15
15
|
import logging
|
|
16
16
|
|
|
17
17
|
from fastmcp import FastMCP
|
|
@@ -154,19 +154,34 @@ def create_github_tools(mcp: FastMCP) -> None:
|
|
|
154
154
|
default=20,
|
|
155
155
|
description="Maximum number of PRs to return (default: 20)",
|
|
156
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
|
+
),
|
|
157
162
|
) -> dict:
|
|
158
163
|
"""
|
|
159
164
|
List pull requests for a GitHub repository.
|
|
160
165
|
|
|
161
166
|
Returns PRs sorted by last updated.
|
|
162
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.
|
|
163
171
|
"""
|
|
164
172
|
try:
|
|
165
173
|
client = _get_client()
|
|
166
|
-
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
|
+
)
|
|
167
181
|
|
|
168
182
|
return {
|
|
169
183
|
"count": len(prs),
|
|
184
|
+
"detail_level": detail_level,
|
|
170
185
|
"prs": [pr.model_dump() for pr in prs],
|
|
171
186
|
}
|
|
172
187
|
except ToolError:
|
|
@@ -180,40 +195,60 @@ def create_github_tools(mcp: FastMCP) -> None:
|
|
|
180
195
|
raise ToolError(f"Failed to list pull requests: {str(e)}")
|
|
181
196
|
|
|
182
197
|
@mcp.tool(tags={"github", "prs"})
|
|
183
|
-
def
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
),
|
|
189
|
-
repo: Optional[str] = Field(
|
|
190
|
-
default=None,
|
|
191
|
-
description="Repository name. Required.",
|
|
198
|
+
def get_prs(
|
|
199
|
+
pr_refs: List[dict] = Field(
|
|
200
|
+
...,
|
|
201
|
+
description="List of PR references. Each item should have 'owner', 'repo', and 'number' keys. "
|
|
202
|
+
"Example: [{'owner': 'org', 'repo': 'myrepo', 'number': 123}, ...]",
|
|
192
203
|
),
|
|
193
204
|
) -> dict:
|
|
194
205
|
"""
|
|
195
|
-
Get detailed information about
|
|
206
|
+
Get detailed information about one or more pull requests.
|
|
207
|
+
|
|
208
|
+
Works for single or multiple PRs - fetches in parallel when multiple.
|
|
209
|
+
Each PR ref needs owner, repo, and number.
|
|
196
210
|
|
|
197
|
-
|
|
211
|
+
Returns full PR details including additions, deletions, and files changed.
|
|
198
212
|
Requires QuickCall authentication with GitHub connected.
|
|
199
213
|
"""
|
|
200
214
|
try:
|
|
201
215
|
client = _get_client()
|
|
202
|
-
pr = client.get_pr(pr_number, owner=owner, repo=repo)
|
|
203
216
|
|
|
204
|
-
|
|
205
|
-
|
|
217
|
+
# Validate input
|
|
218
|
+
validated_refs = []
|
|
219
|
+
for ref in pr_refs:
|
|
220
|
+
if not isinstance(ref, dict):
|
|
221
|
+
raise ToolError(f"Invalid PR ref (must be dict): {ref}")
|
|
222
|
+
if "number" not in ref:
|
|
223
|
+
raise ToolError(f"Missing 'number' in PR ref: {ref}")
|
|
224
|
+
if "owner" not in ref or "repo" not in ref:
|
|
225
|
+
raise ToolError(
|
|
226
|
+
f"Missing 'owner' or 'repo' in PR ref: {ref}. "
|
|
227
|
+
"Each ref must have owner, repo, and number."
|
|
228
|
+
)
|
|
229
|
+
validated_refs.append(
|
|
230
|
+
{
|
|
231
|
+
"owner": ref["owner"],
|
|
232
|
+
"repo": ref["repo"],
|
|
233
|
+
"number": int(ref["number"]),
|
|
234
|
+
}
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
if not validated_refs:
|
|
238
|
+
return {"count": 0, "prs": []}
|
|
206
239
|
|
|
207
|
-
|
|
240
|
+
# Fetch all PRs in parallel
|
|
241
|
+
prs = client.fetch_prs_parallel(validated_refs, max_workers=10)
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
"count": len(prs),
|
|
245
|
+
"requested": len(validated_refs),
|
|
246
|
+
"prs": prs,
|
|
247
|
+
}
|
|
208
248
|
except ToolError:
|
|
209
249
|
raise
|
|
210
|
-
except ValueError as e:
|
|
211
|
-
raise ToolError(
|
|
212
|
-
f"Repository not specified: {str(e)}. "
|
|
213
|
-
f"Please provide both owner and repo parameters."
|
|
214
|
-
)
|
|
215
250
|
except Exception as e:
|
|
216
|
-
raise ToolError(f"Failed to
|
|
251
|
+
raise ToolError(f"Failed to fetch PRs: {str(e)}")
|
|
217
252
|
|
|
218
253
|
@mcp.tool(tags={"github", "commits"})
|
|
219
254
|
def list_commits(
|
|
@@ -241,12 +276,20 @@ def create_github_tools(mcp: FastMCP) -> None:
|
|
|
241
276
|
default=20,
|
|
242
277
|
description="Maximum number of commits to return (default: 20)",
|
|
243
278
|
),
|
|
279
|
+
detail_level: str = Field(
|
|
280
|
+
default="summary",
|
|
281
|
+
description="'summary' for minimal fields (short sha, message title, author, date, url), "
|
|
282
|
+
"'full' for all fields including full commit message. Use 'summary' for large result sets.",
|
|
283
|
+
),
|
|
244
284
|
) -> dict:
|
|
245
285
|
"""
|
|
246
286
|
List commits for a GitHub repository.
|
|
247
287
|
|
|
248
288
|
Returns commits sorted by date (newest first).
|
|
249
289
|
Requires QuickCall authentication with GitHub connected.
|
|
290
|
+
|
|
291
|
+
Use detail_level='summary' (default) to avoid context overflow with large result sets.
|
|
292
|
+
Use get_commit(sha) to get full details for specific commits when needed.
|
|
250
293
|
"""
|
|
251
294
|
try:
|
|
252
295
|
client = _get_client()
|
|
@@ -257,10 +300,12 @@ def create_github_tools(mcp: FastMCP) -> None:
|
|
|
257
300
|
author=author,
|
|
258
301
|
since=since,
|
|
259
302
|
limit=limit,
|
|
303
|
+
detail_level=detail_level,
|
|
260
304
|
)
|
|
261
305
|
|
|
262
306
|
return {
|
|
263
307
|
"count": len(commits),
|
|
308
|
+
"detail_level": detail_level,
|
|
264
309
|
"commits": [commit.model_dump() for commit in commits],
|
|
265
310
|
}
|
|
266
311
|
except ToolError:
|
|
@@ -348,7 +393,7 @@ def create_github_tools(mcp: FastMCP) -> None:
|
|
|
348
393
|
except Exception as e:
|
|
349
394
|
raise ToolError(f"Failed to list branches: {str(e)}")
|
|
350
395
|
|
|
351
|
-
@mcp.tool(tags={"github", "prs"
|
|
396
|
+
@mcp.tool(tags={"github", "prs"})
|
|
352
397
|
def search_merged_prs(
|
|
353
398
|
author: Optional[str] = Field(
|
|
354
399
|
default=None,
|
|
@@ -370,28 +415,20 @@ def create_github_tools(mcp: FastMCP) -> None:
|
|
|
370
415
|
default=100,
|
|
371
416
|
description="Maximum PRs to return (default: 100)",
|
|
372
417
|
),
|
|
418
|
+
detail_level: str = Field(
|
|
419
|
+
default="summary",
|
|
420
|
+
description="'summary' for minimal fields (number, title, merged_at, repo, owner, html_url, author), "
|
|
421
|
+
"'full' adds body and labels. Use 'summary' for large result sets.",
|
|
422
|
+
),
|
|
373
423
|
) -> dict:
|
|
374
424
|
"""
|
|
375
425
|
Search for merged pull requests by author within a time period.
|
|
376
426
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
details (additions, deletions, files) on specific PRs.
|
|
380
|
-
|
|
381
|
-
Claude should analyze the returned PRs to:
|
|
382
|
-
|
|
383
|
-
1. CATEGORIZE by type (look at PR title/labels):
|
|
384
|
-
- Features: "feat:", "add:", "implement", "new", "create"
|
|
385
|
-
- Enhancements: "improve:", "update:", "perf:", "optimize", "enhance"
|
|
386
|
-
- Bug fixes: "fix:", "bugfix:", "hotfix:", "resolve", "patch"
|
|
387
|
-
- Chores: "chore:", "docs:", "test:", "ci:", "refactor:", "bump"
|
|
388
|
-
|
|
389
|
-
2. IDENTIFY top PRs worth highlighting (call get_pr for detailed metrics)
|
|
390
|
-
|
|
391
|
-
3. SUMMARIZE for appraisal with accomplishments grouped by category
|
|
427
|
+
NOTE: For appraisals/performance reviews, use prepare_appraisal_data instead!
|
|
428
|
+
It fetches all PRs with full stats in parallel and avoids context overflow.
|
|
392
429
|
|
|
393
|
-
|
|
394
|
-
|
|
430
|
+
This tool returns basic PR info without stats (additions, deletions).
|
|
431
|
+
Use detail_level='summary' (default) for large result sets.
|
|
395
432
|
|
|
396
433
|
Requires QuickCall authentication with GitHub connected.
|
|
397
434
|
"""
|
|
@@ -399,9 +436,11 @@ def create_github_tools(mcp: FastMCP) -> None:
|
|
|
399
436
|
client = _get_client()
|
|
400
437
|
|
|
401
438
|
# Calculate since_date from days
|
|
402
|
-
from datetime import datetime, timedelta
|
|
439
|
+
from datetime import datetime, timedelta, timezone
|
|
403
440
|
|
|
404
|
-
since_date = (datetime.
|
|
441
|
+
since_date = (datetime.now(timezone.utc) - timedelta(days=days)).strftime(
|
|
442
|
+
"%Y-%m-%d"
|
|
443
|
+
)
|
|
405
444
|
|
|
406
445
|
# Use authenticated user if author not specified
|
|
407
446
|
if not author:
|
|
@@ -415,10 +454,12 @@ def create_github_tools(mcp: FastMCP) -> None:
|
|
|
415
454
|
org=org,
|
|
416
455
|
repo=repo,
|
|
417
456
|
limit=limit,
|
|
457
|
+
detail_level=detail_level,
|
|
418
458
|
)
|
|
419
459
|
|
|
420
460
|
return {
|
|
421
461
|
"count": len(prs),
|
|
462
|
+
"detail_level": detail_level,
|
|
422
463
|
"period": f"Last {days} days",
|
|
423
464
|
"author": author,
|
|
424
465
|
"org": org,
|
|
@@ -430,6 +471,178 @@ def create_github_tools(mcp: FastMCP) -> None:
|
|
|
430
471
|
except Exception as e:
|
|
431
472
|
raise ToolError(f"Failed to search merged PRs: {str(e)}")
|
|
432
473
|
|
|
474
|
+
@mcp.tool(tags={"github", "prs", "appraisal"})
|
|
475
|
+
def prepare_appraisal_data(
|
|
476
|
+
author: Optional[str] = Field(
|
|
477
|
+
default=None,
|
|
478
|
+
description="GitHub username. Defaults to authenticated user.",
|
|
479
|
+
),
|
|
480
|
+
days: int = Field(
|
|
481
|
+
default=180,
|
|
482
|
+
description="Number of days to look back (default: 180 for ~6 months)",
|
|
483
|
+
),
|
|
484
|
+
org: Optional[str] = Field(
|
|
485
|
+
default=None,
|
|
486
|
+
description="GitHub org to search within.",
|
|
487
|
+
),
|
|
488
|
+
repo: Optional[str] = Field(
|
|
489
|
+
default=None,
|
|
490
|
+
description="Specific repo in 'owner/repo' format.",
|
|
491
|
+
),
|
|
492
|
+
) -> dict:
|
|
493
|
+
"""
|
|
494
|
+
Prepare appraisal data by fetching ALL merged PRs with full details.
|
|
495
|
+
|
|
496
|
+
USE THIS TOOL FOR APPRAISALS AND PERFORMANCE REVIEWS!
|
|
497
|
+
|
|
498
|
+
This is the recommended tool for gathering contribution data because it:
|
|
499
|
+
1. Fetches ALL merged PRs with full stats (additions, deletions) in PARALLEL
|
|
500
|
+
2. Dumps everything to a local file (avoids context overflow)
|
|
501
|
+
3. Returns just PR titles for you to review
|
|
502
|
+
4. Then use get_appraisal_pr_details(file_path, pr_numbers) for selected PRs
|
|
503
|
+
|
|
504
|
+
DO NOT use search_merged_prs for appraisals - it doesn't include stats
|
|
505
|
+
and causes context overflow with large result sets.
|
|
506
|
+
"""
|
|
507
|
+
import json
|
|
508
|
+
import tempfile
|
|
509
|
+
from datetime import datetime, timedelta, timezone
|
|
510
|
+
|
|
511
|
+
try:
|
|
512
|
+
client = _get_client()
|
|
513
|
+
|
|
514
|
+
since_date = (datetime.now(timezone.utc) - timedelta(days=days)).strftime(
|
|
515
|
+
"%Y-%m-%d"
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
# Use authenticated user if author not specified
|
|
519
|
+
if not author:
|
|
520
|
+
creds = get_credential_store().get_api_credentials()
|
|
521
|
+
if creds and creds.github_username:
|
|
522
|
+
author = creds.github_username
|
|
523
|
+
|
|
524
|
+
# Step 1: Get list of merged PRs
|
|
525
|
+
pr_list = client.search_merged_prs(
|
|
526
|
+
author=author,
|
|
527
|
+
since_date=since_date,
|
|
528
|
+
org=org,
|
|
529
|
+
repo=repo,
|
|
530
|
+
limit=100,
|
|
531
|
+
detail_level="full", # Get body/labels from search
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
if not pr_list:
|
|
535
|
+
return {
|
|
536
|
+
"count": 0,
|
|
537
|
+
"message": "No merged PRs found for the specified criteria",
|
|
538
|
+
"author": author,
|
|
539
|
+
"period": f"Last {days} days",
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
# Step 2: Prepare refs for parallel fetch
|
|
543
|
+
pr_refs = [
|
|
544
|
+
{"owner": pr["owner"], "repo": pr["repo"], "number": pr["number"]}
|
|
545
|
+
for pr in pr_list
|
|
546
|
+
]
|
|
547
|
+
|
|
548
|
+
# Step 3: Fetch full details in parallel
|
|
549
|
+
full_prs = client.fetch_prs_parallel(pr_refs, max_workers=10)
|
|
550
|
+
|
|
551
|
+
# Step 4: Merge search data with full PR data
|
|
552
|
+
# (search has body/labels, full PR has additions/deletions/files)
|
|
553
|
+
pr_lookup = {(pr["owner"], pr["repo"], pr["number"]): pr for pr in pr_list}
|
|
554
|
+
for pr in full_prs:
|
|
555
|
+
key = (pr["owner"], pr["repo"], pr["number"])
|
|
556
|
+
if key in pr_lookup:
|
|
557
|
+
# Add labels from search (not in PyGithub response for some reason)
|
|
558
|
+
search_pr = pr_lookup[key]
|
|
559
|
+
if "labels" in search_pr:
|
|
560
|
+
pr["labels"] = search_pr["labels"]
|
|
561
|
+
|
|
562
|
+
# Step 5: Dump to file
|
|
563
|
+
dump_data = {
|
|
564
|
+
"author": author,
|
|
565
|
+
"period": f"Last {days} days",
|
|
566
|
+
"org": org,
|
|
567
|
+
"repo": repo,
|
|
568
|
+
"fetched_at": datetime.now(timezone.utc).isoformat(),
|
|
569
|
+
"count": len(full_prs),
|
|
570
|
+
"prs": full_prs,
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
# Create temp file that persists
|
|
574
|
+
fd, file_path = tempfile.mkstemp(suffix=".json", prefix="appraisal_")
|
|
575
|
+
with open(file_path, "w") as f:
|
|
576
|
+
json.dump(dump_data, f, indent=2, default=str)
|
|
577
|
+
|
|
578
|
+
# Step 6: Return file path + just titles for Claude to scan
|
|
579
|
+
pr_titles = [
|
|
580
|
+
{
|
|
581
|
+
"number": pr["number"],
|
|
582
|
+
"title": pr["title"],
|
|
583
|
+
"repo": f"{pr['owner']}/{pr['repo']}",
|
|
584
|
+
}
|
|
585
|
+
for pr in full_prs
|
|
586
|
+
]
|
|
587
|
+
|
|
588
|
+
return {
|
|
589
|
+
"file_path": file_path,
|
|
590
|
+
"count": len(full_prs),
|
|
591
|
+
"author": author,
|
|
592
|
+
"period": f"Last {days} days",
|
|
593
|
+
"pr_titles": pr_titles,
|
|
594
|
+
"next_step": "Review titles above, then call "
|
|
595
|
+
"get_appraisal_pr_details(file_path, pr_numbers) for full details on selected PRs.",
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
except ToolError:
|
|
599
|
+
raise
|
|
600
|
+
except Exception as e:
|
|
601
|
+
raise ToolError(f"Failed to prepare appraisal data: {str(e)}")
|
|
602
|
+
|
|
603
|
+
@mcp.tool(tags={"github", "prs", "appraisal"})
|
|
604
|
+
def get_appraisal_pr_details(
|
|
605
|
+
file_path: str = Field(
|
|
606
|
+
...,
|
|
607
|
+
description="Path to the appraisal data file from prepare_appraisal_data",
|
|
608
|
+
),
|
|
609
|
+
pr_numbers: List[int] = Field(
|
|
610
|
+
..., description="List of PR numbers to get full details for"
|
|
611
|
+
),
|
|
612
|
+
) -> dict:
|
|
613
|
+
"""
|
|
614
|
+
Get full PR details from the appraisal data dump.
|
|
615
|
+
|
|
616
|
+
This reads from the local file created by prepare_appraisal_data.
|
|
617
|
+
NO API CALLS are made - all data comes from the cached dump.
|
|
618
|
+
|
|
619
|
+
Use this after prepare_appraisal_data to get full details for specific PRs
|
|
620
|
+
that Claude has identified as important for the appraisal.
|
|
621
|
+
"""
|
|
622
|
+
import json
|
|
623
|
+
|
|
624
|
+
try:
|
|
625
|
+
with open(file_path) as f:
|
|
626
|
+
data = json.load(f)
|
|
627
|
+
|
|
628
|
+
pr_numbers_set = set(pr_numbers)
|
|
629
|
+
selected_prs = [
|
|
630
|
+
pr for pr in data.get("prs", []) if pr["number"] in pr_numbers_set
|
|
631
|
+
]
|
|
632
|
+
|
|
633
|
+
return {
|
|
634
|
+
"count": len(selected_prs),
|
|
635
|
+
"requested": len(pr_numbers),
|
|
636
|
+
"prs": selected_prs,
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
except FileNotFoundError:
|
|
640
|
+
raise ToolError(f"Appraisal data file not found: {file_path}")
|
|
641
|
+
except json.JSONDecodeError:
|
|
642
|
+
raise ToolError(f"Invalid JSON in appraisal data file: {file_path}")
|
|
643
|
+
except Exception as e:
|
|
644
|
+
raise ToolError(f"Failed to read appraisal data: {str(e)}")
|
|
645
|
+
|
|
433
646
|
@mcp.tool(tags={"github", "status"})
|
|
434
647
|
def check_github_connection() -> dict:
|
|
435
648
|
"""
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
mcp_server/__init__.py,sha256=UJBr5BLG_aU2S4s2fEbRBZYd7GUWDVejxBpqezNBo8Q,98
|
|
2
2
|
mcp_server/server.py,sha256=zGrrYwp7H24pJAAGAVkHDk7Y6IydOR_wo5hIL-e6_50,3001
|
|
3
3
|
mcp_server/api_clients/__init__.py,sha256=kOG5_sxIVpAx_tvf1nq_P0QCkqojAVidRE-wenLS-Wc,207
|
|
4
|
-
mcp_server/api_clients/github_client.py,sha256=
|
|
4
|
+
mcp_server/api_clients/github_client.py,sha256=shaBgAtTXUM-LZ7e5obuIRWylkOqBrim3_DapIN4rYI,25861
|
|
5
5
|
mcp_server/api_clients/slack_client.py,sha256=w3rcGghttfYw8Ird2beNo2LEYLc3rCTbUKMH4X7QQuQ,16447
|
|
6
6
|
mcp_server/auth/__init__.py,sha256=D-JS0Qe7FkeJjYx92u_AqPx8ZRoB3dKMowzzJXlX6cc,780
|
|
7
7
|
mcp_server/auth/credentials.py,sha256=1e1FpiaPPVc5hVdLlvIU2JbhbdHbXH9pzYBDPsUr0hI,20003
|
|
@@ -11,10 +11,10 @@ mcp_server/resources/slack_resources.py,sha256=b_CPxAicwkF3PsBXIat4QoLbDUHM2g_iP
|
|
|
11
11
|
mcp_server/tools/__init__.py,sha256=vIR2ujAaTXm2DgpTsVNz3brI4G34p-Jeg44Qe0uvWc0,405
|
|
12
12
|
mcp_server/tools/auth_tools.py,sha256=kCPjPC1jrVz0XaRAwPea-ue8ybjLLTxyILplBDJ9Mv4,24477
|
|
13
13
|
mcp_server/tools/git_tools.py,sha256=jyCTQR2eSzUFXMt0Y8x66758-VY8YCY14DDUJt7GY2U,13957
|
|
14
|
-
mcp_server/tools/github_tools.py,sha256=
|
|
14
|
+
mcp_server/tools/github_tools.py,sha256=Dcwhok30dpEePlm77--ynnQmkEmQkG_X625076n3F3A,25900
|
|
15
15
|
mcp_server/tools/slack_tools.py,sha256=-HVE_x3Z1KMeYGi1xhyppEwz5ZF-I-ZD0-Up8yBeoYE,11796
|
|
16
16
|
mcp_server/tools/utility_tools.py,sha256=1WiOpJivu6Ug9OLajm77lzsmFfBPgWHs8e1hNCEX_Aw,3359
|
|
17
|
-
quickcall_integrations-0.
|
|
18
|
-
quickcall_integrations-0.
|
|
19
|
-
quickcall_integrations-0.
|
|
20
|
-
quickcall_integrations-0.
|
|
17
|
+
quickcall_integrations-0.3.1.dist-info/METADATA,sha256=_HB-u8R1Wr9qk4sAB9YefLDElDUKYEvavYrNO1V4YY0,7043
|
|
18
|
+
quickcall_integrations-0.3.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
19
|
+
quickcall_integrations-0.3.1.dist-info/entry_points.txt,sha256=kkcunmJUzncYvQ1rOR35V2LPm2HcFTKzdI2l3n7NwiM,66
|
|
20
|
+
quickcall_integrations-0.3.1.dist-info/RECORD,,
|
|
File without changes
|
{quickcall_integrations-0.2.0.dist-info → quickcall_integrations-0.3.1.dist-info}/entry_points.txt
RENAMED
|
File without changes
|