Spaces:
Build error
Build error
Gemini
commited on
Commit
·
fce10de
0
Parent(s):
Initial commit
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .dockerignore +11 -0
- .gitattributes +35 -0
- .github/CODEOWNERS +1 -0
- .github/ISSUE_TEMPLATE/bug_report.md +30 -0
- .github/ISSUE_TEMPLATE/feature_request.md +24 -0
- .github/dependabot.yml +19 -0
- .github/licenses.tmpl +13 -0
- .github/pull_request_template.md +11 -0
- .github/workflows/close-inactive-issues.yml +28 -0
- .github/workflows/code-scanning.yml +82 -0
- .github/workflows/docker-publish.yml +122 -0
- .github/workflows/docs-check.yml +47 -0
- .github/workflows/go.yml +32 -0
- .github/workflows/goreleaser.yml +45 -0
- .github/workflows/license-check.yml +21 -0
- .github/workflows/lint.yml +23 -0
- .gitignore +17 -0
- .golangci.yml +37 -0
- .goreleaser.yaml +44 -0
- .vscode/launch.json +28 -0
- CODE_OF_CONDUCT.md +128 -0
- CONTRIBUTING.md +56 -0
- Dockerfile +28 -0
- LICENSE +21 -0
- README.md +1142 -0
- SECURITY.md +31 -0
- SUPPORT.md +13 -0
- cmd/github-mcp-server/generate_docs.go +354 -0
- cmd/github-mcp-server/main.go +116 -0
- cmd/mcpcurl/README.md +150 -0
- cmd/mcpcurl/main.go +466 -0
- docs/error-handling.md +125 -0
- docs/host-integration.md +193 -0
- docs/installation-guides/README.md +95 -0
- docs/installation-guides/install-claude.md +167 -0
- docs/installation-guides/install-cursor.md +114 -0
- docs/installation-guides/install-other-copilot-ides.md +268 -0
- docs/installation-guides/install-windsurf.md +107 -0
- docs/policies-and-governance.md +216 -0
- docs/remote-server.md +54 -0
- docs/testing.md +34 -0
- e2e/README.md +96 -0
- e2e/e2e_test.go +1626 -0
- go.mod +57 -0
- go.sum +124 -0
- internal/ghmcp/server.go +423 -0
- internal/githubv4mock/githubv4mock.go +218 -0
- internal/githubv4mock/local_round_tripper.go +44 -0
- internal/githubv4mock/objects_are_equal_values.go +113 -0
- internal/githubv4mock/objects_are_equal_values_test.go +73 -0
.dockerignore
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.github
|
| 2 |
+
.vscode
|
| 3 |
+
script
|
| 4 |
+
third-party
|
| 5 |
+
.dockerignore
|
| 6 |
+
.gitignore
|
| 7 |
+
**/*.yml
|
| 8 |
+
**/*.yaml
|
| 9 |
+
**/*.md
|
| 10 |
+
**/*_test.go
|
| 11 |
+
LICENSE
|
.gitattributes
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
.github/CODEOWNERS
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
* @github/github-mcp-server
|
.github/ISSUE_TEMPLATE/bug_report.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: "\U0001F41B Bug report"
|
| 3 |
+
about: Report a bug or unexpected behavior while using GitHub MCP Server
|
| 4 |
+
title: ''
|
| 5 |
+
labels: bug
|
| 6 |
+
assignees: ''
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
### Describe the bug
|
| 11 |
+
|
| 12 |
+
A clear and concise description of what the bug is.
|
| 13 |
+
|
| 14 |
+
### Affected version
|
| 15 |
+
|
| 16 |
+
Please run ` docker run -i --rm ghcr.io/github/github-mcp-server ./github-mcp-server --version` and paste the output below
|
| 17 |
+
|
| 18 |
+
### Steps to reproduce the behavior
|
| 19 |
+
|
| 20 |
+
1. Type this '...'
|
| 21 |
+
2. View the output '....'
|
| 22 |
+
3. See error
|
| 23 |
+
|
| 24 |
+
### Expected vs actual behavior
|
| 25 |
+
|
| 26 |
+
A clear and concise description of what you expected to happen and what actually happened.
|
| 27 |
+
|
| 28 |
+
### Logs
|
| 29 |
+
|
| 30 |
+
Paste any available logs. Redact if needed.
|
.github/ISSUE_TEMPLATE/feature_request.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: "⭐ Submit a feature request"
|
| 3 |
+
about: Surface a feature or problem that you think should be solved
|
| 4 |
+
title: ''
|
| 5 |
+
labels: enhancement
|
| 6 |
+
assignees: ''
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
### Describe the feature or problem you’d like to solve
|
| 11 |
+
|
| 12 |
+
A clear and concise description of what the feature or problem is.
|
| 13 |
+
|
| 14 |
+
### Proposed solution
|
| 15 |
+
|
| 16 |
+
How will it benefit GitHub MCP Server and its users?
|
| 17 |
+
|
| 18 |
+
### Example prompts or workflows (for tools/toolsets only)
|
| 19 |
+
|
| 20 |
+
If it's a new tool or improvement, share 3–5 example prompts or workflows it would enable. Just enough detail to show the value. Clear, valuable use cases are more likely to get approved.
|
| 21 |
+
|
| 22 |
+
### Additional context
|
| 23 |
+
|
| 24 |
+
Add any other context like screenshots or mockups are helpful, if applicable.
|
.github/dependabot.yml
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# To get started with Dependabot version updates, you'll need to specify which
|
| 2 |
+
# package ecosystems to update and where the package manifests are located.
|
| 3 |
+
# Please see the documentation for all configuration options:
|
| 4 |
+
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
| 5 |
+
|
| 6 |
+
version: 2
|
| 7 |
+
updates:
|
| 8 |
+
- package-ecosystem: "gomod"
|
| 9 |
+
directory: "/"
|
| 10 |
+
schedule:
|
| 11 |
+
interval: "weekly"
|
| 12 |
+
- package-ecosystem: "docker"
|
| 13 |
+
directory: "/"
|
| 14 |
+
schedule:
|
| 15 |
+
interval: "weekly"
|
| 16 |
+
- package-ecosystem: "github-actions"
|
| 17 |
+
directory: "/"
|
| 18 |
+
schedule:
|
| 19 |
+
interval: "weekly"
|
.github/licenses.tmpl
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# GitHub MCP Server dependencies
|
| 2 |
+
|
| 3 |
+
The following open source dependencies are used to build the [github/github-mcp-server][] GitHub Model Context Protocol Server.
|
| 4 |
+
|
| 5 |
+
## Go Packages
|
| 6 |
+
|
| 7 |
+
Some packages may only be included on certain architectures or operating systems.
|
| 8 |
+
|
| 9 |
+
{{ range . }}
|
| 10 |
+
- [{{.Name}}](https://pkg.go.dev/{{.Name}}) ([{{.LicenseName}}]({{.LicenseURL}}))
|
| 11 |
+
{{- end }}
|
| 12 |
+
|
| 13 |
+
[github/github-mcp-server]: https://github.com/github/github-mcp-server
|
.github/pull_request_template.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!--
|
| 2 |
+
Thank you for contributing to GitHub MCP Server!
|
| 3 |
+
Please reference an existing issue: `Closes #NUMBER`
|
| 4 |
+
|
| 5 |
+
Screenshots or videos of changed behavior is incredibly helpful and always appreciated.
|
| 6 |
+
Consider addressing the following:
|
| 7 |
+
- Tradeoffs: List tradeoffs you made to take on or pay down tech debt.
|
| 8 |
+
- Alternatives: Describe alternative approaches you considered and why you discarded them.
|
| 9 |
+
-->
|
| 10 |
+
|
| 11 |
+
Closes:
|
.github/workflows/close-inactive-issues.yml
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Close inactive issues
|
| 2 |
+
on:
|
| 3 |
+
schedule:
|
| 4 |
+
- cron: "30 8 * * *"
|
| 5 |
+
|
| 6 |
+
jobs:
|
| 7 |
+
close-issues:
|
| 8 |
+
runs-on: ubuntu-latest
|
| 9 |
+
env:
|
| 10 |
+
PR_DAYS_BEFORE_STALE: 60
|
| 11 |
+
PR_DAYS_BEFORE_CLOSE: 120
|
| 12 |
+
PR_STALE_LABEL: stale
|
| 13 |
+
permissions:
|
| 14 |
+
issues: write
|
| 15 |
+
pull-requests: write
|
| 16 |
+
steps:
|
| 17 |
+
- uses: actions/stale@v9
|
| 18 |
+
with:
|
| 19 |
+
days-before-issue-stale: ${{ env.PR_DAYS_BEFORE_STALE }}
|
| 20 |
+
days-before-issue-close: ${{ env.PR_DAYS_BEFORE_CLOSE }}
|
| 21 |
+
stale-issue-label: ${{ env.PR_STALE_LABEL }}
|
| 22 |
+
stale-issue-message: "This issue is stale because it has been open for ${{ env.PR_DAYS_BEFORE_STALE }} days with no activity. Leave a comment to avoid closing this issue in ${{ env.PR_DAYS_BEFORE_CLOSE }} days."
|
| 23 |
+
close-issue-message: "This issue was closed because it has been inactive for ${{ env.PR_DAYS_BEFORE_CLOSE }} days since being marked as stale."
|
| 24 |
+
days-before-pr-stale: -1
|
| 25 |
+
days-before-pr-close: -1
|
| 26 |
+
# Start with the oldest items first
|
| 27 |
+
ascending: true
|
| 28 |
+
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
.github/workflows/code-scanning.yml
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: "CodeQL"
|
| 2 |
+
run-name: ${{ github.event.inputs.code_scanning_run_name }}
|
| 3 |
+
on: [push, pull_request, workflow_dispatch]
|
| 4 |
+
|
| 5 |
+
concurrency:
|
| 6 |
+
group: ${{ github.workflow }}-${{ github.ref }}
|
| 7 |
+
cancel-in-progress: true
|
| 8 |
+
|
| 9 |
+
env:
|
| 10 |
+
CODE_SCANNING_REF: ${{ github.event.inputs.code_scanning_ref }}
|
| 11 |
+
CODE_SCANNING_BASE_BRANCH: ${{ github.event.inputs.code_scanning_base_branch }}
|
| 12 |
+
CODE_SCANNING_IS_ANALYZING_DEFAULT_BRANCH: ${{ github.event.inputs.code_scanning_is_analyzing_default_branch }}
|
| 13 |
+
|
| 14 |
+
jobs:
|
| 15 |
+
analyze:
|
| 16 |
+
name: Analyze (${{ matrix.language }})
|
| 17 |
+
runs-on: ${{ fromJSON(matrix.runner) }}
|
| 18 |
+
permissions:
|
| 19 |
+
actions: read
|
| 20 |
+
contents: read
|
| 21 |
+
packages: read
|
| 22 |
+
security-events: write
|
| 23 |
+
continue-on-error: false
|
| 24 |
+
strategy:
|
| 25 |
+
fail-fast: false
|
| 26 |
+
matrix:
|
| 27 |
+
include:
|
| 28 |
+
- language: actions
|
| 29 |
+
category: /language:actions
|
| 30 |
+
build-mode: none
|
| 31 |
+
runner: '["ubuntu-22.04"]'
|
| 32 |
+
- language: go
|
| 33 |
+
category: /language:go
|
| 34 |
+
build-mode: autobuild
|
| 35 |
+
runner: '["ubuntu-22.04"]'
|
| 36 |
+
steps:
|
| 37 |
+
- name: Checkout repository
|
| 38 |
+
uses: actions/checkout@v4
|
| 39 |
+
|
| 40 |
+
- name: Initialize CodeQL
|
| 41 |
+
uses: github/codeql-action/init@v3
|
| 42 |
+
with:
|
| 43 |
+
languages: ${{ matrix.language }}
|
| 44 |
+
build-mode: ${{ matrix.build-mode }}
|
| 45 |
+
dependency-caching: ${{ runner.environment == 'github-hosted' }}
|
| 46 |
+
queries: "" # Default query suite
|
| 47 |
+
packs: github/ccr-${{ matrix.language }}-queries
|
| 48 |
+
config: |
|
| 49 |
+
default-setup:
|
| 50 |
+
org:
|
| 51 |
+
model-packs: [ ${{ github.event.inputs.code_scanning_codeql_packs }} ]
|
| 52 |
+
threat-models: [ ]
|
| 53 |
+
- name: Setup proxy for registries
|
| 54 |
+
id: proxy
|
| 55 |
+
uses: github/codeql-action/start-proxy@v3
|
| 56 |
+
with:
|
| 57 |
+
registries_credentials: ${{ secrets.GITHUB_REGISTRIES_PROXY }}
|
| 58 |
+
language: ${{ matrix.language }}
|
| 59 |
+
|
| 60 |
+
- name: Configure
|
| 61 |
+
uses: github/codeql-action/resolve-environment@v3
|
| 62 |
+
id: resolve-environment
|
| 63 |
+
with:
|
| 64 |
+
language: ${{ matrix.language }}
|
| 65 |
+
- name: Setup Go
|
| 66 |
+
uses: actions/setup-go@v5
|
| 67 |
+
if: matrix.language == 'go' && fromJSON(steps.resolve-environment.outputs.environment).configuration.go.version
|
| 68 |
+
with:
|
| 69 |
+
go-version: ${{ fromJSON(steps.resolve-environment.outputs.environment).configuration.go.version }}
|
| 70 |
+
cache: false
|
| 71 |
+
|
| 72 |
+
- name: Autobuild
|
| 73 |
+
uses: github/codeql-action/autobuild@v3
|
| 74 |
+
|
| 75 |
+
- name: Perform CodeQL Analysis
|
| 76 |
+
uses: github/codeql-action/analyze@v3
|
| 77 |
+
env:
|
| 78 |
+
CODEQL_PROXY_HOST: ${{ steps.proxy.outputs.proxy_host }}
|
| 79 |
+
CODEQL_PROXY_PORT: ${{ steps.proxy.outputs.proxy_port }}
|
| 80 |
+
CODEQL_PROXY_CA_CERTIFICATE: ${{ steps.proxy.outputs.proxy_ca_certificate }}
|
| 81 |
+
with:
|
| 82 |
+
category: ${{ matrix.category }}
|
.github/workflows/docker-publish.yml
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Docker
|
| 2 |
+
|
| 3 |
+
# This workflow uses actions that are not certified by GitHub.
|
| 4 |
+
# They are provided by a third-party and are governed by
|
| 5 |
+
# separate terms of service, privacy policy, and support
|
| 6 |
+
# documentation.
|
| 7 |
+
|
| 8 |
+
on:
|
| 9 |
+
schedule:
|
| 10 |
+
- cron: "27 0 * * *"
|
| 11 |
+
push:
|
| 12 |
+
branches: ["main", "next"]
|
| 13 |
+
# Publish semver tags as releases.
|
| 14 |
+
tags: ["v*.*.*"]
|
| 15 |
+
pull_request:
|
| 16 |
+
branches: ["main", "next"]
|
| 17 |
+
|
| 18 |
+
env:
|
| 19 |
+
# Use docker.io for Docker Hub if empty
|
| 20 |
+
REGISTRY: ghcr.io
|
| 21 |
+
# github.repository as <account>/<repo>
|
| 22 |
+
IMAGE_NAME: ${{ github.repository }}
|
| 23 |
+
|
| 24 |
+
jobs:
|
| 25 |
+
build:
|
| 26 |
+
runs-on: ubuntu-latest-xl
|
| 27 |
+
permissions:
|
| 28 |
+
contents: read
|
| 29 |
+
packages: write
|
| 30 |
+
# This is used to complete the identity challenge
|
| 31 |
+
# with sigstore/fulcio when running outside of PRs.
|
| 32 |
+
id-token: write
|
| 33 |
+
|
| 34 |
+
steps:
|
| 35 |
+
- name: Checkout repository
|
| 36 |
+
uses: actions/checkout@v4
|
| 37 |
+
|
| 38 |
+
# Install the cosign tool except on PR
|
| 39 |
+
# https://github.com/sigstore/cosign-installer
|
| 40 |
+
- name: Install cosign
|
| 41 |
+
if: github.event_name != 'pull_request'
|
| 42 |
+
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 #v3.5.0
|
| 43 |
+
with:
|
| 44 |
+
cosign-release: "v2.2.4"
|
| 45 |
+
|
| 46 |
+
# Set up BuildKit Docker container builder to be able to build
|
| 47 |
+
# multi-platform images and export cache
|
| 48 |
+
# https://github.com/docker/setup-buildx-action
|
| 49 |
+
- name: Set up Docker Buildx
|
| 50 |
+
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
|
| 51 |
+
|
| 52 |
+
# Login against a Docker registry except on PR
|
| 53 |
+
# https://github.com/docker/login-action
|
| 54 |
+
- name: Log into registry ${{ env.REGISTRY }}
|
| 55 |
+
if: github.event_name != 'pull_request'
|
| 56 |
+
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
|
| 57 |
+
with:
|
| 58 |
+
registry: ${{ env.REGISTRY }}
|
| 59 |
+
username: ${{ github.actor }}
|
| 60 |
+
password: ${{ secrets.GITHUB_TOKEN }}
|
| 61 |
+
|
| 62 |
+
# Extract metadata (tags, labels) for Docker
|
| 63 |
+
# https://github.com/docker/metadata-action
|
| 64 |
+
- name: Extract Docker metadata
|
| 65 |
+
id: meta
|
| 66 |
+
uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0
|
| 67 |
+
with:
|
| 68 |
+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
| 69 |
+
tags: |
|
| 70 |
+
type=schedule
|
| 71 |
+
type=ref,event=branch
|
| 72 |
+
type=ref,event=tag
|
| 73 |
+
type=ref,event=pr
|
| 74 |
+
type=semver,pattern={{version}}
|
| 75 |
+
type=semver,pattern={{major}}.{{minor}}
|
| 76 |
+
type=semver,pattern={{major}}
|
| 77 |
+
type=sha
|
| 78 |
+
type=edge
|
| 79 |
+
# Custom rule to prevent pre-releases from getting latest tag
|
| 80 |
+
type=raw,value=latest,enable=${{ github.ref_type == 'tag' && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-') }}
|
| 81 |
+
|
| 82 |
+
- name: Go Build Cache for Docker
|
| 83 |
+
uses: actions/cache@v4
|
| 84 |
+
with:
|
| 85 |
+
path: go-build-cache
|
| 86 |
+
key: ${{ runner.os }}-go-build-cache-${{ hashFiles('**/go.sum') }}
|
| 87 |
+
|
| 88 |
+
- name: Inject go-build-cache
|
| 89 |
+
uses: reproducible-containers/buildkit-cache-dance@4b2444fec0c0fb9dbf175a96c094720a692ef810 # v2.1.4
|
| 90 |
+
with:
|
| 91 |
+
cache-source: go-build-cache
|
| 92 |
+
|
| 93 |
+
# Build and push Docker image with Buildx (don't push on PR)
|
| 94 |
+
# https://github.com/docker/build-push-action
|
| 95 |
+
- name: Build and push Docker image
|
| 96 |
+
id: build-and-push
|
| 97 |
+
uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0
|
| 98 |
+
with:
|
| 99 |
+
context: .
|
| 100 |
+
push: ${{ github.event_name != 'pull_request' }}
|
| 101 |
+
tags: ${{ steps.meta.outputs.tags }}
|
| 102 |
+
labels: ${{ steps.meta.outputs.labels }}
|
| 103 |
+
cache-from: type=gha
|
| 104 |
+
cache-to: type=gha,mode=max
|
| 105 |
+
platforms: linux/amd64,linux/arm64
|
| 106 |
+
build-args: |
|
| 107 |
+
VERSION=${{ github.ref_name }}
|
| 108 |
+
|
| 109 |
+
# Sign the resulting Docker image digest except on PRs.
|
| 110 |
+
# This will only write to the public Rekor transparency log when the Docker
|
| 111 |
+
# repository is public to avoid leaking data. If you would like to publish
|
| 112 |
+
# transparency data even for private images, pass --force to cosign below.
|
| 113 |
+
# https://github.com/sigstore/cosign
|
| 114 |
+
- name: Sign the published Docker image
|
| 115 |
+
if: ${{ github.event_name != 'pull_request' }}
|
| 116 |
+
env:
|
| 117 |
+
# https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable
|
| 118 |
+
TAGS: ${{ steps.meta.outputs.tags }}
|
| 119 |
+
DIGEST: ${{ steps.build-and-push.outputs.digest }}
|
| 120 |
+
# This step uses the identity token to provision an ephemeral certificate
|
| 121 |
+
# against the sigstore community Fulcio instance.
|
| 122 |
+
run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}
|
.github/workflows/docs-check.yml
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Documentation Check
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches: [ main ]
|
| 6 |
+
pull_request:
|
| 7 |
+
branches: [ main ]
|
| 8 |
+
|
| 9 |
+
permissions:
|
| 10 |
+
contents: read
|
| 11 |
+
|
| 12 |
+
jobs:
|
| 13 |
+
docs-check:
|
| 14 |
+
runs-on: ubuntu-latest
|
| 15 |
+
steps:
|
| 16 |
+
- name: Checkout code
|
| 17 |
+
uses: actions/checkout@v4
|
| 18 |
+
|
| 19 |
+
- name: Set up Go
|
| 20 |
+
uses: actions/setup-go@v5
|
| 21 |
+
with:
|
| 22 |
+
go-version-file: 'go.mod'
|
| 23 |
+
|
| 24 |
+
- name: Build docs generator
|
| 25 |
+
run: go build -o github-mcp-server ./cmd/github-mcp-server
|
| 26 |
+
|
| 27 |
+
- name: Generate documentation
|
| 28 |
+
run: ./github-mcp-server generate-docs
|
| 29 |
+
|
| 30 |
+
- name: Check for documentation changes
|
| 31 |
+
run: |
|
| 32 |
+
if ! git diff --exit-code README.md; then
|
| 33 |
+
echo "❌ Documentation is out of date!"
|
| 34 |
+
echo ""
|
| 35 |
+
echo "The generated documentation differs from what's committed."
|
| 36 |
+
echo "Please run the following command to update the documentation:"
|
| 37 |
+
echo ""
|
| 38 |
+
echo " go run ./cmd/github-mcp-server generate-docs"
|
| 39 |
+
echo ""
|
| 40 |
+
echo "Then commit the changes."
|
| 41 |
+
echo ""
|
| 42 |
+
echo "Changes detected:"
|
| 43 |
+
git diff README.md
|
| 44 |
+
exit 1
|
| 45 |
+
else
|
| 46 |
+
echo "✅ Documentation is up to date!"
|
| 47 |
+
fi
|
.github/workflows/go.yml
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Build and Test Go Project
|
| 2 |
+
on: [push, pull_request]
|
| 3 |
+
|
| 4 |
+
permissions:
|
| 5 |
+
contents: read
|
| 6 |
+
|
| 7 |
+
jobs:
|
| 8 |
+
build:
|
| 9 |
+
strategy:
|
| 10 |
+
fail-fast: false
|
| 11 |
+
matrix:
|
| 12 |
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
| 13 |
+
|
| 14 |
+
runs-on: ${{ matrix.os }}
|
| 15 |
+
|
| 16 |
+
steps:
|
| 17 |
+
- name: Check out code
|
| 18 |
+
uses: actions/checkout@v4
|
| 19 |
+
|
| 20 |
+
- name: Set up Go
|
| 21 |
+
uses: actions/setup-go@v5
|
| 22 |
+
with:
|
| 23 |
+
go-version-file: "go.mod"
|
| 24 |
+
|
| 25 |
+
- name: Download dependencies
|
| 26 |
+
run: go mod download
|
| 27 |
+
|
| 28 |
+
- name: Run unit tests
|
| 29 |
+
run: script/test
|
| 30 |
+
|
| 31 |
+
- name: Build
|
| 32 |
+
run: go build -v ./cmd/github-mcp-server
|
.github/workflows/goreleaser.yml
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: GoReleaser Release
|
| 2 |
+
on:
|
| 3 |
+
push:
|
| 4 |
+
tags:
|
| 5 |
+
- "v*"
|
| 6 |
+
permissions:
|
| 7 |
+
contents: write
|
| 8 |
+
id-token: write
|
| 9 |
+
attestations: write
|
| 10 |
+
|
| 11 |
+
jobs:
|
| 12 |
+
release:
|
| 13 |
+
runs-on: ubuntu-latest
|
| 14 |
+
|
| 15 |
+
steps:
|
| 16 |
+
- name: Check out code
|
| 17 |
+
uses: actions/checkout@v4
|
| 18 |
+
|
| 19 |
+
- name: Set up Go
|
| 20 |
+
uses: actions/setup-go@v5
|
| 21 |
+
with:
|
| 22 |
+
go-version-file: "go.mod"
|
| 23 |
+
|
| 24 |
+
- name: Download dependencies
|
| 25 |
+
run: go mod download
|
| 26 |
+
|
| 27 |
+
- name: Run GoReleaser
|
| 28 |
+
uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552
|
| 29 |
+
with:
|
| 30 |
+
distribution: goreleaser
|
| 31 |
+
# GoReleaser version
|
| 32 |
+
version: "~> v2"
|
| 33 |
+
# Arguments to pass to GoReleaser
|
| 34 |
+
args: release --clean
|
| 35 |
+
workdir: .
|
| 36 |
+
env:
|
| 37 |
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
| 38 |
+
|
| 39 |
+
- name: Generate signed build provenance attestations for workflow artifacts
|
| 40 |
+
uses: actions/attest-build-provenance@v2
|
| 41 |
+
with:
|
| 42 |
+
subject-path: |
|
| 43 |
+
dist/*.tar.gz
|
| 44 |
+
dist/*.zip
|
| 45 |
+
dist/*.txt
|
.github/workflows/license-check.yml
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Create a github action that runs the license check script and fails if it exits with a non-zero status
|
| 2 |
+
|
| 3 |
+
name: License Check
|
| 4 |
+
on: [push, pull_request]
|
| 5 |
+
permissions:
|
| 6 |
+
contents: read
|
| 7 |
+
|
| 8 |
+
jobs:
|
| 9 |
+
license-check:
|
| 10 |
+
runs-on: ubuntu-latest
|
| 11 |
+
|
| 12 |
+
steps:
|
| 13 |
+
- name: Check out code
|
| 14 |
+
uses: actions/checkout@v4
|
| 15 |
+
|
| 16 |
+
- name: Set up Go
|
| 17 |
+
uses: actions/setup-go@v5
|
| 18 |
+
with:
|
| 19 |
+
go-version-file: "go.mod"
|
| 20 |
+
- name: check licenses
|
| 21 |
+
run: ./script/licenses-check
|
.github/workflows/lint.yml
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: golangci-lint
|
| 2 |
+
on:
|
| 3 |
+
push:
|
| 4 |
+
branches:
|
| 5 |
+
- main
|
| 6 |
+
pull_request:
|
| 7 |
+
|
| 8 |
+
permissions:
|
| 9 |
+
contents: read
|
| 10 |
+
|
| 11 |
+
jobs:
|
| 12 |
+
golangci:
|
| 13 |
+
name: lint
|
| 14 |
+
runs-on: ubuntu-latest
|
| 15 |
+
steps:
|
| 16 |
+
- uses: actions/checkout@v4
|
| 17 |
+
- uses: actions/setup-go@v5
|
| 18 |
+
with:
|
| 19 |
+
go-version: stable
|
| 20 |
+
- name: golangci-lint
|
| 21 |
+
uses: golangci/golangci-lint-action@v8
|
| 22 |
+
with:
|
| 23 |
+
version: v2.1
|
.gitignore
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.idea
|
| 2 |
+
cmd/github-mcp-server/github-mcp-server
|
| 3 |
+
|
| 4 |
+
# VSCode
|
| 5 |
+
.vscode/*
|
| 6 |
+
!.vscode/launch.json
|
| 7 |
+
|
| 8 |
+
# Added by goreleaser init:
|
| 9 |
+
dist/
|
| 10 |
+
__debug_bin*
|
| 11 |
+
|
| 12 |
+
# Go
|
| 13 |
+
vendor
|
| 14 |
+
bin/
|
| 15 |
+
|
| 16 |
+
# macOS
|
| 17 |
+
.DS_Store
|
.golangci.yml
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: "2"
|
| 2 |
+
run:
|
| 3 |
+
concurrency: 4
|
| 4 |
+
tests: true
|
| 5 |
+
linters:
|
| 6 |
+
enable:
|
| 7 |
+
- bodyclose
|
| 8 |
+
- gocritic
|
| 9 |
+
- gosec
|
| 10 |
+
- makezero
|
| 11 |
+
- misspell
|
| 12 |
+
- nakedret
|
| 13 |
+
- revive
|
| 14 |
+
exclusions:
|
| 15 |
+
generated: lax
|
| 16 |
+
presets:
|
| 17 |
+
- comments
|
| 18 |
+
- common-false-positives
|
| 19 |
+
- legacy
|
| 20 |
+
- std-error-handling
|
| 21 |
+
paths:
|
| 22 |
+
- third_party$
|
| 23 |
+
- builtin$
|
| 24 |
+
- examples$
|
| 25 |
+
settings:
|
| 26 |
+
staticcheck:
|
| 27 |
+
checks:
|
| 28 |
+
- "all"
|
| 29 |
+
- -QF1008
|
| 30 |
+
- -ST1000
|
| 31 |
+
formatters:
|
| 32 |
+
exclusions:
|
| 33 |
+
generated: lax
|
| 34 |
+
paths:
|
| 35 |
+
- third_party$
|
| 36 |
+
- builtin$
|
| 37 |
+
- examples$
|
.goreleaser.yaml
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: 2
|
| 2 |
+
project_name: github-mcp-server
|
| 3 |
+
before:
|
| 4 |
+
hooks:
|
| 5 |
+
- go mod tidy
|
| 6 |
+
- go generate ./...
|
| 7 |
+
|
| 8 |
+
builds:
|
| 9 |
+
- env:
|
| 10 |
+
- CGO_ENABLED=0
|
| 11 |
+
ldflags:
|
| 12 |
+
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}
|
| 13 |
+
goos:
|
| 14 |
+
- linux
|
| 15 |
+
- windows
|
| 16 |
+
- darwin
|
| 17 |
+
main: ./cmd/github-mcp-server
|
| 18 |
+
|
| 19 |
+
archives:
|
| 20 |
+
- formats: tar.gz
|
| 21 |
+
# this name template makes the OS and Arch compatible with the results of `uname`.
|
| 22 |
+
name_template: >-
|
| 23 |
+
{{ .ProjectName }}_
|
| 24 |
+
{{- title .Os }}_
|
| 25 |
+
{{- if eq .Arch "amd64" }}x86_64
|
| 26 |
+
{{- else if eq .Arch "386" }}i386
|
| 27 |
+
{{- else }}{{ .Arch }}{{ end }}
|
| 28 |
+
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
| 29 |
+
# use zip for windows archives
|
| 30 |
+
format_overrides:
|
| 31 |
+
- goos: windows
|
| 32 |
+
formats: zip
|
| 33 |
+
|
| 34 |
+
changelog:
|
| 35 |
+
sort: asc
|
| 36 |
+
filters:
|
| 37 |
+
exclude:
|
| 38 |
+
- "^docs:"
|
| 39 |
+
- "^test:"
|
| 40 |
+
|
| 41 |
+
release:
|
| 42 |
+
draft: true
|
| 43 |
+
prerelease: auto
|
| 44 |
+
name_template: "GitHub MCP Server {{.Version}}"
|
.vscode/launch.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
// Use IntelliSense to learn about possible attributes.
|
| 3 |
+
// Hover to view descriptions of existing attributes.
|
| 4 |
+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
| 5 |
+
"version": "0.2.0",
|
| 6 |
+
"configurations": [
|
| 7 |
+
{
|
| 8 |
+
"name": "Launch stdio server",
|
| 9 |
+
"type": "go",
|
| 10 |
+
"request": "launch",
|
| 11 |
+
"mode": "auto",
|
| 12 |
+
"cwd": "${workspaceFolder}",
|
| 13 |
+
"program": "cmd/github-mcp-server/main.go",
|
| 14 |
+
"args": ["stdio"],
|
| 15 |
+
"console": "integratedTerminal",
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"name": "Launch stdio server (read-only)",
|
| 19 |
+
"type": "go",
|
| 20 |
+
"request": "launch",
|
| 21 |
+
"mode": "auto",
|
| 22 |
+
"cwd": "${workspaceFolder}",
|
| 23 |
+
"program": "cmd/github-mcp-server/main.go",
|
| 24 |
+
"args": ["stdio", "--read-only"],
|
| 25 |
+
"console": "integratedTerminal",
|
| 26 |
+
}
|
| 27 |
+
]
|
| 28 |
+
}
|
CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Contributor Covenant Code of Conduct
|
| 2 |
+
|
| 3 |
+
## Our Pledge
|
| 4 |
+
|
| 5 |
+
We as members, contributors, and leaders pledge to make participation in our
|
| 6 |
+
community a harassment-free experience for everyone, regardless of age, body
|
| 7 |
+
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
| 8 |
+
identity and expression, level of experience, education, socio-economic status,
|
| 9 |
+
nationality, personal appearance, race, religion, or sexual identity
|
| 10 |
+
and orientation.
|
| 11 |
+
|
| 12 |
+
We pledge to act and interact in ways that contribute to an open, welcoming,
|
| 13 |
+
diverse, inclusive, and healthy community.
|
| 14 |
+
|
| 15 |
+
## Our Standards
|
| 16 |
+
|
| 17 |
+
Examples of behavior that contributes to a positive environment for our
|
| 18 |
+
community include:
|
| 19 |
+
|
| 20 |
+
* Demonstrating empathy and kindness toward other people
|
| 21 |
+
* Being respectful of differing opinions, viewpoints, and experiences
|
| 22 |
+
* Giving and gracefully accepting constructive feedback
|
| 23 |
+
* Accepting responsibility and apologizing to those affected by our mistakes,
|
| 24 |
+
and learning from the experience
|
| 25 |
+
* Focusing on what is best not just for us as individuals, but for the
|
| 26 |
+
overall community
|
| 27 |
+
|
| 28 |
+
Examples of unacceptable behavior include:
|
| 29 |
+
|
| 30 |
+
* The use of sexualized language or imagery, and sexual attention or
|
| 31 |
+
advances of any kind
|
| 32 |
+
* Trolling, insulting or derogatory comments, and personal or political attacks
|
| 33 |
+
* Public or private harassment
|
| 34 |
+
* Publishing others' private information, such as a physical or email
|
| 35 |
+
address, without their explicit permission
|
| 36 |
+
* Other conduct which could reasonably be considered inappropriate in a
|
| 37 |
+
professional setting
|
| 38 |
+
|
| 39 |
+
## Enforcement Responsibilities
|
| 40 |
+
|
| 41 |
+
Community leaders are responsible for clarifying and enforcing our standards of
|
| 42 |
+
acceptable behavior and will take appropriate and fair corrective action in
|
| 43 |
+
response to any behavior that they deem inappropriate, threatening, offensive,
|
| 44 |
+
or harmful.
|
| 45 |
+
|
| 46 |
+
Community leaders have the right and responsibility to remove, edit, or reject
|
| 47 |
+
comments, commits, code, wiki edits, issues, and other contributions that are
|
| 48 |
+
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
| 49 |
+
decisions when appropriate.
|
| 50 |
+
|
| 51 |
+
## Scope
|
| 52 |
+
|
| 53 |
+
This Code of Conduct applies within all community spaces, and also applies when
|
| 54 |
+
an individual is officially representing the community in public spaces.
|
| 55 |
+
Examples of representing our community include using an official e-mail address,
|
| 56 |
+
posting via an official social media account, or acting as an appointed
|
| 57 |
+
representative at an online or offline event.
|
| 58 |
+
|
| 59 |
+
## Enforcement
|
| 60 |
+
|
| 61 |
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
| 62 |
+
reported to the community leaders responsible for enforcement at
|
| 63 |
+
GitHub.
|
| 64 |
+
All complaints will be reviewed and investigated promptly and fairly.
|
| 65 |
+
|
| 66 |
+
All community leaders are obligated to respect the privacy and security of the
|
| 67 |
+
reporter of any incident.
|
| 68 |
+
|
| 69 |
+
## Enforcement Guidelines
|
| 70 |
+
|
| 71 |
+
Community leaders will follow these Community Impact Guidelines in determining
|
| 72 |
+
the consequences for any action they deem in violation of this Code of Conduct:
|
| 73 |
+
|
| 74 |
+
### 1. Correction
|
| 75 |
+
|
| 76 |
+
**Community Impact**: Use of inappropriate language or other behavior deemed
|
| 77 |
+
unprofessional or unwelcome in the community.
|
| 78 |
+
|
| 79 |
+
**Consequence**: A private, written warning from community leaders, providing
|
| 80 |
+
clarity around the nature of the violation and an explanation of why the
|
| 81 |
+
behavior was inappropriate. A public apology may be requested.
|
| 82 |
+
|
| 83 |
+
### 2. Warning
|
| 84 |
+
|
| 85 |
+
**Community Impact**: A violation through a single incident or series
|
| 86 |
+
of actions.
|
| 87 |
+
|
| 88 |
+
**Consequence**: A warning with consequences for continued behavior. No
|
| 89 |
+
interaction with the people involved, including unsolicited interaction with
|
| 90 |
+
those enforcing the Code of Conduct, for a specified period of time. This
|
| 91 |
+
includes avoiding interactions in community spaces as well as external channels
|
| 92 |
+
like social media. Violating these terms may lead to a temporary or
|
| 93 |
+
permanent ban.
|
| 94 |
+
|
| 95 |
+
### 3. Temporary Ban
|
| 96 |
+
|
| 97 |
+
**Community Impact**: A serious violation of community standards, including
|
| 98 |
+
sustained inappropriate behavior.
|
| 99 |
+
|
| 100 |
+
**Consequence**: A temporary ban from any sort of interaction or public
|
| 101 |
+
communication with the community for a specified period of time. No public or
|
| 102 |
+
private interaction with the people involved, including unsolicited interaction
|
| 103 |
+
with those enforcing the Code of Conduct, is allowed during this period.
|
| 104 |
+
Violating these terms may lead to a permanent ban.
|
| 105 |
+
|
| 106 |
+
### 4. Permanent Ban
|
| 107 |
+
|
| 108 |
+
**Community Impact**: Demonstrating a pattern of violation of community
|
| 109 |
+
standards, including sustained inappropriate behavior, harassment of an
|
| 110 |
+
individual, or aggression toward or disparagement of classes of individuals.
|
| 111 |
+
|
| 112 |
+
**Consequence**: A permanent ban from any sort of public interaction within
|
| 113 |
+
the community.
|
| 114 |
+
|
| 115 |
+
## Attribution
|
| 116 |
+
|
| 117 |
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
| 118 |
+
version 2.0, available at
|
| 119 |
+
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
| 120 |
+
|
| 121 |
+
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
| 122 |
+
enforcement ladder](https://github.com/mozilla/diversity).
|
| 123 |
+
|
| 124 |
+
[homepage]: https://www.contributor-covenant.org
|
| 125 |
+
|
| 126 |
+
For answers to common questions about this code of conduct, see the FAQ at
|
| 127 |
+
https://www.contributor-covenant.org/faq. Translations are available at
|
| 128 |
+
https://www.contributor-covenant.org/translations.
|
CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
## Contributing
|
| 2 |
+
|
| 3 |
+
[fork]: https://github.com/github/github-mcp-server/fork
|
| 4 |
+
[pr]: https://github.com/github/github-mcp-server/compare
|
| 5 |
+
[style]: https://github.com/github/github-mcp-server/blob/main/.golangci.yml
|
| 6 |
+
|
| 7 |
+
Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great.
|
| 8 |
+
|
| 9 |
+
Contributions to this project are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [project's open source license](LICENSE).
|
| 10 |
+
|
| 11 |
+
Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms.
|
| 12 |
+
|
| 13 |
+
## What we're looking for
|
| 14 |
+
|
| 15 |
+
We can't guarantee that every tool, feature, or pull request will be approved or merged. Our focus is on supporting high-quality, high-impact capabilities that advance agentic workflows and deliver clear value to developers.
|
| 16 |
+
|
| 17 |
+
To increase the chances your request is accepted:
|
| 18 |
+
* Include real use cases or examples that demonstrate practical value
|
| 19 |
+
* Please create an issue outlining the scenario and potential impact, so we can triage it promptly and prioritize accordingly.
|
| 20 |
+
* If your request stalls, you can open a Discussion post and link to your issue or PR
|
| 21 |
+
* We actively revisit requests that gain strong community engagement (👍s, comments, or evidence of real-world use)
|
| 22 |
+
|
| 23 |
+
Thanks for contributing and for helping us build toolsets that are truly valuable!
|
| 24 |
+
|
| 25 |
+
## Prerequisites for running and testing code
|
| 26 |
+
|
| 27 |
+
These are one time installations required to be able to test your changes locally as part of the pull request (PR) submission process.
|
| 28 |
+
|
| 29 |
+
1. Install Go [through download](https://go.dev/doc/install) | [through Homebrew](https://formulae.brew.sh/formula/go)
|
| 30 |
+
2. [Install golangci-lint v2](https://golangci-lint.run/welcome/install/#local-installation)
|
| 31 |
+
|
| 32 |
+
## Submitting a pull request
|
| 33 |
+
|
| 34 |
+
1. [Fork][fork] and clone the repository
|
| 35 |
+
2. Make sure the tests pass on your machine: `go test -v ./...`
|
| 36 |
+
3. Make sure linter passes on your machine: `golangci-lint run`
|
| 37 |
+
4. Create a new branch: `git checkout -b my-branch-name`
|
| 38 |
+
5. Add your changes and tests, and make sure the Action workflows still pass
|
| 39 |
+
- Run linter: `script/lint`
|
| 40 |
+
- Update snapshots and run tests: `UPDATE_TOOLSNAPS=true go test ./...`
|
| 41 |
+
- Update readme documentation: `script/generate-docs`
|
| 42 |
+
6. Push to your fork and [submit a pull request][pr] targeting the `main` branch
|
| 43 |
+
7. Pat yourself on the back and wait for your pull request to be reviewed and merged.
|
| 44 |
+
|
| 45 |
+
Here are a few things you can do that will increase the likelihood of your pull request being accepted:
|
| 46 |
+
|
| 47 |
+
- Follow the [style guide][style].
|
| 48 |
+
- Write tests.
|
| 49 |
+
- Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests.
|
| 50 |
+
- Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
|
| 51 |
+
|
| 52 |
+
## Resources
|
| 53 |
+
|
| 54 |
+
- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
|
| 55 |
+
- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
|
| 56 |
+
- [GitHub Help](https://help.github.com)
|
Dockerfile
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM golang:1.24.4-alpine AS build
|
| 2 |
+
ARG VERSION="dev"
|
| 3 |
+
|
| 4 |
+
# Set the working directory
|
| 5 |
+
WORKDIR /build
|
| 6 |
+
|
| 7 |
+
# Install git
|
| 8 |
+
RUN --mount=type=cache,target=/var/cache/apk \
|
| 9 |
+
apk add git
|
| 10 |
+
|
| 11 |
+
# Build the server
|
| 12 |
+
# go build automatically download required module dependencies to /go/pkg/mod
|
| 13 |
+
RUN --mount=type=cache,target=/go/pkg/mod \
|
| 14 |
+
--mount=type=cache,target=/root/.cache/go-build \
|
| 15 |
+
--mount=type=bind,target=. \
|
| 16 |
+
CGO_ENABLED=0 go build -ldflags="-s -w -X main.version=${VERSION} -X main.commit=$(git rev-parse HEAD) -X main.date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
| 17 |
+
-o /bin/github-mcp-server cmd/github-mcp-server/main.go
|
| 18 |
+
|
| 19 |
+
# Make a stage to run the app
|
| 20 |
+
FROM gcr.io/distroless/base-debian12
|
| 21 |
+
# Set the working directory
|
| 22 |
+
WORKDIR /server
|
| 23 |
+
# Copy the binary from the build stage
|
| 24 |
+
COPY --from=build /bin/github-mcp-server .
|
| 25 |
+
# Set the entrypoint to the server binary
|
| 26 |
+
ENTRYPOINT ["/server/github-mcp-server"]
|
| 27 |
+
# Default arguments for ENTRYPOINT
|
| 28 |
+
CMD ["stdio"]
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2025 GitHub
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
ADDED
|
@@ -0,0 +1,1142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# GitHub MCP Server
|
| 2 |
+
|
| 3 |
+
The GitHub MCP Server connects AI tools directly to GitHub's platform. This gives AI agents, assistants, and chatbots the ability to read repositories and code files, manage issues and PRs, analyze code, and automate workflows. All through natural language interactions.
|
| 4 |
+
|
| 5 |
+
### Use Cases
|
| 6 |
+
|
| 7 |
+
- Repository Management: Browse and query code, search files, analyze commits, and understand project structure across any repository you have access to.
|
| 8 |
+
- Issue & PR Automation: Create, update, and manage issues and pull requests. Let AI help triage bugs, review code changes, and maintain project boards.
|
| 9 |
+
- CI/CD & Workflow Intelligence: Monitor GitHub Actions workflow runs, analyze build failures, manage releases, and get insights into your development pipeline.
|
| 10 |
+
- Code Analysis: Examine security findings, review Dependabot alerts, understand code patterns, and get comprehensive insights into your codebase.
|
| 11 |
+
- Team Collaboration: Access discussions, manage notifications, analyze team activity, and streamline processes for your team.
|
| 12 |
+
|
| 13 |
+
Built for developers who want to connect their AI tools to GitHub context and capabilities, from simple natural language queries to complex multi-step agent workflows.
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
## Remote GitHub MCP Server
|
| 18 |
+
|
| 19 |
+
[](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2F%22%7D) [](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2F%22%7D&quality=insiders)
|
| 20 |
+
|
| 21 |
+
The remote GitHub MCP Server is hosted by GitHub and provides the easiest method for getting up and running. If your MCP host does not support remote MCP servers, don't worry! You can use the [local version of the GitHub MCP Server](https://github.com/github/github-mcp-server?tab=readme-ov-file#local-github-mcp-server) instead.
|
| 22 |
+
|
| 23 |
+
### Prerequisites
|
| 24 |
+
|
| 25 |
+
1. A compatible MCP host with remote server support (VS Code 1.101+, Claude Desktop, Cursor, Windsurf, etc.)
|
| 26 |
+
2. Any applicable [policies enabled](https://github.com/github/github-mcp-server/blob/main/docs/policies-and-governance.md)
|
| 27 |
+
|
| 28 |
+
### Install in VS Code
|
| 29 |
+
|
| 30 |
+
For quick installation, use one of the one-click install buttons above. Once you complete that flow, toggle Agent mode (located by the Copilot Chat text input) and the server will start. Make sure you're using [VS Code 1.101](https://code.visualstudio.com/updates/v1_101) or [later](https://code.visualstudio.com/updates) for remote MCP and OAuth support.
|
| 31 |
+
|
| 32 |
+
Alternatively, to manually configure VS Code, choose the appropriate JSON block from the examples below and add it to your host configuration:
|
| 33 |
+
|
| 34 |
+
<table>
|
| 35 |
+
<tr><th>Using OAuth</th><th>Using a GitHub PAT</th></tr>
|
| 36 |
+
<tr><th align=left colspan=2>VS Code (version 1.101 or greater)</th></tr>
|
| 37 |
+
<tr valign=top>
|
| 38 |
+
<td>
|
| 39 |
+
|
| 40 |
+
```json
|
| 41 |
+
{
|
| 42 |
+
"servers": {
|
| 43 |
+
"github": {
|
| 44 |
+
"type": "http",
|
| 45 |
+
"url": "https://api.githubcopilot.com/mcp/"
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
</td>
|
| 52 |
+
<td>
|
| 53 |
+
|
| 54 |
+
```json
|
| 55 |
+
{
|
| 56 |
+
"servers": {
|
| 57 |
+
"github": {
|
| 58 |
+
"type": "http",
|
| 59 |
+
"url": "https://api.githubcopilot.com/mcp/",
|
| 60 |
+
"headers": {
|
| 61 |
+
"Authorization": "Bearer ${input:github_mcp_pat}"
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
},
|
| 65 |
+
"inputs": [
|
| 66 |
+
{
|
| 67 |
+
"type": "promptString",
|
| 68 |
+
"id": "github_mcp_pat",
|
| 69 |
+
"description": "GitHub Personal Access Token",
|
| 70 |
+
"password": true
|
| 71 |
+
}
|
| 72 |
+
]
|
| 73 |
+
}
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
</td>
|
| 77 |
+
</tr>
|
| 78 |
+
</table>
|
| 79 |
+
|
| 80 |
+
### Install in other MCP hosts
|
| 81 |
+
- **[GitHub Copilot in other IDEs](/docs/installation-guides/install-other-copilot-ides.md)** - Installation for JetBrains, Visual Studio, Eclipse, and Xcode with GitHub Copilot
|
| 82 |
+
- **[Claude Applications](/docs/installation-guides/install-claude.md)** - Installation guide for Claude Web, Claude Desktop and Claude Code CLI
|
| 83 |
+
- **[Cursor](/docs/installation-guides/install-cursor.md)** - Installation guide for Cursor IDE
|
| 84 |
+
- **[Windsurf](/docs/installation-guides/install-windsurf.md)** - Installation guide for Windsurf IDE
|
| 85 |
+
|
| 86 |
+
> **Note:** Each MCP host application needs to configure a GitHub App or OAuth App to support remote access via OAuth. Any host application that supports remote MCP servers should support the remote GitHub server with PAT authentication. Configuration details and support levels vary by host. Make sure to refer to the host application's documentation for more info.
|
| 87 |
+
|
| 88 |
+
> ⚠️ **Public Preview Status:** The **remote** GitHub MCP Server is currently in Public Preview. During preview, access may be gated depending on authentication type and surface:
|
| 89 |
+
> - OAuth: Subject to GitHub Copilot Editor Preview Policy until GA
|
| 90 |
+
> - PAT: Controlled via your organization's PAT policies
|
| 91 |
+
> - MCP Servers in Copilot policy: Enables/disables access to all MCP servers in VS Code, with other Copilot editors migrating to this policy in the coming months.
|
| 92 |
+
|
| 93 |
+
### Configuration
|
| 94 |
+
See [Remote Server Documentation](/docs/remote-server.md) on how to pass additional configuration settings to the remote GitHub MCP Server.
|
| 95 |
+
|
| 96 |
+
---
|
| 97 |
+
|
| 98 |
+
## Local GitHub MCP Server
|
| 99 |
+
|
| 100 |
+
[](https://insiders.vscode.dev/redirect/mcp/install?name=github&inputs=%5B%7B%22id%22%3A%22github_token%22%2C%22type%22%3A%22promptString%22%2C%22description%22%3A%22GitHub%20Personal%20Access%20Token%22%2C%22password%22%3Atrue%7D%5D&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22-e%22%2C%22GITHUB_PERSONAL_ACCESS_TOKEN%22%2C%22ghcr.io%2Fgithub%2Fgithub-mcp-server%22%5D%2C%22env%22%3A%7B%22GITHUB_PERSONAL_ACCESS_TOKEN%22%3A%22%24%7Binput%3Agithub_token%7D%22%7D%7D) [](https://insiders.vscode.dev/redirect/mcp/install?name=github&inputs=%5B%7B%22id%22%3A%22github_token%22%2C%22type%22%3A%22promptString%22%2C%22description%22%3A%22GitHub%20Personal%20Access%20Token%22%2C%22password%22%3Atrue%7D%5D&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22-e%22%2C%22GITHUB_PERSONAL_ACCESS_TOKEN%22%2C%22ghcr.io%2Fgithub%2Fgithub-mcp-server%22%5D%2C%22env%22%3A%7B%22GITHUB_PERSONAL_ACCESS_TOKEN%22%3A%22%24%7Binput%3Agithub_token%7D%22%7D%7D&quality=insiders)
|
| 101 |
+
|
| 102 |
+
### Prerequisites
|
| 103 |
+
|
| 104 |
+
1. To run the server in a container, you will need to have [Docker](https://www.docker.com/) installed.
|
| 105 |
+
2. Once Docker is installed, you will also need to ensure Docker is running. The image is public; if you get errors on pull, you may have an expired token and need to `docker logout ghcr.io`.
|
| 106 |
+
3. Lastly you will need to [Create a GitHub Personal Access Token](https://github.com/settings/personal-access-tokens/new).
|
| 107 |
+
The MCP server can use many of the GitHub APIs, so enable the permissions that you feel comfortable granting your AI tools (to learn more about access tokens, please check out the [documentation](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens)).
|
| 108 |
+
|
| 109 |
+
<details><summary><b>Handling PATs Securely</b></summary>
|
| 110 |
+
|
| 111 |
+
### Environment Variables (Recommended)
|
| 112 |
+
To keep your GitHub PAT secure and reusable across different MCP hosts:
|
| 113 |
+
|
| 114 |
+
1. **Store your PAT in environment variables**
|
| 115 |
+
```bash
|
| 116 |
+
export GITHUB_PAT=your_token_here
|
| 117 |
+
```
|
| 118 |
+
Or create a `.env` file:
|
| 119 |
+
```env
|
| 120 |
+
GITHUB_PAT=your_token_here
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
2. **Protect your `.env` file**
|
| 124 |
+
```bash
|
| 125 |
+
# Add to .gitignore to prevent accidental commits
|
| 126 |
+
echo ".env" >> .gitignore
|
| 127 |
+
```
|
| 128 |
+
|
| 129 |
+
3. **Reference the token in configurations**
|
| 130 |
+
```bash
|
| 131 |
+
# CLI usage
|
| 132 |
+
claude mcp update github -e GITHUB_PERSONAL_ACCESS_TOKEN=$GITHUB_PAT
|
| 133 |
+
|
| 134 |
+
# In config files (where supported)
|
| 135 |
+
"env": {
|
| 136 |
+
"GITHUB_PERSONAL_ACCESS_TOKEN": "$GITHUB_PAT"
|
| 137 |
+
}
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
> **Note**: Environment variable support varies by host app and IDE. Some applications (like Windsurf) require hardcoded tokens in config files.
|
| 141 |
+
|
| 142 |
+
### Token Security Best Practices
|
| 143 |
+
|
| 144 |
+
- **Minimum scopes**: Only grant necessary permissions
|
| 145 |
+
- `repo` - Repository operations
|
| 146 |
+
- `read:packages` - Docker image access
|
| 147 |
+
- `read:org` - Organization team access
|
| 148 |
+
- **Separate tokens**: Use different PATs for different projects/environments
|
| 149 |
+
- **Regular rotation**: Update tokens periodically
|
| 150 |
+
- **Never commit**: Keep tokens out of version control
|
| 151 |
+
- **File permissions**: Restrict access to config files containing tokens
|
| 152 |
+
```bash
|
| 153 |
+
chmod 600 ~/.your-app/config.json
|
| 154 |
+
```
|
| 155 |
+
|
| 156 |
+
</details>
|
| 157 |
+
|
| 158 |
+
## Installation
|
| 159 |
+
|
| 160 |
+
### Install in GitHub Copilot on VS Code
|
| 161 |
+
|
| 162 |
+
For quick installation, use one of the one-click install buttons above. Once you complete that flow, toggle Agent mode (located by the Copilot Chat text input) and the server will start.
|
| 163 |
+
|
| 164 |
+
More about using MCP server tools in VS Code's [agent mode documentation](https://code.visualstudio.com/docs/copilot/chat/mcp-servers).
|
| 165 |
+
|
| 166 |
+
Install in GitHub Copilot on other IDEs (JetBrains, Visual Studio, Eclipse, etc.)
|
| 167 |
+
|
| 168 |
+
Add the following JSON block to your IDE's MCP settings.
|
| 169 |
+
|
| 170 |
+
```json
|
| 171 |
+
{
|
| 172 |
+
"mcp": {
|
| 173 |
+
"inputs": [
|
| 174 |
+
{
|
| 175 |
+
"type": "promptString",
|
| 176 |
+
"id": "github_token",
|
| 177 |
+
"description": "GitHub Personal Access Token",
|
| 178 |
+
"password": true
|
| 179 |
+
}
|
| 180 |
+
],
|
| 181 |
+
"servers": {
|
| 182 |
+
"github": {
|
| 183 |
+
"command": "docker",
|
| 184 |
+
"args": [
|
| 185 |
+
"run",
|
| 186 |
+
"-i",
|
| 187 |
+
"--rm",
|
| 188 |
+
"-e",
|
| 189 |
+
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
| 190 |
+
"ghcr.io/github/github-mcp-server"
|
| 191 |
+
],
|
| 192 |
+
"env": {
|
| 193 |
+
"GITHUB_PERSONAL_ACCESS_TOKEN": "${input:github_token}"
|
| 194 |
+
}
|
| 195 |
+
}
|
| 196 |
+
}
|
| 197 |
+
}
|
| 198 |
+
}
|
| 199 |
+
```
|
| 200 |
+
|
| 201 |
+
Optionally, you can add a similar example (i.e. without the mcp key) to a file called `.vscode/mcp.json` in your workspace. This will allow you to share the configuration with other host applications that accept the same format.
|
| 202 |
+
|
| 203 |
+
<details>
|
| 204 |
+
<summary><b>Example JSON block without the MCP key included</b></summary>
|
| 205 |
+
<br>
|
| 206 |
+
|
| 207 |
+
```json
|
| 208 |
+
{
|
| 209 |
+
"inputs": [
|
| 210 |
+
{
|
| 211 |
+
"type": "promptString",
|
| 212 |
+
"id": "github_token",
|
| 213 |
+
"description": "GitHub Personal Access Token",
|
| 214 |
+
"password": true
|
| 215 |
+
}
|
| 216 |
+
],
|
| 217 |
+
"servers": {
|
| 218 |
+
"github": {
|
| 219 |
+
"command": "docker",
|
| 220 |
+
"args": [
|
| 221 |
+
"run",
|
| 222 |
+
"-i",
|
| 223 |
+
"--rm",
|
| 224 |
+
"-e",
|
| 225 |
+
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
| 226 |
+
"ghcr.io/github/github-mcp-server"
|
| 227 |
+
],
|
| 228 |
+
"env": {
|
| 229 |
+
"GITHUB_PERSONAL_ACCESS_TOKEN": "${input:github_token}"
|
| 230 |
+
}
|
| 231 |
+
}
|
| 232 |
+
}
|
| 233 |
+
}
|
| 234 |
+
```
|
| 235 |
+
|
| 236 |
+
</details>
|
| 237 |
+
|
| 238 |
+
### Install in Other MCP Hosts
|
| 239 |
+
|
| 240 |
+
For other MCP host applications, please refer to our installation guides:
|
| 241 |
+
|
| 242 |
+
- **[GitHub Copilot in other IDEs](/docs/installation-guides/install-other-copilot-ides.md)** - Installation for JetBrains, Visual Studio, Eclipse, and Xcode with GitHub Copilot
|
| 243 |
+
- **[Claude Code & Claude Desktop](docs/installation-guides/install-claude.md)** - Installation guide for Claude Code and Claude Desktop
|
| 244 |
+
- **[Cursor](docs/installation-guides/install-cursor.md)** - Installation guide for Cursor IDE
|
| 245 |
+
- **[Windsurf](docs/installation-guides/install-windsurf.md)** - Installation guide for Windsurf IDE
|
| 246 |
+
|
| 247 |
+
For a complete overview of all installation options, see our **[Installation Guides Index](docs/installation-guides)**.
|
| 248 |
+
|
| 249 |
+
> **Note:** Any host application that supports local MCP servers should be able to access the local GitHub MCP server. However, the specific configuration process, syntax and stability of the integration will vary by host application. While many may follow a similar format to the examples above, this is not guaranteed. Please refer to your host application's documentation for the correct MCP configuration syntax and setup process.
|
| 250 |
+
|
| 251 |
+
### Build from source
|
| 252 |
+
|
| 253 |
+
If you don't have Docker, you can use `go build` to build the binary in the
|
| 254 |
+
`cmd/github-mcp-server` directory, and use the `github-mcp-server stdio` command with the `GITHUB_PERSONAL_ACCESS_TOKEN` environment variable set to your token. To specify the output location of the build, use the `-o` flag. You should configure your server to use the built executable as its `command`. For example:
|
| 255 |
+
|
| 256 |
+
```JSON
|
| 257 |
+
{
|
| 258 |
+
"mcp": {
|
| 259 |
+
"servers": {
|
| 260 |
+
"github": {
|
| 261 |
+
"command": "/path/to/github-mcp-server",
|
| 262 |
+
"args": ["stdio"],
|
| 263 |
+
"env": {
|
| 264 |
+
"GITHUB_PERSONAL_ACCESS_TOKEN": "<YOUR_TOKEN>"
|
| 265 |
+
}
|
| 266 |
+
}
|
| 267 |
+
}
|
| 268 |
+
}
|
| 269 |
+
}
|
| 270 |
+
```
|
| 271 |
+
|
| 272 |
+
## Tool Configuration
|
| 273 |
+
|
| 274 |
+
The GitHub MCP Server supports enabling or disabling specific groups of functionalities via the `--toolsets` flag. This allows you to control which GitHub API capabilities are available to your AI tools. Enabling only the toolsets that you need can help the LLM with tool choice and reduce the context size.
|
| 275 |
+
|
| 276 |
+
_Toolsets are not limited to Tools. Relevant MCP Resources and Prompts are also included where applicable._
|
| 277 |
+
|
| 278 |
+
### Available Toolsets
|
| 279 |
+
|
| 280 |
+
The following sets of tools are available (all are on by default):
|
| 281 |
+
|
| 282 |
+
<!-- START AUTOMATED TOOLSETS -->
|
| 283 |
+
| Toolset | Description |
|
| 284 |
+
| ----------------------- | ------------------------------------------------------------- |
|
| 285 |
+
| `context` | **Strongly recommended**: Tools that provide context about the current user and GitHub context you are operating in |
|
| 286 |
+
| `actions` | GitHub Actions workflows and CI/CD operations |
|
| 287 |
+
| `code_security` | Code security related tools, such as GitHub Code Scanning |
|
| 288 |
+
| `dependabot` | Dependabot tools |
|
| 289 |
+
| `discussions` | GitHub Discussions related tools |
|
| 290 |
+
| `experiments` | Experimental features that are not considered stable yet |
|
| 291 |
+
| `gists` | GitHub Gist related tools |
|
| 292 |
+
| `issues` | GitHub Issues related tools |
|
| 293 |
+
| `notifications` | GitHub Notifications related tools |
|
| 294 |
+
| `orgs` | GitHub Organization related tools |
|
| 295 |
+
| `pull_requests` | GitHub Pull Request related tools |
|
| 296 |
+
| `repos` | GitHub Repository related tools |
|
| 297 |
+
| `secret_protection` | Secret protection related tools, such as GitHub Secret Scanning |
|
| 298 |
+
| `security_advisories` | Security advisories related tools |
|
| 299 |
+
| `users` | GitHub User related tools |
|
| 300 |
+
<!-- END AUTOMATED TOOLSETS -->
|
| 301 |
+
|
| 302 |
+
## Tools
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
<!-- START AUTOMATED TOOLS -->
|
| 306 |
+
<details>
|
| 307 |
+
|
| 308 |
+
<summary>Actions</summary>
|
| 309 |
+
|
| 310 |
+
- **cancel_workflow_run** - Cancel workflow run
|
| 311 |
+
- `owner`: Repository owner (string, required)
|
| 312 |
+
- `repo`: Repository name (string, required)
|
| 313 |
+
- `run_id`: The unique identifier of the workflow run (number, required)
|
| 314 |
+
|
| 315 |
+
- **delete_workflow_run_logs** - Delete workflow logs
|
| 316 |
+
- `owner`: Repository owner (string, required)
|
| 317 |
+
- `repo`: Repository name (string, required)
|
| 318 |
+
- `run_id`: The unique identifier of the workflow run (number, required)
|
| 319 |
+
|
| 320 |
+
- **download_workflow_run_artifact** - Download workflow artifact
|
| 321 |
+
- `artifact_id`: The unique identifier of the artifact (number, required)
|
| 322 |
+
- `owner`: Repository owner (string, required)
|
| 323 |
+
- `repo`: Repository name (string, required)
|
| 324 |
+
|
| 325 |
+
- **get_job_logs** - Get job logs
|
| 326 |
+
- `failed_only`: When true, gets logs for all failed jobs in run_id (boolean, optional)
|
| 327 |
+
- `job_id`: The unique identifier of the workflow job (required for single job logs) (number, optional)
|
| 328 |
+
- `owner`: Repository owner (string, required)
|
| 329 |
+
- `repo`: Repository name (string, required)
|
| 330 |
+
- `return_content`: Returns actual log content instead of URLs (boolean, optional)
|
| 331 |
+
- `run_id`: Workflow run ID (required when using failed_only) (number, optional)
|
| 332 |
+
- `tail_lines`: Number of lines to return from the end of the log (number, optional)
|
| 333 |
+
|
| 334 |
+
- **get_workflow_run** - Get workflow run
|
| 335 |
+
- `owner`: Repository owner (string, required)
|
| 336 |
+
- `repo`: Repository name (string, required)
|
| 337 |
+
- `run_id`: The unique identifier of the workflow run (number, required)
|
| 338 |
+
|
| 339 |
+
- **get_workflow_run_logs** - Get workflow run logs
|
| 340 |
+
- `owner`: Repository owner (string, required)
|
| 341 |
+
- `repo`: Repository name (string, required)
|
| 342 |
+
- `run_id`: The unique identifier of the workflow run (number, required)
|
| 343 |
+
|
| 344 |
+
- **get_workflow_run_usage** - Get workflow usage
|
| 345 |
+
- `owner`: Repository owner (string, required)
|
| 346 |
+
- `repo`: Repository name (string, required)
|
| 347 |
+
- `run_id`: The unique identifier of the workflow run (number, required)
|
| 348 |
+
|
| 349 |
+
- **list_workflow_jobs** - List workflow jobs
|
| 350 |
+
- `filter`: Filters jobs by their completed_at timestamp (string, optional)
|
| 351 |
+
- `owner`: Repository owner (string, required)
|
| 352 |
+
- `page`: Page number for pagination (min 1) (number, optional)
|
| 353 |
+
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
|
| 354 |
+
- `repo`: Repository name (string, required)
|
| 355 |
+
- `run_id`: The unique identifier of the workflow run (number, required)
|
| 356 |
+
|
| 357 |
+
- **list_workflow_run_artifacts** - List workflow artifacts
|
| 358 |
+
- `owner`: Repository owner (string, required)
|
| 359 |
+
- `page`: Page number for pagination (min 1) (number, optional)
|
| 360 |
+
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
|
| 361 |
+
- `repo`: Repository name (string, required)
|
| 362 |
+
- `run_id`: The unique identifier of the workflow run (number, required)
|
| 363 |
+
|
| 364 |
+
- **list_workflow_runs** - List workflow runs
|
| 365 |
+
- `actor`: Returns someone's workflow runs. Use the login for the user who created the workflow run. (string, optional)
|
| 366 |
+
- `branch`: Returns workflow runs associated with a branch. Use the name of the branch. (string, optional)
|
| 367 |
+
- `event`: Returns workflow runs for a specific event type (string, optional)
|
| 368 |
+
- `owner`: Repository owner (string, required)
|
| 369 |
+
- `page`: Page number for pagination (min 1) (number, optional)
|
| 370 |
+
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
|
| 371 |
+
- `repo`: Repository name (string, required)
|
| 372 |
+
- `status`: Returns workflow runs with the check run status (string, optional)
|
| 373 |
+
- `workflow_id`: The workflow ID or workflow file name (string, required)
|
| 374 |
+
|
| 375 |
+
- **list_workflows** - List workflows
|
| 376 |
+
- `owner`: Repository owner (string, required)
|
| 377 |
+
- `page`: Page number for pagination (min 1) (number, optional)
|
| 378 |
+
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
|
| 379 |
+
- `repo`: Repository name (string, required)
|
| 380 |
+
|
| 381 |
+
- **rerun_failed_jobs** - Rerun failed jobs
|
| 382 |
+
- `owner`: Repository owner (string, required)
|
| 383 |
+
- `repo`: Repository name (string, required)
|
| 384 |
+
- `run_id`: The unique identifier of the workflow run (number, required)
|
| 385 |
+
|
| 386 |
+
- **rerun_workflow_run** - Rerun workflow run
|
| 387 |
+
- `owner`: Repository owner (string, required)
|
| 388 |
+
- `repo`: Repository name (string, required)
|
| 389 |
+
- `run_id`: The unique identifier of the workflow run (number, required)
|
| 390 |
+
|
| 391 |
+
- **run_workflow** - Run workflow
|
| 392 |
+
- `inputs`: Inputs the workflow accepts (object, optional)
|
| 393 |
+
- `owner`: Repository owner (string, required)
|
| 394 |
+
- `ref`: The git reference for the workflow. The reference can be a branch or tag name. (string, required)
|
| 395 |
+
- `repo`: Repository name (string, required)
|
| 396 |
+
- `workflow_id`: The workflow ID (numeric) or workflow file name (e.g., main.yml, ci.yaml) (string, required)
|
| 397 |
+
|
| 398 |
+
</details>
|
| 399 |
+
|
| 400 |
+
<details>
|
| 401 |
+
|
| 402 |
+
<summary>Code Security</summary>
|
| 403 |
+
|
| 404 |
+
- **get_code_scanning_alert** - Get code scanning alert
|
| 405 |
+
- `alertNumber`: The number of the alert. (number, required)
|
| 406 |
+
- `owner`: The owner of the repository. (string, required)
|
| 407 |
+
- `repo`: The name of the repository. (string, required)
|
| 408 |
+
|
| 409 |
+
- **list_code_scanning_alerts** - List code scanning alerts
|
| 410 |
+
- `owner`: The owner of the repository. (string, required)
|
| 411 |
+
- `ref`: The Git reference for the results you want to list. (string, optional)
|
| 412 |
+
- `repo`: The name of the repository. (string, required)
|
| 413 |
+
- `severity`: Filter code scanning alerts by severity (string, optional)
|
| 414 |
+
- `state`: Filter code scanning alerts by state. Defaults to open (string, optional)
|
| 415 |
+
- `tool_name`: The name of the tool used for code scanning. (string, optional)
|
| 416 |
+
|
| 417 |
+
</details>
|
| 418 |
+
|
| 419 |
+
<details>
|
| 420 |
+
|
| 421 |
+
<summary>Context</summary>
|
| 422 |
+
|
| 423 |
+
- **get_me** - Get my user profile
|
| 424 |
+
- No parameters required
|
| 425 |
+
|
| 426 |
+
- **get_team_members** - Get team members
|
| 427 |
+
- `org`: Organization login (owner) that contains the team. (string, required)
|
| 428 |
+
- `team_slug`: Team slug (string, required)
|
| 429 |
+
|
| 430 |
+
- **get_teams** - Get teams
|
| 431 |
+
- `user`: Username to get teams for. If not provided, uses the authenticated user. (string, optional)
|
| 432 |
+
|
| 433 |
+
</details>
|
| 434 |
+
|
| 435 |
+
<details>
|
| 436 |
+
|
| 437 |
+
<summary>Dependabot</summary>
|
| 438 |
+
|
| 439 |
+
- **get_dependabot_alert** - Get dependabot alert
|
| 440 |
+
- `alertNumber`: The number of the alert. (number, required)
|
| 441 |
+
- `owner`: The owner of the repository. (string, required)
|
| 442 |
+
- `repo`: The name of the repository. (string, required)
|
| 443 |
+
|
| 444 |
+
- **list_dependabot_alerts** - List dependabot alerts
|
| 445 |
+
- `owner`: The owner of the repository. (string, required)
|
| 446 |
+
- `repo`: The name of the repository. (string, required)
|
| 447 |
+
- `severity`: Filter dependabot alerts by severity (string, optional)
|
| 448 |
+
- `state`: Filter dependabot alerts by state. Defaults to open (string, optional)
|
| 449 |
+
|
| 450 |
+
</details>
|
| 451 |
+
|
| 452 |
+
<details>
|
| 453 |
+
|
| 454 |
+
<summary>Discussions</summary>
|
| 455 |
+
|
| 456 |
+
- **get_discussion** - Get discussion
|
| 457 |
+
- `discussionNumber`: Discussion Number (number, required)
|
| 458 |
+
- `owner`: Repository owner (string, required)
|
| 459 |
+
- `repo`: Repository name (string, required)
|
| 460 |
+
|
| 461 |
+
- **get_discussion_comments** - Get discussion comments
|
| 462 |
+
- `after`: Cursor for pagination. Use the endCursor from the previous page's PageInfo for GraphQL APIs. (string, optional)
|
| 463 |
+
- `discussionNumber`: Discussion Number (number, required)
|
| 464 |
+
- `owner`: Repository owner (string, required)
|
| 465 |
+
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
|
| 466 |
+
- `repo`: Repository name (string, required)
|
| 467 |
+
|
| 468 |
+
- **list_discussion_categories** - List discussion categories
|
| 469 |
+
- `owner`: Repository owner (string, required)
|
| 470 |
+
- `repo`: Repository name. If not provided, discussion categories will be queried at the organisation level. (string, optional)
|
| 471 |
+
|
| 472 |
+
- **list_discussions** - List discussions
|
| 473 |
+
- `after`: Cursor for pagination. Use the endCursor from the previous page's PageInfo for GraphQL APIs. (string, optional)
|
| 474 |
+
- `category`: Optional filter by discussion category ID. If provided, only discussions with this category are listed. (string, optional)
|
| 475 |
+
- `direction`: Order direction. (string, optional)
|
| 476 |
+
- `orderBy`: Order discussions by field. If provided, the 'direction' also needs to be provided. (string, optional)
|
| 477 |
+
- `owner`: Repository owner (string, required)
|
| 478 |
+
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
|
| 479 |
+
- `repo`: Repository name. If not provided, discussions will be queried at the organisation level. (string, optional)
|
| 480 |
+
|
| 481 |
+
</details>
|
| 482 |
+
|
| 483 |
+
<details>
|
| 484 |
+
|
| 485 |
+
<summary>Gists</summary>
|
| 486 |
+
|
| 487 |
+
- **create_gist** - Create Gist
|
| 488 |
+
- `content`: Content for simple single-file gist creation (string, required)
|
| 489 |
+
- `description`: Description of the gist (string, optional)
|
| 490 |
+
- `filename`: Filename for simple single-file gist creation (string, required)
|
| 491 |
+
- `public`: Whether the gist is public (boolean, optional)
|
| 492 |
+
|
| 493 |
+
- **list_gists** - List Gists
|
| 494 |
+
- `page`: Page number for pagination (min 1) (number, optional)
|
| 495 |
+
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
|
| 496 |
+
- `since`: Only gists updated after this time (ISO 8601 timestamp) (string, optional)
|
| 497 |
+
- `username`: GitHub username (omit for authenticated user's gists) (string, optional)
|
| 498 |
+
|
| 499 |
+
- **update_gist** - Update Gist
|
| 500 |
+
- `content`: Content for the file (string, required)
|
| 501 |
+
- `description`: Updated description of the gist (string, optional)
|
| 502 |
+
- `filename`: Filename to update or create (string, required)
|
| 503 |
+
- `gist_id`: ID of the gist to update (string, required)
|
| 504 |
+
|
| 505 |
+
</details>
|
| 506 |
+
|
| 507 |
+
<details>
|
| 508 |
+
|
| 509 |
+
<summary>Issues</summary>
|
| 510 |
+
|
| 511 |
+
- **add_issue_comment** - Add comment to issue
|
| 512 |
+
- `body`: Comment content (string, required)
|
| 513 |
+
- `issue_number`: Issue number to comment on (number, required)
|
| 514 |
+
- `owner`: Repository owner (string, required)
|
| 515 |
+
- `repo`: Repository name (string, required)
|
| 516 |
+
|
| 517 |
+
- **add_sub_issue** - Add sub-issue
|
| 518 |
+
- `issue_number`: The number of the parent issue (number, required)
|
| 519 |
+
- `owner`: Repository owner (string, required)
|
| 520 |
+
- `replace_parent`: When true, replaces the sub-issue's current parent issue (boolean, optional)
|
| 521 |
+
- `repo`: Repository name (string, required)
|
| 522 |
+
- `sub_issue_id`: The ID of the sub-issue to add. ID is not the same as issue number (number, required)
|
| 523 |
+
|
| 524 |
+
- **assign_copilot_to_issue** - Assign Copilot to issue
|
| 525 |
+
- `issueNumber`: Issue number (number, required)
|
| 526 |
+
- `owner`: Repository owner (string, required)
|
| 527 |
+
- `repo`: Repository name (string, required)
|
| 528 |
+
|
| 529 |
+
- **create_issue** - Open new issue
|
| 530 |
+
- `assignees`: Usernames to assign to this issue (string[], optional)
|
| 531 |
+
- `body`: Issue body content (string, optional)
|
| 532 |
+
- `labels`: Labels to apply to this issue (string[], optional)
|
| 533 |
+
- `milestone`: Milestone number (number, optional)
|
| 534 |
+
- `owner`: Repository owner (string, required)
|
| 535 |
+
- `repo`: Repository name (string, required)
|
| 536 |
+
- `title`: Issue title (string, required)
|
| 537 |
+
- `type`: Type of this issue (string, optional)
|
| 538 |
+
|
| 539 |
+
- **get_issue** - Get issue details
|
| 540 |
+
- `issue_number`: The number of the issue (number, required)
|
| 541 |
+
- `owner`: The owner of the repository (string, required)
|
| 542 |
+
- `repo`: The name of the repository (string, required)
|
| 543 |
+
|
| 544 |
+
- **get_issue_comments** - Get issue comments
|
| 545 |
+
- `issue_number`: Issue number (number, required)
|
| 546 |
+
- `owner`: Repository owner (string, required)
|
| 547 |
+
- `page`: Page number for pagination (min 1) (number, optional)
|
| 548 |
+
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
|
| 549 |
+
- `repo`: Repository name (string, required)
|
| 550 |
+
|
| 551 |
+
- **list_issue_types** - List available issue types
|
| 552 |
+
- `owner`: The organization owner of the repository (string, required)
|
| 553 |
+
|
| 554 |
+
- **list_issues** - List issues
|
| 555 |
+
- `after`: Cursor for pagination. Use the endCursor from the previous page's PageInfo for GraphQL APIs. (string, optional)
|
| 556 |
+
- `direction`: Order direction. If provided, the 'orderBy' also needs to be provided. (string, optional)
|
| 557 |
+
- `labels`: Filter by labels (string[], optional)
|
| 558 |
+
- `orderBy`: Order issues by field. If provided, the 'direction' also needs to be provided. (string, optional)
|
| 559 |
+
- `owner`: Repository owner (string, required)
|
| 560 |
+
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
|
| 561 |
+
- `repo`: Repository name (string, required)
|
| 562 |
+
- `since`: Filter by date (ISO 8601 timestamp) (string, optional)
|
| 563 |
+
- `state`: Filter by state, by default both open and closed issues are returned when not provided (string, optional)
|
| 564 |
+
|
| 565 |
+
- **list_sub_issues** - List sub-issues
|
| 566 |
+
- `issue_number`: Issue number (number, required)
|
| 567 |
+
- `owner`: Repository owner (string, required)
|
| 568 |
+
- `page`: Page number for pagination (default: 1) (number, optional)
|
| 569 |
+
- `per_page`: Number of results per page (max 100, default: 30) (number, optional)
|
| 570 |
+
- `repo`: Repository name (string, required)
|
| 571 |
+
|
| 572 |
+
- **remove_sub_issue** - Remove sub-issue
|
| 573 |
+
- `issue_number`: The number of the parent issue (number, required)
|
| 574 |
+
- `owner`: Repository owner (string, required)
|
| 575 |
+
- `repo`: Repository name (string, required)
|
| 576 |
+
- `sub_issue_id`: The ID of the sub-issue to remove. ID is not the same as issue number (number, required)
|
| 577 |
+
|
| 578 |
+
- **reprioritize_sub_issue** - Reprioritize sub-issue
|
| 579 |
+
- `after_id`: The ID of the sub-issue to be prioritized after (either after_id OR before_id should be specified) (number, optional)
|
| 580 |
+
- `before_id`: The ID of the sub-issue to be prioritized before (either after_id OR before_id should be specified) (number, optional)
|
| 581 |
+
- `issue_number`: The number of the parent issue (number, required)
|
| 582 |
+
- `owner`: Repository owner (string, required)
|
| 583 |
+
- `repo`: Repository name (string, required)
|
| 584 |
+
- `sub_issue_id`: The ID of the sub-issue to reprioritize. ID is not the same as issue number (number, required)
|
| 585 |
+
|
| 586 |
+
- **search_issues** - Search issues
|
| 587 |
+
- `order`: Sort order (string, optional)
|
| 588 |
+
- `owner`: Optional repository owner. If provided with repo, only issues for this repository are listed. (string, optional)
|
| 589 |
+
- `page`: Page number for pagination (min 1) (number, optional)
|
| 590 |
+
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
|
| 591 |
+
- `query`: Search query using GitHub issues search syntax (string, required)
|
| 592 |
+
- `repo`: Optional repository name. If provided with owner, only issues for this repository are listed. (string, optional)
|
| 593 |
+
- `sort`: Sort field by number of matches of categories, defaults to best match (string, optional)
|
| 594 |
+
|
| 595 |
+
- **update_issue** - Edit issue
|
| 596 |
+
- `assignees`: New assignees (string[], optional)
|
| 597 |
+
- `body`: New description (string, optional)
|
| 598 |
+
- `issue_number`: Issue number to update (number, required)
|
| 599 |
+
- `labels`: New labels (string[], optional)
|
| 600 |
+
- `milestone`: New milestone number (number, optional)
|
| 601 |
+
- `owner`: Repository owner (string, required)
|
| 602 |
+
- `repo`: Repository name (string, required)
|
| 603 |
+
- `state`: New state (string, optional)
|
| 604 |
+
- `title`: New title (string, optional)
|
| 605 |
+
- `type`: New issue type (string, optional)
|
| 606 |
+
|
| 607 |
+
</details>
|
| 608 |
+
|
| 609 |
+
<details>
|
| 610 |
+
|
| 611 |
+
<summary>Notifications</summary>
|
| 612 |
+
|
| 613 |
+
- **dismiss_notification** - Dismiss notification
|
| 614 |
+
- `state`: The new state of the notification (read/done) (string, optional)
|
| 615 |
+
- `threadID`: The ID of the notification thread (string, required)
|
| 616 |
+
|
| 617 |
+
- **get_notification_details** - Get notification details
|
| 618 |
+
- `notificationID`: The ID of the notification (string, required)
|
| 619 |
+
|
| 620 |
+
- **list_notifications** - List notifications
|
| 621 |
+
- `before`: Only show notifications updated before the given time (ISO 8601 format) (string, optional)
|
| 622 |
+
- `filter`: Filter notifications to, use default unless specified. Read notifications are ones that have already been acknowledged by the user. Participating notifications are those that the user is directly involved in, such as issues or pull requests they have commented on or created. (string, optional)
|
| 623 |
+
- `owner`: Optional repository owner. If provided with repo, only notifications for this repository are listed. (string, optional)
|
| 624 |
+
- `page`: Page number for pagination (min 1) (number, optional)
|
| 625 |
+
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
|
| 626 |
+
- `repo`: Optional repository name. If provided with owner, only notifications for this repository are listed. (string, optional)
|
| 627 |
+
- `since`: Only show notifications updated after the given time (ISO 8601 format) (string, optional)
|
| 628 |
+
|
| 629 |
+
- **manage_notification_subscription** - Manage notification subscription
|
| 630 |
+
- `action`: Action to perform: ignore, watch, or delete the notification subscription. (string, required)
|
| 631 |
+
- `notificationID`: The ID of the notification thread. (string, required)
|
| 632 |
+
|
| 633 |
+
- **manage_repository_notification_subscription** - Manage repository notification subscription
|
| 634 |
+
- `action`: Action to perform: ignore, watch, or delete the repository notification subscription. (string, required)
|
| 635 |
+
- `owner`: The account owner of the repository. (string, required)
|
| 636 |
+
- `repo`: The name of the repository. (string, required)
|
| 637 |
+
|
| 638 |
+
- **mark_all_notifications_read** - Mark all notifications as read
|
| 639 |
+
- `lastReadAt`: Describes the last point that notifications were checked (optional). Default: Now (string, optional)
|
| 640 |
+
- `owner`: Optional repository owner. If provided with repo, only notifications for this repository are marked as read. (string, optional)
|
| 641 |
+
- `repo`: Optional repository name. If provided with owner, only notifications for this repository are marked as read. (string, optional)
|
| 642 |
+
|
| 643 |
+
</details>
|
| 644 |
+
|
| 645 |
+
<details>
|
| 646 |
+
|
| 647 |
+
<summary>Organizations</summary>
|
| 648 |
+
|
| 649 |
+
- **search_orgs** - Search organizations
|
| 650 |
+
- `order`: Sort order (string, optional)
|
| 651 |
+
- `page`: Page number for pagination (min 1) (number, optional)
|
| 652 |
+
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
|
| 653 |
+
- `query`: Organization search query. Examples: 'microsoft', 'location:california', 'created:>=2025-01-01'. Search is automatically scoped to type:org. (string, required)
|
| 654 |
+
- `sort`: Sort field by category (string, optional)
|
| 655 |
+
|
| 656 |
+
</details>
|
| 657 |
+
|
| 658 |
+
<details>
|
| 659 |
+
|
| 660 |
+
<summary>Pull Requests</summary>
|
| 661 |
+
|
| 662 |
+
- **add_comment_to_pending_review** - Add review comment to the requester's latest pending pull request review
|
| 663 |
+
- `body`: The text of the review comment (string, required)
|
| 664 |
+
- `line`: The line of the blob in the pull request diff that the comment applies to. For multi-line comments, the last line of the range (number, optional)
|
| 665 |
+
- `owner`: Repository owner (string, required)
|
| 666 |
+
- `path`: The relative path to the file that necessitates a comment (string, required)
|
| 667 |
+
- `pullNumber`: Pull request number (number, required)
|
| 668 |
+
- `repo`: Repository name (string, required)
|
| 669 |
+
- `side`: The side of the diff to comment on. LEFT indicates the previous state, RIGHT indicates the new state (string, optional)
|
| 670 |
+
- `startLine`: For multi-line comments, the first line of the range that the comment applies to (number, optional)
|
| 671 |
+
- `startSide`: For multi-line comments, the starting side of the diff that the comment applies to. LEFT indicates the previous state, RIGHT indicates the new state (string, optional)
|
| 672 |
+
- `subjectType`: The level at which the comment is targeted (string, required)
|
| 673 |
+
|
| 674 |
+
- **create_and_submit_pull_request_review** - Create and submit a pull request review without comments
|
| 675 |
+
- `body`: Review comment text (string, required)
|
| 676 |
+
- `commitID`: SHA of commit to review (string, optional)
|
| 677 |
+
- `event`: Review action to perform (string, required)
|
| 678 |
+
- `owner`: Repository owner (string, required)
|
| 679 |
+
- `pullNumber`: Pull request number (number, required)
|
| 680 |
+
- `repo`: Repository name (string, required)
|
| 681 |
+
|
| 682 |
+
- **create_pending_pull_request_review** - Create pending pull request review
|
| 683 |
+
- `commitID`: SHA of commit to review (string, optional)
|
| 684 |
+
- `owner`: Repository owner (string, required)
|
| 685 |
+
- `pullNumber`: Pull request number (number, required)
|
| 686 |
+
- `repo`: Repository name (string, required)
|
| 687 |
+
|
| 688 |
+
- **create_pull_request** - Open new pull request
|
| 689 |
+
- `base`: Branch to merge into (string, required)
|
| 690 |
+
- `body`: PR description (string, optional)
|
| 691 |
+
- `draft`: Create as draft PR (boolean, optional)
|
| 692 |
+
- `head`: Branch containing changes (string, required)
|
| 693 |
+
- `maintainer_can_modify`: Allow maintainer edits (boolean, optional)
|
| 694 |
+
- `owner`: Repository owner (string, required)
|
| 695 |
+
- `repo`: Repository name (string, required)
|
| 696 |
+
- `title`: PR title (string, required)
|
| 697 |
+
|
| 698 |
+
- **delete_pending_pull_request_review** - Delete the requester's latest pending pull request review
|
| 699 |
+
- `owner`: Repository owner (string, required)
|
| 700 |
+
- `pullNumber`: Pull request number (number, required)
|
| 701 |
+
- `repo`: Repository name (string, required)
|
| 702 |
+
|
| 703 |
+
- **get_pull_request** - Get pull request details
|
| 704 |
+
- `owner`: Repository owner (string, required)
|
| 705 |
+
- `pullNumber`: Pull request number (number, required)
|
| 706 |
+
- `repo`: Repository name (string, required)
|
| 707 |
+
|
| 708 |
+
- **get_pull_request_comments** - Get pull request comments
|
| 709 |
+
- `owner`: Repository owner (string, required)
|
| 710 |
+
- `pullNumber`: Pull request number (number, required)
|
| 711 |
+
- `repo`: Repository name (string, required)
|
| 712 |
+
|
| 713 |
+
- **get_pull_request_diff** - Get pull request diff
|
| 714 |
+
- `owner`: Repository owner (string, required)
|
| 715 |
+
- `pullNumber`: Pull request number (number, required)
|
| 716 |
+
- `repo`: Repository name (string, required)
|
| 717 |
+
|
| 718 |
+
- **get_pull_request_files** - Get pull request files
|
| 719 |
+
- `owner`: Repository owner (string, required)
|
| 720 |
+
- `page`: Page number for pagination (min 1) (number, optional)
|
| 721 |
+
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
|
| 722 |
+
- `pullNumber`: Pull request number (number, required)
|
| 723 |
+
- `repo`: Repository name (string, required)
|
| 724 |
+
|
| 725 |
+
- **get_pull_request_reviews** - Get pull request reviews
|
| 726 |
+
- `owner`: Repository owner (string, required)
|
| 727 |
+
- `pullNumber`: Pull request number (number, required)
|
| 728 |
+
- `repo`: Repository name (string, required)
|
| 729 |
+
|
| 730 |
+
- **get_pull_request_status** - Get pull request status checks
|
| 731 |
+
- `owner`: Repository owner (string, required)
|
| 732 |
+
- `pullNumber`: Pull request number (number, required)
|
| 733 |
+
- `repo`: Repository name (string, required)
|
| 734 |
+
|
| 735 |
+
- **list_pull_requests** - List pull requests
|
| 736 |
+
- `base`: Filter by base branch (string, optional)
|
| 737 |
+
- `direction`: Sort direction (string, optional)
|
| 738 |
+
- `head`: Filter by head user/org and branch (string, optional)
|
| 739 |
+
- `owner`: Repository owner (string, required)
|
| 740 |
+
- `page`: Page number for pagination (min 1) (number, optional)
|
| 741 |
+
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
|
| 742 |
+
- `repo`: Repository name (string, required)
|
| 743 |
+
- `sort`: Sort by (string, optional)
|
| 744 |
+
- `state`: Filter by state (string, optional)
|
| 745 |
+
|
| 746 |
+
- **merge_pull_request** - Merge pull request
|
| 747 |
+
- `commit_message`: Extra detail for merge commit (string, optional)
|
| 748 |
+
- `commit_title`: Title for merge commit (string, optional)
|
| 749 |
+
- `merge_method`: Merge method (string, optional)
|
| 750 |
+
- `owner`: Repository owner (string, required)
|
| 751 |
+
- `pullNumber`: Pull request number (number, required)
|
| 752 |
+
- `repo`: Repository name (string, required)
|
| 753 |
+
|
| 754 |
+
- **request_copilot_review** - Request Copilot review
|
| 755 |
+
- `owner`: Repository owner (string, required)
|
| 756 |
+
- `pullNumber`: Pull request number (number, required)
|
| 757 |
+
- `repo`: Repository name (string, required)
|
| 758 |
+
|
| 759 |
+
- **search_pull_requests** - Search pull requests
|
| 760 |
+
- `order`: Sort order (string, optional)
|
| 761 |
+
- `owner`: Optional repository owner. If provided with repo, only pull requests for this repository are listed. (string, optional)
|
| 762 |
+
- `page`: Page number for pagination (min 1) (number, optional)
|
| 763 |
+
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
|
| 764 |
+
- `query`: Search query using GitHub pull request search syntax (string, required)
|
| 765 |
+
- `repo`: Optional repository name. If provided with owner, only pull requests for this repository are listed. (string, optional)
|
| 766 |
+
- `sort`: Sort field by number of matches of categories, defaults to best match (string, optional)
|
| 767 |
+
|
| 768 |
+
- **submit_pending_pull_request_review** - Submit the requester's latest pending pull request review
|
| 769 |
+
- `body`: The text of the review comment (string, optional)
|
| 770 |
+
- `event`: The event to perform (string, required)
|
| 771 |
+
- `owner`: Repository owner (string, required)
|
| 772 |
+
- `pullNumber`: Pull request number (number, required)
|
| 773 |
+
- `repo`: Repository name (string, required)
|
| 774 |
+
|
| 775 |
+
- **update_pull_request** - Edit pull request
|
| 776 |
+
- `base`: New base branch name (string, optional)
|
| 777 |
+
- `body`: New description (string, optional)
|
| 778 |
+
- `draft`: Mark pull request as draft (true) or ready for review (false) (boolean, optional)
|
| 779 |
+
- `maintainer_can_modify`: Allow maintainer edits (boolean, optional)
|
| 780 |
+
- `owner`: Repository owner (string, required)
|
| 781 |
+
- `pullNumber`: Pull request number to update (number, required)
|
| 782 |
+
- `repo`: Repository name (string, required)
|
| 783 |
+
- `reviewers`: GitHub usernames to request reviews from (string[], optional)
|
| 784 |
+
- `state`: New state (string, optional)
|
| 785 |
+
- `title`: New title (string, optional)
|
| 786 |
+
|
| 787 |
+
- **update_pull_request_branch** - Update pull request branch
|
| 788 |
+
- `expectedHeadSha`: The expected SHA of the pull request's HEAD ref (string, optional)
|
| 789 |
+
- `owner`: Repository owner (string, required)
|
| 790 |
+
- `pullNumber`: Pull request number (number, required)
|
| 791 |
+
- `repo`: Repository name (string, required)
|
| 792 |
+
|
| 793 |
+
</details>
|
| 794 |
+
|
| 795 |
+
<details>
|
| 796 |
+
|
| 797 |
+
<summary>Repositories</summary>
|
| 798 |
+
|
| 799 |
+
- **create_branch** - Create branch
|
| 800 |
+
- `branch`: Name for new branch (string, required)
|
| 801 |
+
- `from_branch`: Source branch (defaults to repo default) (string, optional)
|
| 802 |
+
- `owner`: Repository owner (string, required)
|
| 803 |
+
- `repo`: Repository name (string, required)
|
| 804 |
+
|
| 805 |
+
- **create_or_update_file** - Create or update file
|
| 806 |
+
- `branch`: Branch to create/update the file in (string, required)
|
| 807 |
+
- `content`: Content of the file (string, required)
|
| 808 |
+
- `message`: Commit message (string, required)
|
| 809 |
+
- `owner`: Repository owner (username or organization) (string, required)
|
| 810 |
+
- `path`: Path where to create/update the file (string, required)
|
| 811 |
+
- `repo`: Repository name (string, required)
|
| 812 |
+
- `sha`: Required if updating an existing file. The blob SHA of the file being replaced. (string, optional)
|
| 813 |
+
|
| 814 |
+
- **create_repository** - Create repository
|
| 815 |
+
- `autoInit`: Initialize with README (boolean, optional)
|
| 816 |
+
- `description`: Repository description (string, optional)
|
| 817 |
+
- `name`: Repository name (string, required)
|
| 818 |
+
- `private`: Whether repo should be private (boolean, optional)
|
| 819 |
+
|
| 820 |
+
- **delete_file** - Delete file
|
| 821 |
+
- `branch`: Branch to delete the file from (string, required)
|
| 822 |
+
- `message`: Commit message (string, required)
|
| 823 |
+
- `owner`: Repository owner (username or organization) (string, required)
|
| 824 |
+
- `path`: Path to the file to delete (string, required)
|
| 825 |
+
- `repo`: Repository name (string, required)
|
| 826 |
+
|
| 827 |
+
- **fork_repository** - Fork repository
|
| 828 |
+
- `organization`: Organization to fork to (string, optional)
|
| 829 |
+
- `owner`: Repository owner (string, required)
|
| 830 |
+
- `repo`: Repository name (string, required)
|
| 831 |
+
|
| 832 |
+
- **get_commit** - Get commit details
|
| 833 |
+
- `owner`: Repository owner (string, required)
|
| 834 |
+
- `page`: Page number for pagination (min 1) (number, optional)
|
| 835 |
+
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
|
| 836 |
+
- `repo`: Repository name (string, required)
|
| 837 |
+
- `sha`: Commit SHA, branch name, or tag name (string, required)
|
| 838 |
+
|
| 839 |
+
- **get_file_contents** - Get file or directory contents
|
| 840 |
+
- `owner`: Repository owner (username or organization) (string, required)
|
| 841 |
+
- `path`: Path to file/directory (directories must end with a slash '/') (string, optional)
|
| 842 |
+
- `ref`: Accepts optional git refs such as `refs/tags/{tag}`, `refs/heads/{branch}` or `refs/pull/{pr_number}/head` (string, optional)
|
| 843 |
+
- `repo`: Repository name (string, required)
|
| 844 |
+
- `sha`: Accepts optional commit SHA. If specified, it will be used instead of ref (string, optional)
|
| 845 |
+
|
| 846 |
+
- **get_latest_release** - Get latest release
|
| 847 |
+
- `owner`: Repository owner (string, required)
|
| 848 |
+
- `repo`: Repository name (string, required)
|
| 849 |
+
|
| 850 |
+
- **get_release_by_tag** - Get a release by tag name
|
| 851 |
+
- `owner`: Repository owner (string, required)
|
| 852 |
+
- `repo`: Repository name (string, required)
|
| 853 |
+
- `tag`: Tag name (e.g., 'v1.0.0') (string, required)
|
| 854 |
+
|
| 855 |
+
- **get_tag** - Get tag details
|
| 856 |
+
- `owner`: Repository owner (string, required)
|
| 857 |
+
- `repo`: Repository name (string, required)
|
| 858 |
+
- `tag`: Tag name (string, required)
|
| 859 |
+
|
| 860 |
+
- **list_branches** - List branches
|
| 861 |
+
- `owner`: Repository owner (string, required)
|
| 862 |
+
- `page`: Page number for pagination (min 1) (number, optional)
|
| 863 |
+
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
|
| 864 |
+
- `repo`: Repository name (string, required)
|
| 865 |
+
|
| 866 |
+
- **list_commits** - List commits
|
| 867 |
+
- `author`: Author username or email address to filter commits by (string, optional)
|
| 868 |
+
- `owner`: Repository owner (string, required)
|
| 869 |
+
- `page`: Page number for pagination (min 1) (number, optional)
|
| 870 |
+
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
|
| 871 |
+
- `repo`: Repository name (string, required)
|
| 872 |
+
- `sha`: Commit SHA, branch or tag name to list commits of. If not provided, uses the default branch of the repository. If a commit SHA is provided, will list commits up to that SHA. (string, optional)
|
| 873 |
+
|
| 874 |
+
- **list_releases** - List releases
|
| 875 |
+
- `owner`: Repository owner (string, required)
|
| 876 |
+
- `page`: Page number for pagination (min 1) (number, optional)
|
| 877 |
+
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
|
| 878 |
+
- `repo`: Repository name (string, required)
|
| 879 |
+
|
| 880 |
+
- **list_tags** - List tags
|
| 881 |
+
- `owner`: Repository owner (string, required)
|
| 882 |
+
- `page`: Page number for pagination (min 1) (number, optional)
|
| 883 |
+
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
|
| 884 |
+
- `repo`: Repository name (string, required)
|
| 885 |
+
|
| 886 |
+
- **push_files** - Push files to repository
|
| 887 |
+
- `branch`: Branch to push to (string, required)
|
| 888 |
+
- `files`: Array of file objects to push, each object with path (string) and content (string) (object[], required)
|
| 889 |
+
- `message`: Commit message (string, required)
|
| 890 |
+
- `owner`: Repository owner (string, required)
|
| 891 |
+
- `repo`: Repository name (string, required)
|
| 892 |
+
|
| 893 |
+
- **search_code** - Search code
|
| 894 |
+
- `order`: Sort order for results (string, optional)
|
| 895 |
+
- `page`: Page number for pagination (min 1) (number, optional)
|
| 896 |
+
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
|
| 897 |
+
- `query`: Search query using GitHub's powerful code search syntax. Examples: 'content:Skill language:Java org:github', 'NOT is:archived language:Python OR language:go', 'repo:github/github-mcp-server'. Supports exact matching, language filters, path filters, and more. (string, required)
|
| 898 |
+
- `sort`: Sort field ('indexed' only) (string, optional)
|
| 899 |
+
|
| 900 |
+
- **search_repositories** - Search repositories
|
| 901 |
+
- `page`: Page number for pagination (min 1) (number, optional)
|
| 902 |
+
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
|
| 903 |
+
- `query`: Repository search query. Examples: 'machine learning in:name stars:>1000 language:python', 'topic:react', 'user:facebook'. Supports advanced search syntax for precise filtering. (string, required)
|
| 904 |
+
|
| 905 |
+
</details>
|
| 906 |
+
|
| 907 |
+
<details>
|
| 908 |
+
|
| 909 |
+
<summary>Secret Protection</summary>
|
| 910 |
+
|
| 911 |
+
- **get_secret_scanning_alert** - Get secret scanning alert
|
| 912 |
+
- `alertNumber`: The number of the alert. (number, required)
|
| 913 |
+
- `owner`: The owner of the repository. (string, required)
|
| 914 |
+
- `repo`: The name of the repository. (string, required)
|
| 915 |
+
|
| 916 |
+
- **list_secret_scanning_alerts** - List secret scanning alerts
|
| 917 |
+
- `owner`: The owner of the repository. (string, required)
|
| 918 |
+
- `repo`: The name of the repository. (string, required)
|
| 919 |
+
- `resolution`: Filter by resolution (string, optional)
|
| 920 |
+
- `secret_type`: A comma-separated list of secret types to return. All default secret patterns are returned. To return generic patterns, pass the token name(s) in the parameter. (string, optional)
|
| 921 |
+
- `state`: Filter by state (string, optional)
|
| 922 |
+
|
| 923 |
+
</details>
|
| 924 |
+
|
| 925 |
+
<details>
|
| 926 |
+
|
| 927 |
+
<summary>Security Advisories</summary>
|
| 928 |
+
|
| 929 |
+
- **get_global_security_advisory** - Get a global security advisory
|
| 930 |
+
- `ghsaId`: GitHub Security Advisory ID (format: GHSA-xxxx-xxxx-xxxx). (string, required)
|
| 931 |
+
|
| 932 |
+
- **list_global_security_advisories** - List global security advisories
|
| 933 |
+
- `affects`: Filter advisories by affected package or version (e.g. "package1,package2@1.0.0"). (string, optional)
|
| 934 |
+
- `cveId`: Filter by CVE ID. (string, optional)
|
| 935 |
+
- `cwes`: Filter by Common Weakness Enumeration IDs (e.g. ["79", "284", "22"]). (string[], optional)
|
| 936 |
+
- `ecosystem`: Filter by package ecosystem. (string, optional)
|
| 937 |
+
- `ghsaId`: Filter by GitHub Security Advisory ID (format: GHSA-xxxx-xxxx-xxxx). (string, optional)
|
| 938 |
+
- `isWithdrawn`: Whether to only return withdrawn advisories. (boolean, optional)
|
| 939 |
+
- `modified`: Filter by publish or update date or date range (ISO 8601 date or range). (string, optional)
|
| 940 |
+
- `published`: Filter by publish date or date range (ISO 8601 date or range). (string, optional)
|
| 941 |
+
- `severity`: Filter by severity. (string, optional)
|
| 942 |
+
- `type`: Advisory type. (string, optional)
|
| 943 |
+
- `updated`: Filter by update date or date range (ISO 8601 date or range). (string, optional)
|
| 944 |
+
|
| 945 |
+
- **list_org_repository_security_advisories** - List org repository security advisories
|
| 946 |
+
- `direction`: Sort direction. (string, optional)
|
| 947 |
+
- `org`: The organization login. (string, required)
|
| 948 |
+
- `sort`: Sort field. (string, optional)
|
| 949 |
+
- `state`: Filter by advisory state. (string, optional)
|
| 950 |
+
|
| 951 |
+
- **list_repository_security_advisories** - List repository security advisories
|
| 952 |
+
- `direction`: Sort direction. (string, optional)
|
| 953 |
+
- `owner`: The owner of the repository. (string, required)
|
| 954 |
+
- `repo`: The name of the repository. (string, required)
|
| 955 |
+
- `sort`: Sort field. (string, optional)
|
| 956 |
+
- `state`: Filter by advisory state. (string, optional)
|
| 957 |
+
|
| 958 |
+
</details>
|
| 959 |
+
|
| 960 |
+
<details>
|
| 961 |
+
|
| 962 |
+
<summary>Users</summary>
|
| 963 |
+
|
| 964 |
+
- **search_users** - Search users
|
| 965 |
+
- `order`: Sort order (string, optional)
|
| 966 |
+
- `page`: Page number for pagination (min 1) (number, optional)
|
| 967 |
+
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
|
| 968 |
+
- `query`: User search query. Examples: 'john smith', 'location:seattle', 'followers:>100'. Search is automatically scoped to type:user. (string, required)
|
| 969 |
+
- `sort`: Sort users by number of followers or repositories, or when the person joined GitHub. (string, optional)
|
| 970 |
+
|
| 971 |
+
</details>
|
| 972 |
+
<!-- END AUTOMATED TOOLS -->
|
| 973 |
+
|
| 974 |
+
### Additional Tools in Remote Github MCP Server
|
| 975 |
+
|
| 976 |
+
<details>
|
| 977 |
+
|
| 978 |
+
<summary>Copilot coding agent</summary>
|
| 979 |
+
|
| 980 |
+
- **create_pull_request_with_copilot** - Perform task with GitHub Copilot coding agent
|
| 981 |
+
- `owner`: Repository owner. You can guess the owner, but confirm it with the user before proceeding. (string, required)
|
| 982 |
+
- `repo`: Repository name. You can guess the repository name, but confirm it with the user before proceeding. (string, required)
|
| 983 |
+
- `problem_statement`: Detailed description of the task to be performed (e.g., 'Implement a feature that does X', 'Fix bug Y', etc.) (string, required)
|
| 984 |
+
- `title`: Title for the pull request that will be created (string, required)
|
| 985 |
+
- `base_ref`: Git reference (e.g., branch) that the agent will start its work from. If not specified, defaults to the repository's default branch (string, optional)
|
| 986 |
+
|
| 987 |
+
</details>
|
| 988 |
+
|
| 989 |
+
#### Specifying Toolsets
|
| 990 |
+
|
| 991 |
+
To specify toolsets you want available to the LLM, you can pass an allow-list in two ways:
|
| 992 |
+
|
| 993 |
+
1. **Using Command Line Argument**:
|
| 994 |
+
|
| 995 |
+
```bash
|
| 996 |
+
github-mcp-server --toolsets repos,issues,pull_requests,actions,code_security
|
| 997 |
+
```
|
| 998 |
+
|
| 999 |
+
2. **Using Environment Variable**:
|
| 1000 |
+
```bash
|
| 1001 |
+
GITHUB_TOOLSETS="repos,issues,pull_requests,actions,code_security" ./github-mcp-server
|
| 1002 |
+
```
|
| 1003 |
+
|
| 1004 |
+
The environment variable `GITHUB_TOOLSETS` takes precedence over the command line argument if both are provided.
|
| 1005 |
+
|
| 1006 |
+
### Using Toolsets With Docker
|
| 1007 |
+
|
| 1008 |
+
When using Docker, you can pass the toolsets as environment variables:
|
| 1009 |
+
|
| 1010 |
+
```bash
|
| 1011 |
+
docker run -i --rm \
|
| 1012 |
+
-e GITHUB_PERSONAL_ACCESS_TOKEN=<your-token> \
|
| 1013 |
+
-e GITHUB_TOOLSETS="repos,issues,pull_requests,actions,code_security,experiments" \
|
| 1014 |
+
ghcr.io/github/github-mcp-server
|
| 1015 |
+
```
|
| 1016 |
+
|
| 1017 |
+
### The "all" Toolset
|
| 1018 |
+
|
| 1019 |
+
The special toolset `all` can be provided to enable all available toolsets regardless of any other configuration:
|
| 1020 |
+
|
| 1021 |
+
```bash
|
| 1022 |
+
./github-mcp-server --toolsets all
|
| 1023 |
+
```
|
| 1024 |
+
|
| 1025 |
+
Or using the environment variable:
|
| 1026 |
+
|
| 1027 |
+
```bash
|
| 1028 |
+
GITHUB_TOOLSETS="all" ./github-mcp-server
|
| 1029 |
+
```
|
| 1030 |
+
|
| 1031 |
+
## Dynamic Tool Discovery
|
| 1032 |
+
|
| 1033 |
+
**Note**: This feature is currently in beta and may not be available in all environments. Please test it out and let us know if you encounter any issues.
|
| 1034 |
+
|
| 1035 |
+
Instead of starting with all tools enabled, you can turn on dynamic toolset discovery. Dynamic toolsets allow the MCP host to list and enable toolsets in response to a user prompt. This should help to avoid situations where the model gets confused by the sheer number of tools available.
|
| 1036 |
+
|
| 1037 |
+
### Using Dynamic Tool Discovery
|
| 1038 |
+
|
| 1039 |
+
When using the binary, you can pass the `--dynamic-toolsets` flag.
|
| 1040 |
+
|
| 1041 |
+
```bash
|
| 1042 |
+
./github-mcp-server --dynamic-toolsets
|
| 1043 |
+
```
|
| 1044 |
+
|
| 1045 |
+
When using Docker, you can pass the toolsets as environment variables:
|
| 1046 |
+
|
| 1047 |
+
```bash
|
| 1048 |
+
docker run -i --rm \
|
| 1049 |
+
-e GITHUB_PERSONAL_ACCESS_TOKEN=<your-token> \
|
| 1050 |
+
-e GITHUB_DYNAMIC_TOOLSETS=1 \
|
| 1051 |
+
ghcr.io/github/github-mcp-server
|
| 1052 |
+
```
|
| 1053 |
+
|
| 1054 |
+
## Read-Only Mode
|
| 1055 |
+
|
| 1056 |
+
To run the server in read-only mode, you can use the `--read-only` flag. This will only offer read-only tools, preventing any modifications to repositories, issues, pull requests, etc.
|
| 1057 |
+
|
| 1058 |
+
```bash
|
| 1059 |
+
./github-mcp-server --read-only
|
| 1060 |
+
```
|
| 1061 |
+
|
| 1062 |
+
When using Docker, you can pass the read-only mode as an environment variable:
|
| 1063 |
+
|
| 1064 |
+
```bash
|
| 1065 |
+
docker run -i --rm \
|
| 1066 |
+
-e GITHUB_PERSONAL_ACCESS_TOKEN=<your-token> \
|
| 1067 |
+
-e GITHUB_READ_ONLY=1 \
|
| 1068 |
+
ghcr.io/github/github-mcp-server
|
| 1069 |
+
```
|
| 1070 |
+
|
| 1071 |
+
## GitHub Enterprise Server and Enterprise Cloud with data residency (ghe.com)
|
| 1072 |
+
|
| 1073 |
+
The flag `--gh-host` and the environment variable `GITHUB_HOST` can be used to set
|
| 1074 |
+
the hostname for GitHub Enterprise Server or GitHub Enterprise Cloud with data residency.
|
| 1075 |
+
|
| 1076 |
+
- For GitHub Enterprise Server, prefix the hostname with the `https://` URI scheme, as it otherwise defaults to `http://`, which GitHub Enterprise Server does not support.
|
| 1077 |
+
- For GitHub Enterprise Cloud with data residency, use `https://YOURSUBDOMAIN.ghe.com` as the hostname.
|
| 1078 |
+
``` json
|
| 1079 |
+
"github": {
|
| 1080 |
+
"command": "docker",
|
| 1081 |
+
"args": [
|
| 1082 |
+
"run",
|
| 1083 |
+
"-i",
|
| 1084 |
+
"--rm",
|
| 1085 |
+
"-e",
|
| 1086 |
+
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
| 1087 |
+
"-e",
|
| 1088 |
+
"GITHUB_HOST",
|
| 1089 |
+
"ghcr.io/github/github-mcp-server"
|
| 1090 |
+
],
|
| 1091 |
+
"env": {
|
| 1092 |
+
"GITHUB_PERSONAL_ACCESS_TOKEN": "${input:github_token}",
|
| 1093 |
+
"GITHUB_HOST": "https://<your GHES or ghe.com domain name>"
|
| 1094 |
+
}
|
| 1095 |
+
}
|
| 1096 |
+
```
|
| 1097 |
+
|
| 1098 |
+
## i18n / Overriding Descriptions
|
| 1099 |
+
|
| 1100 |
+
The descriptions of the tools can be overridden by creating a
|
| 1101 |
+
`github-mcp-server-config.json` file in the same directory as the binary.
|
| 1102 |
+
|
| 1103 |
+
The file should contain a JSON object with the tool names as keys and the new
|
| 1104 |
+
descriptions as values. For example:
|
| 1105 |
+
|
| 1106 |
+
```json
|
| 1107 |
+
{
|
| 1108 |
+
"TOOL_ADD_ISSUE_COMMENT_DESCRIPTION": "an alternative description",
|
| 1109 |
+
"TOOL_CREATE_BRANCH_DESCRIPTION": "Create a new branch in a GitHub repository"
|
| 1110 |
+
}
|
| 1111 |
+
```
|
| 1112 |
+
|
| 1113 |
+
You can create an export of the current translations by running the binary with
|
| 1114 |
+
the `--export-translations` flag.
|
| 1115 |
+
|
| 1116 |
+
This flag will preserve any translations/overrides you have made, while adding
|
| 1117 |
+
any new translations that have been added to the binary since the last time you
|
| 1118 |
+
exported.
|
| 1119 |
+
|
| 1120 |
+
```sh
|
| 1121 |
+
./github-mcp-server --export-translations
|
| 1122 |
+
cat github-mcp-server-config.json
|
| 1123 |
+
```
|
| 1124 |
+
|
| 1125 |
+
You can also use ENV vars to override the descriptions. The environment
|
| 1126 |
+
variable names are the same as the keys in the JSON file, prefixed with
|
| 1127 |
+
`GITHUB_MCP_` and all uppercase.
|
| 1128 |
+
|
| 1129 |
+
For example, to override the `TOOL_ADD_ISSUE_COMMENT_DESCRIPTION` tool, you can
|
| 1130 |
+
set the following environment variable:
|
| 1131 |
+
|
| 1132 |
+
```sh
|
| 1133 |
+
export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description"
|
| 1134 |
+
```
|
| 1135 |
+
|
| 1136 |
+
## Library Usage
|
| 1137 |
+
|
| 1138 |
+
The exported Go API of this module should currently be considered unstable, and subject to breaking changes. In the future, we may offer stability; please file an issue if there is a use case where this would be valuable.
|
| 1139 |
+
|
| 1140 |
+
## License
|
| 1141 |
+
|
| 1142 |
+
This project is licensed under the terms of the MIT open source license. Please refer to [MIT](./LICENSE) for the full terms.
|
SECURITY.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Thanks for helping make GitHub safe for everyone.
|
| 2 |
+
|
| 3 |
+
# Security
|
| 4 |
+
|
| 5 |
+
GitHub takes the security of our software products and services seriously, including all of the open source code repositories managed through our GitHub organizations, such as [GitHub](https://github.com/GitHub).
|
| 6 |
+
|
| 7 |
+
Even though [open source repositories are outside of the scope of our bug bounty program](https://bounty.github.com/index.html#scope) and therefore not eligible for bounty rewards, we will ensure that your finding gets passed along to the appropriate maintainers for remediation.
|
| 8 |
+
|
| 9 |
+
## Reporting Security Issues
|
| 10 |
+
|
| 11 |
+
If you believe you have found a security vulnerability in any GitHub-owned repository, please report it to us through coordinated disclosure.
|
| 12 |
+
|
| 13 |
+
**Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.**
|
| 14 |
+
|
| 15 |
+
Instead, please send an email to opensource-security[@]github.com.
|
| 16 |
+
|
| 17 |
+
Please include as much of the information listed below as you can to help us better understand and resolve the issue:
|
| 18 |
+
|
| 19 |
+
* The type of issue (e.g., buffer overflow, SQL injection, or cross-site scripting)
|
| 20 |
+
* Full paths of source file(s) related to the manifestation of the issue
|
| 21 |
+
* The location of the affected source code (tag/branch/commit or direct URL)
|
| 22 |
+
* Any special configuration required to reproduce the issue
|
| 23 |
+
* Step-by-step instructions to reproduce the issue
|
| 24 |
+
* Proof-of-concept or exploit code (if possible)
|
| 25 |
+
* Impact of the issue, including how an attacker might exploit the issue
|
| 26 |
+
|
| 27 |
+
This information will help us triage your report more quickly.
|
| 28 |
+
|
| 29 |
+
## Policy
|
| 30 |
+
|
| 31 |
+
See [GitHub's Safe Harbor Policy](https://docs.github.com/en/site-policy/security-policies/github-bug-bounty-program-legal-safe-harbor#1-safe-harbor-terms)
|
SUPPORT.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Support
|
| 2 |
+
|
| 3 |
+
## How to file issues and get help
|
| 4 |
+
|
| 5 |
+
This project uses GitHub issues to track bugs and feature requests. Please search the existing issues before filing new issues to avoid duplicates. For new issues, file your bug or feature request as a new issue.
|
| 6 |
+
|
| 7 |
+
For help or questions about using this project, please open an issue.
|
| 8 |
+
|
| 9 |
+
- The `github-mcp-server` is under active development and maintained by GitHub staff **AND THE COMMUNITY**. We will do our best to respond to support, feature requests, and community questions in a timely manner.
|
| 10 |
+
|
| 11 |
+
## GitHub Support Policy
|
| 12 |
+
|
| 13 |
+
Support for this project is limited to the resources listed above.
|
cmd/github-mcp-server/generate_docs.go
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package main
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"fmt"
|
| 6 |
+
"net/url"
|
| 7 |
+
"os"
|
| 8 |
+
"regexp"
|
| 9 |
+
"sort"
|
| 10 |
+
"strings"
|
| 11 |
+
|
| 12 |
+
"github.com/github/github-mcp-server/pkg/github"
|
| 13 |
+
"github.com/github/github-mcp-server/pkg/raw"
|
| 14 |
+
"github.com/github/github-mcp-server/pkg/toolsets"
|
| 15 |
+
"github.com/github/github-mcp-server/pkg/translations"
|
| 16 |
+
gogithub "github.com/google/go-github/v74/github"
|
| 17 |
+
"github.com/mark3labs/mcp-go/mcp"
|
| 18 |
+
"github.com/shurcooL/githubv4"
|
| 19 |
+
"github.com/spf13/cobra"
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
var generateDocsCmd = &cobra.Command{
|
| 23 |
+
Use: "generate-docs",
|
| 24 |
+
Short: "Generate documentation for tools and toolsets",
|
| 25 |
+
Long: `Generate the automated sections of README.md and docs/remote-server.md with current tool and toolset information.`,
|
| 26 |
+
RunE: func(_ *cobra.Command, _ []string) error {
|
| 27 |
+
return generateAllDocs()
|
| 28 |
+
},
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
func init() {
|
| 32 |
+
rootCmd.AddCommand(generateDocsCmd)
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// mockGetClient returns a mock GitHub client for documentation generation
|
| 36 |
+
func mockGetClient(_ context.Context) (*gogithub.Client, error) {
|
| 37 |
+
return gogithub.NewClient(nil), nil
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// mockGetGQLClient returns a mock GraphQL client for documentation generation
|
| 41 |
+
func mockGetGQLClient(_ context.Context) (*githubv4.Client, error) {
|
| 42 |
+
return githubv4.NewClient(nil), nil
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// mockGetRawClient returns a mock raw client for documentation generation
|
| 46 |
+
func mockGetRawClient(_ context.Context) (*raw.Client, error) {
|
| 47 |
+
return nil, nil
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
func generateAllDocs() error {
|
| 51 |
+
if err := generateReadmeDocs("README.md"); err != nil {
|
| 52 |
+
return fmt.Errorf("failed to generate README docs: %w", err)
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
if err := generateRemoteServerDocs("docs/remote-server.md"); err != nil {
|
| 56 |
+
return fmt.Errorf("failed to generate remote-server docs: %w", err)
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
return nil
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
func generateReadmeDocs(readmePath string) error {
|
| 63 |
+
// Create translation helper
|
| 64 |
+
t, _ := translations.TranslationHelper()
|
| 65 |
+
|
| 66 |
+
// Create toolset group with mock clients
|
| 67 |
+
tsg := github.DefaultToolsetGroup(false, mockGetClient, mockGetGQLClient, mockGetRawClient, t, 5000)
|
| 68 |
+
|
| 69 |
+
// Generate toolsets documentation
|
| 70 |
+
toolsetsDoc := generateToolsetsDoc(tsg)
|
| 71 |
+
|
| 72 |
+
// Generate tools documentation
|
| 73 |
+
toolsDoc := generateToolsDoc(tsg)
|
| 74 |
+
|
| 75 |
+
// Read the current README.md
|
| 76 |
+
// #nosec G304 - readmePath is controlled by command line flag, not user input
|
| 77 |
+
content, err := os.ReadFile(readmePath)
|
| 78 |
+
if err != nil {
|
| 79 |
+
return fmt.Errorf("failed to read README.md: %w", err)
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
// Replace toolsets section
|
| 83 |
+
updatedContent := replaceSection(string(content), "START AUTOMATED TOOLSETS", "END AUTOMATED TOOLSETS", toolsetsDoc)
|
| 84 |
+
|
| 85 |
+
// Replace tools section
|
| 86 |
+
updatedContent = replaceSection(updatedContent, "START AUTOMATED TOOLS", "END AUTOMATED TOOLS", toolsDoc)
|
| 87 |
+
|
| 88 |
+
// Write back to file
|
| 89 |
+
err = os.WriteFile(readmePath, []byte(updatedContent), 0600)
|
| 90 |
+
if err != nil {
|
| 91 |
+
return fmt.Errorf("failed to write README.md: %w", err)
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
fmt.Println("Successfully updated README.md with automated documentation")
|
| 95 |
+
return nil
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
func generateRemoteServerDocs(docsPath string) error {
|
| 99 |
+
content, err := os.ReadFile(docsPath) //#nosec G304
|
| 100 |
+
if err != nil {
|
| 101 |
+
return fmt.Errorf("failed to read docs file: %w", err)
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
toolsetsDoc := generateRemoteToolsetsDoc()
|
| 105 |
+
|
| 106 |
+
// Replace content between markers
|
| 107 |
+
startMarker := "<!-- START AUTOMATED TOOLSETS -->"
|
| 108 |
+
endMarker := "<!-- END AUTOMATED TOOLSETS -->"
|
| 109 |
+
|
| 110 |
+
contentStr := string(content)
|
| 111 |
+
startIndex := strings.Index(contentStr, startMarker)
|
| 112 |
+
endIndex := strings.Index(contentStr, endMarker)
|
| 113 |
+
|
| 114 |
+
if startIndex == -1 || endIndex == -1 {
|
| 115 |
+
return fmt.Errorf("automation markers not found in %s", docsPath)
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
newContent := contentStr[:startIndex] + startMarker + "\n" + toolsetsDoc + "\n" + endMarker + contentStr[endIndex+len(endMarker):]
|
| 119 |
+
|
| 120 |
+
return os.WriteFile(docsPath, []byte(newContent), 0600) //#nosec G306
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
func generateToolsetsDoc(tsg *toolsets.ToolsetGroup) string {
|
| 124 |
+
var lines []string
|
| 125 |
+
|
| 126 |
+
// Add table header and separator
|
| 127 |
+
lines = append(lines, "| Toolset | Description |")
|
| 128 |
+
lines = append(lines, "| ----------------------- | ------------------------------------------------------------- |")
|
| 129 |
+
|
| 130 |
+
// Add the context toolset row (handled separately in README)
|
| 131 |
+
lines = append(lines, "| `context` | **Strongly recommended**: Tools that provide context about the current user and GitHub context you are operating in |")
|
| 132 |
+
|
| 133 |
+
// Get all toolsets except context (which is handled separately above)
|
| 134 |
+
var toolsetNames []string
|
| 135 |
+
for name := range tsg.Toolsets {
|
| 136 |
+
if name != "context" && name != "dynamic" { // Skip context and dynamic toolsets as they're handled separately
|
| 137 |
+
toolsetNames = append(toolsetNames, name)
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
// Sort toolset names for consistent output
|
| 142 |
+
sort.Strings(toolsetNames)
|
| 143 |
+
|
| 144 |
+
for _, name := range toolsetNames {
|
| 145 |
+
toolset := tsg.Toolsets[name]
|
| 146 |
+
lines = append(lines, fmt.Sprintf("| `%s` | %s |", name, toolset.Description))
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
return strings.Join(lines, "\n")
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
func generateToolsDoc(tsg *toolsets.ToolsetGroup) string {
|
| 153 |
+
var sections []string
|
| 154 |
+
|
| 155 |
+
// Get all toolset names and sort them alphabetically for deterministic order
|
| 156 |
+
var toolsetNames []string
|
| 157 |
+
for name := range tsg.Toolsets {
|
| 158 |
+
if name != "dynamic" { // Skip dynamic toolset as it's handled separately
|
| 159 |
+
toolsetNames = append(toolsetNames, name)
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
+
sort.Strings(toolsetNames)
|
| 163 |
+
|
| 164 |
+
for _, toolsetName := range toolsetNames {
|
| 165 |
+
toolset := tsg.Toolsets[toolsetName]
|
| 166 |
+
|
| 167 |
+
tools := toolset.GetAvailableTools()
|
| 168 |
+
if len(tools) == 0 {
|
| 169 |
+
continue
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
// Sort tools by name for deterministic order
|
| 173 |
+
sort.Slice(tools, func(i, j int) bool {
|
| 174 |
+
return tools[i].Tool.Name < tools[j].Tool.Name
|
| 175 |
+
})
|
| 176 |
+
|
| 177 |
+
// Generate section header - capitalize first letter and replace underscores
|
| 178 |
+
sectionName := formatToolsetName(toolsetName)
|
| 179 |
+
|
| 180 |
+
var toolDocs []string
|
| 181 |
+
for _, serverTool := range tools {
|
| 182 |
+
toolDoc := generateToolDoc(serverTool.Tool)
|
| 183 |
+
toolDocs = append(toolDocs, toolDoc)
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
if len(toolDocs) > 0 {
|
| 187 |
+
section := fmt.Sprintf("<details>\n\n<summary>%s</summary>\n\n%s\n\n</details>",
|
| 188 |
+
sectionName, strings.Join(toolDocs, "\n\n"))
|
| 189 |
+
sections = append(sections, section)
|
| 190 |
+
}
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
return strings.Join(sections, "\n\n")
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
func formatToolsetName(name string) string {
|
| 197 |
+
switch name {
|
| 198 |
+
case "pull_requests":
|
| 199 |
+
return "Pull Requests"
|
| 200 |
+
case "repos":
|
| 201 |
+
return "Repositories"
|
| 202 |
+
case "code_security":
|
| 203 |
+
return "Code Security"
|
| 204 |
+
case "secret_protection":
|
| 205 |
+
return "Secret Protection"
|
| 206 |
+
case "orgs":
|
| 207 |
+
return "Organizations"
|
| 208 |
+
default:
|
| 209 |
+
// Fallback: capitalize first letter and replace underscores with spaces
|
| 210 |
+
parts := strings.Split(name, "_")
|
| 211 |
+
for i, part := range parts {
|
| 212 |
+
if len(part) > 0 {
|
| 213 |
+
parts[i] = strings.ToUpper(string(part[0])) + part[1:]
|
| 214 |
+
}
|
| 215 |
+
}
|
| 216 |
+
return strings.Join(parts, " ")
|
| 217 |
+
}
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
func generateToolDoc(tool mcp.Tool) string {
|
| 221 |
+
var lines []string
|
| 222 |
+
|
| 223 |
+
// Tool name only (using annotation name instead of verbose description)
|
| 224 |
+
lines = append(lines, fmt.Sprintf("- **%s** - %s", tool.Name, tool.Annotations.Title))
|
| 225 |
+
|
| 226 |
+
// Parameters
|
| 227 |
+
schema := tool.InputSchema
|
| 228 |
+
if len(schema.Properties) > 0 {
|
| 229 |
+
// Get parameter names and sort them for deterministic order
|
| 230 |
+
var paramNames []string
|
| 231 |
+
for propName := range schema.Properties {
|
| 232 |
+
paramNames = append(paramNames, propName)
|
| 233 |
+
}
|
| 234 |
+
sort.Strings(paramNames)
|
| 235 |
+
|
| 236 |
+
for _, propName := range paramNames {
|
| 237 |
+
prop := schema.Properties[propName]
|
| 238 |
+
required := contains(schema.Required, propName)
|
| 239 |
+
requiredStr := "optional"
|
| 240 |
+
if required {
|
| 241 |
+
requiredStr = "required"
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
// Get the type and description
|
| 245 |
+
typeStr := "unknown"
|
| 246 |
+
description := ""
|
| 247 |
+
|
| 248 |
+
if propMap, ok := prop.(map[string]interface{}); ok {
|
| 249 |
+
if typeVal, ok := propMap["type"].(string); ok {
|
| 250 |
+
if typeVal == "array" {
|
| 251 |
+
if items, ok := propMap["items"].(map[string]interface{}); ok {
|
| 252 |
+
if itemType, ok := items["type"].(string); ok {
|
| 253 |
+
typeStr = itemType + "[]"
|
| 254 |
+
}
|
| 255 |
+
} else {
|
| 256 |
+
typeStr = "array"
|
| 257 |
+
}
|
| 258 |
+
} else {
|
| 259 |
+
typeStr = typeVal
|
| 260 |
+
}
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
if desc, ok := propMap["description"].(string); ok {
|
| 264 |
+
description = desc
|
| 265 |
+
}
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
paramLine := fmt.Sprintf(" - `%s`: %s (%s, %s)", propName, description, typeStr, requiredStr)
|
| 269 |
+
lines = append(lines, paramLine)
|
| 270 |
+
}
|
| 271 |
+
} else {
|
| 272 |
+
lines = append(lines, " - No parameters required")
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
return strings.Join(lines, "\n")
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
func contains(slice []string, item string) bool {
|
| 279 |
+
for _, s := range slice {
|
| 280 |
+
if s == item {
|
| 281 |
+
return true
|
| 282 |
+
}
|
| 283 |
+
}
|
| 284 |
+
return false
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
func replaceSection(content, startMarker, endMarker, newContent string) string {
|
| 288 |
+
startPattern := fmt.Sprintf(`<!-- %s -->`, regexp.QuoteMeta(startMarker))
|
| 289 |
+
endPattern := fmt.Sprintf(`<!-- %s -->`, regexp.QuoteMeta(endMarker))
|
| 290 |
+
|
| 291 |
+
re := regexp.MustCompile(fmt.Sprintf(`(?s)%s.*?%s`, startPattern, endPattern))
|
| 292 |
+
|
| 293 |
+
replacement := fmt.Sprintf("<!-- %s -->\n%s\n<!-- %s -->", startMarker, newContent, endMarker)
|
| 294 |
+
|
| 295 |
+
return re.ReplaceAllString(content, replacement)
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
func generateRemoteToolsetsDoc() string {
|
| 299 |
+
var buf strings.Builder
|
| 300 |
+
|
| 301 |
+
// Create translation helper
|
| 302 |
+
t, _ := translations.TranslationHelper()
|
| 303 |
+
|
| 304 |
+
// Create toolset group with mock clients
|
| 305 |
+
tsg := github.DefaultToolsetGroup(false, mockGetClient, mockGetGQLClient, mockGetRawClient, t, 5000)
|
| 306 |
+
|
| 307 |
+
// Generate table header
|
| 308 |
+
buf.WriteString("| Name | Description | API URL | 1-Click Install (VS Code) | Read-only Link | 1-Click Read-only Install (VS Code) |\n")
|
| 309 |
+
buf.WriteString("|----------------|--------------------------------------------------|-------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n")
|
| 310 |
+
|
| 311 |
+
// Get all toolsets
|
| 312 |
+
toolsetNames := make([]string, 0, len(tsg.Toolsets))
|
| 313 |
+
for name := range tsg.Toolsets {
|
| 314 |
+
if name != "context" && name != "dynamic" { // Skip context and dynamic toolsets as they're handled separately
|
| 315 |
+
toolsetNames = append(toolsetNames, name)
|
| 316 |
+
}
|
| 317 |
+
}
|
| 318 |
+
sort.Strings(toolsetNames)
|
| 319 |
+
|
| 320 |
+
// Add "all" toolset first (special case)
|
| 321 |
+
buf.WriteString("| all | All available GitHub MCP tools | https://api.githubcopilot.com/mcp/ | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2F%22%7D) | [read-only](https://api.githubcopilot.com/mcp/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Freadonly%22%7D) |\n")
|
| 322 |
+
|
| 323 |
+
// Add individual toolsets
|
| 324 |
+
for _, name := range toolsetNames {
|
| 325 |
+
toolset := tsg.Toolsets[name]
|
| 326 |
+
|
| 327 |
+
formattedName := formatToolsetName(name)
|
| 328 |
+
description := toolset.Description
|
| 329 |
+
apiURL := fmt.Sprintf("https://api.githubcopilot.com/mcp/x/%s", name)
|
| 330 |
+
readonlyURL := fmt.Sprintf("https://api.githubcopilot.com/mcp/x/%s/readonly", name)
|
| 331 |
+
|
| 332 |
+
// Create install config JSON (URL encoded)
|
| 333 |
+
installConfig := url.QueryEscape(fmt.Sprintf(`{"type": "http","url": "%s"}`, apiURL))
|
| 334 |
+
readonlyConfig := url.QueryEscape(fmt.Sprintf(`{"type": "http","url": "%s"}`, readonlyURL))
|
| 335 |
+
|
| 336 |
+
// Fix URL encoding to use %20 instead of + for spaces
|
| 337 |
+
installConfig = strings.ReplaceAll(installConfig, "+", "%20")
|
| 338 |
+
readonlyConfig = strings.ReplaceAll(readonlyConfig, "+", "%20")
|
| 339 |
+
|
| 340 |
+
installLink := fmt.Sprintf("[Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-%s&config=%s)", name, installConfig)
|
| 341 |
+
readonlyInstallLink := fmt.Sprintf("[Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-%s&config=%s)", name, readonlyConfig)
|
| 342 |
+
|
| 343 |
+
buf.WriteString(fmt.Sprintf("| %-14s | %-48s | %-53s | %-218s | %-110s | %-288s |\n",
|
| 344 |
+
formattedName,
|
| 345 |
+
description,
|
| 346 |
+
apiURL,
|
| 347 |
+
installLink,
|
| 348 |
+
fmt.Sprintf("[read-only](%s)", readonlyURL),
|
| 349 |
+
readonlyInstallLink,
|
| 350 |
+
))
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
return buf.String()
|
| 354 |
+
}
|
cmd/github-mcp-server/main.go
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package main
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"errors"
|
| 5 |
+
"fmt"
|
| 6 |
+
"os"
|
| 7 |
+
"strings"
|
| 8 |
+
|
| 9 |
+
"github.com/github/github-mcp-server/internal/ghmcp"
|
| 10 |
+
"github.com/github/github-mcp-server/pkg/github"
|
| 11 |
+
"github.com/spf13/cobra"
|
| 12 |
+
"github.com/spf13/pflag"
|
| 13 |
+
"github.com/spf13/viper"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
// These variables are set by the build process using ldflags.
|
| 17 |
+
var version = "version"
|
| 18 |
+
var commit = "commit"
|
| 19 |
+
var date = "date"
|
| 20 |
+
|
| 21 |
+
var (
|
| 22 |
+
rootCmd = &cobra.Command{
|
| 23 |
+
Use: "server",
|
| 24 |
+
Short: "GitHub MCP Server",
|
| 25 |
+
Long: `A GitHub MCP server that handles various tools and resources.`,
|
| 26 |
+
Version: fmt.Sprintf("Version: %s\nCommit: %s\nBuild Date: %s", version, commit, date),
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
stdioCmd = &cobra.Command{
|
| 30 |
+
Use: "stdio",
|
| 31 |
+
Short: "Start stdio server",
|
| 32 |
+
Long: `Start a server that communicates via standard input/output streams using JSON-RPC messages.`,
|
| 33 |
+
RunE: func(_ *cobra.Command, _ []string) error {
|
| 34 |
+
token := viper.GetString("personal_access_token")
|
| 35 |
+
if token == "" {
|
| 36 |
+
return errors.New("GITHUB_PERSONAL_ACCESS_TOKEN not set")
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
// If you're wondering why we're not using viper.GetStringSlice("toolsets"),
|
| 40 |
+
// it's because viper doesn't handle comma-separated values correctly for env
|
| 41 |
+
// vars when using GetStringSlice.
|
| 42 |
+
// https://github.com/spf13/viper/issues/380
|
| 43 |
+
var enabledToolsets []string
|
| 44 |
+
if err := viper.UnmarshalKey("toolsets", &enabledToolsets); err != nil {
|
| 45 |
+
return fmt.Errorf("failed to unmarshal toolsets: %w", err)
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
stdioServerConfig := ghmcp.StdioServerConfig{
|
| 49 |
+
Version: version,
|
| 50 |
+
Host: viper.GetString("host"),
|
| 51 |
+
Token: token,
|
| 52 |
+
EnabledToolsets: enabledToolsets,
|
| 53 |
+
DynamicToolsets: viper.GetBool("dynamic_toolsets"),
|
| 54 |
+
ReadOnly: viper.GetBool("read-only"),
|
| 55 |
+
ExportTranslations: viper.GetBool("export-translations"),
|
| 56 |
+
EnableCommandLogging: viper.GetBool("enable-command-logging"),
|
| 57 |
+
LogFilePath: viper.GetString("log-file"),
|
| 58 |
+
ContentWindowSize: viper.GetInt("content-window-size"),
|
| 59 |
+
}
|
| 60 |
+
return ghmcp.RunStdioServer(stdioServerConfig)
|
| 61 |
+
},
|
| 62 |
+
}
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
func init() {
|
| 66 |
+
cobra.OnInitialize(initConfig)
|
| 67 |
+
rootCmd.SetGlobalNormalizationFunc(wordSepNormalizeFunc)
|
| 68 |
+
|
| 69 |
+
rootCmd.SetVersionTemplate("{{.Short}}\n{{.Version}}\n")
|
| 70 |
+
|
| 71 |
+
// Add global flags that will be shared by all commands
|
| 72 |
+
rootCmd.PersistentFlags().StringSlice("toolsets", github.DefaultTools, "An optional comma separated list of groups of tools to allow, defaults to enabling all")
|
| 73 |
+
rootCmd.PersistentFlags().Bool("dynamic-toolsets", false, "Enable dynamic toolsets")
|
| 74 |
+
rootCmd.PersistentFlags().Bool("read-only", false, "Restrict the server to read-only operations")
|
| 75 |
+
rootCmd.PersistentFlags().String("log-file", "", "Path to log file")
|
| 76 |
+
rootCmd.PersistentFlags().Bool("enable-command-logging", false, "When enabled, the server will log all command requests and responses to the log file")
|
| 77 |
+
rootCmd.PersistentFlags().Bool("export-translations", false, "Save translations to a JSON file")
|
| 78 |
+
rootCmd.PersistentFlags().String("gh-host", "", "Specify the GitHub hostname (for GitHub Enterprise etc.)")
|
| 79 |
+
rootCmd.PersistentFlags().Int("content-window-size", 5000, "Specify the content window size")
|
| 80 |
+
|
| 81 |
+
// Bind flag to viper
|
| 82 |
+
_ = viper.BindPFlag("toolsets", rootCmd.PersistentFlags().Lookup("toolsets"))
|
| 83 |
+
_ = viper.BindPFlag("dynamic_toolsets", rootCmd.PersistentFlags().Lookup("dynamic-toolsets"))
|
| 84 |
+
_ = viper.BindPFlag("read-only", rootCmd.PersistentFlags().Lookup("read-only"))
|
| 85 |
+
_ = viper.BindPFlag("log-file", rootCmd.PersistentFlags().Lookup("log-file"))
|
| 86 |
+
_ = viper.BindPFlag("enable-command-logging", rootCmd.PersistentFlags().Lookup("enable-command-logging"))
|
| 87 |
+
_ = viper.BindPFlag("export-translations", rootCmd.PersistentFlags().Lookup("export-translations"))
|
| 88 |
+
_ = viper.BindPFlag("host", rootCmd.PersistentFlags().Lookup("gh-host"))
|
| 89 |
+
_ = viper.BindPFlag("content-window-size", rootCmd.PersistentFlags().Lookup("content-window-size"))
|
| 90 |
+
|
| 91 |
+
// Add subcommands
|
| 92 |
+
rootCmd.AddCommand(stdioCmd)
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
func initConfig() {
|
| 96 |
+
// Initialize Viper configuration
|
| 97 |
+
viper.SetEnvPrefix("github")
|
| 98 |
+
viper.AutomaticEnv()
|
| 99 |
+
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
func main() {
|
| 103 |
+
if err := rootCmd.Execute(); err != nil {
|
| 104 |
+
fmt.Fprintf(os.Stderr, "%v\n", err)
|
| 105 |
+
os.Exit(1)
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
func wordSepNormalizeFunc(_ *pflag.FlagSet, name string) pflag.NormalizedName {
|
| 110 |
+
from := []string{"_"}
|
| 111 |
+
to := "-"
|
| 112 |
+
for _, sep := range from {
|
| 113 |
+
name = strings.ReplaceAll(name, sep, to)
|
| 114 |
+
}
|
| 115 |
+
return pflag.NormalizedName(name)
|
| 116 |
+
}
|
cmd/mcpcurl/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# mcpcurl
|
| 2 |
+
|
| 3 |
+
A CLI tool that dynamically builds commands based on schemas retrieved from MCP servers that can
|
| 4 |
+
be executed against the configured MCP server.
|
| 5 |
+
|
| 6 |
+
## Overview
|
| 7 |
+
|
| 8 |
+
`mcpcurl` is a command-line interface that:
|
| 9 |
+
|
| 10 |
+
1. Connects to an MCP server via stdio
|
| 11 |
+
2. Dynamically retrieves the available tools schema
|
| 12 |
+
3. Generates CLI commands corresponding to each tool
|
| 13 |
+
4. Handles parameter validation based on the schema
|
| 14 |
+
5. Executes commands and displays responses
|
| 15 |
+
|
| 16 |
+
## Installation
|
| 17 |
+
|
| 18 |
+
### Prerequisites
|
| 19 |
+
- Go 1.21 or later
|
| 20 |
+
- Access to the GitHub MCP Server from either Docker or local build
|
| 21 |
+
|
| 22 |
+
### Build from Source
|
| 23 |
+
```bash
|
| 24 |
+
cd cmd/mcpcurl
|
| 25 |
+
go build -o mcpcurl
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
### Using Go Install
|
| 29 |
+
```bash
|
| 30 |
+
go install github.com/github/github-mcp-server/cmd/mcpcurl@latest
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
### Verify Installation
|
| 34 |
+
```bash
|
| 35 |
+
./mcpcurl --help
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
## Usage
|
| 39 |
+
|
| 40 |
+
```console
|
| 41 |
+
mcpcurl --stdio-server-cmd="<command to start MCP server>" <command> [flags]
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
The `--stdio-server-cmd` flag is required for all commands and specifies the command to run the MCP server.
|
| 45 |
+
|
| 46 |
+
### Available Commands
|
| 47 |
+
|
| 48 |
+
- `tools`: Contains all dynamically generated tool commands from the schema
|
| 49 |
+
- `schema`: Fetches and displays the raw schema from the MCP server
|
| 50 |
+
- `help`: Shows help for any command
|
| 51 |
+
|
| 52 |
+
### Examples
|
| 53 |
+
|
| 54 |
+
List available tools in Github's MCP server:
|
| 55 |
+
|
| 56 |
+
```console
|
| 57 |
+
% ./mcpcurl --stdio-server-cmd "docker run -i --rm -e GITHUB_PERSONAL_ACCESS_TOKEN mcp/github" tools --help
|
| 58 |
+
Contains all dynamically generated tool commands from the schema
|
| 59 |
+
|
| 60 |
+
Usage:
|
| 61 |
+
mcpcurl tools [command]
|
| 62 |
+
|
| 63 |
+
Available Commands:
|
| 64 |
+
add_issue_comment Add a comment to an existing issue
|
| 65 |
+
create_branch Create a new branch in a GitHub repository
|
| 66 |
+
create_issue Create a new issue in a GitHub repository
|
| 67 |
+
create_or_update_file Create or update a single file in a GitHub repository
|
| 68 |
+
create_pull_request Create a new pull request in a GitHub repository
|
| 69 |
+
create_repository Create a new GitHub repository in your account
|
| 70 |
+
fork_repository Fork a GitHub repository to your account or specified organization
|
| 71 |
+
get_file_contents Get the contents of a file or directory from a GitHub repository
|
| 72 |
+
get_issue Get details of a specific issue in a GitHub repository
|
| 73 |
+
get_issue_comments Get comments for a GitHub issue
|
| 74 |
+
list_commits Get list of commits of a branch in a GitHub repository
|
| 75 |
+
list_issues List issues in a GitHub repository with filtering options
|
| 76 |
+
push_files Push multiple files to a GitHub repository in a single commit
|
| 77 |
+
search_code Search for code across GitHub repositories
|
| 78 |
+
search_issues Search for issues and pull requests across GitHub repositories
|
| 79 |
+
search_repositories Search for GitHub repositories
|
| 80 |
+
search_users Search for users on GitHub
|
| 81 |
+
update_issue Update an existing issue in a GitHub repository
|
| 82 |
+
|
| 83 |
+
Flags:
|
| 84 |
+
-h, --help help for tools
|
| 85 |
+
|
| 86 |
+
Global Flags:
|
| 87 |
+
--pretty Pretty print MCP response (only for JSON responses) (default true)
|
| 88 |
+
--stdio-server-cmd string Shell command to invoke MCP server via stdio (required)
|
| 89 |
+
|
| 90 |
+
Use "mcpcurl tools [command] --help" for more information about a command.
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
Get help for a specific tool:
|
| 94 |
+
|
| 95 |
+
```console
|
| 96 |
+
% ./mcpcurl --stdio-server-cmd "docker run -i --rm -e GITHUB_PERSONAL_ACCESS_TOKEN mcp/github" tools get_issue --help
|
| 97 |
+
Get details of a specific issue in a GitHub repository
|
| 98 |
+
|
| 99 |
+
Usage:
|
| 100 |
+
mcpcurl tools get_issue [flags]
|
| 101 |
+
|
| 102 |
+
Flags:
|
| 103 |
+
-h, --help help for get_issue
|
| 104 |
+
--issue_number float
|
| 105 |
+
--owner string
|
| 106 |
+
--repo string
|
| 107 |
+
|
| 108 |
+
Global Flags:
|
| 109 |
+
--pretty Pretty print MCP response (only for JSON responses) (default true)
|
| 110 |
+
--stdio-server-cmd string Shell command to invoke MCP server via stdio (required)
|
| 111 |
+
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
Use one of the tools:
|
| 115 |
+
|
| 116 |
+
```console
|
| 117 |
+
% ./mcpcurl --stdio-server-cmd "docker run -i --rm -e GITHUB_PERSONAL_ACCESS_TOKEN mcp/github" tools get_issue --owner golang --repo go --issue_number 1
|
| 118 |
+
{
|
| 119 |
+
"active_lock_reason": null,
|
| 120 |
+
"assignee": null,
|
| 121 |
+
"assignees": [],
|
| 122 |
+
"author_association": "CONTRIBUTOR",
|
| 123 |
+
"body": "by **rsc+personal@swtch.com**:\n\n\u003cpre\u003eWhat steps will reproduce the problem?\n1. Run build on Ubuntu 9.10, which uses gcc 4.4.1\n\nWhat is the expected output? What do you see instead?\n\nCgo fails with the following error:\n\n{{{\ngo/misc/cgo/stdio$ make\ncgo file.go\ncould not determine kind of name for C.CString\ncould not determine kind of name for C.puts\ncould not determine kind of name for C.fflushstdout\ncould not determine kind of name for C.free\nthrow: sys·mapaccess1: key not in map\n\npanic PC=0x2b01c2b96a08\nthrow+0x33 /media/scratch/workspace/go/src/pkg/runtime/runtime.c:71\n throw(0x4d2daf, 0x0)\nsys·mapaccess1+0x74 \n/media/scratch/workspace/go/src/pkg/runtime/hashmap.c:769\n sys·mapaccess1(0xc2b51930, 0x2b01)\nmain·*Prog·loadDebugInfo+0xa67 \n/media/scratch/workspace/go/src/cmd/cgo/gcc.go:164\n main·*Prog·loadDebugInfo(0xc2bc0000, 0x2b01)\nmain·main+0x352 \n/media/scratch/workspace/go/src/cmd/cgo/main.go:68\n main·main()\nmainstart+0xf \n/media/scratch/workspace/go/src/pkg/runtime/amd64/asm.s:55\n mainstart()\ngoexit /media/scratch/workspace/go/src/pkg/runtime/proc.c:133\n goexit()\nmake: *** [file.cgo1.go] Error 2\n}}}\n\nPlease use labels and text to provide additional information.\u003c/pre\u003e\n",
|
| 124 |
+
"closed_at": "2014-12-08T10:02:16Z",
|
| 125 |
+
"closed_by": null,
|
| 126 |
+
"comments": 12,
|
| 127 |
+
"comments_url": "https://api.github.com/repos/golang/go/issues/1/comments",
|
| 128 |
+
"created_at": "2009-10-22T06:07:26Z",
|
| 129 |
+
"events_url": "https://api.github.com/repos/golang/go/issues/1/events",
|
| 130 |
+
[...]
|
| 131 |
+
}
|
| 132 |
+
```
|
| 133 |
+
|
| 134 |
+
## Dynamic Commands
|
| 135 |
+
|
| 136 |
+
All tools provided by the MCP server are automatically available as subcommands under the `tools` command. Each generated command has:
|
| 137 |
+
|
| 138 |
+
- Appropriate flags matching the tool's input schema
|
| 139 |
+
- Validation for required parameters
|
| 140 |
+
- Type validation
|
| 141 |
+
- Enum validation (for string parameters with allowable values)
|
| 142 |
+
- Help text generated from the tool's description
|
| 143 |
+
|
| 144 |
+
## How It Works
|
| 145 |
+
|
| 146 |
+
1. `mcpcurl` makes a JSON-RPC request to the server using the `tools/list` method
|
| 147 |
+
2. The server responds with a schema describing all available tools
|
| 148 |
+
3. `mcpcurl` dynamically builds a command structure based on this schema
|
| 149 |
+
4. When a command is executed, arguments are converted to a JSON-RPC request
|
| 150 |
+
5. The request is sent to the server via stdin, and the response is printed to stdout
|
cmd/mcpcurl/main.go
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package main
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bytes"
|
| 5 |
+
"crypto/rand"
|
| 6 |
+
"encoding/json"
|
| 7 |
+
"fmt"
|
| 8 |
+
"io"
|
| 9 |
+
"math/big"
|
| 10 |
+
"os"
|
| 11 |
+
"os/exec"
|
| 12 |
+
"slices"
|
| 13 |
+
"strings"
|
| 14 |
+
|
| 15 |
+
"github.com/spf13/cobra"
|
| 16 |
+
"github.com/spf13/viper"
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
type (
|
| 20 |
+
// SchemaResponse represents the top-level response containing tools
|
| 21 |
+
SchemaResponse struct {
|
| 22 |
+
Result Result `json:"result"`
|
| 23 |
+
JSONRPC string `json:"jsonrpc"`
|
| 24 |
+
ID int `json:"id"`
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
// Result contains the list of available tools
|
| 28 |
+
Result struct {
|
| 29 |
+
Tools []Tool `json:"tools"`
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
// Tool represents a single command with its schema
|
| 33 |
+
Tool struct {
|
| 34 |
+
Name string `json:"name"`
|
| 35 |
+
Description string `json:"description"`
|
| 36 |
+
InputSchema InputSchema `json:"inputSchema"`
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
// InputSchema defines the structure of a tool's input parameters
|
| 40 |
+
InputSchema struct {
|
| 41 |
+
Type string `json:"type"`
|
| 42 |
+
Properties map[string]Property `json:"properties"`
|
| 43 |
+
Required []string `json:"required"`
|
| 44 |
+
AdditionalProperties bool `json:"additionalProperties"`
|
| 45 |
+
Schema string `json:"$schema"`
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// Property defines a single parameter's type and constraints
|
| 49 |
+
Property struct {
|
| 50 |
+
Type string `json:"type"`
|
| 51 |
+
Description string `json:"description"`
|
| 52 |
+
Enum []string `json:"enum,omitempty"`
|
| 53 |
+
Minimum *float64 `json:"minimum,omitempty"`
|
| 54 |
+
Maximum *float64 `json:"maximum,omitempty"`
|
| 55 |
+
Items *PropertyItem `json:"items,omitempty"`
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
// PropertyItem defines the type of items in an array property
|
| 59 |
+
PropertyItem struct {
|
| 60 |
+
Type string `json:"type"`
|
| 61 |
+
Properties map[string]Property `json:"properties,omitempty"`
|
| 62 |
+
Required []string `json:"required,omitempty"`
|
| 63 |
+
AdditionalProperties bool `json:"additionalProperties,omitempty"`
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
// JSONRPCRequest represents a JSON-RPC 2.0 request
|
| 67 |
+
JSONRPCRequest struct {
|
| 68 |
+
JSONRPC string `json:"jsonrpc"`
|
| 69 |
+
ID int `json:"id"`
|
| 70 |
+
Method string `json:"method"`
|
| 71 |
+
Params RequestParams `json:"params"`
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
// RequestParams contains the tool name and arguments
|
| 75 |
+
RequestParams struct {
|
| 76 |
+
Name string `json:"name"`
|
| 77 |
+
Arguments map[string]interface{} `json:"arguments"`
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// Content matches the response format of a text content response
|
| 81 |
+
Content struct {
|
| 82 |
+
Type string `json:"type"`
|
| 83 |
+
Text string `json:"text"`
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
ResponseResult struct {
|
| 87 |
+
Content []Content `json:"content"`
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
Response struct {
|
| 91 |
+
Result ResponseResult `json:"result"`
|
| 92 |
+
JSONRPC string `json:"jsonrpc"`
|
| 93 |
+
ID int `json:"id"`
|
| 94 |
+
}
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
var (
|
| 98 |
+
// Create root command
|
| 99 |
+
rootCmd = &cobra.Command{
|
| 100 |
+
Use: "mcpcurl",
|
| 101 |
+
Short: "CLI tool with dynamically generated commands",
|
| 102 |
+
Long: "A CLI tool for interacting with MCP API based on dynamically loaded schemas",
|
| 103 |
+
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
|
| 104 |
+
// Skip validation for help and completion commands
|
| 105 |
+
if cmd.Name() == "help" || cmd.Name() == "completion" {
|
| 106 |
+
return nil
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
// Check if the required global flag is provided
|
| 110 |
+
serverCmd, _ := cmd.Flags().GetString("stdio-server-cmd")
|
| 111 |
+
if serverCmd == "" {
|
| 112 |
+
return fmt.Errorf("--stdio-server-cmd is required")
|
| 113 |
+
}
|
| 114 |
+
return nil
|
| 115 |
+
},
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
// Add schema command
|
| 119 |
+
schemaCmd = &cobra.Command{
|
| 120 |
+
Use: "schema",
|
| 121 |
+
Short: "Fetch schema from MCP server",
|
| 122 |
+
Long: "Fetches the tools schema from the MCP server specified by --stdio-server-cmd",
|
| 123 |
+
RunE: func(cmd *cobra.Command, _ []string) error {
|
| 124 |
+
serverCmd, _ := cmd.Flags().GetString("stdio-server-cmd")
|
| 125 |
+
if serverCmd == "" {
|
| 126 |
+
return fmt.Errorf("--stdio-server-cmd is required")
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
// Build the JSON-RPC request for tools/list
|
| 130 |
+
jsonRequest, err := buildJSONRPCRequest("tools/list", "", nil)
|
| 131 |
+
if err != nil {
|
| 132 |
+
return fmt.Errorf("failed to build JSON-RPC request: %w", err)
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
// Execute the server command and pass the JSON-RPC request
|
| 136 |
+
response, err := executeServerCommand(serverCmd, jsonRequest)
|
| 137 |
+
if err != nil {
|
| 138 |
+
return fmt.Errorf("error executing server command: %w", err)
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
// Output the response
|
| 142 |
+
fmt.Println(response)
|
| 143 |
+
return nil
|
| 144 |
+
},
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
// Create the tools command
|
| 148 |
+
toolsCmd = &cobra.Command{
|
| 149 |
+
Use: "tools",
|
| 150 |
+
Short: "Access available tools",
|
| 151 |
+
Long: "Contains all dynamically generated tool commands from the schema",
|
| 152 |
+
}
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
func main() {
|
| 156 |
+
rootCmd.AddCommand(schemaCmd)
|
| 157 |
+
|
| 158 |
+
// Add global flag for stdio server command
|
| 159 |
+
rootCmd.PersistentFlags().String("stdio-server-cmd", "", "Shell command to invoke MCP server via stdio (required)")
|
| 160 |
+
_ = rootCmd.MarkPersistentFlagRequired("stdio-server-cmd")
|
| 161 |
+
|
| 162 |
+
// Add global flag for pretty printing
|
| 163 |
+
rootCmd.PersistentFlags().Bool("pretty", true, "Pretty print MCP response (only for JSON or JSONL responses)")
|
| 164 |
+
|
| 165 |
+
// Add the tools command to the root command
|
| 166 |
+
rootCmd.AddCommand(toolsCmd)
|
| 167 |
+
|
| 168 |
+
// Execute the root command once to parse flags
|
| 169 |
+
_ = rootCmd.ParseFlags(os.Args[1:])
|
| 170 |
+
|
| 171 |
+
// Get pretty flag
|
| 172 |
+
prettyPrint, err := rootCmd.Flags().GetBool("pretty")
|
| 173 |
+
if err != nil {
|
| 174 |
+
_, _ = fmt.Fprintf(os.Stderr, "Error getting pretty flag: %v\n", err)
|
| 175 |
+
os.Exit(1)
|
| 176 |
+
}
|
| 177 |
+
// Get server command
|
| 178 |
+
serverCmd, err := rootCmd.Flags().GetString("stdio-server-cmd")
|
| 179 |
+
if err == nil && serverCmd != "" {
|
| 180 |
+
// Fetch schema from server
|
| 181 |
+
jsonRequest, err := buildJSONRPCRequest("tools/list", "", nil)
|
| 182 |
+
if err == nil {
|
| 183 |
+
response, err := executeServerCommand(serverCmd, jsonRequest)
|
| 184 |
+
if err == nil {
|
| 185 |
+
// Parse the schema response
|
| 186 |
+
var schemaResp SchemaResponse
|
| 187 |
+
if err := json.Unmarshal([]byte(response), &schemaResp); err == nil {
|
| 188 |
+
// Add all the generated commands as subcommands of tools
|
| 189 |
+
for _, tool := range schemaResp.Result.Tools {
|
| 190 |
+
addCommandFromTool(toolsCmd, &tool, prettyPrint)
|
| 191 |
+
}
|
| 192 |
+
}
|
| 193 |
+
}
|
| 194 |
+
}
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
// Execute
|
| 198 |
+
if err := rootCmd.Execute(); err != nil {
|
| 199 |
+
_, _ = fmt.Fprintf(os.Stderr, "Error executing command: %v\n", err)
|
| 200 |
+
os.Exit(1)
|
| 201 |
+
}
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
// addCommandFromTool creates a cobra command from a tool schema
|
| 205 |
+
func addCommandFromTool(toolsCmd *cobra.Command, tool *Tool, prettyPrint bool) {
|
| 206 |
+
// Create command from tool
|
| 207 |
+
cmd := &cobra.Command{
|
| 208 |
+
Use: tool.Name,
|
| 209 |
+
Short: tool.Description,
|
| 210 |
+
Run: func(cmd *cobra.Command, _ []string) {
|
| 211 |
+
// Build a map of arguments from flags
|
| 212 |
+
arguments, err := buildArgumentsMap(cmd, tool)
|
| 213 |
+
if err != nil {
|
| 214 |
+
_, _ = fmt.Fprintf(os.Stderr, "failed to build arguments map: %v\n", err)
|
| 215 |
+
return
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
jsonData, err := buildJSONRPCRequest("tools/call", tool.Name, arguments)
|
| 219 |
+
if err != nil {
|
| 220 |
+
_, _ = fmt.Fprintf(os.Stderr, "failed to build JSONRPC request: %v\n", err)
|
| 221 |
+
return
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
// Execute the server command
|
| 225 |
+
serverCmd, err := cmd.Flags().GetString("stdio-server-cmd")
|
| 226 |
+
if err != nil {
|
| 227 |
+
_, _ = fmt.Fprintf(os.Stderr, "failed to get stdio-server-cmd: %v\n", err)
|
| 228 |
+
return
|
| 229 |
+
}
|
| 230 |
+
response, err := executeServerCommand(serverCmd, jsonData)
|
| 231 |
+
if err != nil {
|
| 232 |
+
_, _ = fmt.Fprintf(os.Stderr, "error executing server command: %v\n", err)
|
| 233 |
+
return
|
| 234 |
+
}
|
| 235 |
+
if err := printResponse(response, prettyPrint); err != nil {
|
| 236 |
+
_, _ = fmt.Fprintf(os.Stderr, "error printing response: %v\n", err)
|
| 237 |
+
return
|
| 238 |
+
}
|
| 239 |
+
},
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
// Initialize viper for this command
|
| 243 |
+
viperInit := func() {
|
| 244 |
+
viper.Reset()
|
| 245 |
+
viper.AutomaticEnv()
|
| 246 |
+
viper.SetEnvPrefix(strings.ToUpper(tool.Name))
|
| 247 |
+
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
// We'll call the init function directly instead of with cobra.OnInitialize
|
| 251 |
+
// to avoid conflicts between commands
|
| 252 |
+
viperInit()
|
| 253 |
+
|
| 254 |
+
// Add flags based on schema properties
|
| 255 |
+
for name, prop := range tool.InputSchema.Properties {
|
| 256 |
+
isRequired := slices.Contains(tool.InputSchema.Required, name)
|
| 257 |
+
|
| 258 |
+
// Enhance description to indicate if parameter is optional
|
| 259 |
+
description := prop.Description
|
| 260 |
+
if !isRequired {
|
| 261 |
+
description += " (optional)"
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
switch prop.Type {
|
| 265 |
+
case "string":
|
| 266 |
+
cmd.Flags().String(name, "", description)
|
| 267 |
+
if len(prop.Enum) > 0 {
|
| 268 |
+
// Add validation in PreRun for enum values
|
| 269 |
+
cmd.PreRunE = func(cmd *cobra.Command, _ []string) error {
|
| 270 |
+
for flagName, property := range tool.InputSchema.Properties {
|
| 271 |
+
if len(property.Enum) > 0 {
|
| 272 |
+
value, _ := cmd.Flags().GetString(flagName)
|
| 273 |
+
if value != "" && !slices.Contains(property.Enum, value) {
|
| 274 |
+
return fmt.Errorf("%s must be one of: %s", flagName, strings.Join(property.Enum, ", "))
|
| 275 |
+
}
|
| 276 |
+
}
|
| 277 |
+
}
|
| 278 |
+
return nil
|
| 279 |
+
}
|
| 280 |
+
}
|
| 281 |
+
case "number":
|
| 282 |
+
cmd.Flags().Float64(name, 0, description)
|
| 283 |
+
case "integer":
|
| 284 |
+
cmd.Flags().Int64(name, 0, description)
|
| 285 |
+
case "boolean":
|
| 286 |
+
cmd.Flags().Bool(name, false, description)
|
| 287 |
+
case "array":
|
| 288 |
+
if prop.Items != nil {
|
| 289 |
+
switch prop.Items.Type {
|
| 290 |
+
case "string":
|
| 291 |
+
cmd.Flags().StringSlice(name, []string{}, description)
|
| 292 |
+
case "object":
|
| 293 |
+
cmd.Flags().String(name+"-json", "", description+" (provide as JSON array)")
|
| 294 |
+
}
|
| 295 |
+
}
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
if isRequired {
|
| 299 |
+
_ = cmd.MarkFlagRequired(name)
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
// Bind flag to viper
|
| 303 |
+
_ = viper.BindPFlag(name, cmd.Flags().Lookup(name))
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
// Add command to root
|
| 307 |
+
toolsCmd.AddCommand(cmd)
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
// buildArgumentsMap extracts flag values into a map of arguments
|
| 311 |
+
func buildArgumentsMap(cmd *cobra.Command, tool *Tool) (map[string]interface{}, error) {
|
| 312 |
+
arguments := make(map[string]interface{})
|
| 313 |
+
|
| 314 |
+
for name, prop := range tool.InputSchema.Properties {
|
| 315 |
+
switch prop.Type {
|
| 316 |
+
case "string":
|
| 317 |
+
if value, _ := cmd.Flags().GetString(name); value != "" {
|
| 318 |
+
arguments[name] = value
|
| 319 |
+
}
|
| 320 |
+
case "number":
|
| 321 |
+
if value, _ := cmd.Flags().GetFloat64(name); value != 0 {
|
| 322 |
+
arguments[name] = value
|
| 323 |
+
}
|
| 324 |
+
case "integer":
|
| 325 |
+
if value, _ := cmd.Flags().GetInt64(name); value != 0 {
|
| 326 |
+
arguments[name] = value
|
| 327 |
+
}
|
| 328 |
+
case "boolean":
|
| 329 |
+
// For boolean, we need to check if it was explicitly set
|
| 330 |
+
if cmd.Flags().Changed(name) {
|
| 331 |
+
value, _ := cmd.Flags().GetBool(name)
|
| 332 |
+
arguments[name] = value
|
| 333 |
+
}
|
| 334 |
+
case "array":
|
| 335 |
+
if prop.Items != nil {
|
| 336 |
+
switch prop.Items.Type {
|
| 337 |
+
case "string":
|
| 338 |
+
if values, _ := cmd.Flags().GetStringSlice(name); len(values) > 0 {
|
| 339 |
+
arguments[name] = values
|
| 340 |
+
}
|
| 341 |
+
case "object":
|
| 342 |
+
if jsonStr, _ := cmd.Flags().GetString(name + "-json"); jsonStr != "" {
|
| 343 |
+
var jsonArray []interface{}
|
| 344 |
+
if err := json.Unmarshal([]byte(jsonStr), &jsonArray); err != nil {
|
| 345 |
+
return nil, fmt.Errorf("error parsing JSON for %s: %w", name, err)
|
| 346 |
+
}
|
| 347 |
+
arguments[name] = jsonArray
|
| 348 |
+
}
|
| 349 |
+
}
|
| 350 |
+
}
|
| 351 |
+
}
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
return arguments, nil
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
// buildJSONRPCRequest creates a JSON-RPC request with the given tool name and arguments
|
| 358 |
+
func buildJSONRPCRequest(method, toolName string, arguments map[string]interface{}) (string, error) {
|
| 359 |
+
id, err := rand.Int(rand.Reader, big.NewInt(10000))
|
| 360 |
+
if err != nil {
|
| 361 |
+
return "", fmt.Errorf("failed to generate random ID: %w", err)
|
| 362 |
+
}
|
| 363 |
+
request := JSONRPCRequest{
|
| 364 |
+
JSONRPC: "2.0",
|
| 365 |
+
ID: int(id.Int64()), // Random ID between 0 and 9999
|
| 366 |
+
Method: method,
|
| 367 |
+
Params: RequestParams{
|
| 368 |
+
Name: toolName,
|
| 369 |
+
Arguments: arguments,
|
| 370 |
+
},
|
| 371 |
+
}
|
| 372 |
+
jsonData, err := json.Marshal(request)
|
| 373 |
+
if err != nil {
|
| 374 |
+
return "", fmt.Errorf("failed to marshal JSON request: %w", err)
|
| 375 |
+
}
|
| 376 |
+
return string(jsonData), nil
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
// executeServerCommand runs the specified command, sends the JSON request to stdin,
|
| 380 |
+
// and returns the response from stdout
|
| 381 |
+
func executeServerCommand(cmdStr, jsonRequest string) (string, error) {
|
| 382 |
+
// Split the command string into command and arguments
|
| 383 |
+
cmdParts := strings.Fields(cmdStr)
|
| 384 |
+
if len(cmdParts) == 0 {
|
| 385 |
+
return "", fmt.Errorf("empty command")
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
cmd := exec.Command(cmdParts[0], cmdParts[1:]...) //nolint:gosec //mcpcurl is a test command that needs to execute arbitrary shell commands
|
| 389 |
+
|
| 390 |
+
// Setup stdin pipe
|
| 391 |
+
stdin, err := cmd.StdinPipe()
|
| 392 |
+
if err != nil {
|
| 393 |
+
return "", fmt.Errorf("failed to create stdin pipe: %w", err)
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
// Setup stdout and stderr pipes
|
| 397 |
+
var stdout, stderr bytes.Buffer
|
| 398 |
+
cmd.Stdout = &stdout
|
| 399 |
+
cmd.Stderr = &stderr
|
| 400 |
+
|
| 401 |
+
// Start the command
|
| 402 |
+
if err := cmd.Start(); err != nil {
|
| 403 |
+
return "", fmt.Errorf("failed to start command: %w", err)
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
// Write the JSON request to stdin
|
| 407 |
+
if _, err := io.WriteString(stdin, jsonRequest+"\n"); err != nil {
|
| 408 |
+
return "", fmt.Errorf("failed to write to stdin: %w", err)
|
| 409 |
+
}
|
| 410 |
+
_ = stdin.Close()
|
| 411 |
+
|
| 412 |
+
// Wait for the command to complete
|
| 413 |
+
if err := cmd.Wait(); err != nil {
|
| 414 |
+
return "", fmt.Errorf("command failed: %w, stderr: %s", err, stderr.String())
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
return stdout.String(), nil
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
func printResponse(response string, prettyPrint bool) error {
|
| 421 |
+
if !prettyPrint {
|
| 422 |
+
fmt.Println(response)
|
| 423 |
+
return nil
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
// Parse the JSON response
|
| 427 |
+
var resp Response
|
| 428 |
+
if err := json.Unmarshal([]byte(response), &resp); err != nil {
|
| 429 |
+
return fmt.Errorf("failed to parse JSON: %w", err)
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
// Extract text from content items of type "text"
|
| 433 |
+
for _, content := range resp.Result.Content {
|
| 434 |
+
if content.Type == "text" {
|
| 435 |
+
var textContentObj map[string]interface{}
|
| 436 |
+
err := json.Unmarshal([]byte(content.Text), &textContentObj)
|
| 437 |
+
|
| 438 |
+
if err == nil {
|
| 439 |
+
prettyText, err := json.MarshalIndent(textContentObj, "", " ")
|
| 440 |
+
if err != nil {
|
| 441 |
+
return fmt.Errorf("failed to pretty print text content: %w", err)
|
| 442 |
+
}
|
| 443 |
+
fmt.Println(string(prettyText))
|
| 444 |
+
continue
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
// Fallback parsing as JSONL
|
| 448 |
+
var textContentList []map[string]interface{}
|
| 449 |
+
if err := json.Unmarshal([]byte(content.Text), &textContentList); err != nil {
|
| 450 |
+
return fmt.Errorf("failed to parse text content as a list: %w", err)
|
| 451 |
+
}
|
| 452 |
+
prettyText, err := json.MarshalIndent(textContentList, "", " ")
|
| 453 |
+
if err != nil {
|
| 454 |
+
return fmt.Errorf("failed to pretty print array content: %w", err)
|
| 455 |
+
}
|
| 456 |
+
fmt.Println(string(prettyText))
|
| 457 |
+
}
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
// If no text content found, print the original response
|
| 461 |
+
if len(resp.Result.Content) == 0 {
|
| 462 |
+
fmt.Println(response)
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
return nil
|
| 466 |
+
}
|
docs/error-handling.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Error Handling
|
| 2 |
+
|
| 3 |
+
This document describes the error handling patterns used in the GitHub MCP Server, specifically how we handle GitHub API errors and avoid direct use of mcp-go error types.
|
| 4 |
+
|
| 5 |
+
## Overview
|
| 6 |
+
|
| 7 |
+
The GitHub MCP Server implements a custom error handling approach that serves two primary purposes:
|
| 8 |
+
|
| 9 |
+
1. **Tool Response Generation**: Return appropriate MCP tool error responses to clients
|
| 10 |
+
2. **Middleware Inspection**: Store detailed error information in the request context for middleware analysis
|
| 11 |
+
|
| 12 |
+
This dual approach enables better observability and debugging capabilities, particularly for remote server deployments where understanding the nature of failures (rate limiting, authentication, 404s, 500s, etc.) is crucial for validation and monitoring.
|
| 13 |
+
|
| 14 |
+
## Error Types
|
| 15 |
+
|
| 16 |
+
### GitHubAPIError
|
| 17 |
+
|
| 18 |
+
Used for REST API errors from the GitHub API:
|
| 19 |
+
|
| 20 |
+
```go
|
| 21 |
+
type GitHubAPIError struct {
|
| 22 |
+
Message string `json:"message"`
|
| 23 |
+
Response *github.Response `json:"-"`
|
| 24 |
+
Err error `json:"-"`
|
| 25 |
+
}
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
### GitHubGraphQLError
|
| 29 |
+
|
| 30 |
+
Used for GraphQL API errors from the GitHub API:
|
| 31 |
+
|
| 32 |
+
```go
|
| 33 |
+
type GitHubGraphQLError struct {
|
| 34 |
+
Message string `json:"message"`
|
| 35 |
+
Err error `json:"-"`
|
| 36 |
+
}
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
## Usage Patterns
|
| 40 |
+
|
| 41 |
+
### For GitHub REST API Errors
|
| 42 |
+
|
| 43 |
+
Instead of directly returning `mcp.NewToolResultError()`, use:
|
| 44 |
+
|
| 45 |
+
```go
|
| 46 |
+
return ghErrors.NewGitHubAPIErrorResponse(ctx, message, response, err), nil
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
This function:
|
| 50 |
+
- Creates a `GitHubAPIError` with the provided message, response, and error
|
| 51 |
+
- Stores the error in the context for middleware inspection
|
| 52 |
+
- Returns an appropriate MCP tool error response
|
| 53 |
+
|
| 54 |
+
### For GitHub GraphQL API Errors
|
| 55 |
+
|
| 56 |
+
```go
|
| 57 |
+
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, message, err), nil
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
### Context Management
|
| 61 |
+
|
| 62 |
+
The error handling system uses context to store errors for later inspection:
|
| 63 |
+
|
| 64 |
+
```go
|
| 65 |
+
// Initialize context with error tracking
|
| 66 |
+
ctx = errors.ContextWithGitHubErrors(ctx)
|
| 67 |
+
|
| 68 |
+
// Retrieve errors for inspection (typically in middleware)
|
| 69 |
+
apiErrors, err := errors.GetGitHubAPIErrors(ctx)
|
| 70 |
+
graphqlErrors, err := errors.GetGitHubGraphQLErrors(ctx)
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
## Design Principles
|
| 74 |
+
|
| 75 |
+
### User-Actionable vs. Developer Errors
|
| 76 |
+
|
| 77 |
+
- **User-actionable errors** (authentication failures, rate limits, 404s) should be returned as failed tool calls using the error response functions
|
| 78 |
+
- **Developer errors** (JSON marshaling failures, internal logic errors) should be returned as actual Go errors that bubble up through the MCP framework
|
| 79 |
+
|
| 80 |
+
### Context Limitations
|
| 81 |
+
|
| 82 |
+
This approach was designed to work around current limitations in mcp-go where context is not propagated through each step of request processing. By storing errors in context values, middleware can inspect them without requiring context propagation.
|
| 83 |
+
|
| 84 |
+
### Graceful Error Handling
|
| 85 |
+
|
| 86 |
+
Error storage operations in context are designed to fail gracefully - if context storage fails, the tool will still return an appropriate error response to the client.
|
| 87 |
+
|
| 88 |
+
## Benefits
|
| 89 |
+
|
| 90 |
+
1. **Observability**: Middleware can inspect the specific types of GitHub API errors occurring
|
| 91 |
+
2. **Debugging**: Detailed error information is preserved without exposing potentially sensitive data in logs
|
| 92 |
+
3. **Validation**: Remote servers can use error types and HTTP status codes to validate that changes don't break functionality
|
| 93 |
+
4. **Privacy**: Error inspection can be done programmatically using `errors.Is` checks without logging PII
|
| 94 |
+
|
| 95 |
+
## Example Implementation
|
| 96 |
+
|
| 97 |
+
```go
|
| 98 |
+
func GetIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
|
| 99 |
+
return mcp.NewTool("get_issue", /* ... */),
|
| 100 |
+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
| 101 |
+
owner, err := RequiredParam[string](request, "owner")
|
| 102 |
+
if err != nil {
|
| 103 |
+
return mcp.NewToolResultError(err.Error()), nil
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
client, err := getClient(ctx)
|
| 107 |
+
if err != nil {
|
| 108 |
+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
issue, resp, err := client.Issues.Get(ctx, owner, repo, issueNumber)
|
| 112 |
+
if err != nil {
|
| 113 |
+
return ghErrors.NewGitHubAPIErrorResponse(ctx,
|
| 114 |
+
"failed to get issue",
|
| 115 |
+
resp,
|
| 116 |
+
err,
|
| 117 |
+
), nil
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
return MarshalledTextResult(issue), nil
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
This approach ensures that both the client receives an appropriate error response and any middleware can inspect the underlying GitHub API error for monitoring and debugging purposes.
|
docs/host-integration.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# GitHub Remote MCP Integration Guide for MCP Host Authors
|
| 2 |
+
|
| 3 |
+
This guide outlines high-level considerations for MCP Host authors who want to allow installation of the Remote GitHub MCP server.
|
| 4 |
+
|
| 5 |
+
The goal is to explain the architecture at a high-level, define key requirements, and provide guidance to get you started, while pointing to official documentation for deeper implementation details.
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## Table of Contents
|
| 10 |
+
|
| 11 |
+
- [Understanding MCP Architecture](#understanding-mcp-architecture)
|
| 12 |
+
- [Connecting to the Remote GitHub MCP Server](#connecting-to-the-remote-github-mcp-server)
|
| 13 |
+
- [Authentication and Authorization](#authentication-and-authorization)
|
| 14 |
+
- [OAuth Support on GitHub](#oauth-support-on-github)
|
| 15 |
+
- [Create an OAuth-enabled App Using the GitHub UI](#create-an-oauth-enabled-app-using-the-github-ui)
|
| 16 |
+
- [Things to Consider](#things-to-consider)
|
| 17 |
+
- [Initiating the OAuth Flow from your Client Application](#initiating-the-oauth-flow-from-your-client-application)
|
| 18 |
+
- [Handling Organization Access Restrictions](#handling-organization-access-restrictions)
|
| 19 |
+
- [Essential Security Considerations](#essential-security-considerations)
|
| 20 |
+
- [Additional Resources](#additional-resources)
|
| 21 |
+
|
| 22 |
+
---
|
| 23 |
+
|
| 24 |
+
## Understanding MCP Architecture
|
| 25 |
+
|
| 26 |
+
The Model Context Protocol (MCP) enables seamless communication between your application and various external tools through an architecture defined by the [MCP Standard](https://modelcontextprotocol.io/).
|
| 27 |
+
|
| 28 |
+
### High-level Architecture
|
| 29 |
+
|
| 30 |
+
The diagram below illustrates how a single client application can connect to multiple MCP Servers, each providing access to a unique set of resources. Notice that some MCP Servers are running locally (side-by-side with the client application) while others are hosted remotely. GitHub's MCP offerings are available to run either locally or remotely.
|
| 31 |
+
|
| 32 |
+
```mermaid
|
| 33 |
+
flowchart LR
|
| 34 |
+
subgraph "Local Runtime Environment"
|
| 35 |
+
subgraph "Client Application (e.g., IDE)"
|
| 36 |
+
CLIENTAPP[Application Runtime]
|
| 37 |
+
CX["MCP Client (FileSystem)"]
|
| 38 |
+
CY["MCP Client (GitHub)"]
|
| 39 |
+
CZ["MCP Client (Other)"]
|
| 40 |
+
end
|
| 41 |
+
|
| 42 |
+
LOCALMCP[File System MCP Server]
|
| 43 |
+
end
|
| 44 |
+
|
| 45 |
+
subgraph "Internet"
|
| 46 |
+
GITHUBMCP[GitHub Remote MCP Server]
|
| 47 |
+
OTHERMCP[Other Remote MCP Server]
|
| 48 |
+
end
|
| 49 |
+
|
| 50 |
+
CLIENTAPP --> CX
|
| 51 |
+
CLIENTAPP --> CY
|
| 52 |
+
CLIENTAPP --> CZ
|
| 53 |
+
|
| 54 |
+
CX <-->|"stdio"| LOCALMCP
|
| 55 |
+
CY <-->|"OAuth 2.0 + HTTP/SSE"| GITHUBMCP
|
| 56 |
+
CZ <-->|"OAuth 2.0 + HTTP/SSE"| OTHERMCP
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
### Runtime Environment
|
| 60 |
+
|
| 61 |
+
- **Application**: The user-facing application you are building. It instantiates one or more MCP clients and orchestrates tool calls.
|
| 62 |
+
- **MCP Client**: A component within your client application that maintains a 1:1 connection with a single MCP server.
|
| 63 |
+
- **MCP Server**: A service that provides access to a specific set of tools.
|
| 64 |
+
- **Local MCP Server**: An MCP Server running locally, side-by-side with the Application.
|
| 65 |
+
- **Remote MCP Server**: An MCP Server running remotely, accessed via the internet. Most Remote MCP Servers require authentication via OAuth.
|
| 66 |
+
|
| 67 |
+
For more detail, see the [official MCP specification](https://modelcontextprotocol.io/specification/2025-06-18).
|
| 68 |
+
|
| 69 |
+
> [!NOTE]
|
| 70 |
+
> GitHub offers both a Local MCP Server and a Remote MCP Server.
|
| 71 |
+
|
| 72 |
+
---
|
| 73 |
+
|
| 74 |
+
## Connecting to the Remote GitHub MCP Server
|
| 75 |
+
|
| 76 |
+
### Authentication and Authorization
|
| 77 |
+
|
| 78 |
+
GitHub MCP Servers require a valid access token in the `Authorization` header. This is true for both the Local GitHub MCP Server and the Remote GitHub MCP Server.
|
| 79 |
+
|
| 80 |
+
For the Remote GitHub MCP Server, the recommended way to obtain a valid access token is to ensure your client application supports [OAuth 2.1](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13). It should be noted, however, that you may also supply any valid access token. For example, you may supply a pre-generated Personal Access Token (PAT).
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
> [!IMPORTANT]
|
| 84 |
+
> The Remote GitHub MCP Server itself does not provide Authentication services.
|
| 85 |
+
> Your client application must obtain valid GitHub access tokens through one of the supported methods.
|
| 86 |
+
|
| 87 |
+
The expected flow for obtaining a valid access token via OAuth is depicted in the [MCP Specification](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#authorization-flow-steps). For convenience, we've embedded a copy of the authorization flow below. Please study it carefully as the remainder of this document is written with this flow in mind.
|
| 88 |
+
|
| 89 |
+
```mermaid
|
| 90 |
+
sequenceDiagram
|
| 91 |
+
participant B as User-Agent (Browser)
|
| 92 |
+
participant C as Client
|
| 93 |
+
participant M as MCP Server (Resource Server)
|
| 94 |
+
participant A as Authorization Server
|
| 95 |
+
|
| 96 |
+
C->>M: MCP request without token
|
| 97 |
+
M->>C: HTTP 401 Unauthorized with WWW-Authenticate header
|
| 98 |
+
Note over C: Extract resource_metadata URL from WWW-Authenticate
|
| 99 |
+
|
| 100 |
+
C->>M: Request Protected Resource Metadata
|
| 101 |
+
M->>C: Return metadata
|
| 102 |
+
|
| 103 |
+
Note over C: Parse metadata and extract authorization server(s)<br/>Client determines AS to use
|
| 104 |
+
|
| 105 |
+
C->>A: GET /.well-known/oauth-authorization-server
|
| 106 |
+
A->>C: Authorization server metadata response
|
| 107 |
+
|
| 108 |
+
alt Dynamic client registration
|
| 109 |
+
C->>A: POST /register
|
| 110 |
+
A->>C: Client Credentials
|
| 111 |
+
end
|
| 112 |
+
|
| 113 |
+
Note over C: Generate PKCE parameters
|
| 114 |
+
C->>B: Open browser with authorization URL + code_challenge
|
| 115 |
+
B->>A: Authorization request
|
| 116 |
+
Note over A: User authorizes
|
| 117 |
+
A->>B: Redirect to callback with authorization code
|
| 118 |
+
B->>C: Authorization code callback
|
| 119 |
+
C->>A: Token request + code_verifier
|
| 120 |
+
A->>C: Access token (+ refresh token)
|
| 121 |
+
C->>M: MCP request with access token
|
| 122 |
+
M-->>C: MCP response
|
| 123 |
+
Note over C,M: MCP communication continues with valid token
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
> [!NOTE]
|
| 127 |
+
> Dynamic Client Registration is NOT supported by Remote GitHub MCP Server at this time.
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
#### OAuth Support on GitHub
|
| 131 |
+
|
| 132 |
+
GitHub offers two solutions for obtaining access tokens via OAuth: [**GitHub Apps**](https://docs.github.com/en/apps/using-github-apps/about-using-github-apps#about-github-apps) and [**OAuth Apps**](https://docs.github.com/en/apps/oauth-apps). These solutions are typically created, administered, and maintained by GitHub Organization administrators. Collaborate with a GitHub Organization administrator to configure either a **GitHub App** or an **OAuth App** to allow your client application to utilize GitHub OAuth support. Furthermore, be aware that it may be necessary for users of your client application to register your **GitHub App** or **OAuth App** within their own GitHub Organization in order to generate authorization tokens capable of accessing Organization's GitHub resources.
|
| 133 |
+
|
| 134 |
+
> [!TIP]
|
| 135 |
+
> Before proceeding, check whether your organization already supports one of these solutions. Administrators of your GitHub Organization can help you determine what **GitHub Apps** or **OAuth Apps** are already registered. If there's an existing **GitHub App** or **OAuth App** that fits your use case, consider reusing it for Remote MCP Authorization. That said, be sure to take heed of the following warning.
|
| 136 |
+
|
| 137 |
+
> [!WARNING]
|
| 138 |
+
> Both **GitHub Apps** and **OAuth Apps** require the client application to pass a "client secret" in order to initiate the OAuth flow. If your client application is designed to run in an uncontrolled environment (i.e. customer-provided hardware), end users will be able to discover your "client secret" and potentially exploit it for other purposes. In such cases, our recommendation is to register a new **GitHub App** (or **OAuth App**) exclusively dedicated to servicing OAuth requests from your client application.
|
| 139 |
+
|
| 140 |
+
#### Create an OAuth-enabled App Using the GitHub UI
|
| 141 |
+
|
| 142 |
+
Detailed instructions for creating a **GitHub App** can be found at ["Creating GitHub Apps"](https://docs.github.com/en/apps/creating-github-apps/about-creating-github-apps/about-creating-github-apps#building-a-github-app). (RECOMMENDED)<br/>
|
| 143 |
+
Detailed instructions for creating an **OAuth App** can be found ["Creating an OAuth App"](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app).
|
| 144 |
+
|
| 145 |
+
For guidance on which type of app to choose, see ["Differences Between GitHub Apps and OAuth Apps"](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/differences-between-github-apps-and-oauth-apps).
|
| 146 |
+
|
| 147 |
+
#### Things to Consider:
|
| 148 |
+
- Tokens provided by **GitHub Apps** are generally more secure because they:
|
| 149 |
+
- include an expiration
|
| 150 |
+
- include support for fine-grained permissions
|
| 151 |
+
- **GitHub Apps** must be installed on a GitHub Organization before they can be used.<br/>In general, installation must be approved by someone in the Organization with administrator permissions. For more details, see [this explanation](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/differences-between-github-apps-and-oauth-apps#who-can-install-github-apps-and-authorize-oauth-apps).<br/>By contrast, **OAuth Apps** don't require installation and, typically, can be used immediately.
|
| 152 |
+
- Members of an Organization may use the GitHub UI to [request that a GitHub App be installed](https://docs.github.com/en/apps/using-github-apps/requesting-a-github-app-from-your-organization-owner) organization-wide.
|
| 153 |
+
- While not strictly necessary, if you expect that a wide range of users will use your MCP Server, consider publishing its corresponding **GitHub App** or **OAuth App** on the [GitHub App Marketplace](https://github.com/marketplace?type=apps) to ensure that it's discoverable by your audience.
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
#### Initiating the OAuth Flow from your Client Application
|
| 157 |
+
|
| 158 |
+
For **GitHub Apps**, details on initiating the OAuth flow from a client application are described in detail [here](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app#using-the-web-application-flow-to-generate-a-user-access-token).
|
| 159 |
+
|
| 160 |
+
For **OAuth Apps**, details on initiating the OAuth flow from a client application are described in detail [here](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#web-application-flow).
|
| 161 |
+
|
| 162 |
+
> [!IMPORTANT]
|
| 163 |
+
> For endpoint discovery, be sure to honor the [`WWW-Authenticate` information provided](https://modelcontextprotocol.io/specification/draft/basic/authorization#authorization-server-location) by the Remote GitHub MCP Server rather than relying on hard-coded endpoints like `https://github.com/login/oauth/authorize`.
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
### Handling Organization Access Restrictions
|
| 167 |
+
Organizations may block **GitHub Apps** and **OAuth Apps** until explicitly approved. Within your client application code, you can provide actionable next steps for a smooth user experience in the event that OAuth-related calls fail due to your **GitHub App** or **OAuth App** being unavailable (i.e. not registered within the user's organization).
|
| 168 |
+
|
| 169 |
+
1. Detect the specific error.
|
| 170 |
+
2. Notify the user clearly.
|
| 171 |
+
3. Depending on their GitHub organization privileges:
|
| 172 |
+
- Org Members: Prompt them to request approval from a GitHub organization admin, within the organization where access has not been approved.
|
| 173 |
+
- Org Admins: Link them to the corresponding GitHub organization’s App approval settings at `https://github.com/organizations/[ORG_NAME]/settings/oauth_application_policy`
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
## Essential Security Considerations
|
| 177 |
+
- **Token Storage**: Use secure platform APIs (e.g. keytar for Node.js).
|
| 178 |
+
- **Input Validation**: Sanitize all tool arguments.
|
| 179 |
+
- **HTTPS Only**: Never send requests over plaintext HTTP. Always use HTTPS in production.
|
| 180 |
+
- **PKCE:** We strongly recommend implementing [PKCE](https://datatracker.ietf.org/doc/html/rfc7636) for all OAuth flows to prevent code interception, to prepare for upcoming PKCE support.
|
| 181 |
+
|
| 182 |
+
## Additional Resources
|
| 183 |
+
- [MCP Official Spec](https://modelcontextprotocol.io/specification/draft)
|
| 184 |
+
- [MCP SDKs](https://modelcontextprotocol.io/sdk/java/mcp-overview)
|
| 185 |
+
- [GitHub Docs on Creating GitHub Apps](https://docs.github.com/en/apps/creating-github-apps)
|
| 186 |
+
- [GitHub Docs on Using GitHub Apps](https://docs.github.com/en/apps/using-github-apps/about-using-github-apps)
|
| 187 |
+
- [GitHub Docs on Creating OAuth Apps](https://docs.github.com/en/apps/oauth-apps)
|
| 188 |
+
- GitHub Docs on Installing OAuth Apps into a [Personal Account](https://docs.github.com/en/apps/oauth-apps/using-oauth-apps/installing-an-oauth-app-in-your-personal-account) and [Organization](https://docs.github.com/en/apps/oauth-apps/using-oauth-apps/installing-an-oauth-app-in-your-organization)
|
| 189 |
+
- [Managing OAuth Apps at the Organization Level](https://docs.github.com/en/organizations/managing-oauth-access-to-your-organizations-data)
|
| 190 |
+
- [Managing Programmatic Access at the GitHub Organization Level](https://docs.github.com/en/organizations/managing-programmatic-access-to-your-organization)
|
| 191 |
+
- [Building Copilot Extensions](https://docs.github.com/en/copilot/building-copilot-extensions)
|
| 192 |
+
- [Managing App/Extension Visibility](https://docs.github.com/en/copilot/building-copilot-extensions/managing-the-availability-of-your-copilot-extension) (including GitHub Marketplace information)
|
| 193 |
+
- [Example Implementation in VS Code Repository](https://github.com/microsoft/vscode/blob/main/src/vs/workbench/api/common/extHostMcp.ts#L313)
|
docs/installation-guides/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# GitHub MCP Server Installation Guides
|
| 2 |
+
|
| 3 |
+
This directory contains detailed installation instructions for the GitHub MCP Server across different host applications and IDEs. Choose the guide that matches your development environment.
|
| 4 |
+
|
| 5 |
+
## Installation Guides by Host Application
|
| 6 |
+
- **[GitHub Copilot in other IDEs](install-other-copilot-ides.md)** - Installation for JetBrains, Visual Studio, Eclipse, and Xcode with GitHub Copilot
|
| 7 |
+
- **[Claude Applications](install-claude.md)** - Installation guide for Claude Web, Claude Desktop and Claude Code CLI
|
| 8 |
+
- **[Cursor](install-cursor.md)** - Installation guide for Cursor IDE
|
| 9 |
+
- **[Windsurf](install-windsurf.md)** - Installation guide for Windsurf IDE
|
| 10 |
+
|
| 11 |
+
## Support by Host Application
|
| 12 |
+
|
| 13 |
+
| Host Application | Local GitHub MCP Support | Remote GitHub MCP Support | Prerequisites | Difficulty |
|
| 14 |
+
|-----------------|---------------|----------------|---------------|------------|
|
| 15 |
+
| Copilot in VS Code | ✅ | ✅ Full (OAuth + PAT) | Local: Docker or Go build, GitHub PAT<br>Remote: VS Code 1.101+ | Easy |
|
| 16 |
+
| Copilot Coding Agent | ✅ | ✅ Full (on by default; no auth needed) | Any _paid_ copilot license | Default on |
|
| 17 |
+
| Copilot in Visual Studio | ✅ | ✅ PAT + ❌ No OAuth | Local: Docker or Go build, GitHub PAT<br>Remote: Visual Studio 17.14+ | Easy |
|
| 18 |
+
| Copilot in JetBrains | ✅ | ✅ PAT + ❌ No OAuth | Local: Docker or Go build, GitHub PAT<br>Remote: JetBrains Copilot Extension v1.5.35+ | Easy |
|
| 19 |
+
| Claude Code | ✅ | ✅ PAT + ❌ No OAuth| GitHub MCP Server binary or remote URL, GitHub PAT | Easy |
|
| 20 |
+
| Claude Desktop | ✅ | ✅ PAT + ❌ No OAuth | Docker or Go build, GitHub PAT | Moderate |
|
| 21 |
+
| Cursor | ✅ | ✅ PAT + ❌ No OAuth | Docker or Go build, GitHub PAT | Easy |
|
| 22 |
+
| Windsurf | ✅ | ✅ PAT + ❌ No OAuth | Docker or Go build, GitHub PAT | Easy |
|
| 23 |
+
| Copilot in Xcode | ✅ | ✅ PAT + ❌ No OAuth | Local: Docker or Go build, GitHub PAT<br>Remote: Copilot for Xcode latest version | Easy |
|
| 24 |
+
| Copilot in Eclipse | ✅ | ✅ PAT + ❌ No OAuth | Local: Docker or Go build, GitHub PAT<br>Remote: TBD | Easy |
|
| 25 |
+
|
| 26 |
+
**Legend:**
|
| 27 |
+
- ✅ = Fully supported
|
| 28 |
+
- ❌ = Not yet supported
|
| 29 |
+
|
| 30 |
+
**Note:** Remote MCP support requires host applications to register a GitHub App or OAuth app for OAuth flow support – even if the new OAuth spec is supported by that host app. Currently, only VS Code has full remote GitHub server support.
|
| 31 |
+
|
| 32 |
+
## Installation Methods
|
| 33 |
+
|
| 34 |
+
The GitHub MCP Server can be installed using several methods. **Docker is the most popular and recommended approach** for most users, but alternatives are available depending on your needs:
|
| 35 |
+
|
| 36 |
+
### 🐳 Docker (Most Common & Recommended)
|
| 37 |
+
- **Pros**: No local build required, consistent environment, easy updates, works across all platforms
|
| 38 |
+
- **Cons**: Requires Docker installed and running
|
| 39 |
+
- **Best for**: Most users, especially those already using Docker or wanting the simplest setup
|
| 40 |
+
- **Used by**: Claude Desktop, Copilot in VS Code, Cursor, Windsurf, etc.
|
| 41 |
+
|
| 42 |
+
### 📦 Pre-built Binary (Lightweight Alternative)
|
| 43 |
+
- **Pros**: No Docker required, direct execution via stdio, minimal setup
|
| 44 |
+
- **Cons**: Need to manually download and manage updates, platform-specific binaries
|
| 45 |
+
- **Best for**: Minimal environments, users who prefer not to use Docker
|
| 46 |
+
- **Used by**: Claude Code CLI, lightweight setups
|
| 47 |
+
|
| 48 |
+
### 🔨 Build from Source (Advanced Users)
|
| 49 |
+
- **Pros**: Latest features, full customization, no external dependencies
|
| 50 |
+
- **Cons**: Requires Go development environment, more complex setup
|
| 51 |
+
- **Prerequisites**: [Go 1.24+](https://go.dev/doc/install)
|
| 52 |
+
- **Build command**: `go build -o github-mcp-server cmd/github-mcp-server/main.go`
|
| 53 |
+
- **Best for**: Developers who want the latest features or need custom modifications
|
| 54 |
+
|
| 55 |
+
### Important Notes on the GitHub MCP Server
|
| 56 |
+
|
| 57 |
+
- **Docker Image**: The official Docker image is now `ghcr.io/github/github-mcp-server`
|
| 58 |
+
- **npm Package**: The npm package @modelcontextprotocol/server-github is no longer supported as of April 2025
|
| 59 |
+
- **Remote Server**: The remote server URL is `https://api.githubcopilot.com/mcp/`
|
| 60 |
+
|
| 61 |
+
## General Prerequisites
|
| 62 |
+
|
| 63 |
+
All installations with Personal Access Tokens (PAT) require:
|
| 64 |
+
- **GitHub Personal Access Token (PAT)**: [Create one here](https://github.com/settings/personal-access-tokens/new)
|
| 65 |
+
|
| 66 |
+
Optional (depending on installation method):
|
| 67 |
+
- **Docker** (for Docker-based installations): [Download Docker](https://www.docker.com/)
|
| 68 |
+
- **Go 1.24+** (for building from source): [Install Go](https://go.dev/doc/install)
|
| 69 |
+
|
| 70 |
+
## Security Best Practices
|
| 71 |
+
|
| 72 |
+
Regardless of which installation method you choose, follow these security guidelines:
|
| 73 |
+
|
| 74 |
+
1. **Secure Token Storage**: Never commit your GitHub PAT to version control
|
| 75 |
+
2. **Limit Token Scope**: Only grant necessary permissions to your GitHub PAT
|
| 76 |
+
3. **File Permissions**: Restrict access to configuration files containing tokens
|
| 77 |
+
4. **Regular Rotation**: Periodically rotate your GitHub Personal Access Tokens
|
| 78 |
+
5. **Environment Variables**: Use environment variables when supported by your host
|
| 79 |
+
|
| 80 |
+
## Getting Help
|
| 81 |
+
|
| 82 |
+
If you encounter issues:
|
| 83 |
+
1. Check the troubleshooting section in your specific installation guide
|
| 84 |
+
2. Verify your GitHub PAT has the required permissions
|
| 85 |
+
3. Ensure Docker is running (for local installations)
|
| 86 |
+
4. Review your host application's logs for error messages
|
| 87 |
+
5. Consult the main [README.md](README.md) for additional configuration options
|
| 88 |
+
|
| 89 |
+
## Configuration Options
|
| 90 |
+
|
| 91 |
+
After installation, you may want to explore:
|
| 92 |
+
- **Toolsets**: Enable/disable specific GitHub API capabilities
|
| 93 |
+
- **Read-Only Mode**: Restrict to read-only operations
|
| 94 |
+
- **Dynamic Tool Discovery**: Enable tools on-demand
|
| 95 |
+
|
docs/installation-guides/install-claude.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Install GitHub MCP Server in Claude Applications
|
| 2 |
+
|
| 3 |
+
## Claude Code CLI
|
| 4 |
+
|
| 5 |
+
### Prerequisites
|
| 6 |
+
- Claude Code CLI installed
|
| 7 |
+
- [GitHub Personal Access Token](https://github.com/settings/personal-access-tokens/new)
|
| 8 |
+
- For local setup: [Docker](https://www.docker.com/) installed and running
|
| 9 |
+
- Open Claude Code inside the directory for your project (recommended for best experience and clear scope of configuration)
|
| 10 |
+
|
| 11 |
+
<details>
|
| 12 |
+
<summary><b>Storing Your PAT Securely</b></summary>
|
| 13 |
+
<br>
|
| 14 |
+
|
| 15 |
+
For security, avoid hardcoding your token. One common approach:
|
| 16 |
+
|
| 17 |
+
1. Store your token in `.env` file
|
| 18 |
+
```
|
| 19 |
+
GITHUB_PAT=your_token_here
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
2. Add to .gitignore
|
| 23 |
+
```bash
|
| 24 |
+
echo -e ".env\n.mcp.json" >> .gitignore
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
</details>
|
| 28 |
+
|
| 29 |
+
### Remote Server Setup (Streamable HTTP)
|
| 30 |
+
|
| 31 |
+
1. Run the following command in the Claude Code CLI
|
| 32 |
+
```bash
|
| 33 |
+
claude mcp add --transport http github https://api.githubcopilot.com/mcp -H "Authorization: Bearer YOUR_GITHUB_PAT"
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
With an environment variable:
|
| 37 |
+
```bash
|
| 38 |
+
claude mcp add --transport http github https://api.githubcopilot.com/mcp -H "Authorization: Bearer $(grep GITHUB_PAT .env | cut -d '=' -f2)"
|
| 39 |
+
```
|
| 40 |
+
2. Restart Claude Code
|
| 41 |
+
3. Run `claude mcp list` to see if the GitHub server is configured
|
| 42 |
+
|
| 43 |
+
### Local Server Setup (Docker required)
|
| 44 |
+
|
| 45 |
+
### With Docker
|
| 46 |
+
1. Run the following command in the Claude Code CLI:
|
| 47 |
+
```bash
|
| 48 |
+
claude mcp add github -e GITHUB_PERSONAL_ACCESS_TOKEN=YOUR_GITHUB_PAT -- docker run -i --rm -e GITHUB_PERSONAL_ACCESS_TOKEN ghcr.io/github/github-mcp-server
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
With an environment variable:
|
| 52 |
+
```bash
|
| 53 |
+
claude mcp add github -e GITHUB_PERSONAL_ACCESS_TOKEN=$(grep GITHUB_PAT .env | cut -d '=' -f2) -- docker run -i --rm -e GITHUB_PERSONAL_ACCESS_TOKEN ghcr.io/github/github-mcp-server
|
| 54 |
+
```
|
| 55 |
+
2. Restart Claude Code
|
| 56 |
+
3. Run `claude mcp list` to see if the GitHub server is configured
|
| 57 |
+
|
| 58 |
+
### With a Binary (no Docker)
|
| 59 |
+
|
| 60 |
+
1. Download [release binary](https://github.com/github/github-mcp-server/releases)
|
| 61 |
+
2. Add to your `PATH`
|
| 62 |
+
3. Run:
|
| 63 |
+
```bash
|
| 64 |
+
claude mcp add-json github '{"command": "github-mcp-server", "args": ["stdio"], "env": {"GITHUB_PERSONAL_ACCESS_TOKEN": "YOUR_GITHUB_PAT"}}'
|
| 65 |
+
```
|
| 66 |
+
2. Restart Claude Code
|
| 67 |
+
3. Run `claude mcp list` to see if the GitHub server is configured
|
| 68 |
+
|
| 69 |
+
### Verification
|
| 70 |
+
```bash
|
| 71 |
+
claude mcp list
|
| 72 |
+
claude mcp get github
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
---
|
| 76 |
+
|
| 77 |
+
## Claude Desktop
|
| 78 |
+
|
| 79 |
+
> ⚠️ **Note**: Some users have reported compatibility issues with Claude Desktop and Docker-based MCP servers. We're investigating. If you experience issues, try using another MCP host, while we look into it!
|
| 80 |
+
|
| 81 |
+
### Prerequisites
|
| 82 |
+
- Claude Desktop installed (latest version)
|
| 83 |
+
- [GitHub Personal Access Token](https://github.com/settings/personal-access-tokens/new)
|
| 84 |
+
- [Docker](https://www.docker.com/) installed and running
|
| 85 |
+
|
| 86 |
+
> **Note**: Claude Desktop supports MCP servers that are both local (stdio) and remote ("connectors"). Remote servers can generally be added via Settings → Connectors → "Add custom connector". However, the GitHub remote MCP server requires OAuth authentication through a registered GitHub App (or OAuth App), which is not currently supported. Use the local Docker setup instead.
|
| 87 |
+
|
| 88 |
+
### Configuration File Location
|
| 89 |
+
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
| 90 |
+
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
| 91 |
+
- **Linux**: `~/.config/Claude/claude_desktop_config.json`
|
| 92 |
+
|
| 93 |
+
### Local Server Setup (Docker)
|
| 94 |
+
|
| 95 |
+
Add this codeblock to your `claude_desktop_config.json`:
|
| 96 |
+
|
| 97 |
+
```json
|
| 98 |
+
{
|
| 99 |
+
"mcpServers": {
|
| 100 |
+
"github": {
|
| 101 |
+
"command": "docker",
|
| 102 |
+
"args": [
|
| 103 |
+
"run",
|
| 104 |
+
"-i",
|
| 105 |
+
"--rm",
|
| 106 |
+
"-e",
|
| 107 |
+
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
| 108 |
+
"ghcr.io/github/github-mcp-server"
|
| 109 |
+
],
|
| 110 |
+
"env": {
|
| 111 |
+
"GITHUB_PERSONAL_ACCESS_TOKEN": "YOUR_GITHUB_PAT"
|
| 112 |
+
}
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
### Manual Setup Steps
|
| 119 |
+
1. Open Claude Desktop
|
| 120 |
+
2. Go to Settings → Developer → Edit Config
|
| 121 |
+
3. Paste the code block above in your configuration file
|
| 122 |
+
4. If you're navigating to the configuration file outside of the app:
|
| 123 |
+
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
| 124 |
+
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
| 125 |
+
5. Open the file in a text editor
|
| 126 |
+
6. Paste one of the code blocks above, based on your chosen configuration (remote or local)
|
| 127 |
+
7. Replace `YOUR_GITHUB_PAT` with your actual token or $GITHUB_PAT environment variable
|
| 128 |
+
8. Save the file
|
| 129 |
+
9. Restart Claude Desktop
|
| 130 |
+
|
| 131 |
+
---
|
| 132 |
+
|
| 133 |
+
## Troubleshooting
|
| 134 |
+
|
| 135 |
+
**Authentication Failed:**
|
| 136 |
+
- Verify PAT has `repo` scope
|
| 137 |
+
- Check token hasn't expired
|
| 138 |
+
|
| 139 |
+
**Remote Server:**
|
| 140 |
+
- Verify URL: `https://api.githubcopilot.com/mcp`
|
| 141 |
+
|
| 142 |
+
**Docker Issues (Local Only):**
|
| 143 |
+
- Ensure Docker Desktop is running
|
| 144 |
+
- Try: `docker pull ghcr.io/github/github-mcp-server`
|
| 145 |
+
- If pull fails: `docker logout ghcr.io` then retry
|
| 146 |
+
|
| 147 |
+
**Server Not Starting / Tools Not Showing:**
|
| 148 |
+
- Run `claude mcp list` to view currently configured MCP servers
|
| 149 |
+
- Validate JSON syntax
|
| 150 |
+
- If using an environment variable to store your PAT, make sure you're properly sourcing your PAT using the environment variable
|
| 151 |
+
- Restart Claude Code and check `/mcp` command
|
| 152 |
+
- Delete the GitHub server by running `claude mcp remove github` and repeating the setup process with a different method
|
| 153 |
+
- Make sure you're running Claude Code within the project you're currently working on to ensure the MCP configuration is properly scoped to your project
|
| 154 |
+
- Check logs:
|
| 155 |
+
- Claude Code: Use `/mcp` command
|
| 156 |
+
- Claude Desktop: `ls ~/Library/Logs/Claude/` and `cat ~/Library/Logs/Claude/mcp-server-*.log` (macOS) or `%APPDATA%\Claude\logs\` (Windows)
|
| 157 |
+
|
| 158 |
+
---
|
| 159 |
+
|
| 160 |
+
## Important Notes
|
| 161 |
+
|
| 162 |
+
- The npm package `@modelcontextprotocol/server-github` is deprecated as of April 2025
|
| 163 |
+
- Remote server requires Streamable HTTP support (check your Claude version)
|
| 164 |
+
- Configuration scopes for Claude Code:
|
| 165 |
+
- `-s user`: Available across all projects
|
| 166 |
+
- `-s project`: Shared via `.mcp.json` file
|
| 167 |
+
- Default: `local` (current project only)
|
docs/installation-guides/install-cursor.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Install GitHub MCP Server in Cursor
|
| 2 |
+
|
| 3 |
+
## Prerequisites
|
| 4 |
+
|
| 5 |
+
1. Cursor IDE installed (latest version)
|
| 6 |
+
2. [GitHub Personal Access Token](https://github.com/settings/personal-access-tokens/new) with appropriate scopes
|
| 7 |
+
3. For local installation: [Docker](https://www.docker.com/) installed and running
|
| 8 |
+
|
| 9 |
+
## Remote Server Setup (Recommended)
|
| 10 |
+
|
| 11 |
+
[](https://cursor.com/en/install-mcp?name=github&config=eyJ1cmwiOiJodHRwczovL2FwaS5naXRodWJjb3BpbG90LmNvbS9tY3AvIiwiaGVhZGVycyI6eyJBdXRob3JpemF0aW9uIjoiQmVhcmVyIFlPVVJfR0lUSFVCX1BBVCJ9fQ%3D%3D)
|
| 12 |
+
|
| 13 |
+
Uses GitHub's hosted server at https://api.githubcopilot.com/mcp/. Requires Cursor v0.48.0+ for Streamable HTTP support. While Cursor supports OAuth for some MCP servers, the GitHub server currently requires a Personal Access Token.
|
| 14 |
+
|
| 15 |
+
### Install steps
|
| 16 |
+
|
| 17 |
+
1. Click the install button above and follow the flow, or go directly to your global MCP configuration file at `~/.cursor/mcp.json` and enter the code block below
|
| 18 |
+
2. In Tools & Integrations > MCP tools, click the pencil icon next to "github"
|
| 19 |
+
3. Replace `YOUR_GITHUB_PAT` with your actual [GitHub Personal Access Token](https://github.com/settings/tokens)
|
| 20 |
+
4. Save the file
|
| 21 |
+
5. Restart Cursor
|
| 22 |
+
|
| 23 |
+
### Streamable HTTP Configuration
|
| 24 |
+
|
| 25 |
+
```json
|
| 26 |
+
{
|
| 27 |
+
"mcpServers": {
|
| 28 |
+
"github": {
|
| 29 |
+
"url": "https://api.githubcopilot.com/mcp/",
|
| 30 |
+
"headers": {
|
| 31 |
+
"Authorization": "Bearer YOUR_GITHUB_PAT"
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
## Local Server Setup
|
| 39 |
+
|
| 40 |
+
[](https://cursor.com/en/install-mcp?name=github&config=eyJjb21tYW5kIjoiZG9ja2VyIHJ1biAtaSAtLXJtIC1lIEdJVEhVQl9QRVJTT05BTF9BQ0NFU1NfVE9LRU4gZ2hjci5pby9naXRodWIvZ2l0aHViLW1jcC1zZXJ2ZXIiLCJlbnYiOnsiR0lUSFVCX1BFUlNPTkFMX0FDQ0VTU19UT0tFTiI6IllPVVJfR0lUSFVCX1BBVCJ9fQ%3D%3D)
|
| 41 |
+
|
| 42 |
+
The local GitHub MCP server runs via Docker and requires Docker Desktop to be installed and running.
|
| 43 |
+
|
| 44 |
+
### Install steps
|
| 45 |
+
|
| 46 |
+
1. Click the install button above and follow the flow, or go directly to your global MCP configuration file at `~/.cursor/mcp.json` and enter the code block below
|
| 47 |
+
2. In Tools & Integrations > MCP tools, click the pencil icon next to "github"
|
| 48 |
+
3. Replace `YOUR_GITHUB_PAT` with your actual [GitHub Personal Access Token](https://github.com/settings/tokens)
|
| 49 |
+
4. Save the file
|
| 50 |
+
5. Restart Cursor
|
| 51 |
+
|
| 52 |
+
### Docker Configuration
|
| 53 |
+
|
| 54 |
+
```json
|
| 55 |
+
{
|
| 56 |
+
"mcpServers": {
|
| 57 |
+
"github": {
|
| 58 |
+
"command": "docker",
|
| 59 |
+
"args": [
|
| 60 |
+
"run",
|
| 61 |
+
"-i",
|
| 62 |
+
"--rm",
|
| 63 |
+
"-e",
|
| 64 |
+
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
| 65 |
+
"ghcr.io/github/github-mcp-server"
|
| 66 |
+
],
|
| 67 |
+
"env": {
|
| 68 |
+
"GITHUB_PERSONAL_ACCESS_TOKEN": "YOUR_GITHUB_PAT"
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
> **Important**: The npm package `@modelcontextprotocol/server-github` is no longer supported as of April 2025. Use the official Docker image `ghcr.io/github/github-mcp-server` instead.
|
| 76 |
+
|
| 77 |
+
## Configuration Files
|
| 78 |
+
|
| 79 |
+
- **Global (all projects)**: `~/.cursor/mcp.json`
|
| 80 |
+
- **Project-specific**: `.cursor/mcp.json` in project root
|
| 81 |
+
|
| 82 |
+
## Verify Installation
|
| 83 |
+
|
| 84 |
+
1. Restart Cursor completely
|
| 85 |
+
2. Check for green dot in Settings → Tools & Integrations → MCP Tools
|
| 86 |
+
3. In chat/composer, check "Available Tools"
|
| 87 |
+
4. Test with: "List my GitHub repositories"
|
| 88 |
+
|
| 89 |
+
## Troubleshooting
|
| 90 |
+
|
| 91 |
+
### Remote Server Issues
|
| 92 |
+
|
| 93 |
+
- **Streamable HTTP not working**: Ensure you're using Cursor v0.48.0 or later
|
| 94 |
+
- **Authentication failures**: Verify PAT has correct scopes
|
| 95 |
+
- **Connection errors**: Check firewall/proxy settings
|
| 96 |
+
|
| 97 |
+
### Local Server Issues
|
| 98 |
+
|
| 99 |
+
- **Docker errors**: Ensure Docker Desktop is running
|
| 100 |
+
- **Image pull failures**: Try `docker logout ghcr.io` then retry
|
| 101 |
+
- **Docker not found**: Install Docker Desktop and ensure it's running
|
| 102 |
+
|
| 103 |
+
### General Issues
|
| 104 |
+
|
| 105 |
+
- **MCP not loading**: Restart Cursor completely after configuration
|
| 106 |
+
- **Invalid JSON**: Validate that json format is correct
|
| 107 |
+
- **Tools not appearing**: Check server shows green dot in MCP settings
|
| 108 |
+
- **Check logs**: Look for MCP-related errors in Cursor logs
|
| 109 |
+
|
| 110 |
+
## Important Notes
|
| 111 |
+
|
| 112 |
+
- **Docker image**: `ghcr.io/github/github-mcp-server` (official and supported)
|
| 113 |
+
- **npm package**: `@modelcontextprotocol/server-github` (deprecated as of April 2025 - no longer functional)
|
| 114 |
+
- **Cursor specifics**: Supports both project and global configurations, uses `mcpServers` key
|
docs/installation-guides/install-other-copilot-ides.md
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Install GitHub MCP Server in Copilot IDEs
|
| 2 |
+
|
| 3 |
+
Quick setup guide for the GitHub MCP server in GitHub Copilot across different IDEs. For VS Code instructions, refer to the [VS Code install guide in the README](/README.md#installation-in-vs-code)
|
| 4 |
+
|
| 5 |
+
### Requirements:
|
| 6 |
+
- **GitHub Copilot License**: Any Copilot plan (Free, Pro, Pro+, Business, Enterprise) for Copilot access
|
| 7 |
+
- **GitHub Account**: Individual GitHub account (organization/enterprise membership optional) for GitHub MCP server access
|
| 8 |
+
- **MCP Servers in Copilot Policy**: Organizations assigning Copilot seats must enable this policy for all MCP access in Copilot for VS Code and Copilot Coding Agent – all other Copilot IDEs will migrate to this policy in the coming months
|
| 9 |
+
- **Editor Preview Policy**: Organizations assigning Copilot seats must enable this policy for OAuth access while the Remote GitHub MCP Server is in public preview
|
| 10 |
+
|
| 11 |
+
> **Note:** All Copilot IDEs now support the remote GitHub MCP server. VS Code offers OAuth authentication, while Visual Studio, JetBrains IDEs, Xcode, and Eclipse currently use PAT authentication with OAuth support coming soon.
|
| 12 |
+
|
| 13 |
+
## Visual Studio
|
| 14 |
+
|
| 15 |
+
Requires Visual Studio 2022 version 17.14.9 or later.
|
| 16 |
+
|
| 17 |
+
### Remote Server (Recommended)
|
| 18 |
+
|
| 19 |
+
The remote GitHub MCP server is hosted by GitHub and provides automatic updates with no local setup required.
|
| 20 |
+
|
| 21 |
+
#### Configuration
|
| 22 |
+
1. Create an `.mcp.json` file in your solution or %USERPROFILE% directory.
|
| 23 |
+
2. Add this configuration:
|
| 24 |
+
```json
|
| 25 |
+
{
|
| 26 |
+
"servers": {
|
| 27 |
+
"github": {
|
| 28 |
+
"url": "https://api.githubcopilot.com/mcp/"
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
```
|
| 33 |
+
3. Save the file. Wait for CodeLens to update to offer a way to authenticate to the new server, activate that and pick the GitHub account to authenticate with.
|
| 34 |
+
4. In the GitHub Copilot Chat window, switch to Agent mode.
|
| 35 |
+
5. Activate the tool picker in the Chat window and enable one or more tools from the "github" MCP server.
|
| 36 |
+
|
| 37 |
+
### Local Server
|
| 38 |
+
|
| 39 |
+
For users who prefer to run the GitHub MCP server locally. Requires Docker installed and running.
|
| 40 |
+
|
| 41 |
+
#### Configuration
|
| 42 |
+
1. Create an `.mcp.json` file in your solution or %USERPROFILE% directory.
|
| 43 |
+
2. Add this configuration:
|
| 44 |
+
```json
|
| 45 |
+
{
|
| 46 |
+
"inputs": [
|
| 47 |
+
{
|
| 48 |
+
"id": "github_pat",
|
| 49 |
+
"description": "GitHub personal access token",
|
| 50 |
+
"type": "promptString",
|
| 51 |
+
"password": true
|
| 52 |
+
}
|
| 53 |
+
],
|
| 54 |
+
"servers": {
|
| 55 |
+
"github": {
|
| 56 |
+
"type": "stdio",
|
| 57 |
+
"command": "docker",
|
| 58 |
+
"args": [
|
| 59 |
+
"run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN",
|
| 60 |
+
"ghcr.io/github/github-mcp-server"
|
| 61 |
+
],
|
| 62 |
+
"env": {
|
| 63 |
+
"GITHUB_PERSONAL_ACCESS_TOKEN": "${input:github_pat}"
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
```
|
| 69 |
+
3. Save the file. Wait for CodeLens to update to offer a way to provide user inputs, activate that and paste in a PAT you generate from https://github.com/settings/tokens.
|
| 70 |
+
4. In the GitHub Copilot Chat window, switch to Agent mode.
|
| 71 |
+
5. Activate the tool picker in the Chat window and enable one or more tools from the "github" MCP server.
|
| 72 |
+
|
| 73 |
+
**Documentation:** [Visual Studio MCP Guide](https://learn.microsoft.com/visualstudio/ide/mcp-servers)
|
| 74 |
+
|
| 75 |
+
---
|
| 76 |
+
|
| 77 |
+
## JetBrains IDEs
|
| 78 |
+
|
| 79 |
+
Agent mode and MCP support available in public preview across IntelliJ IDEA, PyCharm, WebStorm, and other JetBrains IDEs.
|
| 80 |
+
|
| 81 |
+
### Remote Server (Recommended)
|
| 82 |
+
|
| 83 |
+
The remote GitHub MCP server is hosted by GitHub and provides automatic updates with no local setup required.
|
| 84 |
+
|
| 85 |
+
> **Note**: OAuth authentication for the remote GitHub server is not yet supported in JetBrains IDEs. You must use a Personal Access Token (PAT).
|
| 86 |
+
|
| 87 |
+
#### Configuration Steps
|
| 88 |
+
1. Install/update the GitHub Copilot plugin
|
| 89 |
+
2. Click **GitHub Copilot icon in the status bar** → **Edit Settings** → **Model Context Protocol** → **Configure**
|
| 90 |
+
3. Add configuration:
|
| 91 |
+
```json
|
| 92 |
+
{
|
| 93 |
+
"servers": {
|
| 94 |
+
"github": {
|
| 95 |
+
"url": "https://api.githubcopilot.com/mcp/",
|
| 96 |
+
"requestInit": {
|
| 97 |
+
"headers": {
|
| 98 |
+
"Authorization": "Bearer YOUR_GITHUB_PAT"
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
}
|
| 104 |
+
```
|
| 105 |
+
4. Press `Ctrl + S` or `Command + S` to save, or close the `mcp.json` file. The configuration should take effect immediately and restart all the MCP servers defined. You can restart the IDE if needed.
|
| 106 |
+
|
| 107 |
+
### Local Server
|
| 108 |
+
|
| 109 |
+
For users who prefer to run the GitHub MCP server locally. Requires Docker installed and running.
|
| 110 |
+
|
| 111 |
+
#### Configuration
|
| 112 |
+
```json
|
| 113 |
+
{
|
| 114 |
+
"servers": {
|
| 115 |
+
"github": {
|
| 116 |
+
"command": "docker",
|
| 117 |
+
"args": [
|
| 118 |
+
"run", "-i", "--rm",
|
| 119 |
+
"-e", "GITHUB_PERSONAL_ACCESS_TOKEN",
|
| 120 |
+
"ghcr.io/github/github-mcp-server"
|
| 121 |
+
],
|
| 122 |
+
"env": {
|
| 123 |
+
"GITHUB_PERSONAL_ACCESS_TOKEN": "YOUR_GITHUB_PAT"
|
| 124 |
+
}
|
| 125 |
+
}
|
| 126 |
+
}
|
| 127 |
+
}
|
| 128 |
+
```
|
| 129 |
+
|
| 130 |
+
**Documentation:** [JetBrains Copilot Guide](https://plugins.jetbrains.com/plugin/17718-github-copilot)
|
| 131 |
+
|
| 132 |
+
---
|
| 133 |
+
|
| 134 |
+
## Xcode
|
| 135 |
+
|
| 136 |
+
Agent mode and MCP support now available in public preview for Xcode.
|
| 137 |
+
|
| 138 |
+
### Remote Server (Recommended)
|
| 139 |
+
|
| 140 |
+
The remote GitHub MCP server is hosted by GitHub and provides automatic updates with no local setup required.
|
| 141 |
+
|
| 142 |
+
> **Note**: OAuth authentication for the remote GitHub server is not yet supported in Xcode. You must use a Personal Access Token (PAT).
|
| 143 |
+
|
| 144 |
+
#### Configuration Steps
|
| 145 |
+
1. Install/update [GitHub Copilot for Xcode](https://github.com/github/CopilotForXcode)
|
| 146 |
+
2. Open **GitHub Copilot for Xcode app** → **Agent Mode** → **🛠️ Tool Picker** → **Edit Config**
|
| 147 |
+
3. Configure your MCP servers:
|
| 148 |
+
```json
|
| 149 |
+
{
|
| 150 |
+
"servers": {
|
| 151 |
+
"github": {
|
| 152 |
+
"url": "https://api.githubcopilot.com/mcp/",
|
| 153 |
+
"requestInit": {
|
| 154 |
+
"headers": {
|
| 155 |
+
"Authorization": "Bearer YOUR_GITHUB_PAT"
|
| 156 |
+
}
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
```
|
| 162 |
+
|
| 163 |
+
### Local Server
|
| 164 |
+
|
| 165 |
+
For users who prefer to run the GitHub MCP server locally. Requires Docker installed and running.
|
| 166 |
+
|
| 167 |
+
#### Configuration
|
| 168 |
+
```json
|
| 169 |
+
{
|
| 170 |
+
"servers": {
|
| 171 |
+
"github": {
|
| 172 |
+
"command": "docker",
|
| 173 |
+
"args": [
|
| 174 |
+
"run", "-i", "--rm",
|
| 175 |
+
"-e", "GITHUB_PERSONAL_ACCESS_TOKEN",
|
| 176 |
+
"ghcr.io/github/github-mcp-server"
|
| 177 |
+
],
|
| 178 |
+
"env": {
|
| 179 |
+
"GITHUB_PERSONAL_ACCESS_TOKEN": "YOUR_GITHUB_PAT"
|
| 180 |
+
}
|
| 181 |
+
}
|
| 182 |
+
}
|
| 183 |
+
}
|
| 184 |
+
```
|
| 185 |
+
|
| 186 |
+
**Documentation:** [Xcode Copilot Guide](https://devblogs.microsoft.com/xcode/github-copilot-exploring-agent-mode-and-mcp-support-in-public-preview-for-xcode/)
|
| 187 |
+
|
| 188 |
+
---
|
| 189 |
+
|
| 190 |
+
## Eclipse
|
| 191 |
+
|
| 192 |
+
MCP support available with Eclipse 2024-03+ and latest version of the GitHub Copilot plugin.
|
| 193 |
+
|
| 194 |
+
### Remote Server (Recommended)
|
| 195 |
+
|
| 196 |
+
The remote GitHub MCP server is hosted by GitHub and provides automatic updates with no local setup required.
|
| 197 |
+
|
| 198 |
+
> **Note**: OAuth authentication for the remote GitHub server is not yet supported in Eclipse. You must use a Personal Access Token (PAT).
|
| 199 |
+
|
| 200 |
+
#### Configuration Steps
|
| 201 |
+
1. Install GitHub Copilot extension from Eclipse Marketplace
|
| 202 |
+
2. Click the **GitHub Copilot icon** → **Edit Preferences** → **MCP** (under **GitHub Copilot**)
|
| 203 |
+
3. Add GitHub MCP server configuration:
|
| 204 |
+
```json
|
| 205 |
+
{
|
| 206 |
+
"servers": {
|
| 207 |
+
"github": {
|
| 208 |
+
"url": "https://api.githubcopilot.com/mcp/",
|
| 209 |
+
"requestInit": {
|
| 210 |
+
"headers": {
|
| 211 |
+
"Authorization": "Bearer YOUR_GITHUB_PAT"
|
| 212 |
+
}
|
| 213 |
+
}
|
| 214 |
+
}
|
| 215 |
+
}
|
| 216 |
+
}
|
| 217 |
+
```
|
| 218 |
+
4. Click the "Apply and Close" button in the preference dialog and the configuration will take effect automatically.
|
| 219 |
+
|
| 220 |
+
### Local Server
|
| 221 |
+
|
| 222 |
+
For users who prefer to run the GitHub MCP server locally. Requires Docker installed and running.
|
| 223 |
+
|
| 224 |
+
#### Configuration
|
| 225 |
+
```json
|
| 226 |
+
{
|
| 227 |
+
"servers": {
|
| 228 |
+
"github": {
|
| 229 |
+
"command": "docker",
|
| 230 |
+
"args": [
|
| 231 |
+
"run", "-i", "--rm",
|
| 232 |
+
"-e", "GITHUB_PERSONAL_ACCESS_TOKEN",
|
| 233 |
+
"ghcr.io/github/github-mcp-server"
|
| 234 |
+
],
|
| 235 |
+
"env": {
|
| 236 |
+
"GITHUB_PERSONAL_ACCESS_TOKEN": "YOUR_GITHUB_PAT"
|
| 237 |
+
}
|
| 238 |
+
}
|
| 239 |
+
}
|
| 240 |
+
}
|
| 241 |
+
```
|
| 242 |
+
|
| 243 |
+
**Documentation:** [Eclipse Copilot plugin](https://marketplace.eclipse.org/content/github-copilot)
|
| 244 |
+
|
| 245 |
+
---
|
| 246 |
+
|
| 247 |
+
## GitHub Personal Access Token
|
| 248 |
+
|
| 249 |
+
For PAT authentication, see our [Personal Access Token documentation](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) for setup instructions.
|
| 250 |
+
|
| 251 |
+
---
|
| 252 |
+
|
| 253 |
+
## Usage
|
| 254 |
+
|
| 255 |
+
After setup:
|
| 256 |
+
1. Restart your IDE completely
|
| 257 |
+
2. Open Agent mode in Copilot Chat
|
| 258 |
+
3. Try: *"List recent issues in this repository"*
|
| 259 |
+
4. Copilot can now access GitHub data and perform repository operations
|
| 260 |
+
|
| 261 |
+
---
|
| 262 |
+
|
| 263 |
+
## Troubleshooting
|
| 264 |
+
|
| 265 |
+
- **Connection issues**: Verify GitHub PAT permissions and IDE version compatibility
|
| 266 |
+
- **Authentication errors**: Check if your organization has enabled the MCP policy for Copilot
|
| 267 |
+
- **Tools not appearing**: Restart IDE after configuration changes and check error logs
|
| 268 |
+
- **Local server issues**: Ensure Docker is running for Docker-based setups
|
docs/installation-guides/install-windsurf.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Install GitHub MCP Server in Windsurf
|
| 2 |
+
|
| 3 |
+
## Prerequisites
|
| 4 |
+
1. Windsurf IDE installed (latest version)
|
| 5 |
+
2. [GitHub Personal Access Token](https://github.com/settings/personal-access-tokens/new) with appropriate scopes
|
| 6 |
+
3. For local installation: [Docker](https://www.docker.com/) installed and running
|
| 7 |
+
|
| 8 |
+
## Remote Server Setup (Recommended)
|
| 9 |
+
|
| 10 |
+
The remote GitHub MCP server is hosted by GitHub at `https://api.githubcopilot.com/mcp/` and supports Streamable HTTP protocol. Windsurf currently supports PAT authentication only.
|
| 11 |
+
|
| 12 |
+
### Streamable HTTP Configuration
|
| 13 |
+
Windsurf supports Streamable HTTP servers with a `serverUrl` field:
|
| 14 |
+
|
| 15 |
+
```json
|
| 16 |
+
{
|
| 17 |
+
"mcpServers": {
|
| 18 |
+
"github": {
|
| 19 |
+
"serverUrl": "https://api.githubcopilot.com/mcp/",
|
| 20 |
+
"headers": {
|
| 21 |
+
"Authorization": "Bearer YOUR_GITHUB_PAT"
|
| 22 |
+
}
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
## Local Server Setup
|
| 29 |
+
|
| 30 |
+
### Docker Installation (Required)
|
| 31 |
+
**Important**: The npm package `@modelcontextprotocol/server-github` is no longer supported as of April 2025. Use the official Docker image `ghcr.io/github/github-mcp-server` instead.
|
| 32 |
+
|
| 33 |
+
```json
|
| 34 |
+
{
|
| 35 |
+
"mcpServers": {
|
| 36 |
+
"github": {
|
| 37 |
+
"command": "docker",
|
| 38 |
+
"args": [
|
| 39 |
+
"run",
|
| 40 |
+
"-i",
|
| 41 |
+
"--rm",
|
| 42 |
+
"-e",
|
| 43 |
+
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
| 44 |
+
"ghcr.io/github/github-mcp-server"
|
| 45 |
+
],
|
| 46 |
+
"env": {
|
| 47 |
+
"GITHUB_PERSONAL_ACCESS_TOKEN": "YOUR_GITHUB_PAT"
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
## Installation Steps
|
| 55 |
+
|
| 56 |
+
### Via Plugin Store
|
| 57 |
+
1. Open Windsurf and navigate to Cascade
|
| 58 |
+
2. Click the **Plugins** icon or **hammer icon** (🔨)
|
| 59 |
+
3. Search for "GitHub MCP Server"
|
| 60 |
+
4. Click **Install** and enter your PAT when prompted
|
| 61 |
+
5. Click **Refresh** (🔄)
|
| 62 |
+
|
| 63 |
+
### Manual Configuration
|
| 64 |
+
1. Click the hammer icon (🔨) in Cascade
|
| 65 |
+
2. Click **Configure** to open `~/.codeium/windsurf/mcp_config.json`
|
| 66 |
+
3. Add your chosen configuration from above
|
| 67 |
+
4. Save the file
|
| 68 |
+
5. Click **Refresh** (🔄) in the MCP toolbar
|
| 69 |
+
|
| 70 |
+
## Configuration Details
|
| 71 |
+
|
| 72 |
+
- **File path**: `~/.codeium/windsurf/mcp_config.json`
|
| 73 |
+
- **Scope**: Global configuration only (no per-project support)
|
| 74 |
+
- **Format**: Must be valid JSON (use a linter to verify)
|
| 75 |
+
|
| 76 |
+
## Verification
|
| 77 |
+
|
| 78 |
+
After installation:
|
| 79 |
+
1. Look for "1 available MCP server" in the MCP toolbar
|
| 80 |
+
2. Click the hammer icon to see available GitHub tools
|
| 81 |
+
3. Test with: "List my GitHub repositories"
|
| 82 |
+
4. Check for green dot next to the server name
|
| 83 |
+
|
| 84 |
+
## Troubleshooting
|
| 85 |
+
|
| 86 |
+
### Remote Server Issues
|
| 87 |
+
- **Authentication failures**: Verify PAT has correct scopes and hasn't expired
|
| 88 |
+
- **Connection errors**: Check firewall/proxy settings for HTTPS connections
|
| 89 |
+
- **Streamable HTTP not working**: Ensure you're using the correct `serverUrl` field format
|
| 90 |
+
|
| 91 |
+
### Local Server Issues
|
| 92 |
+
- **Docker errors**: Ensure Docker Desktop is running
|
| 93 |
+
- **Image pull failures**: Try `docker logout ghcr.io` then retry
|
| 94 |
+
- **Docker not found**: Install Docker Desktop and ensure it's running
|
| 95 |
+
|
| 96 |
+
### General Issues
|
| 97 |
+
- **Invalid JSON**: Validate with [jsonlint.com](https://jsonlint.com)
|
| 98 |
+
- **Tools not appearing**: Restart Windsurf completely
|
| 99 |
+
- **Check logs**: `~/.codeium/windsurf/logs/`
|
| 100 |
+
|
| 101 |
+
## Important Notes
|
| 102 |
+
|
| 103 |
+
- **Official repository**: [github/github-mcp-server](https://github.com/github/github-mcp-server)
|
| 104 |
+
- **Remote server URL**: `https://api.githubcopilot.com/mcp/`
|
| 105 |
+
- **Docker image**: `ghcr.io/github/github-mcp-server` (official and supported)
|
| 106 |
+
- **npm package**: `@modelcontextprotocol/server-github` (deprecated as of April 2025 - no longer functional)
|
| 107 |
+
- **Windsurf limitations**: No environment variable interpolation, global config only
|
docs/policies-and-governance.md
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Policies & Governance for the GitHub MCP Server
|
| 2 |
+
|
| 3 |
+
Organizations and enterprises have several existing control mechanisms for the GitHub MCP server on GitHub.com:
|
| 4 |
+
- MCP servers in Copilot Policy
|
| 5 |
+
- Copilot Editor Preview Policy (temporary)
|
| 6 |
+
- OAuth App Access Policies
|
| 7 |
+
- GitHub App Installation
|
| 8 |
+
- Personal Access Token (PAT) policies
|
| 9 |
+
- SSO Enforcement
|
| 10 |
+
|
| 11 |
+
This document outlines how these policies apply to different deployment modes, authentication methods, and host applications – while providing guidance for managing GitHub MCP Server access across your organization.
|
| 12 |
+
|
| 13 |
+
## How the GitHub MCP Server Works
|
| 14 |
+
|
| 15 |
+
The GitHub MCP Server provides access to GitHub resources and capabilities through a standardized protocol, with flexible deployment and authentication options tailored to different use cases. It supports two deployment modes, both built on the same underlying codebase.
|
| 16 |
+
|
| 17 |
+
### 1. Local GitHub MCP Server
|
| 18 |
+
* **Runs:** Locally alongside your IDE or application
|
| 19 |
+
* **Authentication & Controls:** Requires Personal Access Tokens (PATs). Users must generate and configure a PAT to connect. Managed via [PAT policies](https://docs.github.com/organizations/managing-programmatic-access-to-your-organization/setting-a-personal-access-token-policy-for-your-organization#restricting-access-by-personal-access-tokens).
|
| 20 |
+
* Can optionally use GitHub App installation tokens when embedded in a GitHub App-based tool (rare).
|
| 21 |
+
|
| 22 |
+
**Supported SKUs:** Can be used with GitHub Enterprise Server (GHES) and GitHub Enterprise Cloud (GHEC).
|
| 23 |
+
|
| 24 |
+
### 2. Remote GitHub MCP Server
|
| 25 |
+
* **Runs:** As a hosted service accessed over the internet
|
| 26 |
+
* **Authentication & Controls:** (determined by the chosen authentication method)
|
| 27 |
+
* **GitHub App Installation Tokens:** Uses a signed JWT to request installation access tokens (similar to the OAuth 2.0 client credentials flow) to operate as the application itself. Provides granular control via [installation](https://docs.github.com/apps/using-github-apps/installing-a-github-app-from-a-third-party#requirements-to-install-a-github-app), [permissions](https://docs.github.com/apps/creating-github-apps/registering-a-github-app/choosing-permissions-for-a-github-app) and [repository access controls](https://docs.github.com/apps/using-github-apps/reviewing-and-modifying-installed-github-apps#modifying-repository-access).
|
| 28 |
+
* **OAuth Authorization Code Flow:** Uses the standard OAuth 2.0 Authorization Code flow. Controlled via [OAuth App access policies](https://docs.github.com/organizations/managing-oauth-access-to-your-organizations-data/about-oauth-app-access-restrictions) for OAuth apps. For GitHub Apps that sign in ([are authorized by](https://docs.github.com/apps/using-github-apps/authorizing-github-apps)) a user, control access to your organization via [installation](https://docs.github.com/apps/using-github-apps/installing-a-github-app-from-a-third-party#requirements-to-install-a-github-app).
|
| 29 |
+
* **Personal Access Tokens (PATs):** Managed via [PAT policies](https://docs.github.com/organizations/managing-programmatic-access-to-your-organization/setting-a-personal-access-token-policy-for-your-organization#restricting-access-by-personal-access-tokens).
|
| 30 |
+
* **SSO enforcement:** Applies when using OAuth Apps, GitHub Apps, and PATs to access resources in organizations and enterprises with SSO enabled. Acts as an overlay control. Users must have a valid SSO session for your organization or enterprise when signing into the app or creating the token in order for the token to access your resources. Learn more in the [SSO documentation](https://docs.github.com/enterprise-cloud@latest/authentication/authenticating-with-single-sign-on/about-authentication-with-single-sign-on#about-oauth-apps-github-apps-and-sso).
|
| 31 |
+
|
| 32 |
+
**Supported Platforms:** Currently available only on GitHub Enterprise Cloud (GHEC). Remote hosting for GHES is not supported at this time.
|
| 33 |
+
|
| 34 |
+
> **Note:** This does not apply to the Local GitHub MCP Server, which uses PATs and does not rely on GitHub App installations.
|
| 35 |
+
|
| 36 |
+
#### Enterprise Install Considerations
|
| 37 |
+
|
| 38 |
+
- When using the Remote GitHub MCP Server, if authenticating with OAuth instead of PAT, each host application must have a registered GitHub App (or OAuth App) to authenticate on behalf of the user.
|
| 39 |
+
- Enterprises may choose to install these apps in multiple organizations (e.g., per team or department) to scope access narrowly, or at the enterprise level to centralize access control across all child organizations.
|
| 40 |
+
- Enterprise installation is only supported for GitHub Apps. OAuth Apps can only be installed on a per organization basis in multi-org enterprises.
|
| 41 |
+
|
| 42 |
+
### Security Principles for Both Modes
|
| 43 |
+
* **Authentication:** Required for all operations, no anonymous access
|
| 44 |
+
* **Authorization:** Access enforced by GitHub's native permission model. Users and apps cannot use an MCP server to access more resources than they could otherwise access normally via the API.
|
| 45 |
+
* **Communication:** All data transmitted over HTTPS with optional SSE for real-time updates
|
| 46 |
+
* **Rate Limiting:** Subject to GitHub API rate limits based on authentication method
|
| 47 |
+
* **Token Storage:** Tokens should be stored securely using platform-appropriate credential storage
|
| 48 |
+
* **Audit Trail:** All underlying API calls are logged in GitHub's audit log when available
|
| 49 |
+
|
| 50 |
+
For integration architecture and implementation details, see the [Host Integration Guide](https://github.com/github/github-mcp-server/blob/main/docs/host-integration.md).
|
| 51 |
+
|
| 52 |
+
## Where It's Used
|
| 53 |
+
|
| 54 |
+
The GitHub MCP server can be accessed in various environments (referred to as "host" applications):
|
| 55 |
+
* **First-party Hosts:** GitHub Copilot in VS Code, Visual Studio, JetBrains, Eclipse, and Xcode with integrated MCP support, as well as Copilot Coding Agent.
|
| 56 |
+
* **Third-party Hosts:** Editors outside the GitHub ecosystem, such as Claude, Cursor, Windsurf, and Cline, that support connecting to MCP servers, as well as AI chat applications like Claude Desktop and other AI assistants that connect to MCP servers to fetch GitHub context or execute write actions.
|
| 57 |
+
|
| 58 |
+
## What It Can Access
|
| 59 |
+
|
| 60 |
+
The MCP server accesses GitHub resources based on the permissions granted through the chosen authentication method (PAT, OAuth, or GitHub App). These may include:
|
| 61 |
+
* Repository contents (files, branches, commits)
|
| 62 |
+
* Issues and pull requests
|
| 63 |
+
* Organization and team metadata
|
| 64 |
+
* User profile information
|
| 65 |
+
* Actions workflow runs, logs, and statuses
|
| 66 |
+
* Security and vulnerability alerts (if explicitly granted)
|
| 67 |
+
|
| 68 |
+
Access is always constrained by GitHub's public API permission model and the authenticated user's privileges.
|
| 69 |
+
|
| 70 |
+
## Control Mechanisms
|
| 71 |
+
|
| 72 |
+
### 1. Copilot Editors (first-party) → MCP Servers in Copilot Policy
|
| 73 |
+
|
| 74 |
+
* **Policy:** MCP servers in Copilot
|
| 75 |
+
* **Location:** Enterprise/Org → Policies → Copilot
|
| 76 |
+
* **What it controls:** When disabled, **completely blocks all GitHub MCP Server access** (both remote and local) for affected Copilot editors. Currently applies to VS Code and Copilot Coding Agent, with more Copilot editors expected to migrate to this policy over time.
|
| 77 |
+
* **Impact when disabled:** Host applications governed by this policy cannot connect to the GitHub MCP Server through any authentication method (OAuth, PAT, or GitHub App).
|
| 78 |
+
* **What it does NOT affect:**
|
| 79 |
+
* MCP support in Copilot on IDEs that are still in public preview (Visual Studio, JetBrains, Xcode, Eclipse)
|
| 80 |
+
* Third-party IDE or host apps (like Claude, Cursor, Windsurf) not governed by GitHub's Copilot policies
|
| 81 |
+
* Community-authored MCP servers using GitHub's public APIs
|
| 82 |
+
|
| 83 |
+
> **Important:** This policy provides comprehensive control over GitHub MCP Server access in Copilot editors. When disabled, users in affected applications will not be able to use the GitHub MCP Server regardless of deployment mode (remote or local) or authentication method.
|
| 84 |
+
|
| 85 |
+
#### Temporary: Copilot Editor Preview Policy
|
| 86 |
+
|
| 87 |
+
* **Policy:** Editor Preview Features
|
| 88 |
+
* **Status:** Being phased out as editors migrate to the "MCP servers in Copilot" policy above, and once the Remote GitHub MCP server goes GA
|
| 89 |
+
* **What it controls:** When disabled, prevents remaining Copilot editors from using the Remote GitHub MCP Server through OAuth connections in all first-party and third-party host applications (does not affect local deployments or PAT authentication)
|
| 90 |
+
|
| 91 |
+
> **Note:** As Copilot editors migrate from the "Copilot Editor Preview" policy to the "MCP servers in Copilot" policy, the scope of control becomes more centralized, blocking both remote and local GitHub MCP Server access when disabled. Access in third-party hosts is governed separately by OAuth App, GitHub App, and PAT policies.
|
| 92 |
+
|
| 93 |
+
### 2. Third-Party Host Apps (e.g., Claude, Cursor, Windsurf) → OAuth App or GitHub App Controls
|
| 94 |
+
|
| 95 |
+
#### a. OAuth App Access Policies
|
| 96 |
+
* **Control Mechanism:** OAuth App access restrictions
|
| 97 |
+
* **Location:** Org → Settings → Third-party Access → OAuth app policy
|
| 98 |
+
* **How it works:**
|
| 99 |
+
* Organization admins must approve OAuth App requests before host apps can access organization data
|
| 100 |
+
* Only applies when the host registers an OAuth App AND the user connects via OAuth 2.0 flow
|
| 101 |
+
|
| 102 |
+
#### b. GitHub App Installation
|
| 103 |
+
* **Control Mechanism:** GitHub App installation and permissions
|
| 104 |
+
* **Location:** Org → Settings → Third-party Access → GitHub Apps
|
| 105 |
+
* **What it controls:** Organization admins must install the app, select repositories, and grant permissions before the app can access organization-owned data or resources through the Remote GitHub Server.
|
| 106 |
+
* **How it works:**
|
| 107 |
+
* Organization admins must install the app, specify repositories, and approve permissions
|
| 108 |
+
* Only applies when the host registers a GitHub App AND the user authenticates through that flow
|
| 109 |
+
|
| 110 |
+
> **Note:** The authentication methods available depend on what your host application supports. While PATs work with any remote MCP-compatible host, OAuth and GitHub App authentication are only available if the host has registered an app with GitHub. Check your host application's documentation or support for more info.
|
| 111 |
+
|
| 112 |
+
### 3. PAT Access from Any Host → PAT Restrictions
|
| 113 |
+
|
| 114 |
+
* **Types:** Fine-grained PATs (recommended) and Classic tokens (legacy)
|
| 115 |
+
* **Location:**
|
| 116 |
+
* User level: [Personal Settings → Developer Settings → Personal Access Tokens](https://docs.github.com/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#fine-grained-personal-access-tokens)
|
| 117 |
+
* Enterprise/Organization level: [Enterprise/Organization → Settings → Personal Access Tokens](https://docs.github.com/organizations/managing-programmatic-access-to-your-organization/setting-a-personal-access-token-policy-for-your-organization) (to control PAT creation/access policies)
|
| 118 |
+
* **What it controls:** Applies to all host apps and both local & remote GitHub MCP servers when users authenticate via PAT.
|
| 119 |
+
* **How it works:** Access limited to the repositories and scopes selected on the token.
|
| 120 |
+
* **Limitations:** PATs do not adhere to OAuth App policies and GitHub App installation controls. They are user-scoped and not recommended for production automation.
|
| 121 |
+
* **Organization controls:**
|
| 122 |
+
* Classic PATs: Can be completely disabled organization-wide
|
| 123 |
+
* Fine-grained PATs: Cannot be disabled but require explicit approval for organization access
|
| 124 |
+
|
| 125 |
+
> **Recommendation:** We recommend using fine-grained PATs over classic tokens. Classic tokens have broader scopes and can be disabled in organization settings.
|
| 126 |
+
|
| 127 |
+
### 4. SSO Enforcement (overlay control)
|
| 128 |
+
|
| 129 |
+
* **Location:** Enterprise/Organization → SSO settings
|
| 130 |
+
* **What it controls:** OAuth tokens and PATs must map to a recent SSO login to access SSO-protected organization data.
|
| 131 |
+
* **How it works:** Applies to ALL host apps when using OAuth or PATs.
|
| 132 |
+
|
| 133 |
+
> **Exception:** Does NOT apply to GitHub App installation tokens (these are installation-scoped, not user-scoped)
|
| 134 |
+
|
| 135 |
+
## Current Limitations
|
| 136 |
+
|
| 137 |
+
While the GitHub MCP Server provides dynamic tooling and capabilities, the following enterprise governance features are not yet available:
|
| 138 |
+
|
| 139 |
+
### Single Enterprise/Organization-Level Toggle
|
| 140 |
+
|
| 141 |
+
GitHub does not provide a single toggle that blocks all GitHub MCP server traffic for every user. Admins can achieve equivalent coverage by combining the controls shown here:
|
| 142 |
+
* **First-party Copilot Editors (GitHub Copilot in VS Code, Visual Studio, JetBrains, Eclipse):**
|
| 143 |
+
* Disable the "MCP servers in Copilot" policy for comprehensive control
|
| 144 |
+
* Or disable the Editor Preview Features policy (for editors still using the legacy policy)
|
| 145 |
+
* **Third-party Host Applications:**
|
| 146 |
+
* Configure OAuth app restrictions
|
| 147 |
+
* Manage GitHub App installations
|
| 148 |
+
* **PAT Access in All Host Applications:**
|
| 149 |
+
* Implement fine-grained PAT policies (applies to both remote and local deployments)
|
| 150 |
+
|
| 151 |
+
### MCP-Specific Audit Logging
|
| 152 |
+
|
| 153 |
+
At present, MCP traffic appears in standard GitHub audit logs as normal API calls. Purpose-built logging for MCP is on the roadmap, but the following views are not yet available:
|
| 154 |
+
* Real-time list of active MCP connections
|
| 155 |
+
* Dashboards showing granular MCP usage data, like tools or host apps
|
| 156 |
+
* Granular, action-by-action audit logs
|
| 157 |
+
|
| 158 |
+
Until those arrive, teams can continue to monitor MCP activity through existing API log entries and OAuth/GitHub App events.
|
| 159 |
+
|
| 160 |
+
## Security Best Practices
|
| 161 |
+
|
| 162 |
+
### For Organizations
|
| 163 |
+
|
| 164 |
+
**GitHub App Management**
|
| 165 |
+
* Review [GitHub App installations](https://docs.github.com/apps/using-github-apps/reviewing-and-modifying-installed-github-apps) regularly
|
| 166 |
+
* Audit permissions and repository access
|
| 167 |
+
* Monitor installation events in audit logs
|
| 168 |
+
* Document approved GitHub Apps and their business purposes
|
| 169 |
+
|
| 170 |
+
**OAuth App Governance**
|
| 171 |
+
* Manage [OAuth App access policies](https://docs.github.com/organizations/managing-oauth-access-to-your-organizations-data/about-oauth-app-access-restrictions)
|
| 172 |
+
* Establish review processes for approved applications
|
| 173 |
+
* Monitor which third-party applications are requesting access
|
| 174 |
+
* Maintain an allowlist of approved OAuth applications
|
| 175 |
+
|
| 176 |
+
**Token Management**
|
| 177 |
+
* Mandate fine-grained Personal Access Tokens over classic tokens
|
| 178 |
+
* Establish token expiration policies (90 days maximum recommended)
|
| 179 |
+
* Implement automated token rotation reminders
|
| 180 |
+
* Review and enforce [PAT restrictions](https://docs.github.com/organizations/managing-programmatic-access-to-your-organization/setting-a-personal-access-token-policy-for-your-organization) at the appropriate level
|
| 181 |
+
|
| 182 |
+
### For Developers and Users
|
| 183 |
+
|
| 184 |
+
**Authentication Security**
|
| 185 |
+
* Prioritize OAuth 2.0 flows over long-lived tokens
|
| 186 |
+
* Prefer fine-grained PATs to PATs (Classic)
|
| 187 |
+
* Store tokens securely using platform-appropriate credential management
|
| 188 |
+
* Store credentials in secret management systems, not source code
|
| 189 |
+
|
| 190 |
+
**Scope Minimization**
|
| 191 |
+
* Request only the minimum required scopes for your use case
|
| 192 |
+
* Regularly review and revoke unused token permissions
|
| 193 |
+
* Use repository-specific access instead of organization-wide access
|
| 194 |
+
* Document why each permission is needed for your integration
|
| 195 |
+
|
| 196 |
+
## Resources
|
| 197 |
+
|
| 198 |
+
**MCP:**
|
| 199 |
+
* [Model Context Protocol Specification](https://modelcontextprotocol.io/specification/2025-03-26)
|
| 200 |
+
* [Model Context Protocol Authorization](https://modelcontextprotocol.io/specification/draft/basic/authorization)
|
| 201 |
+
|
| 202 |
+
**GitHub Governance & Controls:**
|
| 203 |
+
* [Managing OAuth App Access](https://docs.github.com/organizations/managing-oauth-access-to-your-organizations-data/about-oauth-app-access-restrictions)
|
| 204 |
+
* [GitHub App Permissions](https://docs.github.com/apps/creating-github-apps/registering-a-github-app/choosing-permissions-for-a-github-app)
|
| 205 |
+
* [Updating permissions for a GitHub App](https://docs.github.com/apps/using-github-apps/approving-updated-permissions-for-a-github-app)
|
| 206 |
+
* [PAT Policies](https://docs.github.com/organizations/managing-programmatic-access-to-your-organization/setting-a-personal-access-token-policy-for-your-organization)
|
| 207 |
+
* [Fine-grained PATs](https://docs.github.com/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#fine-grained-personal-access-tokens)
|
| 208 |
+
* [Setting a PAT policy for your organization](https://docs.github.com/organizations/managing-oauth-access-to-your-organizations-data/about-oauth-app-access-restrictions)
|
| 209 |
+
|
| 210 |
+
---
|
| 211 |
+
|
| 212 |
+
**Questions or Feedback?**
|
| 213 |
+
|
| 214 |
+
Open an [issue in the github-mcp-server repository](https://github.com/github/github-mcp-server/issues) with the label "policies & governance" attached.
|
| 215 |
+
|
| 216 |
+
This document reflects GitHub MCP Server policies as of July 2025. Policies and capabilities continue to evolve based on customer feedback and security best practices.
|
docs/remote-server.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Remote GitHub MCP Server 🚀
|
| 2 |
+
|
| 3 |
+
[](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2F%22%7D) [](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2F%22%7D&quality=insiders)
|
| 4 |
+
|
| 5 |
+
Easily connect to the GitHub MCP Server using the hosted version – no local setup or runtime required.
|
| 6 |
+
|
| 7 |
+
**URL:** https://api.githubcopilot.com/mcp/
|
| 8 |
+
|
| 9 |
+
## About
|
| 10 |
+
|
| 11 |
+
The remote GitHub MCP server is built using this repository as a library, and binding it into GitHub server infrastructure with an internal repository. You can open issues and propose changes in this repository, and we regularly update the remote server to include the latest version of this code.
|
| 12 |
+
|
| 13 |
+
The remote server has [additional tools](#toolsets-only-available-in-the-remote-mcp-server) that are not available in the local MCP server, such as the `create_pull_request_with_copilot` tool for invoking Copilot coding agent.
|
| 14 |
+
|
| 15 |
+
## Remote MCP Toolsets
|
| 16 |
+
|
| 17 |
+
Below is a table of available toolsets for the remote GitHub MCP Server. Each toolset is provided as a distinct URL so you can mix and match to create the perfect combination of tools for your use-case. Add `/readonly` to the end of any URL to restrict the tools in the toolset to only those that enable read access. We also provide the option to use [headers](#headers) instead.
|
| 18 |
+
|
| 19 |
+
<!-- START AUTOMATED TOOLSETS -->
|
| 20 |
+
| Name | Description | API URL | 1-Click Install (VS Code) | Read-only Link | 1-Click Read-only Install (VS Code) |
|
| 21 |
+
|----------------|--------------------------------------------------|-------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
| 22 |
+
| all | All available GitHub MCP tools | https://api.githubcopilot.com/mcp/ | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2F%22%7D) | [read-only](https://api.githubcopilot.com/mcp/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Freadonly%22%7D) |
|
| 23 |
+
| Actions | GitHub Actions workflows and CI/CD operations | https://api.githubcopilot.com/mcp/x/actions | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-actions&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Factions%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/actions/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-actions&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Factions%2Freadonly%22%7D) |
|
| 24 |
+
| Code Security | Code security related tools, such as GitHub Code Scanning | https://api.githubcopilot.com/mcp/x/code_security | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-code_security&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fcode_security%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/code_security/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-code_security&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fcode_security%2Freadonly%22%7D) |
|
| 25 |
+
| Dependabot | Dependabot tools | https://api.githubcopilot.com/mcp/x/dependabot | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-dependabot&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fdependabot%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/dependabot/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-dependabot&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fdependabot%2Freadonly%22%7D) |
|
| 26 |
+
| Discussions | GitHub Discussions related tools | https://api.githubcopilot.com/mcp/x/discussions | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-discussions&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fdiscussions%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/discussions/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-discussions&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fdiscussions%2Freadonly%22%7D) |
|
| 27 |
+
| Experiments | Experimental features that are not considered stable yet | https://api.githubcopilot.com/mcp/x/experiments | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-experiments&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fexperiments%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/experiments/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-experiments&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fexperiments%2Freadonly%22%7D) |
|
| 28 |
+
| Gists | GitHub Gist related tools | https://api.githubcopilot.com/mcp/x/gists | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-gists&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fgists%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/gists/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-gists&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fgists%2Freadonly%22%7D) |
|
| 29 |
+
| Issues | GitHub Issues related tools | https://api.githubcopilot.com/mcp/x/issues | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-issues&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fissues%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/issues/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-issues&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fissues%2Freadonly%22%7D) |
|
| 30 |
+
| Notifications | GitHub Notifications related tools | https://api.githubcopilot.com/mcp/x/notifications | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-notifications&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fnotifications%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/notifications/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-notifications&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fnotifications%2Freadonly%22%7D) |
|
| 31 |
+
| Organizations | GitHub Organization related tools | https://api.githubcopilot.com/mcp/x/orgs | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-orgs&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Forgs%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/orgs/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-orgs&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Forgs%2Freadonly%22%7D) |
|
| 32 |
+
| Pull Requests | GitHub Pull Request related tools | https://api.githubcopilot.com/mcp/x/pull_requests | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-pull_requests&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fpull_requests%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/pull_requests/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-pull_requests&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fpull_requests%2Freadonly%22%7D) |
|
| 33 |
+
| Repositories | GitHub Repository related tools | https://api.githubcopilot.com/mcp/x/repos | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-repos&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Frepos%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/repos/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-repos&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Frepos%2Freadonly%22%7D) |
|
| 34 |
+
| Secret Protection | Secret protection related tools, such as GitHub Secret Scanning | https://api.githubcopilot.com/mcp/x/secret_protection | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-secret_protection&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fsecret_protection%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/secret_protection/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-secret_protection&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fsecret_protection%2Freadonly%22%7D) |
|
| 35 |
+
| Security Advisories | Security advisories related tools | https://api.githubcopilot.com/mcp/x/security_advisories | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-security_advisories&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fsecurity_advisories%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/security_advisories/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-security_advisories&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fsecurity_advisories%2Freadonly%22%7D) |
|
| 36 |
+
| Users | GitHub User related tools | https://api.githubcopilot.com/mcp/x/users | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-users&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fusers%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/users/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-users&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fusers%2Freadonly%22%7D) |
|
| 37 |
+
|
| 38 |
+
<!-- END AUTOMATED TOOLSETS -->
|
| 39 |
+
|
| 40 |
+
### Additional _Remote_ Server Toolsets
|
| 41 |
+
|
| 42 |
+
These toolsets are only available in the remote GitHub MCP Server and are not included in the local MCP server.
|
| 43 |
+
|
| 44 |
+
| Name | Description | API URL | 1-Click Install (VS Code) | Read-only Link | 1-Click Read-only Install (VS Code) |
|
| 45 |
+
| -------------------- | --------------------------------------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| 46 |
+
| Copilot coding agent | Perform task with GitHub Copilot coding agent | https://api.githubcopilot.com/mcp/x/copilot | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-copilot&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fcopilot%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/copilot/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-copilot&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fcopilot%2Freadonly%22%7D) |
|
| 47 |
+
|
| 48 |
+
### Headers
|
| 49 |
+
|
| 50 |
+
You can configure toolsets and readonly mode by providing HTTP headers in your server configuration.
|
| 51 |
+
|
| 52 |
+
The headers are:
|
| 53 |
+
- `X-MCP-Toolsets=<toolset>,<toolset>...`
|
| 54 |
+
- `X-MCP-Readonly=true`
|
docs/testing.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Testing
|
| 2 |
+
|
| 3 |
+
This project uses a combination of unit tests and end-to-end (e2e) tests to ensure correctness and stability.
|
| 4 |
+
|
| 5 |
+
## Unit Testing Patterns
|
| 6 |
+
|
| 7 |
+
- Unit tests are located alongside implementation, with filenames ending in `_test.go`.
|
| 8 |
+
- Currently the preference is to use internal tests i.e. test files do not have `_test` package suffix.
|
| 9 |
+
- Tests use [testify](https://github.com/stretchr/testify) for assertions and require statements. Use `require` when continuing the test is not meaningful, for example it is almost never correct to continue after an error expectation.
|
| 10 |
+
- Mocking is performed using [go-github-mock](https://github.com/migueleliasweb/go-github-mock) or `githubv4mock` for simulating GitHub rest and GQL API responses.
|
| 11 |
+
- Each tool's schema is snapshotted and checked for changes using the `toolsnaps` utility (see below).
|
| 12 |
+
- Tests are designed to be explicit and verbose to aid maintainability and clarity.
|
| 13 |
+
- Handler unit tests should take the form of:
|
| 14 |
+
1. Test tool snapshot
|
| 15 |
+
1. Very important expectations against the schema (e.g. `ReadOnly` annotation)
|
| 16 |
+
1. Behavioural tests in table-driven form
|
| 17 |
+
|
| 18 |
+
## End-to-End (e2e) Tests
|
| 19 |
+
|
| 20 |
+
- E2E tests are located in the [`e2e/`](../e2e/) directory. See the [e2e/README.md](../e2e/README.md) for full details on running and debugging these tests.
|
| 21 |
+
|
| 22 |
+
## toolsnaps: Tool Schema Snapshots
|
| 23 |
+
|
| 24 |
+
- The `toolsnaps` utility ensures that the JSON schema for each tool does not change unexpectedly.
|
| 25 |
+
- Snapshots are stored in `__toolsnaps__/*.snap` files, where `*` represents the name of the tool
|
| 26 |
+
- When running tests, the current tool schema is compared to the snapshot. If there is a difference, the test will fail and show a diff.
|
| 27 |
+
- If you intentionally change a tool's schema, update the snapshots by running tests with the environment variable: `UPDATE_TOOLSNAPS=true go test ./...`
|
| 28 |
+
- In CI (when `GITHUB_ACTIONS=true`), missing snapshots will cause a test failure to ensure snapshots are always
|
| 29 |
+
committed.
|
| 30 |
+
|
| 31 |
+
## Notes
|
| 32 |
+
|
| 33 |
+
- Some tools that mutate global state (e.g., marking all notifications as read) are tested primarily with unit tests, not e2e, to avoid side effects.
|
| 34 |
+
- For more on the limitations and philosophy of the e2e suite, see the [e2e/README.md](../e2e/README.md).
|
e2e/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# End To End (e2e) Tests
|
| 2 |
+
|
| 3 |
+
The purpose of the E2E tests is to have a simple (currently) test that gives maintainers some confidence in the black box behavior of our artifacts. It does this by:
|
| 4 |
+
* Building the `github-mcp-server` docker image
|
| 5 |
+
* Running the image
|
| 6 |
+
* Interacting with the server via stdio
|
| 7 |
+
* Issuing requests that interact with the live GitHub API
|
| 8 |
+
|
| 9 |
+
## Running the Tests
|
| 10 |
+
|
| 11 |
+
A service must be running that supports image building and container creation via the `docker` CLI.
|
| 12 |
+
|
| 13 |
+
Since these tests require a token to interact with real resources on the GitHub API, it is gated behind the `e2e` build flag.
|
| 14 |
+
|
| 15 |
+
```
|
| 16 |
+
GITHUB_MCP_SERVER_E2E_TOKEN=<YOUR TOKEN> go test -v --tags e2e ./e2e
|
| 17 |
+
```
|
| 18 |
+
|
| 19 |
+
The `GITHUB_MCP_SERVER_E2E_TOKEN` environment variable is mapped to `GITHUB_PERSONAL_ACCESS_TOKEN` internally, but separated to avoid accidental reuse of credentials.
|
| 20 |
+
|
| 21 |
+
## Example
|
| 22 |
+
|
| 23 |
+
The following diff adjusts the `get_me` tool to return `foobar` as the user login.
|
| 24 |
+
|
| 25 |
+
```diff
|
| 26 |
+
diff --git a/pkg/github/context_tools.go b/pkg/github/context_tools.go
|
| 27 |
+
index 1c91d70..ac4ef2b 100644
|
| 28 |
+
--- a/pkg/github/context_tools.go
|
| 29 |
+
+++ b/pkg/github/context_tools.go
|
| 30 |
+
@@ -39,6 +39,8 @@ func GetMe(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mc
|
| 31 |
+
return mcp.NewToolResultError(fmt.Sprintf("failed to get user: %s", string(body))), nil
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
+ user.Login = sPtr("foobar")
|
| 35 |
+
+
|
| 36 |
+
r, err := json.Marshal(user)
|
| 37 |
+
if err != nil {
|
| 38 |
+
return nil, fmt.Errorf("failed to marshal user: %w", err)
|
| 39 |
+
@@ -47,3 +49,7 @@ func GetMe(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mc
|
| 40 |
+
return mcp.NewToolResultText(string(r)), nil
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
+
|
| 44 |
+
+func sPtr(s string) *string {
|
| 45 |
+
+ return &s
|
| 46 |
+
+}
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
Running the tests:
|
| 50 |
+
|
| 51 |
+
```
|
| 52 |
+
➜ GITHUB_MCP_SERVER_E2E_TOKEN=$(gh auth token) go test -v --tags e2e ./e2e
|
| 53 |
+
=== RUN TestE2E
|
| 54 |
+
e2e_test.go:92: Building Docker image for e2e tests...
|
| 55 |
+
e2e_test.go:36: Starting Stdio MCP client...
|
| 56 |
+
=== RUN TestE2E/Initialize
|
| 57 |
+
=== RUN TestE2E/CallTool_get_me
|
| 58 |
+
e2e_test.go:85:
|
| 59 |
+
Error Trace: /Users/williammartin/workspace/github-mcp-server/e2e/e2e_test.go:85
|
| 60 |
+
Error: Not equal:
|
| 61 |
+
expected: "foobar"
|
| 62 |
+
actual : "williammartin"
|
| 63 |
+
|
| 64 |
+
Diff:
|
| 65 |
+
--- Expected
|
| 66 |
+
+++ Actual
|
| 67 |
+
@@ -1 +1 @@
|
| 68 |
+
-foobar
|
| 69 |
+
+williammartin
|
| 70 |
+
Test: TestE2E/CallTool_get_me
|
| 71 |
+
Messages: expected login to match
|
| 72 |
+
--- FAIL: TestE2E (1.05s)
|
| 73 |
+
--- PASS: TestE2E/Initialize (0.09s)
|
| 74 |
+
--- FAIL: TestE2E/CallTool_get_me (0.46s)
|
| 75 |
+
FAIL
|
| 76 |
+
FAIL github.com/github/github-mcp-server/e2e 1.433s
|
| 77 |
+
FAIL
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
## Debugging the Tests
|
| 81 |
+
|
| 82 |
+
It is possible to provide `GITHUB_MCP_SERVER_E2E_DEBUG=true` to run the e2e tests with an in-process version of the MCP server. This has slightly reduced coverage as it doesn't integrate with Docker, or make use of the cobra/viper configuration parsing. However, it allows for placing breakpoints in the MCP Server internals, supporting much better debugging flows than the fully black-box tests.
|
| 83 |
+
|
| 84 |
+
One might argue that the lack of visibility into failures for the black box tests also indicates a product need, but this solves for the immediate pain point felt as a maintainer.
|
| 85 |
+
|
| 86 |
+
## Limitations
|
| 87 |
+
|
| 88 |
+
The current test suite is intentionally very limited in scope. This is because the maintenance costs on e2e tests tend to increase significantly over time. To read about some challenges with GitHub integration tests, see [go-github integration tests README](https://github.com/google/go-github/blob/5b75aa86dba5cf4af2923afa0938774f37fa0a67/test/README.md). We will expand this suite circumspectly!
|
| 89 |
+
|
| 90 |
+
The tests are quite repetitive and verbose. This is intentional as we want to see them develop more before committing to abstractions.
|
| 91 |
+
|
| 92 |
+
Currently, visibility into failures is not particularly good. We're hoping that we can pull apart the mcp-go client and have it hook into streams representing stdio without requiring an exec. This way we can get breakpoints in the debugger easily.
|
| 93 |
+
|
| 94 |
+
### Global State Mutation Tests
|
| 95 |
+
|
| 96 |
+
Some tools (such as those that mark all notifications as read) would change the global state for the tester, and are also not idempotent, so they offer little value for end to end tests and instead should rely on unit testing and manual verifications.
|
e2e/e2e_test.go
ADDED
|
@@ -0,0 +1,1626 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//go:build e2e
|
| 2 |
+
|
| 3 |
+
package e2e_test
|
| 4 |
+
|
| 5 |
+
import (
|
| 6 |
+
"context"
|
| 7 |
+
"encoding/json"
|
| 8 |
+
"fmt"
|
| 9 |
+
"net/http"
|
| 10 |
+
"os"
|
| 11 |
+
"os/exec"
|
| 12 |
+
"slices"
|
| 13 |
+
"strings"
|
| 14 |
+
"sync"
|
| 15 |
+
"testing"
|
| 16 |
+
"time"
|
| 17 |
+
|
| 18 |
+
"github.com/github/github-mcp-server/internal/ghmcp"
|
| 19 |
+
"github.com/github/github-mcp-server/pkg/github"
|
| 20 |
+
"github.com/github/github-mcp-server/pkg/translations"
|
| 21 |
+
gogithub "github.com/google/go-github/v74/github"
|
| 22 |
+
mcpClient "github.com/mark3labs/mcp-go/client"
|
| 23 |
+
"github.com/mark3labs/mcp-go/mcp"
|
| 24 |
+
"github.com/stretchr/testify/require"
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
var (
|
| 28 |
+
// Shared variables and sync.Once instances to ensure one-time execution
|
| 29 |
+
getTokenOnce sync.Once
|
| 30 |
+
token string
|
| 31 |
+
|
| 32 |
+
getHostOnce sync.Once
|
| 33 |
+
host string
|
| 34 |
+
|
| 35 |
+
buildOnce sync.Once
|
| 36 |
+
buildError error
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
// getE2EToken ensures the environment variable is checked only once and returns the token
|
| 40 |
+
func getE2EToken(t *testing.T) string {
|
| 41 |
+
getTokenOnce.Do(func() {
|
| 42 |
+
token = os.Getenv("GITHUB_MCP_SERVER_E2E_TOKEN")
|
| 43 |
+
if token == "" {
|
| 44 |
+
t.Fatalf("GITHUB_MCP_SERVER_E2E_TOKEN environment variable is not set")
|
| 45 |
+
}
|
| 46 |
+
})
|
| 47 |
+
return token
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
// getE2EHost ensures the environment variable is checked only once and returns the host
|
| 51 |
+
func getE2EHost() string {
|
| 52 |
+
getHostOnce.Do(func() {
|
| 53 |
+
host = os.Getenv("GITHUB_MCP_SERVER_E2E_HOST")
|
| 54 |
+
})
|
| 55 |
+
return host
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
func getRESTClient(t *testing.T) *gogithub.Client {
|
| 59 |
+
// Get token and ensure Docker image is built
|
| 60 |
+
token := getE2EToken(t)
|
| 61 |
+
|
| 62 |
+
// Create a new GitHub client with the token
|
| 63 |
+
ghClient := gogithub.NewClient(nil).WithAuthToken(token)
|
| 64 |
+
|
| 65 |
+
if host := getE2EHost(); host != "" && host != "https://github.com" {
|
| 66 |
+
var err error
|
| 67 |
+
// Currently this works for GHEC because the API is exposed at the api subdomain and the path prefix
|
| 68 |
+
// but it would be preferable to extract the host parsing from the main server logic, and use it here.
|
| 69 |
+
ghClient, err = ghClient.WithEnterpriseURLs(host, host)
|
| 70 |
+
require.NoError(t, err, "expected to create GitHub client with host")
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
return ghClient
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
// ensureDockerImageBuilt makes sure the Docker image is built only once across all tests
|
| 77 |
+
func ensureDockerImageBuilt(t *testing.T) {
|
| 78 |
+
buildOnce.Do(func() {
|
| 79 |
+
t.Log("Building Docker image for e2e tests...")
|
| 80 |
+
cmd := exec.Command("docker", "build", "-t", "github/e2e-github-mcp-server", ".")
|
| 81 |
+
cmd.Dir = ".." // Run this in the context of the root, where the Dockerfile is located.
|
| 82 |
+
output, err := cmd.CombinedOutput()
|
| 83 |
+
buildError = err
|
| 84 |
+
if err != nil {
|
| 85 |
+
t.Logf("Docker build output: %s", string(output))
|
| 86 |
+
}
|
| 87 |
+
})
|
| 88 |
+
|
| 89 |
+
// Check if the build was successful
|
| 90 |
+
require.NoError(t, buildError, "expected to build Docker image successfully")
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
// clientOpts holds configuration options for the MCP client setup
|
| 94 |
+
type clientOpts struct {
|
| 95 |
+
// Toolsets to enable in the MCP server
|
| 96 |
+
enabledToolsets []string
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
// clientOption defines a function type for configuring ClientOpts
|
| 100 |
+
type clientOption func(*clientOpts)
|
| 101 |
+
|
| 102 |
+
// withToolsets returns an option that either sets the GITHUB_TOOLSETS envvar when executing in docker,
|
| 103 |
+
// or sets the toolsets in the MCP server when running in-process.
|
| 104 |
+
func withToolsets(toolsets []string) clientOption {
|
| 105 |
+
return func(opts *clientOpts) {
|
| 106 |
+
opts.enabledToolsets = toolsets
|
| 107 |
+
}
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
func setupMCPClient(t *testing.T, options ...clientOption) *mcpClient.Client {
|
| 111 |
+
// Get token and ensure Docker image is built
|
| 112 |
+
token := getE2EToken(t)
|
| 113 |
+
|
| 114 |
+
// Create and configure options
|
| 115 |
+
opts := &clientOpts{}
|
| 116 |
+
|
| 117 |
+
// Apply all options to configure the opts struct
|
| 118 |
+
for _, option := range options {
|
| 119 |
+
option(opts)
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
// By default, we run the tests including the Docker image, but with DEBUG
|
| 123 |
+
// enabled, we run the server in-process, allowing for easier debugging.
|
| 124 |
+
var client *mcpClient.Client
|
| 125 |
+
if os.Getenv("GITHUB_MCP_SERVER_E2E_DEBUG") == "" {
|
| 126 |
+
ensureDockerImageBuilt(t)
|
| 127 |
+
|
| 128 |
+
// Prepare Docker arguments
|
| 129 |
+
args := []string{
|
| 130 |
+
"docker",
|
| 131 |
+
"run",
|
| 132 |
+
"-i",
|
| 133 |
+
"--rm",
|
| 134 |
+
"-e",
|
| 135 |
+
"GITHUB_PERSONAL_ACCESS_TOKEN", // Personal access token is all required
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
host := getE2EHost()
|
| 139 |
+
if host != "" {
|
| 140 |
+
args = append(args, "-e", "GITHUB_HOST")
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
// Add toolsets environment variable to the Docker arguments
|
| 144 |
+
if len(opts.enabledToolsets) > 0 {
|
| 145 |
+
args = append(args, "-e", "GITHUB_TOOLSETS")
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
// Add the image name
|
| 149 |
+
args = append(args, "github/e2e-github-mcp-server")
|
| 150 |
+
|
| 151 |
+
// Construct the env vars for the MCP Client to execute docker with
|
| 152 |
+
dockerEnvVars := []string{
|
| 153 |
+
fmt.Sprintf("GITHUB_PERSONAL_ACCESS_TOKEN=%s", token),
|
| 154 |
+
fmt.Sprintf("GITHUB_TOOLSETS=%s", strings.Join(opts.enabledToolsets, ",")),
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
if host != "" {
|
| 158 |
+
dockerEnvVars = append(dockerEnvVars, fmt.Sprintf("GITHUB_HOST=%s", host))
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
// Create the client
|
| 162 |
+
t.Log("Starting Stdio MCP client...")
|
| 163 |
+
var err error
|
| 164 |
+
client, err = mcpClient.NewStdioMCPClient(args[0], dockerEnvVars, args[1:]...)
|
| 165 |
+
require.NoError(t, err, "expected to create client successfully")
|
| 166 |
+
} else {
|
| 167 |
+
// We need this because the fully compiled server has a default for the viper config, which is
|
| 168 |
+
// not in scope for using the MCP server directly. This probably indicates that we should refactor
|
| 169 |
+
// so that there is a shared setup mechanism, but let's wait till we feel more friction.
|
| 170 |
+
enabledToolsets := opts.enabledToolsets
|
| 171 |
+
if enabledToolsets == nil {
|
| 172 |
+
enabledToolsets = github.DefaultTools
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
ghServer, err := ghmcp.NewMCPServer(ghmcp.MCPServerConfig{
|
| 176 |
+
Token: token,
|
| 177 |
+
EnabledToolsets: enabledToolsets,
|
| 178 |
+
Host: getE2EHost(),
|
| 179 |
+
Translator: translations.NullTranslationHelper,
|
| 180 |
+
})
|
| 181 |
+
require.NoError(t, err, "expected to construct MCP server successfully")
|
| 182 |
+
|
| 183 |
+
t.Log("Starting In Process MCP client...")
|
| 184 |
+
client, err = mcpClient.NewInProcessClient(ghServer)
|
| 185 |
+
require.NoError(t, err, "expected to create in-process client successfully")
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
t.Cleanup(func() {
|
| 189 |
+
require.NoError(t, client.Close(), "expected to close client successfully")
|
| 190 |
+
})
|
| 191 |
+
|
| 192 |
+
// Initialize the client
|
| 193 |
+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
| 194 |
+
defer cancel()
|
| 195 |
+
|
| 196 |
+
request := mcp.InitializeRequest{}
|
| 197 |
+
request.Params.ProtocolVersion = "2025-03-26"
|
| 198 |
+
request.Params.ClientInfo = mcp.Implementation{
|
| 199 |
+
Name: "e2e-test-client",
|
| 200 |
+
Version: "0.0.1",
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
result, err := client.Initialize(ctx, request)
|
| 204 |
+
require.NoError(t, err, "failed to initialize client")
|
| 205 |
+
require.Equal(t, "github-mcp-server", result.ServerInfo.Name, "unexpected server name")
|
| 206 |
+
|
| 207 |
+
return client
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
func TestGetMe(t *testing.T) {
|
| 211 |
+
t.Parallel()
|
| 212 |
+
|
| 213 |
+
mcpClient := setupMCPClient(t)
|
| 214 |
+
ctx := context.Background()
|
| 215 |
+
|
| 216 |
+
// When we call the "get_me" tool
|
| 217 |
+
request := mcp.CallToolRequest{}
|
| 218 |
+
request.Params.Name = "get_me"
|
| 219 |
+
|
| 220 |
+
response, err := mcpClient.CallTool(ctx, request)
|
| 221 |
+
require.NoError(t, err, "expected to call 'get_me' tool successfully")
|
| 222 |
+
|
| 223 |
+
require.False(t, response.IsError, "expected result not to be an error")
|
| 224 |
+
require.Len(t, response.Content, 1, "expected content to have one item")
|
| 225 |
+
|
| 226 |
+
textContent, ok := response.Content[0].(mcp.TextContent)
|
| 227 |
+
require.True(t, ok, "expected content to be of type TextContent")
|
| 228 |
+
|
| 229 |
+
var trimmedContent struct {
|
| 230 |
+
Login string `json:"login"`
|
| 231 |
+
}
|
| 232 |
+
err = json.Unmarshal([]byte(textContent.Text), &trimmedContent)
|
| 233 |
+
require.NoError(t, err, "expected to unmarshal text content successfully")
|
| 234 |
+
|
| 235 |
+
// Then the login in the response should match the login obtained via the same
|
| 236 |
+
// token using the GitHub API.
|
| 237 |
+
ghClient := getRESTClient(t)
|
| 238 |
+
user, _, err := ghClient.Users.Get(context.Background(), "")
|
| 239 |
+
require.NoError(t, err, "expected to get user successfully")
|
| 240 |
+
require.Equal(t, trimmedContent.Login, *user.Login, "expected login to match")
|
| 241 |
+
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
func TestToolsets(t *testing.T) {
|
| 245 |
+
t.Parallel()
|
| 246 |
+
|
| 247 |
+
mcpClient := setupMCPClient(
|
| 248 |
+
t,
|
| 249 |
+
withToolsets([]string{"repos", "issues"}),
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
ctx := context.Background()
|
| 253 |
+
|
| 254 |
+
request := mcp.ListToolsRequest{}
|
| 255 |
+
response, err := mcpClient.ListTools(ctx, request)
|
| 256 |
+
require.NoError(t, err, "expected to list tools successfully")
|
| 257 |
+
|
| 258 |
+
// We could enumerate the tools here, but we'll need to expose that information
|
| 259 |
+
// declaratively in the MCP server, so for the moment let's just check the existence
|
| 260 |
+
// of an issue and repo tool, and the non-existence of a pull_request tool.
|
| 261 |
+
var toolsContains = func(expectedName string) bool {
|
| 262 |
+
return slices.ContainsFunc(response.Tools, func(tool mcp.Tool) bool {
|
| 263 |
+
return tool.Name == expectedName
|
| 264 |
+
})
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
require.True(t, toolsContains("get_issue"), "expected to find 'get_issue' tool")
|
| 268 |
+
require.True(t, toolsContains("list_branches"), "expected to find 'list_branches' tool")
|
| 269 |
+
require.False(t, toolsContains("get_pull_request"), "expected not to find 'get_pull_request' tool")
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
func TestTags(t *testing.T) {
|
| 273 |
+
t.Parallel()
|
| 274 |
+
|
| 275 |
+
mcpClient := setupMCPClient(t)
|
| 276 |
+
|
| 277 |
+
ctx := context.Background()
|
| 278 |
+
|
| 279 |
+
// First, who am I
|
| 280 |
+
getMeRequest := mcp.CallToolRequest{}
|
| 281 |
+
getMeRequest.Params.Name = "get_me"
|
| 282 |
+
|
| 283 |
+
t.Log("Getting current user...")
|
| 284 |
+
resp, err := mcpClient.CallTool(ctx, getMeRequest)
|
| 285 |
+
require.NoError(t, err, "expected to call 'get_me' tool successfully")
|
| 286 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 287 |
+
|
| 288 |
+
require.False(t, resp.IsError, "expected result not to be an error")
|
| 289 |
+
require.Len(t, resp.Content, 1, "expected content to have one item")
|
| 290 |
+
|
| 291 |
+
textContent, ok := resp.Content[0].(mcp.TextContent)
|
| 292 |
+
require.True(t, ok, "expected content to be of type TextContent")
|
| 293 |
+
|
| 294 |
+
var trimmedGetMeText struct {
|
| 295 |
+
Login string `json:"login"`
|
| 296 |
+
}
|
| 297 |
+
err = json.Unmarshal([]byte(textContent.Text), &trimmedGetMeText)
|
| 298 |
+
require.NoError(t, err, "expected to unmarshal text content successfully")
|
| 299 |
+
|
| 300 |
+
currentOwner := trimmedGetMeText.Login
|
| 301 |
+
|
| 302 |
+
// Then create a repository with a README (via autoInit)
|
| 303 |
+
repoName := fmt.Sprintf("github-mcp-server-e2e-%s-%d", t.Name(), time.Now().UnixMilli())
|
| 304 |
+
createRepoRequest := mcp.CallToolRequest{}
|
| 305 |
+
createRepoRequest.Params.Name = "create_repository"
|
| 306 |
+
createRepoRequest.Params.Arguments = map[string]any{
|
| 307 |
+
"name": repoName,
|
| 308 |
+
"private": true,
|
| 309 |
+
"autoInit": true,
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
t.Logf("Creating repository %s/%s...", currentOwner, repoName)
|
| 313 |
+
_, err = mcpClient.CallTool(ctx, createRepoRequest)
|
| 314 |
+
require.NoError(t, err, "expected to call 'get_me' tool successfully")
|
| 315 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 316 |
+
|
| 317 |
+
// Cleanup the repository after the test
|
| 318 |
+
t.Cleanup(func() {
|
| 319 |
+
// MCP Server doesn't support deletions, but we can use the GitHub Client
|
| 320 |
+
ghClient := getRESTClient(t)
|
| 321 |
+
t.Logf("Deleting repository %s/%s...", currentOwner, repoName)
|
| 322 |
+
_, err := ghClient.Repositories.Delete(context.Background(), currentOwner, repoName)
|
| 323 |
+
require.NoError(t, err, "expected to delete repository successfully")
|
| 324 |
+
})
|
| 325 |
+
|
| 326 |
+
// Then create a tag
|
| 327 |
+
// MCP Server doesn't support tag creation, but we can use the GitHub Client
|
| 328 |
+
ghClient := getRESTClient(t)
|
| 329 |
+
t.Logf("Creating tag %s/%s:%s...", currentOwner, repoName, "v0.0.1")
|
| 330 |
+
ref, _, err := ghClient.Git.GetRef(context.Background(), currentOwner, repoName, "refs/heads/main")
|
| 331 |
+
require.NoError(t, err, "expected to get ref successfully")
|
| 332 |
+
|
| 333 |
+
tagObj, _, err := ghClient.Git.CreateTag(context.Background(), currentOwner, repoName, &gogithub.Tag{
|
| 334 |
+
Tag: gogithub.Ptr("v0.0.1"),
|
| 335 |
+
Message: gogithub.Ptr("v0.0.1"),
|
| 336 |
+
Object: &gogithub.GitObject{
|
| 337 |
+
SHA: ref.Object.SHA,
|
| 338 |
+
Type: gogithub.Ptr("commit"),
|
| 339 |
+
},
|
| 340 |
+
})
|
| 341 |
+
require.NoError(t, err, "expected to create tag object successfully")
|
| 342 |
+
|
| 343 |
+
_, _, err = ghClient.Git.CreateRef(context.Background(), currentOwner, repoName, &gogithub.Reference{
|
| 344 |
+
Ref: gogithub.Ptr("refs/tags/v0.0.1"),
|
| 345 |
+
Object: &gogithub.GitObject{
|
| 346 |
+
SHA: tagObj.SHA,
|
| 347 |
+
},
|
| 348 |
+
})
|
| 349 |
+
require.NoError(t, err, "expected to create tag ref successfully")
|
| 350 |
+
|
| 351 |
+
// List the tags
|
| 352 |
+
listTagsRequest := mcp.CallToolRequest{}
|
| 353 |
+
listTagsRequest.Params.Name = "list_tags"
|
| 354 |
+
listTagsRequest.Params.Arguments = map[string]any{
|
| 355 |
+
"owner": currentOwner,
|
| 356 |
+
"repo": repoName,
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
t.Logf("Listing tags for %s/%s...", currentOwner, repoName)
|
| 360 |
+
resp, err = mcpClient.CallTool(ctx, listTagsRequest)
|
| 361 |
+
require.NoError(t, err, "expected to call 'list_tags' tool successfully")
|
| 362 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 363 |
+
|
| 364 |
+
require.False(t, resp.IsError, "expected result not to be an error")
|
| 365 |
+
require.Len(t, resp.Content, 1, "expected content to have one item")
|
| 366 |
+
|
| 367 |
+
textContent, ok = resp.Content[0].(mcp.TextContent)
|
| 368 |
+
require.True(t, ok, "expected content to be of type TextContent")
|
| 369 |
+
|
| 370 |
+
var trimmedTags []struct {
|
| 371 |
+
Name string `json:"name"`
|
| 372 |
+
Commit struct {
|
| 373 |
+
SHA string `json:"sha"`
|
| 374 |
+
} `json:"commit"`
|
| 375 |
+
}
|
| 376 |
+
err = json.Unmarshal([]byte(textContent.Text), &trimmedTags)
|
| 377 |
+
require.NoError(t, err, "expected to unmarshal text content successfully")
|
| 378 |
+
|
| 379 |
+
require.Len(t, trimmedTags, 1, "expected to find one tag")
|
| 380 |
+
require.Equal(t, "v0.0.1", trimmedTags[0].Name, "expected tag name to match")
|
| 381 |
+
require.Equal(t, *ref.Object.SHA, trimmedTags[0].Commit.SHA, "expected tag SHA to match")
|
| 382 |
+
|
| 383 |
+
// And fetch an individual tag
|
| 384 |
+
getTagRequest := mcp.CallToolRequest{}
|
| 385 |
+
getTagRequest.Params.Name = "get_tag"
|
| 386 |
+
getTagRequest.Params.Arguments = map[string]any{
|
| 387 |
+
"owner": currentOwner,
|
| 388 |
+
"repo": repoName,
|
| 389 |
+
"tag": "v0.0.1",
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
t.Logf("Getting tag %s/%s:%s...", currentOwner, repoName, "v0.0.1")
|
| 393 |
+
resp, err = mcpClient.CallTool(ctx, getTagRequest)
|
| 394 |
+
require.NoError(t, err, "expected to call 'get_tag' tool successfully")
|
| 395 |
+
require.False(t, resp.IsError, "expected result not to be an error")
|
| 396 |
+
|
| 397 |
+
var trimmedTag []struct { // don't understand why this is an array
|
| 398 |
+
Name string `json:"name"`
|
| 399 |
+
Commit struct {
|
| 400 |
+
SHA string `json:"sha"`
|
| 401 |
+
} `json:"commit"`
|
| 402 |
+
}
|
| 403 |
+
err = json.Unmarshal([]byte(textContent.Text), &trimmedTag)
|
| 404 |
+
require.NoError(t, err, "expected to unmarshal text content successfully")
|
| 405 |
+
require.Len(t, trimmedTag, 1, "expected to find one tag")
|
| 406 |
+
require.Equal(t, "v0.0.1", trimmedTag[0].Name, "expected tag name to match")
|
| 407 |
+
require.Equal(t, *ref.Object.SHA, trimmedTag[0].Commit.SHA, "expected tag SHA to match")
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
func TestFileDeletion(t *testing.T) {
|
| 411 |
+
t.Parallel()
|
| 412 |
+
|
| 413 |
+
mcpClient := setupMCPClient(t)
|
| 414 |
+
|
| 415 |
+
ctx := context.Background()
|
| 416 |
+
|
| 417 |
+
// First, who am I
|
| 418 |
+
getMeRequest := mcp.CallToolRequest{}
|
| 419 |
+
getMeRequest.Params.Name = "get_me"
|
| 420 |
+
|
| 421 |
+
t.Log("Getting current user...")
|
| 422 |
+
resp, err := mcpClient.CallTool(ctx, getMeRequest)
|
| 423 |
+
require.NoError(t, err, "expected to call 'get_me' tool successfully")
|
| 424 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 425 |
+
|
| 426 |
+
require.False(t, resp.IsError, "expected result not to be an error")
|
| 427 |
+
require.Len(t, resp.Content, 1, "expected content to have one item")
|
| 428 |
+
|
| 429 |
+
textContent, ok := resp.Content[0].(mcp.TextContent)
|
| 430 |
+
require.True(t, ok, "expected content to be of type TextContent")
|
| 431 |
+
|
| 432 |
+
var trimmedGetMeText struct {
|
| 433 |
+
Login string `json:"login"`
|
| 434 |
+
}
|
| 435 |
+
err = json.Unmarshal([]byte(textContent.Text), &trimmedGetMeText)
|
| 436 |
+
require.NoError(t, err, "expected to unmarshal text content successfully")
|
| 437 |
+
|
| 438 |
+
currentOwner := trimmedGetMeText.Login
|
| 439 |
+
|
| 440 |
+
// Then create a repository with a README (via autoInit)
|
| 441 |
+
repoName := fmt.Sprintf("github-mcp-server-e2e-%s-%d", t.Name(), time.Now().UnixMilli())
|
| 442 |
+
createRepoRequest := mcp.CallToolRequest{}
|
| 443 |
+
createRepoRequest.Params.Name = "create_repository"
|
| 444 |
+
createRepoRequest.Params.Arguments = map[string]any{
|
| 445 |
+
"name": repoName,
|
| 446 |
+
"private": true,
|
| 447 |
+
"autoInit": true,
|
| 448 |
+
}
|
| 449 |
+
t.Logf("Creating repository %s/%s...", currentOwner, repoName)
|
| 450 |
+
_, err = mcpClient.CallTool(ctx, createRepoRequest)
|
| 451 |
+
require.NoError(t, err, "expected to call 'get_me' tool successfully")
|
| 452 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 453 |
+
|
| 454 |
+
// Cleanup the repository after the test
|
| 455 |
+
t.Cleanup(func() {
|
| 456 |
+
// MCP Server doesn't support deletions, but we can use the GitHub Client
|
| 457 |
+
ghClient := getRESTClient(t)
|
| 458 |
+
t.Logf("Deleting repository %s/%s...", currentOwner, repoName)
|
| 459 |
+
_, err := ghClient.Repositories.Delete(context.Background(), currentOwner, repoName)
|
| 460 |
+
require.NoError(t, err, "expected to delete repository successfully")
|
| 461 |
+
})
|
| 462 |
+
|
| 463 |
+
// Create a branch on which to create a new commit
|
| 464 |
+
createBranchRequest := mcp.CallToolRequest{}
|
| 465 |
+
createBranchRequest.Params.Name = "create_branch"
|
| 466 |
+
createBranchRequest.Params.Arguments = map[string]any{
|
| 467 |
+
"owner": currentOwner,
|
| 468 |
+
"repo": repoName,
|
| 469 |
+
"branch": "test-branch",
|
| 470 |
+
"from_branch": "main",
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
t.Logf("Creating branch in %s/%s...", currentOwner, repoName)
|
| 474 |
+
resp, err = mcpClient.CallTool(ctx, createBranchRequest)
|
| 475 |
+
require.NoError(t, err, "expected to call 'create_branch' tool successfully")
|
| 476 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 477 |
+
|
| 478 |
+
// Create a commit with a new file
|
| 479 |
+
commitRequest := mcp.CallToolRequest{}
|
| 480 |
+
commitRequest.Params.Name = "create_or_update_file"
|
| 481 |
+
commitRequest.Params.Arguments = map[string]any{
|
| 482 |
+
"owner": currentOwner,
|
| 483 |
+
"repo": repoName,
|
| 484 |
+
"path": "test-file.txt",
|
| 485 |
+
"content": fmt.Sprintf("Created by e2e test %s", t.Name()),
|
| 486 |
+
"message": "Add test file",
|
| 487 |
+
"branch": "test-branch",
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
t.Logf("Creating commit with new file in %s/%s...", currentOwner, repoName)
|
| 491 |
+
resp, err = mcpClient.CallTool(ctx, commitRequest)
|
| 492 |
+
require.NoError(t, err, "expected to call 'create_or_update_file' tool successfully")
|
| 493 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 494 |
+
|
| 495 |
+
// Check the file exists
|
| 496 |
+
getFileContentsRequest := mcp.CallToolRequest{}
|
| 497 |
+
getFileContentsRequest.Params.Name = "get_file_contents"
|
| 498 |
+
getFileContentsRequest.Params.Arguments = map[string]any{
|
| 499 |
+
"owner": currentOwner,
|
| 500 |
+
"repo": repoName,
|
| 501 |
+
"path": "test-file.txt",
|
| 502 |
+
"branch": "test-branch",
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
t.Logf("Getting file contents in %s/%s...", currentOwner, repoName)
|
| 506 |
+
resp, err = mcpClient.CallTool(ctx, getFileContentsRequest)
|
| 507 |
+
require.NoError(t, err, "expected to call 'get_file_contents' tool successfully")
|
| 508 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 509 |
+
|
| 510 |
+
embeddedResource, ok := resp.Content[1].(mcp.EmbeddedResource)
|
| 511 |
+
require.True(t, ok, "expected content to be of type EmbeddedResource")
|
| 512 |
+
|
| 513 |
+
// raw api
|
| 514 |
+
textResource, ok := embeddedResource.Resource.(mcp.TextResourceContents)
|
| 515 |
+
require.True(t, ok, "expected embedded resource to be of type TextResourceContents")
|
| 516 |
+
|
| 517 |
+
require.Equal(t, fmt.Sprintf("Created by e2e test %s", t.Name()), textResource.Text, "expected file content to match")
|
| 518 |
+
|
| 519 |
+
// Delete the file
|
| 520 |
+
deleteFileRequest := mcp.CallToolRequest{}
|
| 521 |
+
deleteFileRequest.Params.Name = "delete_file"
|
| 522 |
+
deleteFileRequest.Params.Arguments = map[string]any{
|
| 523 |
+
"owner": currentOwner,
|
| 524 |
+
"repo": repoName,
|
| 525 |
+
"path": "test-file.txt",
|
| 526 |
+
"message": "Delete test file",
|
| 527 |
+
"branch": "test-branch",
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
t.Logf("Deleting file in %s/%s...", currentOwner, repoName)
|
| 531 |
+
resp, err = mcpClient.CallTool(ctx, deleteFileRequest)
|
| 532 |
+
require.NoError(t, err, "expected to call 'delete_file' tool successfully")
|
| 533 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 534 |
+
|
| 535 |
+
// See that there is a commit that removes the file
|
| 536 |
+
listCommitsRequest := mcp.CallToolRequest{}
|
| 537 |
+
listCommitsRequest.Params.Name = "list_commits"
|
| 538 |
+
listCommitsRequest.Params.Arguments = map[string]any{
|
| 539 |
+
"owner": currentOwner,
|
| 540 |
+
"repo": repoName,
|
| 541 |
+
"sha": "test-branch", // can be SHA or branch, which is an unfortunate API design
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
t.Logf("Listing commits in %s/%s...", currentOwner, repoName)
|
| 545 |
+
resp, err = mcpClient.CallTool(ctx, listCommitsRequest)
|
| 546 |
+
require.NoError(t, err, "expected to call 'list_commits' tool successfully")
|
| 547 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 548 |
+
|
| 549 |
+
textContent, ok = resp.Content[0].(mcp.TextContent)
|
| 550 |
+
require.True(t, ok, "expected content to be of type TextContent")
|
| 551 |
+
|
| 552 |
+
var trimmedListCommitsText []struct {
|
| 553 |
+
SHA string `json:"sha"`
|
| 554 |
+
Commit struct {
|
| 555 |
+
Message string `json:"message"`
|
| 556 |
+
}
|
| 557 |
+
Files []struct {
|
| 558 |
+
Filename string `json:"filename"`
|
| 559 |
+
Deletions int `json:"deletions"`
|
| 560 |
+
}
|
| 561 |
+
}
|
| 562 |
+
err = json.Unmarshal([]byte(textContent.Text), &trimmedListCommitsText)
|
| 563 |
+
require.NoError(t, err, "expected to unmarshal text content successfully")
|
| 564 |
+
require.GreaterOrEqual(t, len(trimmedListCommitsText), 1, "expected to find at least one commit")
|
| 565 |
+
|
| 566 |
+
deletionCommit := trimmedListCommitsText[0]
|
| 567 |
+
require.Equal(t, "Delete test file", deletionCommit.Commit.Message, "expected commit message to match")
|
| 568 |
+
|
| 569 |
+
// Now get the commit so we can look at the file changes because list_commits doesn't include them
|
| 570 |
+
getCommitRequest := mcp.CallToolRequest{}
|
| 571 |
+
getCommitRequest.Params.Name = "get_commit"
|
| 572 |
+
getCommitRequest.Params.Arguments = map[string]any{
|
| 573 |
+
"owner": currentOwner,
|
| 574 |
+
"repo": repoName,
|
| 575 |
+
"sha": deletionCommit.SHA,
|
| 576 |
+
}
|
| 577 |
+
|
| 578 |
+
t.Logf("Getting commit %s/%s:%s...", currentOwner, repoName, deletionCommit.SHA)
|
| 579 |
+
resp, err = mcpClient.CallTool(ctx, getCommitRequest)
|
| 580 |
+
require.NoError(t, err, "expected to call 'get_commit' tool successfully")
|
| 581 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 582 |
+
|
| 583 |
+
textContent, ok = resp.Content[0].(mcp.TextContent)
|
| 584 |
+
require.True(t, ok, "expected content to be of type TextContent")
|
| 585 |
+
|
| 586 |
+
var trimmedGetCommitText struct {
|
| 587 |
+
Files []struct {
|
| 588 |
+
Filename string `json:"filename"`
|
| 589 |
+
Deletions int `json:"deletions"`
|
| 590 |
+
}
|
| 591 |
+
}
|
| 592 |
+
err = json.Unmarshal([]byte(textContent.Text), &trimmedGetCommitText)
|
| 593 |
+
require.NoError(t, err, "expected to unmarshal text content successfully")
|
| 594 |
+
require.Len(t, trimmedGetCommitText.Files, 1, "expected to find one file change")
|
| 595 |
+
require.Equal(t, "test-file.txt", trimmedGetCommitText.Files[0].Filename, "expected filename to match")
|
| 596 |
+
require.Equal(t, 1, trimmedGetCommitText.Files[0].Deletions, "expected one deletion")
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
func TestDirectoryDeletion(t *testing.T) {
|
| 600 |
+
t.Parallel()
|
| 601 |
+
|
| 602 |
+
mcpClient := setupMCPClient(t)
|
| 603 |
+
|
| 604 |
+
ctx := context.Background()
|
| 605 |
+
|
| 606 |
+
// First, who am I
|
| 607 |
+
getMeRequest := mcp.CallToolRequest{}
|
| 608 |
+
getMeRequest.Params.Name = "get_me"
|
| 609 |
+
|
| 610 |
+
t.Log("Getting current user...")
|
| 611 |
+
resp, err := mcpClient.CallTool(ctx, getMeRequest)
|
| 612 |
+
require.NoError(t, err, "expected to call 'get_me' tool successfully")
|
| 613 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 614 |
+
|
| 615 |
+
require.False(t, resp.IsError, "expected result not to be an error")
|
| 616 |
+
require.Len(t, resp.Content, 1, "expected content to have one item")
|
| 617 |
+
|
| 618 |
+
textContent, ok := resp.Content[0].(mcp.TextContent)
|
| 619 |
+
require.True(t, ok, "expected content to be of type TextContent")
|
| 620 |
+
|
| 621 |
+
var trimmedGetMeText struct {
|
| 622 |
+
Login string `json:"login"`
|
| 623 |
+
}
|
| 624 |
+
err = json.Unmarshal([]byte(textContent.Text), &trimmedGetMeText)
|
| 625 |
+
require.NoError(t, err, "expected to unmarshal text content successfully")
|
| 626 |
+
|
| 627 |
+
currentOwner := trimmedGetMeText.Login
|
| 628 |
+
|
| 629 |
+
// Then create a repository with a README (via autoInit)
|
| 630 |
+
repoName := fmt.Sprintf("github-mcp-server-e2e-%s-%d", t.Name(), time.Now().UnixMilli())
|
| 631 |
+
createRepoRequest := mcp.CallToolRequest{}
|
| 632 |
+
createRepoRequest.Params.Name = "create_repository"
|
| 633 |
+
createRepoRequest.Params.Arguments = map[string]any{
|
| 634 |
+
"name": repoName,
|
| 635 |
+
"private": true,
|
| 636 |
+
"autoInit": true,
|
| 637 |
+
}
|
| 638 |
+
t.Logf("Creating repository %s/%s...", currentOwner, repoName)
|
| 639 |
+
_, err = mcpClient.CallTool(ctx, createRepoRequest)
|
| 640 |
+
require.NoError(t, err, "expected to call 'get_me' tool successfully")
|
| 641 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 642 |
+
|
| 643 |
+
// Cleanup the repository after the test
|
| 644 |
+
t.Cleanup(func() {
|
| 645 |
+
// MCP Server doesn't support deletions, but we can use the GitHub Client
|
| 646 |
+
ghClient := getRESTClient(t)
|
| 647 |
+
t.Logf("Deleting repository %s/%s...", currentOwner, repoName)
|
| 648 |
+
_, err := ghClient.Repositories.Delete(context.Background(), currentOwner, repoName)
|
| 649 |
+
require.NoError(t, err, "expected to delete repository successfully")
|
| 650 |
+
})
|
| 651 |
+
|
| 652 |
+
// Create a branch on which to create a new commit
|
| 653 |
+
createBranchRequest := mcp.CallToolRequest{}
|
| 654 |
+
createBranchRequest.Params.Name = "create_branch"
|
| 655 |
+
createBranchRequest.Params.Arguments = map[string]any{
|
| 656 |
+
"owner": currentOwner,
|
| 657 |
+
"repo": repoName,
|
| 658 |
+
"branch": "test-branch",
|
| 659 |
+
"from_branch": "main",
|
| 660 |
+
}
|
| 661 |
+
|
| 662 |
+
t.Logf("Creating branch in %s/%s...", currentOwner, repoName)
|
| 663 |
+
resp, err = mcpClient.CallTool(ctx, createBranchRequest)
|
| 664 |
+
require.NoError(t, err, "expected to call 'create_branch' tool successfully")
|
| 665 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 666 |
+
|
| 667 |
+
// Create a commit with a new file
|
| 668 |
+
commitRequest := mcp.CallToolRequest{}
|
| 669 |
+
commitRequest.Params.Name = "create_or_update_file"
|
| 670 |
+
commitRequest.Params.Arguments = map[string]any{
|
| 671 |
+
"owner": currentOwner,
|
| 672 |
+
"repo": repoName,
|
| 673 |
+
"path": "test-dir/test-file.txt",
|
| 674 |
+
"content": fmt.Sprintf("Created by e2e test %s", t.Name()),
|
| 675 |
+
"message": "Add test file",
|
| 676 |
+
"branch": "test-branch",
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
t.Logf("Creating commit with new file in %s/%s...", currentOwner, repoName)
|
| 680 |
+
resp, err = mcpClient.CallTool(ctx, commitRequest)
|
| 681 |
+
require.NoError(t, err, "expected to call 'create_or_update_file' tool successfully")
|
| 682 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 683 |
+
|
| 684 |
+
textContent, ok = resp.Content[0].(mcp.TextContent)
|
| 685 |
+
require.True(t, ok, "expected content to be of type TextContent")
|
| 686 |
+
|
| 687 |
+
// Check the file exists
|
| 688 |
+
getFileContentsRequest := mcp.CallToolRequest{}
|
| 689 |
+
getFileContentsRequest.Params.Name = "get_file_contents"
|
| 690 |
+
getFileContentsRequest.Params.Arguments = map[string]any{
|
| 691 |
+
"owner": currentOwner,
|
| 692 |
+
"repo": repoName,
|
| 693 |
+
"path": "test-dir/test-file.txt",
|
| 694 |
+
"branch": "test-branch",
|
| 695 |
+
}
|
| 696 |
+
|
| 697 |
+
t.Logf("Getting file contents in %s/%s...", currentOwner, repoName)
|
| 698 |
+
resp, err = mcpClient.CallTool(ctx, getFileContentsRequest)
|
| 699 |
+
require.NoError(t, err, "expected to call 'get_file_contents' tool successfully")
|
| 700 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 701 |
+
|
| 702 |
+
embeddedResource, ok := resp.Content[1].(mcp.EmbeddedResource)
|
| 703 |
+
require.True(t, ok, "expected content to be of type EmbeddedResource")
|
| 704 |
+
|
| 705 |
+
// raw api
|
| 706 |
+
textResource, ok := embeddedResource.Resource.(mcp.TextResourceContents)
|
| 707 |
+
require.True(t, ok, "expected embedded resource to be of type TextResourceContents")
|
| 708 |
+
|
| 709 |
+
require.Equal(t, fmt.Sprintf("Created by e2e test %s", t.Name()), textResource.Text, "expected file content to match")
|
| 710 |
+
|
| 711 |
+
// Delete the directory containing the file
|
| 712 |
+
deleteFileRequest := mcp.CallToolRequest{}
|
| 713 |
+
deleteFileRequest.Params.Name = "delete_file"
|
| 714 |
+
deleteFileRequest.Params.Arguments = map[string]any{
|
| 715 |
+
"owner": currentOwner,
|
| 716 |
+
"repo": repoName,
|
| 717 |
+
"path": "test-dir",
|
| 718 |
+
"message": "Delete test directory",
|
| 719 |
+
"branch": "test-branch",
|
| 720 |
+
}
|
| 721 |
+
|
| 722 |
+
t.Logf("Deleting directory in %s/%s...", currentOwner, repoName)
|
| 723 |
+
resp, err = mcpClient.CallTool(ctx, deleteFileRequest)
|
| 724 |
+
require.NoError(t, err, "expected to call 'delete_file' tool successfully")
|
| 725 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 726 |
+
|
| 727 |
+
// See that there is a commit that removes the directory
|
| 728 |
+
listCommitsRequest := mcp.CallToolRequest{}
|
| 729 |
+
listCommitsRequest.Params.Name = "list_commits"
|
| 730 |
+
listCommitsRequest.Params.Arguments = map[string]any{
|
| 731 |
+
"owner": currentOwner,
|
| 732 |
+
"repo": repoName,
|
| 733 |
+
"sha": "test-branch", // can be SHA or branch, which is an unfortunate API design
|
| 734 |
+
}
|
| 735 |
+
|
| 736 |
+
t.Logf("Listing commits in %s/%s...", currentOwner, repoName)
|
| 737 |
+
resp, err = mcpClient.CallTool(ctx, listCommitsRequest)
|
| 738 |
+
require.NoError(t, err, "expected to call 'list_commits' tool successfully")
|
| 739 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 740 |
+
|
| 741 |
+
textContent, ok = resp.Content[0].(mcp.TextContent)
|
| 742 |
+
require.True(t, ok, "expected content to be of type TextContent")
|
| 743 |
+
|
| 744 |
+
var trimmedListCommitsText []struct {
|
| 745 |
+
SHA string `json:"sha"`
|
| 746 |
+
Commit struct {
|
| 747 |
+
Message string `json:"message"`
|
| 748 |
+
}
|
| 749 |
+
Files []struct {
|
| 750 |
+
Filename string `json:"filename"`
|
| 751 |
+
Deletions int `json:"deletions"`
|
| 752 |
+
} `json:"files"`
|
| 753 |
+
}
|
| 754 |
+
err = json.Unmarshal([]byte(textContent.Text), &trimmedListCommitsText)
|
| 755 |
+
require.NoError(t, err, "expected to unmarshal text content successfully")
|
| 756 |
+
require.GreaterOrEqual(t, len(trimmedListCommitsText), 1, "expected to find at least one commit")
|
| 757 |
+
|
| 758 |
+
deletionCommit := trimmedListCommitsText[0]
|
| 759 |
+
require.Equal(t, "Delete test directory", deletionCommit.Commit.Message, "expected commit message to match")
|
| 760 |
+
|
| 761 |
+
// Now get the commit so we can look at the file changes because list_commits doesn't include them
|
| 762 |
+
getCommitRequest := mcp.CallToolRequest{}
|
| 763 |
+
getCommitRequest.Params.Name = "get_commit"
|
| 764 |
+
getCommitRequest.Params.Arguments = map[string]any{
|
| 765 |
+
"owner": currentOwner,
|
| 766 |
+
"repo": repoName,
|
| 767 |
+
"sha": deletionCommit.SHA,
|
| 768 |
+
}
|
| 769 |
+
|
| 770 |
+
t.Logf("Getting commit %s/%s:%s...", currentOwner, repoName, deletionCommit.SHA)
|
| 771 |
+
resp, err = mcpClient.CallTool(ctx, getCommitRequest)
|
| 772 |
+
require.NoError(t, err, "expected to call 'get_commit' tool successfully")
|
| 773 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 774 |
+
|
| 775 |
+
textContent, ok = resp.Content[0].(mcp.TextContent)
|
| 776 |
+
require.True(t, ok, "expected content to be of type TextContent")
|
| 777 |
+
|
| 778 |
+
var trimmedGetCommitText struct {
|
| 779 |
+
Files []struct {
|
| 780 |
+
Filename string `json:"filename"`
|
| 781 |
+
Deletions int `json:"deletions"`
|
| 782 |
+
}
|
| 783 |
+
}
|
| 784 |
+
err = json.Unmarshal([]byte(textContent.Text), &trimmedGetCommitText)
|
| 785 |
+
require.NoError(t, err, "expected to unmarshal text content successfully")
|
| 786 |
+
require.Len(t, trimmedGetCommitText.Files, 1, "expected to find one file change")
|
| 787 |
+
require.Equal(t, "test-dir/test-file.txt", trimmedGetCommitText.Files[0].Filename, "expected filename to match")
|
| 788 |
+
require.Equal(t, 1, trimmedGetCommitText.Files[0].Deletions, "expected one deletion")
|
| 789 |
+
}
|
| 790 |
+
|
| 791 |
+
func TestRequestCopilotReview(t *testing.T) {
|
| 792 |
+
t.Parallel()
|
| 793 |
+
|
| 794 |
+
if getE2EHost() != "" && getE2EHost() != "https://github.com" {
|
| 795 |
+
t.Skip("Skipping test because the host does not support copilot reviews")
|
| 796 |
+
}
|
| 797 |
+
|
| 798 |
+
mcpClient := setupMCPClient(t)
|
| 799 |
+
ctx := context.Background()
|
| 800 |
+
|
| 801 |
+
// First, who am I
|
| 802 |
+
getMeRequest := mcp.CallToolRequest{}
|
| 803 |
+
getMeRequest.Params.Name = "get_me"
|
| 804 |
+
|
| 805 |
+
t.Log("Getting current user...")
|
| 806 |
+
resp, err := mcpClient.CallTool(ctx, getMeRequest)
|
| 807 |
+
require.NoError(t, err, "expected to call 'get_me' tool successfully")
|
| 808 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 809 |
+
|
| 810 |
+
require.False(t, resp.IsError, "expected result not to be an error")
|
| 811 |
+
require.Len(t, resp.Content, 1, "expected content to have one item")
|
| 812 |
+
|
| 813 |
+
textContent, ok := resp.Content[0].(mcp.TextContent)
|
| 814 |
+
require.True(t, ok, "expected content to be of type TextContent")
|
| 815 |
+
|
| 816 |
+
var trimmedGetMeText struct {
|
| 817 |
+
Login string `json:"login"`
|
| 818 |
+
}
|
| 819 |
+
err = json.Unmarshal([]byte(textContent.Text), &trimmedGetMeText)
|
| 820 |
+
require.NoError(t, err, "expected to unmarshal text content successfully")
|
| 821 |
+
|
| 822 |
+
currentOwner := trimmedGetMeText.Login
|
| 823 |
+
|
| 824 |
+
// Then create a repository with a README (via autoInit)
|
| 825 |
+
repoName := fmt.Sprintf("github-mcp-server-e2e-%s-%d", t.Name(), time.Now().UnixMilli())
|
| 826 |
+
createRepoRequest := mcp.CallToolRequest{}
|
| 827 |
+
createRepoRequest.Params.Name = "create_repository"
|
| 828 |
+
createRepoRequest.Params.Arguments = map[string]any{
|
| 829 |
+
"name": repoName,
|
| 830 |
+
"private": true,
|
| 831 |
+
"autoInit": true,
|
| 832 |
+
}
|
| 833 |
+
|
| 834 |
+
t.Logf("Creating repository %s/%s...", currentOwner, repoName)
|
| 835 |
+
_, err = mcpClient.CallTool(ctx, createRepoRequest)
|
| 836 |
+
require.NoError(t, err, "expected to call 'create_repository' tool successfully")
|
| 837 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 838 |
+
|
| 839 |
+
// Cleanup the repository after the test
|
| 840 |
+
t.Cleanup(func() {
|
| 841 |
+
// MCP Server doesn't support deletions, but we can use the GitHub Client
|
| 842 |
+
ghClient := gogithub.NewClient(nil).WithAuthToken(getE2EToken(t))
|
| 843 |
+
t.Logf("Deleting repository %s/%s...", currentOwner, repoName)
|
| 844 |
+
_, err := ghClient.Repositories.Delete(context.Background(), currentOwner, repoName)
|
| 845 |
+
require.NoError(t, err, "expected to delete repository successfully")
|
| 846 |
+
})
|
| 847 |
+
|
| 848 |
+
// Create a branch on which to create a new commit
|
| 849 |
+
createBranchRequest := mcp.CallToolRequest{}
|
| 850 |
+
createBranchRequest.Params.Name = "create_branch"
|
| 851 |
+
createBranchRequest.Params.Arguments = map[string]any{
|
| 852 |
+
"owner": currentOwner,
|
| 853 |
+
"repo": repoName,
|
| 854 |
+
"branch": "test-branch",
|
| 855 |
+
"from_branch": "main",
|
| 856 |
+
}
|
| 857 |
+
|
| 858 |
+
t.Logf("Creating branch in %s/%s...", currentOwner, repoName)
|
| 859 |
+
resp, err = mcpClient.CallTool(ctx, createBranchRequest)
|
| 860 |
+
require.NoError(t, err, "expected to call 'create_branch' tool successfully")
|
| 861 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 862 |
+
|
| 863 |
+
// Create a commit with a new file
|
| 864 |
+
commitRequest := mcp.CallToolRequest{}
|
| 865 |
+
commitRequest.Params.Name = "create_or_update_file"
|
| 866 |
+
commitRequest.Params.Arguments = map[string]any{
|
| 867 |
+
"owner": currentOwner,
|
| 868 |
+
"repo": repoName,
|
| 869 |
+
"path": "test-file.txt",
|
| 870 |
+
"content": fmt.Sprintf("Created by e2e test %s", t.Name()),
|
| 871 |
+
"message": "Add test file",
|
| 872 |
+
"branch": "test-branch",
|
| 873 |
+
}
|
| 874 |
+
|
| 875 |
+
t.Logf("Creating commit with new file in %s/%s...", currentOwner, repoName)
|
| 876 |
+
resp, err = mcpClient.CallTool(ctx, commitRequest)
|
| 877 |
+
require.NoError(t, err, "expected to call 'create_or_update_file' tool successfully")
|
| 878 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 879 |
+
|
| 880 |
+
textContent, ok = resp.Content[0].(mcp.TextContent)
|
| 881 |
+
require.True(t, ok, "expected content to be of type TextContent")
|
| 882 |
+
|
| 883 |
+
var trimmedCommitText struct {
|
| 884 |
+
SHA string `json:"sha"`
|
| 885 |
+
}
|
| 886 |
+
err = json.Unmarshal([]byte(textContent.Text), &trimmedCommitText)
|
| 887 |
+
require.NoError(t, err, "expected to unmarshal text content successfully")
|
| 888 |
+
commitId := trimmedCommitText.SHA
|
| 889 |
+
|
| 890 |
+
// Create a pull request
|
| 891 |
+
prRequest := mcp.CallToolRequest{}
|
| 892 |
+
prRequest.Params.Name = "create_pull_request"
|
| 893 |
+
prRequest.Params.Arguments = map[string]any{
|
| 894 |
+
"owner": currentOwner,
|
| 895 |
+
"repo": repoName,
|
| 896 |
+
"title": "Test PR",
|
| 897 |
+
"body": "This is a test PR",
|
| 898 |
+
"head": "test-branch",
|
| 899 |
+
"base": "main",
|
| 900 |
+
"commitId": commitId,
|
| 901 |
+
}
|
| 902 |
+
|
| 903 |
+
t.Logf("Creating pull request in %s/%s...", currentOwner, repoName)
|
| 904 |
+
resp, err = mcpClient.CallTool(ctx, prRequest)
|
| 905 |
+
require.NoError(t, err, "expected to call 'create_pull_request' tool successfully")
|
| 906 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 907 |
+
|
| 908 |
+
// Request a copilot review
|
| 909 |
+
requestCopilotReviewRequest := mcp.CallToolRequest{}
|
| 910 |
+
requestCopilotReviewRequest.Params.Name = "request_copilot_review"
|
| 911 |
+
requestCopilotReviewRequest.Params.Arguments = map[string]any{
|
| 912 |
+
"owner": currentOwner,
|
| 913 |
+
"repo": repoName,
|
| 914 |
+
"pullNumber": 1,
|
| 915 |
+
}
|
| 916 |
+
|
| 917 |
+
t.Logf("Requesting Copilot review for pull request in %s/%s...", currentOwner, repoName)
|
| 918 |
+
resp, err = mcpClient.CallTool(ctx, requestCopilotReviewRequest)
|
| 919 |
+
require.NoError(t, err, "expected to call 'request_copilot_review' tool successfully")
|
| 920 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 921 |
+
|
| 922 |
+
textContent, ok = resp.Content[0].(mcp.TextContent)
|
| 923 |
+
require.True(t, ok, "expected content to be of type TextContent")
|
| 924 |
+
require.Equal(t, "", textContent.Text, "expected content to be empty")
|
| 925 |
+
|
| 926 |
+
// Finally, get requested reviews and see copilot is in there
|
| 927 |
+
// MCP Server doesn't support requesting reviews yet, but we can use the GitHub Client
|
| 928 |
+
ghClient := gogithub.NewClient(nil).WithAuthToken(getE2EToken(t))
|
| 929 |
+
t.Logf("Getting reviews for pull request in %s/%s...", currentOwner, repoName)
|
| 930 |
+
reviewRequests, _, err := ghClient.PullRequests.ListReviewers(context.Background(), currentOwner, repoName, 1, nil)
|
| 931 |
+
require.NoError(t, err, "expected to get review requests successfully")
|
| 932 |
+
|
| 933 |
+
// Check that there is one review request from copilot
|
| 934 |
+
require.Len(t, reviewRequests.Users, 1, "expected to find one review request")
|
| 935 |
+
require.Equal(t, "Copilot", *reviewRequests.Users[0].Login, "expected review request to be for Copilot")
|
| 936 |
+
require.Equal(t, "Bot", *reviewRequests.Users[0].Type, "expected review request to be for Bot")
|
| 937 |
+
}
|
| 938 |
+
|
| 939 |
+
func TestAssignCopilotToIssue(t *testing.T) {
|
| 940 |
+
t.Parallel()
|
| 941 |
+
|
| 942 |
+
if getE2EHost() != "" && getE2EHost() != "https://github.com" {
|
| 943 |
+
t.Skip("Skipping test because the host does not support copilot being assigned to issues")
|
| 944 |
+
}
|
| 945 |
+
|
| 946 |
+
mcpClient := setupMCPClient(t)
|
| 947 |
+
ctx := context.Background()
|
| 948 |
+
|
| 949 |
+
// First, who am I
|
| 950 |
+
getMeRequest := mcp.CallToolRequest{}
|
| 951 |
+
getMeRequest.Params.Name = "get_me"
|
| 952 |
+
|
| 953 |
+
t.Log("Getting current user...")
|
| 954 |
+
resp, err := mcpClient.CallTool(ctx, getMeRequest)
|
| 955 |
+
require.NoError(t, err, "expected to call 'get_me' tool successfully")
|
| 956 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 957 |
+
|
| 958 |
+
require.False(t, resp.IsError, "expected result not to be an error")
|
| 959 |
+
require.Len(t, resp.Content, 1, "expected content to have one item")
|
| 960 |
+
|
| 961 |
+
textContent, ok := resp.Content[0].(mcp.TextContent)
|
| 962 |
+
require.True(t, ok, "expected content to be of type TextContent")
|
| 963 |
+
|
| 964 |
+
var trimmedGetMeText struct {
|
| 965 |
+
Login string `json:"login"`
|
| 966 |
+
}
|
| 967 |
+
err = json.Unmarshal([]byte(textContent.Text), &trimmedGetMeText)
|
| 968 |
+
require.NoError(t, err, "expected to unmarshal text content successfully")
|
| 969 |
+
|
| 970 |
+
currentOwner := trimmedGetMeText.Login
|
| 971 |
+
|
| 972 |
+
// Then create a repository with a README (via autoInit)
|
| 973 |
+
repoName := fmt.Sprintf("github-mcp-server-e2e-%s-%d", t.Name(), time.Now().UnixMilli())
|
| 974 |
+
createRepoRequest := mcp.CallToolRequest{}
|
| 975 |
+
createRepoRequest.Params.Name = "create_repository"
|
| 976 |
+
createRepoRequest.Params.Arguments = map[string]any{
|
| 977 |
+
"name": repoName,
|
| 978 |
+
"private": true,
|
| 979 |
+
"autoInit": true,
|
| 980 |
+
}
|
| 981 |
+
|
| 982 |
+
t.Logf("Creating repository %s/%s...", currentOwner, repoName)
|
| 983 |
+
_, err = mcpClient.CallTool(ctx, createRepoRequest)
|
| 984 |
+
require.NoError(t, err, "expected to call 'create_repository' tool successfully")
|
| 985 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 986 |
+
|
| 987 |
+
// Cleanup the repository after the test
|
| 988 |
+
t.Cleanup(func() {
|
| 989 |
+
// MCP Server doesn't support deletions, but we can use the GitHub Client
|
| 990 |
+
ghClient := getRESTClient(t)
|
| 991 |
+
t.Logf("Deleting repository %s/%s...", currentOwner, repoName)
|
| 992 |
+
_, err := ghClient.Repositories.Delete(context.Background(), currentOwner, repoName)
|
| 993 |
+
require.NoError(t, err, "expected to delete repository successfully")
|
| 994 |
+
})
|
| 995 |
+
|
| 996 |
+
// Create an issue
|
| 997 |
+
createIssueRequest := mcp.CallToolRequest{}
|
| 998 |
+
createIssueRequest.Params.Name = "create_issue"
|
| 999 |
+
createIssueRequest.Params.Arguments = map[string]any{
|
| 1000 |
+
"owner": currentOwner,
|
| 1001 |
+
"repo": repoName,
|
| 1002 |
+
"title": "Test issue to assign copilot to",
|
| 1003 |
+
}
|
| 1004 |
+
|
| 1005 |
+
t.Logf("Creating issue in %s/%s...", currentOwner, repoName)
|
| 1006 |
+
resp, err = mcpClient.CallTool(ctx, createIssueRequest)
|
| 1007 |
+
require.NoError(t, err, "expected to call 'create_issue' tool successfully")
|
| 1008 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 1009 |
+
|
| 1010 |
+
// Assign copilot to the issue
|
| 1011 |
+
assignCopilotRequest := mcp.CallToolRequest{}
|
| 1012 |
+
assignCopilotRequest.Params.Name = "assign_copilot_to_issue"
|
| 1013 |
+
assignCopilotRequest.Params.Arguments = map[string]any{
|
| 1014 |
+
"owner": currentOwner,
|
| 1015 |
+
"repo": repoName,
|
| 1016 |
+
"issueNumber": 1,
|
| 1017 |
+
}
|
| 1018 |
+
|
| 1019 |
+
t.Logf("Assigning copilot to issue in %s/%s...", currentOwner, repoName)
|
| 1020 |
+
resp, err = mcpClient.CallTool(ctx, assignCopilotRequest)
|
| 1021 |
+
require.NoError(t, err, "expected to call 'assign_copilot_to_issue' tool successfully")
|
| 1022 |
+
|
| 1023 |
+
textContent, ok = resp.Content[0].(mcp.TextContent)
|
| 1024 |
+
require.True(t, ok, "expected content to be of type TextContent")
|
| 1025 |
+
|
| 1026 |
+
possibleExpectedFailure := "copilot isn't available as an assignee for this issue. Please inform the user to visit https://docs.github.com/en/copilot/using-github-copilot/using-copilot-coding-agent-to-work-on-tasks/about-assigning-tasks-to-copilot for more information."
|
| 1027 |
+
if resp.IsError && textContent.Text == possibleExpectedFailure {
|
| 1028 |
+
t.Skip("skipping because copilot wasn't available as an assignee on this issue, it's likely that the owner doesn't have copilot enabled in their settings")
|
| 1029 |
+
}
|
| 1030 |
+
|
| 1031 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 1032 |
+
|
| 1033 |
+
require.Equal(t, "successfully assigned copilot to issue", textContent.Text)
|
| 1034 |
+
|
| 1035 |
+
// Check that copilot is assigned to the issue
|
| 1036 |
+
// MCP Server doesn't support getting assignees yet
|
| 1037 |
+
ghClient := getRESTClient(t)
|
| 1038 |
+
assignees, response, err := ghClient.Issues.Get(context.Background(), currentOwner, repoName, 1)
|
| 1039 |
+
require.NoError(t, err, "expected to get issue successfully")
|
| 1040 |
+
require.Equal(t, http.StatusOK, response.StatusCode, "expected to get issue successfully")
|
| 1041 |
+
require.Len(t, assignees.Assignees, 1, "expected to find one assignee")
|
| 1042 |
+
require.Equal(t, "Copilot", *assignees.Assignees[0].Login, "expected copilot to be assigned to the issue")
|
| 1043 |
+
}
|
| 1044 |
+
|
| 1045 |
+
func TestPullRequestAtomicCreateAndSubmit(t *testing.T) {
|
| 1046 |
+
t.Parallel()
|
| 1047 |
+
|
| 1048 |
+
mcpClient := setupMCPClient(t)
|
| 1049 |
+
|
| 1050 |
+
ctx := context.Background()
|
| 1051 |
+
|
| 1052 |
+
// First, who am I
|
| 1053 |
+
getMeRequest := mcp.CallToolRequest{}
|
| 1054 |
+
getMeRequest.Params.Name = "get_me"
|
| 1055 |
+
|
| 1056 |
+
t.Log("Getting current user...")
|
| 1057 |
+
resp, err := mcpClient.CallTool(ctx, getMeRequest)
|
| 1058 |
+
require.NoError(t, err, "expected to call 'get_me' tool successfully")
|
| 1059 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 1060 |
+
|
| 1061 |
+
require.False(t, resp.IsError, "expected result not to be an error")
|
| 1062 |
+
require.Len(t, resp.Content, 1, "expected content to have one item")
|
| 1063 |
+
|
| 1064 |
+
textContent, ok := resp.Content[0].(mcp.TextContent)
|
| 1065 |
+
require.True(t, ok, "expected content to be of type TextContent")
|
| 1066 |
+
|
| 1067 |
+
var trimmedGetMeText struct {
|
| 1068 |
+
Login string `json:"login"`
|
| 1069 |
+
}
|
| 1070 |
+
err = json.Unmarshal([]byte(textContent.Text), &trimmedGetMeText)
|
| 1071 |
+
require.NoError(t, err, "expected to unmarshal text content successfully")
|
| 1072 |
+
|
| 1073 |
+
currentOwner := trimmedGetMeText.Login
|
| 1074 |
+
|
| 1075 |
+
// Then create a repository with a README (via autoInit)
|
| 1076 |
+
repoName := fmt.Sprintf("github-mcp-server-e2e-%s-%d", t.Name(), time.Now().UnixMilli())
|
| 1077 |
+
createRepoRequest := mcp.CallToolRequest{}
|
| 1078 |
+
createRepoRequest.Params.Name = "create_repository"
|
| 1079 |
+
createRepoRequest.Params.Arguments = map[string]any{
|
| 1080 |
+
"name": repoName,
|
| 1081 |
+
"private": true,
|
| 1082 |
+
"autoInit": true,
|
| 1083 |
+
}
|
| 1084 |
+
|
| 1085 |
+
t.Logf("Creating repository %s/%s...", currentOwner, repoName)
|
| 1086 |
+
_, err = mcpClient.CallTool(ctx, createRepoRequest)
|
| 1087 |
+
require.NoError(t, err, "expected to call 'get_me' tool successfully")
|
| 1088 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 1089 |
+
|
| 1090 |
+
// Cleanup the repository after the test
|
| 1091 |
+
t.Cleanup(func() {
|
| 1092 |
+
// MCP Server doesn't support deletions, but we can use the GitHub Client
|
| 1093 |
+
ghClient := getRESTClient(t)
|
| 1094 |
+
t.Logf("Deleting repository %s/%s...", currentOwner, repoName)
|
| 1095 |
+
_, err := ghClient.Repositories.Delete(context.Background(), currentOwner, repoName)
|
| 1096 |
+
require.NoError(t, err, "expected to delete repository successfully")
|
| 1097 |
+
})
|
| 1098 |
+
|
| 1099 |
+
// Create a branch on which to create a new commit
|
| 1100 |
+
createBranchRequest := mcp.CallToolRequest{}
|
| 1101 |
+
createBranchRequest.Params.Name = "create_branch"
|
| 1102 |
+
createBranchRequest.Params.Arguments = map[string]any{
|
| 1103 |
+
"owner": currentOwner,
|
| 1104 |
+
"repo": repoName,
|
| 1105 |
+
"branch": "test-branch",
|
| 1106 |
+
"from_branch": "main",
|
| 1107 |
+
}
|
| 1108 |
+
|
| 1109 |
+
t.Logf("Creating branch in %s/%s...", currentOwner, repoName)
|
| 1110 |
+
resp, err = mcpClient.CallTool(ctx, createBranchRequest)
|
| 1111 |
+
require.NoError(t, err, "expected to call 'create_branch' tool successfully")
|
| 1112 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 1113 |
+
|
| 1114 |
+
// Create a commit with a new file
|
| 1115 |
+
commitRequest := mcp.CallToolRequest{}
|
| 1116 |
+
commitRequest.Params.Name = "create_or_update_file"
|
| 1117 |
+
commitRequest.Params.Arguments = map[string]any{
|
| 1118 |
+
"owner": currentOwner,
|
| 1119 |
+
"repo": repoName,
|
| 1120 |
+
"path": "test-file.txt",
|
| 1121 |
+
"content": fmt.Sprintf("Created by e2e test %s", t.Name()),
|
| 1122 |
+
"message": "Add test file",
|
| 1123 |
+
"branch": "test-branch",
|
| 1124 |
+
}
|
| 1125 |
+
|
| 1126 |
+
t.Logf("Creating commit with new file in %s/%s...", currentOwner, repoName)
|
| 1127 |
+
resp, err = mcpClient.CallTool(ctx, commitRequest)
|
| 1128 |
+
require.NoError(t, err, "expected to call 'create_or_update_file' tool successfully")
|
| 1129 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 1130 |
+
|
| 1131 |
+
textContent, ok = resp.Content[0].(mcp.TextContent)
|
| 1132 |
+
require.True(t, ok, "expected content to be of type TextContent")
|
| 1133 |
+
|
| 1134 |
+
var trimmedCommitText struct {
|
| 1135 |
+
Commit struct {
|
| 1136 |
+
SHA string `json:"sha"`
|
| 1137 |
+
} `json:"commit"`
|
| 1138 |
+
}
|
| 1139 |
+
err = json.Unmarshal([]byte(textContent.Text), &trimmedCommitText)
|
| 1140 |
+
require.NoError(t, err, "expected to unmarshal text content successfully")
|
| 1141 |
+
commitID := trimmedCommitText.Commit.SHA
|
| 1142 |
+
|
| 1143 |
+
// Create a pull request
|
| 1144 |
+
prRequest := mcp.CallToolRequest{}
|
| 1145 |
+
prRequest.Params.Name = "create_pull_request"
|
| 1146 |
+
prRequest.Params.Arguments = map[string]any{
|
| 1147 |
+
"owner": currentOwner,
|
| 1148 |
+
"repo": repoName,
|
| 1149 |
+
"title": "Test PR",
|
| 1150 |
+
"body": "This is a test PR",
|
| 1151 |
+
"head": "test-branch",
|
| 1152 |
+
"base": "main",
|
| 1153 |
+
}
|
| 1154 |
+
|
| 1155 |
+
t.Logf("Creating pull request in %s/%s...", currentOwner, repoName)
|
| 1156 |
+
resp, err = mcpClient.CallTool(ctx, prRequest)
|
| 1157 |
+
require.NoError(t, err, "expected to call 'create_pull_request' tool successfully")
|
| 1158 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 1159 |
+
|
| 1160 |
+
// Create and submit a review
|
| 1161 |
+
createAndSubmitReviewRequest := mcp.CallToolRequest{}
|
| 1162 |
+
createAndSubmitReviewRequest.Params.Name = "create_and_submit_pull_request_review"
|
| 1163 |
+
createAndSubmitReviewRequest.Params.Arguments = map[string]any{
|
| 1164 |
+
"owner": currentOwner,
|
| 1165 |
+
"repo": repoName,
|
| 1166 |
+
"pullNumber": 1,
|
| 1167 |
+
"event": "COMMENT", // the only event we can use as the creator of the PR
|
| 1168 |
+
"body": "Looks good if you like bad code I guess!",
|
| 1169 |
+
"commitID": commitID,
|
| 1170 |
+
}
|
| 1171 |
+
|
| 1172 |
+
t.Logf("Creating and submitting review for pull request in %s/%s...", currentOwner, repoName)
|
| 1173 |
+
resp, err = mcpClient.CallTool(ctx, createAndSubmitReviewRequest)
|
| 1174 |
+
require.NoError(t, err, "expected to call 'create_and_submit_pull_request_review' tool successfully")
|
| 1175 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 1176 |
+
|
| 1177 |
+
// Finally, get the list of reviews and see that our review has been submitted
|
| 1178 |
+
getPullRequestsReview := mcp.CallToolRequest{}
|
| 1179 |
+
getPullRequestsReview.Params.Name = "get_pull_request_reviews"
|
| 1180 |
+
getPullRequestsReview.Params.Arguments = map[string]any{
|
| 1181 |
+
"owner": currentOwner,
|
| 1182 |
+
"repo": repoName,
|
| 1183 |
+
"pullNumber": 1,
|
| 1184 |
+
}
|
| 1185 |
+
|
| 1186 |
+
t.Logf("Getting reviews for pull request in %s/%s...", currentOwner, repoName)
|
| 1187 |
+
resp, err = mcpClient.CallTool(ctx, getPullRequestsReview)
|
| 1188 |
+
require.NoError(t, err, "expected to call 'get_pull_request_reviews' tool successfully")
|
| 1189 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 1190 |
+
|
| 1191 |
+
textContent, ok = resp.Content[0].(mcp.TextContent)
|
| 1192 |
+
require.True(t, ok, "expected content to be of type TextContent")
|
| 1193 |
+
|
| 1194 |
+
var reviews []struct {
|
| 1195 |
+
State string `json:"state"`
|
| 1196 |
+
}
|
| 1197 |
+
err = json.Unmarshal([]byte(textContent.Text), &reviews)
|
| 1198 |
+
require.NoError(t, err, "expected to unmarshal text content successfully")
|
| 1199 |
+
|
| 1200 |
+
// Check that there is one review
|
| 1201 |
+
require.Len(t, reviews, 1, "expected to find one review")
|
| 1202 |
+
require.Equal(t, "COMMENTED", reviews[0].State, "expected review state to be COMMENTED")
|
| 1203 |
+
}
|
| 1204 |
+
|
| 1205 |
+
func TestPullRequestReviewCommentSubmit(t *testing.T) {
|
| 1206 |
+
t.Parallel()
|
| 1207 |
+
|
| 1208 |
+
mcpClient := setupMCPClient(t)
|
| 1209 |
+
|
| 1210 |
+
ctx := context.Background()
|
| 1211 |
+
|
| 1212 |
+
// First, who am I
|
| 1213 |
+
getMeRequest := mcp.CallToolRequest{}
|
| 1214 |
+
getMeRequest.Params.Name = "get_me"
|
| 1215 |
+
|
| 1216 |
+
t.Log("Getting current user...")
|
| 1217 |
+
resp, err := mcpClient.CallTool(ctx, getMeRequest)
|
| 1218 |
+
require.NoError(t, err, "expected to call 'get_me' tool successfully")
|
| 1219 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 1220 |
+
|
| 1221 |
+
require.False(t, resp.IsError, "expected result not to be an error")
|
| 1222 |
+
require.Len(t, resp.Content, 1, "expected content to have one item")
|
| 1223 |
+
|
| 1224 |
+
textContent, ok := resp.Content[0].(mcp.TextContent)
|
| 1225 |
+
require.True(t, ok, "expected content to be of type TextContent")
|
| 1226 |
+
|
| 1227 |
+
var trimmedGetMeText struct {
|
| 1228 |
+
Login string `json:"login"`
|
| 1229 |
+
}
|
| 1230 |
+
err = json.Unmarshal([]byte(textContent.Text), &trimmedGetMeText)
|
| 1231 |
+
require.NoError(t, err, "expected to unmarshal text content successfully")
|
| 1232 |
+
|
| 1233 |
+
currentOwner := trimmedGetMeText.Login
|
| 1234 |
+
|
| 1235 |
+
// Then create a repository with a README (via autoInit)
|
| 1236 |
+
repoName := fmt.Sprintf("github-mcp-server-e2e-%s-%d", t.Name(), time.Now().UnixMilli())
|
| 1237 |
+
createRepoRequest := mcp.CallToolRequest{}
|
| 1238 |
+
createRepoRequest.Params.Name = "create_repository"
|
| 1239 |
+
createRepoRequest.Params.Arguments = map[string]any{
|
| 1240 |
+
"name": repoName,
|
| 1241 |
+
"private": true,
|
| 1242 |
+
"autoInit": true,
|
| 1243 |
+
}
|
| 1244 |
+
|
| 1245 |
+
t.Logf("Creating repository %s/%s...", currentOwner, repoName)
|
| 1246 |
+
_, err = mcpClient.CallTool(ctx, createRepoRequest)
|
| 1247 |
+
require.NoError(t, err, "expected to call 'create_repository' tool successfully")
|
| 1248 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 1249 |
+
|
| 1250 |
+
// Cleanup the repository after the test
|
| 1251 |
+
t.Cleanup(func() {
|
| 1252 |
+
// MCP Server doesn't support deletions, but we can use the GitHub Client
|
| 1253 |
+
ghClient := getRESTClient(t)
|
| 1254 |
+
t.Logf("Deleting repository %s/%s...", currentOwner, repoName)
|
| 1255 |
+
_, err := ghClient.Repositories.Delete(context.Background(), currentOwner, repoName)
|
| 1256 |
+
require.NoError(t, err, "expected to delete repository successfully")
|
| 1257 |
+
})
|
| 1258 |
+
|
| 1259 |
+
// Create a branch on which to create a new commit
|
| 1260 |
+
createBranchRequest := mcp.CallToolRequest{}
|
| 1261 |
+
createBranchRequest.Params.Name = "create_branch"
|
| 1262 |
+
createBranchRequest.Params.Arguments = map[string]any{
|
| 1263 |
+
"owner": currentOwner,
|
| 1264 |
+
"repo": repoName,
|
| 1265 |
+
"branch": "test-branch",
|
| 1266 |
+
"from_branch": "main",
|
| 1267 |
+
}
|
| 1268 |
+
|
| 1269 |
+
t.Logf("Creating branch in %s/%s...", currentOwner, repoName)
|
| 1270 |
+
resp, err = mcpClient.CallTool(ctx, createBranchRequest)
|
| 1271 |
+
require.NoError(t, err, "expected to call 'create_branch' tool successfully")
|
| 1272 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 1273 |
+
|
| 1274 |
+
// Create a commit with a new file
|
| 1275 |
+
commitRequest := mcp.CallToolRequest{}
|
| 1276 |
+
commitRequest.Params.Name = "create_or_update_file"
|
| 1277 |
+
commitRequest.Params.Arguments = map[string]any{
|
| 1278 |
+
"owner": currentOwner,
|
| 1279 |
+
"repo": repoName,
|
| 1280 |
+
"path": "test-file.txt",
|
| 1281 |
+
"content": fmt.Sprintf("Created by e2e test %s\nwith multiple lines", t.Name()),
|
| 1282 |
+
"message": "Add test file",
|
| 1283 |
+
"branch": "test-branch",
|
| 1284 |
+
}
|
| 1285 |
+
|
| 1286 |
+
t.Logf("Creating commit with new file in %s/%s...", currentOwner, repoName)
|
| 1287 |
+
resp, err = mcpClient.CallTool(ctx, commitRequest)
|
| 1288 |
+
require.NoError(t, err, "expected to call 'create_or_update_file' tool successfully")
|
| 1289 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 1290 |
+
|
| 1291 |
+
textContent, ok = resp.Content[0].(mcp.TextContent)
|
| 1292 |
+
require.True(t, ok, "expected content to be of type TextContent")
|
| 1293 |
+
|
| 1294 |
+
var trimmedCommitText struct {
|
| 1295 |
+
Commit struct {
|
| 1296 |
+
SHA string `json:"sha"`
|
| 1297 |
+
} `json:"commit"`
|
| 1298 |
+
}
|
| 1299 |
+
err = json.Unmarshal([]byte(textContent.Text), &trimmedCommitText)
|
| 1300 |
+
require.NoError(t, err, "expected to unmarshal text content successfully")
|
| 1301 |
+
commitId := trimmedCommitText.Commit.SHA
|
| 1302 |
+
|
| 1303 |
+
// Create a pull request
|
| 1304 |
+
prRequest := mcp.CallToolRequest{}
|
| 1305 |
+
prRequest.Params.Name = "create_pull_request"
|
| 1306 |
+
prRequest.Params.Arguments = map[string]any{
|
| 1307 |
+
"owner": currentOwner,
|
| 1308 |
+
"repo": repoName,
|
| 1309 |
+
"title": "Test PR",
|
| 1310 |
+
"body": "This is a test PR",
|
| 1311 |
+
"head": "test-branch",
|
| 1312 |
+
"base": "main",
|
| 1313 |
+
}
|
| 1314 |
+
|
| 1315 |
+
t.Logf("Creating pull request in %s/%s...", currentOwner, repoName)
|
| 1316 |
+
resp, err = mcpClient.CallTool(ctx, prRequest)
|
| 1317 |
+
require.NoError(t, err, "expected to call 'create_pull_request' tool successfully")
|
| 1318 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 1319 |
+
|
| 1320 |
+
// Create a review for the pull request, but we can't approve it
|
| 1321 |
+
// because the current owner also owns the PR.
|
| 1322 |
+
createPendingPullRequestReviewRequest := mcp.CallToolRequest{}
|
| 1323 |
+
createPendingPullRequestReviewRequest.Params.Name = "create_pending_pull_request_review"
|
| 1324 |
+
createPendingPullRequestReviewRequest.Params.Arguments = map[string]any{
|
| 1325 |
+
"owner": currentOwner,
|
| 1326 |
+
"repo": repoName,
|
| 1327 |
+
"pullNumber": 1,
|
| 1328 |
+
}
|
| 1329 |
+
|
| 1330 |
+
t.Logf("Creating pending review for pull request in %s/%s...", currentOwner, repoName)
|
| 1331 |
+
resp, err = mcpClient.CallTool(ctx, createPendingPullRequestReviewRequest)
|
| 1332 |
+
require.NoError(t, err, "expected to call 'create_pending_pull_request_review' tool successfully")
|
| 1333 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 1334 |
+
|
| 1335 |
+
textContent, ok = resp.Content[0].(mcp.TextContent)
|
| 1336 |
+
require.True(t, ok, "expected content to be of type TextContent")
|
| 1337 |
+
require.Equal(t, "pending pull request created", textContent.Text)
|
| 1338 |
+
|
| 1339 |
+
// Add a file review comment
|
| 1340 |
+
addFileReviewCommentRequest := mcp.CallToolRequest{}
|
| 1341 |
+
addFileReviewCommentRequest.Params.Name = "add_comment_to_pending_review"
|
| 1342 |
+
addFileReviewCommentRequest.Params.Arguments = map[string]any{
|
| 1343 |
+
"owner": currentOwner,
|
| 1344 |
+
"repo": repoName,
|
| 1345 |
+
"pullNumber": 1,
|
| 1346 |
+
"path": "test-file.txt",
|
| 1347 |
+
"subjectType": "FILE",
|
| 1348 |
+
"body": "File review comment",
|
| 1349 |
+
}
|
| 1350 |
+
|
| 1351 |
+
t.Logf("Adding file review comment to pull request in %s/%s...", currentOwner, repoName)
|
| 1352 |
+
resp, err = mcpClient.CallTool(ctx, addFileReviewCommentRequest)
|
| 1353 |
+
require.NoError(t, err, "expected to call 'add_comment_to_pending_review' tool successfully")
|
| 1354 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 1355 |
+
|
| 1356 |
+
// Add a single line review comment
|
| 1357 |
+
addSingleLineReviewCommentRequest := mcp.CallToolRequest{}
|
| 1358 |
+
addSingleLineReviewCommentRequest.Params.Name = "add_comment_to_pending_review"
|
| 1359 |
+
addSingleLineReviewCommentRequest.Params.Arguments = map[string]any{
|
| 1360 |
+
"owner": currentOwner,
|
| 1361 |
+
"repo": repoName,
|
| 1362 |
+
"pullNumber": 1,
|
| 1363 |
+
"path": "test-file.txt",
|
| 1364 |
+
"subjectType": "LINE",
|
| 1365 |
+
"body": "Single line review comment",
|
| 1366 |
+
"line": 1,
|
| 1367 |
+
"side": "RIGHT",
|
| 1368 |
+
"commitId": commitId,
|
| 1369 |
+
}
|
| 1370 |
+
|
| 1371 |
+
t.Logf("Adding single line review comment to pull request in %s/%s...", currentOwner, repoName)
|
| 1372 |
+
resp, err = mcpClient.CallTool(ctx, addSingleLineReviewCommentRequest)
|
| 1373 |
+
require.NoError(t, err, "expected to call 'add_comment_to_pending_review' tool successfully")
|
| 1374 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 1375 |
+
|
| 1376 |
+
// Add a multiline review comment
|
| 1377 |
+
addMultilineReviewCommentRequest := mcp.CallToolRequest{}
|
| 1378 |
+
addMultilineReviewCommentRequest.Params.Name = "add_comment_to_pending_review"
|
| 1379 |
+
addMultilineReviewCommentRequest.Params.Arguments = map[string]any{
|
| 1380 |
+
"owner": currentOwner,
|
| 1381 |
+
"repo": repoName,
|
| 1382 |
+
"pullNumber": 1,
|
| 1383 |
+
"path": "test-file.txt",
|
| 1384 |
+
"subjectType": "LINE",
|
| 1385 |
+
"body": "Multiline review comment",
|
| 1386 |
+
"startLine": 1,
|
| 1387 |
+
"line": 2,
|
| 1388 |
+
"startSide": "RIGHT",
|
| 1389 |
+
"side": "RIGHT",
|
| 1390 |
+
"commitId": commitId,
|
| 1391 |
+
}
|
| 1392 |
+
|
| 1393 |
+
t.Logf("Adding multi line review comment to pull request in %s/%s...", currentOwner, repoName)
|
| 1394 |
+
resp, err = mcpClient.CallTool(ctx, addMultilineReviewCommentRequest)
|
| 1395 |
+
require.NoError(t, err, "expected to call 'add_comment_to_pending_review' tool successfully")
|
| 1396 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 1397 |
+
|
| 1398 |
+
// Submit the review
|
| 1399 |
+
submitReviewRequest := mcp.CallToolRequest{}
|
| 1400 |
+
submitReviewRequest.Params.Name = "submit_pending_pull_request_review"
|
| 1401 |
+
submitReviewRequest.Params.Arguments = map[string]any{
|
| 1402 |
+
"owner": currentOwner,
|
| 1403 |
+
"repo": repoName,
|
| 1404 |
+
"pullNumber": 1,
|
| 1405 |
+
"event": "COMMENT", // the only event we can use as the creator of the PR
|
| 1406 |
+
"body": "Looks good if you like bad code I guess!",
|
| 1407 |
+
}
|
| 1408 |
+
|
| 1409 |
+
t.Logf("Submitting review for pull request in %s/%s...", currentOwner, repoName)
|
| 1410 |
+
resp, err = mcpClient.CallTool(ctx, submitReviewRequest)
|
| 1411 |
+
require.NoError(t, err, "expected to call 'submit_pending_pull_request_review' tool successfully")
|
| 1412 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 1413 |
+
|
| 1414 |
+
// Finally, get the review and see that it has been created
|
| 1415 |
+
getPullRequestsReview := mcp.CallToolRequest{}
|
| 1416 |
+
getPullRequestsReview.Params.Name = "get_pull_request_reviews"
|
| 1417 |
+
getPullRequestsReview.Params.Arguments = map[string]any{
|
| 1418 |
+
"owner": currentOwner,
|
| 1419 |
+
"repo": repoName,
|
| 1420 |
+
"pullNumber": 1,
|
| 1421 |
+
}
|
| 1422 |
+
|
| 1423 |
+
t.Logf("Getting reviews for pull request in %s/%s...", currentOwner, repoName)
|
| 1424 |
+
resp, err = mcpClient.CallTool(ctx, getPullRequestsReview)
|
| 1425 |
+
require.NoError(t, err, "expected to call 'get_pull_request_reviews' tool successfully")
|
| 1426 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 1427 |
+
|
| 1428 |
+
textContent, ok = resp.Content[0].(mcp.TextContent)
|
| 1429 |
+
require.True(t, ok, "expected content to be of type TextContent")
|
| 1430 |
+
|
| 1431 |
+
var reviews []struct {
|
| 1432 |
+
ID int `json:"id"`
|
| 1433 |
+
State string `json:"state"`
|
| 1434 |
+
}
|
| 1435 |
+
err = json.Unmarshal([]byte(textContent.Text), &reviews)
|
| 1436 |
+
require.NoError(t, err, "expected to unmarshal text content successfully")
|
| 1437 |
+
|
| 1438 |
+
// Check that there is one review
|
| 1439 |
+
require.Len(t, reviews, 1, "expected to find one review")
|
| 1440 |
+
require.Equal(t, "COMMENTED", reviews[0].State, "expected review state to be COMMENTED")
|
| 1441 |
+
|
| 1442 |
+
// Check that there are three review comments
|
| 1443 |
+
// MCP Server doesn't support this, but we can use the GitHub Client
|
| 1444 |
+
ghClient := getRESTClient(t)
|
| 1445 |
+
comments, _, err := ghClient.PullRequests.ListReviewComments(context.Background(), currentOwner, repoName, 1, int64(reviews[0].ID), nil)
|
| 1446 |
+
require.NoError(t, err, "expected to list review comments successfully")
|
| 1447 |
+
require.Equal(t, 3, len(comments), "expected to find three review comments")
|
| 1448 |
+
}
|
| 1449 |
+
|
| 1450 |
+
func TestPullRequestReviewDeletion(t *testing.T) {
|
| 1451 |
+
t.Parallel()
|
| 1452 |
+
|
| 1453 |
+
mcpClient := setupMCPClient(t)
|
| 1454 |
+
|
| 1455 |
+
ctx := context.Background()
|
| 1456 |
+
|
| 1457 |
+
// First, who am I
|
| 1458 |
+
getMeRequest := mcp.CallToolRequest{}
|
| 1459 |
+
getMeRequest.Params.Name = "get_me"
|
| 1460 |
+
|
| 1461 |
+
t.Log("Getting current user...")
|
| 1462 |
+
resp, err := mcpClient.CallTool(ctx, getMeRequest)
|
| 1463 |
+
require.NoError(t, err, "expected to call 'get_me' tool successfully")
|
| 1464 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 1465 |
+
|
| 1466 |
+
require.False(t, resp.IsError, "expected result not to be an error")
|
| 1467 |
+
require.Len(t, resp.Content, 1, "expected content to have one item")
|
| 1468 |
+
|
| 1469 |
+
textContent, ok := resp.Content[0].(mcp.TextContent)
|
| 1470 |
+
require.True(t, ok, "expected content to be of type TextContent")
|
| 1471 |
+
|
| 1472 |
+
var trimmedGetMeText struct {
|
| 1473 |
+
Login string `json:"login"`
|
| 1474 |
+
}
|
| 1475 |
+
err = json.Unmarshal([]byte(textContent.Text), &trimmedGetMeText)
|
| 1476 |
+
require.NoError(t, err, "expected to unmarshal text content successfully")
|
| 1477 |
+
|
| 1478 |
+
currentOwner := trimmedGetMeText.Login
|
| 1479 |
+
|
| 1480 |
+
// Then create a repository with a README (via autoInit)
|
| 1481 |
+
repoName := fmt.Sprintf("github-mcp-server-e2e-%s-%d", t.Name(), time.Now().UnixMilli())
|
| 1482 |
+
createRepoRequest := mcp.CallToolRequest{}
|
| 1483 |
+
createRepoRequest.Params.Name = "create_repository"
|
| 1484 |
+
createRepoRequest.Params.Arguments = map[string]any{
|
| 1485 |
+
"name": repoName,
|
| 1486 |
+
"private": true,
|
| 1487 |
+
"autoInit": true,
|
| 1488 |
+
}
|
| 1489 |
+
|
| 1490 |
+
t.Logf("Creating repository %s/%s...", currentOwner, repoName)
|
| 1491 |
+
_, err = mcpClient.CallTool(ctx, createRepoRequest)
|
| 1492 |
+
require.NoError(t, err, "expected to call 'get_me' tool successfully")
|
| 1493 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 1494 |
+
|
| 1495 |
+
// Cleanup the repository after the test
|
| 1496 |
+
t.Cleanup(func() {
|
| 1497 |
+
// MCP Server doesn't support deletions, but we can use the GitHub Client
|
| 1498 |
+
ghClient := getRESTClient(t)
|
| 1499 |
+
t.Logf("Deleting repository %s/%s...", currentOwner, repoName)
|
| 1500 |
+
_, err := ghClient.Repositories.Delete(context.Background(), currentOwner, repoName)
|
| 1501 |
+
require.NoError(t, err, "expected to delete repository successfully")
|
| 1502 |
+
})
|
| 1503 |
+
|
| 1504 |
+
// Create a branch on which to create a new commit
|
| 1505 |
+
createBranchRequest := mcp.CallToolRequest{}
|
| 1506 |
+
createBranchRequest.Params.Name = "create_branch"
|
| 1507 |
+
createBranchRequest.Params.Arguments = map[string]any{
|
| 1508 |
+
"owner": currentOwner,
|
| 1509 |
+
"repo": repoName,
|
| 1510 |
+
"branch": "test-branch",
|
| 1511 |
+
"from_branch": "main",
|
| 1512 |
+
}
|
| 1513 |
+
|
| 1514 |
+
t.Logf("Creating branch in %s/%s...", currentOwner, repoName)
|
| 1515 |
+
resp, err = mcpClient.CallTool(ctx, createBranchRequest)
|
| 1516 |
+
require.NoError(t, err, "expected to call 'create_branch' tool successfully")
|
| 1517 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 1518 |
+
|
| 1519 |
+
// Create a commit with a new file
|
| 1520 |
+
commitRequest := mcp.CallToolRequest{}
|
| 1521 |
+
commitRequest.Params.Name = "create_or_update_file"
|
| 1522 |
+
commitRequest.Params.Arguments = map[string]any{
|
| 1523 |
+
"owner": currentOwner,
|
| 1524 |
+
"repo": repoName,
|
| 1525 |
+
"path": "test-file.txt",
|
| 1526 |
+
"content": fmt.Sprintf("Created by e2e test %s", t.Name()),
|
| 1527 |
+
"message": "Add test file",
|
| 1528 |
+
"branch": "test-branch",
|
| 1529 |
+
}
|
| 1530 |
+
|
| 1531 |
+
t.Logf("Creating commit with new file in %s/%s...", currentOwner, repoName)
|
| 1532 |
+
resp, err = mcpClient.CallTool(ctx, commitRequest)
|
| 1533 |
+
require.NoError(t, err, "expected to call 'create_or_update_file' tool successfully")
|
| 1534 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 1535 |
+
|
| 1536 |
+
// Create a pull request
|
| 1537 |
+
prRequest := mcp.CallToolRequest{}
|
| 1538 |
+
prRequest.Params.Name = "create_pull_request"
|
| 1539 |
+
prRequest.Params.Arguments = map[string]any{
|
| 1540 |
+
"owner": currentOwner,
|
| 1541 |
+
"repo": repoName,
|
| 1542 |
+
"title": "Test PR",
|
| 1543 |
+
"body": "This is a test PR",
|
| 1544 |
+
"head": "test-branch",
|
| 1545 |
+
"base": "main",
|
| 1546 |
+
}
|
| 1547 |
+
|
| 1548 |
+
t.Logf("Creating pull request in %s/%s...", currentOwner, repoName)
|
| 1549 |
+
resp, err = mcpClient.CallTool(ctx, prRequest)
|
| 1550 |
+
require.NoError(t, err, "expected to call 'create_pull_request' tool successfully")
|
| 1551 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 1552 |
+
|
| 1553 |
+
// Create a review for the pull request, but we can't approve it
|
| 1554 |
+
// because the current owner also owns the PR.
|
| 1555 |
+
createPendingPullRequestReviewRequest := mcp.CallToolRequest{}
|
| 1556 |
+
createPendingPullRequestReviewRequest.Params.Name = "create_pending_pull_request_review"
|
| 1557 |
+
createPendingPullRequestReviewRequest.Params.Arguments = map[string]any{
|
| 1558 |
+
"owner": currentOwner,
|
| 1559 |
+
"repo": repoName,
|
| 1560 |
+
"pullNumber": 1,
|
| 1561 |
+
}
|
| 1562 |
+
|
| 1563 |
+
t.Logf("Creating pending review for pull request in %s/%s...", currentOwner, repoName)
|
| 1564 |
+
resp, err = mcpClient.CallTool(ctx, createPendingPullRequestReviewRequest)
|
| 1565 |
+
require.NoError(t, err, "expected to call 'create_pending_pull_request_review' tool successfully")
|
| 1566 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 1567 |
+
|
| 1568 |
+
textContent, ok = resp.Content[0].(mcp.TextContent)
|
| 1569 |
+
require.True(t, ok, "expected content to be of type TextContent")
|
| 1570 |
+
require.Equal(t, "pending pull request created", textContent.Text)
|
| 1571 |
+
|
| 1572 |
+
// See that there is a pending review
|
| 1573 |
+
getPullRequestsReview := mcp.CallToolRequest{}
|
| 1574 |
+
getPullRequestsReview.Params.Name = "get_pull_request_reviews"
|
| 1575 |
+
getPullRequestsReview.Params.Arguments = map[string]any{
|
| 1576 |
+
"owner": currentOwner,
|
| 1577 |
+
"repo": repoName,
|
| 1578 |
+
"pullNumber": 1,
|
| 1579 |
+
}
|
| 1580 |
+
|
| 1581 |
+
t.Logf("Getting reviews for pull request in %s/%s...", currentOwner, repoName)
|
| 1582 |
+
resp, err = mcpClient.CallTool(ctx, getPullRequestsReview)
|
| 1583 |
+
require.NoError(t, err, "expected to call 'get_pull_request_reviews' tool successfully")
|
| 1584 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 1585 |
+
|
| 1586 |
+
textContent, ok = resp.Content[0].(mcp.TextContent)
|
| 1587 |
+
require.True(t, ok, "expected content to be of type TextContent")
|
| 1588 |
+
|
| 1589 |
+
var reviews []struct {
|
| 1590 |
+
State string `json:"state"`
|
| 1591 |
+
}
|
| 1592 |
+
err = json.Unmarshal([]byte(textContent.Text), &reviews)
|
| 1593 |
+
require.NoError(t, err, "expected to unmarshal text content successfully")
|
| 1594 |
+
|
| 1595 |
+
// Check that there is one review
|
| 1596 |
+
require.Len(t, reviews, 1, "expected to find one review")
|
| 1597 |
+
require.Equal(t, "PENDING", reviews[0].State, "expected review state to be PENDING")
|
| 1598 |
+
|
| 1599 |
+
// Delete the review
|
| 1600 |
+
deleteReviewRequest := mcp.CallToolRequest{}
|
| 1601 |
+
deleteReviewRequest.Params.Name = "delete_pending_pull_request_review"
|
| 1602 |
+
deleteReviewRequest.Params.Arguments = map[string]any{
|
| 1603 |
+
"owner": currentOwner,
|
| 1604 |
+
"repo": repoName,
|
| 1605 |
+
"pullNumber": 1,
|
| 1606 |
+
}
|
| 1607 |
+
|
| 1608 |
+
t.Logf("Deleting review for pull request in %s/%s...", currentOwner, repoName)
|
| 1609 |
+
resp, err = mcpClient.CallTool(ctx, deleteReviewRequest)
|
| 1610 |
+
require.NoError(t, err, "expected to call 'delete_pending_pull_request_review' tool successfully")
|
| 1611 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 1612 |
+
|
| 1613 |
+
// See that there are no reviews
|
| 1614 |
+
t.Logf("Getting reviews for pull request in %s/%s...", currentOwner, repoName)
|
| 1615 |
+
resp, err = mcpClient.CallTool(ctx, getPullRequestsReview)
|
| 1616 |
+
require.NoError(t, err, "expected to call 'get_pull_request_reviews' tool successfully")
|
| 1617 |
+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
|
| 1618 |
+
|
| 1619 |
+
textContent, ok = resp.Content[0].(mcp.TextContent)
|
| 1620 |
+
require.True(t, ok, "expected content to be of type TextContent")
|
| 1621 |
+
|
| 1622 |
+
var noReviews []struct{}
|
| 1623 |
+
err = json.Unmarshal([]byte(textContent.Text), &noReviews)
|
| 1624 |
+
require.NoError(t, err, "expected to unmarshal text content successfully")
|
| 1625 |
+
require.Len(t, noReviews, 0, "expected to find no reviews")
|
| 1626 |
+
}
|
go.mod
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module github.com/github/github-mcp-server
|
| 2 |
+
|
| 3 |
+
go 1.23.7
|
| 4 |
+
|
| 5 |
+
require (
|
| 6 |
+
github.com/google/go-github/v74 v74.0.0
|
| 7 |
+
github.com/josephburnett/jd v1.9.2
|
| 8 |
+
github.com/mark3labs/mcp-go v0.36.0
|
| 9 |
+
github.com/migueleliasweb/go-github-mock v1.3.0
|
| 10 |
+
github.com/spf13/cobra v1.9.1
|
| 11 |
+
github.com/spf13/viper v1.20.1
|
| 12 |
+
github.com/stretchr/testify v1.10.0
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
require (
|
| 16 |
+
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
| 17 |
+
github.com/buger/jsonparser v1.1.1 // indirect
|
| 18 |
+
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
| 19 |
+
github.com/go-openapi/swag v0.21.1 // indirect
|
| 20 |
+
github.com/invopop/jsonschema v0.13.0 // indirect
|
| 21 |
+
github.com/josharian/intern v1.0.0 // indirect
|
| 22 |
+
github.com/mailru/easyjson v0.7.7 // indirect
|
| 23 |
+
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
| 24 |
+
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect
|
| 25 |
+
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
|
| 26 |
+
gopkg.in/yaml.v2 v2.4.0 // indirect
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
require (
|
| 30 |
+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
| 31 |
+
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
| 32 |
+
github.com/go-viper/mapstructure/v2 v2.3.0
|
| 33 |
+
github.com/google/go-github/v71 v71.0.0 // indirect
|
| 34 |
+
github.com/google/go-querystring v1.1.0 // indirect
|
| 35 |
+
github.com/google/uuid v1.6.0 // indirect
|
| 36 |
+
github.com/gorilla/mux v1.8.0 // indirect
|
| 37 |
+
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
| 38 |
+
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
| 39 |
+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
| 40 |
+
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
| 41 |
+
github.com/sagikazarmark/locafero v0.9.0 // indirect
|
| 42 |
+
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7
|
| 43 |
+
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466
|
| 44 |
+
github.com/sourcegraph/conc v0.3.0 // indirect
|
| 45 |
+
github.com/spf13/afero v1.14.0 // indirect
|
| 46 |
+
github.com/spf13/cast v1.7.1 // indirect
|
| 47 |
+
github.com/spf13/pflag v1.0.6
|
| 48 |
+
github.com/subosito/gotenv v1.6.0 // indirect
|
| 49 |
+
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
| 50 |
+
go.uber.org/multierr v1.11.0 // indirect
|
| 51 |
+
golang.org/x/oauth2 v0.29.0 // indirect
|
| 52 |
+
golang.org/x/sys v0.31.0 // indirect
|
| 53 |
+
golang.org/x/text v0.23.0 // indirect
|
| 54 |
+
golang.org/x/time v0.5.0 // indirect
|
| 55 |
+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
| 56 |
+
gopkg.in/yaml.v3 v3.0.1 // indirect
|
| 57 |
+
)
|
go.sum
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
| 2 |
+
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
| 3 |
+
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
| 4 |
+
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
| 5 |
+
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
| 6 |
+
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
| 7 |
+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
| 8 |
+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
| 9 |
+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
| 10 |
+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
| 11 |
+
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
| 12 |
+
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
| 13 |
+
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
| 14 |
+
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
| 15 |
+
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
| 16 |
+
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
| 17 |
+
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
| 18 |
+
github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrKU=
|
| 19 |
+
github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
| 20 |
+
github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk=
|
| 21 |
+
github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
| 22 |
+
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
| 23 |
+
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
| 24 |
+
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
| 25 |
+
github.com/google/go-github/v71 v71.0.0 h1:Zi16OymGKZZMm8ZliffVVJ/Q9YZreDKONCr+WUd0Z30=
|
| 26 |
+
github.com/google/go-github/v71 v71.0.0/go.mod h1:URZXObp2BLlMjwu0O8g4y6VBneUj2bCHgnI8FfgZ51M=
|
| 27 |
+
github.com/google/go-github/v74 v74.0.0 h1:yZcddTUn8DPbj11GxnMrNiAnXH14gNs559AsUpNpPgM=
|
| 28 |
+
github.com/google/go-github/v74 v74.0.0/go.mod h1:ubn/YdyftV80VPSI26nSJvaEsTOnsjrxG3o9kJhcyak=
|
| 29 |
+
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
| 30 |
+
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
| 31 |
+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
| 32 |
+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
| 33 |
+
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
| 34 |
+
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
| 35 |
+
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
| 36 |
+
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
| 37 |
+
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
|
| 38 |
+
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
| 39 |
+
github.com/josephburnett/jd v1.9.2 h1:ECJRRFXCCqbtidkAHckHGSZm/JIaAxS1gygHLF8MI5Y=
|
| 40 |
+
github.com/josephburnett/jd v1.9.2/go.mod h1:bImDr8QXpxMb3SD+w1cDRHp97xP6UwI88xUAuxwDQfM=
|
| 41 |
+
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
| 42 |
+
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
| 43 |
+
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
| 44 |
+
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
| 45 |
+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
| 46 |
+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
| 47 |
+
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
| 48 |
+
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
| 49 |
+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
| 50 |
+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
| 51 |
+
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
| 52 |
+
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
| 53 |
+
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
| 54 |
+
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
| 55 |
+
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
| 56 |
+
github.com/mark3labs/mcp-go v0.36.0 h1:rIZaijrRYPeSbJG8/qNDe0hWlGrCJ7FWHNMz2SQpTis=
|
| 57 |
+
github.com/mark3labs/mcp-go v0.36.0/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g=
|
| 58 |
+
github.com/migueleliasweb/go-github-mock v1.3.0 h1:2sVP9JEMB2ubQw1IKto3/fzF51oFC6eVWOOFDgQoq88=
|
| 59 |
+
github.com/migueleliasweb/go-github-mock v1.3.0/go.mod h1:ipQhV8fTcj/G6m7BKzin08GaJ/3B5/SonRAkgrk0zCY=
|
| 60 |
+
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
| 61 |
+
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
| 62 |
+
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
| 63 |
+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
| 64 |
+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
| 65 |
+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
| 66 |
+
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
| 67 |
+
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
| 68 |
+
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
| 69 |
+
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
|
| 70 |
+
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
|
| 71 |
+
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7 h1:cYCy18SHPKRkvclm+pWm1Lk4YrREb4IOIb/YdFO0p2M=
|
| 72 |
+
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8=
|
| 73 |
+
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0=
|
| 74 |
+
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE=
|
| 75 |
+
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
| 76 |
+
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
| 77 |
+
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
|
| 78 |
+
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
|
| 79 |
+
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
| 80 |
+
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
| 81 |
+
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
| 82 |
+
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
| 83 |
+
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
| 84 |
+
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
| 85 |
+
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
| 86 |
+
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
| 87 |
+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
| 88 |
+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
| 89 |
+
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
| 90 |
+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
| 91 |
+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
| 92 |
+
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
| 93 |
+
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
| 94 |
+
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
| 95 |
+
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
| 96 |
+
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
| 97 |
+
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
| 98 |
+
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
|
| 99 |
+
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
|
| 100 |
+
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
| 101 |
+
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
| 102 |
+
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
| 103 |
+
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
| 104 |
+
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
|
| 105 |
+
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
| 106 |
+
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
| 107 |
+
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
| 108 |
+
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
| 109 |
+
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
| 110 |
+
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
| 111 |
+
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
| 112 |
+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
| 113 |
+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
| 114 |
+
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
| 115 |
+
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
| 116 |
+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
| 117 |
+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
| 118 |
+
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
| 119 |
+
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
| 120 |
+
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
| 121 |
+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
| 122 |
+
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
| 123 |
+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
| 124 |
+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
internal/ghmcp/server.go
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package ghmcp
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"fmt"
|
| 6 |
+
"io"
|
| 7 |
+
"log"
|
| 8 |
+
"log/slog"
|
| 9 |
+
"net/http"
|
| 10 |
+
"net/url"
|
| 11 |
+
"os"
|
| 12 |
+
"os/signal"
|
| 13 |
+
"strings"
|
| 14 |
+
"syscall"
|
| 15 |
+
|
| 16 |
+
"github.com/github/github-mcp-server/pkg/errors"
|
| 17 |
+
"github.com/github/github-mcp-server/pkg/github"
|
| 18 |
+
mcplog "github.com/github/github-mcp-server/pkg/log"
|
| 19 |
+
"github.com/github/github-mcp-server/pkg/raw"
|
| 20 |
+
"github.com/github/github-mcp-server/pkg/translations"
|
| 21 |
+
gogithub "github.com/google/go-github/v74/github"
|
| 22 |
+
"github.com/mark3labs/mcp-go/mcp"
|
| 23 |
+
"github.com/mark3labs/mcp-go/server"
|
| 24 |
+
"github.com/shurcooL/githubv4"
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
type MCPServerConfig struct {
|
| 28 |
+
// Version of the server
|
| 29 |
+
Version string
|
| 30 |
+
|
| 31 |
+
// GitHub Host to target for API requests (e.g. github.com or github.enterprise.com)
|
| 32 |
+
Host string
|
| 33 |
+
|
| 34 |
+
// GitHub Token to authenticate with the GitHub API
|
| 35 |
+
Token string
|
| 36 |
+
|
| 37 |
+
// EnabledToolsets is a list of toolsets to enable
|
| 38 |
+
// See: https://github.com/github/github-mcp-server?tab=readme-ov-file#tool-configuration
|
| 39 |
+
EnabledToolsets []string
|
| 40 |
+
|
| 41 |
+
// Whether to enable dynamic toolsets
|
| 42 |
+
// See: https://github.com/github/github-mcp-server?tab=readme-ov-file#dynamic-tool-discovery
|
| 43 |
+
DynamicToolsets bool
|
| 44 |
+
|
| 45 |
+
// ReadOnly indicates if we should only offer read-only tools
|
| 46 |
+
ReadOnly bool
|
| 47 |
+
|
| 48 |
+
// Translator provides translated text for the server tooling
|
| 49 |
+
Translator translations.TranslationHelperFunc
|
| 50 |
+
|
| 51 |
+
// Content window size
|
| 52 |
+
ContentWindowSize int
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
const stdioServerLogPrefix = "stdioserver"
|
| 56 |
+
|
| 57 |
+
func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) {
|
| 58 |
+
apiHost, err := parseAPIHost(cfg.Host)
|
| 59 |
+
if err != nil {
|
| 60 |
+
return nil, fmt.Errorf("failed to parse API host: %w", err)
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// Construct our REST client
|
| 64 |
+
restClient := gogithub.NewClient(nil).WithAuthToken(cfg.Token)
|
| 65 |
+
restClient.UserAgent = fmt.Sprintf("github-mcp-server/%s", cfg.Version)
|
| 66 |
+
restClient.BaseURL = apiHost.baseRESTURL
|
| 67 |
+
restClient.UploadURL = apiHost.uploadURL
|
| 68 |
+
|
| 69 |
+
// Construct our GraphQL client
|
| 70 |
+
// We're using NewEnterpriseClient here unconditionally as opposed to NewClient because we already
|
| 71 |
+
// did the necessary API host parsing so that github.com will return the correct URL anyway.
|
| 72 |
+
gqlHTTPClient := &http.Client{
|
| 73 |
+
Transport: &bearerAuthTransport{
|
| 74 |
+
transport: http.DefaultTransport,
|
| 75 |
+
token: cfg.Token,
|
| 76 |
+
},
|
| 77 |
+
} // We're going to wrap the Transport later in beforeInit
|
| 78 |
+
gqlClient := githubv4.NewEnterpriseClient(apiHost.graphqlURL.String(), gqlHTTPClient)
|
| 79 |
+
|
| 80 |
+
// When a client send an initialize request, update the user agent to include the client info.
|
| 81 |
+
beforeInit := func(_ context.Context, _ any, message *mcp.InitializeRequest) {
|
| 82 |
+
userAgent := fmt.Sprintf(
|
| 83 |
+
"github-mcp-server/%s (%s/%s)",
|
| 84 |
+
cfg.Version,
|
| 85 |
+
message.Params.ClientInfo.Name,
|
| 86 |
+
message.Params.ClientInfo.Version,
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
restClient.UserAgent = userAgent
|
| 90 |
+
|
| 91 |
+
gqlHTTPClient.Transport = &userAgentTransport{
|
| 92 |
+
transport: gqlHTTPClient.Transport,
|
| 93 |
+
agent: userAgent,
|
| 94 |
+
}
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
hooks := &server.Hooks{
|
| 98 |
+
OnBeforeInitialize: []server.OnBeforeInitializeFunc{beforeInit},
|
| 99 |
+
OnBeforeAny: []server.BeforeAnyHookFunc{
|
| 100 |
+
func(ctx context.Context, _ any, _ mcp.MCPMethod, _ any) {
|
| 101 |
+
// Ensure the context is cleared of any previous errors
|
| 102 |
+
// as context isn't propagated through middleware
|
| 103 |
+
errors.ContextWithGitHubErrors(ctx)
|
| 104 |
+
},
|
| 105 |
+
},
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
ghServer := github.NewServer(cfg.Version, server.WithHooks(hooks))
|
| 109 |
+
|
| 110 |
+
enabledToolsets := cfg.EnabledToolsets
|
| 111 |
+
if cfg.DynamicToolsets {
|
| 112 |
+
// filter "all" from the enabled toolsets
|
| 113 |
+
enabledToolsets = make([]string, 0, len(cfg.EnabledToolsets))
|
| 114 |
+
for _, toolset := range cfg.EnabledToolsets {
|
| 115 |
+
if toolset != "all" {
|
| 116 |
+
enabledToolsets = append(enabledToolsets, toolset)
|
| 117 |
+
}
|
| 118 |
+
}
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
getClient := func(_ context.Context) (*gogithub.Client, error) {
|
| 122 |
+
return restClient, nil // closing over client
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
getGQLClient := func(_ context.Context) (*githubv4.Client, error) {
|
| 126 |
+
return gqlClient, nil // closing over client
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
getRawClient := func(ctx context.Context) (*raw.Client, error) {
|
| 130 |
+
client, err := getClient(ctx)
|
| 131 |
+
if err != nil {
|
| 132 |
+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
|
| 133 |
+
}
|
| 134 |
+
return raw.NewClient(client, apiHost.rawURL), nil // closing over client
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
// Create default toolsets
|
| 138 |
+
tsg := github.DefaultToolsetGroup(cfg.ReadOnly, getClient, getGQLClient, getRawClient, cfg.Translator, cfg.ContentWindowSize)
|
| 139 |
+
err = tsg.EnableToolsets(enabledToolsets)
|
| 140 |
+
|
| 141 |
+
if err != nil {
|
| 142 |
+
return nil, fmt.Errorf("failed to enable toolsets: %w", err)
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
// Register all mcp functionality with the server
|
| 146 |
+
tsg.RegisterAll(ghServer)
|
| 147 |
+
|
| 148 |
+
if cfg.DynamicToolsets {
|
| 149 |
+
dynamic := github.InitDynamicToolset(ghServer, tsg, cfg.Translator)
|
| 150 |
+
dynamic.RegisterTools(ghServer)
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
return ghServer, nil
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
type StdioServerConfig struct {
|
| 157 |
+
// Version of the server
|
| 158 |
+
Version string
|
| 159 |
+
|
| 160 |
+
// GitHub Host to target for API requests (e.g. github.com or github.enterprise.com)
|
| 161 |
+
Host string
|
| 162 |
+
|
| 163 |
+
// GitHub Token to authenticate with the GitHub API
|
| 164 |
+
Token string
|
| 165 |
+
|
| 166 |
+
// EnabledToolsets is a list of toolsets to enable
|
| 167 |
+
// See: https://github.com/github/github-mcp-server?tab=readme-ov-file#tool-configuration
|
| 168 |
+
EnabledToolsets []string
|
| 169 |
+
|
| 170 |
+
// Whether to enable dynamic toolsets
|
| 171 |
+
// See: https://github.com/github/github-mcp-server?tab=readme-ov-file#dynamic-tool-discovery
|
| 172 |
+
DynamicToolsets bool
|
| 173 |
+
|
| 174 |
+
// ReadOnly indicates if we should only register read-only tools
|
| 175 |
+
ReadOnly bool
|
| 176 |
+
|
| 177 |
+
// ExportTranslations indicates if we should export translations
|
| 178 |
+
// See: https://github.com/github/github-mcp-server?tab=readme-ov-file#i18n--overriding-descriptions
|
| 179 |
+
ExportTranslations bool
|
| 180 |
+
|
| 181 |
+
// EnableCommandLogging indicates if we should log commands
|
| 182 |
+
EnableCommandLogging bool
|
| 183 |
+
|
| 184 |
+
// Path to the log file if not stderr
|
| 185 |
+
LogFilePath string
|
| 186 |
+
|
| 187 |
+
// Content window size
|
| 188 |
+
ContentWindowSize int
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
// RunStdioServer is not concurrent safe.
|
| 192 |
+
func RunStdioServer(cfg StdioServerConfig) error {
|
| 193 |
+
// Create app context
|
| 194 |
+
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
| 195 |
+
defer stop()
|
| 196 |
+
|
| 197 |
+
t, dumpTranslations := translations.TranslationHelper()
|
| 198 |
+
|
| 199 |
+
ghServer, err := NewMCPServer(MCPServerConfig{
|
| 200 |
+
Version: cfg.Version,
|
| 201 |
+
Host: cfg.Host,
|
| 202 |
+
Token: cfg.Token,
|
| 203 |
+
EnabledToolsets: cfg.EnabledToolsets,
|
| 204 |
+
DynamicToolsets: cfg.DynamicToolsets,
|
| 205 |
+
ReadOnly: cfg.ReadOnly,
|
| 206 |
+
Translator: t,
|
| 207 |
+
ContentWindowSize: cfg.ContentWindowSize,
|
| 208 |
+
})
|
| 209 |
+
if err != nil {
|
| 210 |
+
return fmt.Errorf("failed to create MCP server: %w", err)
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
stdioServer := server.NewStdioServer(ghServer)
|
| 214 |
+
|
| 215 |
+
var slogHandler slog.Handler
|
| 216 |
+
var logOutput io.Writer
|
| 217 |
+
if cfg.LogFilePath != "" {
|
| 218 |
+
file, err := os.OpenFile(cfg.LogFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
|
| 219 |
+
if err != nil {
|
| 220 |
+
return fmt.Errorf("failed to open log file: %w", err)
|
| 221 |
+
}
|
| 222 |
+
logOutput = file
|
| 223 |
+
slogHandler = slog.NewTextHandler(logOutput, &slog.HandlerOptions{Level: slog.LevelDebug})
|
| 224 |
+
} else {
|
| 225 |
+
logOutput = os.Stderr
|
| 226 |
+
slogHandler = slog.NewTextHandler(logOutput, &slog.HandlerOptions{Level: slog.LevelInfo})
|
| 227 |
+
}
|
| 228 |
+
logger := slog.New(slogHandler)
|
| 229 |
+
logger.Info("starting server", "version", cfg.Version, "host", cfg.Host, "dynamicToolsets", cfg.DynamicToolsets, "readOnly", cfg.ReadOnly)
|
| 230 |
+
stdLogger := log.New(logOutput, stdioServerLogPrefix, 0)
|
| 231 |
+
stdioServer.SetErrorLogger(stdLogger)
|
| 232 |
+
|
| 233 |
+
if cfg.ExportTranslations {
|
| 234 |
+
// Once server is initialized, all translations are loaded
|
| 235 |
+
dumpTranslations()
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
// Start listening for messages
|
| 239 |
+
errC := make(chan error, 1)
|
| 240 |
+
go func() {
|
| 241 |
+
in, out := io.Reader(os.Stdin), io.Writer(os.Stdout)
|
| 242 |
+
|
| 243 |
+
if cfg.EnableCommandLogging {
|
| 244 |
+
loggedIO := mcplog.NewIOLogger(in, out, logger)
|
| 245 |
+
in, out = loggedIO, loggedIO
|
| 246 |
+
}
|
| 247 |
+
// enable GitHub errors in the context
|
| 248 |
+
ctx := errors.ContextWithGitHubErrors(ctx)
|
| 249 |
+
errC <- stdioServer.Listen(ctx, in, out)
|
| 250 |
+
}()
|
| 251 |
+
|
| 252 |
+
// Output github-mcp-server string
|
| 253 |
+
_, _ = fmt.Fprintf(os.Stderr, "GitHub MCP Server running on stdio\n")
|
| 254 |
+
|
| 255 |
+
// Wait for shutdown signal
|
| 256 |
+
select {
|
| 257 |
+
case <-ctx.Done():
|
| 258 |
+
logger.Info("shutting down server", "signal", "context done")
|
| 259 |
+
case err := <-errC:
|
| 260 |
+
if err != nil {
|
| 261 |
+
logger.Error("error running server", "error", err)
|
| 262 |
+
return fmt.Errorf("error running server: %w", err)
|
| 263 |
+
}
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
return nil
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
type apiHost struct {
|
| 270 |
+
baseRESTURL *url.URL
|
| 271 |
+
graphqlURL *url.URL
|
| 272 |
+
uploadURL *url.URL
|
| 273 |
+
rawURL *url.URL
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
func newDotcomHost() (apiHost, error) {
|
| 277 |
+
baseRestURL, err := url.Parse("https://api.github.com/")
|
| 278 |
+
if err != nil {
|
| 279 |
+
return apiHost{}, fmt.Errorf("failed to parse dotcom REST URL: %w", err)
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
gqlURL, err := url.Parse("https://api.github.com/graphql")
|
| 283 |
+
if err != nil {
|
| 284 |
+
return apiHost{}, fmt.Errorf("failed to parse dotcom GraphQL URL: %w", err)
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
uploadURL, err := url.Parse("https://uploads.github.com")
|
| 288 |
+
if err != nil {
|
| 289 |
+
return apiHost{}, fmt.Errorf("failed to parse dotcom Upload URL: %w", err)
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
rawURL, err := url.Parse("https://raw.githubusercontent.com/")
|
| 293 |
+
if err != nil {
|
| 294 |
+
return apiHost{}, fmt.Errorf("failed to parse dotcom Raw URL: %w", err)
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
return apiHost{
|
| 298 |
+
baseRESTURL: baseRestURL,
|
| 299 |
+
graphqlURL: gqlURL,
|
| 300 |
+
uploadURL: uploadURL,
|
| 301 |
+
rawURL: rawURL,
|
| 302 |
+
}, nil
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
func newGHECHost(hostname string) (apiHost, error) {
|
| 306 |
+
u, err := url.Parse(hostname)
|
| 307 |
+
if err != nil {
|
| 308 |
+
return apiHost{}, fmt.Errorf("failed to parse GHEC URL: %w", err)
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
// Unsecured GHEC would be an error
|
| 312 |
+
if u.Scheme == "http" {
|
| 313 |
+
return apiHost{}, fmt.Errorf("GHEC URL must be HTTPS")
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
restURL, err := url.Parse(fmt.Sprintf("https://api.%s/", u.Hostname()))
|
| 317 |
+
if err != nil {
|
| 318 |
+
return apiHost{}, fmt.Errorf("failed to parse GHEC REST URL: %w", err)
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
gqlURL, err := url.Parse(fmt.Sprintf("https://api.%s/graphql", u.Hostname()))
|
| 322 |
+
if err != nil {
|
| 323 |
+
return apiHost{}, fmt.Errorf("failed to parse GHEC GraphQL URL: %w", err)
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
uploadURL, err := url.Parse(fmt.Sprintf("https://uploads.%s", u.Hostname()))
|
| 327 |
+
if err != nil {
|
| 328 |
+
return apiHost{}, fmt.Errorf("failed to parse GHEC Upload URL: %w", err)
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
rawURL, err := url.Parse(fmt.Sprintf("https://raw.%s/", u.Hostname()))
|
| 332 |
+
if err != nil {
|
| 333 |
+
return apiHost{}, fmt.Errorf("failed to parse GHEC Raw URL: %w", err)
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
return apiHost{
|
| 337 |
+
baseRESTURL: restURL,
|
| 338 |
+
graphqlURL: gqlURL,
|
| 339 |
+
uploadURL: uploadURL,
|
| 340 |
+
rawURL: rawURL,
|
| 341 |
+
}, nil
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
func newGHESHost(hostname string) (apiHost, error) {
|
| 345 |
+
u, err := url.Parse(hostname)
|
| 346 |
+
if err != nil {
|
| 347 |
+
return apiHost{}, fmt.Errorf("failed to parse GHES URL: %w", err)
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
restURL, err := url.Parse(fmt.Sprintf("%s://%s/api/v3/", u.Scheme, u.Hostname()))
|
| 351 |
+
if err != nil {
|
| 352 |
+
return apiHost{}, fmt.Errorf("failed to parse GHES REST URL: %w", err)
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
gqlURL, err := url.Parse(fmt.Sprintf("%s://%s/api/graphql", u.Scheme, u.Hostname()))
|
| 356 |
+
if err != nil {
|
| 357 |
+
return apiHost{}, fmt.Errorf("failed to parse GHES GraphQL URL: %w", err)
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
uploadURL, err := url.Parse(fmt.Sprintf("%s://%s/api/uploads/", u.Scheme, u.Hostname()))
|
| 361 |
+
if err != nil {
|
| 362 |
+
return apiHost{}, fmt.Errorf("failed to parse GHES Upload URL: %w", err)
|
| 363 |
+
}
|
| 364 |
+
rawURL, err := url.Parse(fmt.Sprintf("%s://%s/raw/", u.Scheme, u.Hostname()))
|
| 365 |
+
if err != nil {
|
| 366 |
+
return apiHost{}, fmt.Errorf("failed to parse GHES Raw URL: %w", err)
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
return apiHost{
|
| 370 |
+
baseRESTURL: restURL,
|
| 371 |
+
graphqlURL: gqlURL,
|
| 372 |
+
uploadURL: uploadURL,
|
| 373 |
+
rawURL: rawURL,
|
| 374 |
+
}, nil
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
// Note that this does not handle ports yet, so development environments are out.
|
| 378 |
+
func parseAPIHost(s string) (apiHost, error) {
|
| 379 |
+
if s == "" {
|
| 380 |
+
return newDotcomHost()
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
u, err := url.Parse(s)
|
| 384 |
+
if err != nil {
|
| 385 |
+
return apiHost{}, fmt.Errorf("could not parse host as URL: %s", s)
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
if u.Scheme == "" {
|
| 389 |
+
return apiHost{}, fmt.Errorf("host must have a scheme (http or https): %s", s)
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
if strings.HasSuffix(u.Hostname(), "github.com") {
|
| 393 |
+
return newDotcomHost()
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
if strings.HasSuffix(u.Hostname(), "ghe.com") {
|
| 397 |
+
return newGHECHost(s)
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
return newGHESHost(s)
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
type userAgentTransport struct {
|
| 404 |
+
transport http.RoundTripper
|
| 405 |
+
agent string
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
func (t *userAgentTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
| 409 |
+
req = req.Clone(req.Context())
|
| 410 |
+
req.Header.Set("User-Agent", t.agent)
|
| 411 |
+
return t.transport.RoundTrip(req)
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
type bearerAuthTransport struct {
|
| 415 |
+
transport http.RoundTripper
|
| 416 |
+
token string
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
func (t *bearerAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
| 420 |
+
req = req.Clone(req.Context())
|
| 421 |
+
req.Header.Set("Authorization", "Bearer "+t.token)
|
| 422 |
+
return t.transport.RoundTrip(req)
|
| 423 |
+
}
|
internal/githubv4mock/githubv4mock.go
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// githubv4mock package provides a mock GraphQL server used for testing queries produced via
|
| 2 |
+
// shurcooL/githubv4 or shurcooL/graphql modules.
|
| 3 |
+
package githubv4mock
|
| 4 |
+
|
| 5 |
+
import (
|
| 6 |
+
"encoding/json"
|
| 7 |
+
"fmt"
|
| 8 |
+
"io"
|
| 9 |
+
"net/http"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
type Matcher struct {
|
| 13 |
+
Request string
|
| 14 |
+
Variables map[string]any
|
| 15 |
+
|
| 16 |
+
Response GQLResponse
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
// NewQueryMatcher constructs a new matcher for the provided query and variables.
|
| 20 |
+
// If the provided query is a string, it will be used-as-is, otherwise it will be
|
| 21 |
+
// converted to a string using the constructQuery function taken from shurcooL/graphql.
|
| 22 |
+
func NewQueryMatcher(query any, variables map[string]any, response GQLResponse) Matcher {
|
| 23 |
+
queryString, ok := query.(string)
|
| 24 |
+
if !ok {
|
| 25 |
+
queryString = constructQuery(query, variables)
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
return Matcher{
|
| 29 |
+
Request: queryString,
|
| 30 |
+
Variables: variables,
|
| 31 |
+
Response: response,
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// NewMutationMatcher constructs a new matcher for the provided mutation and variables.
|
| 36 |
+
// If the provided mutation is a string, it will be used-as-is, otherwise it will be
|
| 37 |
+
// converted to a string using the constructMutation function taken from shurcooL/graphql.
|
| 38 |
+
//
|
| 39 |
+
// The input parameter is a special form of variable, matching the usage in shurcooL/githubv4. It will be added
|
| 40 |
+
// to the query as a variable called `input`. Furthermore, it will be converted to a map[string]any
|
| 41 |
+
// to be used for later equality comparison, as when the http handler is called, the request body will no longer
|
| 42 |
+
// contain the input struct type information.
|
| 43 |
+
func NewMutationMatcher(mutation any, input any, variables map[string]any, response GQLResponse) Matcher {
|
| 44 |
+
mutationString, ok := mutation.(string)
|
| 45 |
+
if !ok {
|
| 46 |
+
// Matching shurcooL/githubv4 mutation behaviour found in https://github.com/shurcooL/githubv4/blob/48295856cce734663ddbd790ff54800f784f3193/githubv4.go#L45-L56
|
| 47 |
+
if variables == nil {
|
| 48 |
+
variables = map[string]any{"input": input}
|
| 49 |
+
} else {
|
| 50 |
+
variables["input"] = input
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
mutationString = constructMutation(mutation, variables)
|
| 54 |
+
m, _ := githubv4InputStructToMap(input)
|
| 55 |
+
variables["input"] = m
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
return Matcher{
|
| 59 |
+
Request: mutationString,
|
| 60 |
+
Variables: variables,
|
| 61 |
+
Response: response,
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
type GQLResponse struct {
|
| 66 |
+
Data map[string]any `json:"data"`
|
| 67 |
+
Errors []struct {
|
| 68 |
+
Message string `json:"message"`
|
| 69 |
+
} `json:"errors,omitempty"`
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
// DataResponse is the happy path response constructor for a mocked GraphQL request.
|
| 73 |
+
func DataResponse(data map[string]any) GQLResponse {
|
| 74 |
+
return GQLResponse{
|
| 75 |
+
Data: data,
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
// ErrorResponse is the unhappy path response constructor for a mocked GraphQL request.\
|
| 80 |
+
// Note that for the moment it is only possible to return a single error message.
|
| 81 |
+
func ErrorResponse(errorMsg string) GQLResponse {
|
| 82 |
+
return GQLResponse{
|
| 83 |
+
Errors: []struct {
|
| 84 |
+
Message string `json:"message"`
|
| 85 |
+
}{
|
| 86 |
+
{
|
| 87 |
+
Message: errorMsg,
|
| 88 |
+
},
|
| 89 |
+
},
|
| 90 |
+
}
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
// githubv4InputStructToMap converts a struct to a map[string]any, it uses JSON marshalling rather than reflection
|
| 94 |
+
// to do so, because the json struct tags are used in the real implementation to produce the variable key names,
|
| 95 |
+
// and we need to ensure that when variable matching occurs in the http handler, the keys correctly match.
|
| 96 |
+
func githubv4InputStructToMap(s any) (map[string]any, error) {
|
| 97 |
+
jsonBytes, err := json.Marshal(s)
|
| 98 |
+
if err != nil {
|
| 99 |
+
return nil, err
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
var result map[string]any
|
| 103 |
+
err = json.Unmarshal(jsonBytes, &result)
|
| 104 |
+
return result, err
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
// NewMockedHTTPClient creates a new HTTP client that registers a handler for /graphql POST requests.
|
| 108 |
+
// For each request, an attempt will be be made to match the request body against the provided matchers.
|
| 109 |
+
// If a match is found, the corresponding response will be returned with StatusOK.
|
| 110 |
+
//
|
| 111 |
+
// Note that query and variable matching can be slightly fickle. The client expects an EXACT match on the query,
|
| 112 |
+
// which in most cases will have been constructed from a type with graphql tags. The query construction code in
|
| 113 |
+
// shurcooL/githubv4 uses the field types to derive the query string, thus a go string is not the same as a graphql.ID,
|
| 114 |
+
// even though `type ID string`. It is therefore expected that matching variables have the right type for example:
|
| 115 |
+
//
|
| 116 |
+
// githubv4mock.NewQueryMatcher(
|
| 117 |
+
// struct {
|
| 118 |
+
// Repository struct {
|
| 119 |
+
// PullRequest struct {
|
| 120 |
+
// ID githubv4.ID
|
| 121 |
+
// } `graphql:"pullRequest(number: $prNum)"`
|
| 122 |
+
// } `graphql:"repository(owner: $owner, name: $repo)"`
|
| 123 |
+
// }{},
|
| 124 |
+
// map[string]any{
|
| 125 |
+
// "owner": githubv4.String("owner"),
|
| 126 |
+
// "repo": githubv4.String("repo"),
|
| 127 |
+
// "prNum": githubv4.Int(42),
|
| 128 |
+
// },
|
| 129 |
+
// githubv4mock.DataResponse(
|
| 130 |
+
// map[string]any{
|
| 131 |
+
// "repository": map[string]any{
|
| 132 |
+
// "pullRequest": map[string]any{
|
| 133 |
+
// "id": "PR_kwDODKw3uc6WYN1T",
|
| 134 |
+
// },
|
| 135 |
+
// },
|
| 136 |
+
// },
|
| 137 |
+
// ),
|
| 138 |
+
// )
|
| 139 |
+
//
|
| 140 |
+
// To aid in variable equality checks, values are considered equal if they approximate to the same type. This is
|
| 141 |
+
// required because when the http handler is called, the request body no longer has the type information. This manifests
|
| 142 |
+
// particularly when using the githubv4.Input types which have type deffed fields in their structs. For example:
|
| 143 |
+
//
|
| 144 |
+
// type CloseIssueInput struct {
|
| 145 |
+
// IssueID ID `json:"issueId"`
|
| 146 |
+
// StateReason *IssueClosedStateReason `json:"stateReason,omitempty"`
|
| 147 |
+
// }
|
| 148 |
+
//
|
| 149 |
+
// This client does not currently provide a mechanism for out-of-band errors e.g. returning a 500,
|
| 150 |
+
// and errors are constrained to GQL errors returned in the response body with a 200 status code.
|
| 151 |
+
func NewMockedHTTPClient(ms ...Matcher) *http.Client {
|
| 152 |
+
matchers := make(map[string]Matcher, len(ms))
|
| 153 |
+
for _, m := range ms {
|
| 154 |
+
matchers[m.Request] = m
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
mux := http.NewServeMux()
|
| 158 |
+
mux.HandleFunc("/graphql", func(w http.ResponseWriter, r *http.Request) {
|
| 159 |
+
if r.Method != http.MethodPost {
|
| 160 |
+
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
| 161 |
+
return
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
gqlRequest, err := parseBody(r.Body)
|
| 165 |
+
if err != nil {
|
| 166 |
+
http.Error(w, "invalid request body", http.StatusBadRequest)
|
| 167 |
+
return
|
| 168 |
+
}
|
| 169 |
+
defer func() { _ = r.Body.Close() }()
|
| 170 |
+
|
| 171 |
+
matcher, ok := matchers[gqlRequest.Query]
|
| 172 |
+
if !ok {
|
| 173 |
+
http.Error(w, fmt.Sprintf("no matcher found for query %s", gqlRequest.Query), http.StatusNotFound)
|
| 174 |
+
return
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
if len(gqlRequest.Variables) > 0 {
|
| 178 |
+
if len(gqlRequest.Variables) != len(matcher.Variables) {
|
| 179 |
+
http.Error(w, "variables do not have the same length", http.StatusBadRequest)
|
| 180 |
+
return
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
for k, v := range matcher.Variables {
|
| 184 |
+
if !objectsAreEqualValues(v, gqlRequest.Variables[k]) {
|
| 185 |
+
http.Error(w, "variable does not match", http.StatusBadRequest)
|
| 186 |
+
return
|
| 187 |
+
}
|
| 188 |
+
}
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
responseBody, err := json.Marshal(matcher.Response)
|
| 192 |
+
if err != nil {
|
| 193 |
+
http.Error(w, "error marshalling response", http.StatusInternalServerError)
|
| 194 |
+
return
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
w.Header().Set("Content-Type", "application/json")
|
| 198 |
+
w.WriteHeader(http.StatusOK)
|
| 199 |
+
_, _ = w.Write(responseBody)
|
| 200 |
+
})
|
| 201 |
+
|
| 202 |
+
return &http.Client{Transport: &localRoundTripper{
|
| 203 |
+
handler: mux,
|
| 204 |
+
}}
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
type gqlRequest struct {
|
| 208 |
+
Query string `json:"query"`
|
| 209 |
+
Variables map[string]any `json:"variables,omitempty"`
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
func parseBody(r io.Reader) (gqlRequest, error) {
|
| 213 |
+
var req gqlRequest
|
| 214 |
+
err := json.NewDecoder(r).Decode(&req)
|
| 215 |
+
return req, err
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
func Ptr[T any](v T) *T { return &v }
|
internal/githubv4mock/local_round_tripper.go
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Ths contents of this file are taken from https://github.com/shurcooL/graphql/blob/ed46e5a4646634fc16cb07c3b8db389542cc8847/graphql_test.go#L155-L165
|
| 2 |
+
// because they are not exported by the module, and we would like to use them in building the githubv4mock test utility.
|
| 3 |
+
//
|
| 4 |
+
// The original license, copied from https://github.com/shurcooL/graphql/blob/ed46e5a4646634fc16cb07c3b8db389542cc8847/LICENSE
|
| 5 |
+
//
|
| 6 |
+
// MIT License
|
| 7 |
+
|
| 8 |
+
// Copyright (c) 2017 Dmitri Shuralyov
|
| 9 |
+
|
| 10 |
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 11 |
+
// of this software and associated documentation files (the "Software"), to deal
|
| 12 |
+
// in the Software without restriction, including without limitation the rights
|
| 13 |
+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 14 |
+
// copies of the Software, and to permit persons to whom the Software is
|
| 15 |
+
// furnished to do so, subject to the following conditions:
|
| 16 |
+
|
| 17 |
+
// The above copyright notice and this permission notice shall be included in all
|
| 18 |
+
// copies or substantial portions of the Software.
|
| 19 |
+
|
| 20 |
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 21 |
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 22 |
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 23 |
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 24 |
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 25 |
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 26 |
+
// SOFTWARE.
|
| 27 |
+
package githubv4mock
|
| 28 |
+
|
| 29 |
+
import (
|
| 30 |
+
"net/http"
|
| 31 |
+
"net/http/httptest"
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
// localRoundTripper is an http.RoundTripper that executes HTTP transactions
|
| 35 |
+
// by using handler directly, instead of going over an HTTP connection.
|
| 36 |
+
type localRoundTripper struct {
|
| 37 |
+
handler http.Handler
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
func (l localRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
| 41 |
+
w := httptest.NewRecorder()
|
| 42 |
+
l.handler.ServeHTTP(w, req)
|
| 43 |
+
return w.Result(), nil
|
| 44 |
+
}
|
internal/githubv4mock/objects_are_equal_values.go
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// The contents of this file are taken from https://github.com/stretchr/testify/blob/016e2e9c269209287f33ec203f340a9a723fe22c/assert/assertions.go#L166
|
| 2 |
+
// because I do not want to take a dependency on the entire testify module just to use this equality check.
|
| 3 |
+
//
|
| 4 |
+
// There is a modification in objectsAreEqual to check that typed nils are equal, even if their types are different.
|
| 5 |
+
//
|
| 6 |
+
// The original license, copied from https://github.com/stretchr/testify/blob/016e2e9c269209287f33ec203f340a9a723fe22c/LICENSE
|
| 7 |
+
//
|
| 8 |
+
// MIT License
|
| 9 |
+
//
|
| 10 |
+
// Copyright (c) 2012-2020 Mat Ryer, Tyler Bunnell and contributors.
|
| 11 |
+
|
| 12 |
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 13 |
+
// of this software and associated documentation files (the "Software"), to deal
|
| 14 |
+
// in the Software without restriction, including without limitation the rights
|
| 15 |
+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 16 |
+
// copies of the Software, and to permit persons to whom the Software is
|
| 17 |
+
// furnished to do so, subject to the following conditions:
|
| 18 |
+
|
| 19 |
+
// The above copyright notice and this permission notice shall be included in all
|
| 20 |
+
// copies or substantial portions of the Software.
|
| 21 |
+
|
| 22 |
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 23 |
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 24 |
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 25 |
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 26 |
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 27 |
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 28 |
+
// SOFTWARE.
|
| 29 |
+
package githubv4mock
|
| 30 |
+
|
| 31 |
+
import (
|
| 32 |
+
"bytes"
|
| 33 |
+
"reflect"
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
func objectsAreEqualValues(expected, actual any) bool {
|
| 37 |
+
if objectsAreEqual(expected, actual) {
|
| 38 |
+
return true
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
expectedValue := reflect.ValueOf(expected)
|
| 42 |
+
actualValue := reflect.ValueOf(actual)
|
| 43 |
+
if !expectedValue.IsValid() || !actualValue.IsValid() {
|
| 44 |
+
return false
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
expectedType := expectedValue.Type()
|
| 48 |
+
actualType := actualValue.Type()
|
| 49 |
+
if !expectedType.ConvertibleTo(actualType) {
|
| 50 |
+
return false
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
if !isNumericType(expectedType) || !isNumericType(actualType) {
|
| 54 |
+
// Attempt comparison after type conversion
|
| 55 |
+
return reflect.DeepEqual(
|
| 56 |
+
expectedValue.Convert(actualType).Interface(), actual,
|
| 57 |
+
)
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// If BOTH values are numeric, there are chances of false positives due
|
| 61 |
+
// to overflow or underflow. So, we need to make sure to always convert
|
| 62 |
+
// the smaller type to a larger type before comparing.
|
| 63 |
+
if expectedType.Size() >= actualType.Size() {
|
| 64 |
+
return actualValue.Convert(expectedType).Interface() == expected
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
return expectedValue.Convert(actualType).Interface() == actual
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// objectsAreEqual determines if two objects are considered equal.
|
| 71 |
+
//
|
| 72 |
+
// This function does no assertion of any kind.
|
| 73 |
+
func objectsAreEqual(expected, actual any) bool {
|
| 74 |
+
// There is a modification in objectsAreEqual to check that typed nils are equal, even if their types are different.
|
| 75 |
+
// This is required because when a nil is provided as a variable, the type is not known.
|
| 76 |
+
if isNil(expected) && isNil(actual) {
|
| 77 |
+
return true
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
exp, ok := expected.([]byte)
|
| 81 |
+
if !ok {
|
| 82 |
+
return reflect.DeepEqual(expected, actual)
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
act, ok := actual.([]byte)
|
| 86 |
+
if !ok {
|
| 87 |
+
return false
|
| 88 |
+
}
|
| 89 |
+
if exp == nil || act == nil {
|
| 90 |
+
return exp == nil && act == nil
|
| 91 |
+
}
|
| 92 |
+
return bytes.Equal(exp, act)
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// isNumericType returns true if the type is one of:
|
| 96 |
+
// int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64,
|
| 97 |
+
// float32, float64, complex64, complex128
|
| 98 |
+
func isNumericType(t reflect.Type) bool {
|
| 99 |
+
return t.Kind() >= reflect.Int && t.Kind() <= reflect.Complex128
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
func isNil(i any) bool {
|
| 103 |
+
if i == nil {
|
| 104 |
+
return true
|
| 105 |
+
}
|
| 106 |
+
v := reflect.ValueOf(i)
|
| 107 |
+
switch v.Kind() {
|
| 108 |
+
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
|
| 109 |
+
return v.IsNil()
|
| 110 |
+
default:
|
| 111 |
+
return false
|
| 112 |
+
}
|
| 113 |
+
}
|
internal/githubv4mock/objects_are_equal_values_test.go
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// The contents of this file are taken from https://github.com/stretchr/testify/blob/016e2e9c269209287f33ec203f340a9a723fe22c/assert/assertions_test.go#L140-L174
|
| 2 |
+
//
|
| 3 |
+
// There is a modification to test objectsAreEqualValues to check that typed nils are equal, even if their types are different.
|
| 4 |
+
|
| 5 |
+
// The original license, copied from https://github.com/stretchr/testify/blob/016e2e9c269209287f33ec203f340a9a723fe22c/LICENSE
|
| 6 |
+
//
|
| 7 |
+
// MIT License
|
| 8 |
+
//
|
| 9 |
+
// Copyright (c) 2012-2020 Mat Ryer, Tyler Bunnell and contributors.
|
| 10 |
+
|
| 11 |
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 12 |
+
// of this software and associated documentation files (the "Software"), to deal
|
| 13 |
+
// in the Software without restriction, including without limitation the rights
|
| 14 |
+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 15 |
+
// copies of the Software, and to permit persons to whom the Software is
|
| 16 |
+
// furnished to do so, subject to the following conditions:
|
| 17 |
+
|
| 18 |
+
// The above copyright notice and this permission notice shall be included in all
|
| 19 |
+
// copies or substantial portions of the Software.
|
| 20 |
+
|
| 21 |
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 22 |
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 23 |
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 24 |
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 25 |
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 26 |
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 27 |
+
// SOFTWARE.
|
| 28 |
+
package githubv4mock
|
| 29 |
+
|
| 30 |
+
import (
|
| 31 |
+
"fmt"
|
| 32 |
+
"math"
|
| 33 |
+
"testing"
|
| 34 |
+
"time"
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
func TestObjectsAreEqualValues(t *testing.T) {
|
| 38 |
+
now := time.Now()
|
| 39 |
+
|
| 40 |
+
cases := []struct {
|
| 41 |
+
expected interface{}
|
| 42 |
+
actual interface{}
|
| 43 |
+
result bool
|
| 44 |
+
}{
|
| 45 |
+
{uint32(10), int32(10), true},
|
| 46 |
+
{0, nil, false},
|
| 47 |
+
{nil, 0, false},
|
| 48 |
+
{now, now.In(time.Local), false}, // should not be time zone independent
|
| 49 |
+
{int(270), int8(14), false}, // should handle overflow/underflow
|
| 50 |
+
{int8(14), int(270), false},
|
| 51 |
+
{[]int{270, 270}, []int8{14, 14}, false},
|
| 52 |
+
{complex128(1e+100 + 1e+100i), complex64(complex(math.Inf(0), math.Inf(0))), false},
|
| 53 |
+
{complex64(complex(math.Inf(0), math.Inf(0))), complex128(1e+100 + 1e+100i), false},
|
| 54 |
+
{complex128(1e+100 + 1e+100i), 270, false},
|
| 55 |
+
{270, complex128(1e+100 + 1e+100i), false},
|
| 56 |
+
{complex128(1e+100 + 1e+100i), 3.14, false},
|
| 57 |
+
{3.14, complex128(1e+100 + 1e+100i), false},
|
| 58 |
+
{complex128(1e+10 + 1e+10i), complex64(1e+10 + 1e+10i), true},
|
| 59 |
+
{complex64(1e+10 + 1e+10i), complex128(1e+10 + 1e+10i), true},
|
| 60 |
+
{(*string)(nil), nil, true}, // typed nil vs untyped nil
|
| 61 |
+
{(*string)(nil), (*int)(nil), true}, // different typed nils
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
for _, c := range cases {
|
| 65 |
+
t.Run(fmt.Sprintf("ObjectsAreEqualValues(%#v, %#v)", c.expected, c.actual), func(t *testing.T) {
|
| 66 |
+
res := objectsAreEqualValues(c.expected, c.actual)
|
| 67 |
+
|
| 68 |
+
if res != c.result {
|
| 69 |
+
t.Errorf("ObjectsAreEqualValues(%#v, %#v) should return %#v", c.expected, c.actual, c.result)
|
| 70 |
+
}
|
| 71 |
+
})
|
| 72 |
+
}
|
| 73 |
+
}
|