quickcall-integrations 0.5.0__tar.gz → 0.6.0__tar.gz

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.
Files changed (52) hide show
  1. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/PKG-INFO +19 -1
  2. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/README.md +18 -0
  3. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/mcp_server/api_clients/github_client.py +139 -0
  4. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/mcp_server/tools/github_tools.py +86 -2
  5. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/plugins/quickcall/.claude-plugin/plugin.json +1 -1
  6. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/pyproject.toml +1 -1
  7. quickcall_integrations-0.6.0/tests/test_comment_management.py +271 -0
  8. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/.claude-plugin/marketplace.json +0 -0
  9. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  10. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  11. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/.github/ISSUE_TEMPLATE/task.yml +0 -0
  12. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/.github/workflows/publish-pypi.yml +0 -0
  13. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/.gitignore +0 -0
  14. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/.pre-commit-config.yaml +0 -0
  15. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/.quickcall-issue-template.yaml +0 -0
  16. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/Dockerfile +0 -0
  17. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/assets/logo.png +0 -0
  18. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/mcp_server/__init__.py +0 -0
  19. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/mcp_server/api_clients/__init__.py +0 -0
  20. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/mcp_server/api_clients/slack_client.py +0 -0
  21. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/mcp_server/auth/__init__.py +0 -0
  22. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/mcp_server/auth/credentials.py +0 -0
  23. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/mcp_server/auth/device_flow.py +0 -0
  24. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/mcp_server/resources/__init__.py +0 -0
  25. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/mcp_server/resources/github_resources.py +0 -0
  26. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/mcp_server/resources/slack_resources.py +0 -0
  27. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/mcp_server/server.py +0 -0
  28. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/mcp_server/tools/__init__.py +0 -0
  29. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/mcp_server/tools/auth_tools.py +0 -0
  30. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/mcp_server/tools/git_tools.py +0 -0
  31. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/mcp_server/tools/slack_tools.py +0 -0
  32. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/mcp_server/tools/utility_tools.py +0 -0
  33. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/plugins/quickcall/commands/appraisal.md +0 -0
  34. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/plugins/quickcall/commands/connect-github-pat.md +0 -0
  35. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/plugins/quickcall/commands/connect.md +0 -0
  36. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/plugins/quickcall/commands/pr-summary.md +0 -0
  37. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/plugins/quickcall/commands/projects.md +0 -0
  38. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/plugins/quickcall/commands/slack-summary.md +0 -0
  39. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/plugins/quickcall/commands/status.md +0 -0
  40. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/plugins/quickcall/commands/updates.md +0 -0
  41. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/requirements.txt +0 -0
  42. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/tests/README.md +0 -0
  43. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/tests/appraisal/__init__.py +0 -0
  44. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/tests/appraisal/setup_test_data.py +0 -0
  45. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/tests/test_appraisal_integration.py +0 -0
  46. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/tests/test_appraisal_tools.py +0 -0
  47. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/tests/test_integrations.py +0 -0
  48. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/tests/test_pr_integration.py +0 -0
  49. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/tests/test_project_integration.py +0 -0
  50. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/tests/test_project_tools.py +0 -0
  51. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/tests/test_tools.py +0 -0
  52. {quickcall_integrations-0.5.0 → quickcall_integrations-0.6.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quickcall-integrations
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: MCP server with developer integrations for Claude Code and Cursor
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: fastmcp>=2.13.0
@@ -252,10 +252,28 @@ The `manage_issues` tool provides full issue lifecycle management:
252
252
  | `close` | Close issue(s) |
253
253
  | `reopen` | Reopen issue(s) |
254
254
  | `comment` | Add comment to issue(s) |
255
+ | `list_comments` | List comments with limit and order |
256
+ | `update_comment` | Edit existing comment by ID |
257
+ | `delete_comment` | Delete comment by ID |
255
258
  | `add_sub_issue` | Add child issue to parent |
256
259
  | `remove_sub_issue` | Remove child from parent |
257
260
  | `list_sub_issues` | List sub-issues of a parent |
258
261
 
262
+ ### Comment Management
263
+
264
+ | Parameter | Description |
265
+ |-----------|-------------|
266
+ | `comment_id` | Comment ID for update/delete operations |
267
+ | `comments_limit` | Max comments to return (default: 10) |
268
+ | `comments_order` | `'asc'` (oldest first) or `'desc'` (newest first) |
269
+
270
+ **Examples:**
271
+ ```
272
+ List last 5 comments on issue #42 (newest first)
273
+ Update comment 123456 on issue #42 with new text
274
+ Delete comment 123456 from issue #42
275
+ ```
276
+
259
277
  ### List Filters
260
278
 
261
279
  | Filter | Description |
@@ -239,10 +239,28 @@ The `manage_issues` tool provides full issue lifecycle management:
239
239
  | `close` | Close issue(s) |
240
240
  | `reopen` | Reopen issue(s) |
241
241
  | `comment` | Add comment to issue(s) |
242
+ | `list_comments` | List comments with limit and order |
243
+ | `update_comment` | Edit existing comment by ID |
244
+ | `delete_comment` | Delete comment by ID |
242
245
  | `add_sub_issue` | Add child issue to parent |
243
246
  | `remove_sub_issue` | Remove child from parent |
244
247
  | `list_sub_issues` | List sub-issues of a parent |
245
248
 
249
+ ### Comment Management
250
+
251
+ | Parameter | Description |
252
+ |-----------|-------------|
253
+ | `comment_id` | Comment ID for update/delete operations |
254
+ | `comments_limit` | Max comments to return (default: 10) |
255
+ | `comments_order` | `'asc'` (oldest first) or `'desc'` (newest first) |
256
+
257
+ **Examples:**
258
+ ```
259
+ List last 5 comments on issue #42 (newest first)
260
+ Update comment 123456 on issue #42 with new text
261
+ Delete comment 123456 from issue #42
262
+ ```
263
+
246
264
  ### List Filters
247
265
 
248
266
  | Filter | Description |
@@ -1301,6 +1301,145 @@ class GitHubClient:
1301
1301
  "issue_number": issue_number,
1302
1302
  }
