github-mcp-connector 0.2.0__tar.gz → 0.4.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.2.0/src/github_mcp_connector.egg-info → github_mcp_connector-0.4.0}/PKG-INFO +5 -2
  2. {github_mcp_connector-0.2.0 → github_mcp_connector-0.4.0}/README.md +4 -1
  3. {github_mcp_connector-0.2.0 → github_mcp_connector-0.4.0}/pyproject.toml +1 -1
  4. {github_mcp_connector-0.2.0 → github_mcp_connector-0.4.0}/src/github_mcp/client.py +4 -0
  5. {github_mcp_connector-0.2.0 → github_mcp_connector-0.4.0}/src/github_mcp/server.py +87 -0
  6. {github_mcp_connector-0.2.0 → github_mcp_connector-0.4.0/src/github_mcp_connector.egg-info}/PKG-INFO +5 -2
  7. {github_mcp_connector-0.2.0 → github_mcp_connector-0.4.0}/tests/test_server.py +115 -0
  8. {github_mcp_connector-0.2.0 → github_mcp_connector-0.4.0}/LICENSE +0 -0
  9. {github_mcp_connector-0.2.0 → github_mcp_connector-0.4.0}/setup.cfg +0 -0
  10. {github_mcp_connector-0.2.0 → github_mcp_connector-0.4.0}/src/github_mcp/__init__.py +0 -0
  11. {github_mcp_connector-0.2.0 → github_mcp_connector-0.4.0}/src/github_mcp/__main__.py +0 -0
  12. {github_mcp_connector-0.2.0 → github_mcp_connector-0.4.0}/src/github_mcp/config.py +0 -0
  13. {github_mcp_connector-0.2.0 → github_mcp_connector-0.4.0}/src/github_mcp_connector.egg-info/SOURCES.txt +0 -0
  14. {github_mcp_connector-0.2.0 → github_mcp_connector-0.4.0}/src/github_mcp_connector.egg-info/dependency_links.txt +0 -0
  15. {github_mcp_connector-0.2.0 → github_mcp_connector-0.4.0}/src/github_mcp_connector.egg-info/entry_points.txt +0 -0
  16. {github_mcp_connector-0.2.0 → github_mcp_connector-0.4.0}/src/github_mcp_connector.egg-info/requires.txt +0 -0
  17. {github_mcp_connector-0.2.0 → github_mcp_connector-0.4.0}/src/github_mcp_connector.egg-info/top_level.txt +0 -0
  18. {github_mcp_connector-0.2.0 → github_mcp_connector-0.4.0}/tests/test_client.py +0 -0
  19. {github_mcp_connector-0.2.0 → github_mcp_connector-0.4.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.2.0
3
+ Version: 0.4.0
4
4
  Summary: A Model Context Protocol (MCP) connector for GitHub, for use with Claude.
5
5
  Author: winnerlose2026
6
6
  License: MIT
@@ -47,7 +47,7 @@ Enterprise Server.
47
47
  - 📦 **Repositories** — metadata, branches, file contents, directory listings, your repo list
48
48
  - 🧾 **Commits** — history, single-commit detail, and diffs between refs
49
49
  - 🐛 **Issues** — list, read, create, comment, and edit/close
50
- - 🔀 **Pull requests** — list, read, fetch diffs and changed files
50
+ - 🔀 **Pull requests** — list, read, fetch diffs/changed files, open new PRs, and merge
51
51
  - ⚙️ **Actions & releases** — list workflow runs and releases
52
52
  - ✍️ **Repo writes** — create branches and commit files (gated by read-only mode)
53
53
  - 🔒 **Read-only mode** — flip one env var to disable every write tool
@@ -76,10 +76,13 @@ Enterprise Server.
76
76
  | `list_releases` | Releases for a repository | |
77
77
  | `search_issues` | Search issues and PRs across GitHub | |
78
78
  | `search_code` | Search code across GitHub | |
79
+ | `create_pull_request` | Open a new pull request (supports draft) | ✅ |
80
+ | `merge_pull_request` | Merge a PR (merge/squash/rebase) | ✅ |
79
81
  | `create_issue` | Open a new issue | ✅ |
80
82
  | `update_issue` | Edit/close/reopen an issue | ✅ |
81
83
  | `add_issue_comment` | Comment on an issue or PR | ✅ |
82
84
  | `create_branch` | Create a branch from a ref | ✅ |
85
+ | `delete_branch` | Delete a branch | ✅ |
83
86
  | `create_or_update_file` | Commit a file (create or update) | ✅ |
84
87
 
85
88
  Tools marked **Write** are disabled when `GITHUB_MCP_READ_ONLY` is set.
@@ -17,7 +17,7 @@ Enterprise Server.
17
17
  - 📦 **Repositories** — metadata, branches, file contents, directory listings, your repo list
18
18
  - 🧾 **Commits** — history, single-commit detail, and diffs between refs
19
19
  - 🐛 **Issues** — list, read, create, comment, and edit/close
20
- - 🔀 **Pull requests** — list, read, fetch diffs and changed files
20
+ - 🔀 **Pull requests** — list, read, fetch diffs/changed files, open new PRs, and merge
21
21
  - ⚙️ **Actions & releases** — list workflow runs and releases
22
22
  - ✍️ **Repo writes** — create branches and commit files (gated by read-only mode)
23
23
  - 🔒 **Read-only mode** — flip one env var to disable every write tool
@@ -46,10 +46,13 @@ Enterprise Server.
46
46
  | `list_releases` | Releases for a repository | |
47
47
  | `search_issues` | Search issues and PRs across GitHub | |
48
48
  | `search_code` | Search code across GitHub | |
49
+ | `create_pull_request` | Open a new pull request (supports draft) | ✅ |
50
+ | `merge_pull_request` | Merge a PR (merge/squash/rebase) | ✅ |
49
51
  | `create_issue` | Open a new issue | ✅ |
50
52
  | `update_issue` | Edit/close/reopen an issue | ✅ |
51
53
  | `add_issue_comment` | Comment on an issue or PR | ✅ |
52
54
  | `create_branch` | Create a branch from a ref | ✅ |
55
+ | `delete_branch` | Delete a branch | ✅ |
53
56
  | `create_or_update_file` | Commit a file (create or update) | ✅ |
54
57
 
55
58
  Tools marked **Write** are disabled when `GITHUB_MCP_READ_ONLY` is set.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "github-mcp-connector"
7
- version = "0.2.0"
7
+ version = "0.4.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"
@@ -136,6 +136,10 @@ class GitHubClient:
136
136
  return None
137
137
  return response.json()
138
138
 
139
+ async def delete(self, path: str) -> None:
140
+ # GitHub returns 204 No Content on a successful delete.
141
+ await self._request("DELETE", path)
142
+
139
143
 
140
144
  def _extract_error_detail(response: httpx.Response) -> str:
141
145
  """Pull the most useful human-readable error message out of a response."""
@@ -695,6 +695,22 @@ async def create_branch(
695
695
  }
696
696
 
697
697
 
698
+ @mcp.tool()
699
+ async def delete_branch(owner: str, repo: str, branch: str) -> dict[str, Any]:
700
+ """Delete a branch from a repository.
701
+
702
+ `branch` is the branch name (e.g. `feature-x`, not `refs/heads/feature-x`).
703
+ This permanently removes the ref; it cannot delete the repository's default
704
+ branch (GitHub rejects that). Disabled in read-only mode; requires a token
705
+ with write access.
706
+ """
707
+ _require_token()
708
+ _require_write()
709
+ async with GitHubClient(config) as gh:
710
+ await gh.delete(f"/repos/{owner}/{repo}/git/refs/heads/{branch}")
711
+ return {"deleted": True, "branch": branch}
712
+
713
+
698
714
  @mcp.tool()
699
715
  async def create_or_update_file(
700
716
  owner: str,
@@ -748,6 +764,77 @@ async def create_or_update_file(
748
764
  }
749
765
 
750
766
 
767
+ @mcp.tool()
768
+ async def create_pull_request(
769
+ owner: str,
770
+ repo: str,
771
+ title: str,
772
+ head: str,
773
+ base: str,
774
+ body: str | None = None,
775
+ draft: bool = False,
776
+ maintainer_can_modify: bool = True,
777
+ ) -> dict[str, Any]:
778
+ """Open a new pull request.
779
+
780
+ `head` is the branch with your changes (for a cross-fork PR use
781
+ `owner:branch`); `base` is the branch you want to merge into (e.g. `main`).
782
+ Set `draft=True` to open it as a draft. Disabled in read-only mode; requires
783
+ a token with write access to the repository.
784
+ """
785
+ _require_token()
786
+ _require_write()
787
+ payload: dict[str, Any] = {
788
+ "title": title,
789
+ "head": head,
790
+ "base": base,
791
+ "draft": draft,
792
+ "maintainer_can_modify": maintainer_can_modify,
793
+ }
794
+ if body is not None:
795
+ payload["body"] = body
796
+ async with GitHubClient(config) as gh:
797
+ pull = await gh.post(f"/repos/{owner}/{repo}/pulls", json=payload)
798
+ summary = _summarize_pull(pull)
799
+ summary["body"] = pull.get("body")
800
+ return summary
801
+
802
+
803
+ @mcp.tool()
804
+ async def merge_pull_request(
805
+ owner: str,
806
+ repo: str,
807
+ pull_number: int,
808
+ merge_method: str = "merge",
809
+ commit_title: str | None = None,
810
+ commit_message: str | None = None,
811
+ ) -> dict[str, Any]:
812
+ """Merge a pull request.
813
+
814
+ `merge_method` is one of `merge` (merge commit), `squash`, or `rebase`.
815
+ `commit_title`/`commit_message` optionally override the merge commit text
816
+ (ignored for `rebase`). Fails if the PR isn't mergeable (conflicts, failing
817
+ required checks, or branch protection). Disabled in read-only mode; requires
818
+ a token with write access.
819
+ """
820
+ _require_token()
821
+ _require_write()
822
+ payload: dict[str, Any] = {"merge_method": merge_method}
823
+ if commit_title is not None:
824
+ payload["commit_title"] = commit_title
825
+ if commit_message is not None:
826
+ payload["commit_message"] = commit_message
827
+ async with GitHubClient(config) as gh:
828
+ result = await gh.put(
829
+ f"/repos/{owner}/{repo}/pulls/{pull_number}/merge", json=payload
830
+ )
831
+ return {
832
+ "merged": result.get("merged"),
833
+ "sha": result.get("sha"),
834
+ "message": result.get("message"),
835
+ }
836
+
837
+
751
838
  def main(argv: list[str] | None = None) -> None:
752
839
  """Console entry point. Selects the transport from CLI args/env."""
753
840
  import argparse
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github-mcp-connector
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: A Model Context Protocol (MCP) connector for GitHub, for use with Claude.
5
5
  Author: winnerlose2026
6
6
  License: MIT
@@ -47,7 +47,7 @@ Enterprise Server.
47
47
  - 📦 **Repositories** — metadata, branches, file contents, directory listings, your repo list
48
48
  - 🧾 **Commits** — history, single-commit detail, and diffs between refs
49
49
  - 🐛 **Issues** — list, read, create, comment, and edit/close
50
- - 🔀 **Pull requests** — list, read, fetch diffs and changed files
50
+ - 🔀 **Pull requests** — list, read, fetch diffs/changed files, open new PRs, and merge
51
51
  - ⚙️ **Actions & releases** — list workflow runs and releases
52
52
  - ✍️ **Repo writes** — create branches and commit files (gated by read-only mode)
53
53
  - 🔒 **Read-only mode** — flip one env var to disable every write tool
@@ -76,10 +76,13 @@ Enterprise Server.
76
76
  | `list_releases` | Releases for a repository | |
77
77
  | `search_issues` | Search issues and PRs across GitHub | |
78
78
  | `search_code` | Search code across GitHub | |
79
+ | `create_pull_request` | Open a new pull request (supports draft) | ✅ |
80
+ | `merge_pull_request` | Merge a PR (merge/squash/rebase) | ✅ |
79
81
  | `create_issue` | Open a new issue | ✅ |
80
82
  | `update_issue` | Edit/close/reopen an issue | ✅ |
81
83
  | `add_issue_comment` | Comment on an issue or PR | ✅ |
82
84
  | `create_branch` | Create a branch from a ref | ✅ |
85
+ | `delete_branch` | Delete a branch | ✅ |
83
86
  | `create_or_update_file` | Commit a file (create or update) | ✅ |
84
87
 
85
88
  Tools marked **Write** are disabled when `GITHUB_MCP_READ_ONLY` is set.
@@ -379,3 +379,118 @@ async def test_create_or_update_file_creates_new_on_404(monkeypatch):
379
379
  result = await server.create_or_update_file("o", "r", "new.md", "x", "add")
380
380
  assert "sha" not in captured["body"] # no sha -> create
381
381
  assert result["created"] is True
382
+
383
+
384
+ async def test_delete_branch_blocked_in_read_only(monkeypatch):
385
+ install_mock(monkeypatch, lambda r: httpx.Response(204), read_only=True)
386
+ with pytest.raises(GitHubError) as exc:
387
+ await server.delete_branch("o", "r", "feature")
388
+ assert exc.value.status_code == 403
389
+
390
+
391
+ async def test_delete_branch_calls_correct_ref(monkeypatch):
392
+ captured = {}
393
+
394
+ def handler(request):
395
+ captured["method"] = request.method
396
+ captured["path"] = request.url.path
397
+ return httpx.Response(204)
398
+
399
+ install_mock(monkeypatch, handler)
400
+ result = await server.delete_branch("o", "r", "feature-x")
401
+ assert captured["method"] == "DELETE"
402
+ assert captured["path"] == "/repos/o/r/git/refs/heads/feature-x"
403
+ assert result == {"deleted": True, "branch": "feature-x"}
404
+
405
+
406
+ async def test_delete_branch_surfaces_api_error(monkeypatch):
407
+ def handler(request):
408
+ return httpx.Response(422, json={"message": "Reference does not exist"})
409
+
410
+ install_mock(monkeypatch, handler)
411
+ with pytest.raises(GitHubError) as exc:
412
+ await server.delete_branch("o", "r", "missing")
413
+ assert exc.value.status_code == 422
414
+
415
+
416
+ async def test_create_pull_request_blocked_in_read_only(monkeypatch):
417
+ install_mock(monkeypatch, lambda r: httpx.Response(200, json={}),
418
+ read_only=True)
419
+ with pytest.raises(GitHubError) as exc:
420
+ await server.create_pull_request("o", "r", "t", "feature", "main")
421
+ assert exc.value.status_code == 403
422
+
423
+
424
+ async def test_create_pull_request_posts_payload(monkeypatch):
425
+ captured = {}
426
+
427
+ def handler(request):
428
+ import json
429
+ assert request.method == "POST"
430
+ assert request.url.path == "/repos/o/r/pulls"
431
+ captured["body"] = json.loads(request.content)
432
+ return httpx.Response(
433
+ 201,
434
+ json={
435
+ "number": 12,
436
+ "title": "t",
437
+ "state": "open",
438
+ "draft": True,
439
+ "body": "desc",
440
+ "head": {"ref": "feature"},
441
+ "base": {"ref": "main"},
442
+ },
443
+ )
444
+
445
+ install_mock(monkeypatch, handler)
446
+ result = await server.create_pull_request(
447
+ "o", "r", "t", "feature", "main", body="desc", draft=True
448
+ )
449
+ assert captured["body"]["title"] == "t"
450
+ assert captured["body"]["head"] == "feature"
451
+ assert captured["body"]["base"] == "main"
452
+ assert captured["body"]["draft"] is True
453
+ assert captured["body"]["body"] == "desc"
454
+ assert result["number"] == 12
455
+ assert result["draft"] is True
456
+ assert result["body"] == "desc"
457
+
458
+
459
+ async def test_merge_pull_request_blocked_in_read_only(monkeypatch):
460
+ install_mock(monkeypatch, lambda r: httpx.Response(200, json={}),
461
+ read_only=True)
462
+ with pytest.raises(GitHubError) as exc:
463
+ await server.merge_pull_request("o", "r", 1)
464
+ assert exc.value.status_code == 403
465
+
466
+
467
+ async def test_merge_pull_request_puts_payload(monkeypatch):
468
+ captured = {}
469
+
470
+ def handler(request):
471
+ import json
472
+ assert request.method == "PUT"
473
+ assert request.url.path == "/repos/o/r/pulls/5/merge"
474
+ captured["body"] = json.loads(request.content)
475
+ return httpx.Response(
476
+ 200, json={"merged": True, "sha": "abc", "message": "Pull Request successfully merged"}
477
+ )
478
+
479
+ install_mock(monkeypatch, handler)
480
+ result = await server.merge_pull_request(
481
+ "o", "r", 5, merge_method="squash", commit_title="T"
482
+ )
483
+ assert captured["body"]["merge_method"] == "squash"
484
+ assert captured["body"]["commit_title"] == "T"
485
+ assert result["merged"] is True
486
+ assert result["sha"] == "abc"
487
+
488
+
489
+ async def test_merge_pull_request_surfaces_conflict(monkeypatch):
490
+ def handler(request):
491
+ return httpx.Response(405, json={"message": "Pull Request is not mergeable"})
492
+
493
+ install_mock(monkeypatch, handler)
494
+ with pytest.raises(GitHubError) as exc:
495
+ await server.merge_pull_request("o", "r", 5)
496
+ assert exc.value.status_code == 405