Gemini commited on
Commit
fce10de
·
0 Parent(s):

Initial commit

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +11 -0
  2. .gitattributes +35 -0
  3. .github/CODEOWNERS +1 -0
  4. .github/ISSUE_TEMPLATE/bug_report.md +30 -0
  5. .github/ISSUE_TEMPLATE/feature_request.md +24 -0
  6. .github/dependabot.yml +19 -0
  7. .github/licenses.tmpl +13 -0
  8. .github/pull_request_template.md +11 -0
  9. .github/workflows/close-inactive-issues.yml +28 -0
  10. .github/workflows/code-scanning.yml +82 -0
  11. .github/workflows/docker-publish.yml +122 -0
  12. .github/workflows/docs-check.yml +47 -0
  13. .github/workflows/go.yml +32 -0
  14. .github/workflows/goreleaser.yml +45 -0
  15. .github/workflows/license-check.yml +21 -0
  16. .github/workflows/lint.yml +23 -0
  17. .gitignore +17 -0
  18. .golangci.yml +37 -0
  19. .goreleaser.yaml +44 -0
  20. .vscode/launch.json +28 -0
  21. CODE_OF_CONDUCT.md +128 -0
  22. CONTRIBUTING.md +56 -0
  23. Dockerfile +28 -0
  24. LICENSE +21 -0
  25. README.md +1142 -0
  26. SECURITY.md +31 -0
  27. SUPPORT.md +13 -0
  28. cmd/github-mcp-server/generate_docs.go +354 -0
  29. cmd/github-mcp-server/main.go +116 -0
  30. cmd/mcpcurl/README.md +150 -0
  31. cmd/mcpcurl/main.go +466 -0
  32. docs/error-handling.md +125 -0
  33. docs/host-integration.md +193 -0
  34. docs/installation-guides/README.md +95 -0
  35. docs/installation-guides/install-claude.md +167 -0
  36. docs/installation-guides/install-cursor.md +114 -0
  37. docs/installation-guides/install-other-copilot-ides.md +268 -0
  38. docs/installation-guides/install-windsurf.md +107 -0
  39. docs/policies-and-governance.md +216 -0
  40. docs/remote-server.md +54 -0
  41. docs/testing.md +34 -0
  42. e2e/README.md +96 -0
  43. e2e/e2e_test.go +1626 -0
  44. go.mod +57 -0
  45. go.sum +124 -0
  46. internal/ghmcp/server.go +423 -0
  47. internal/githubv4mock/githubv4mock.go +218 -0
  48. internal/githubv4mock/local_round_tripper.go +44 -0
  49. internal/githubv4mock/objects_are_equal_values.go +113 -0
  50. 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
+ [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](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) [![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install_Server-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](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
+ [![Install with Docker in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](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) [![Install with Docker in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install_Server-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](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
+ [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](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
+ [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](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
+ [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](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) [![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install_Server-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](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
+ }