1303
1303
 
1304
+ def list_issue_comments(
1305
+ self,
1306
+ issue_number: int,
1307
+ owner: Optional[str] = None,
1308
+ repo: Optional[str] = None,
1309
+ limit: int = 10,
1310
+ order: str = "asc",
1311
+ ) -> List[Dict[str, Any]]:
1312
+ """
1313
+ List comments on a GitHub issue.
1314
+
1315
+ Args:
1316
+ issue_number: Issue number
1317
+ owner: Repository owner
1318
+ repo: Repository name
1319
+ limit: Maximum comments to return (default: 10)
1320
+ order: 'asc' for oldest first, 'desc' for newest first (default: 'asc')
1321
+
1322
+ Returns:
1323
+ List of comment dicts with id, body, author, timestamps, url
1324
+ """
1325
+ gh_repo = self._get_repo(owner, repo)
1326
+ issue = gh_repo.get_issue(issue_number)
1327
+
1328
+ comments = []
1329
+ all_comments = list(issue.get_comments())
1330
+
1331
+ # Apply order
1332
+ if order == "desc":
1333
+ all_comments = all_comments[::-1]
1334
+
1335
+ # Apply limit
1336
+ for comment in all_comments[:limit]:
1337
+ comments.append(
1338
+ {
1339
+ "id": comment.id,
1340
+ "body": comment.body,
1341
+ "author": comment.user.login if comment.user else "unknown",
1342
+ "created_at": comment.created_at.isoformat(),
1343
+ "updated_at": comment.updated_at.isoformat()
1344
+ if comment.updated_at
1345
+ else None,
1346
+ "html_url": comment.html_url,
1347
+ }
1348
+ )
1349
+
1350
+ return comments
1351
+
1352
+ def get_issue_comment(
1353
+ self,
1354
+ comment_id: int,
1355
+ owner: Optional[str] = None,
1356
+ repo: Optional[str] = None,
1357
+ ) -> Dict[str, Any]:
1358
+ """
1359
+ Get a specific comment by ID.
1360
+
1361
+ Args:
1362
+ comment_id: Comment ID
1363
+ owner: Repository owner
1364
+ repo: Repository name
1365
+
1366
+ Returns:
1367
+ Comment dict with id, body, author, timestamps, url
1368
+ """
1369
+ gh_repo = self._get_repo(owner, repo)
1370
+ comment = gh_repo.get_issue_comment(comment_id)
1371
+
1372
+ return {
1373
+ "id": comment.id,
1374
+ "body": comment.body,
1375
+ "author": comment.user.login if comment.user else "unknown",
1376
+ "created_at": comment.created_at.isoformat(),
1377
+ "updated_at": comment.updated_at.isoformat()
1378
+ if comment.updated_at
1379
+ else None,
1380
+ "html_url": comment.html_url,
1381
+ }
1382
+
1383
+ def update_issue_comment(
1384
+ self,
1385
+ comment_id: int,
1386
+ body: str,
1387
+ owner: Optional[str] = None,
1388
+ repo: Optional[str] = None,
1389
+ ) -> Dict[str, Any]:
1390
+ """
1391
+ Update an existing comment.
1392
+
1393
+ Args:
1394
+ comment_id: Comment ID
1395
+ body: New comment body
1396
+ owner: Repository owner
1397
+ repo: Repository name
1398
+
1399
+ Returns:
1400
+ Updated comment dict
1401
+ """
1402
+ gh_repo = self._get_repo(owner, repo)
1403
+ comment = gh_repo.get_issue_comment(comment_id)
1404
+ comment.edit(body)
1405
+
1406
+ return {
1407
+ "id": comment.id,
1408
+ "body": comment.body,
1409
+ "author": comment.user.login if comment.user else "unknown",
1410
+ "created_at": comment.created_at.isoformat(),
1411
+ "updated_at": comment.updated_at.isoformat()
1412
+ if comment.updated_at
1413
+ else None,
1414
+ "html_url": comment.html_url,
1415
+ }
1416
+
1417
+ def delete_issue_comment(
1418
+ self,
1419
+ comment_id: int,
1420
+ owner: Optional[str] = None,
1421
+ repo: Optional[str] = None,
1422
+ ) -> Dict[str, Any]:
1423
+ """
1424
+ Delete a comment.
1425
+
1426
+ Args:
1427
+ comment_id: Comment ID
1428
+ owner: Repository owner
1429
+ repo: Repository name
1430
+
1431
+ Returns:
1432
+ Dict with deleted comment_id
1433
+ """
1434
+ gh_repo = self._get_repo(owner, repo)
1435
+ comment = gh_repo.get_issue_comment(comment_id)
1436
+ comment.delete()
1437
+
1438
+ return {
1439
+ "deleted": True,
1440
+ "comment_id": comment_id,
1441
+ }
1442
+
1304
1443
  def get_issue(
1305
1444
  self,
1306
1445
  issue_number: int,
@@ -571,11 +571,12 @@ def create_github_tools(mcp: FastMCP) -> None:
571
571
  action: str = Field(
572
572
  ...,
573
573
  description="Action: 'list', 'view', 'create', 'update', 'close', 'reopen', 'comment', "
574
+ "'list_comments', 'update_comment', 'delete_comment', "
574
575
  "'add_sub_issue', 'remove_sub_issue', 'list_sub_issues'",
575
576
  ),
576
577
  issue_numbers: Optional[List[int]] = Field(
577
578
  default=None,
578
- description="Issue number(s). Required for view/update/close/reopen/comment/sub-issue/project ops.",
579
+ description="Issue number(s). Required for view/update/close/reopen/comment/sub-issue/comment ops.",
579
580
  ),
580
581
  title: Optional[str] = Field(
581
582
  default=None,
@@ -583,7 +584,7 @@ def create_github_tools(mcp: FastMCP) -> None:
583
584
  ),
584
585
  body: Optional[str] = Field(
585
586
  default=None,
586
- description="Issue body (for 'create'/'update') or comment text (for 'comment')",
587
+ description="Issue body (for 'create'/'update') or comment text (for 'comment'/'update_comment')",
587
588
  ),
588
589
  labels: Optional[List[str]] = Field(
589
590
  default=None,
@@ -602,6 +603,18 @@ def create_github_tools(mcp: FastMCP) -> None:
602
603
  description="Parent issue number. For 'create': attach new issue as sub-issue. "
603
604
  "For 'add_sub_issue'/'remove_sub_issue'/'list_sub_issues': the parent issue.",
604
605
  ),
606
+ comment_id: Optional[int] = Field(
607
+ default=None,
608
+ description="Comment ID for 'update_comment' or 'delete_comment' actions.",
609
+ ),
610
+ comments_limit: Optional[int] = Field(
611
+ default=10,
612
+ description="Max comments to return for 'list_comments' (default: 10).",
613
+ ),
614
+ comments_order: Optional[str] = Field(
615
+ default="asc",
616
+ description="Comment order for 'list_comments': 'asc' (oldest first) or 'desc' (newest first).",
617
+ ),
605
618
  owner: Optional[str] = Field(
606
619
  default=None,
607
620
  description="Repository owner",
@@ -644,6 +657,10 @@ def create_github_tools(mcp: FastMCP) -> None:
644
657
  - create as sub-issue: manage_issues(action="create", title="Task 1", parent_issue=42)
645
658
  - close multiple: manage_issues(action="close", issue_numbers=[1, 2, 3])
646
659
  - comment: manage_issues(action="comment", issue_numbers=[42], body="Fixed!")
660
+ - list comments: manage_issues(action="list_comments", issue_numbers=[42], comments_limit=5)
661
+ - list newest comments: manage_issues(action="list_comments", issue_numbers=[42], comments_order="desc")
662
+ - update comment: manage_issues(action="update_comment", issue_numbers=[42], comment_id=123, body="Updated")
663
+ - delete comment: manage_issues(action="delete_comment", issue_numbers=[42], comment_id=123)
647
664
  - add sub-issues: manage_issues(action="add_sub_issue", issue_numbers=[43,44], parent_issue=42)
648
665
  - remove sub-issue: manage_issues(action="remove_sub_issue", issue_numbers=[43], parent_issue=42)
649
666
  - list sub-issues: manage_issues(action="list_sub_issues", parent_issue=42)
@@ -830,6 +847,66 @@ def create_github_tools(mcp: FastMCP) -> None:
830
847
  }
831
848
  )
