├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml ├── licenses.tmpl ├── pull_request_template.md └── workflows │ ├── code-scanning.yml │ ├── docker-publish.yml │ ├── go.yml │ ├── goreleaser.yml │ ├── license-check.yml │ └── lint.yaml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yaml ├── .vscode └── launch.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── cmd ├── github-mcp-server │ └── main.go └── mcpcurl │ ├── README.md │ └── main.go ├── e2e ├── README.md └── e2e_test.go ├── go.mod ├── go.sum ├── pkg ├── github │ ├── code_scanning.go │ ├── code_scanning_test.go │ ├── context_tools.go │ ├── context_tools_test.go │ ├── dynamic_tools.go │ ├── helper_test.go │ ├── issues.go │ ├── issues_test.go │ ├── pullrequests.go │ ├── pullrequests_test.go │ ├── repositories.go │ ├── repositories_test.go │ ├── repository_resource.go │ ├── repository_resource_test.go │ ├── resources.go │ ├── search.go │ ├── search_test.go │ ├── secret_scanning.go │ ├── secret_scanning_test.go │ ├── server.go │ ├── server_test.go │ └── tools.go ├── log │ ├── io.go │ └── io_test.go ├── toolsets │ ├── toolsets.go │ └── toolsets_test.go └── translations │ └── translations.go ├── script ├── get-me ├── licenses ├── licenses-check └── prettyprint-log ├── third-party-licenses.darwin.md ├── third-party-licenses.linux.md ├── third-party-licenses.windows.md └── third-party ├── github.com ├── fsnotify │ └── fsnotify │ │ └── LICENSE ├── github │ └── github-mcp-server │ │ └── LICENSE ├── go-viper │ └── mapstructure │ │ └── v2 │ │ └── LICENSE ├── google │ ├── go-github │ │ └── v69 │ │ │ └── github │ │ │ └── LICENSE │ ├── go-querystring │ │ └── query │ │ │ └── LICENSE │ └── uuid │ │ └── LICENSE ├── inconshreveable │ └── mousetrap │ │ └── LICENSE ├── mark3labs │ └── mcp-go │ │ └── LICENSE ├── pelletier │ └── go-toml │ │ └── v2 │ │ └── LICENSE ├── sagikazarmark │ └── locafero │ │ └── LICENSE ├── sirupsen │ └── logrus │ │ └── LICENSE ├── sourcegraph │ └── conc │ │ └── LICENSE ├── spf13 │ ├── afero │ │ └── LICENSE.txt │ ├── cast │ │ └── LICENSE │ ├── cobra │ │ └── LICENSE.txt │ ├── pflag │ │ └── LICENSE │ └── viper │ │ └── LICENSE ├── subosito │ └── gotenv │ │ └── LICENSE └── yosida95 │ └── uritemplate │ └── v3 │ └── LICENSE ├── golang.org └── x │ ├── sys │ ├── unix │ │ └── LICENSE │ └── windows │ │ └── LICENSE │ └── text │ └── LICENSE └── gopkg.in └── yaml.v3 ├── LICENSE └── NOTICE /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @github/github-mcp-server 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 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. 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 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 | ### Additional context 19 | 20 | Add any other context like screenshots or mockups are helpful, if applicable. -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /.github/licenses.tmpl: -------------------------------------------------------------------------------- 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 14 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 10 | 11 | Closes: 12 | -------------------------------------------------------------------------------- /.github/workflows/code-scanning.yml: -------------------------------------------------------------------------------- 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 }} 83 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 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"] 13 | # Publish semver tags as releases. 14 | tags: ["v*.*.*"] 15 | pull_request: 16 | branches: ["main"] 17 | 18 | env: 19 | # Use docker.io for Docker Hub if empty 20 | REGISTRY: ghcr.io 21 | # github.repository as / 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} 123 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 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: go test -race ./... 30 | 31 | - name: Build 32 | run: go build -v ./cmd/github-mcp-server 33 | -------------------------------------------------------------------------------- /.github/workflows/goreleaser.yml: -------------------------------------------------------------------------------- 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 46 | -------------------------------------------------------------------------------- /.github/workflows/license-check.yml: -------------------------------------------------------------------------------- 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 22 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: 3 | push: 4 | pull_request: 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Check out code 15 | uses: actions/checkout@v4 16 | 17 | - name: Set up Go 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version-file: 'go.mod' 21 | 22 | - name: Verify dependencies 23 | run: | 24 | go mod verify 25 | go mod download 26 | 27 | LINT_VERSION=1.64.8 28 | curl -fsSL https://github.com/golangci/golangci-lint/releases/download/v${LINT_VERSION}/golangci-lint-${LINT_VERSION}-linux-amd64.tar.gz | \ 29 | tar xz --strip-components 1 --wildcards \*/golangci-lint 30 | mkdir -p bin && mv golangci-lint bin/ 31 | 32 | - name: Run checks 33 | run: | 34 | STATUS=0 35 | assert-nothing-changed() { 36 | local diff 37 | "$@" >/dev/null || return 1 38 | if ! diff="$(git diff -U1 --color --exit-code)"; then 39 | printf '\e[31mError: running `\e[1m%s\e[22m` results in modifications that you must check into version control:\e[0m\n%s\n\n' "$*" "$diff" >&2 40 | git checkout -- . 41 | STATUS=1 42 | fi 43 | } 44 | 45 | assert-nothing-changed go fmt ./... 46 | assert-nothing-changed go mod tidy 47 | 48 | bin/golangci-lint run --out-format=colored-line-number --timeout=3m || STATUS=$? 49 | 50 | exit $STATUS 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 5m 3 | tests: true 4 | concurrency: 4 5 | 6 | linters: 7 | enable: 8 | - govet 9 | - errcheck 10 | - staticcheck 11 | - gofmt 12 | - goimports 13 | - revive 14 | - ineffassign 15 | - typecheck 16 | - unused 17 | - gosimple 18 | - misspell 19 | - nakedret 20 | - bodyclose 21 | - gocritic 22 | - makezero 23 | - gosec 24 | 25 | output: 26 | formats: colored-line-number 27 | print-issued-lines: true 28 | print-linter-name: true 29 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 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}}" 45 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 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: -------------------------------------------------------------------------------- 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. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 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 | ## Prerequisites for running and testing code 14 | 15 | These are one time installations required to be able to test your changes locally as part of the pull request (PR) submission process. 16 | 17 | 1. install Go [through download](https://go.dev/doc/install) | [through Homebrew](https://formulae.brew.sh/formula/go) 18 | 1. [install golangci-lint](https://golangci-lint.run/welcome/install/#local-installation) 19 | 20 | ## Submitting a pull request 21 | 22 | 1. [Fork][fork] and clone the repository 23 | 1. Make sure the tests pass on your machine: `go test -v ./...` 24 | 1. Make sure linter passes on your machine: `golangci-lint run` 25 | 1. Create a new branch: `git checkout -b my-branch-name` 26 | 1. Make your change, add tests, and make sure the tests and linter still pass 27 | 1. Push to your fork and [submit a pull request][pr] 28 | 1. Pat yourself on the back and wait for your pull request to be reviewed and merged. 29 | 30 | Here are a few things you can do that will increase the likelihood of your pull request being accepted: 31 | 32 | - Follow the [style guide][style]. 33 | - Write tests. 34 | - 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. 35 | - Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). 36 | 37 | ## Resources 38 | 39 | - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) 40 | - [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) 41 | - [GitHub Help](https://help.github.com) 42 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG VERSION="dev" 2 | 3 | FROM golang:1.24.2 AS build 4 | # allow this step access to build arg 5 | ARG VERSION 6 | # Set the working directory 7 | WORKDIR /build 8 | 9 | RUN go env -w GOMODCACHE=/root/.cache/go-build 10 | 11 | # Install dependencies 12 | COPY go.mod go.sum ./ 13 | RUN --mount=type=cache,target=/root/.cache/go-build go mod download 14 | 15 | COPY . ./ 16 | # Build the server 17 | RUN --mount=type=cache,target=/root/.cache/go-build 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)" \ 18 | -o github-mcp-server cmd/github-mcp-server/main.go 19 | 20 | # Make a stage to run the app 21 | FROM gcr.io/distroless/base-debian12 22 | # Set the working directory 23 | WORKDIR /server 24 | # Copy the binary from the build stage 25 | COPY --from=build /build/github-mcp-server . 26 | # Command to run the server 27 | CMD ["./github-mcp-server", "stdio"] 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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. 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 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) 32 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 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. 14 | -------------------------------------------------------------------------------- /cmd/github-mcp-server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | stdlog "log" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | 12 | "github.com/github/github-mcp-server/pkg/github" 13 | iolog "github.com/github/github-mcp-server/pkg/log" 14 | "github.com/github/github-mcp-server/pkg/translations" 15 | gogithub "github.com/google/go-github/v69/github" 16 | "github.com/mark3labs/mcp-go/mcp" 17 | "github.com/mark3labs/mcp-go/server" 18 | log "github.com/sirupsen/logrus" 19 | "github.com/spf13/cobra" 20 | "github.com/spf13/viper" 21 | ) 22 | 23 | var version = "version" 24 | var commit = "commit" 25 | var date = "date" 26 | 27 | var ( 28 | rootCmd = &cobra.Command{ 29 | Use: "server", 30 | Short: "GitHub MCP Server", 31 | Long: `A GitHub MCP server that handles various tools and resources.`, 32 | Version: fmt.Sprintf("Version: %s\nCommit: %s\nBuild Date: %s", version, commit, date), 33 | } 34 | 35 | stdioCmd = &cobra.Command{ 36 | Use: "stdio", 37 | Short: "Start stdio server", 38 | Long: `Start a server that communicates via standard input/output streams using JSON-RPC messages.`, 39 | Run: func(_ *cobra.Command, _ []string) { 40 | logFile := viper.GetString("log-file") 41 | readOnly := viper.GetBool("read-only") 42 | exportTranslations := viper.GetBool("export-translations") 43 | logger, err := initLogger(logFile) 44 | if err != nil { 45 | stdlog.Fatal("Failed to initialize logger:", err) 46 | } 47 | 48 | enabledToolsets := viper.GetStringSlice("toolsets") 49 | 50 | logCommands := viper.GetBool("enable-command-logging") 51 | cfg := runConfig{ 52 | readOnly: readOnly, 53 | logger: logger, 54 | logCommands: logCommands, 55 | exportTranslations: exportTranslations, 56 | enabledToolsets: enabledToolsets, 57 | } 58 | if err := runStdioServer(cfg); err != nil { 59 | stdlog.Fatal("failed to run stdio server:", err) 60 | } 61 | }, 62 | } 63 | ) 64 | 65 | func init() { 66 | cobra.OnInitialize(initConfig) 67 | 68 | rootCmd.SetVersionTemplate("{{.Short}}\n{{.Version}}\n") 69 | 70 | // Add global flags that will be shared by all commands 71 | rootCmd.PersistentFlags().StringSlice("toolsets", github.DefaultTools, "An optional comma separated list of groups of tools to allow, defaults to enabling all") 72 | rootCmd.PersistentFlags().Bool("dynamic-toolsets", false, "Enable dynamic toolsets") 73 | rootCmd.PersistentFlags().Bool("read-only", false, "Restrict the server to read-only operations") 74 | rootCmd.PersistentFlags().String("log-file", "", "Path to log file") 75 | rootCmd.PersistentFlags().Bool("enable-command-logging", false, "When enabled, the server will log all command requests and responses to the log file") 76 | rootCmd.PersistentFlags().Bool("export-translations", false, "Save translations to a JSON file") 77 | rootCmd.PersistentFlags().String("gh-host", "", "Specify the GitHub hostname (for GitHub Enterprise etc.)") 78 | 79 | // Bind flag to viper 80 | _ = viper.BindPFlag("toolsets", rootCmd.PersistentFlags().Lookup("toolsets")) 81 | _ = viper.BindPFlag("dynamic_toolsets", rootCmd.PersistentFlags().Lookup("dynamic-toolsets")) 82 | _ = viper.BindPFlag("read-only", rootCmd.PersistentFlags().Lookup("read-only")) 83 | _ = viper.BindPFlag("log-file", rootCmd.PersistentFlags().Lookup("log-file")) 84 | _ = viper.BindPFlag("enable-command-logging", rootCmd.PersistentFlags().Lookup("enable-command-logging")) 85 | _ = viper.BindPFlag("export-translations", rootCmd.PersistentFlags().Lookup("export-translations")) 86 | _ = viper.BindPFlag("host", rootCmd.PersistentFlags().Lookup("gh-host")) 87 | 88 | // Add subcommands 89 | rootCmd.AddCommand(stdioCmd) 90 | } 91 | 92 | func initConfig() { 93 | // Initialize Viper configuration 94 | viper.SetEnvPrefix("github") 95 | viper.AutomaticEnv() 96 | } 97 | 98 | func initLogger(outPath string) (*log.Logger, error) { 99 | if outPath == "" { 100 | return log.New(), nil 101 | } 102 | 103 | file, err := os.OpenFile(outPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) 104 | if err != nil { 105 | return nil, fmt.Errorf("failed to open log file: %w", err) 106 | } 107 | 108 | logger := log.New() 109 | logger.SetLevel(log.DebugLevel) 110 | logger.SetOutput(file) 111 | 112 | return logger, nil 113 | } 114 | 115 | type runConfig struct { 116 | readOnly bool 117 | logger *log.Logger 118 | logCommands bool 119 | exportTranslations bool 120 | enabledToolsets []string 121 | } 122 | 123 | func runStdioServer(cfg runConfig) error { 124 | // Create app context 125 | ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) 126 | defer stop() 127 | 128 | // Create GH client 129 | token := viper.GetString("personal_access_token") 130 | if token == "" { 131 | cfg.logger.Fatal("GITHUB_PERSONAL_ACCESS_TOKEN not set") 132 | } 133 | ghClient := gogithub.NewClient(nil).WithAuthToken(token) 134 | ghClient.UserAgent = fmt.Sprintf("github-mcp-server/%s", version) 135 | 136 | host := viper.GetString("host") 137 | 138 | if host != "" { 139 | var err error 140 | ghClient, err = ghClient.WithEnterpriseURLs(host, host) 141 | if err != nil { 142 | return fmt.Errorf("failed to create GitHub client with host: %w", err) 143 | } 144 | } 145 | 146 | t, dumpTranslations := translations.TranslationHelper() 147 | 148 | beforeInit := func(_ context.Context, _ any, message *mcp.InitializeRequest) { 149 | ghClient.UserAgent = fmt.Sprintf("github-mcp-server/%s (%s/%s)", version, message.Params.ClientInfo.Name, message.Params.ClientInfo.Version) 150 | } 151 | 152 | getClient := func(_ context.Context) (*gogithub.Client, error) { 153 | return ghClient, nil // closing over client 154 | } 155 | 156 | hooks := &server.Hooks{ 157 | OnBeforeInitialize: []server.OnBeforeInitializeFunc{beforeInit}, 158 | } 159 | // Create server 160 | ghServer := github.NewServer(version, server.WithHooks(hooks)) 161 | 162 | enabled := cfg.enabledToolsets 163 | dynamic := viper.GetBool("dynamic_toolsets") 164 | if dynamic { 165 | // filter "all" from the enabled toolsets 166 | enabled = make([]string, 0, len(cfg.enabledToolsets)) 167 | for _, toolset := range cfg.enabledToolsets { 168 | if toolset != "all" { 169 | enabled = append(enabled, toolset) 170 | } 171 | } 172 | } 173 | 174 | // Create default toolsets 175 | toolsets, err := github.InitToolsets(enabled, cfg.readOnly, getClient, t) 176 | context := github.InitContextToolset(getClient, t) 177 | 178 | if err != nil { 179 | stdlog.Fatal("Failed to initialize toolsets:", err) 180 | } 181 | 182 | // Register resources with the server 183 | github.RegisterResources(ghServer, getClient, t) 184 | // Register the tools with the server 185 | toolsets.RegisterTools(ghServer) 186 | context.RegisterTools(ghServer) 187 | 188 | if dynamic { 189 | dynamic := github.InitDynamicToolset(ghServer, toolsets, t) 190 | dynamic.RegisterTools(ghServer) 191 | } 192 | 193 | stdioServer := server.NewStdioServer(ghServer) 194 | 195 | stdLogger := stdlog.New(cfg.logger.Writer(), "stdioserver", 0) 196 | stdioServer.SetErrorLogger(stdLogger) 197 | 198 | if cfg.exportTranslations { 199 | // Once server is initialized, all translations are loaded 200 | dumpTranslations() 201 | } 202 | 203 | // Start listening for messages 204 | errC := make(chan error, 1) 205 | go func() { 206 | in, out := io.Reader(os.Stdin), io.Writer(os.Stdout) 207 | 208 | if cfg.logCommands { 209 | loggedIO := iolog.NewIOLogger(in, out, cfg.logger) 210 | in, out = loggedIO, loggedIO 211 | } 212 | 213 | errC <- stdioServer.Listen(ctx, in, out) 214 | }() 215 | 216 | // Output github-mcp-server string 217 | _, _ = fmt.Fprintf(os.Stderr, "GitHub MCP Server running on stdio\n") 218 | 219 | // Wait for shutdown signal 220 | select { 221 | case <-ctx.Done(): 222 | cfg.logger.Infof("shutting down server...") 223 | case err := <-errC: 224 | if err != nil { 225 | return fmt.Errorf("error running server: %w", err) 226 | } 227 | } 228 | 229 | return nil 230 | } 231 | 232 | func main() { 233 | if err := rootCmd.Execute(); err != nil { 234 | fmt.Println(err) 235 | os.Exit(1) 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /cmd/mcpcurl/README.md: -------------------------------------------------------------------------------- 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 | ## Usage 19 | 20 | ```bash 21 | mcpcurl --stdio-server-cmd="" [flags] 22 | ``` 23 | 24 | The `--stdio-server-cmd` flag is required for all commands and specifies the command to run the MCP server. 25 | 26 | ### Available Commands 27 | 28 | - `tools`: Contains all dynamically generated tool commands from the schema 29 | - `schema`: Fetches and displays the raw schema from the MCP server 30 | - `help`: Shows help for any command 31 | 32 | ### Examples 33 | 34 | List available tools in Anthropic's MCP server: 35 | 36 | ```bash 37 | % ./mcpcurl --stdio-server-cmd "docker run -i --rm -e GITHUB_PERSONAL_ACCESS_TOKEN mcp/github" tools --help 38 | Contains all dynamically generated tool commands from the schema 39 | 40 | Usage: 41 | mcpcurl tools [command] 42 | 43 | Available Commands: 44 | add_issue_comment Add a comment to an existing issue 45 | create_branch Create a new branch in a GitHub repository 46 | create_issue Create a new issue in a GitHub repository 47 | create_or_update_file Create or update a single file in a GitHub repository 48 | create_pull_request Create a new pull request in a GitHub repository 49 | create_repository Create a new GitHub repository in your account 50 | fork_repository Fork a GitHub repository to your account or specified organization 51 | get_file_contents Get the contents of a file or directory from a GitHub repository 52 | get_issue Get details of a specific issue in a GitHub repository 53 | get_issue_comments Get comments for a GitHub issue 54 | list_commits Get list of commits of a branch in a GitHub repository 55 | list_issues List issues in a GitHub repository with filtering options 56 | push_files Push multiple files to a GitHub repository in a single commit 57 | search_code Search for code across GitHub repositories 58 | search_issues Search for issues and pull requests across GitHub repositories 59 | search_repositories Search for GitHub repositories 60 | search_users Search for users on GitHub 61 | update_issue Update an existing issue in a GitHub repository 62 | 63 | Flags: 64 | -h, --help help for tools 65 | 66 | Global Flags: 67 | --pretty Pretty print MCP response (only for JSON responses) (default true) 68 | --stdio-server-cmd string Shell command to invoke MCP server via stdio (required) 69 | 70 | Use "mcpcurl tools [command] --help" for more information about a command. 71 | ``` 72 | 73 | Get help for a specific tool: 74 | 75 | ```bash 76 | % ./mcpcurl --stdio-server-cmd "docker run -i --rm -e GITHUB_PERSONAL_ACCESS_TOKEN mcp/github" tools get_issue --help 77 | Get details of a specific issue in a GitHub repository 78 | 79 | Usage: 80 | mcpcurl tools get_issue [flags] 81 | 82 | Flags: 83 | -h, --help help for get_issue 84 | --issue_number float 85 | --owner string 86 | --repo string 87 | 88 | Global Flags: 89 | --pretty Pretty print MCP response (only for JSON responses) (default true) 90 | --stdio-server-cmd string Shell command to invoke MCP server via stdio (required) 91 | 92 | ``` 93 | 94 | Use one of the tools: 95 | 96 | ```bash 97 | % ./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 98 | { 99 | "active_lock_reason": null, 100 | "assignee": null, 101 | "assignees": [], 102 | "author_association": "CONTRIBUTOR", 103 | "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", 104 | "closed_at": "2014-12-08T10:02:16Z", 105 | "closed_by": null, 106 | "comments": 12, 107 | "comments_url": "https://api.github.com/repos/golang/go/issues/1/comments", 108 | "created_at": "2009-10-22T06:07:26Z", 109 | "events_url": "https://api.github.com/repos/golang/go/issues/1/events", 110 | [...] 111 | } 112 | ``` 113 | 114 | ## Dynamic Commands 115 | 116 | All tools provided by the MCP server are automatically available as subcommands under the `tools` command. Each generated command has: 117 | 118 | - Appropriate flags matching the tool's input schema 119 | - Validation for required parameters 120 | - Type validation 121 | - Enum validation (for string parameters with allowable values) 122 | - Help text generated from the tool's description 123 | 124 | ## How It Works 125 | 126 | 1. `mcpcurl` makes a JSON-RPC request to the server using the `tools/list` method 127 | 2. The server responds with a schema describing all available tools 128 | 3. `mcpcurl` dynamically builds a command structure based on this schema 129 | 4. When a command is executed, arguments are converted to a JSON-RPC request 130 | 5. The request is sent to the server via stdin, and the response is printed to stdout 131 | -------------------------------------------------------------------------------- /e2e/README.md: -------------------------------------------------------------------------------- 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= 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 | ## Limitations 81 | 82 | 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! 83 | 84 | Currently, visibility into failures is not particularly good. 85 | -------------------------------------------------------------------------------- /e2e/e2e_test.go: -------------------------------------------------------------------------------- 1 | //go:build e2e 2 | 3 | package e2e_test 4 | 5 | import ( 6 | "context" 7 | "encoding/json" 8 | "os" 9 | "os/exec" 10 | "testing" 11 | "time" 12 | 13 | "github.com/google/go-github/v69/github" 14 | mcpClient "github.com/mark3labs/mcp-go/client" 15 | "github.com/mark3labs/mcp-go/mcp" 16 | "github.com/stretchr/testify/require" 17 | ) 18 | 19 | func TestE2E(t *testing.T) { 20 | e2eServerToken := os.Getenv("GITHUB_MCP_SERVER_E2E_TOKEN") 21 | if e2eServerToken == "" { 22 | t.Fatalf("GITHUB_MCP_SERVER_E2E_TOKEN environment variable is not set") 23 | } 24 | 25 | // Build the Docker image for the MCP server. 26 | buildDockerImage(t) 27 | 28 | t.Setenv("GITHUB_PERSONAL_ACCESS_TOKEN", e2eServerToken) // The MCP Client merges the existing environment. 29 | args := []string{ 30 | "docker", 31 | "run", 32 | "-i", 33 | "--rm", 34 | "-e", 35 | "GITHUB_PERSONAL_ACCESS_TOKEN", 36 | "github/e2e-github-mcp-server", 37 | } 38 | t.Log("Starting Stdio MCP client...") 39 | client, err := mcpClient.NewStdioMCPClient(args[0], []string{}, args[1:]...) 40 | require.NoError(t, err, "expected to create client successfully") 41 | 42 | t.Run("Initialize", func(t *testing.T) { 43 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 44 | defer cancel() 45 | 46 | request := mcp.InitializeRequest{} 47 | request.Params.ProtocolVersion = "2025-03-26" 48 | request.Params.ClientInfo = mcp.Implementation{ 49 | Name: "e2e-test-client", 50 | Version: "0.0.1", 51 | } 52 | 53 | result, err := client.Initialize(ctx, request) 54 | require.NoError(t, err, "expected to initialize successfully") 55 | 56 | require.Equal(t, "github-mcp-server", result.ServerInfo.Name) 57 | }) 58 | 59 | t.Run("CallTool get_me", func(t *testing.T) { 60 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 61 | defer cancel() 62 | 63 | // When we call the "get_me" tool 64 | request := mcp.CallToolRequest{} 65 | request.Params.Name = "get_me" 66 | 67 | response, err := client.CallTool(ctx, request) 68 | require.NoError(t, err, "expected to call 'get_me' tool successfully") 69 | 70 | require.False(t, response.IsError, "expected result not to be an error") 71 | require.Len(t, response.Content, 1, "expected content to have one item") 72 | 73 | textContent, ok := response.Content[0].(mcp.TextContent) 74 | require.True(t, ok, "expected content to be of type TextContent") 75 | 76 | var trimmedContent struct { 77 | Login string `json:"login"` 78 | } 79 | err = json.Unmarshal([]byte(textContent.Text), &trimmedContent) 80 | require.NoError(t, err, "expected to unmarshal text content successfully") 81 | 82 | // Then the login in the response should match the login obtained via the same 83 | // token using the GitHub API. 84 | client := github.NewClient(nil).WithAuthToken(e2eServerToken) 85 | user, _, err := client.Users.Get(context.Background(), "") 86 | require.NoError(t, err, "expected to get user successfully") 87 | require.Equal(t, trimmedContent.Login, *user.Login, "expected login to match") 88 | }) 89 | 90 | require.NoError(t, client.Close(), "expected to close client successfully") 91 | } 92 | 93 | func buildDockerImage(t *testing.T) { 94 | t.Log("Building Docker image for e2e tests...") 95 | 96 | cmd := exec.Command("docker", "build", "-t", "github/e2e-github-mcp-server", ".") 97 | cmd.Dir = ".." // Run this in the context of the root, where the Dockerfile is located. 98 | output, err := cmd.CombinedOutput() 99 | require.NoError(t, err, "expected to build Docker image successfully, output: %s", string(output)) 100 | } 101 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/github/github-mcp-server 2 | 3 | go 1.23.7 4 | 5 | require ( 6 | github.com/google/go-github/v69 v69.2.0 7 | github.com/mark3labs/mcp-go v0.22.0 8 | github.com/migueleliasweb/go-github-mock v1.1.0 9 | github.com/sirupsen/logrus v1.9.3 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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 17 | github.com/fsnotify/fsnotify v1.8.0 // indirect 18 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 19 | github.com/google/go-cmp v0.7.0 // indirect 20 | github.com/google/go-github/v64 v64.0.0 // indirect 21 | github.com/google/go-querystring v1.1.0 // indirect 22 | github.com/google/uuid v1.6.0 // indirect 23 | github.com/gorilla/mux v1.8.0 // indirect 24 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 25 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 26 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 27 | github.com/rogpeppe/go-internal v1.13.1 // indirect 28 | github.com/sagikazarmark/locafero v0.9.0 // indirect 29 | github.com/sourcegraph/conc v0.3.0 // indirect 30 | github.com/spf13/afero v1.14.0 // indirect 31 | github.com/spf13/cast v1.7.1 // indirect 32 | github.com/spf13/pflag v1.0.6 // indirect 33 | github.com/subosito/gotenv v1.6.0 // indirect 34 | github.com/yosida95/uritemplate/v3 v3.0.2 // indirect 35 | go.uber.org/multierr v1.11.0 // indirect 36 | golang.org/x/sys v0.31.0 // indirect 37 | golang.org/x/text v0.23.0 // indirect 38 | golang.org/x/time v0.5.0 // indirect 39 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 40 | gopkg.in/yaml.v3 v3.0.1 // indirect 41 | ) 42 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 5 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 7 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 8 | github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= 9 | github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 10 | github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= 11 | github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 12 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 13 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 14 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 15 | github.com/google/go-github/v64 v64.0.0 h1:4G61sozmY3eiPAjjoOHponXDBONm+utovTKbyUb2Qdg= 16 | github.com/google/go-github/v64 v64.0.0/go.mod h1:xB3vqMQNdHzilXBiO2I+M7iEFtHf+DP/omBOv6tQzVo= 17 | github.com/google/go-github/v69 v69.2.0 h1:wR+Wi/fN2zdUx9YxSmYE0ktiX9IAR/BeePzeaUUbEHE= 18 | github.com/google/go-github/v69 v69.2.0/go.mod h1:xne4jymxLR6Uj9b7J7PyTpkMYstEMMwGZa0Aehh1azM= 19 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 20 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 21 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 22 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 23 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 24 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 25 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 26 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 27 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 28 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 29 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 30 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 31 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 32 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 33 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 34 | github.com/mark3labs/mcp-go v0.22.0 h1:cCEBWi4Yy9Kio+OW1hWIyi4WLsSr+RBBK6FI5tj+b7I= 35 | github.com/mark3labs/mcp-go v0.22.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4= 36 | github.com/migueleliasweb/go-github-mock v1.1.0 h1:GKaOBPsrPGkAKgtfuWY8MclS1xR6MInkx1SexJucMwE= 37 | github.com/migueleliasweb/go-github-mock v1.1.0/go.mod h1:pYe/XlGs4BGMfRY4vmeixVsODHnVDDhJ9zoi0qzSMHc= 38 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 39 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 40 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 41 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 42 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 43 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 44 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 45 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 46 | github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k= 47 | github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= 48 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 49 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 50 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 51 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 52 | github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= 53 | github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= 54 | github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= 55 | github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 56 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 57 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 58 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 59 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 60 | github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= 61 | github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= 62 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 63 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 64 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 65 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 66 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 67 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 68 | github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= 69 | github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= 70 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 71 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 72 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 73 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 74 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 75 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 76 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 77 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 78 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 79 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 80 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 81 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 82 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 83 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 84 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 85 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 86 | -------------------------------------------------------------------------------- /pkg/github/code_scanning.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | 10 | "github.com/github/github-mcp-server/pkg/translations" 11 | "github.com/google/go-github/v69/github" 12 | "github.com/mark3labs/mcp-go/mcp" 13 | "github.com/mark3labs/mcp-go/server" 14 | ) 15 | 16 | func GetCodeScanningAlert(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { 17 | return mcp.NewTool("get_code_scanning_alert", 18 | mcp.WithDescription(t("TOOL_GET_CODE_SCANNING_ALERT_DESCRIPTION", "Get details of a specific code scanning alert in a GitHub repository.")), 19 | mcp.WithToolAnnotation(mcp.ToolAnnotation{ 20 | Title: t("TOOL_GET_CODE_SCANNING_ALERT_USER_TITLE", "Get code scanning alert"), 21 | ReadOnlyHint: true, 22 | }), 23 | mcp.WithString("owner", 24 | mcp.Required(), 25 | mcp.Description("The owner of the repository."), 26 | ), 27 | mcp.WithString("repo", 28 | mcp.Required(), 29 | mcp.Description("The name of the repository."), 30 | ), 31 | mcp.WithNumber("alertNumber", 32 | mcp.Required(), 33 | mcp.Description("The number of the alert."), 34 | ), 35 | ), 36 | func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { 37 | owner, err := requiredParam[string](request, "owner") 38 | if err != nil { 39 | return mcp.NewToolResultError(err.Error()), nil 40 | } 41 | repo, err := requiredParam[string](request, "repo") 42 | if err != nil { 43 | return mcp.NewToolResultError(err.Error()), nil 44 | } 45 | alertNumber, err := RequiredInt(request, "alertNumber") 46 | if err != nil { 47 | return mcp.NewToolResultError(err.Error()), nil 48 | } 49 | 50 | client, err := getClient(ctx) 51 | if err != nil { 52 | return nil, fmt.Errorf("failed to get GitHub client: %w", err) 53 | } 54 | 55 | alert, resp, err := client.CodeScanning.GetAlert(ctx, owner, repo, int64(alertNumber)) 56 | if err != nil { 57 | return nil, fmt.Errorf("failed to get alert: %w", err) 58 | } 59 | defer func() { _ = resp.Body.Close() }() 60 | 61 | if resp.StatusCode != http.StatusOK { 62 | body, err := io.ReadAll(resp.Body) 63 | if err != nil { 64 | return nil, fmt.Errorf("failed to read response body: %w", err) 65 | } 66 | return mcp.NewToolResultError(fmt.Sprintf("failed to get alert: %s", string(body))), nil 67 | } 68 | 69 | r, err := json.Marshal(alert) 70 | if err != nil { 71 | return nil, fmt.Errorf("failed to marshal alert: %w", err) 72 | } 73 | 74 | return mcp.NewToolResultText(string(r)), nil 75 | } 76 | } 77 | 78 | func ListCodeScanningAlerts(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { 79 | return mcp.NewTool("list_code_scanning_alerts", 80 | mcp.WithDescription(t("TOOL_LIST_CODE_SCANNING_ALERTS_DESCRIPTION", "List code scanning alerts in a GitHub repository.")), 81 | mcp.WithToolAnnotation(mcp.ToolAnnotation{ 82 | Title: t("TOOL_LIST_CODE_SCANNING_ALERTS_USER_TITLE", "List code scanning alerts"), 83 | ReadOnlyHint: true, 84 | }), 85 | mcp.WithString("owner", 86 | mcp.Required(), 87 | mcp.Description("The owner of the repository."), 88 | ), 89 | mcp.WithString("repo", 90 | mcp.Required(), 91 | mcp.Description("The name of the repository."), 92 | ), 93 | mcp.WithString("ref", 94 | mcp.Description("The Git reference for the results you want to list."), 95 | ), 96 | mcp.WithString("state", 97 | mcp.Description("Filter code scanning alerts by state. Defaults to open"), 98 | mcp.DefaultString("open"), 99 | mcp.Enum("open", "closed", "dismissed", "fixed"), 100 | ), 101 | mcp.WithString("severity", 102 | mcp.Description("Filter code scanning alerts by severity"), 103 | mcp.Enum("critical", "high", "medium", "low", "warning", "note", "error"), 104 | ), 105 | mcp.WithString("tool_name", 106 | mcp.Description("The name of the tool used for code scanning."), 107 | ), 108 | ), 109 | func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { 110 | owner, err := requiredParam[string](request, "owner") 111 | if err != nil { 112 | return mcp.NewToolResultError(err.Error()), nil 113 | } 114 | repo, err := requiredParam[string](request, "repo") 115 | if err != nil { 116 | return mcp.NewToolResultError(err.Error()), nil 117 | } 118 | ref, err := OptionalParam[string](request, "ref") 119 | if err != nil { 120 | return mcp.NewToolResultError(err.Error()), nil 121 | } 122 | state, err := OptionalParam[string](request, "state") 123 | if err != nil { 124 | return mcp.NewToolResultError(err.Error()), nil 125 | } 126 | severity, err := OptionalParam[string](request, "severity") 127 | if err != nil { 128 | return mcp.NewToolResultError(err.Error()), nil 129 | } 130 | toolName, err := OptionalParam[string](request, "tool_name") 131 | if err != nil { 132 | return mcp.NewToolResultError(err.Error()), nil 133 | } 134 | 135 | client, err := getClient(ctx) 136 | if err != nil { 137 | return nil, fmt.Errorf("failed to get GitHub client: %w", err) 138 | } 139 | alerts, resp, err := client.CodeScanning.ListAlertsForRepo(ctx, owner, repo, &github.AlertListOptions{Ref: ref, State: state, Severity: severity, ToolName: toolName}) 140 | if err != nil { 141 | return nil, fmt.Errorf("failed to list alerts: %w", err) 142 | } 143 | defer func() { _ = resp.Body.Close() }() 144 | 145 | if resp.StatusCode != http.StatusOK { 146 | body, err := io.ReadAll(resp.Body) 147 | if err != nil { 148 | return nil, fmt.Errorf("failed to read response body: %w", err) 149 | } 150 | return mcp.NewToolResultError(fmt.Sprintf("failed to list alerts: %s", string(body))), nil 151 | } 152 | 153 | r, err := json.Marshal(alerts) 154 | if err != nil { 155 | return nil, fmt.Errorf("failed to marshal alerts: %w", err) 156 | } 157 | 158 | return mcp.NewToolResultText(string(r)), nil 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /pkg/github/code_scanning_test.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net/http" 7 | "testing" 8 | 9 | "github.com/github/github-mcp-server/pkg/translations" 10 | "github.com/google/go-github/v69/github" 11 | "github.com/migueleliasweb/go-github-mock/src/mock" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func Test_GetCodeScanningAlert(t *testing.T) { 17 | // Verify tool definition once 18 | mockClient := github.NewClient(nil) 19 | tool, _ := GetCodeScanningAlert(stubGetClientFn(mockClient), translations.NullTranslationHelper) 20 | 21 | assert.Equal(t, "get_code_scanning_alert", tool.Name) 22 | assert.NotEmpty(t, tool.Description) 23 | assert.Contains(t, tool.InputSchema.Properties, "owner") 24 | assert.Contains(t, tool.InputSchema.Properties, "repo") 25 | assert.Contains(t, tool.InputSchema.Properties, "alertNumber") 26 | assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "alertNumber"}) 27 | 28 | // Setup mock alert for success case 29 | mockAlert := &github.Alert{ 30 | Number: github.Ptr(42), 31 | State: github.Ptr("open"), 32 | Rule: &github.Rule{ID: github.Ptr("test-rule"), Description: github.Ptr("Test Rule Description")}, 33 | HTMLURL: github.Ptr("https://github.com/owner/repo/security/code-scanning/42"), 34 | } 35 | 36 | tests := []struct { 37 | name string 38 | mockedClient *http.Client 39 | requestArgs map[string]interface{} 40 | expectError bool 41 | expectedAlert *github.Alert 42 | expectedErrMsg string 43 | }{ 44 | { 45 | name: "successful alert fetch", 46 | mockedClient: mock.NewMockedHTTPClient( 47 | mock.WithRequestMatch( 48 | mock.GetReposCodeScanningAlertsByOwnerByRepoByAlertNumber, 49 | mockAlert, 50 | ), 51 | ), 52 | requestArgs: map[string]interface{}{ 53 | "owner": "owner", 54 | "repo": "repo", 55 | "alertNumber": float64(42), 56 | }, 57 | expectError: false, 58 | expectedAlert: mockAlert, 59 | }, 60 | { 61 | name: "alert fetch fails", 62 | mockedClient: mock.NewMockedHTTPClient( 63 | mock.WithRequestMatchHandler( 64 | mock.GetReposCodeScanningAlertsByOwnerByRepoByAlertNumber, 65 | http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 66 | w.WriteHeader(http.StatusNotFound) 67 | _, _ = w.Write([]byte(`{"message": "Not Found"}`)) 68 | }), 69 | ), 70 | ), 71 | requestArgs: map[string]interface{}{ 72 | "owner": "owner", 73 | "repo": "repo", 74 | "alertNumber": float64(9999), 75 | }, 76 | expectError: true, 77 | expectedErrMsg: "failed to get alert", 78 | }, 79 | } 80 | 81 | for _, tc := range tests { 82 | t.Run(tc.name, func(t *testing.T) { 83 | // Setup client with mock 84 | client := github.NewClient(tc.mockedClient) 85 | _, handler := GetCodeScanningAlert(stubGetClientFn(client), translations.NullTranslationHelper) 86 | 87 | // Create call request 88 | request := createMCPRequest(tc.requestArgs) 89 | 90 | // Call handler 91 | result, err := handler(context.Background(), request) 92 | 93 | // Verify results 94 | if tc.expectError { 95 | require.Error(t, err) 96 | assert.Contains(t, err.Error(), tc.expectedErrMsg) 97 | return 98 | } 99 | 100 | require.NoError(t, err) 101 | 102 | // Parse the result and get the text content if no error 103 | textContent := getTextResult(t, result) 104 | 105 | // Unmarshal and verify the result 106 | var returnedAlert github.Alert 107 | err = json.Unmarshal([]byte(textContent.Text), &returnedAlert) 108 | assert.NoError(t, err) 109 | assert.Equal(t, *tc.expectedAlert.Number, *returnedAlert.Number) 110 | assert.Equal(t, *tc.expectedAlert.State, *returnedAlert.State) 111 | assert.Equal(t, *tc.expectedAlert.Rule.ID, *returnedAlert.Rule.ID) 112 | assert.Equal(t, *tc.expectedAlert.HTMLURL, *returnedAlert.HTMLURL) 113 | 114 | }) 115 | } 116 | } 117 | 118 | func Test_ListCodeScanningAlerts(t *testing.T) { 119 | // Verify tool definition once 120 | mockClient := github.NewClient(nil) 121 | tool, _ := ListCodeScanningAlerts(stubGetClientFn(mockClient), translations.NullTranslationHelper) 122 | 123 | assert.Equal(t, "list_code_scanning_alerts", tool.Name) 124 | assert.NotEmpty(t, tool.Description) 125 | assert.Contains(t, tool.InputSchema.Properties, "owner") 126 | assert.Contains(t, tool.InputSchema.Properties, "repo") 127 | assert.Contains(t, tool.InputSchema.Properties, "ref") 128 | assert.Contains(t, tool.InputSchema.Properties, "state") 129 | assert.Contains(t, tool.InputSchema.Properties, "severity") 130 | assert.Contains(t, tool.InputSchema.Properties, "tool_name") 131 | assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo"}) 132 | 133 | // Setup mock alerts for success case 134 | mockAlerts := []*github.Alert{ 135 | { 136 | Number: github.Ptr(42), 137 | State: github.Ptr("open"), 138 | Rule: &github.Rule{ID: github.Ptr("test-rule-1"), Description: github.Ptr("Test Rule 1")}, 139 | HTMLURL: github.Ptr("https://github.com/owner/repo/security/code-scanning/42"), 140 | }, 141 | { 142 | Number: github.Ptr(43), 143 | State: github.Ptr("fixed"), 144 | Rule: &github.Rule{ID: github.Ptr("test-rule-2"), Description: github.Ptr("Test Rule 2")}, 145 | HTMLURL: github.Ptr("https://github.com/owner/repo/security/code-scanning/43"), 146 | }, 147 | } 148 | 149 | tests := []struct { 150 | name string 151 | mockedClient *http.Client 152 | requestArgs map[string]interface{} 153 | expectError bool 154 | expectedAlerts []*github.Alert 155 | expectedErrMsg string 156 | }{ 157 | { 158 | name: "successful alerts listing", 159 | mockedClient: mock.NewMockedHTTPClient( 160 | mock.WithRequestMatchHandler( 161 | mock.GetReposCodeScanningAlertsByOwnerByRepo, 162 | expectQueryParams(t, map[string]string{ 163 | "ref": "main", 164 | "state": "open", 165 | "severity": "high", 166 | "tool_name": "codeql", 167 | }).andThen( 168 | mockResponse(t, http.StatusOK, mockAlerts), 169 | ), 170 | ), 171 | ), 172 | requestArgs: map[string]interface{}{ 173 | "owner": "owner", 174 | "repo": "repo", 175 | "ref": "main", 176 | "state": "open", 177 | "severity": "high", 178 | "tool_name": "codeql", 179 | }, 180 | expectError: false, 181 | expectedAlerts: mockAlerts, 182 | }, 183 | { 184 | name: "alerts listing fails", 185 | mockedClient: mock.NewMockedHTTPClient( 186 | mock.WithRequestMatchHandler( 187 | mock.GetReposCodeScanningAlertsByOwnerByRepo, 188 | http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 189 | w.WriteHeader(http.StatusUnauthorized) 190 | _, _ = w.Write([]byte(`{"message": "Unauthorized access"}`)) 191 | }), 192 | ), 193 | ), 194 | requestArgs: map[string]interface{}{ 195 | "owner": "owner", 196 | "repo": "repo", 197 | }, 198 | expectError: true, 199 | expectedErrMsg: "failed to list alerts", 200 | }, 201 | } 202 | 203 | for _, tc := range tests { 204 | t.Run(tc.name, func(t *testing.T) { 205 | // Setup client with mock 206 | client := github.NewClient(tc.mockedClient) 207 | _, handler := ListCodeScanningAlerts(stubGetClientFn(client), translations.NullTranslationHelper) 208 | 209 | // Create call request 210 | request := createMCPRequest(tc.requestArgs) 211 | 212 | // Call handler 213 | result, err := handler(context.Background(), request) 214 | 215 | // Verify results 216 | if tc.expectError { 217 | require.Error(t, err) 218 | assert.Contains(t, err.Error(), tc.expectedErrMsg) 219 | return 220 | } 221 | 222 | require.NoError(t, err) 223 | 224 | // Parse the result and get the text content if no error 225 | textContent := getTextResult(t, result) 226 | 227 | // Unmarshal and verify the result 228 | var returnedAlerts []*github.Alert 229 | err = json.Unmarshal([]byte(textContent.Text), &returnedAlerts) 230 | assert.NoError(t, err) 231 | assert.Len(t, returnedAlerts, len(tc.expectedAlerts)) 232 | for i, alert := range returnedAlerts { 233 | assert.Equal(t, *tc.expectedAlerts[i].Number, *alert.Number) 234 | assert.Equal(t, *tc.expectedAlerts[i].State, *alert.State) 235 | assert.Equal(t, *tc.expectedAlerts[i].Rule.ID, *alert.Rule.ID) 236 | assert.Equal(t, *tc.expectedAlerts[i].HTMLURL, *alert.HTMLURL) 237 | } 238 | }) 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /pkg/github/context_tools.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | 10 | "github.com/github/github-mcp-server/pkg/translations" 11 | "github.com/mark3labs/mcp-go/mcp" 12 | "github.com/mark3labs/mcp-go/server" 13 | ) 14 | 15 | // GetMe creates a tool to get details of the authenticated user. 16 | func GetMe(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { 17 | return mcp.NewTool("get_me", 18 | mcp.WithDescription(t("TOOL_GET_ME_DESCRIPTION", "Get details of the authenticated GitHub user. Use this when a request include \"me\", \"my\"...")), 19 | mcp.WithToolAnnotation(mcp.ToolAnnotation{ 20 | Title: t("TOOL_GET_ME_USER_TITLE", "Get my user profile"), 21 | ReadOnlyHint: true, 22 | }), 23 | mcp.WithString("reason", 24 | mcp.Description("Optional: reason the session was created"), 25 | ), 26 | ), 27 | func(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { 28 | client, err := getClient(ctx) 29 | if err != nil { 30 | return nil, fmt.Errorf("failed to get GitHub client: %w", err) 31 | } 32 | user, resp, err := client.Users.Get(ctx, "") 33 | if err != nil { 34 | return nil, fmt.Errorf("failed to get user: %w", err) 35 | } 36 | defer func() { _ = resp.Body.Close() }() 37 | 38 | if resp.StatusCode != http.StatusOK { 39 | body, err := io.ReadAll(resp.Body) 40 | if err != nil { 41 | return nil, fmt.Errorf("failed to read response body: %w", err) 42 | } 43 | return mcp.NewToolResultError(fmt.Sprintf("failed to get user: %s", string(body))), nil 44 | } 45 | 46 | r, err := json.Marshal(user) 47 | if err != nil { 48 | return nil, fmt.Errorf("failed to marshal user: %w", err) 49 | } 50 | 51 | return mcp.NewToolResultText(string(r)), nil 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /pkg/github/context_tools_test.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net/http" 7 | "testing" 8 | "time" 9 | 10 | "github.com/github/github-mcp-server/pkg/translations" 11 | "github.com/google/go-github/v69/github" 12 | "github.com/migueleliasweb/go-github-mock/src/mock" 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func Test_GetMe(t *testing.T) { 18 | // Verify tool definition 19 | mockClient := github.NewClient(nil) 20 | tool, _ := GetMe(stubGetClientFn(mockClient), translations.NullTranslationHelper) 21 | 22 | assert.Equal(t, "get_me", tool.Name) 23 | assert.NotEmpty(t, tool.Description) 24 | assert.Contains(t, tool.InputSchema.Properties, "reason") 25 | assert.Empty(t, tool.InputSchema.Required) // No required parameters 26 | 27 | // Setup mock user response 28 | mockUser := &github.User{ 29 | Login: github.Ptr("testuser"), 30 | Name: github.Ptr("Test User"), 31 | Email: github.Ptr("test@example.com"), 32 | Bio: github.Ptr("GitHub user for testing"), 33 | Company: github.Ptr("Test Company"), 34 | Location: github.Ptr("Test Location"), 35 | HTMLURL: github.Ptr("https://github.com/testuser"), 36 | CreatedAt: &github.Timestamp{Time: time.Now().Add(-365 * 24 * time.Hour)}, 37 | Type: github.Ptr("User"), 38 | Plan: &github.Plan{ 39 | Name: github.Ptr("pro"), 40 | }, 41 | } 42 | 43 | tests := []struct { 44 | name string 45 | mockedClient *http.Client 46 | requestArgs map[string]interface{} 47 | expectError bool 48 | expectedUser *github.User 49 | expectedErrMsg string 50 | }{ 51 | { 52 | name: "successful get user", 53 | mockedClient: mock.NewMockedHTTPClient( 54 | mock.WithRequestMatch( 55 | mock.GetUser, 56 | mockUser, 57 | ), 58 | ), 59 | requestArgs: map[string]interface{}{}, 60 | expectError: false, 61 | expectedUser: mockUser, 62 | }, 63 | { 64 | name: "successful get user with reason", 65 | mockedClient: mock.NewMockedHTTPClient( 66 | mock.WithRequestMatch( 67 | mock.GetUser, 68 | mockUser, 69 | ), 70 | ), 71 | requestArgs: map[string]interface{}{ 72 | "reason": "Testing API", 73 | }, 74 | expectError: false, 75 | expectedUser: mockUser, 76 | }, 77 | { 78 | name: "get user fails", 79 | mockedClient: mock.NewMockedHTTPClient( 80 | mock.WithRequestMatchHandler( 81 | mock.GetUser, 82 | http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 83 | w.WriteHeader(http.StatusUnauthorized) 84 | _, _ = w.Write([]byte(`{"message": "Unauthorized"}`)) 85 | }), 86 | ), 87 | ), 88 | requestArgs: map[string]interface{}{}, 89 | expectError: true, 90 | expectedErrMsg: "failed to get user", 91 | }, 92 | } 93 | 94 | for _, tc := range tests { 95 | t.Run(tc.name, func(t *testing.T) { 96 | // Setup client with mock 97 | client := github.NewClient(tc.mockedClient) 98 | _, handler := GetMe(stubGetClientFn(client), translations.NullTranslationHelper) 99 | 100 | // Create call request 101 | request := createMCPRequest(tc.requestArgs) 102 | 103 | // Call handler 104 | result, err := handler(context.Background(), request) 105 | 106 | // Verify results 107 | if tc.expectError { 108 | require.Error(t, err) 109 | assert.Contains(t, err.Error(), tc.expectedErrMsg) 110 | return 111 | } 112 | 113 | require.NoError(t, err) 114 | 115 | // Parse result and get text content if no error 116 | textContent := getTextResult(t, result) 117 | 118 | // Unmarshal and verify the result 119 | var returnedUser github.User 120 | err = json.Unmarshal([]byte(textContent.Text), &returnedUser) 121 | require.NoError(t, err) 122 | 123 | // Verify user details 124 | assert.Equal(t, *tc.expectedUser.Login, *returnedUser.Login) 125 | assert.Equal(t, *tc.expectedUser.Name, *returnedUser.Name) 126 | assert.Equal(t, *tc.expectedUser.Email, *returnedUser.Email) 127 | assert.Equal(t, *tc.expectedUser.Bio, *returnedUser.Bio) 128 | assert.Equal(t, *tc.expectedUser.HTMLURL, *returnedUser.HTMLURL) 129 | assert.Equal(t, *tc.expectedUser.Type, *returnedUser.Type) 130 | }) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /pkg/github/dynamic_tools.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/github/github-mcp-server/pkg/toolsets" 9 | "github.com/github/github-mcp-server/pkg/translations" 10 | "github.com/mark3labs/mcp-go/mcp" 11 | "github.com/mark3labs/mcp-go/server" 12 | ) 13 | 14 | func ToolsetEnum(toolsetGroup *toolsets.ToolsetGroup) mcp.PropertyOption { 15 | toolsetNames := make([]string, 0, len(toolsetGroup.Toolsets)) 16 | for name := range toolsetGroup.Toolsets { 17 | toolsetNames = append(toolsetNames, name) 18 | } 19 | return mcp.Enum(toolsetNames...) 20 | } 21 | 22 | func EnableToolset(s *server.MCPServer, toolsetGroup *toolsets.ToolsetGroup, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { 23 | return mcp.NewTool("enable_toolset", 24 | mcp.WithDescription(t("TOOL_ENABLE_TOOLSET_DESCRIPTION", "Enable one of the sets of tools the GitHub MCP server provides, use get_toolset_tools and list_available_toolsets first to see what this will enable")), 25 | mcp.WithToolAnnotation(mcp.ToolAnnotation{ 26 | Title: t("TOOL_ENABLE_TOOLSET_USER_TITLE", "Enable a toolset"), 27 | // Not modifying GitHub data so no need to show a warning 28 | ReadOnlyHint: true, 29 | }), 30 | mcp.WithString("toolset", 31 | mcp.Required(), 32 | mcp.Description("The name of the toolset to enable"), 33 | ToolsetEnum(toolsetGroup), 34 | ), 35 | ), 36 | func(_ context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { 37 | // We need to convert the toolsets back to a map for JSON serialization 38 | toolsetName, err := requiredParam[string](request, "toolset") 39 | if err != nil { 40 | return mcp.NewToolResultError(err.Error()), nil 41 | } 42 | toolset := toolsetGroup.Toolsets[toolsetName] 43 | if toolset == nil { 44 | return mcp.NewToolResultError(fmt.Sprintf("Toolset %s not found", toolsetName)), nil 45 | } 46 | if toolset.Enabled { 47 | return mcp.NewToolResultText(fmt.Sprintf("Toolset %s is already enabled", toolsetName)), nil 48 | } 49 | 50 | toolset.Enabled = true 51 | 52 | // caution: this currently affects the global tools and notifies all clients: 53 | // 54 | // Send notification to all initialized sessions 55 | // s.sendNotificationToAllClients("notifications/tools/list_changed", nil) 56 | s.AddTools(toolset.GetActiveTools()...) 57 | 58 | return mcp.NewToolResultText(fmt.Sprintf("Toolset %s enabled", toolsetName)), nil 59 | } 60 | } 61 | 62 | func ListAvailableToolsets(toolsetGroup *toolsets.ToolsetGroup, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { 63 | return mcp.NewTool("list_available_toolsets", 64 | mcp.WithDescription(t("TOOL_LIST_AVAILABLE_TOOLSETS_DESCRIPTION", "List all available toolsets this GitHub MCP server can offer, providing the enabled status of each. Use this when a task could be achieved with a GitHub tool and the currently available tools aren't enough. Call get_toolset_tools with these toolset names to discover specific tools you can call")), 65 | mcp.WithToolAnnotation(mcp.ToolAnnotation{ 66 | Title: t("TOOL_LIST_AVAILABLE_TOOLSETS_USER_TITLE", "List available toolsets"), 67 | ReadOnlyHint: true, 68 | }), 69 | ), 70 | func(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { 71 | // We need to convert the toolsetGroup back to a map for JSON serialization 72 | 73 | payload := []map[string]string{} 74 | 75 | for name, ts := range toolsetGroup.Toolsets { 76 | { 77 | t := map[string]string{ 78 | "name": name, 79 | "description": ts.Description, 80 | "can_enable": "true", 81 | "currently_enabled": fmt.Sprintf("%t", ts.Enabled), 82 | } 83 | payload = append(payload, t) 84 | } 85 | } 86 | 87 | r, err := json.Marshal(payload) 88 | if err != nil { 89 | return nil, fmt.Errorf("failed to marshal features: %w", err) 90 | } 91 | 92 | return mcp.NewToolResultText(string(r)), nil 93 | } 94 | } 95 | 96 | func GetToolsetsTools(toolsetGroup *toolsets.ToolsetGroup, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { 97 | return mcp.NewTool("get_toolset_tools", 98 | mcp.WithDescription(t("TOOL_GET_TOOLSET_TOOLS_DESCRIPTION", "Lists all the capabilities that are enabled with the specified toolset, use this to get clarity on whether enabling a toolset would help you to complete a task")), 99 | mcp.WithToolAnnotation(mcp.ToolAnnotation{ 100 | Title: t("TOOL_GET_TOOLSET_TOOLS_USER_TITLE", "List all tools in a toolset"), 101 | ReadOnlyHint: true, 102 | }), 103 | mcp.WithString("toolset", 104 | mcp.Required(), 105 | mcp.Description("The name of the toolset you want to get the tools for"), 106 | ToolsetEnum(toolsetGroup), 107 | ), 108 | ), 109 | func(_ context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { 110 | // We need to convert the toolsetGroup back to a map for JSON serialization 111 | toolsetName, err := requiredParam[string](request, "toolset") 112 | if err != nil { 113 | return mcp.NewToolResultError(err.Error()), nil 114 | } 115 | toolset := toolsetGroup.Toolsets[toolsetName] 116 | if toolset == nil { 117 | return mcp.NewToolResultError(fmt.Sprintf("Toolset %s not found", toolsetName)), nil 118 | } 119 | payload := []map[string]string{} 120 | 121 | for _, st := range toolset.GetAvailableTools() { 122 | tool := map[string]string{ 123 | "name": st.Tool.Name, 124 | "description": st.Tool.Description, 125 | "can_enable": "true", 126 | "toolset": toolsetName, 127 | } 128 | payload = append(payload, tool) 129 | } 130 | 131 | r, err := json.Marshal(payload) 132 | if err != nil { 133 | return nil, fmt.Errorf("failed to marshal features: %w", err) 134 | } 135 | 136 | return mcp.NewToolResultText(string(r)), nil 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /pkg/github/helper_test.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/mark3labs/mcp-go/mcp" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | // expectQueryParams is a helper function to create a partial mock that expects a 14 | // request with the given query parameters, with the ability to chain a response handler. 15 | func expectQueryParams(t *testing.T, expectedQueryParams map[string]string) *partialMock { 16 | return &partialMock{ 17 | t: t, 18 | expectedQueryParams: expectedQueryParams, 19 | } 20 | } 21 | 22 | // expectRequestBody is a helper function to create a partial mock that expects a 23 | // request with the given body, with the ability to chain a response handler. 24 | func expectRequestBody(t *testing.T, expectedRequestBody any) *partialMock { 25 | return &partialMock{ 26 | t: t, 27 | expectedRequestBody: expectedRequestBody, 28 | } 29 | } 30 | 31 | type partialMock struct { 32 | t *testing.T 33 | expectedQueryParams map[string]string 34 | expectedRequestBody any 35 | } 36 | 37 | func (p *partialMock) andThen(responseHandler http.HandlerFunc) http.HandlerFunc { 38 | p.t.Helper() 39 | return func(w http.ResponseWriter, r *http.Request) { 40 | if p.expectedRequestBody != nil { 41 | var unmarshaledRequestBody any 42 | err := json.NewDecoder(r.Body).Decode(&unmarshaledRequestBody) 43 | require.NoError(p.t, err) 44 | 45 | require.Equal(p.t, p.expectedRequestBody, unmarshaledRequestBody) 46 | } 47 | 48 | if p.expectedQueryParams != nil { 49 | require.Equal(p.t, len(p.expectedQueryParams), len(r.URL.Query())) 50 | for k, v := range p.expectedQueryParams { 51 | require.Equal(p.t, v, r.URL.Query().Get(k)) 52 | } 53 | } 54 | 55 | responseHandler(w, r) 56 | } 57 | } 58 | 59 | // mockResponse is a helper function to create a mock HTTP response handler 60 | // that returns a specified status code and marshaled body. 61 | func mockResponse(t *testing.T, code int, body interface{}) http.HandlerFunc { 62 | t.Helper() 63 | return func(w http.ResponseWriter, _ *http.Request) { 64 | w.WriteHeader(code) 65 | b, err := json.Marshal(body) 66 | require.NoError(t, err) 67 | _, _ = w.Write(b) 68 | } 69 | } 70 | 71 | // createMCPRequest is a helper function to create a MCP request with the given arguments. 72 | func createMCPRequest(args map[string]interface{}) mcp.CallToolRequest { 73 | return mcp.CallToolRequest{ 74 | Params: struct { 75 | Name string `json:"name"` 76 | Arguments map[string]interface{} `json:"arguments,omitempty"` 77 | Meta *struct { 78 | ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"` 79 | } `json:"_meta,omitempty"` 80 | }{ 81 | Arguments: args, 82 | }, 83 | } 84 | } 85 | 86 | // getTextResult is a helper function that returns a text result from a tool call. 87 | func getTextResult(t *testing.T, result *mcp.CallToolResult) mcp.TextContent { 88 | t.Helper() 89 | assert.NotNil(t, result) 90 | require.Len(t, result.Content, 1) 91 | require.IsType(t, mcp.TextContent{}, result.Content[0]) 92 | textContent := result.Content[0].(mcp.TextContent) 93 | assert.Equal(t, "text", textContent.Type) 94 | return textContent 95 | } 96 | 97 | func TestOptionalParamOK(t *testing.T) { 98 | tests := []struct { 99 | name string 100 | args map[string]interface{} 101 | paramName string 102 | expectedVal interface{} 103 | expectedOk bool 104 | expectError bool 105 | errorMsg string 106 | }{ 107 | { 108 | name: "present and correct type (string)", 109 | args: map[string]interface{}{"myParam": "hello"}, 110 | paramName: "myParam", 111 | expectedVal: "hello", 112 | expectedOk: true, 113 | expectError: false, 114 | }, 115 | { 116 | name: "present and correct type (bool)", 117 | args: map[string]interface{}{"myParam": true}, 118 | paramName: "myParam", 119 | expectedVal: true, 120 | expectedOk: true, 121 | expectError: false, 122 | }, 123 | { 124 | name: "present and correct type (number)", 125 | args: map[string]interface{}{"myParam": float64(123)}, 126 | paramName: "myParam", 127 | expectedVal: float64(123), 128 | expectedOk: true, 129 | expectError: false, 130 | }, 131 | { 132 | name: "present but wrong type (string expected, got bool)", 133 | args: map[string]interface{}{"myParam": true}, 134 | paramName: "myParam", 135 | expectedVal: "", // Zero value for string 136 | expectedOk: true, // ok is true because param exists 137 | expectError: true, 138 | errorMsg: "parameter myParam is not of type string, is bool", 139 | }, 140 | { 141 | name: "present but wrong type (bool expected, got string)", 142 | args: map[string]interface{}{"myParam": "true"}, 143 | paramName: "myParam", 144 | expectedVal: false, // Zero value for bool 145 | expectedOk: true, // ok is true because param exists 146 | expectError: true, 147 | errorMsg: "parameter myParam is not of type bool, is string", 148 | }, 149 | { 150 | name: "parameter not present", 151 | args: map[string]interface{}{"anotherParam": "value"}, 152 | paramName: "myParam", 153 | expectedVal: "", // Zero value for string 154 | expectedOk: false, 155 | expectError: false, 156 | }, 157 | } 158 | 159 | for _, tc := range tests { 160 | t.Run(tc.name, func(t *testing.T) { 161 | request := createMCPRequest(tc.args) 162 | 163 | // Test with string type assertion 164 | if _, isString := tc.expectedVal.(string); isString || tc.errorMsg == "parameter myParam is not of type string, is bool" { 165 | val, ok, err := OptionalParamOK[string](request, tc.paramName) 166 | if tc.expectError { 167 | require.Error(t, err) 168 | assert.Contains(t, err.Error(), tc.errorMsg) 169 | assert.Equal(t, tc.expectedOk, ok) // Check ok even on error 170 | assert.Equal(t, tc.expectedVal, val) // Check zero value on error 171 | } else { 172 | require.NoError(t, err) 173 | assert.Equal(t, tc.expectedOk, ok) 174 | assert.Equal(t, tc.expectedVal, val) 175 | } 176 | } 177 | 178 | // Test with bool type assertion 179 | if _, isBool := tc.expectedVal.(bool); isBool || tc.errorMsg == "parameter myParam is not of type bool, is string" { 180 | val, ok, err := OptionalParamOK[bool](request, tc.paramName) 181 | if tc.expectError { 182 | require.Error(t, err) 183 | assert.Contains(t, err.Error(), tc.errorMsg) 184 | assert.Equal(t, tc.expectedOk, ok) // Check ok even on error 185 | assert.Equal(t, tc.expectedVal, val) // Check zero value on error 186 | } else { 187 | require.NoError(t, err) 188 | assert.Equal(t, tc.expectedOk, ok) 189 | assert.Equal(t, tc.expectedVal, val) 190 | } 191 | } 192 | 193 | // Test with float64 type assertion (for number case) 194 | if _, isFloat := tc.expectedVal.(float64); isFloat { 195 | val, ok, err := OptionalParamOK[float64](request, tc.paramName) 196 | if tc.expectError { 197 | // This case shouldn't happen for float64 in the defined tests 198 | require.Fail(t, "Unexpected error case for float64") 199 | } else { 200 | require.NoError(t, err) 201 | assert.Equal(t, tc.expectedOk, ok) 202 | assert.Equal(t, tc.expectedVal, val) 203 | } 204 | } 205 | }) 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /pkg/github/repository_resource.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "mime" 10 | "net/http" 11 | "path/filepath" 12 | "strings" 13 | 14 | "github.com/github/github-mcp-server/pkg/translations" 15 | "github.com/google/go-github/v69/github" 16 | "github.com/mark3labs/mcp-go/mcp" 17 | "github.com/mark3labs/mcp-go/server" 18 | ) 19 | 20 | // GetRepositoryResourceContent defines the resource template and handler for getting repository content. 21 | func GetRepositoryResourceContent(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { 22 | return mcp.NewResourceTemplate( 23 | "repo://{owner}/{repo}/contents{/path*}", // Resource template 24 | t("RESOURCE_REPOSITORY_CONTENT_DESCRIPTION", "Repository Content"), 25 | ), 26 | RepositoryResourceContentsHandler(getClient) 27 | } 28 | 29 | // GetRepositoryResourceBranchContent defines the resource template and handler for getting repository content for a branch. 30 | func GetRepositoryResourceBranchContent(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { 31 | return mcp.NewResourceTemplate( 32 | "repo://{owner}/{repo}/refs/heads/{branch}/contents{/path*}", // Resource template 33 | t("RESOURCE_REPOSITORY_CONTENT_BRANCH_DESCRIPTION", "Repository Content for specific branch"), 34 | ), 35 | RepositoryResourceContentsHandler(getClient) 36 | } 37 | 38 | // GetRepositoryResourceCommitContent defines the resource template and handler for getting repository content for a commit. 39 | func GetRepositoryResourceCommitContent(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { 40 | return mcp.NewResourceTemplate( 41 | "repo://{owner}/{repo}/sha/{sha}/contents{/path*}", // Resource template 42 | t("RESOURCE_REPOSITORY_CONTENT_COMMIT_DESCRIPTION", "Repository Content for specific commit"), 43 | ), 44 | RepositoryResourceContentsHandler(getClient) 45 | } 46 | 47 | // GetRepositoryResourceTagContent defines the resource template and handler for getting repository content for a tag. 48 | func GetRepositoryResourceTagContent(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { 49 | return mcp.NewResourceTemplate( 50 | "repo://{owner}/{repo}/refs/tags/{tag}/contents{/path*}", // Resource template 51 | t("RESOURCE_REPOSITORY_CONTENT_TAG_DESCRIPTION", "Repository Content for specific tag"), 52 | ), 53 | RepositoryResourceContentsHandler(getClient) 54 | } 55 | 56 | // GetRepositoryResourcePrContent defines the resource template and handler for getting repository content for a pull request. 57 | func GetRepositoryResourcePrContent(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { 58 | return mcp.NewResourceTemplate( 59 | "repo://{owner}/{repo}/refs/pull/{prNumber}/head/contents{/path*}", // Resource template 60 | t("RESOURCE_REPOSITORY_CONTENT_PR_DESCRIPTION", "Repository Content for specific pull request"), 61 | ), 62 | RepositoryResourceContentsHandler(getClient) 63 | } 64 | 65 | // RepositoryResourceContentsHandler returns a handler function for repository content requests. 66 | func RepositoryResourceContentsHandler(getClient GetClientFn) func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { 67 | return func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { 68 | // the matcher will give []string with one element 69 | // https://github.com/mark3labs/mcp-go/pull/54 70 | o, ok := request.Params.Arguments["owner"].([]string) 71 | if !ok || len(o) == 0 { 72 | return nil, errors.New("owner is required") 73 | } 74 | owner := o[0] 75 | 76 | r, ok := request.Params.Arguments["repo"].([]string) 77 | if !ok || len(r) == 0 { 78 | return nil, errors.New("repo is required") 79 | } 80 | repo := r[0] 81 | 82 | // path should be a joined list of the path parts 83 | path := "" 84 | p, ok := request.Params.Arguments["path"].([]string) 85 | if ok { 86 | path = strings.Join(p, "/") 87 | } 88 | 89 | opts := &github.RepositoryContentGetOptions{} 90 | 91 | sha, ok := request.Params.Arguments["sha"].([]string) 92 | if ok && len(sha) > 0 { 93 | opts.Ref = sha[0] 94 | } 95 | 96 | branch, ok := request.Params.Arguments["branch"].([]string) 97 | if ok && len(branch) > 0 { 98 | opts.Ref = "refs/heads/" + branch[0] 99 | } 100 | 101 | tag, ok := request.Params.Arguments["tag"].([]string) 102 | if ok && len(tag) > 0 { 103 | opts.Ref = "refs/tags/" + tag[0] 104 | } 105 | prNumber, ok := request.Params.Arguments["prNumber"].([]string) 106 | if ok && len(prNumber) > 0 { 107 | opts.Ref = "refs/pull/" + prNumber[0] + "/head" 108 | } 109 | 110 | client, err := getClient(ctx) 111 | if err != nil { 112 | return nil, fmt.Errorf("failed to get GitHub client: %w", err) 113 | } 114 | fileContent, directoryContent, _, err := client.Repositories.GetContents(ctx, owner, repo, path, opts) 115 | if err != nil { 116 | return nil, err 117 | } 118 | 119 | if directoryContent != nil { 120 | var resources []mcp.ResourceContents 121 | for _, entry := range directoryContent { 122 | mimeType := "text/directory" 123 | if entry.GetType() == "file" { 124 | // this is system dependent, and a best guess 125 | ext := filepath.Ext(entry.GetName()) 126 | mimeType = mime.TypeByExtension(ext) 127 | if ext == ".md" { 128 | mimeType = "text/markdown" 129 | } 130 | } 131 | resources = append(resources, mcp.TextResourceContents{ 132 | URI: entry.GetHTMLURL(), 133 | MIMEType: mimeType, 134 | Text: entry.GetName(), 135 | }) 136 | 137 | } 138 | return resources, nil 139 | 140 | } 141 | if fileContent != nil { 142 | if fileContent.Content != nil { 143 | // download the file content from fileContent.GetDownloadURL() and use the content-type header to determine the MIME type 144 | // and return the content as a blob unless it is a text file, where you can return the content as text 145 | req, err := http.NewRequest("GET", fileContent.GetDownloadURL(), nil) 146 | if err != nil { 147 | return nil, fmt.Errorf("failed to create request: %w", err) 148 | } 149 | 150 | resp, err := client.Client().Do(req) 151 | if err != nil { 152 | return nil, fmt.Errorf("failed to send request: %w", err) 153 | } 154 | defer func() { _ = resp.Body.Close() }() 155 | 156 | if resp.StatusCode != http.StatusOK { 157 | body, err := io.ReadAll(resp.Body) 158 | if err != nil { 159 | return nil, fmt.Errorf("failed to read response body: %w", err) 160 | } 161 | return nil, fmt.Errorf("failed to fetch file content: %s", string(body)) 162 | } 163 | 164 | ext := filepath.Ext(fileContent.GetName()) 165 | mimeType := resp.Header.Get("Content-Type") 166 | if ext == ".md" { 167 | mimeType = "text/markdown" 168 | } else if mimeType == "" { 169 | // backstop to the file extension if the content type is not set 170 | mimeType = mime.TypeByExtension(filepath.Ext(fileContent.GetName())) 171 | } 172 | 173 | // if the content is a string, return it as text 174 | if strings.HasPrefix(mimeType, "text") { 175 | content, err := io.ReadAll(resp.Body) 176 | if err != nil { 177 | return nil, fmt.Errorf("failed to parse the response body: %w", err) 178 | } 179 | 180 | return []mcp.ResourceContents{ 181 | mcp.TextResourceContents{ 182 | URI: request.Params.URI, 183 | MIMEType: mimeType, 184 | Text: string(content), 185 | }, 186 | }, nil 187 | } 188 | // otherwise, read the content and encode it as base64 189 | decodedContent, err := io.ReadAll(resp.Body) 190 | if err != nil { 191 | return nil, fmt.Errorf("failed to parse the response body: %w", err) 192 | } 193 | 194 | return []mcp.ResourceContents{ 195 | mcp.BlobResourceContents{ 196 | URI: request.Params.URI, 197 | MIMEType: mimeType, 198 | Blob: base64.StdEncoding.EncodeToString(decodedContent), // Encode content as Base64 199 | }, 200 | }, nil 201 | } 202 | } 203 | 204 | return nil, errors.New("no repository resource content found") 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /pkg/github/repository_resource_test.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/github/github-mcp-server/pkg/translations" 9 | "github.com/google/go-github/v69/github" 10 | "github.com/mark3labs/mcp-go/mcp" 11 | "github.com/migueleliasweb/go-github-mock/src/mock" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | var GetRawReposContentsByOwnerByRepoByPath mock.EndpointPattern = mock.EndpointPattern{ 16 | Pattern: "/{owner}/{repo}/main/{path:.+}", 17 | Method: "GET", 18 | } 19 | 20 | func Test_repositoryResourceContentsHandler(t *testing.T) { 21 | mockDirContent := []*github.RepositoryContent{ 22 | { 23 | Type: github.Ptr("file"), 24 | Name: github.Ptr("README.md"), 25 | Path: github.Ptr("README.md"), 26 | SHA: github.Ptr("abc123"), 27 | Size: github.Ptr(42), 28 | HTMLURL: github.Ptr("https://github.com/owner/repo/blob/main/README.md"), 29 | DownloadURL: github.Ptr("https://raw.githubusercontent.com/owner/repo/main/README.md"), 30 | }, 31 | { 32 | Type: github.Ptr("dir"), 33 | Name: github.Ptr("src"), 34 | Path: github.Ptr("src"), 35 | SHA: github.Ptr("def456"), 36 | HTMLURL: github.Ptr("https://github.com/owner/repo/tree/main/src"), 37 | DownloadURL: github.Ptr("https://raw.githubusercontent.com/owner/repo/main/src"), 38 | }, 39 | } 40 | expectedDirContent := []mcp.TextResourceContents{ 41 | { 42 | URI: "https://github.com/owner/repo/blob/main/README.md", 43 | MIMEType: "text/markdown", 44 | Text: "README.md", 45 | }, 46 | { 47 | URI: "https://github.com/owner/repo/tree/main/src", 48 | MIMEType: "text/directory", 49 | Text: "src", 50 | }, 51 | } 52 | 53 | mockTextContent := &github.RepositoryContent{ 54 | Type: github.Ptr("file"), 55 | Name: github.Ptr("README.md"), 56 | Path: github.Ptr("README.md"), 57 | Content: github.Ptr("# Test Repository\n\nThis is a test repository."), 58 | SHA: github.Ptr("abc123"), 59 | Size: github.Ptr(42), 60 | HTMLURL: github.Ptr("https://github.com/owner/repo/blob/main/README.md"), 61 | DownloadURL: github.Ptr("https://raw.githubusercontent.com/owner/repo/main/README.md"), 62 | } 63 | 64 | mockFileContent := &github.RepositoryContent{ 65 | Type: github.Ptr("file"), 66 | Name: github.Ptr("data.png"), 67 | Path: github.Ptr("data.png"), 68 | Content: github.Ptr("IyBUZXN0IFJlcG9zaXRvcnkKClRoaXMgaXMgYSB0ZXN0IHJlcG9zaXRvcnku"), // Base64 encoded "# Test Repository\n\nThis is a test repository." 69 | SHA: github.Ptr("abc123"), 70 | Size: github.Ptr(42), 71 | HTMLURL: github.Ptr("https://github.com/owner/repo/blob/main/data.png"), 72 | DownloadURL: github.Ptr("https://raw.githubusercontent.com/owner/repo/main/data.png"), 73 | } 74 | 75 | expectedFileContent := []mcp.BlobResourceContents{ 76 | { 77 | Blob: "IyBUZXN0IFJlcG9zaXRvcnkKClRoaXMgaXMgYSB0ZXN0IHJlcG9zaXRvcnku", 78 | MIMEType: "image/png", 79 | URI: "", 80 | }, 81 | } 82 | 83 | expectedTextContent := []mcp.TextResourceContents{ 84 | { 85 | Text: "# Test Repository\n\nThis is a test repository.", 86 | MIMEType: "text/markdown", 87 | URI: "", 88 | }, 89 | } 90 | 91 | tests := []struct { 92 | name string 93 | mockedClient *http.Client 94 | requestArgs map[string]any 95 | expectError string 96 | expectedResult any 97 | expectedErrMsg string 98 | }{ 99 | { 100 | name: "missing owner", 101 | mockedClient: mock.NewMockedHTTPClient( 102 | mock.WithRequestMatch( 103 | mock.GetReposContentsByOwnerByRepoByPath, 104 | mockFileContent, 105 | ), 106 | ), 107 | requestArgs: map[string]any{}, 108 | expectError: "owner is required", 109 | }, 110 | { 111 | name: "missing repo", 112 | mockedClient: mock.NewMockedHTTPClient( 113 | mock.WithRequestMatch( 114 | mock.GetReposContentsByOwnerByRepoByPath, 115 | mockFileContent, 116 | ), 117 | ), 118 | requestArgs: map[string]any{ 119 | "owner": []string{"owner"}, 120 | }, 121 | expectError: "repo is required", 122 | }, 123 | { 124 | name: "successful blob content fetch", 125 | mockedClient: mock.NewMockedHTTPClient( 126 | mock.WithRequestMatch( 127 | mock.GetReposContentsByOwnerByRepoByPath, 128 | mockFileContent, 129 | ), 130 | mock.WithRequestMatchHandler( 131 | GetRawReposContentsByOwnerByRepoByPath, 132 | http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 133 | w.Header().Set("Content-Type", "image/png") 134 | // as this is given as a png, it will return the content as a blob 135 | _, err := w.Write([]byte("# Test Repository\n\nThis is a test repository.")) 136 | require.NoError(t, err) 137 | }), 138 | ), 139 | ), 140 | requestArgs: map[string]any{ 141 | "owner": []string{"owner"}, 142 | "repo": []string{"repo"}, 143 | "path": []string{"data.png"}, 144 | "branch": []string{"main"}, 145 | }, 146 | expectedResult: expectedFileContent, 147 | }, 148 | { 149 | name: "successful text content fetch", 150 | mockedClient: mock.NewMockedHTTPClient( 151 | mock.WithRequestMatch( 152 | mock.GetReposContentsByOwnerByRepoByPath, 153 | mockTextContent, 154 | ), 155 | mock.WithRequestMatch( 156 | GetRawReposContentsByOwnerByRepoByPath, 157 | []byte("# Test Repository\n\nThis is a test repository."), 158 | ), 159 | ), 160 | requestArgs: map[string]any{ 161 | "owner": []string{"owner"}, 162 | "repo": []string{"repo"}, 163 | "path": []string{"README.md"}, 164 | "branch": []string{"main"}, 165 | }, 166 | expectedResult: expectedTextContent, 167 | }, 168 | { 169 | name: "successful directory content fetch", 170 | mockedClient: mock.NewMockedHTTPClient( 171 | mock.WithRequestMatch( 172 | mock.GetReposContentsByOwnerByRepoByPath, 173 | mockDirContent, 174 | ), 175 | ), 176 | requestArgs: map[string]any{ 177 | "owner": []string{"owner"}, 178 | "repo": []string{"repo"}, 179 | "path": []string{"src"}, 180 | }, 181 | expectedResult: expectedDirContent, 182 | }, 183 | { 184 | name: "no data", 185 | mockedClient: mock.NewMockedHTTPClient( 186 | mock.WithRequestMatch( 187 | mock.GetReposContentsByOwnerByRepoByPath, 188 | ), 189 | ), 190 | requestArgs: map[string]any{ 191 | "owner": []string{"owner"}, 192 | "repo": []string{"repo"}, 193 | "path": []string{"src"}, 194 | }, 195 | expectedResult: nil, 196 | expectError: "no repository resource content found", 197 | }, 198 | { 199 | name: "empty data", 200 | mockedClient: mock.NewMockedHTTPClient( 201 | mock.WithRequestMatch( 202 | mock.GetReposContentsByOwnerByRepoByPath, 203 | []*github.RepositoryContent{}, 204 | ), 205 | ), 206 | requestArgs: map[string]any{ 207 | "owner": []string{"owner"}, 208 | "repo": []string{"repo"}, 209 | "path": []string{"src"}, 210 | }, 211 | expectedResult: nil, 212 | }, 213 | { 214 | name: "content fetch fails", 215 | mockedClient: mock.NewMockedHTTPClient( 216 | mock.WithRequestMatchHandler( 217 | mock.GetReposContentsByOwnerByRepoByPath, 218 | http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 219 | w.WriteHeader(http.StatusNotFound) 220 | _, _ = w.Write([]byte(`{"message": "Not Found"}`)) 221 | }), 222 | ), 223 | ), 224 | requestArgs: map[string]any{ 225 | "owner": []string{"owner"}, 226 | "repo": []string{"repo"}, 227 | "path": []string{"nonexistent.md"}, 228 | "branch": []string{"main"}, 229 | }, 230 | expectError: "404 Not Found", 231 | }, 232 | } 233 | 234 | for _, tc := range tests { 235 | t.Run(tc.name, func(t *testing.T) { 236 | client := github.NewClient(tc.mockedClient) 237 | handler := RepositoryResourceContentsHandler((stubGetClientFn(client))) 238 | 239 | request := mcp.ReadResourceRequest{ 240 | Params: struct { 241 | URI string `json:"uri"` 242 | Arguments map[string]any `json:"arguments,omitempty"` 243 | }{ 244 | Arguments: tc.requestArgs, 245 | }, 246 | } 247 | 248 | resp, err := handler(context.TODO(), request) 249 | 250 | if tc.expectError != "" { 251 | require.ErrorContains(t, err, tc.expectedErrMsg) 252 | return 253 | } 254 | 255 | require.NoError(t, err) 256 | require.ElementsMatch(t, resp, tc.expectedResult) 257 | }) 258 | } 259 | } 260 | 261 | func Test_GetRepositoryResourceContent(t *testing.T) { 262 | tmpl, _ := GetRepositoryResourceContent(nil, translations.NullTranslationHelper) 263 | require.Equal(t, "repo://{owner}/{repo}/contents{/path*}", tmpl.URITemplate.Raw()) 264 | } 265 | 266 | func Test_GetRepositoryResourceBranchContent(t *testing.T) { 267 | tmpl, _ := GetRepositoryResourceBranchContent(nil, translations.NullTranslationHelper) 268 | require.Equal(t, "repo://{owner}/{repo}/refs/heads/{branch}/contents{/path*}", tmpl.URITemplate.Raw()) 269 | } 270 | func Test_GetRepositoryResourceCommitContent(t *testing.T) { 271 | tmpl, _ := GetRepositoryResourceCommitContent(nil, translations.NullTranslationHelper) 272 | require.Equal(t, "repo://{owner}/{repo}/sha/{sha}/contents{/path*}", tmpl.URITemplate.Raw()) 273 | } 274 | 275 | func Test_GetRepositoryResourceTagContent(t *testing.T) { 276 | tmpl, _ := GetRepositoryResourceTagContent(nil, translations.NullTranslationHelper) 277 | require.Equal(t, "repo://{owner}/{repo}/refs/tags/{tag}/contents{/path*}", tmpl.URITemplate.Raw()) 278 | } 279 | 280 | func Test_GetRepositoryResourcePrContent(t *testing.T) { 281 | tmpl, _ := GetRepositoryResourcePrContent(nil, translations.NullTranslationHelper) 282 | require.Equal(t, "repo://{owner}/{repo}/refs/pull/{prNumber}/head/contents{/path*}", tmpl.URITemplate.Raw()) 283 | } 284 | -------------------------------------------------------------------------------- /pkg/github/resources.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "github.com/github/github-mcp-server/pkg/translations" 5 | "github.com/mark3labs/mcp-go/server" 6 | ) 7 | 8 | func RegisterResources(s *server.MCPServer, getClient GetClientFn, t translations.TranslationHelperFunc) { 9 | s.AddResourceTemplate(GetRepositoryResourceContent(getClient, t)) 10 | s.AddResourceTemplate(GetRepositoryResourceBranchContent(getClient, t)) 11 | s.AddResourceTemplate(GetRepositoryResourceCommitContent(getClient, t)) 12 | s.AddResourceTemplate(GetRepositoryResourceTagContent(getClient, t)) 13 | s.AddResourceTemplate(GetRepositoryResourcePrContent(getClient, t)) 14 | } 15 | -------------------------------------------------------------------------------- /pkg/github/search.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | 9 | "github.com/github/github-mcp-server/pkg/translations" 10 | "github.com/google/go-github/v69/github" 11 | "github.com/mark3labs/mcp-go/mcp" 12 | "github.com/mark3labs/mcp-go/server" 13 | ) 14 | 15 | // SearchRepositories creates a tool to search for GitHub repositories. 16 | func SearchRepositories(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { 17 | return mcp.NewTool("search_repositories", 18 | mcp.WithDescription(t("TOOL_SEARCH_REPOSITORIES_DESCRIPTION", "Search for GitHub repositories")), 19 | mcp.WithToolAnnotation(mcp.ToolAnnotation{ 20 | Title: t("TOOL_SEARCH_REPOSITORIES_USER_TITLE", "Search repositories"), 21 | ReadOnlyHint: true, 22 | }), 23 | mcp.WithString("query", 24 | mcp.Required(), 25 | mcp.Description("Search query"), 26 | ), 27 | WithPagination(), 28 | ), 29 | func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { 30 | query, err := requiredParam[string](request, "query") 31 | if err != nil { 32 | return mcp.NewToolResultError(err.Error()), nil 33 | } 34 | pagination, err := OptionalPaginationParams(request) 35 | if err != nil { 36 | return mcp.NewToolResultError(err.Error()), nil 37 | } 38 | 39 | opts := &github.SearchOptions{ 40 | ListOptions: github.ListOptions{ 41 | Page: pagination.page, 42 | PerPage: pagination.perPage, 43 | }, 44 | } 45 | 46 | client, err := getClient(ctx) 47 | if err != nil { 48 | return nil, fmt.Errorf("failed to get GitHub client: %w", err) 49 | } 50 | result, resp, err := client.Search.Repositories(ctx, query, opts) 51 | if err != nil { 52 | return nil, fmt.Errorf("failed to search repositories: %w", err) 53 | } 54 | defer func() { _ = resp.Body.Close() }() 55 | 56 | if resp.StatusCode != 200 { 57 | body, err := io.ReadAll(resp.Body) 58 | if err != nil { 59 | return nil, fmt.Errorf("failed to read response body: %w", err) 60 | } 61 | return mcp.NewToolResultError(fmt.Sprintf("failed to search repositories: %s", string(body))), nil 62 | } 63 | 64 | r, err := json.Marshal(result) 65 | if err != nil { 66 | return nil, fmt.Errorf("failed to marshal response: %w", err) 67 | } 68 | 69 | return mcp.NewToolResultText(string(r)), nil 70 | } 71 | } 72 | 73 | // SearchCode creates a tool to search for code across GitHub repositories. 74 | func SearchCode(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { 75 | return mcp.NewTool("search_code", 76 | mcp.WithDescription(t("TOOL_SEARCH_CODE_DESCRIPTION", "Search for code across GitHub repositories")), 77 | mcp.WithToolAnnotation(mcp.ToolAnnotation{ 78 | Title: t("TOOL_SEARCH_CODE_USER_TITLE", "Search code"), 79 | ReadOnlyHint: true, 80 | }), 81 | mcp.WithString("q", 82 | mcp.Required(), 83 | mcp.Description("Search query using GitHub code search syntax"), 84 | ), 85 | mcp.WithString("sort", 86 | mcp.Description("Sort field ('indexed' only)"), 87 | ), 88 | mcp.WithString("order", 89 | mcp.Description("Sort order"), 90 | mcp.Enum("asc", "desc"), 91 | ), 92 | WithPagination(), 93 | ), 94 | func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { 95 | query, err := requiredParam[string](request, "q") 96 | if err != nil { 97 | return mcp.NewToolResultError(err.Error()), nil 98 | } 99 | sort, err := OptionalParam[string](request, "sort") 100 | if err != nil { 101 | return mcp.NewToolResultError(err.Error()), nil 102 | } 103 | order, err := OptionalParam[string](request, "order") 104 | if err != nil { 105 | return mcp.NewToolResultError(err.Error()), nil 106 | } 107 | pagination, err := OptionalPaginationParams(request) 108 | if err != nil { 109 | return mcp.NewToolResultError(err.Error()), nil 110 | } 111 | 112 | opts := &github.SearchOptions{ 113 | Sort: sort, 114 | Order: order, 115 | ListOptions: github.ListOptions{ 116 | PerPage: pagination.perPage, 117 | Page: pagination.page, 118 | }, 119 | } 120 | 121 | client, err := getClient(ctx) 122 | if err != nil { 123 | return nil, fmt.Errorf("failed to get GitHub client: %w", err) 124 | } 125 | 126 | result, resp, err := client.Search.Code(ctx, query, opts) 127 | if err != nil { 128 | return nil, fmt.Errorf("failed to search code: %w", err) 129 | } 130 | defer func() { _ = resp.Body.Close() }() 131 | 132 | if resp.StatusCode != 200 { 133 | body, err := io.ReadAll(resp.Body) 134 | if err != nil { 135 | return nil, fmt.Errorf("failed to read response body: %w", err) 136 | } 137 | return mcp.NewToolResultError(fmt.Sprintf("failed to search code: %s", string(body))), nil 138 | } 139 | 140 | r, err := json.Marshal(result) 141 | if err != nil { 142 | return nil, fmt.Errorf("failed to marshal response: %w", err) 143 | } 144 | 145 | return mcp.NewToolResultText(string(r)), nil 146 | } 147 | } 148 | 149 | // SearchUsers creates a tool to search for GitHub users. 150 | func SearchUsers(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { 151 | return mcp.NewTool("search_users", 152 | mcp.WithDescription(t("TOOL_SEARCH_USERS_DESCRIPTION", "Search for GitHub users")), 153 | mcp.WithToolAnnotation(mcp.ToolAnnotation{ 154 | Title: t("TOOL_SEARCH_USERS_USER_TITLE", "Search users"), 155 | ReadOnlyHint: true, 156 | }), 157 | mcp.WithString("q", 158 | mcp.Required(), 159 | mcp.Description("Search query using GitHub users search syntax"), 160 | ), 161 | mcp.WithString("sort", 162 | mcp.Description("Sort field by category"), 163 | mcp.Enum("followers", "repositories", "joined"), 164 | ), 165 | mcp.WithString("order", 166 | mcp.Description("Sort order"), 167 | mcp.Enum("asc", "desc"), 168 | ), 169 | WithPagination(), 170 | ), 171 | func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { 172 | query, err := requiredParam[string](request, "q") 173 | if err != nil { 174 | return mcp.NewToolResultError(err.Error()), nil 175 | } 176 | sort, err := OptionalParam[string](request, "sort") 177 | if err != nil { 178 | return mcp.NewToolResultError(err.Error()), nil 179 | } 180 | order, err := OptionalParam[string](request, "order") 181 | if err != nil { 182 | return mcp.NewToolResultError(err.Error()), nil 183 | } 184 | pagination, err := OptionalPaginationParams(request) 185 | if err != nil { 186 | return mcp.NewToolResultError(err.Error()), nil 187 | } 188 | 189 | opts := &github.SearchOptions{ 190 | Sort: sort, 191 | Order: order, 192 | ListOptions: github.ListOptions{ 193 | PerPage: pagination.perPage, 194 | Page: pagination.page, 195 | }, 196 | } 197 | 198 | client, err := getClient(ctx) 199 | if err != nil { 200 | return nil, fmt.Errorf("failed to get GitHub client: %w", err) 201 | } 202 | 203 | result, resp, err := client.Search.Users(ctx, query, opts) 204 | if err != nil { 205 | return nil, fmt.Errorf("failed to search users: %w", err) 206 | } 207 | defer func() { _ = resp.Body.Close() }() 208 | 209 | if resp.StatusCode != 200 { 210 | body, err := io.ReadAll(resp.Body) 211 | if err != nil { 212 | return nil, fmt.Errorf("failed to read response body: %w", err) 213 | } 214 | return mcp.NewToolResultError(fmt.Sprintf("failed to search users: %s", string(body))), nil 215 | } 216 | 217 | r, err := json.Marshal(result) 218 | if err != nil { 219 | return nil, fmt.Errorf("failed to marshal response: %w", err) 220 | } 221 | 222 | return mcp.NewToolResultText(string(r)), nil 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /pkg/github/secret_scanning.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | 10 | "github.com/github/github-mcp-server/pkg/translations" 11 | "github.com/google/go-github/v69/github" 12 | "github.com/mark3labs/mcp-go/mcp" 13 | "github.com/mark3labs/mcp-go/server" 14 | ) 15 | 16 | func GetSecretScanningAlert(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { 17 | return mcp.NewTool( 18 | "get_secret_scanning_alert", 19 | mcp.WithDescription(t("TOOL_GET_SECRET_SCANNING_ALERT_DESCRIPTION", "Get details of a specific secret scanning alert in a GitHub repository.")), 20 | mcp.WithToolAnnotation(mcp.ToolAnnotation{ 21 | Title: t("TOOL_GET_SECRET_SCANNING_ALERT_USER_TITLE", "Get secret scanning alert"), 22 | ReadOnlyHint: true, 23 | }), 24 | mcp.WithString("owner", 25 | mcp.Required(), 26 | mcp.Description("The owner of the repository."), 27 | ), 28 | mcp.WithString("repo", 29 | mcp.Required(), 30 | mcp.Description("The name of the repository."), 31 | ), 32 | mcp.WithNumber("alertNumber", 33 | mcp.Required(), 34 | mcp.Description("The number of the alert."), 35 | ), 36 | ), 37 | func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { 38 | owner, err := requiredParam[string](request, "owner") 39 | if err != nil { 40 | return mcp.NewToolResultError(err.Error()), nil 41 | } 42 | repo, err := requiredParam[string](request, "repo") 43 | if err != nil { 44 | return mcp.NewToolResultError(err.Error()), nil 45 | } 46 | alertNumber, err := RequiredInt(request, "alertNumber") 47 | if err != nil { 48 | return mcp.NewToolResultError(err.Error()), nil 49 | } 50 | 51 | client, err := getClient(ctx) 52 | if err != nil { 53 | return nil, fmt.Errorf("failed to get GitHub client: %w", err) 54 | } 55 | 56 | alert, resp, err := client.SecretScanning.GetAlert(ctx, owner, repo, int64(alertNumber)) 57 | if err != nil { 58 | return nil, fmt.Errorf("failed to get alert: %w", err) 59 | } 60 | defer func() { _ = resp.Body.Close() }() 61 | 62 | if resp.StatusCode != http.StatusOK { 63 | body, err := io.ReadAll(resp.Body) 64 | if err != nil { 65 | return nil, fmt.Errorf("failed to read response body: %w", err) 66 | } 67 | return mcp.NewToolResultError(fmt.Sprintf("failed to get alert: %s", string(body))), nil 68 | } 69 | 70 | r, err := json.Marshal(alert) 71 | if err != nil { 72 | return nil, fmt.Errorf("failed to marshal alert: %w", err) 73 | } 74 | 75 | return mcp.NewToolResultText(string(r)), nil 76 | } 77 | } 78 | 79 | func ListSecretScanningAlerts(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { 80 | return mcp.NewTool( 81 | "list_secret_scanning_alerts", 82 | mcp.WithDescription(t("TOOL_LIST_SECRET_SCANNING_ALERTS_DESCRIPTION", "List secret scanning alerts in a GitHub repository.")), 83 | mcp.WithToolAnnotation(mcp.ToolAnnotation{ 84 | Title: t("TOOL_LIST_SECRET_SCANNING_ALERTS_USER_TITLE", "List secret scanning alerts"), 85 | ReadOnlyHint: true, 86 | }), 87 | mcp.WithString("owner", 88 | mcp.Required(), 89 | mcp.Description("The owner of the repository."), 90 | ), 91 | mcp.WithString("repo", 92 | mcp.Required(), 93 | mcp.Description("The name of the repository."), 94 | ), 95 | mcp.WithString("state", 96 | mcp.Description("Filter by state"), 97 | mcp.Enum("open", "resolved"), 98 | ), 99 | mcp.WithString("secret_type", 100 | mcp.Description("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."), 101 | ), 102 | mcp.WithString("resolution", 103 | mcp.Description("Filter by resolution"), 104 | mcp.Enum("false_positive", "wont_fix", "revoked", "pattern_edited", "pattern_deleted", "used_in_tests"), 105 | ), 106 | ), 107 | func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { 108 | owner, err := requiredParam[string](request, "owner") 109 | if err != nil { 110 | return mcp.NewToolResultError(err.Error()), nil 111 | } 112 | repo, err := requiredParam[string](request, "repo") 113 | if err != nil { 114 | return mcp.NewToolResultError(err.Error()), nil 115 | } 116 | state, err := OptionalParam[string](request, "state") 117 | if err != nil { 118 | return mcp.NewToolResultError(err.Error()), nil 119 | } 120 | secretType, err := OptionalParam[string](request, "secret_type") 121 | if err != nil { 122 | return mcp.NewToolResultError(err.Error()), nil 123 | } 124 | resolution, err := OptionalParam[string](request, "resolution") 125 | if err != nil { 126 | return mcp.NewToolResultError(err.Error()), nil 127 | } 128 | 129 | client, err := getClient(ctx) 130 | if err != nil { 131 | return nil, fmt.Errorf("failed to get GitHub client: %w", err) 132 | } 133 | alerts, resp, err := client.SecretScanning.ListAlertsForRepo(ctx, owner, repo, &github.SecretScanningAlertListOptions{State: state, SecretType: secretType, Resolution: resolution}) 134 | if err != nil { 135 | return nil, fmt.Errorf("failed to list alerts: %w", err) 136 | } 137 | defer func() { _ = resp.Body.Close() }() 138 | 139 | if resp.StatusCode != http.StatusOK { 140 | body, err := io.ReadAll(resp.Body) 141 | if err != nil { 142 | return nil, fmt.Errorf("failed to read response body: %w", err) 143 | } 144 | return mcp.NewToolResultError(fmt.Sprintf("failed to list alerts: %s", string(body))), nil 145 | } 146 | 147 | r, err := json.Marshal(alerts) 148 | if err != nil { 149 | return nil, fmt.Errorf("failed to marshal alerts: %w", err) 150 | } 151 | 152 | return mcp.NewToolResultText(string(r)), nil 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /pkg/github/secret_scanning_test.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net/http" 7 | "testing" 8 | 9 | "github.com/github/github-mcp-server/pkg/translations" 10 | "github.com/google/go-github/v69/github" 11 | "github.com/migueleliasweb/go-github-mock/src/mock" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func Test_GetSecretScanningAlert(t *testing.T) { 17 | mockClient := github.NewClient(nil) 18 | tool, _ := GetSecretScanningAlert(stubGetClientFn(mockClient), translations.NullTranslationHelper) 19 | 20 | assert.Equal(t, "get_secret_scanning_alert", tool.Name) 21 | assert.NotEmpty(t, tool.Description) 22 | assert.Contains(t, tool.InputSchema.Properties, "owner") 23 | assert.Contains(t, tool.InputSchema.Properties, "repo") 24 | assert.Contains(t, tool.InputSchema.Properties, "alertNumber") 25 | assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "alertNumber"}) 26 | 27 | // Setup mock alert for success case 28 | mockAlert := &github.SecretScanningAlert{ 29 | Number: github.Ptr(42), 30 | State: github.Ptr("open"), 31 | HTMLURL: github.Ptr("https://github.com/owner/private-repo/security/secret-scanning/42"), 32 | } 33 | 34 | tests := []struct { 35 | name string 36 | mockedClient *http.Client 37 | requestArgs map[string]interface{} 38 | expectError bool 39 | expectedAlert *github.SecretScanningAlert 40 | expectedErrMsg string 41 | }{ 42 | { 43 | name: "successful alert fetch", 44 | mockedClient: mock.NewMockedHTTPClient( 45 | mock.WithRequestMatch( 46 | mock.GetReposSecretScanningAlertsByOwnerByRepoByAlertNumber, 47 | mockAlert, 48 | ), 49 | ), 50 | requestArgs: map[string]interface{}{ 51 | "owner": "owner", 52 | "repo": "repo", 53 | "alertNumber": float64(42), 54 | }, 55 | expectError: false, 56 | expectedAlert: mockAlert, 57 | }, 58 | { 59 | name: "alert fetch fails", 60 | mockedClient: mock.NewMockedHTTPClient( 61 | mock.WithRequestMatchHandler( 62 | mock.GetReposSecretScanningAlertsByOwnerByRepoByAlertNumber, 63 | http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 64 | w.WriteHeader(http.StatusNotFound) 65 | _, _ = w.Write([]byte(`{"message": "Not Found"}`)) 66 | }), 67 | ), 68 | ), 69 | requestArgs: map[string]interface{}{ 70 | "owner": "owner", 71 | "repo": "repo", 72 | "alertNumber": float64(9999), 73 | }, 74 | expectError: true, 75 | expectedErrMsg: "failed to get alert", 76 | }, 77 | } 78 | 79 | for _, tc := range tests { 80 | t.Run(tc.name, func(t *testing.T) { 81 | // Setup client with mock 82 | client := github.NewClient(tc.mockedClient) 83 | _, handler := GetSecretScanningAlert(stubGetClientFn(client), translations.NullTranslationHelper) 84 | 85 | // Create call request 86 | request := createMCPRequest(tc.requestArgs) 87 | 88 | // Call handler 89 | result, err := handler(context.Background(), request) 90 | 91 | // Verify results 92 | if tc.expectError { 93 | require.Error(t, err) 94 | assert.Contains(t, err.Error(), tc.expectedErrMsg) 95 | return 96 | } 97 | 98 | require.NoError(t, err) 99 | 100 | // Parse the result and get the text content if no error 101 | textContent := getTextResult(t, result) 102 | 103 | // Unmarshal and verify the result 104 | var returnedAlert github.Alert 105 | err = json.Unmarshal([]byte(textContent.Text), &returnedAlert) 106 | assert.NoError(t, err) 107 | assert.Equal(t, *tc.expectedAlert.Number, *returnedAlert.Number) 108 | assert.Equal(t, *tc.expectedAlert.State, *returnedAlert.State) 109 | assert.Equal(t, *tc.expectedAlert.HTMLURL, *returnedAlert.HTMLURL) 110 | 111 | }) 112 | } 113 | } 114 | 115 | func Test_ListSecretScanningAlerts(t *testing.T) { 116 | // Verify tool definition once 117 | mockClient := github.NewClient(nil) 118 | tool, _ := ListSecretScanningAlerts(stubGetClientFn(mockClient), translations.NullTranslationHelper) 119 | 120 | assert.Equal(t, "list_secret_scanning_alerts", tool.Name) 121 | assert.NotEmpty(t, tool.Description) 122 | assert.Contains(t, tool.InputSchema.Properties, "owner") 123 | assert.Contains(t, tool.InputSchema.Properties, "repo") 124 | assert.Contains(t, tool.InputSchema.Properties, "state") 125 | assert.Contains(t, tool.InputSchema.Properties, "secret_type") 126 | assert.Contains(t, tool.InputSchema.Properties, "resolution") 127 | assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo"}) 128 | 129 | // Setup mock alerts for success case 130 | resolvedAlert := github.SecretScanningAlert{ 131 | Number: github.Ptr(2), 132 | HTMLURL: github.Ptr("https://github.com/owner/private-repo/security/secret-scanning/2"), 133 | State: github.Ptr("resolved"), 134 | Resolution: github.Ptr("false_positive"), 135 | SecretType: github.Ptr("adafruit_io_key"), 136 | } 137 | openAlert := github.SecretScanningAlert{ 138 | Number: github.Ptr(2), 139 | HTMLURL: github.Ptr("https://github.com/owner/private-repo/security/secret-scanning/3"), 140 | State: github.Ptr("open"), 141 | Resolution: github.Ptr("false_positive"), 142 | SecretType: github.Ptr("adafruit_io_key"), 143 | } 144 | 145 | tests := []struct { 146 | name string 147 | mockedClient *http.Client 148 | requestArgs map[string]interface{} 149 | expectError bool 150 | expectedAlerts []*github.SecretScanningAlert 151 | expectedErrMsg string 152 | }{ 153 | { 154 | name: "successful resolved alerts listing", 155 | mockedClient: mock.NewMockedHTTPClient( 156 | mock.WithRequestMatchHandler( 157 | mock.GetReposSecretScanningAlertsByOwnerByRepo, 158 | expectQueryParams(t, map[string]string{ 159 | "state": "resolved", 160 | }).andThen( 161 | mockResponse(t, http.StatusOK, []*github.SecretScanningAlert{&resolvedAlert}), 162 | ), 163 | ), 164 | ), 165 | requestArgs: map[string]interface{}{ 166 | "owner": "owner", 167 | "repo": "repo", 168 | "state": "resolved", 169 | }, 170 | expectError: false, 171 | expectedAlerts: []*github.SecretScanningAlert{&resolvedAlert}, 172 | }, 173 | { 174 | name: "successful alerts listing", 175 | mockedClient: mock.NewMockedHTTPClient( 176 | mock.WithRequestMatchHandler( 177 | mock.GetReposSecretScanningAlertsByOwnerByRepo, 178 | expectQueryParams(t, map[string]string{}).andThen( 179 | mockResponse(t, http.StatusOK, []*github.SecretScanningAlert{&resolvedAlert, &openAlert}), 180 | ), 181 | ), 182 | ), 183 | requestArgs: map[string]interface{}{ 184 | "owner": "owner", 185 | "repo": "repo", 186 | }, 187 | expectError: false, 188 | expectedAlerts: []*github.SecretScanningAlert{&resolvedAlert, &openAlert}, 189 | }, 190 | { 191 | name: "alerts listing fails", 192 | mockedClient: mock.NewMockedHTTPClient( 193 | mock.WithRequestMatchHandler( 194 | mock.GetReposSecretScanningAlertsByOwnerByRepo, 195 | http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 196 | w.WriteHeader(http.StatusUnauthorized) 197 | _, _ = w.Write([]byte(`{"message": "Unauthorized access"}`)) 198 | }), 199 | ), 200 | ), 201 | requestArgs: map[string]interface{}{ 202 | "owner": "owner", 203 | "repo": "repo", 204 | }, 205 | expectError: true, 206 | expectedErrMsg: "failed to list alerts", 207 | }, 208 | } 209 | 210 | for _, tc := range tests { 211 | t.Run(tc.name, func(t *testing.T) { 212 | client := github.NewClient(tc.mockedClient) 213 | _, handler := ListSecretScanningAlerts(stubGetClientFn(client), translations.NullTranslationHelper) 214 | 215 | request := createMCPRequest(tc.requestArgs) 216 | 217 | result, err := handler(context.Background(), request) 218 | 219 | if tc.expectError { 220 | require.Error(t, err) 221 | assert.Contains(t, err.Error(), tc.expectedErrMsg) 222 | return 223 | } 224 | 225 | require.NoError(t, err) 226 | 227 | textContent := getTextResult(t, result) 228 | 229 | // Unmarshal and verify the result 230 | var returnedAlerts []*github.SecretScanningAlert 231 | err = json.Unmarshal([]byte(textContent.Text), &returnedAlerts) 232 | assert.NoError(t, err) 233 | assert.Len(t, returnedAlerts, len(tc.expectedAlerts)) 234 | for i, alert := range returnedAlerts { 235 | assert.Equal(t, *tc.expectedAlerts[i].Number, *alert.Number) 236 | assert.Equal(t, *tc.expectedAlerts[i].HTMLURL, *alert.HTMLURL) 237 | assert.Equal(t, *tc.expectedAlerts[i].State, *alert.State) 238 | assert.Equal(t, *tc.expectedAlerts[i].Resolution, *alert.Resolution) 239 | assert.Equal(t, *tc.expectedAlerts[i].SecretType, *alert.SecretType) 240 | } 241 | }) 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /pkg/github/server.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/google/go-github/v69/github" 8 | "github.com/mark3labs/mcp-go/mcp" 9 | "github.com/mark3labs/mcp-go/server" 10 | ) 11 | 12 | // NewServer creates a new GitHub MCP server with the specified GH client and logger. 13 | 14 | func NewServer(version string, opts ...server.ServerOption) *server.MCPServer { 15 | // Add default options 16 | defaultOpts := []server.ServerOption{ 17 | server.WithToolCapabilities(true), 18 | server.WithResourceCapabilities(true, true), 19 | server.WithLogging(), 20 | } 21 | opts = append(defaultOpts, opts...) 22 | 23 | // Create a new MCP server 24 | s := server.NewMCPServer( 25 | "github-mcp-server", 26 | version, 27 | opts..., 28 | ) 29 | return s 30 | } 31 | 32 | // OptionalParamOK is a helper function that can be used to fetch a requested parameter from the request. 33 | // It returns the value, a boolean indicating if the parameter was present, and an error if the type is wrong. 34 | func OptionalParamOK[T any](r mcp.CallToolRequest, p string) (value T, ok bool, err error) { 35 | // Check if the parameter is present in the request 36 | val, exists := r.Params.Arguments[p] 37 | if !exists { 38 | // Not present, return zero value, false, no error 39 | return 40 | } 41 | 42 | // Check if the parameter is of the expected type 43 | value, ok = val.(T) 44 | if !ok { 45 | // Present but wrong type 46 | err = fmt.Errorf("parameter %s is not of type %T, is %T", p, value, val) 47 | ok = true // Set ok to true because the parameter *was* present, even if wrong type 48 | return 49 | } 50 | 51 | // Present and correct type 52 | ok = true 53 | return 54 | } 55 | 56 | // isAcceptedError checks if the error is an accepted error. 57 | func isAcceptedError(err error) bool { 58 | var acceptedError *github.AcceptedError 59 | return errors.As(err, &acceptedError) 60 | } 61 | 62 | // requiredParam is a helper function that can be used to fetch a requested parameter from the request. 63 | // It does the following checks: 64 | // 1. Checks if the parameter is present in the request. 65 | // 2. Checks if the parameter is of the expected type. 66 | // 3. Checks if the parameter is not empty, i.e: non-zero value 67 | func requiredParam[T comparable](r mcp.CallToolRequest, p string) (T, error) { 68 | var zero T 69 | 70 | // Check if the parameter is present in the request 71 | if _, ok := r.Params.Arguments[p]; !ok { 72 | return zero, fmt.Errorf("missing required parameter: %s", p) 73 | } 74 | 75 | // Check if the parameter is of the expected type 76 | if _, ok := r.Params.Arguments[p].(T); !ok { 77 | return zero, fmt.Errorf("parameter %s is not of type %T", p, zero) 78 | } 79 | 80 | if r.Params.Arguments[p].(T) == zero { 81 | return zero, fmt.Errorf("missing required parameter: %s", p) 82 | 83 | } 84 | 85 | return r.Params.Arguments[p].(T), nil 86 | } 87 | 88 | // RequiredInt is a helper function that can be used to fetch a requested parameter from the request. 89 | // It does the following checks: 90 | // 1. Checks if the parameter is present in the request. 91 | // 2. Checks if the parameter is of the expected type. 92 | // 3. Checks if the parameter is not empty, i.e: non-zero value 93 | func RequiredInt(r mcp.CallToolRequest, p string) (int, error) { 94 | v, err := requiredParam[float64](r, p) 95 | if err != nil { 96 | return 0, err 97 | } 98 | return int(v), nil 99 | } 100 | 101 | // OptionalParam is a helper function that can be used to fetch a requested parameter from the request. 102 | // It does the following checks: 103 | // 1. Checks if the parameter is present in the request, if not, it returns its zero-value 104 | // 2. If it is present, it checks if the parameter is of the expected type and returns it 105 | func OptionalParam[T any](r mcp.CallToolRequest, p string) (T, error) { 106 | var zero T 107 | 108 | // Check if the parameter is present in the request 109 | if _, ok := r.Params.Arguments[p]; !ok { 110 | return zero, nil 111 | } 112 | 113 | // Check if the parameter is of the expected type 114 | if _, ok := r.Params.Arguments[p].(T); !ok { 115 | return zero, fmt.Errorf("parameter %s is not of type %T, is %T", p, zero, r.Params.Arguments[p]) 116 | } 117 | 118 | return r.Params.Arguments[p].(T), nil 119 | } 120 | 121 | // OptionalIntParam is a helper function that can be used to fetch a requested parameter from the request. 122 | // It does the following checks: 123 | // 1. Checks if the parameter is present in the request, if not, it returns its zero-value 124 | // 2. If it is present, it checks if the parameter is of the expected type and returns it 125 | func OptionalIntParam(r mcp.CallToolRequest, p string) (int, error) { 126 | v, err := OptionalParam[float64](r, p) 127 | if err != nil { 128 | return 0, err 129 | } 130 | return int(v), nil 131 | } 132 | 133 | // OptionalIntParamWithDefault is a helper function that can be used to fetch a requested parameter from the request 134 | // similar to optionalIntParam, but it also takes a default value. 135 | func OptionalIntParamWithDefault(r mcp.CallToolRequest, p string, d int) (int, error) { 136 | v, err := OptionalIntParam(r, p) 137 | if err != nil { 138 | return 0, err 139 | } 140 | if v == 0 { 141 | return d, nil 142 | } 143 | return v, nil 144 | } 145 | 146 | // OptionalStringArrayParam is a helper function that can be used to fetch a requested parameter from the request. 147 | // It does the following checks: 148 | // 1. Checks if the parameter is present in the request, if not, it returns its zero-value 149 | // 2. If it is present, iterates the elements and checks each is a string 150 | func OptionalStringArrayParam(r mcp.CallToolRequest, p string) ([]string, error) { 151 | // Check if the parameter is present in the request 152 | if _, ok := r.Params.Arguments[p]; !ok { 153 | return []string{}, nil 154 | } 155 | 156 | switch v := r.Params.Arguments[p].(type) { 157 | case nil: 158 | return []string{}, nil 159 | case []string: 160 | return v, nil 161 | case []any: 162 | strSlice := make([]string, len(v)) 163 | for i, v := range v { 164 | s, ok := v.(string) 165 | if !ok { 166 | return []string{}, fmt.Errorf("parameter %s is not of type string, is %T", p, v) 167 | } 168 | strSlice[i] = s 169 | } 170 | return strSlice, nil 171 | default: 172 | return []string{}, fmt.Errorf("parameter %s could not be coerced to []string, is %T", p, r.Params.Arguments[p]) 173 | } 174 | } 175 | 176 | // WithPagination returns a ToolOption that adds "page" and "perPage" parameters to the tool. 177 | // The "page" parameter is optional, min 1. The "perPage" parameter is optional, min 1, max 100. 178 | func WithPagination() mcp.ToolOption { 179 | return func(tool *mcp.Tool) { 180 | mcp.WithNumber("page", 181 | mcp.Description("Page number for pagination (min 1)"), 182 | mcp.Min(1), 183 | )(tool) 184 | 185 | mcp.WithNumber("perPage", 186 | mcp.Description("Results per page for pagination (min 1, max 100)"), 187 | mcp.Min(1), 188 | mcp.Max(100), 189 | )(tool) 190 | } 191 | } 192 | 193 | type PaginationParams struct { 194 | page int 195 | perPage int 196 | } 197 | 198 | // OptionalPaginationParams returns the "page" and "perPage" parameters from the request, 199 | // or their default values if not present, "page" default is 1, "perPage" default is 30. 200 | // In future, we may want to make the default values configurable, or even have this 201 | // function returned from `withPagination`, where the defaults are provided alongside 202 | // the min/max values. 203 | func OptionalPaginationParams(r mcp.CallToolRequest) (PaginationParams, error) { 204 | page, err := OptionalIntParamWithDefault(r, "page", 1) 205 | if err != nil { 206 | return PaginationParams{}, err 207 | } 208 | perPage, err := OptionalIntParamWithDefault(r, "perPage", 30) 209 | if err != nil { 210 | return PaginationParams{}, err 211 | } 212 | return PaginationParams{ 213 | page: page, 214 | perPage: perPage, 215 | }, nil 216 | } 217 | -------------------------------------------------------------------------------- /pkg/github/tools.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/github/github-mcp-server/pkg/toolsets" 7 | "github.com/github/github-mcp-server/pkg/translations" 8 | "github.com/google/go-github/v69/github" 9 | "github.com/mark3labs/mcp-go/server" 10 | ) 11 | 12 | type GetClientFn func(context.Context) (*github.Client, error) 13 | 14 | var DefaultTools = []string{"all"} 15 | 16 | func InitToolsets(passedToolsets []string, readOnly bool, getClient GetClientFn, t translations.TranslationHelperFunc) (*toolsets.ToolsetGroup, error) { 17 | // Create a new toolset group 18 | tsg := toolsets.NewToolsetGroup(readOnly) 19 | 20 | // Define all available features with their default state (disabled) 21 | // Create toolsets 22 | repos := toolsets.NewToolset("repos", "GitHub Repository related tools"). 23 | AddReadTools( 24 | toolsets.NewServerTool(SearchRepositories(getClient, t)), 25 | toolsets.NewServerTool(GetFileContents(getClient, t)), 26 | toolsets.NewServerTool(ListCommits(getClient, t)), 27 | toolsets.NewServerTool(SearchCode(getClient, t)), 28 | toolsets.NewServerTool(GetCommit(getClient, t)), 29 | toolsets.NewServerTool(ListBranches(getClient, t)), 30 | ). 31 | AddWriteTools( 32 | toolsets.NewServerTool(CreateOrUpdateFile(getClient, t)), 33 | toolsets.NewServerTool(CreateRepository(getClient, t)), 34 | toolsets.NewServerTool(ForkRepository(getClient, t)), 35 | toolsets.NewServerTool(CreateBranch(getClient, t)), 36 | toolsets.NewServerTool(PushFiles(getClient, t)), 37 | ) 38 | issues := toolsets.NewToolset("issues", "GitHub Issues related tools"). 39 | AddReadTools( 40 | toolsets.NewServerTool(GetIssue(getClient, t)), 41 | toolsets.NewServerTool(SearchIssues(getClient, t)), 42 | toolsets.NewServerTool(ListIssues(getClient, t)), 43 | toolsets.NewServerTool(GetIssueComments(getClient, t)), 44 | ). 45 | AddWriteTools( 46 | toolsets.NewServerTool(CreateIssue(getClient, t)), 47 | toolsets.NewServerTool(AddIssueComment(getClient, t)), 48 | toolsets.NewServerTool(UpdateIssue(getClient, t)), 49 | ) 50 | users := toolsets.NewToolset("users", "GitHub User related tools"). 51 | AddReadTools( 52 | toolsets.NewServerTool(SearchUsers(getClient, t)), 53 | ) 54 | pullRequests := toolsets.NewToolset("pull_requests", "GitHub Pull Request related tools"). 55 | AddReadTools( 56 | toolsets.NewServerTool(GetPullRequest(getClient, t)), 57 | toolsets.NewServerTool(ListPullRequests(getClient, t)), 58 | toolsets.NewServerTool(GetPullRequestFiles(getClient, t)), 59 | toolsets.NewServerTool(GetPullRequestStatus(getClient, t)), 60 | toolsets.NewServerTool(GetPullRequestComments(getClient, t)), 61 | toolsets.NewServerTool(GetPullRequestReviews(getClient, t)), 62 | ). 63 | AddWriteTools( 64 | toolsets.NewServerTool(MergePullRequest(getClient, t)), 65 | toolsets.NewServerTool(UpdatePullRequestBranch(getClient, t)), 66 | toolsets.NewServerTool(CreatePullRequestReview(getClient, t)), 67 | toolsets.NewServerTool(CreatePullRequest(getClient, t)), 68 | toolsets.NewServerTool(UpdatePullRequest(getClient, t)), 69 | toolsets.NewServerTool(AddPullRequestReviewComment(getClient, t)), 70 | ) 71 | codeSecurity := toolsets.NewToolset("code_security", "Code security related tools, such as GitHub Code Scanning"). 72 | AddReadTools( 73 | toolsets.NewServerTool(GetCodeScanningAlert(getClient, t)), 74 | toolsets.NewServerTool(ListCodeScanningAlerts(getClient, t)), 75 | ) 76 | secretProtection := toolsets.NewToolset("secret_protection", "Secret protection related tools, such as GitHub Secret Scanning"). 77 | AddReadTools( 78 | toolsets.NewServerTool(GetSecretScanningAlert(getClient, t)), 79 | toolsets.NewServerTool(ListSecretScanningAlerts(getClient, t)), 80 | ) 81 | // Keep experiments alive so the system doesn't error out when it's always enabled 82 | experiments := toolsets.NewToolset("experiments", "Experimental features that are not considered stable yet") 83 | 84 | // Add toolsets to the group 85 | tsg.AddToolset(repos) 86 | tsg.AddToolset(issues) 87 | tsg.AddToolset(users) 88 | tsg.AddToolset(pullRequests) 89 | tsg.AddToolset(codeSecurity) 90 | tsg.AddToolset(secretProtection) 91 | tsg.AddToolset(experiments) 92 | // Enable the requested features 93 | 94 | if err := tsg.EnableToolsets(passedToolsets); err != nil { 95 | return nil, err 96 | } 97 | 98 | return tsg, nil 99 | } 100 | 101 | func InitContextToolset(getClient GetClientFn, t translations.TranslationHelperFunc) *toolsets.Toolset { 102 | // Create a new context toolset 103 | contextTools := toolsets.NewToolset("context", "Tools that provide context about the current user and GitHub context you are operating in"). 104 | AddReadTools( 105 | toolsets.NewServerTool(GetMe(getClient, t)), 106 | ) 107 | contextTools.Enabled = true 108 | return contextTools 109 | } 110 | 111 | // InitDynamicToolset creates a dynamic toolset that can be used to enable other toolsets, and so requires the server and toolset group as arguments 112 | func InitDynamicToolset(s *server.MCPServer, tsg *toolsets.ToolsetGroup, t translations.TranslationHelperFunc) *toolsets.Toolset { 113 | // Create a new dynamic toolset 114 | // Need to add the dynamic toolset last so it can be used to enable other toolsets 115 | dynamicToolSelection := toolsets.NewToolset("dynamic", "Discover GitHub MCP tools that can help achieve tasks by enabling additional sets of tools, you can control the enablement of any toolset to access its tools when this toolset is enabled."). 116 | AddReadTools( 117 | toolsets.NewServerTool(ListAvailableToolsets(tsg, t)), 118 | toolsets.NewServerTool(GetToolsetsTools(tsg, t)), 119 | toolsets.NewServerTool(EnableToolset(s, tsg, t)), 120 | ) 121 | 122 | dynamicToolSelection.Enabled = true 123 | return dynamicToolSelection 124 | } 125 | -------------------------------------------------------------------------------- /pkg/log/io.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "io" 5 | 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | // IOLogger is a wrapper around io.Reader and io.Writer that can be used 10 | // to log the data being read and written from the underlying streams 11 | type IOLogger struct { 12 | reader io.Reader 13 | writer io.Writer 14 | logger *log.Logger 15 | } 16 | 17 | // NewIOLogger creates a new IOLogger instance 18 | func NewIOLogger(r io.Reader, w io.Writer, logger *log.Logger) *IOLogger { 19 | return &IOLogger{ 20 | reader: r, 21 | writer: w, 22 | logger: logger, 23 | } 24 | } 25 | 26 | // Read reads data from the underlying io.Reader and logs it. 27 | func (l *IOLogger) Read(p []byte) (n int, err error) { 28 | if l.reader == nil { 29 | return 0, io.EOF 30 | } 31 | n, err = l.reader.Read(p) 32 | if n > 0 { 33 | l.logger.Infof("[stdin]: received %d bytes: %s", n, string(p[:n])) 34 | } 35 | return n, err 36 | } 37 | 38 | // Write writes data to the underlying io.Writer and logs it. 39 | func (l *IOLogger) Write(p []byte) (n int, err error) { 40 | if l.writer == nil { 41 | return 0, io.ErrClosedPipe 42 | } 43 | l.logger.Infof("[stdout]: sending %d bytes: %s", len(p), string(p)) 44 | return l.writer.Write(p) 45 | } 46 | -------------------------------------------------------------------------------- /pkg/log/io_test.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | 8 | log "github.com/sirupsen/logrus" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestLoggedReadWriter(t *testing.T) { 13 | t.Run("Read method logs and passes data", func(t *testing.T) { 14 | // Setup 15 | inputData := "test input data" 16 | reader := strings.NewReader(inputData) 17 | 18 | // Create logger with buffer to capture output 19 | var logBuffer bytes.Buffer 20 | logger := log.New() 21 | logger.SetOutput(&logBuffer) 22 | logger.SetFormatter(&log.TextFormatter{ 23 | DisableTimestamp: true, 24 | }) 25 | 26 | lrw := NewIOLogger(reader, nil, logger) 27 | 28 | // Test Read 29 | buf := make([]byte, 100) 30 | n, err := lrw.Read(buf) 31 | 32 | // Assertions 33 | assert.NoError(t, err) 34 | assert.Equal(t, len(inputData), n) 35 | assert.Equal(t, inputData, string(buf[:n])) 36 | assert.Contains(t, logBuffer.String(), "[stdin]") 37 | assert.Contains(t, logBuffer.String(), inputData) 38 | }) 39 | 40 | t.Run("Write method logs and passes data", func(t *testing.T) { 41 | // Setup 42 | outputData := "test output data" 43 | var writeBuffer bytes.Buffer 44 | 45 | // Create logger with buffer to capture output 46 | var logBuffer bytes.Buffer 47 | logger := log.New() 48 | logger.SetOutput(&logBuffer) 49 | logger.SetFormatter(&log.TextFormatter{ 50 | DisableTimestamp: true, 51 | }) 52 | 53 | lrw := NewIOLogger(nil, &writeBuffer, logger) 54 | 55 | // Test Write 56 | n, err := lrw.Write([]byte(outputData)) 57 | 58 | // Assertions 59 | assert.NoError(t, err) 60 | assert.Equal(t, len(outputData), n) 61 | assert.Equal(t, outputData, writeBuffer.String()) 62 | assert.Contains(t, logBuffer.String(), "[stdout]") 63 | assert.Contains(t, logBuffer.String(), outputData) 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /pkg/toolsets/toolsets.go: -------------------------------------------------------------------------------- 1 | package toolsets 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/mark3labs/mcp-go/mcp" 7 | "github.com/mark3labs/mcp-go/server" 8 | ) 9 | 10 | func NewServerTool(tool mcp.Tool, handler server.ToolHandlerFunc) server.ServerTool { 11 | return server.ServerTool{Tool: tool, Handler: handler} 12 | } 13 | 14 | type Toolset struct { 15 | Name string 16 | Description string 17 | Enabled bool 18 | readOnly bool 19 | writeTools []server.ServerTool 20 | readTools []server.ServerTool 21 | } 22 | 23 | func (t *Toolset) GetActiveTools() []server.ServerTool { 24 | if t.Enabled { 25 | if t.readOnly { 26 | return t.readTools 27 | } 28 | return append(t.readTools, t.writeTools...) 29 | } 30 | return nil 31 | } 32 | 33 | func (t *Toolset) GetAvailableTools() []server.ServerTool { 34 | if t.readOnly { 35 | return t.readTools 36 | } 37 | return append(t.readTools, t.writeTools...) 38 | } 39 | 40 | func (t *Toolset) RegisterTools(s *server.MCPServer) { 41 | if !t.Enabled { 42 | return 43 | } 44 | for _, tool := range t.readTools { 45 | s.AddTool(tool.Tool, tool.Handler) 46 | } 47 | if !t.readOnly { 48 | for _, tool := range t.writeTools { 49 | s.AddTool(tool.Tool, tool.Handler) 50 | } 51 | } 52 | } 53 | 54 | func (t *Toolset) SetReadOnly() { 55 | // Set the toolset to read-only 56 | t.readOnly = true 57 | } 58 | 59 | func (t *Toolset) AddWriteTools(tools ...server.ServerTool) *Toolset { 60 | // Silently ignore if the toolset is read-only to avoid any breach of that contract 61 | for _, tool := range tools { 62 | if tool.Tool.Annotations.ReadOnlyHint { 63 | panic(fmt.Sprintf("tool (%s) is incorrectly annotated as read-only", tool.Tool.Name)) 64 | } 65 | } 66 | if !t.readOnly { 67 | t.writeTools = append(t.writeTools, tools...) 68 | } 69 | return t 70 | } 71 | 72 | func (t *Toolset) AddReadTools(tools ...server.ServerTool) *Toolset { 73 | for _, tool := range tools { 74 | if !tool.Tool.Annotations.ReadOnlyHint { 75 | panic(fmt.Sprintf("tool (%s) must be annotated as read-only", tool.Tool.Name)) 76 | } 77 | tool.Tool.Annotations = mcp.ToolAnnotation{ 78 | ReadOnlyHint: true, 79 | Title: tool.Tool.Annotations.Title, 80 | } 81 | } 82 | t.readTools = append(t.readTools, tools...) 83 | return t 84 | } 85 | 86 | type ToolsetGroup struct { 87 | Toolsets map[string]*Toolset 88 | everythingOn bool 89 | readOnly bool 90 | } 91 | 92 | func NewToolsetGroup(readOnly bool) *ToolsetGroup { 93 | return &ToolsetGroup{ 94 | Toolsets: make(map[string]*Toolset), 95 | everythingOn: false, 96 | readOnly: readOnly, 97 | } 98 | } 99 | 100 | func (tg *ToolsetGroup) AddToolset(ts *Toolset) { 101 | if tg.readOnly { 102 | ts.SetReadOnly() 103 | } 104 | tg.Toolsets[ts.Name] = ts 105 | } 106 | 107 | func NewToolset(name string, description string) *Toolset { 108 | return &Toolset{ 109 | Name: name, 110 | Description: description, 111 | Enabled: false, 112 | readOnly: false, 113 | } 114 | } 115 | 116 | func (tg *ToolsetGroup) IsEnabled(name string) bool { 117 | // If everythingOn is true, all features are enabled 118 | if tg.everythingOn { 119 | return true 120 | } 121 | 122 | feature, exists := tg.Toolsets[name] 123 | if !exists { 124 | return false 125 | } 126 | return feature.Enabled 127 | } 128 | 129 | func (tg *ToolsetGroup) EnableToolsets(names []string) error { 130 | // Special case for "all" 131 | for _, name := range names { 132 | if name == "all" { 133 | tg.everythingOn = true 134 | break 135 | } 136 | err := tg.EnableToolset(name) 137 | if err != nil { 138 | return err 139 | } 140 | } 141 | // Do this after to ensure all toolsets are enabled if "all" is present anywhere in list 142 | if tg.everythingOn { 143 | for name := range tg.Toolsets { 144 | err := tg.EnableToolset(name) 145 | if err != nil { 146 | return err 147 | } 148 | } 149 | return nil 150 | } 151 | return nil 152 | } 153 | 154 | func (tg *ToolsetGroup) EnableToolset(name string) error { 155 | toolset, exists := tg.Toolsets[name] 156 | if !exists { 157 | return fmt.Errorf("toolset %s does not exist", name) 158 | } 159 | toolset.Enabled = true 160 | tg.Toolsets[name] = toolset 161 | return nil 162 | } 163 | 164 | func (tg *ToolsetGroup) RegisterTools(s *server.MCPServer) { 165 | for _, toolset := range tg.Toolsets { 166 | toolset.RegisterTools(s) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /pkg/toolsets/toolsets_test.go: -------------------------------------------------------------------------------- 1 | package toolsets 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestNewToolsetGroup(t *testing.T) { 8 | tsg := NewToolsetGroup(false) 9 | if tsg == nil { 10 | t.Fatal("Expected NewToolsetGroup to return a non-nil pointer") 11 | } 12 | if tsg.Toolsets == nil { 13 | t.Fatal("Expected Toolsets map to be initialized") 14 | } 15 | if len(tsg.Toolsets) != 0 { 16 | t.Fatalf("Expected Toolsets map to be empty, got %d items", len(tsg.Toolsets)) 17 | } 18 | if tsg.everythingOn { 19 | t.Fatal("Expected everythingOn to be initialized as false") 20 | } 21 | } 22 | 23 | func TestAddToolset(t *testing.T) { 24 | tsg := NewToolsetGroup(false) 25 | 26 | // Test adding a toolset 27 | toolset := NewToolset("test-toolset", "A test toolset") 28 | toolset.Enabled = true 29 | tsg.AddToolset(toolset) 30 | 31 | // Verify toolset was added correctly 32 | if len(tsg.Toolsets) != 1 { 33 | t.Errorf("Expected 1 toolset, got %d", len(tsg.Toolsets)) 34 | } 35 | 36 | toolset, exists := tsg.Toolsets["test-toolset"] 37 | if !exists { 38 | t.Fatal("Feature was not added to the map") 39 | } 40 | 41 | if toolset.Name != "test-toolset" { 42 | t.Errorf("Expected toolset name to be 'test-toolset', got '%s'", toolset.Name) 43 | } 44 | 45 | if toolset.Description != "A test toolset" { 46 | t.Errorf("Expected toolset description to be 'A test toolset', got '%s'", toolset.Description) 47 | } 48 | 49 | if !toolset.Enabled { 50 | t.Error("Expected toolset to be enabled") 51 | } 52 | 53 | // Test adding another toolset 54 | anotherToolset := NewToolset("another-toolset", "Another test toolset") 55 | tsg.AddToolset(anotherToolset) 56 | 57 | if len(tsg.Toolsets) != 2 { 58 | t.Errorf("Expected 2 toolsets, got %d", len(tsg.Toolsets)) 59 | } 60 | 61 | // Test overriding existing toolset 62 | updatedToolset := NewToolset("test-toolset", "Updated description") 63 | tsg.AddToolset(updatedToolset) 64 | 65 | toolset = tsg.Toolsets["test-toolset"] 66 | if toolset.Description != "Updated description" { 67 | t.Errorf("Expected toolset description to be updated to 'Updated description', got '%s'", toolset.Description) 68 | } 69 | 70 | if toolset.Enabled { 71 | t.Error("Expected toolset to be disabled after update") 72 | } 73 | } 74 | 75 | func TestIsEnabled(t *testing.T) { 76 | tsg := NewToolsetGroup(false) 77 | 78 | // Test with non-existent toolset 79 | if tsg.IsEnabled("non-existent") { 80 | t.Error("Expected IsEnabled to return false for non-existent toolset") 81 | } 82 | 83 | // Test with disabled toolset 84 | disabledToolset := NewToolset("disabled-toolset", "A disabled toolset") 85 | tsg.AddToolset(disabledToolset) 86 | if tsg.IsEnabled("disabled-toolset") { 87 | t.Error("Expected IsEnabled to return false for disabled toolset") 88 | } 89 | 90 | // Test with enabled toolset 91 | enabledToolset := NewToolset("enabled-toolset", "An enabled toolset") 92 | enabledToolset.Enabled = true 93 | tsg.AddToolset(enabledToolset) 94 | if !tsg.IsEnabled("enabled-toolset") { 95 | t.Error("Expected IsEnabled to return true for enabled toolset") 96 | } 97 | } 98 | 99 | func TestEnableFeature(t *testing.T) { 100 | tsg := NewToolsetGroup(false) 101 | 102 | // Test enabling non-existent toolset 103 | err := tsg.EnableToolset("non-existent") 104 | if err == nil { 105 | t.Error("Expected error when enabling non-existent toolset") 106 | } 107 | 108 | // Test enabling toolset 109 | testToolset := NewToolset("test-toolset", "A test toolset") 110 | tsg.AddToolset(testToolset) 111 | 112 | if tsg.IsEnabled("test-toolset") { 113 | t.Error("Expected toolset to be disabled initially") 114 | } 115 | 116 | err = tsg.EnableToolset("test-toolset") 117 | if err != nil { 118 | t.Errorf("Expected no error when enabling toolset, got: %v", err) 119 | } 120 | 121 | if !tsg.IsEnabled("test-toolset") { 122 | t.Error("Expected toolset to be enabled after EnableFeature call") 123 | } 124 | 125 | // Test enabling already enabled toolset 126 | err = tsg.EnableToolset("test-toolset") 127 | if err != nil { 128 | t.Errorf("Expected no error when enabling already enabled toolset, got: %v", err) 129 | } 130 | } 131 | 132 | func TestEnableToolsets(t *testing.T) { 133 | tsg := NewToolsetGroup(false) 134 | 135 | // Prepare toolsets 136 | toolset1 := NewToolset("toolset1", "Feature 1") 137 | toolset2 := NewToolset("toolset2", "Feature 2") 138 | tsg.AddToolset(toolset1) 139 | tsg.AddToolset(toolset2) 140 | 141 | // Test enabling multiple toolsets 142 | err := tsg.EnableToolsets([]string{"toolset1", "toolset2"}) 143 | if err != nil { 144 | t.Errorf("Expected no error when enabling toolsets, got: %v", err) 145 | } 146 | 147 | if !tsg.IsEnabled("toolset1") { 148 | t.Error("Expected toolset1 to be enabled") 149 | } 150 | 151 | if !tsg.IsEnabled("toolset2") { 152 | t.Error("Expected toolset2 to be enabled") 153 | } 154 | 155 | // Test with non-existent toolset in the list 156 | err = tsg.EnableToolsets([]string{"toolset1", "non-existent"}) 157 | if err == nil { 158 | t.Error("Expected error when enabling list with non-existent toolset") 159 | } 160 | 161 | // Test with empty list 162 | err = tsg.EnableToolsets([]string{}) 163 | if err != nil { 164 | t.Errorf("Expected no error with empty toolset list, got: %v", err) 165 | } 166 | 167 | // Test enabling everything through EnableToolsets 168 | tsg = NewToolsetGroup(false) 169 | err = tsg.EnableToolsets([]string{"all"}) 170 | if err != nil { 171 | t.Errorf("Expected no error when enabling 'all', got: %v", err) 172 | } 173 | 174 | if !tsg.everythingOn { 175 | t.Error("Expected everythingOn to be true after enabling 'all' via EnableToolsets") 176 | } 177 | } 178 | 179 | func TestEnableEverything(t *testing.T) { 180 | tsg := NewToolsetGroup(false) 181 | 182 | // Add a disabled toolset 183 | testToolset := NewToolset("test-toolset", "A test toolset") 184 | tsg.AddToolset(testToolset) 185 | 186 | // Verify it's disabled 187 | if tsg.IsEnabled("test-toolset") { 188 | t.Error("Expected toolset to be disabled initially") 189 | } 190 | 191 | // Enable "all" 192 | err := tsg.EnableToolsets([]string{"all"}) 193 | if err != nil { 194 | t.Errorf("Expected no error when enabling 'eall', got: %v", err) 195 | } 196 | 197 | // Verify everythingOn was set 198 | if !tsg.everythingOn { 199 | t.Error("Expected everythingOn to be true after enabling 'eall'") 200 | } 201 | 202 | // Verify the previously disabled toolset is now enabled 203 | if !tsg.IsEnabled("test-toolset") { 204 | t.Error("Expected toolset to be enabled when everythingOn is true") 205 | } 206 | 207 | // Verify a non-existent toolset is also enabled 208 | if !tsg.IsEnabled("non-existent") { 209 | t.Error("Expected non-existent toolset to be enabled when everythingOn is true") 210 | } 211 | } 212 | 213 | func TestIsEnabledWithEverythingOn(t *testing.T) { 214 | tsg := NewToolsetGroup(false) 215 | 216 | // Enable "everything" 217 | err := tsg.EnableToolsets([]string{"all"}) 218 | if err != nil { 219 | t.Errorf("Expected no error when enabling 'all', got: %v", err) 220 | } 221 | 222 | // Test that any toolset name returns true with IsEnabled 223 | if !tsg.IsEnabled("some-toolset") { 224 | t.Error("Expected IsEnabled to return true for any toolset when everythingOn is true") 225 | } 226 | 227 | if !tsg.IsEnabled("another-toolset") { 228 | t.Error("Expected IsEnabled to return true for any toolset when everythingOn is true") 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /pkg/translations/translations.go: -------------------------------------------------------------------------------- 1 | package translations 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "os" 8 | "strings" 9 | 10 | "github.com/spf13/viper" 11 | ) 12 | 13 | type TranslationHelperFunc func(key string, defaultValue string) string 14 | 15 | func NullTranslationHelper(_ string, defaultValue string) string { 16 | return defaultValue 17 | } 18 | 19 | func TranslationHelper() (TranslationHelperFunc, func()) { 20 | var translationKeyMap = map[string]string{} 21 | v := viper.New() 22 | 23 | // Load from JSON file 24 | v.SetConfigName("github-mcp-server-config") 25 | v.SetConfigType("json") 26 | v.AddConfigPath(".") 27 | 28 | if err := v.ReadInConfig(); err != nil { 29 | // ignore error if file not found as it is not required 30 | if _, ok := err.(viper.ConfigFileNotFoundError); !ok { 31 | log.Printf("Could not read JSON config: %v", err) 32 | } 33 | } 34 | 35 | // create a function that takes both a key, and a default value and returns either the default value or an override value 36 | return func(key string, defaultValue string) string { 37 | key = strings.ToUpper(key) 38 | if value, exists := translationKeyMap[key]; exists { 39 | return value 40 | } 41 | // check if the env var exists 42 | if value, exists := os.LookupEnv("GITHUB_MCP_" + key); exists { 43 | // TODO I could not get Viper to play ball reading the env var 44 | translationKeyMap[key] = value 45 | return value 46 | } 47 | 48 | v.SetDefault(key, defaultValue) 49 | translationKeyMap[key] = v.GetString(key) 50 | return translationKeyMap[key] 51 | }, func() { 52 | // dump the translationKeyMap to a json file 53 | if err := DumpTranslationKeyMap(translationKeyMap); err != nil { 54 | log.Fatalf("Could not dump translation key map: %v", err) 55 | } 56 | } 57 | } 58 | 59 | // dump translationKeyMap to a json file called github-mcp-server-config.json 60 | func DumpTranslationKeyMap(translationKeyMap map[string]string) error { 61 | file, err := os.Create("github-mcp-server-config.json") 62 | if err != nil { 63 | return fmt.Errorf("error creating file: %v", err) 64 | } 65 | defer func() { _ = file.Close() }() 66 | 67 | // marshal the map to json 68 | jsonData, err := json.MarshalIndent(translationKeyMap, "", " ") 69 | if err != nil { 70 | return fmt.Errorf("error marshaling map to JSON: %v", err) 71 | } 72 | 73 | // write the json data to the file 74 | if _, err := file.Write(jsonData); err != nil { 75 | return fmt.Errorf("error writing to file: %v", err) 76 | } 77 | 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /script/get-me: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo '{"jsonrpc":"2.0","id":3,"params":{"name":"get_me"},"method":"tools/call"}' | go run cmd/github-mcp-server/main.go stdio | jq . 4 | -------------------------------------------------------------------------------- /script/licenses: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | go install github.com/google/go-licenses@latest 4 | 5 | rm -rf third-party 6 | mkdir -p third-party 7 | export TEMPDIR="$(mktemp -d)" 8 | 9 | trap "rm -fr ${TEMPDIR}" EXIT 10 | 11 | for goos in linux darwin windows ; do 12 | # Note: we ignore warnings because we want the command to succeed, however the output should be checked 13 | # for any new warnings, and potentially we may need to add license information. 14 | # 15 | # Normally these warnings are packages containing non go code, which may or may not require explicit attribution, 16 | # depending on the license. 17 | GOOS="${goos}" go-licenses save ./... --save_path="${TEMPDIR}/${goos}" --force || echo "Ignore warnings" 18 | GOOS="${goos}" go-licenses report ./... --template .github/licenses.tmpl > third-party-licenses.${goos}.md || echo "Ignore warnings" 19 | cp -fR "${TEMPDIR}/${goos}"/* third-party/ 20 | done 21 | 22 | -------------------------------------------------------------------------------- /script/licenses-check: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | go install github.com/google/go-licenses@latest 4 | 5 | for goos in linux darwin windows ; do 6 | # Note: we ignore warnings because we want the command to succeed, however the output should be checked 7 | # for any new warnings, and potentially we may need to add license information. 8 | # 9 | # Normally these warnings are packages containing non go code, which may or may not require explicit attribution, 10 | # depending on the license. 11 | GOOS="${goos}" go-licenses report ./... --template .github/licenses.tmpl > third-party-licenses.${goos}.copy.md || echo "Ignore warnings" 12 | if ! diff -s third-party-licenses.${goos}.copy.md third-party-licenses.${goos}.md; then 13 | echo "License check failed.\n\nPlease update the license file by running \`.script/licenses\` and committing the output." 14 | rm -f third-party-licenses.${goos}.copy.md 15 | exit 1 16 | fi 17 | rm -f third-party-licenses.${goos}.copy.md 18 | done 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /script/prettyprint-log: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to pretty print the output of the github-mcp-server 4 | # log. 5 | # 6 | # It uses colored output when running on a terminal. 7 | 8 | # show script help 9 | show_help() { 10 | cat <&2 55 | exit 1 56 | fi 57 | input="$1" 58 | else 59 | input="/dev/stdin" 60 | fi 61 | 62 | # check if we are in a terminal for showing colors 63 | if test -t 1; then 64 | is_terminal="1" 65 | else 66 | is_terminal="0" 67 | fi 68 | 69 | # Processs each log line, print whether is stdin or stdout, using different 70 | # colors if we output to a terminal, and pretty print json data using jq 71 | sed -nE 's/^.*\[(stdin|stdout)\]:.* ([0-9]+) bytes: (.*)\\n"$/\1 \2 \3/p' $input | 72 | while read -r io bytes json; do 73 | # Unescape the JSON string safely 74 | unescaped=$(echo "$json" | awk '{ print "echo -e \"" $0 "\" | jq ." }' | bash) 75 | echo "$(color $io)($bytes bytes):$(reset)" 76 | echo "$unescaped" | jq . 77 | echo 78 | done 79 | -------------------------------------------------------------------------------- /third-party-licenses.darwin.md: -------------------------------------------------------------------------------- 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 | 10 | - [github.com/fsnotify/fsnotify](https://pkg.go.dev/github.com/fsnotify/fsnotify) ([BSD-3-Clause](https://github.com/fsnotify/fsnotify/blob/v1.8.0/LICENSE)) 11 | - [github.com/github/github-mcp-server](https://pkg.go.dev/github.com/github/github-mcp-server) ([MIT](https://github.com/github/github-mcp-server/blob/HEAD/LICENSE)) 12 | - [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.2.1/LICENSE)) 13 | - [github.com/google/go-github/v69/github](https://pkg.go.dev/github.com/google/go-github/v69/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v69.2.0/LICENSE)) 14 | - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.1.0/LICENSE)) 15 | - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE)) 16 | - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.22.0/LICENSE)) 17 | - [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.3/LICENSE)) 18 | - [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.9.0/LICENSE)) 19 | - [github.com/sirupsen/logrus](https://pkg.go.dev/github.com/sirupsen/logrus) ([MIT](https://github.com/sirupsen/logrus/blob/v1.9.3/LICENSE)) 20 | - [github.com/sourcegraph/conc](https://pkg.go.dev/github.com/sourcegraph/conc) ([MIT](https://github.com/sourcegraph/conc/blob/v0.3.0/LICENSE)) 21 | - [github.com/spf13/afero](https://pkg.go.dev/github.com/spf13/afero) ([Apache-2.0](https://github.com/spf13/afero/blob/v1.14.0/LICENSE.txt)) 22 | - [github.com/spf13/cast](https://pkg.go.dev/github.com/spf13/cast) ([MIT](https://github.com/spf13/cast/blob/v1.7.1/LICENSE)) 23 | - [github.com/spf13/cobra](https://pkg.go.dev/github.com/spf13/cobra) ([Apache-2.0](https://github.com/spf13/cobra/blob/v1.9.1/LICENSE.txt)) 24 | - [github.com/spf13/pflag](https://pkg.go.dev/github.com/spf13/pflag) ([BSD-3-Clause](https://github.com/spf13/pflag/blob/v1.0.6/LICENSE)) 25 | - [github.com/spf13/viper](https://pkg.go.dev/github.com/spf13/viper) ([MIT](https://github.com/spf13/viper/blob/v1.20.1/LICENSE)) 26 | - [github.com/subosito/gotenv](https://pkg.go.dev/github.com/subosito/gotenv) ([MIT](https://github.com/subosito/gotenv/blob/v1.6.0/LICENSE)) 27 | - [github.com/yosida95/uritemplate/v3](https://pkg.go.dev/github.com/yosida95/uritemplate/v3) ([BSD-3-Clause](https://github.com/yosida95/uritemplate/blob/v3.0.2/LICENSE)) 28 | - [golang.org/x/sys/unix](https://pkg.go.dev/golang.org/x/sys/unix) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.31.0:LICENSE)) 29 | - [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.23.0:LICENSE)) 30 | - [gopkg.in/yaml.v3](https://pkg.go.dev/gopkg.in/yaml.v3) ([MIT](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE)) 31 | 32 | [github/github-mcp-server]: https://github.com/github/github-mcp-server 33 | -------------------------------------------------------------------------------- /third-party-licenses.linux.md: -------------------------------------------------------------------------------- 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 | 10 | - [github.com/fsnotify/fsnotify](https://pkg.go.dev/github.com/fsnotify/fsnotify) ([BSD-3-Clause](https://github.com/fsnotify/fsnotify/blob/v1.8.0/LICENSE)) 11 | - [github.com/github/github-mcp-server](https://pkg.go.dev/github.com/github/github-mcp-server) ([MIT](https://github.com/github/github-mcp-server/blob/HEAD/LICENSE)) 12 | - [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.2.1/LICENSE)) 13 | - [github.com/google/go-github/v69/github](https://pkg.go.dev/github.com/google/go-github/v69/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v69.2.0/LICENSE)) 14 | - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.1.0/LICENSE)) 15 | - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE)) 16 | - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.22.0/LICENSE)) 17 | - [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.3/LICENSE)) 18 | - [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.9.0/LICENSE)) 19 | - [github.com/sirupsen/logrus](https://pkg.go.dev/github.com/sirupsen/logrus) ([MIT](https://github.com/sirupsen/logrus/blob/v1.9.3/LICENSE)) 20 | - [github.com/sourcegraph/conc](https://pkg.go.dev/github.com/sourcegraph/conc) ([MIT](https://github.com/sourcegraph/conc/blob/v0.3.0/LICENSE)) 21 | - [github.com/spf13/afero](https://pkg.go.dev/github.com/spf13/afero) ([Apache-2.0](https://github.com/spf13/afero/blob/v1.14.0/LICENSE.txt)) 22 | - [github.com/spf13/cast](https://pkg.go.dev/github.com/spf13/cast) ([MIT](https://github.com/spf13/cast/blob/v1.7.1/LICENSE)) 23 | - [github.com/spf13/cobra](https://pkg.go.dev/github.com/spf13/cobra) ([Apache-2.0](https://github.com/spf13/cobra/blob/v1.9.1/LICENSE.txt)) 24 | - [github.com/spf13/pflag](https://pkg.go.dev/github.com/spf13/pflag) ([BSD-3-Clause](https://github.com/spf13/pflag/blob/v1.0.6/LICENSE)) 25 | - [github.com/spf13/viper](https://pkg.go.dev/github.com/spf13/viper) ([MIT](https://github.com/spf13/viper/blob/v1.20.1/LICENSE)) 26 | - [github.com/subosito/gotenv](https://pkg.go.dev/github.com/subosito/gotenv) ([MIT](https://github.com/subosito/gotenv/blob/v1.6.0/LICENSE)) 27 | - [github.com/yosida95/uritemplate/v3](https://pkg.go.dev/github.com/yosida95/uritemplate/v3) ([BSD-3-Clause](https://github.com/yosida95/uritemplate/blob/v3.0.2/LICENSE)) 28 | - [golang.org/x/sys/unix](https://pkg.go.dev/golang.org/x/sys/unix) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.31.0:LICENSE)) 29 | - [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.23.0:LICENSE)) 30 | - [gopkg.in/yaml.v3](https://pkg.go.dev/gopkg.in/yaml.v3) ([MIT](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE)) 31 | 32 | [github/github-mcp-server]: https://github.com/github/github-mcp-server 33 | -------------------------------------------------------------------------------- /third-party-licenses.windows.md: -------------------------------------------------------------------------------- 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 | 10 | - [github.com/fsnotify/fsnotify](https://pkg.go.dev/github.com/fsnotify/fsnotify) ([BSD-3-Clause](https://github.com/fsnotify/fsnotify/blob/v1.8.0/LICENSE)) 11 | - [github.com/github/github-mcp-server](https://pkg.go.dev/github.com/github/github-mcp-server) ([MIT](https://github.com/github/github-mcp-server/blob/HEAD/LICENSE)) 12 | - [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.2.1/LICENSE)) 13 | - [github.com/google/go-github/v69/github](https://pkg.go.dev/github.com/google/go-github/v69/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v69.2.0/LICENSE)) 14 | - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.1.0/LICENSE)) 15 | - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE)) 16 | - [github.com/inconshreveable/mousetrap](https://pkg.go.dev/github.com/inconshreveable/mousetrap) ([Apache-2.0](https://github.com/inconshreveable/mousetrap/blob/v1.1.0/LICENSE)) 17 | - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.22.0/LICENSE)) 18 | - [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.3/LICENSE)) 19 | - [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.9.0/LICENSE)) 20 | - [github.com/sirupsen/logrus](https://pkg.go.dev/github.com/sirupsen/logrus) ([MIT](https://github.com/sirupsen/logrus/blob/v1.9.3/LICENSE)) 21 | - [github.com/sourcegraph/conc](https://pkg.go.dev/github.com/sourcegraph/conc) ([MIT](https://github.com/sourcegraph/conc/blob/v0.3.0/LICENSE)) 22 | - [github.com/spf13/afero](https://pkg.go.dev/github.com/spf13/afero) ([Apache-2.0](https://github.com/spf13/afero/blob/v1.14.0/LICENSE.txt)) 23 | - [github.com/spf13/cast](https://pkg.go.dev/github.com/spf13/cast) ([MIT](https://github.com/spf13/cast/blob/v1.7.1/LICENSE)) 24 | - [github.com/spf13/cobra](https://pkg.go.dev/github.com/spf13/cobra) ([Apache-2.0](https://github.com/spf13/cobra/blob/v1.9.1/LICENSE.txt)) 25 | - [github.com/spf13/pflag](https://pkg.go.dev/github.com/spf13/pflag) ([BSD-3-Clause](https://github.com/spf13/pflag/blob/v1.0.6/LICENSE)) 26 | - [github.com/spf13/viper](https://pkg.go.dev/github.com/spf13/viper) ([MIT](https://github.com/spf13/viper/blob/v1.20.1/LICENSE)) 27 | - [github.com/subosito/gotenv](https://pkg.go.dev/github.com/subosito/gotenv) ([MIT](https://github.com/subosito/gotenv/blob/v1.6.0/LICENSE)) 28 | - [github.com/yosida95/uritemplate/v3](https://pkg.go.dev/github.com/yosida95/uritemplate/v3) ([BSD-3-Clause](https://github.com/yosida95/uritemplate/blob/v3.0.2/LICENSE)) 29 | - [golang.org/x/sys/windows](https://pkg.go.dev/golang.org/x/sys/windows) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.31.0:LICENSE)) 30 | - [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.23.0:LICENSE)) 31 | - [gopkg.in/yaml.v3](https://pkg.go.dev/gopkg.in/yaml.v3) ([MIT](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE)) 32 | 33 | [github/github-mcp-server]: https://github.com/github/github-mcp-server 34 | -------------------------------------------------------------------------------- /third-party/github.com/fsnotify/fsnotify/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2012 The Go Authors. All rights reserved. 2 | Copyright © fsnotify Authors. All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, this 10 | list of conditions and the following disclaimer in the documentation and/or 11 | other materials provided with the distribution. 12 | * Neither the name of Google Inc. nor the names of its contributors may be used 13 | to endorse or promote products derived from this software without specific 14 | prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 23 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /third-party/github.com/github/github-mcp-server/LICENSE: -------------------------------------------------------------------------------- 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. 22 | -------------------------------------------------------------------------------- /third-party/github.com/go-viper/mapstructure/v2/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Mitchell Hashimoto 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /third-party/github.com/google/go-github/v69/github/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 The go-github AUTHORS. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /third-party/github.com/google/go-querystring/query/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Google. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /third-party/github.com/google/uuid/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009,2014 Google Inc. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /third-party/github.com/mark3labs/mcp-go/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Anthropic, PBC 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. 22 | -------------------------------------------------------------------------------- /third-party/github.com/pelletier/go-toml/v2/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | go-toml v2 4 | Copyright (c) 2021 - 2023 Thomas Pelletier 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /third-party/github.com/sagikazarmark/locafero/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Márk Sági-Kazár 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /third-party/github.com/sirupsen/logrus/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Simon Eskildsen 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /third-party/github.com/sourcegraph/conc/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Sourcegraph 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. 22 | -------------------------------------------------------------------------------- /third-party/github.com/spf13/afero/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | -------------------------------------------------------------------------------- /third-party/github.com/spf13/cast/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Steve Francia 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. -------------------------------------------------------------------------------- /third-party/github.com/spf13/pflag/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Alex Ogier. All rights reserved. 2 | Copyright (c) 2012 The Go Authors. All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following disclaimer 12 | in the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Google Inc. nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /third-party/github.com/spf13/viper/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Steve Francia 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. -------------------------------------------------------------------------------- /third-party/github.com/subosito/gotenv/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Alif Rachmawadi 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /third-party/github.com/yosida95/uritemplate/v3/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2016, Kohei YOSHIDA . All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the copyright holder nor the names of its 12 | contributors may be used to endorse or promote products derived from 13 | this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 18 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 19 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 21 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 23 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /third-party/golang.org/x/sys/unix/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2009 The Go Authors. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google LLC nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /third-party/golang.org/x/sys/windows/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2009 The Go Authors. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google LLC nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /third-party/golang.org/x/text/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2009 The Go Authors. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google LLC nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /third-party/gopkg.in/yaml.v3/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | This project is covered by two different licenses: MIT and Apache. 3 | 4 | #### MIT License #### 5 | 6 | The following files were ported to Go from C files of libyaml, and thus 7 | are still covered by their original MIT license, with the additional 8 | copyright staring in 2011 when the project was ported over: 9 | 10 | apic.go emitterc.go parserc.go readerc.go scannerc.go 11 | writerc.go yamlh.go yamlprivateh.go 12 | 13 | Copyright (c) 2006-2010 Kirill Simonov 14 | Copyright (c) 2006-2011 Kirill Simonov 15 | 16 | Permission is hereby granted, free of charge, to any person obtaining a copy of 17 | this software and associated documentation files (the "Software"), to deal in 18 | the Software without restriction, including without limitation the rights to 19 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 20 | of the Software, and to permit persons to whom the Software is furnished to do 21 | so, subject to the following conditions: 22 | 23 | The above copyright notice and this permission notice shall be included in all 24 | copies or substantial portions of the Software. 25 | 26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 29 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 30 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 31 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 32 | SOFTWARE. 33 | 34 | ### Apache License ### 35 | 36 | All the remaining project files are covered by the Apache license: 37 | 38 | Copyright (c) 2011-2019 Canonical Ltd 39 | 40 | Licensed under the Apache License, Version 2.0 (the "License"); 41 | you may not use this file except in compliance with the License. 42 | You may obtain a copy of the License at 43 | 44 | http://www.apache.org/licenses/LICENSE-2.0 45 | 46 | Unless required by applicable law or agreed to in writing, software 47 | distributed under the License is distributed on an "AS IS" BASIS, 48 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 49 | See the License for the specific language governing permissions and 50 | limitations under the License. 51 | -------------------------------------------------------------------------------- /third-party/gopkg.in/yaml.v3/NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2011-2016 Canonical Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | --------------------------------------------------------------------------------