quickcall-integrations 0.3.5__py3-none-any.whl → 0.3.7__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.
@@ -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
- config = _load_issue_templates_config()
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 = config.get("templates", {})
118
- defaults = config.get("defaults", {})
83
+ templates = _get_all_templates()
119
84
 
120
85
  if not templates:
121
- lines = ["Issue Templates:", ""]
122
- if defaults:
123
- labels = defaults.get("labels", [])
124
- lines.append("Default template:")
125
- if labels:
126
- lines.append(f" Labels: {', '.join(labels)}")
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 name, template in templates.items():
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
- body_preview = template.get("body", "")[:80]
136
- lines.append(f"- {name}")
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 body_preview:
140
- lines.append(f" Body: {body_preview}...")
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='<name>')"
113
+ "Usage: manage_issues(action='create', title='...', template='<key>')"
145
114
  )
146
115
 
147
116
  return "\n".join(lines)
@@ -44,14 +44,85 @@ DEFAULT_ISSUE_TEMPLATE: Dict[str, Any] = {
44
44
  }
45
45
 
46
46
 
47
- def _load_issue_template(template_type: Optional[str] = None) -> Dict[str, Any]:
47
+ def _load_github_native_templates() -> Dict[str, Dict[str, Any]]:
48
48
  """
49
- Load issue template from ISSUE_TEMPLATE_PATH in .quickcall.env.
50
- Returns defaults if not configured.
49
+ Load GitHub native issue templates from .github/ISSUE_TEMPLATE/*.yml.
50
+ Returns dict of template_name -> template_config.
51
51
  """
52
- template_path = os.getenv("ISSUE_TEMPLATE_PATH")
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
- # Check .quickcall.env in project root
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 not template_path:
67
- return DEFAULT_ISSUE_TEMPLATE
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
- try:
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
- return config.get("defaults", DEFAULT_ISSUE_TEMPLATE)
80
- except Exception as e:
81
- logger.warning(f"Failed to load issue template: {e}")
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', or 'comment'",
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. Supports bulk operations.",
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., 'bug', 'feature')",
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, or comment.
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
- - create: manage_issues(action="create", title="Bug", template="bug")
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=assignees,
653
+ assignees=final_assignees,
535
654
  owner=owner,
536
655
  repo=repo,
537
656
  )
538
- return {"action": "created", "issue": issue}
539
657
 
540
- # All other actions require issue_numbers
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
- if action == "update":
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quickcall-integrations
3
- Version: 0.3.5
3
+ Version: 0.3.7
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
@@ -1,21 +1,21 @@
1
1
  mcp_server/__init__.py,sha256=6KGzjSPyVB6vQh150DwBjINM_CsZNDhOzwSQFWpXz0U,301
2
2
  mcp_server/server.py,sha256=kv5hh0J-M7yENUBBNI1bkq1y7MB0zn5R_-R1tib6_sk,3108
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=HBLKgwZpbbdhroladE0l4mGjmuoa8uKCTf3oEL6S_P4,29298
4
+ mcp_server/api_clients/github_client.py,sha256=EXwNEP2u5r5YbVgxyaJO0Yb7eQixMk6QWhid7IIQ5uY,37568
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=sDS0W5c16i_UGvhG8Sh1RO93FxRn-hHVAdI9hlWuhx0,20011
8
8
  mcp_server/auth/device_flow.py,sha256=NXNWHzd-CA4dlhEVCgUhwfpe9TpMKpLSJuyFCh70xKs,8371
9
9
  mcp_server/resources/__init__.py,sha256=JrMa3Kf-DmeCB4GwVNfmfw9OGnxF9pJJxCw9Y7u7ujQ,35
10
- mcp_server/resources/github_resources.py,sha256=gS0mTe7UHySAKiD4AqsFZ6pf7ua8Cq9dDrUqRu72_QM,5180
10
+ mcp_server/resources/github_resources.py,sha256=sXE06j9jrSDODxH2832fiCtY9n1lKBQR8QZ8U5wYbJY,4030
11
11
  mcp_server/resources/slack_resources.py,sha256=b_CPxAicwkF3PsBXIat4QoLbDUHM2g_iPzgzvVpwjaw,1687
12
12
  mcp_server/tools/__init__.py,sha256=vIR2ujAaTXm2DgpTsVNz3brI4G34p-Jeg44Qe0uvWc0,405
13
13
  mcp_server/tools/auth_tools.py,sha256=kCPjPC1jrVz0XaRAwPea-ue8ybjLLTxyILplBDJ9Mv4,24477
14
14
  mcp_server/tools/git_tools.py,sha256=jyCTQR2eSzUFXMt0Y8x66758-VY8YCY14DDUJt7GY2U,13957
15
- mcp_server/tools/github_tools.py,sha256=x6aRg03QQd6QV-PePHLUZyOSuKDv06LIVUUCE5qXycU,29951
15
+ mcp_server/tools/github_tools.py,sha256=ogLo0j1G44LTBIOknPgOG-JgTnLaNLGbLj1UccgWtoE,38337
16
16
  mcp_server/tools/slack_tools.py,sha256=-HVE_x3Z1KMeYGi1xhyppEwz5ZF-I-ZD0-Up8yBeoYE,11796
17
17
  mcp_server/tools/utility_tools.py,sha256=oxAXpdqtPeB5Ug5dvk54V504r-8v1AO4_px-sO6LFOw,3910
18
- quickcall_integrations-0.3.5.dist-info/METADATA,sha256=T5g_OrKJJtEX_xfntDbj8tlcSWTIDTDyT74uW4xf4Rg,7070
19
- quickcall_integrations-0.3.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
20
- quickcall_integrations-0.3.5.dist-info/entry_points.txt,sha256=kkcunmJUzncYvQ1rOR35V2LPm2HcFTKzdI2l3n7NwiM,66
21
- quickcall_integrations-0.3.5.dist-info/RECORD,,
18
+ quickcall_integrations-0.3.7.dist-info/METADATA,sha256=_76mJSIKsz6RU0o6uAmHYEXK9NetNnSMviVycU1T3M0,7070
19
+ quickcall_integrations-0.3.7.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
20
+ quickcall_integrations-0.3.7.dist-info/entry_points.txt,sha256=kkcunmJUzncYvQ1rOR35V2LPm2HcFTKzdI2l3n7NwiM,66
21
+ quickcall_integrations-0.3.7.dist-info/RECORD,,