832
849
 
850
+ # === LIST COMMENTS ACTION ===
851
+ elif action == "list_comments":
852
+ comments = client.list_issue_comments(
853
+ issue_number=issue_number,
854
+ owner=owner,
855
+ repo=repo,
856
+ limit=comments_limit or 10,
857
+ order=comments_order or "asc",
858
+ )
859
+ results.append(
860
+ {
861
+ "number": issue_number,
862
+ "comments_count": len(comments),
863
+ "comments": comments,
864
+ }
865
+ )
866
+
867
+ # === UPDATE COMMENT ACTION ===
868
+ elif action == "update_comment":
869
+ if not comment_id:
870
+ raise ToolError(
871
+ "'comment_id' is required for 'update_comment' action"
872
+ )
873
+ if not body:
874
+ raise ToolError(
875
+ "'body' is required for 'update_comment' action"
876
+ )
877
+ updated = client.update_issue_comment(
878
+ comment_id=comment_id,
879
+ body=body,
880
+ owner=owner,
881
+ repo=repo,
882
+ )
883
+ results.append(
884
+ {
885
+ "number": issue_number,
886
+ "status": "comment_updated",
887
+ "comment": updated,
888
+ }
889
+ )
890
+
891
+ # === DELETE COMMENT ACTION ===
892
+ elif action == "delete_comment":
893
+ if not comment_id:
894
+ raise ToolError(
895
+ "'comment_id' is required for 'delete_comment' action"
896
+ )
897
+ client.delete_issue_comment(
898
+ comment_id=comment_id,
899
+ owner=owner,
900
+ repo=repo,
901
+ )
902
+ results.append(
903
+ {
904
+ "number": issue_number,
905
+ "status": "comment_deleted",
906
+ "comment_id": comment_id,
907
+ }
908
+ )
909
+
833
910
  else:
