quickcall-integrations 0.3.5__tar.gz → 0.3.7__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.
- quickcall_integrations-0.3.7/.quickcall-issue-template.yaml +38 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/PKG-INFO +1 -1
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/mcp_server/api_clients/github_client.py +226 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/mcp_server/resources/github_resources.py +26 -57
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/mcp_server/tools/github_tools.py +245 -28
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/plugins/quickcall/.claude-plugin/plugin.json +1 -1
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/pyproject.toml +1 -1
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/uv.lock +1 -1
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/.claude-plugin/marketplace.json +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/.github/workflows/publish-pypi.yml +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/.gitignore +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/.pre-commit-config.yaml +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/Dockerfile +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/README.md +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/assets/logo.png +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/mcp_server/__init__.py +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/mcp_server/api_clients/__init__.py +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/mcp_server/api_clients/slack_client.py +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/mcp_server/auth/__init__.py +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/mcp_server/auth/credentials.py +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/mcp_server/auth/device_flow.py +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/mcp_server/resources/__init__.py +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/mcp_server/resources/slack_resources.py +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/mcp_server/server.py +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/mcp_server/tools/__init__.py +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/mcp_server/tools/auth_tools.py +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/mcp_server/tools/git_tools.py +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/mcp_server/tools/slack_tools.py +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/mcp_server/tools/utility_tools.py +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/plugins/quickcall/commands/appraisal.md +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/plugins/quickcall/commands/connect-github-pat.md +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/plugins/quickcall/commands/connect.md +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/plugins/quickcall/commands/slack-summary.md +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/plugins/quickcall/commands/status.md +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/plugins/quickcall/commands/updates.md +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/requirements.txt +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/tests/README.md +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/tests/appraisal/__init__.py +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/tests/appraisal/setup_test_data.py +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/tests/test_appraisal_integration.py +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/tests/test_appraisal_tools.py +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/tests/test_integrations.py +0 -0
- {quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/tests/test_tools.py +0 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
defaults:
|
|
2
|
+
labels: ["triage"]
|
|
3
|
+
body: |
|
|
4
|
+
## Description
|
|
5
|
+
|
|
6
|
+
## Details
|
|
7
|
+
|
|
8
|
+
templates:
|
|
9
|
+
bug:
|
|
10
|
+
labels: ["bug", "triage"]
|
|
11
|
+
body: |
|
|
12
|
+
## Bug Description
|
|
13
|
+
|
|
14
|
+
## Steps to Reproduce
|
|
15
|
+
1.
|
|
16
|
+
2.
|
|
17
|
+
|
|
18
|
+
## Expected Behavior
|
|
19
|
+
|
|
20
|
+
## Actual Behavior
|
|
21
|
+
|
|
22
|
+
feature:
|
|
23
|
+
labels: ["enhancement"]
|
|
24
|
+
body: |
|
|
25
|
+
## Feature Request
|
|
26
|
+
|
|
27
|
+
## Problem Statement
|
|
28
|
+
|
|
29
|
+
## Proposed Solution
|
|
30
|
+
|
|
31
|
+
## Alternatives Considered
|
|
32
|
+
|
|
33
|
+
docs:
|
|
34
|
+
labels: ["documentation"]
|
|
35
|
+
body: |
|
|
36
|
+
## Documentation Update
|
|
37
|
+
|
|
38
|
+
## What needs to be documented?
|
|
@@ -686,6 +686,232 @@ class GitHubClient:
|
|
|
686
686
|
"issue_number": issue_number,
|
|
687
687
|
}
|
|
688
688
|
|
|
689
|
+
def get_issue(
|
|
690
|
+
self,
|
|
691
|
+
issue_number: int,
|
|
692
|
+
owner: Optional[str] = None,
|
|
693
|
+
repo: Optional[str] = None,
|
|
694
|
+
include_sub_issues: bool = True,
|
|
695
|
+
) -> Dict[str, Any]:
|
|
696
|
+
"""
|
|
697
|
+
Get detailed information about a GitHub issue.
|
|
698
|
+
|
|
699
|
+
Args:
|
|
700
|
+
issue_number: Issue number
|
|
701
|
+
owner: Repository owner
|
|
702
|
+
repo: Repository name
|
|
703
|
+
include_sub_issues: Whether to fetch sub-issues list
|
|
704
|
+
|
|
705
|
+
Returns:
|
|
706
|
+
Issue details including sub-issues if requested
|
|
707
|
+
"""
|
|
708
|
+
gh_repo = self._get_repo(owner, repo)
|
|
709
|
+
issue = gh_repo.get_issue(issue_number)
|
|
710
|
+
|
|
711
|
+
result = {
|
|
712
|
+
"number": issue.number,
|
|
713
|
+
"id": issue.id, # Internal ID needed for sub-issues API
|
|
714
|
+
"title": issue.title,
|
|
715
|
+
"body": issue.body,
|
|
716
|
+
"state": issue.state,
|
|
717
|
+
"html_url": issue.html_url,
|
|
718
|
+
"labels": [label.name for label in issue.labels],
|
|
719
|
+
"assignees": [a.login for a in issue.assignees],
|
|
720
|
+
"created_at": issue.created_at.isoformat(),
|
|
721
|
+
"updated_at": issue.updated_at.isoformat() if issue.updated_at else None,
|
|
722
|
+
"closed_at": issue.closed_at.isoformat() if issue.closed_at else None,
|
|
723
|
+
"comments_count": issue.comments,
|
|
724
|
+
"author": issue.user.login if issue.user else "unknown",
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
# Fetch sub-issues if requested
|
|
728
|
+
if include_sub_issues:
|
|
729
|
+
owner = owner or self.default_owner
|
|
730
|
+
repo_name = repo or self.default_repo
|
|
731
|
+
sub_issues = self.list_sub_issues(issue_number, owner=owner, repo=repo_name)
|
|
732
|
+
result["sub_issues"] = sub_issues
|
|
733
|
+
result["sub_issues_count"] = len(sub_issues)
|
|
734
|
+
|
|
735
|
+
return result
|
|
736
|
+
|
|
737
|
+
# ========================================================================
|
|
738
|
+
# Sub-Issue Operations (GitHub's native sub-issues feature)
|
|
739
|
+
# ========================================================================
|
|
740
|
+
|
|
741
|
+
def list_sub_issues(
|
|
742
|
+
self,
|
|
743
|
+
parent_issue_number: int,
|
|
744
|
+
owner: Optional[str] = None,
|
|
745
|
+
repo: Optional[str] = None,
|
|
746
|
+
) -> List[Dict[str, Any]]:
|
|
747
|
+
"""
|
|
748
|
+
List sub-issues of a parent issue.
|
|
749
|
+
|
|
750
|
+
Args:
|
|
751
|
+
parent_issue_number: Parent issue number
|
|
752
|
+
owner: Repository owner
|
|
753
|
+
repo: Repository name
|
|
754
|
+
|
|
755
|
+
Returns:
|
|
756
|
+
List of sub-issue summaries
|
|
757
|
+
"""
|
|
758
|
+
owner = owner or self.default_owner
|
|
759
|
+
repo = repo or self.default_repo
|
|
760
|
+
|
|
761
|
+
if not owner or not repo:
|
|
762
|
+
raise ValueError("Repository owner and name must be specified")
|
|
763
|
+
|
|
764
|
+
try:
|
|
765
|
+
with httpx.Client() as client:
|
|
766
|
+
response = client.get(
|
|
767
|
+
f"https://api.github.com/repos/{owner}/{repo}/issues/{parent_issue_number}/sub_issues",
|
|
768
|
+
headers={
|
|
769
|
+
"Authorization": f"Bearer {self.token}",
|
|
770
|
+
"Accept": "application/vnd.github+json",
|
|
771
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
772
|
+
},
|
|
773
|
+
timeout=30.0,
|
|
774
|
+
)
|
|
775
|
+
response.raise_for_status()
|
|
776
|
+
data = response.json()
|
|
777
|
+
|
|
778
|
+
return [
|
|
779
|
+
{
|
|
780
|
+
"number": item["number"],
|
|
781
|
+
"id": item["id"],
|
|
782
|
+
"title": item["title"],
|
|
783
|
+
"state": item["state"],
|
|
784
|
+
"html_url": item["html_url"],
|
|
785
|
+
}
|
|
786
|
+
for item in data
|
|
787
|
+
]
|
|
788
|
+
except httpx.HTTPStatusError as e:
|
|
789
|
+
if e.response.status_code == 404:
|
|
790
|
+
# No sub-issues or feature not enabled
|
|
791
|
+
return []
|
|
792
|
+
logger.error(f"Failed to list sub-issues: HTTP {e.response.status_code}")
|
|
793
|
+
raise GithubException(e.response.status_code, e.response.json())
|
|
794
|
+
except Exception as e:
|
|
795
|
+
logger.error(f"Failed to list sub-issues: {e}")
|
|
796
|
+
return []
|
|
797
|
+
|
|
798
|
+
def add_sub_issue(
|
|
799
|
+
self,
|
|
800
|
+
parent_issue_number: int,
|
|
801
|
+
child_issue_number: int,
|
|
802
|
+
owner: Optional[str] = None,
|
|
803
|
+
repo: Optional[str] = None,
|
|
804
|
+
) -> Dict[str, Any]:
|
|
805
|
+
"""
|
|
806
|
+
Add an existing issue as a sub-issue to a parent.
|
|
807
|
+
|
|
808
|
+
Args:
|
|
809
|
+
parent_issue_number: Parent issue number
|
|
810
|
+
child_issue_number: Child issue number to add as sub-issue
|
|
811
|
+
owner: Repository owner
|
|
812
|
+
repo: Repository name
|
|
813
|
+
|
|
814
|
+
Returns:
|
|
815
|
+
Result with parent and child info
|
|
816
|
+
"""
|
|
817
|
+
owner = owner or self.default_owner
|
|
818
|
+
repo = repo or self.default_repo
|
|
819
|
+
|
|
820
|
+
if not owner or not repo:
|
|
821
|
+
raise ValueError("Repository owner and name must be specified")
|
|
822
|
+
|
|
823
|
+
# First, get the child issue's internal ID (required by API)
|
|
824
|
+
gh_repo = self._get_repo(owner, repo)
|
|
825
|
+
child_issue = gh_repo.get_issue(child_issue_number)
|
|
826
|
+
child_id = child_issue.id
|
|
827
|
+
|
|
828
|
+
try:
|
|
829
|
+
with httpx.Client() as client:
|
|
830
|
+
response = client.post(
|
|
831
|
+
f"https://api.github.com/repos/{owner}/{repo}/issues/{parent_issue_number}/sub_issues",
|
|
832
|
+
headers={
|
|
833
|
+
"Authorization": f"Bearer {self.token}",
|
|
834
|
+
"Accept": "application/vnd.github+json",
|
|
835
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
836
|
+
},
|
|
837
|
+
json={"sub_issue_id": child_id},
|
|
838
|
+
timeout=30.0,
|
|
839
|
+
)
|
|
840
|
+
response.raise_for_status()
|
|
841
|
+
|
|
842
|
+
return {
|
|
843
|
+
"success": True,
|
|
844
|
+
"parent_issue": parent_issue_number,
|
|
845
|
+
"child_issue": child_issue_number,
|
|
846
|
+
"child_id": child_id,
|
|
847
|
+
}
|
|
848
|
+
except httpx.HTTPStatusError as e:
|
|
849
|
+
logger.error(f"Failed to add sub-issue: HTTP {e.response.status_code}")
|
|
850
|
+
error_data = e.response.json() if e.response.content else {}
|
|
851
|
+
raise GithubException(
|
|
852
|
+
e.response.status_code,
|
|
853
|
+
error_data,
|
|
854
|
+
message=f"Failed to add #{child_issue_number} as sub-issue of #{parent_issue_number}",
|
|
855
|
+
)
|
|
856
|
+
|
|
857
|
+
def remove_sub_issue(
|
|
858
|
+
self,
|
|
859
|
+
parent_issue_number: int,
|
|
860
|
+
child_issue_number: int,
|
|
861
|
+
owner: Optional[str] = None,
|
|
862
|
+
repo: Optional[str] = None,
|
|
863
|
+
) -> Dict[str, Any]:
|
|
864
|
+
"""
|
|
865
|
+
Remove a sub-issue from a parent.
|
|
866
|
+
|
|
867
|
+
Args:
|
|
868
|
+
parent_issue_number: Parent issue number
|
|
869
|
+
child_issue_number: Child issue number to remove
|
|
870
|
+
owner: Repository owner
|
|
871
|
+
repo: Repository name
|
|
872
|
+
|
|
873
|
+
Returns:
|
|
874
|
+
Result with parent and child info
|
|
875
|
+
"""
|
|
876
|
+
owner = owner or self.default_owner
|
|
877
|
+
repo = repo or self.default_repo
|
|
878
|
+
|
|
879
|
+
if not owner or not repo:
|
|
880
|
+
raise ValueError("Repository owner and name must be specified")
|
|
881
|
+
|
|
882
|
+
# Get the child issue's internal ID
|
|
883
|
+
gh_repo = self._get_repo(owner, repo)
|
|
884
|
+
child_issue = gh_repo.get_issue(child_issue_number)
|
|
885
|
+
child_id = child_issue.id
|
|
886
|
+
|
|
887
|
+
try:
|
|
888
|
+
with httpx.Client() as client:
|
|
889
|
+
response = client.delete(
|
|
890
|
+
f"https://api.github.com/repos/{owner}/{repo}/issues/{parent_issue_number}/sub_issues/{child_id}",
|
|
891
|
+
headers={
|
|
892
|
+
"Authorization": f"Bearer {self.token}",
|
|
893
|
+
"Accept": "application/vnd.github+json",
|
|
894
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
895
|
+
},
|
|
896
|
+
timeout=30.0,
|
|
897
|
+
)
|
|
898
|
+
response.raise_for_status()
|
|
899
|
+
|
|
900
|
+
return {
|
|
901
|
+
"success": True,
|
|
902
|
+
"parent_issue": parent_issue_number,
|
|
903
|
+
"child_issue": child_issue_number,
|
|
904
|
+
"removed": True,
|
|
905
|
+
}
|
|
906
|
+
except httpx.HTTPStatusError as e:
|
|
907
|
+
logger.error(f"Failed to remove sub-issue: HTTP {e.response.status_code}")
|
|
908
|
+
error_data = e.response.json() if e.response.content else {}
|
|
909
|
+
raise GithubException(
|
|
910
|
+
e.response.status_code,
|
|
911
|
+
error_data,
|
|
912
|
+
message=f"Failed to remove #{child_issue_number} from #{parent_issue_number}",
|
|
913
|
+
)
|
|
914
|
+
|
|
689
915
|
# ========================================================================
|
|
690
916
|
# Search Operations (for Appraisals)
|
|
691
917
|
# ========================================================================
|
|
@@ -5,50 +5,14 @@ Resources are automatically available in Claude's context when connected.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
|
-
from typing import Any, Dict, Optional
|
|
9
8
|
|
|
10
|
-
import yaml
|
|
11
9
|
from fastmcp import FastMCP
|
|
12
10
|
|
|
13
11
|
from mcp_server.auth import get_credential_store, get_github_pat
|
|
14
|
-
from mcp_server.auth.credentials import _find_project_root, _parse_env_file
|
|
15
12
|
|
|
16
13
|
logger = logging.getLogger(__name__)
|
|
17
14
|
|
|
18
15
|
|
|
19
|
-
def _load_issue_templates_config() -> Optional[Dict[str, Any]]:
|
|
20
|
-
"""
|
|
21
|
-
Load issue templates from ISSUE_TEMPLATE_PATH in .quickcall.env.
|
|
22
|
-
Returns None if not configured or file doesn't exist.
|
|
23
|
-
"""
|
|
24
|
-
import os
|
|
25
|
-
from pathlib import Path
|
|
26
|
-
|
|
27
|
-
template_path = os.getenv("ISSUE_TEMPLATE_PATH")
|
|
28
|
-
|
|
29
|
-
# Check .quickcall.env in project root
|
|
30
|
-
if not template_path:
|
|
31
|
-
project_root = _find_project_root()
|
|
32
|
-
if project_root:
|
|
33
|
-
config_path = project_root / ".quickcall.env"
|
|
34
|
-
if config_path.exists():
|
|
35
|
-
env_vars = _parse_env_file(config_path)
|
|
36
|
-
if "ISSUE_TEMPLATE_PATH" in env_vars:
|
|
37
|
-
template_path = env_vars["ISSUE_TEMPLATE_PATH"]
|
|
38
|
-
if not Path(template_path).is_absolute():
|
|
39
|
-
template_path = str(project_root / template_path)
|
|
40
|
-
|
|
41
|
-
if not template_path:
|
|
42
|
-
return None
|
|
43
|
-
|
|
44
|
-
try:
|
|
45
|
-
with open(template_path) as f:
|
|
46
|
-
return yaml.safe_load(f) or {}
|
|
47
|
-
except Exception as e:
|
|
48
|
-
logger.warning(f"Failed to load issue templates: {e}")
|
|
49
|
-
return None
|
|
50
|
-
|
|
51
|
-
|
|
52
16
|
def create_github_resources(mcp: FastMCP) -> None:
|
|
53
17
|
"""Add GitHub resources to the MCP server."""
|
|
54
18
|
|
|
@@ -107,41 +71,46 @@ def create_github_resources(mcp: FastMCP) -> None:
|
|
|
107
71
|
"""
|
|
108
72
|
Available issue templates from project configuration.
|
|
109
73
|
|
|
74
|
+
Supports both:
|
|
75
|
+
- GitHub native templates (.github/ISSUE_TEMPLATE/*.yml)
|
|
76
|
+
- Custom templates (ISSUE_TEMPLATE_PATH in .quickcall.env)
|
|
77
|
+
|
|
110
78
|
Use template names when creating issues with manage_issues.
|
|
111
79
|
"""
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if not config:
|
|
115
|
-
return "No issue templates configured.\n\nTo configure:\n1. Create a YAML file with your templates\n2. Add ISSUE_TEMPLATE_PATH=/path/to/templates.yaml to .quickcall.env"
|
|
80
|
+
# Import here to avoid circular imports
|
|
81
|
+
from mcp_server.tools.github_tools import _get_all_templates
|
|
116
82
|
|
|
117
|
-
templates =
|
|
118
|
-
defaults = config.get("defaults", {})
|
|
83
|
+
templates = _get_all_templates()
|
|
119
84
|
|
|
120
85
|
if not templates:
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if defaults.get("body"):
|
|
128
|
-
lines.append(f" Body template: {defaults['body'][:100]}...")
|
|
129
|
-
return "\n".join(lines)
|
|
86
|
+
return (
|
|
87
|
+
"No issue templates found.\n\n"
|
|
88
|
+
"Supported sources:\n"
|
|
89
|
+
"1. GitHub native: .github/ISSUE_TEMPLATE/*.yml\n"
|
|
90
|
+
"2. Custom: Add ISSUE_TEMPLATE_PATH to .quickcall.env"
|
|
91
|
+
)
|
|
130
92
|
|
|
131
93
|
lines = ["Available Issue Templates:", ""]
|
|
132
94
|
|
|
133
|
-
for
|
|
95
|
+
for key, template in templates.items():
|
|
96
|
+
name = template.get("name", key)
|
|
97
|
+
description = template.get("description", "")
|
|
134
98
|
labels = template.get("labels", [])
|
|
135
|
-
|
|
136
|
-
|
|
99
|
+
title_prefix = template.get("title_prefix", "")
|
|
100
|
+
|
|
101
|
+
lines.append(f"- {key}")
|
|
102
|
+
if name != key:
|
|
103
|
+
lines.append(f" Name: {name}")
|
|
104
|
+
if description:
|
|
105
|
+
lines.append(f" Description: {description}")
|
|
137
106
|
if labels:
|
|
138
107
|
lines.append(f" Labels: {', '.join(labels)}")
|
|
139
|
-
if
|
|
140
|
-
lines.append(f"
|
|
108
|
+
if title_prefix:
|
|
109
|
+
lines.append(f" Title prefix: {title_prefix}")
|
|
141
110
|
|
|
142
111
|
lines.append("")
|
|
143
112
|
lines.append(
|
|
144
|
-
"Usage: manage_issues(action='create', title='...', template='<
|
|
113
|
+
"Usage: manage_issues(action='create', title='...', template='<key>')"
|
|
145
114
|
)
|
|
146
115
|
|
|
147
116
|
return "\n".join(lines)
|
{quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/mcp_server/tools/github_tools.py
RENAMED
|
@@ -44,14 +44,85 @@ DEFAULT_ISSUE_TEMPLATE: Dict[str, Any] = {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
|
|
47
|
-
def
|
|
47
|
+
def _load_github_native_templates() -> Dict[str, Dict[str, Any]]:
|
|
48
48
|
"""
|
|
49
|
-
Load issue
|
|
50
|
-
Returns
|
|
49
|
+
Load GitHub native issue templates from .github/ISSUE_TEMPLATE/*.yml.
|
|
50
|
+
Returns dict of template_name -> template_config.
|
|
51
51
|
"""
|
|
52
|
-
|
|
52
|
+
project_root = _find_project_root()
|
|
53
|
+
if not project_root:
|
|
54
|
+
return {}
|
|
55
|
+
|
|
56
|
+
template_dir = project_root / ".github" / "ISSUE_TEMPLATE"
|
|
57
|
+
if not template_dir.exists():
|
|
58
|
+
return {}
|
|
59
|
+
|
|
60
|
+
templates = {}
|
|
61
|
+
for template_file in template_dir.glob("*.yml"):
|
|
62
|
+
try:
|
|
63
|
+
with open(template_file) as f:
|
|
64
|
+
config = yaml.safe_load(f) or {}
|
|
65
|
+
|
|
66
|
+
# Extract template name (use filename without extension as fallback)
|
|
67
|
+
name = config.get("name", template_file.stem)
|
|
68
|
+
# Use filename stem as key for easier matching
|
|
69
|
+
key = template_file.stem
|
|
70
|
+
|
|
71
|
+
# Convert GitHub template format to our format
|
|
72
|
+
templates[key] = {
|
|
73
|
+
"name": name,
|
|
74
|
+
"description": config.get("description", ""),
|
|
75
|
+
"title_prefix": config.get("title", ""),
|
|
76
|
+
"labels": config.get("labels", []),
|
|
77
|
+
"assignees": config.get("assignees", []),
|
|
78
|
+
"body": _github_template_body_to_markdown(config.get("body", [])),
|
|
79
|
+
}
|
|
80
|
+
except Exception as e:
|
|
81
|
+
logger.warning(f"Failed to load GitHub template {template_file}: {e}")
|
|
82
|
+
|
|
83
|
+
return templates
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _github_template_body_to_markdown(body: List[Dict[str, Any]]) -> str:
|
|
87
|
+
"""Convert GitHub issue template body fields to markdown."""
|
|
88
|
+
if not body:
|
|
89
|
+
return ""
|
|
90
|
+
|
|
91
|
+
lines = []
|
|
92
|
+
for field in body:
|
|
93
|
+
field_type = field.get("type", "")
|
|
94
|
+
attrs = field.get("attributes", {})
|
|
95
|
+
label = attrs.get("label", "")
|
|
96
|
+
|
|
97
|
+
if field_type in ("textarea", "input"):
|
|
98
|
+
if label:
|
|
99
|
+
lines.append(f"## {label}")
|
|
100
|
+
lines.append("")
|
|
101
|
+
placeholder = attrs.get("placeholder", "")
|
|
102
|
+
if placeholder:
|
|
103
|
+
lines.append(placeholder)
|
|
104
|
+
lines.append("")
|
|
105
|
+
elif field_type == "markdown":
|
|
106
|
+
value = attrs.get("value", "")
|
|
107
|
+
if value:
|
|
108
|
+
lines.append(value)
|
|
109
|
+
lines.append("")
|
|
110
|
+
|
|
111
|
+
return "\n".join(lines)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _get_all_templates() -> Dict[str, Dict[str, Any]]:
|
|
115
|
+
"""
|
|
116
|
+
Get all available issue templates from both sources.
|
|
117
|
+
Priority: Custom templates (.quickcall.env) > GitHub native templates
|
|
118
|
+
"""
|
|
119
|
+
templates = {}
|
|
120
|
+
|
|
121
|
+
# 1. Load GitHub native templates first (lower priority)
|
|
122
|
+
templates.update(_load_github_native_templates())
|
|
53
123
|
|
|
54
|
-
#
|
|
124
|
+
# 2. Load custom templates (higher priority, can override)
|
|
125
|
+
template_path = os.getenv("ISSUE_TEMPLATE_PATH")
|
|
55
126
|
if not template_path:
|
|
56
127
|
project_root = _find_project_root()
|
|
57
128
|
if project_root:
|
|
@@ -63,24 +134,52 @@ def _load_issue_template(template_type: Optional[str] = None) -> Dict[str, Any]:
|
|
|
63
134
|
if not Path(template_path).is_absolute():
|
|
64
135
|
template_path = str(project_root / template_path)
|
|
65
136
|
|
|
66
|
-
if
|
|
67
|
-
|
|
137
|
+
if template_path:
|
|
138
|
+
try:
|
|
139
|
+
with open(template_path) as f:
|
|
140
|
+
config = yaml.safe_load(f) or {}
|
|
141
|
+
custom_templates = config.get("templates", {})
|
|
142
|
+
for key, tpl in custom_templates.items():
|
|
143
|
+
templates[key] = {
|
|
144
|
+
"name": key,
|
|
145
|
+
"description": "",
|
|
146
|
+
"title_prefix": "",
|
|
147
|
+
"labels": tpl.get("labels", []),
|
|
148
|
+
"assignees": tpl.get("assignees", []),
|
|
149
|
+
"body": tpl.get("body", ""),
|
|
150
|
+
}
|
|
151
|
+
except Exception as e:
|
|
152
|
+
logger.warning(f"Failed to load custom templates: {e}")
|
|
68
153
|
|
|
69
|
-
|
|
70
|
-
with open(template_path) as f:
|
|
71
|
-
config = yaml.safe_load(f) or {}
|
|
154
|
+
return templates
|
|
72
155
|
|
|
73
|
-
# If template_type specified, look for it in templates section
|
|
74
|
-
if template_type and "templates" in config:
|
|
75
|
-
return config["templates"].get(
|
|
76
|
-
template_type, config.get("defaults", DEFAULT_ISSUE_TEMPLATE)
|
|
77
|
-
)
|
|
78
156
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
157
|
+
def _load_issue_template(template_type: Optional[str] = None) -> Dict[str, Any]:
|
|
158
|
+
"""
|
|
159
|
+
Load issue template from available sources.
|
|
160
|
+
|
|
161
|
+
Sources (in priority order):
|
|
162
|
+
1. Custom templates from ISSUE_TEMPLATE_PATH in .quickcall.env
|
|
163
|
+
2. GitHub native templates from .github/ISSUE_TEMPLATE/*.yml
|
|
164
|
+
|
|
165
|
+
Returns defaults if no template found.
|
|
166
|
+
"""
|
|
167
|
+
if not template_type:
|
|
82
168
|
return DEFAULT_ISSUE_TEMPLATE
|
|
83
169
|
|
|
170
|
+
all_templates = _get_all_templates()
|
|
171
|
+
|
|
172
|
+
if template_type in all_templates:
|
|
173
|
+
tpl = all_templates[template_type]
|
|
174
|
+
return {
|
|
175
|
+
"labels": tpl.get("labels", []),
|
|
176
|
+
"assignees": tpl.get("assignees", []),
|
|
177
|
+
"body": tpl.get("body", ""),
|
|
178
|
+
"title_prefix": tpl.get("title_prefix", ""),
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return DEFAULT_ISSUE_TEMPLATE
|
|
182
|
+
|
|
84
183
|
|
|
85
184
|
# Track whether we're using PAT mode for status reporting
|
|
86
185
|
_using_pat_mode: bool = False
|
|
@@ -471,11 +570,12 @@ def create_github_tools(mcp: FastMCP) -> None:
|
|
|
471
570
|
def manage_issues(
|
|
472
571
|
action: str = Field(
|
|
473
572
|
...,
|
|
474
|
-
description="Action: 'create', 'update', 'close', 'reopen',
|
|
573
|
+
description="Action: 'view', 'create', 'update', 'close', 'reopen', 'comment', "
|
|
574
|
+
"'add_sub_issue', 'remove_sub_issue', 'list_sub_issues'",
|
|
475
575
|
),
|
|
476
576
|
issue_numbers: Optional[List[int]] = Field(
|
|
477
577
|
default=None,
|
|
478
|
-
description="Issue number(s). Required for update/close/reopen/comment
|
|
578
|
+
description="Issue number(s). Required for view/update/close/reopen/comment/sub-issue ops.",
|
|
479
579
|
),
|
|
480
580
|
title: Optional[str] = Field(
|
|
481
581
|
default=None,
|
|
@@ -495,7 +595,12 @@ def create_github_tools(mcp: FastMCP) -> None:
|
|
|
495
595
|
),
|
|
496
596
|
template: Optional[str] = Field(
|
|
497
597
|
default=None,
|
|
498
|
-
description="Template name for 'create' (e.g., '
|
|
598
|
+
description="Template name for 'create' (e.g., 'bug_report', 'feature_request')",
|
|
599
|
+
),
|
|
600
|
+
parent_issue: Optional[int] = Field(
|
|
601
|
+
default=None,
|
|
602
|
+
description="Parent issue number. For 'create': attach new issue as sub-issue. "
|
|
603
|
+
"For 'add_sub_issue'/'remove_sub_issue'/'list_sub_issues': the parent issue.",
|
|
499
604
|
),
|
|
500
605
|
owner: Optional[str] = Field(
|
|
501
606
|
default=None,
|
|
@@ -507,18 +612,24 @@ def create_github_tools(mcp: FastMCP) -> None:
|
|
|
507
612
|
),
|
|
508
613
|
) -> dict:
|
|
509
614
|
"""
|
|
510
|
-
Manage GitHub issues: create, update, close, reopen,
|
|
615
|
+
Manage GitHub issues: view, create, update, close, reopen, comment, and sub-issues.
|
|
511
616
|
|
|
512
|
-
Supports bulk operations for close/reopen/comment via issue_numbers list.
|
|
617
|
+
Supports bulk operations for view/close/reopen/comment via issue_numbers list.
|
|
513
618
|
|
|
514
619
|
Examples:
|
|
515
|
-
-
|
|
620
|
+
- view: manage_issues(action="view", issue_numbers=[42])
|
|
621
|
+
- create: manage_issues(action="create", title="Bug", template="bug_report")
|
|
622
|
+
- create as sub-issue: manage_issues(action="create", title="Task 1", parent_issue=42)
|
|
516
623
|
- close multiple: manage_issues(action="close", issue_numbers=[1, 2, 3])
|
|
517
624
|
- comment: manage_issues(action="comment", issue_numbers=[42], body="Fixed!")
|
|
625
|
+
- add sub-issues: manage_issues(action="add_sub_issue", issue_numbers=[43,44], parent_issue=42)
|
|
626
|
+
- remove sub-issue: manage_issues(action="remove_sub_issue", issue_numbers=[43], parent_issue=42)
|
|
627
|
+
- list sub-issues: manage_issues(action="list_sub_issues", parent_issue=42)
|
|
518
628
|
"""
|
|
519
629
|
try:
|
|
520
630
|
client = _get_client()
|
|
521
631
|
|
|
632
|
+
# === CREATE ACTION ===
|
|
522
633
|
if action == "create":
|
|
523
634
|
if not title:
|
|
524
635
|
raise ToolError("'title' is required for 'create' action")
|
|
@@ -526,24 +637,79 @@ def create_github_tools(mcp: FastMCP) -> None:
|
|
|
526
637
|
tpl = _load_issue_template(template)
|
|
527
638
|
final_body = body if body is not None else tpl.get("body", "")
|
|
528
639
|
final_labels = labels if labels is not None else tpl.get("labels", [])
|
|
640
|
+
final_assignees = (
|
|
641
|
+
assignees if assignees is not None else tpl.get("assignees", [])
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
# Apply title prefix from template if present
|
|
645
|
+
title_prefix = tpl.get("title_prefix", "")
|
|
646
|
+
if title_prefix and not title.startswith(title_prefix):
|
|
647
|
+
title = f"{title_prefix}{title}"
|
|
529
648
|
|
|
530
649
|
issue = client.create_issue(
|
|
531
650
|
title=title,
|
|
532
651
|
body=final_body,
|
|
533
652
|
labels=final_labels,
|
|
534
|
-
assignees=
|
|
653
|
+
assignees=final_assignees,
|
|
535
654
|
owner=owner,
|
|
536
655
|
repo=repo,
|
|
537
656
|
)
|
|
538
|
-
return {"action": "created", "issue": issue}
|
|
539
657
|
|
|
540
|
-
|
|
658
|
+
result = {"action": "created", "issue": issue}
|
|
659
|
+
|
|
660
|
+
# If parent_issue specified, add as sub-issue
|
|
661
|
+
if parent_issue:
|
|
662
|
+
try:
|
|
663
|
+
sub_result = client.add_sub_issue(
|
|
664
|
+
parent_issue_number=parent_issue,
|
|
665
|
+
child_issue_number=issue["number"],
|
|
666
|
+
owner=owner,
|
|
667
|
+
repo=repo,
|
|
668
|
+
)
|
|
669
|
+
result["sub_issue_of"] = parent_issue
|
|
670
|
+
result["sub_issue_linked"] = sub_result.get("success", False)
|
|
671
|
+
except Exception as e:
|
|
672
|
+
result["sub_issue_error"] = str(e)
|
|
673
|
+
|
|
674
|
+
return result
|
|
675
|
+
|
|
676
|
+
# === LIST SUB-ISSUES ACTION ===
|
|
677
|
+
if action == "list_sub_issues":
|
|
678
|
+
if not parent_issue:
|
|
679
|
+
raise ToolError(
|
|
680
|
+
"'parent_issue' is required for 'list_sub_issues' action"
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
sub_issues = client.list_sub_issues(
|
|
684
|
+
parent_issue_number=parent_issue,
|
|
685
|
+
owner=owner,
|
|
686
|
+
repo=repo,
|
|
687
|
+
)
|
|
688
|
+
return {
|
|
689
|
+
"action": "list_sub_issues",
|
|
690
|
+
"parent_issue": parent_issue,
|
|
691
|
+
"count": len(sub_issues),
|
|
692
|
+
"sub_issues": sub_issues,
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
# === ALL OTHER ACTIONS REQUIRE issue_numbers ===
|
|
541
696
|
if not issue_numbers:
|
|
542
697
|
raise ToolError(f"'issue_numbers' required for '{action}' action")
|
|
543
698
|
|
|
544
699
|
results = []
|
|
545
700
|
for issue_number in issue_numbers:
|
|
546
|
-
|
|
701
|
+
# === VIEW ACTION ===
|
|
702
|
+
if action == "view":
|
|
703
|
+
issue_data = client.get_issue(
|
|
704
|
+
issue_number=issue_number,
|
|
705
|
+
owner=owner,
|
|
706
|
+
repo=repo,
|
|
707
|
+
include_sub_issues=True,
|
|
708
|
+
)
|
|
709
|
+
results.append(issue_data)
|
|
710
|
+
|
|
711
|
+
# === UPDATE ACTION ===
|
|
712
|
+
elif action == "update":
|
|
547
713
|
client.update_issue(
|
|
548
714
|
issue_number=issue_number,
|
|
549
715
|
title=title,
|
|
@@ -555,14 +721,17 @@ def create_github_tools(mcp: FastMCP) -> None:
|
|
|
555
721
|
)
|
|
556
722
|
results.append({"number": issue_number, "status": "updated"})
|
|
557
723
|
|
|
724
|
+
# === CLOSE ACTION ===
|
|
558
725
|
elif action == "close":
|
|
559
726
|
client.close_issue(issue_number, owner=owner, repo=repo)
|
|
560
727
|
results.append({"number": issue_number, "status": "closed"})
|
|
561
728
|
|
|
729
|
+
# === REOPEN ACTION ===
|
|
562
730
|
elif action == "reopen":
|
|
563
731
|
client.reopen_issue(issue_number, owner=owner, repo=repo)
|
|
564
732
|
results.append({"number": issue_number, "status": "reopened"})
|
|
565
733
|
|
|
734
|
+
# === COMMENT ACTION ===
|
|
566
735
|
elif action == "comment":
|
|
567
736
|
if not body:
|
|
568
737
|
raise ToolError("'body' is required for 'comment' action")
|
|
@@ -577,9 +746,57 @@ def create_github_tools(mcp: FastMCP) -> None:
|
|
|
577
746
|
}
|
|
578
747
|
)
|
|
579
748
|
|
|
749
|
+
# === ADD SUB-ISSUE ACTION ===
|
|
750
|
+
elif action == "add_sub_issue":
|
|
751
|
+
if not parent_issue:
|
|
752
|
+
raise ToolError(
|
|
753
|
+
"'parent_issue' is required for 'add_sub_issue' action"
|
|
754
|
+
)
|
|
755
|
+
sub_result = client.add_sub_issue(
|
|
756
|
+
parent_issue_number=parent_issue,
|
|
757
|
+
child_issue_number=issue_number,
|
|
758
|
+
owner=owner,
|
|
759
|
+
repo=repo,
|
|
760
|
+
)
|
|
761
|
+
results.append(
|
|
762
|
+
{
|
|
763
|
+
"number": issue_number,
|
|
764
|
+
"status": "added_as_sub_issue",
|
|
765
|
+
"parent_issue": parent_issue,
|
|
766
|
+
}
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
# === REMOVE SUB-ISSUE ACTION ===
|
|
770
|
+
elif action == "remove_sub_issue":
|
|
771
|
+
if not parent_issue:
|
|
772
|
+
raise ToolError(
|
|
773
|
+
"'parent_issue' is required for 'remove_sub_issue' action"
|
|
774
|
+
)
|
|
775
|
+
client.remove_sub_issue(
|
|
776
|
+
parent_issue_number=parent_issue,
|
|
777
|
+
child_issue_number=issue_number,
|
|
778
|
+
owner=owner,
|
|
779
|
+
repo=repo,
|
|
780
|
+
)
|
|
781
|
+
results.append(
|
|
782
|
+
{
|
|
783
|
+
"number": issue_number,
|
|
784
|
+
"status": "removed_from_parent",
|
|
785
|
+
"parent_issue": parent_issue,
|
|
786
|
+
}
|
|
787
|
+
)
|
|
788
|
+
|
|
580
789
|
else:
|
|
581
790
|
raise ToolError(f"Invalid action: {action}")
|
|
582
791
|
|
|
792
|
+
# Return format depends on action
|
|
793
|
+
if action == "view":
|
|
794
|
+
return {
|
|
795
|
+
"action": "view",
|
|
796
|
+
"count": len(results),
|
|
797
|
+
"issues": results,
|
|
798
|
+
}
|
|
799
|
+
|
|
583
800
|
return {"action": action, "count": len(results), "results": results}
|
|
584
801
|
|
|
585
802
|
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.6.
|
|
4
|
+
"version": "0.6.6",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Sagar Sarkale"
|
|
7
7
|
}
|
{quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/.claude-plugin/marketplace.json
RENAMED
|
File without changes
|
{quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/.github/workflows/publish-pypi.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/mcp_server/api_clients/__init__.py
RENAMED
|
File without changes
|
{quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/mcp_server/api_clients/slack_client.py
RENAMED
|
File without changes
|
|
File without changes
|
{quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/mcp_server/auth/credentials.py
RENAMED
|
File without changes
|
{quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/mcp_server/auth/device_flow.py
RENAMED
|
File without changes
|
{quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/mcp_server/resources/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/mcp_server/tools/auth_tools.py
RENAMED
|
File without changes
|
|
File without changes
|
{quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/mcp_server/tools/slack_tools.py
RENAMED
|
File without changes
|
{quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/mcp_server/tools/utility_tools.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/plugins/quickcall/commands/connect.md
RENAMED
|
File without changes
|
|
File without changes
|
{quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/plugins/quickcall/commands/status.md
RENAMED
|
File without changes
|
{quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/plugins/quickcall/commands/updates.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/tests/appraisal/setup_test_data.py
RENAMED
|
File without changes
|
{quickcall_integrations-0.3.5 → quickcall_integrations-0.3.7}/tests/test_appraisal_integration.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|