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.
- {github_mcp_connector-0.7.0/src/github_mcp_connector.egg-info → github_mcp_connector-0.9.0}/PKG-INFO +17 -5
- {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/README.md +16 -4
- {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/pyproject.toml +1 -1
- {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/src/github_mcp/server.py +316 -0
- {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0/src/github_mcp_connector.egg-info}/PKG-INFO +17 -5
- {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/tests/test_server.py +229 -0
- {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/LICENSE +0 -0
- {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/setup.cfg +0 -0
- {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/src/github_mcp/__init__.py +0 -0
- {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/src/github_mcp/__main__.py +0 -0
- {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/src/github_mcp/client.py +0 -0
- {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/src/github_mcp/config.py +0 -0
- {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/src/github_mcp_connector.egg-info/SOURCES.txt +0 -0
- {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/src/github_mcp_connector.egg-info/dependency_links.txt +0 -0
- {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/src/github_mcp_connector.egg-info/entry_points.txt +0 -0
- {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/src/github_mcp_connector.egg-info/requires.txt +0 -0
- {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/src/github_mcp_connector.egg-info/top_level.txt +0 -0
- {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/tests/test_client.py +0 -0
- {github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0}/tests/test_config.py +0 -0
{github_mcp_connector-0.7.0/src/github_mcp_connector.egg-info → github_mcp_connector-0.9.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: github-mcp-connector
|
|
3
|
-
Version: 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
|
|
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
|
|
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
|
+
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
|
{github_mcp_connector-0.7.0 → github_mcp_connector-0.9.0/src/github_mcp_connector.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: github-mcp-connector
|
|
3
|
-
Version: 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
|
|
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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|