834
911
  raise ToolError(f"Invalid action: {action}")
835
912
 
@@ -841,6 +918,13 @@ def create_github_tools(mcp: FastMCP) -> None:
841
918
  "issues": results,
842
919
  }
843
920
 
921
+ if action == "list_comments":
922
+ return {
923
+ "action": "list_comments",
924
+ "count": len(results),
925
+ "issues": results,
926
+ }
927
+
844
928
  return {"action": action, "count": len(results), "results": results}
845
929
 
846
930
  except ToolError:
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "quickcall",
3
3
  "description": "Integrate quickcall into dev workflows - eliminate interruptions for developers. Ask about your work, get instant answers. No more context switching.",
4
- "version": "0.8.0",
4
+ "version": "0.9.0",
5
5
  "author": {
6
6
  "name": "Sagar Sarkale"
7
7
  }
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "quickcall-integrations"
3
- version = "0.5.0"
3
+ version = "0.6.0"
4
4
  description = "MCP server with developer integrations for Claude Code and Cursor"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -0,0 +1,271 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Integration test for Issue Comment Management.
4
+
5
+ This test performs a full round-trip:
6
+ 1. Creates a test issue
7
+ 2. Adds multiple comments
8
+ 3. Lists comments (asc and desc order)
9
+ 4. Updates a comment
10
+ 5. Deletes a comment
11
+ 6. Closes/cleans up the test issue
12
+
13
+ Run with: uv run python tests/test_comment_management.py
14
+
15
+ Requires:
16
+ - GITHUB_TOKEN env var or .quickcall.env with GitHub PAT
17
+ - Access to quickcall-dev/quickcall-integrations repo
18
+ """
19
+
20
+ import sys
21
+ import time
22
+
23
+
24
+ def run_integration_test():
25
+ """Run the full comment management integration test."""
26
+ from mcp_server.api_clients.github_client import GitHubClient
27
+ from mcp_server.auth import get_github_pat
28
+
29
+ print("=" * 60)
30
+ print("Issue Comment Management Integration Test")
31
+ print("=" * 60)
32
+
33
+ # Check for credentials
34
+ pat_token, source = get_github_pat()
35
+ if not pat_token:
36
+ print("\n❌ No GitHub PAT found!")
37
+ print(" Set GITHUB_TOKEN env var or create .quickcall.env")
38
+ return False
39
+
40
+ print(f"\n✅ Using PAT from: {source}")
41
+
42
+ client = GitHubClient(token=pat_token)
43
+ username = client.get_authenticated_user()
44
+ print(f"✅ Authenticated as: {username}")
45
+
46
+ # Configuration for test
47
+ TEST_ORG = "quickcall-dev"
48
+ TEST_REPO = "quickcall-integrations"
49
+ TEST_ISSUE_TITLE = "[TEST] Comment Management Test - Safe to Delete"
50
+
51
+ created_issue_number = None
52
+ comment_ids = []
53
+
54
+ try:
55
+ # Step 1: Create a test issue
56
+ print("\n--- Step 1: Create test issue ---")
57
+ issue = client.create_issue(
58
+ title=TEST_ISSUE_TITLE,
59
+ body="This is an automated test issue for comment management.\n\n"
60
+ "**This issue will be closed automatically after the test.**",
61
+ labels=["test"],
62
+ owner=TEST_ORG,
63
+ repo=TEST_REPO,
64
+ )
65
+ created_issue_number = issue["number"]
66
+ print(f"✅ Created issue #{created_issue_number}")
67
+ print(f" URL: {issue['html_url']}")
68
+
69
+ time.sleep(1)
70
+
71
+ # Step 2: Add multiple comments
72
+ print("\n--- Step 2: Add 3 comments ---")
73
+ for i in range(1, 4):
74
+ comment = client.comment_on_issue(
75
+ issue_number=created_issue_number,
76
+ body=f"Test comment #{i}",
77
+ owner=TEST_ORG,
78
+ repo=TEST_REPO,
79
+ )
80
+ comment_ids.append(comment["id"])
81
+ print(f"✅ Added comment #{i} (ID: {comment['id']})")
82
+ time.sleep(0.5)
83
+
84
+ # Step 3: List comments (ascending - oldest first)
85
+ print("\n--- Step 3: List comments (oldest first) ---")
86
+ comments_asc = client.list_issue_comments(
87
+ issue_number=created_issue_number,
88
+ owner=TEST_ORG,
89
+ repo=TEST_REPO,
90
+ limit=10,
91
+ order="asc",
92
+ )
93
+ print(f"✅ Found {len(comments_asc)} comments (asc)")
94
+ for c in comments_asc:
95
+ print(f" - {c['body'][:50]} (by {c['author']})")
96
+
97
+ # Step 4: List comments (descending - newest first)
98
+ print("\n--- Step 4: List comments (newest first) ---")
99
+ comments_desc = client.list_issue_comments(
100
+ issue_number=created_issue_number,
101
+ owner=TEST_ORG,
102
+ repo=TEST_REPO,
103
+ limit=10,
104
+ order="desc",
105
+ )
106
+ print(f"✅ Found {len(comments_desc)} comments (desc)")
107
+ for c in comments_desc:
108
+ print(f" - {c['body'][:50]} (by {c['author']})")
109
+
110
+ # Verify order is reversed
111
+ if comments_asc and comments_desc:
112
+ assert comments_asc[0]["id"] == comments_desc[-1]["id"], (
113
+ "Order should be reversed"
114
+ )
115
+ print("✅ Order verification passed")
116
+
117
+ # Step 5: List with limit
118
+ print("\n--- Step 5: List with limit=2 ---")
119
+ comments_limited = client.list_issue_comments(
120
+ issue_number=created_issue_number,
121
+ owner=TEST_ORG,
122
+ repo=TEST_REPO,
123
+ limit=2,
124
+ order="asc",
125
+ )
126
+ print(f"✅ Found {len(comments_limited)} comments (limited to 2)")
127
+ assert len(comments_limited) == 2, "Should return exactly 2 comments"
128
+
129
+ # Step 6: Update a comment
130
+ print("\n--- Step 6: Update comment ---")
131
+ comment_to_update = comment_ids[1] # Update the second comment
132
+ updated = client.update_issue_comment(
133
+ comment_id=comment_to_update,
134
+ body="Test comment #2 (UPDATED)",
135
+ owner=TEST_ORG,
136
+ repo=TEST_REPO,
137
+ )
138
+ print(f"✅ Updated comment ID {comment_to_update}")
139
+ print(f" New body: {updated['body']}")
140
+ assert "UPDATED" in updated["body"], "Comment should be updated"
141
+
142
+ # Step 7: Get specific comment
143
+ print("\n--- Step 7: Get specific comment ---")
144
+ fetched = client.get_issue_comment(
145
+ comment_id=comment_to_update,
146
+ owner=TEST_ORG,
147
+ repo=TEST_REPO,
148
+ )
149
+ print(f"✅ Fetched comment ID {fetched['id']}")
150
+ print(f" Body: {fetched['body']}")
151
+
152
+ # Step 8: Delete a comment
153
+ print("\n--- Step 8: Delete comment ---")
154
+ comment_to_delete = comment_ids[2] # Delete the third comment
155
+ result = client.delete_issue_comment(
156
+ comment_id=comment_to_delete,
157
+ owner=TEST_ORG,
158
+ repo=TEST_REPO,
159
+ )
160
+ print(f"✅ Deleted comment ID {comment_to_delete}")
161
+ assert result["deleted"] is True, "Should return deleted=True"
162
+
163
+ # Verify deletion
164
+ time.sleep(1)
165
+ comments_after = client.list_issue_comments(
166
+ issue_number=created_issue_number,
167
+ owner=TEST_ORG,
168
+ repo=TEST_REPO,
169
+ limit=10,
170
+ )
171
+ print(f"✅ Verified: now {len(comments_after)} comments (was 3)")
172
+ assert len(comments_after) == 2, "Should have 2 comments after deletion"
173
+
174
+ # Step 9: Cleanup - close issue
175
+ print("\n--- Step 9: Close test issue ---")
176
+ client.close_issue(created_issue_number, owner=TEST_ORG, repo=TEST_REPO)
177
+ print(f"✅ Closed issue #{created_issue_number}")
178
+
179
+ print("\n" + "=" * 60)
180
+ print("✅ ALL COMMENT MANAGEMENT TESTS PASSED!")
181
+ print("=" * 60)
182
+ return True
183
+
184
+ except Exception as e:
185
+ print(f"\n❌ Test failed with error: {e}")
186
+ import traceback
187
+
188
+ traceback.print_exc()
189
+
190
+ # Cleanup
191
+ if created_issue_number:
192
+ try:
193
+ print(f"\n--- Cleanup: Closing issue #{created_issue_number} ---")
194
+ client.close_issue(created_issue_number, owner=TEST_ORG, repo=TEST_REPO)
195
+ print(f"✅ Closed issue #{created_issue_number}")
196
+ except Exception as cleanup_error:
197
+ print(f"⚠️ Cleanup failed: {cleanup_error}")
198
+
199
+ return False
200
+
201
+
202
+ def test_error_handling():
203
+ """Test error handling for invalid comment operations."""
204
+ from mcp_server.api_clients.github_client import GitHubClient
205
+ from github import GithubException, UnknownObjectException
206
+ from mcp_server.auth import get_github_pat
207
+
208
+ print("\n" + "=" * 60)
209
+ print("Test: Error Handling")
210
+ print("=" * 60)
211
+
212
+ pat_token, _ = get_github_pat()
213
+ if not pat_token:
214
+ print("⚠️ Skipping - no PAT")
215
+ return True
216
+
217
+ client = GitHubClient(token=pat_token)
218
+
219
+ # Test 1: Get non-existent comment
220
+ print("\nTest 1: Get non-existent comment")
221
+ try:
222
+ client.get_issue_comment(
223
+ comment_id=999999999,
224
+ owner="quickcall-dev",
225
+ repo="quickcall-integrations",
226
+ )
227
+ print("❌ Should have raised an exception")
228
+ return False
229
+ except (GithubException, UnknownObjectException):
230
+ print("✅ Correctly raised exception for non-existent comment")
231
+
232
+ # Test 2: Update non-existent comment
233
+ print("\nTest 2: Update non-existent comment")
234
+ try:
235
+ client.update_issue_comment(
236
+ comment_id=999999999,
237
+ body="test",
238
+ owner="quickcall-dev",
239
+ repo="quickcall-integrations",
240
+ )
241
+ print("❌ Should have raised an exception")
242
+ return False
243
+ except (GithubException, UnknownObjectException):
244
+ print("✅ Correctly raised exception for update")
245
+
246
+ print("\n✅ All error handling tests passed!")
247
+ return True
248
+
249
+
250
+ if __name__ == "__main__":
251
+ results = []
252
+
253
+ # Run main integration test
254
+ results.append(("Full Comment Management Test", run_integration_test()))
255
+
256
+ # Run error handling test
257
+ results.append(("Error Handling", test_error_handling()))
258
+
259
+ # Summary
260
+ print("\n" + "=" * 60)
261
+ print("TEST SUMMARY")
262
+ print("=" * 60)
263
+ all_passed = True
264
+ for name, passed in results:
265
+ status = "✅ PASSED" if passed else "❌ FAILED"
266
+ print(f" {status}: {name}")
267
+ if not passed:
268
+ all_passed = False
269
+
270
+ print("=" * 60)
271
+ sys.exit(0 if all_passed else 1)