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.
@@ -1,54 +1,110 @@
1
1
  """
2
2
  GitHub Tools - Pull requests and commits via GitHub API.
3
3
 
4
- These tools require authentication via QuickCall.
5
- Connect using connect_quickcall tool first.
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 get_credential_store, is_authenticated
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
- """Get the GitHub client, raising error if not configured."""
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
- if not store.is_authenticated():
26
- raise ToolError(
27
- "Not connected to QuickCall. "
28
- "Run connect_quickcall to authenticate and enable GitHub tools."
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
- # Fetch fresh credentials from API
32
- creds = store.get_api_credentials()
76
+ # No authentication available - provide helpful error message
77
+ _using_pat_mode = False
78
+ _pat_source = None
33
79
 
34
- if not creds or not creds.github_connected:
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 to enable GitHub tools."
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
- if not creds.github_token:
88
+ else:
89
+ # Not connected to QuickCall at all
41
90
  raise ToolError(
42
- "Could not fetch GitHub token. "
43
- "Try reconnecting GitHub at quickcall.dev/assistant."
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
- # Create client with fresh token and installation ID
47
- return GitHubClient(
48
- token=creds.github_token,
49
- default_owner=creds.github_username,
50
- installation_id=creds.github_installation_id,
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(owner=owner, repo=repo, state=state, limit=limit)
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", "status"})
296
- def check_github_connection() -> dict:
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
- Check if GitHub is connected and working.
405
+ Search for merged pull requests by author within a time period.
299
406
 
300
- Tests the GitHub connection by fetching your account info.
301
- Use this to verify your GitHub integration is working.
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
- store = get_credential_store()
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
- "connected": False,
308
- "error": "Not connected to QuickCall. Run connect_quickcall first.",
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
- creds = store.get_api_credentials()
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
- "connected": False,
316
- "error": "Could not fetch credentials from QuickCall.",
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
- if not creds.github_connected:
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
- "connected": False,
322
- "error": "GitHub not connected. Connect at quickcall.dev/assistant.",
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
- username = client.get_authenticated_user()
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": True,
331
- "username": username,
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 {