github-mcp-connector 0.7.0__tar.gz → 0.9.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 (19) hide show
  1. {github_mcp_connector-0.7.0/src/github_mcp_connector.egg-info → github_mcp_connector-0.9.0}/PKG-INFO +17 -5
  2. {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/README.md +16 -4
  3. {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/pyproject.toml +1 -1
  4. {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/src/github_mcp/server.py +316 -0
  5. {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0/src/github_mcp_connector.egg-info}/PKG-INFO +17 -5
  6. {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/tests/test_server.py +229 -0
  7. {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/LICENSE +0 -0
  8. {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/setup.cfg +0 -0
  9. {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/src/github_mcp/__init__.py +0 -0
  10. {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/src/github_mcp/__main__.py +0 -0
  11. {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/src/github_mcp/client.py +0 -0
  12. {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/src/github_mcp/config.py +0 -0
  13. {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/src/github_mcp_connector.egg-info/SOURCES.txt +0 -0
  14. {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/src/github_mcp_connector.egg-info/dependency_links.txt +0 -0
  15. {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/src/github_mcp_connector.egg-info/entry_points.txt +0 -0
  16. {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/src/github_mcp_connector.egg-info/requires.txt +0 -0
  17. {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/src/github_mcp_connector.egg-info/top_level.txt +0 -0
  18. {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/tests/test_client.py +0 -0
  19. {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/tests/test_config.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github-mcp-connector
3
- Version: 0.7.0
3
+ Version: 0.9.0
4
4
  Summary: A Model Context Protocol (MCP) connector for GitHub, for use with Claude.
5
5
  Author: winnerlose2026
6
6
  License: MIT
@@ -46,15 +46,15 @@ Enterprise Server.
46
46
 
47
47
  ## Features
48
48
 
49
- - 🔍 **Search & discover** repositories, issues/PRs, and code — plus find reusable repos (across everything your token can see, license/activity-aware) for what you're building
50
- - 📦 **Repositories** — metadata, branches, file contents, directory listings, your repo list
49
+ - 🔍 **Search & discover** repositories, issues, PRs, commits, code, and users — plus find reusable repos (across everything your token can see, license/activity-aware) for what you're building
50
+ - 📦 **Repositories** — metadata, branches, tags, file contents, directory listings, your repo list; create and fork repos
51
51
  - 🧾 **Commits** — history, single-commit detail, and diffs between refs
52
52
  - 🐛 **Issues** — list, read, create, comment, read comments, edit/close, label, assign
53
53
  - 🔀 **Pull requests** — list, read, diffs/files, comments, open, update, merge, and review
54
- - ⚙️ **Actions & releases** — list runs/jobs, read failed-job logs, re-run/dispatch workflows, commit statuses & check runs, list/create releases
54
+ - ⚙️ **Actions & releases** — list runs/jobs, read failed-job logs, re-run/dispatch/**schedule** workflows, commit statuses & check runs, list/create releases
55
55
  - 🔐 **Security** — secret-scanning, code-scanning, and Dependabot alerts
56
56
  - 🔔 **Notifications** — list and mark read across all your repos
57
- - ✍️ **Repo writes** — create branches, commit files, manage labels/assignees, create gists (gated by read-only mode)
57
+ - ✍️ **Repo writes** — create/fork repos, create branches, commit files, manage labels/assignees, create gists (gated by read-only mode)
58
58
  - 🔒 **Read-only mode** — flip one env var to disable every write tool
59
59
  - 🏢 **Enterprise-friendly** — set `GITHUB_API_URL` for GitHub Enterprise Server
60
60
 
@@ -92,7 +92,15 @@ Enterprise Server.
92
92
  | `list_code_scanning_alerts` | Code-scanning (CodeQL) alerts | |
93
93
  | `list_dependabot_alerts` | Dependabot vulnerability alerts | |
94
94
  | `search_issues` | Search issues and PRs across GitHub | |
95
+ | `search_pull_requests` | Search PRs across GitHub (auto-adds `is:pr`) | |
96
+ | `search_commits` | Search commits across GitHub | |
97
+ | `search_users` | Search users and organizations | |
95
98
  | `search_code` | Search code across GitHub | |
99
+ | `list_labels` | Labels defined in a repository | |
100
+ | `list_tags` | Git tags with their commit SHAs | |
101
+ | `get_tag` | Resolve a tag to its commit | |
102
+ | `list_gists` | The authenticated user's gists | |
103
+ | `get_latest_release` | A repository's latest published release | |
96
104
  | `find_reusable_repositories` | Find reusable repos for what you're building (license/activity-aware; public + accessible private) | |
97
105
  | `create_pull_request` | Open a new pull request (supports draft) | ✅ |
98
106
  | `update_pull_request` | Edit title/body/base, close/reopen a PR | ✅ |
@@ -101,6 +109,7 @@ Enterprise Server.
101
109
  | `add_pull_request_review_comment` | Inline comment on a PR diff line | ✅ |
102
110
  | `rerun_workflow_run` | Re-run a workflow run (or just failed jobs) | ✅ |
103
111
  | `trigger_workflow` | Dispatch a workflow_dispatch run | ✅ |
112
+ | `create_scheduled_workflow` | Commit a cron-scheduled workflow ("do X later") | ✅ |
104
113
  | `create_release` | Create a release (and its tag) | ✅ |
105
114
  | `create_issue` | Open a new issue | ✅ |
106
115
  | `update_issue` | Edit/close/reopen an issue | ✅ |
@@ -113,6 +122,9 @@ Enterprise Server.
113
122
  | `delete_branch` | Delete a branch | ✅ |
114
123
  | `create_or_update_file` | Commit a file (create or update) | ✅ |
115
124
  | `create_gist` | Create a (secret or public) gist | ✅ |
125
+ | `create_repository` | Create a new repo (user or org) | ✅ |
126
+ | `fork_repository` | Fork a repo to your account/an org | ✅ |
127
+ | `create_label` | Create a label in a repository | ✅ |
116
128
 
117
129
  Tools marked **Write** are disabled when `GITHUB_MCP_READ_ONLY` is set.
118
130
 
@@ -16,15 +16,15 @@ Enterprise Server.
16
16
 
17
17
  ## Features
18
18
 
19
- - 🔍 **Search & discover** repositories, issues/PRs, and code — plus find reusable repos (across everything your token can see, license/activity-aware) for what you're building
20
- - 📦 **Repositories** — metadata, branches, file contents, directory listings, your repo list
19
+ - 🔍 **Search & discover** repositories, issues, PRs, commits, code, and users — plus find reusable repos (across everything your token can see, license/activity-aware) for what you're building
20
+ - 📦 **Repositories** — metadata, branches, tags, file contents, directory listings, your repo list; create and fork repos
21
21
  - 🧾 **Commits** — history, single-commit detail, and diffs between refs
22
22
  - 🐛 **Issues** — list, read, create, comment, read comments, edit/close, label, assign
23
23
  - 🔀 **Pull requests** — list, read, diffs/files, comments, open, update, merge, and review
24
- - ⚙️ **Actions & releases** — list runs/jobs, read failed-job logs, re-run/dispatch workflows, commit statuses & check runs, list/create releases
24
+ - ⚙️ **Actions & releases** — list runs/jobs, read failed-job logs, re-run/dispatch/**schedule** workflows, commit statuses & check runs, list/create releases
25
25
  - 🔐 **Security** — secret-scanning, code-scanning, and Dependabot alerts
26
26
  - 🔔 **Notifications** — list and mark read across all your repos
27
- - ✍️ **Repo writes** — create branches, commit files, manage labels/assignees, create gists (gated by read-only mode)
27
+ - ✍️ **Repo writes** — create/fork repos, create branches, commit files, manage labels/assignees, create gists (gated by read-only mode)
28
28
  - 🔒 **Read-only mode** — flip one env var to disable every write tool
29
29
  - 🏢 **Enterprise-friendly** — set `GITHUB_API_URL` for GitHub Enterprise Server
30
30
 
@@ -62,7 +62,15 @@ Enterprise Server.
62
62
  | `list_code_scanning_alerts` | Code-scanning (CodeQL) alerts | |
63
63
  | `list_dependabot_alerts` | Dependabot vulnerability alerts | |
64
64
  | `search_issues` | Search issues and PRs across GitHub | |
65
+ | `search_pull_requests` | Search PRs across GitHub (auto-adds `is:pr`) | |
66
+ | `search_commits` | Search commits across GitHub | |
67
+ | `search_users` | Search users and organizations | |
65
68
  | `search_code` | Search code across GitHub | |
69
+ | `list_labels` | Labels defined in a repository | |
70
+ | `list_tags` | Git tags with their commit SHAs | |
71
+ | `get_tag` | Resolve a tag to its commit | |
72
+ | `list_gists` | The authenticated user's gists | |
73
+ | `get_latest_release` | A repository's latest published release | |
66
74
  | `find_reusable_repositories` | Find reusable repos for what you're building (license/activity-aware; public + accessible private) | |
67
75
  | `create_pull_request` | Open a new pull request (supports draft) | ✅ |
68
76
  | `update_pull_request` | Edit title/body/base, close/reopen a PR | ✅ |
@@ -71,6 +79,7 @@ Enterprise Server.
71
79
  | `add_pull_request_review_comment` | Inline comment on a PR diff line | ✅ |
72
80
  | `rerun_workflow_run` | Re-run a workflow run (or just failed jobs) | ✅ |
73
81
  | `trigger_workflow` | Dispatch a workflow_dispatch run | ✅ |
82
+ | `create_scheduled_workflow` | Commit a cron-scheduled workflow ("do X later") | ✅ |
74
83
  | `create_release` | Create a release (and its tag) | ✅ |
75
84
  | `create_issue` | Open a new issue | ✅ |
76
85
  | `update_issue` | Edit/close/reopen an issue | ✅ |
@@ -83,6 +92,9 @@ Enterprise Server.
83
92
  | `delete_branch` | Delete a branch | ✅ |
84
93
  | `create_or_update_file` | Commit a file (create or update) | ✅ |
85
94
  | `create_gist` | Create a (secret or public) gist | ✅ |
95
+ | `create_repository` | Create a new repo (user or org) | ✅ |
96
+ | `fork_repository` | Fork a repo to your account/an org | ✅ |
97
+ | `create_label` | Create a label in a repository | ✅ |
86
98
 
87
99
  Tools marked **Write** are disabled when `GITHUB_MCP_READ_ONLY` is set.
88
100
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "github-mcp-connector"
7
- version = "0.7.0"
7
+ version = "0.9.0"
8
8
  description = "A Model Context Protocol (MCP) connector for GitHub, for use with Claude."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -12,6 +12,7 @@ variable ``GITHUB_MCP_READ_ONLY`` is truthy.
12
12
  from __future__ import annotations
13
13
 
14
14
  import base64
15
+ import re
15
16
  from typing import Any
16
17
  from urllib.parse import quote
17
18
 
@@ -566,6 +567,142 @@ async def search_code(query: str, limit: int = 10) -> list[dict[str, Any]]:
566
567
  ]
567
568
 
568
569
 
570
+ @mcp.tool()
571
+ async def search_pull_requests(query: str, limit: int = 10) -> list[dict[str, Any]]:
572
+ """Search pull requests across GitHub (gh search prs).
573
+
574
+ Wraps GitHub's issue search with an `is:pr` qualifier added automatically, so
575
+ `query` only needs the PR-specific parts, e.g.
576
+ `repo:octocat/hello-world is:open review:required` or `author:octocat is:merged`.
577
+ Returns up to `limit` (max 50) pull requests.
578
+ """
579
+ _require_token()
580
+ limit = max(1, min(limit, 50))
581
+ full_query = query if "is:pr" in query else f"is:pr {query}"
582
+ async with GitHubClient(config) as gh:
583
+ result = await gh.get(
584
+ "/search/issues", params={"q": full_query, "per_page": limit}
585
+ )
586
+ return [_summarize_issue(item) for item in result.get("items", [])]
587
+
588
+
589
+ @mcp.tool()
590
+ async def search_commits(query: str, limit: int = 10) -> list[dict[str, Any]]:
591
+ """Search commits across GitHub (gh search commits).
592
+
593
+ Example queries: `repo:octocat/hello-world fix bug`, `author:octocat merge`,
594
+ `org:anthropics committer-date:>2024-01-01`. Returns up to `limit` (max 50)
595
+ commits.
596
+ """
597
+ _require_token()
598
+ limit = max(1, min(limit, 50))
599
+ async with GitHubClient(config) as gh:
600
+ result = await gh.get(
601
+ "/search/commits", params={"q": query, "per_page": limit}
602
+ )
603
+ return [_summarize_commit(item) for item in result.get("items", [])]
604
+
605
+
606
+ @mcp.tool()
607
+ async def search_users(query: str, limit: int = 10) -> list[dict[str, Any]]:
608
+ """Search for users and organizations on GitHub (gh search users).
609
+
610
+ Example queries: `octocat`, `location:berlin language:python`,
611
+ `type:org anthropic`. Returns up to `limit` (max 50) accounts.
612
+ """
613
+ _require_token()
614
+ limit = max(1, min(limit, 50))
615
+ async with GitHubClient(config) as gh:
616
+ result = await gh.get(
617
+ "/search/users", params={"q": query, "per_page": limit}
618
+ )
619
+ return [
620
+ {
621
+ "login": item.get("login"),
622
+ "type": item.get("type"),
623
+ "html_url": item.get("html_url"),
624
+ }
625
+ for item in result.get("items", [])
626
+ ]
627
+
628
+
629
+ @mcp.tool()
630
+ async def list_labels(owner: str, repo: str, limit: int = 50) -> list[dict[str, Any]]:
631
+ """List the labels defined in a repository (gh label list).
632
+
633
+ Returns each label's name, color (hex, no leading `#`), and description, up to
634
+ `limit` (max 100).
635
+ """
636
+ _require_token()
637
+ limit = max(1, min(limit, 100))
638
+ async with GitHubClient(config) as gh:
639
+ labels = await gh.get(
640
+ f"/repos/{owner}/{repo}/labels", params={"per_page": limit}
641
+ )
642
+ return [
643
+ {
644
+ "name": label.get("name"),
645
+ "color": label.get("color"),
646
+ "description": label.get("description"),
647
+ }
648
+ for label in labels
649
+ ]
650
+
651
+
652
+ @mcp.tool()
653
+ async def list_tags(owner: str, repo: str, limit: int = 30) -> list[dict[str, Any]]:
654
+ """List git tags in a repository, with the commit SHA each points at."""
655
+ _require_token()
656
+ limit = max(1, min(limit, 100))
657
+ async with GitHubClient(config) as gh:
658
+ tags = await gh.get(f"/repos/{owner}/{repo}/tags", params={"per_page": limit})
659
+ return [
660
+ {
661
+ "name": tag.get("name"),
662
+ "sha": (tag.get("commit") or {}).get("sha"),
663
+ }
664
+ for tag in tags
665
+ ]
666
+
667
+
668
+ @mcp.tool()
669
+ async def get_tag(owner: str, repo: str, tag: str) -> dict[str, Any]:
670
+ """Resolve a single tag to its commit (the commit the tag ref points at).
671
+
672
+ `tag` is the tag name (e.g. `v1.2.0`). Works for both lightweight and
673
+ annotated tags; returns the underlying commit's SHA, message, and author.
674
+ """
675
+ _require_token()
676
+ async with GitHubClient(config) as gh:
677
+ commit = await gh.get(f"/repos/{owner}/{repo}/commits/tags/{quote(tag)}")
678
+ summary = _summarize_commit(commit)
679
+ summary["tag"] = tag
680
+ return summary
681
+
682
+
683
+ @mcp.tool()
684
+ async def list_gists(limit: int = 30) -> list[dict[str, Any]]:
685
+ """List the authenticated user's gists, most recent first (gh gist list)."""
686
+ _require_token()
687
+ limit = max(1, min(limit, 100))
688
+ async with GitHubClient(config) as gh:
689
+ gists = await gh.get("/gists", params={"per_page": limit})
690
+ return [_summarize_gist(gist) for gist in gists]
691
+
692
+
693
+ @mcp.tool()
694
+ async def get_latest_release(owner: str, repo: str) -> dict[str, Any]:
695
+ """Get a repository's latest published release (gh release view --latest).
696
+
697
+ Returns the most recent non-draft, non-prerelease release. Raises a 404 if the
698
+ repository has no published releases.
699
+ """
700
+ _require_token()
701
+ async with GitHubClient(config) as gh:
702
+ release = await gh.get(f"/repos/{owner}/{repo}/releases/latest")
703
+ return _summarize_release(release)
704
+
705
+
569
706
  @mcp.tool()
570
707
  async def list_my_repositories(
571
708
  affiliation: str = "owner,collaborator,organization_member",
@@ -1628,6 +1765,185 @@ async def create_gist(
1628
1765
  return _summarize_gist(gist)
1629
1766
 
1630
1767
 
1768
+ @mcp.tool()
1769
+ async def create_scheduled_workflow(
1770
+ owner: str,
1771
+ repo: str,
1772
+ name: str,
1773
+ cron: str,
1774
+ run: str,
1775
+ runs_on: str = "ubuntu-latest",
1776
+ branch: str | None = None,
1777
+ filename: str | None = None,
1778
+ message: str | None = None,
1779
+ ) -> dict[str, Any]:
1780
+ """Schedule recurring work by committing a cron GitHub Actions workflow.
1781
+
1782
+ This is GitHub's native "do X later / on a schedule": it creates (or updates)
1783
+ a workflow file at `.github/workflows/<filename>` that runs `run` (a shell
1784
+ script) on the `cron` schedule, and also adds a `workflow_dispatch` trigger
1785
+ so you can run it on demand. `cron` is standard 5-field UTC cron
1786
+ (e.g. "0 9 * * 1" = 09:00 UTC every Monday).
1787
+
1788
+ IMPORTANT: GitHub only fires `schedule` triggers from a repository's DEFAULT
1789
+ branch, so `branch` defaults to it — committing to another branch will run on
1790
+ demand but never on the schedule. `filename` defaults to a slug of `name`.
1791
+ Disabled in read-only mode; requires write access (and the token needs the
1792
+ `workflow` scope to add workflow files).
1793
+ """
1794
+ _require_token()
1795
+ _require_write()
1796
+ if len(cron.split()) != 5:
1797
+ raise GitHubError(
1798
+ 400,
1799
+ "CRON",
1800
+ f"/repos/{owner}/{repo}",
1801
+ f"`cron` must be a 5-field cron expression (got: {cron!r}).",
1802
+ )
1803
+ slug = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") or "scheduled"
1804
+ fname = filename or f"{slug}.yml"
1805
+ path = f".github/workflows/{fname}"
1806
+ indented_run = "\n".join(" " + line for line in run.splitlines())
1807
+ content = (
1808
+ f"name: {name}\n"
1809
+ "on:\n"
1810
+ " schedule:\n"
1811
+ f' - cron: "{cron}"\n'
1812
+ " workflow_dispatch: {}\n"
1813
+ "permissions:\n"
1814
+ " contents: read\n"
1815
+ "jobs:\n"
1816
+ " scheduled:\n"
1817
+ f" runs-on: {runs_on}\n"
1818
+ " steps:\n"
1819
+ " - uses: actions/checkout@v4\n"
1820
+ " - name: Run scheduled command\n"
1821
+ " run: |\n"
1822
+ f"{indented_run}\n"
1823
+ )
1824
+ encoded = base64.b64encode(content.encode("utf-8")).decode("ascii")
1825
+ commit_message = message or f"Add scheduled workflow: {name}"
1826
+ async with GitHubClient(config) as gh:
1827
+ if branch is None:
1828
+ repo_data = await gh.get(f"/repos/{owner}/{repo}")
1829
+ branch = repo_data.get("default_branch")
1830
+ payload: dict[str, Any] = {
1831
+ "message": commit_message,
1832
+ "content": encoded,
1833
+ "branch": branch,
1834
+ }
1835
+ # If the workflow file already exists on this branch, update it.
1836
+ try:
1837
+ existing = await gh.get(
1838
+ f"/repos/{owner}/{repo}/contents/{path}", params={"ref": branch}
1839
+ )
1840
+ if isinstance(existing, dict) and existing.get("sha"):
1841
+ payload["sha"] = existing.get("sha")
1842
+ except GitHubError as exc:
1843
+ if exc.status_code != 404:
1844
+ raise
1845
+ result = await gh.put(f"/repos/{owner}/{repo}/contents/{path}", json=payload)
1846
+ commit = result.get("commit", {})
1847
+ content_info = result.get("content", {})
1848
+ return {
1849
+ "path": content_info.get("path", path),
1850
+ "branch": branch,
1851
+ "cron": cron,
1852
+ "runs_on": runs_on,
1853
+ "commit_sha": commit.get("sha"),
1854
+ "html_url": content_info.get("html_url"),
1855
+ "note": "Scheduled runs fire only from the default branch and start "
1856
+ "from the next cron tick; use trigger_workflow to run it now.",
1857
+ }
1858
+
1859
+
1860
+ @mcp.tool()
1861
+ async def create_repository(
1862
+ name: str,
1863
+ description: str | None = None,
1864
+ private: bool = False,
1865
+ org: str | None = None,
1866
+ auto_init: bool = False,
1867
+ homepage: str | None = None,
1868
+ ) -> dict[str, Any]:
1869
+ """Create a new repository (gh repo create).
1870
+
1871
+ Creates the repo under the authenticated user, or under `org` if given. Set
1872
+ `auto_init=True` to seed an initial commit with a README (so the repo has a
1873
+ default branch you can push to immediately). Disabled in read-only mode;
1874
+ requires a token with the `repo` scope.
1875
+ """
1876
+ _require_token()
1877
+ _require_write()
1878
+ payload: dict[str, Any] = {"name": name, "private": private}
1879
+ if description is not None:
1880
+ payload["description"] = description
1881
+ if homepage is not None:
1882
+ payload["homepage"] = homepage
1883
+ if auto_init:
1884
+ payload["auto_init"] = True
1885
+ path = f"/orgs/{org}/repos" if org else "/user/repos"
1886
+ async with GitHubClient(config) as gh:
1887
+ repo = await gh.post(path, json=payload)
1888
+ return _summarize_repo(repo)
1889
+
1890
+
1891
+ @mcp.tool()
1892
+ async def fork_repository(
1893
+ owner: str,
1894
+ repo: str,
1895
+ organization: str | None = None,
1896
+ default_branch_only: bool = False,
1897
+ ) -> dict[str, Any]:
1898
+ """Fork a repository to your account or an organization (gh repo fork).
1899
+
1900
+ Forks `owner/repo` into the authenticated account, or into `organization` if
1901
+ given. Set `default_branch_only=True` to fork just the default branch. The
1902
+ fork is created asynchronously by GitHub, so it may take a moment to become
1903
+ fully available. Disabled in read-only mode; requires write access.
1904
+ """
1905
+ _require_token()
1906
+ _require_write()
1907
+ payload: dict[str, Any] = {}
1908
+ if organization is not None:
1909
+ payload["organization"] = organization
1910
+ if default_branch_only:
1911
+ payload["default_branch_only"] = True
1912
+ async with GitHubClient(config) as gh:
1913
+ fork = await gh.post(f"/repos/{owner}/{repo}/forks", json=payload)
1914
+ return _summarize_repo(fork)
1915
+
1916
+
1917
+ @mcp.tool()
1918
+ async def create_label(
1919
+ owner: str,
1920
+ repo: str,
1921
+ name: str,
1922
+ color: str | None = None,
1923
+ description: str | None = None,
1924
+ ) -> dict[str, Any]:
1925
+ """Create a label in a repository (gh label create).
1926
+
1927
+ `color` is a 6-character hex code without the leading `#` (e.g. `d73a4a`); if
1928
+ omitted GitHub assigns a default. Disabled in read-only mode; requires write
1929
+ access.
1930
+ """
1931
+ _require_token()
1932
+ _require_write()
1933
+ payload: dict[str, Any] = {"name": name}
1934
+ if color is not None:
1935
+ payload["color"] = color.lstrip("#")
1936
+ if description is not None:
1937
+ payload["description"] = description
1938
+ async with GitHubClient(config) as gh:
1939
+ label = await gh.post(f"/repos/{owner}/{repo}/labels", json=payload)
1940
+ return {
1941
+ "name": label.get("name"),
1942
+ "color": label.get("color"),
1943
+ "description": label.get("description"),
1944
+ }
1945
+
1946
+
1631
1947
  def main(argv: list[str] | None = None) -> None:
1632
1948
  """Console entry point. Selects the transport from CLI args/env."""
1633
1949
  import argparse
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github-mcp-connector
3
- Version: 0.7.0
3
+ Version: 0.9.0
4
4
  Summary: A Model Context Protocol (MCP) connector for GitHub, for use with Claude.
5
5
  Author: winnerlose2026
6
6
  License: MIT
@@ -46,15 +46,15 @@ Enterprise Server.
46
46
 
47
47
  ## Features
48
48
 
49
- - 🔍 **Search & discover** repositories, issues/PRs, and code — plus find reusable repos (across everything your token can see, license/activity-aware) for what you're building
50
- - 📦 **Repositories** — metadata, branches, file contents, directory listings, your repo list
49
+ - 🔍 **Search & discover** repositories, issues, PRs, commits, code, and users — plus find reusable repos (across everything your token can see, license/activity-aware) for what you're building
50
+ - 📦 **Repositories** — metadata, branches, tags, file contents, directory listings, your repo list; create and fork repos
51
51
  - 🧾 **Commits** — history, single-commit detail, and diffs between refs
52
52
  - 🐛 **Issues** — list, read, create, comment, read comments, edit/close, label, assign
53
53
  - 🔀 **Pull requests** — list, read, diffs/files, comments, open, update, merge, and review
54
- - ⚙️ **Actions & releases** — list runs/jobs, read failed-job logs, re-run/dispatch workflows, commit statuses & check runs, list/create releases
54
+ - ⚙️ **Actions & releases** — list runs/jobs, read failed-job logs, re-run/dispatch/**schedule** workflows, commit statuses & check runs, list/create releases
55
55
  - 🔐 **Security** — secret-scanning, code-scanning, and Dependabot alerts
56
56
  - 🔔 **Notifications** — list and mark read across all your repos
57
- - ✍️ **Repo writes** — create branches, commit files, manage labels/assignees, create gists (gated by read-only mode)
57
+ - ✍️ **Repo writes** — create/fork repos, create branches, commit files, manage labels/assignees, create gists (gated by read-only mode)
58
58
  - 🔒 **Read-only mode** — flip one env var to disable every write tool
59
59
  - 🏢 **Enterprise-friendly** — set `GITHUB_API_URL` for GitHub Enterprise Server
60
60
 
@@ -92,7 +92,15 @@ Enterprise Server.
92
92
  | `list_code_scanning_alerts` | Code-scanning (CodeQL) alerts | |
93
93
  | `list_dependabot_alerts` | Dependabot vulnerability alerts | |
94
94
  | `search_issues` | Search issues and PRs across GitHub | |
95
+ | `search_pull_requests` | Search PRs across GitHub (auto-adds `is:pr`) | |
96
+ | `search_commits` | Search commits across GitHub | |
97
+ | `search_users` | Search users and organizations | |
95
98
  | `search_code` | Search code across GitHub | |
99
+ | `list_labels` | Labels defined in a repository | |
100
+ | `list_tags` | Git tags with their commit SHAs | |
101
+ | `get_tag` | Resolve a tag to its commit | |
102
+ | `list_gists` | The authenticated user's gists | |
103
+ | `get_latest_release` | A repository's latest published release | |
96
104
  | `find_reusable_repositories` | Find reusable repos for what you're building (license/activity-aware; public + accessible private) | |
97
105
  | `create_pull_request` | Open a new pull request (supports draft) | ✅ |
98
106
  | `update_pull_request` | Edit title/body/base, close/reopen a PR | ✅ |
@@ -101,6 +109,7 @@ Enterprise Server.
101
109
  | `add_pull_request_review_comment` | Inline comment on a PR diff line | ✅ |
102
110
  | `rerun_workflow_run` | Re-run a workflow run (or just failed jobs) | ✅ |
103
111
  | `trigger_workflow` | Dispatch a workflow_dispatch run | ✅ |
112
+ | `create_scheduled_workflow` | Commit a cron-scheduled workflow ("do X later") | ✅ |
104
113
  | `create_release` | Create a release (and its tag) | ✅ |
105
114
  | `create_issue` | Open a new issue | ✅ |
106
115
  | `update_issue` | Edit/close/reopen an issue | ✅ |
@@ -113,6 +122,9 @@ Enterprise Server.
113
122
  | `delete_branch` | Delete a branch | ✅ |
114
123
  | `create_or_update_file` | Commit a file (create or update) | ✅ |
115
124
  | `create_gist` | Create a (secret or public) gist | ✅ |
125
+ | `create_repository` | Create a new repo (user or org) | ✅ |
126
+ | `fork_repository` | Fork a repo to your account/an org | ✅ |
127
+ | `create_label` | Create a label in a repository | ✅ |
116
128
 
117
129
  Tools marked **Write** are disabled when `GITHUB_MCP_READ_ONLY` is set.
118
130
 
@@ -1056,3 +1056,232 @@ async def test_get_repository_tree_errors_without_default_branch(monkeypatch):
1056
1056
  with pytest.raises(GitHubError) as exc:
1057
1057
  await server.get_repository_tree("o", "r")
1058
1058
  assert exc.value.status_code == 404
1059
+
1060
+
1061
+ async def test_create_scheduled_workflow_read_only(monkeypatch):
1062
+ install_mock(monkeypatch, lambda r: httpx.Response(200, json={}),
1063
+ read_only=True)
1064
+ with pytest.raises(GitHubError) as exc:
1065
+ await server.create_scheduled_workflow("o", "r", "Nightly", "0 9 * * *",
1066
+ "echo hi")
1067
+ assert exc.value.status_code == 403
1068
+
1069
+
1070
+ async def test_create_scheduled_workflow_bad_cron(monkeypatch):
1071
+ install_mock(monkeypatch, lambda r: httpx.Response(200, json={}))
1072
+ with pytest.raises(GitHubError) as exc:
1073
+ await server.create_scheduled_workflow("o", "r", "x", "not cron", "echo")
1074
+ assert exc.value.status_code == 400
1075
+
1076
+
1077
+ async def test_create_scheduled_workflow_commits_yaml(monkeypatch):
1078
+ import base64
1079
+ captured = {}
1080
+
1081
+ def handler(request):
1082
+ import json
1083
+ p = request.url.path
1084
+ if request.method == "GET" and p == "/repos/o/r":
1085
+ return httpx.Response(200, json={"default_branch": "main"})
1086
+ if request.method == "GET" and p.startswith("/repos/o/r/contents/"):
1087
+ return httpx.Response(404, json={"message": "Not Found"})
1088
+ if request.method == "PUT" and p == "/repos/o/r/contents/.github/workflows/nightly-backup.yml":
1089
+ captured["body"] = json.loads(request.content)
1090
+ captured["path"] = p
1091
+ return httpx.Response(
1092
+ 201,
1093
+ json={"content": {"path": ".github/workflows/nightly-backup.yml",
1094
+ "sha": "s", "html_url": "u"},
1095
+ "commit": {"sha": "c"}},
1096
+ )
1097
+ raise AssertionError(f"unexpected {request.method} {p}")
1098
+
1099
+ install_mock(monkeypatch, handler)
1100
+ result = await server.create_scheduled_workflow(
1101
+ "o", "r", "Nightly Backup", "0 9 * * 1", "echo hi\nrun-backup.sh"
1102
+ )
1103
+ body = captured["body"]
1104
+ assert body["branch"] == "main" # defaulted to default branch
1105
+ assert "sha" not in body # new file
1106
+ yaml = base64.b64decode(body["content"]).decode()
1107
+ assert "schedule:" in yaml
1108
+ assert 'cron: "0 9 * * 1"' in yaml
1109
+ assert "workflow_dispatch: {}" in yaml
1110
+ assert "run-backup.sh" in yaml
1111
+ assert result["cron"] == "0 9 * * 1"
1112
+ assert result["branch"] == "main"
1113
+
1114
+
1115
+ # --- gh-CLI parity tools ----------------------------------------------------
1116
+
1117
+
1118
+ async def test_search_pull_requests_adds_is_pr(monkeypatch):
1119
+ captured = {}
1120
+
1121
+ def handler(request):
1122
+ assert request.url.path == "/search/issues"
1123
+ captured["q"] = request.url.params["q"]
1124
+ return httpx.Response(200, json={"items": [{"number": 5, "title": "t",
1125
+ "state": "open"}]})
1126
+
1127
+ install_mock(monkeypatch, handler)
1128
+ result = await server.search_pull_requests("repo:o/r is:open")
1129
+ assert "is:pr" in captured["q"]
1130
+ assert result[0]["number"] == 5
1131
+
1132
+
1133
+ async def test_search_pull_requests_no_double_is_pr(monkeypatch):
1134
+ captured = {}
1135
+
1136
+ def handler(request):
1137
+ captured["q"] = request.url.params["q"]
1138
+ return httpx.Response(200, json={"items": []})
1139
+
1140
+ install_mock(monkeypatch, handler)
1141
+ await server.search_pull_requests("is:pr author:me")
1142
+ assert captured["q"].count("is:pr") == 1
1143
+
1144
+
1145
+ async def test_search_commits(monkeypatch):
1146
+ def handler(request):
1147
+ assert request.url.path == "/search/commits"
1148
+ return httpx.Response(200, json={"items": [
1149
+ {"sha": "abc", "commit": {"message": "fix",
1150
+ "author": {"name": "JD", "date": "2024"}}}]})
1151
+
1152
+ install_mock(monkeypatch, handler)
1153
+ result = await server.search_commits("repo:o/r fix")
1154
+ assert result[0]["sha"] == "abc"
1155
+ assert result[0]["message"] == "fix"
1156
+
1157
+
1158
+ async def test_search_users(monkeypatch):
1159
+ def handler(request):
1160
+ assert request.url.path == "/search/users"
1161
+ return httpx.Response(200, json={"items": [
1162
+ {"login": "octocat", "type": "User", "html_url": "u", "extra": 1}]})
1163
+
1164
+ install_mock(monkeypatch, handler)
1165
+ result = await server.search_users("octocat")
1166
+ assert result[0]["login"] == "octocat"
1167
+ assert "extra" not in result[0]
1168
+
1169
+
1170
+ async def test_list_labels(monkeypatch):
1171
+ def handler(request):
1172
+ assert request.url.path == "/repos/o/r/labels"
1173
+ return httpx.Response(200, json=[
1174
+ {"name": "bug", "color": "d73a4a", "description": "broken"}])
1175
+
1176
+ install_mock(monkeypatch, handler)
1177
+ result = await server.list_labels("o", "r")
1178
+ assert result[0]["name"] == "bug"
1179
+ assert result[0]["color"] == "d73a4a"
1180
+
1181
+
1182
+ async def test_list_tags(monkeypatch):
1183
+ def handler(request):
1184
+ assert request.url.path == "/repos/o/r/tags"
1185
+ return httpx.Response(200, json=[
1186
+ {"name": "v1.0", "commit": {"sha": "deadbeef"}}])
1187
+
1188
+ install_mock(monkeypatch, handler)
1189
+ result = await server.list_tags("o", "r")
1190
+ assert result[0]["name"] == "v1.0"
1191
+ assert result[0]["sha"] == "deadbeef"
1192
+
1193
+
1194
+ async def test_get_tag_resolves_commit(monkeypatch):
1195
+ def handler(request):
1196
+ assert request.url.path == "/repos/o/r/commits/tags/v1.2.0"
1197
+ return httpx.Response(200, json={
1198
+ "sha": "abc", "commit": {"message": "release",
1199
+ "author": {"name": "JD", "date": "2024"}}})
1200
+
1201
+ install_mock(monkeypatch, handler)
1202
+ result = await server.get_tag("o", "r", "v1.2.0")
1203
+ assert result["tag"] == "v1.2.0"
1204
+ assert result["sha"] == "abc"
1205
+
1206
+
1207
+ async def test_list_gists(monkeypatch):
1208
+ def handler(request):
1209
+ assert request.url.path == "/gists"
1210
+ return httpx.Response(200, json=[
1211
+ {"id": "g1", "description": "d", "public": True,
1212
+ "files": {"a.txt": {}}, "html_url": "u"}])
1213
+
1214
+ install_mock(monkeypatch, handler)
1215
+ result = await server.list_gists()
1216
+ assert result[0]["id"] == "g1"
1217
+ assert result[0]["files"] == ["a.txt"]
1218
+
1219
+
1220
+ async def test_get_latest_release(monkeypatch):
1221
+ def handler(request):
1222
+ assert request.url.path == "/repos/o/r/releases/latest"
1223
+ return httpx.Response(200, json={"tag_name": "v2.0", "name": "v2.0",
1224
+ "draft": False, "prerelease": False})
1225
+
1226
+ install_mock(monkeypatch, handler)
1227
+ result = await server.get_latest_release("o", "r")
1228
+ assert result["tag_name"] == "v2.0"
1229
+
1230
+
1231
+ async def test_create_repository_read_only(monkeypatch):
1232
+ install_mock(monkeypatch, lambda r: httpx.Response(200, json={}),
1233
+ read_only=True)
1234
+ with pytest.raises(GitHubError) as exc:
1235
+ await server.create_repository("newrepo")
1236
+ assert exc.value.status_code == 403
1237
+
1238
+
1239
+ async def test_create_repository_user_and_org(monkeypatch):
1240
+ import json
1241
+ captured = {}
1242
+
1243
+ def handler(request):
1244
+ captured["path"] = request.url.path
1245
+ captured["body"] = json.loads(request.content)
1246
+ return httpx.Response(201, json={"full_name": "me/newrepo",
1247
+ "stargazers_count": 0})
1248
+
1249
+ install_mock(monkeypatch, handler)
1250
+ await server.create_repository("newrepo", description="d", auto_init=True)
1251
+ assert captured["path"] == "/user/repos"
1252
+ assert captured["body"]["auto_init"] is True
1253
+
1254
+ await server.create_repository("newrepo", org="acme")
1255
+ assert captured["path"] == "/orgs/acme/repos"
1256
+
1257
+
1258
+ async def test_fork_repository(monkeypatch):
1259
+ import json
1260
+ captured = {}
1261
+
1262
+ def handler(request):
1263
+ assert request.url.path == "/repos/o/r/forks"
1264
+ captured["body"] = json.loads(request.content)
1265
+ return httpx.Response(202, json={"full_name": "me/r",
1266
+ "stargazers_count": 0})
1267
+
1268
+ install_mock(monkeypatch, handler)
1269
+ result = await server.fork_repository("o", "r", organization="acme")
1270
+ assert captured["body"]["organization"] == "acme"
1271
+ assert result["full_name"] == "me/r"
1272
+
1273
+
1274
+ async def test_create_label_strips_hash(monkeypatch):
1275
+ import json
1276
+ captured = {}
1277
+
1278
+ def handler(request):
1279
+ assert request.url.path == "/repos/o/r/labels"
1280
+ captured["body"] = json.loads(request.content)
1281
+ return httpx.Response(201, json={"name": "bug", "color": "d73a4a",
1282
+ "description": "x"})
1283
+
1284
+ install_mock(monkeypatch, handler)
1285
+ result = await server.create_label("o", "r", "bug", color="#d73a4a")
1286
+ assert captured["body"]["color"] == "d73a4a" # leading # stripped
1287
+ assert result["name"] == "bug"