diff --git a/Dockerfile b/Dockerfile
index 1281db4c0..a26f19a81 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM golang:1.24.3-alpine AS build
+FROM golang:1.24.4-alpine AS build
ARG VERSION="dev"
# Set the working directory
diff --git a/README.md b/README.md
index 9dba301d3..a5c1e3136 100644
--- a/README.md
+++ b/README.md
@@ -4,14 +4,128 @@ The GitHub MCP Server is a [Model Context Protocol (MCP)](https://modelcontextpr
server that provides seamless integration with GitHub APIs, enabling advanced
automation and interaction capabilities for developers and tools.
-[](https://insiders.vscode.dev/redirect/mcp/install?name=github&inputs=%5B%7B%22id%22%3A%22github_token%22%2C%22type%22%3A%22promptString%22%2C%22description%22%3A%22GitHub%20Personal%20Access%20Token%22%2C%22password%22%3Atrue%7D%5D&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22-e%22%2C%22GITHUB_PERSONAL_ACCESS_TOKEN%22%2C%22ghcr.io%2Fgithub%2Fgithub-mcp-server%22%5D%2C%22env%22%3A%7B%22GITHUB_PERSONAL_ACCESS_TOKEN%22%3A%22%24%7Binput%3Agithub_token%7D%22%7D%7D) [](https://insiders.vscode.dev/redirect/mcp/install?name=github&inputs=%5B%7B%22id%22%3A%22github_token%22%2C%22type%22%3A%22promptString%22%2C%22description%22%3A%22GitHub%20Personal%20Access%20Token%22%2C%22password%22%3Atrue%7D%5D&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22-e%22%2C%22GITHUB_PERSONAL_ACCESS_TOKEN%22%2C%22ghcr.io%2Fgithub%2Fgithub-mcp-server%22%5D%2C%22env%22%3A%7B%22GITHUB_PERSONAL_ACCESS_TOKEN%22%3A%22%24%7Binput%3Agithub_token%7D%22%7D%7D&quality=insiders)
-
-## Use Cases
+### Use Cases
- Automating GitHub workflows and processes.
- Extracting and analyzing data from GitHub repositories.
- Building AI powered tools and applications that interact with GitHub's ecosystem.
+---
+
+## Remote GitHub MCP Server
+
+[](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2F%22%7D) [](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2F%22%7D&quality=insiders)
+
+The remote GitHub MCP Server is hosted by GitHub and provides the easiest method for getting up and running. If your MCP host does not support remote MCP servers, don't worry! You can use the [local version of the GitHub MCP Server](https://github.com/github/github-mcp-server?tab=readme-ov-file#local-github-mcp-server) instead.
+
+## Prerequisites
+
+1. An MCP host that supports the latest MCP specification and remote servers, such as [VS Code](https://code.visualstudio.com/).
+
+## Installation
+
+### Usage with VS Code
+
+For quick installation, use one of the one-click install buttons above. Once you complete that flow, toggle Agent mode (located by the Copilot Chat text input) and the server will start. Make sure you're using [VS Code 1.101](https://code.visualstudio.com/updates/v1_101) or [later](https://code.visualstudio.com/updates) for remote MCP and OAuth support.
+
+
+Alternatively, to manually configure VS Code, choose the appropriate JSON block from the examples below and add it to your host configuration:
+
+
+Using OAuth | Using a GitHub PAT |
+VS Code (version 1.101 or greater) |
+
+
+
+```json
+{
+ "servers": {
+ "github-remote": {
+ "type": "http",
+ "url": "https://api.githubcopilot.com/mcp/"
+ }
+ }
+}
+```
+
+ |
+
+
+```json
+{
+ "servers": {
+ "github-remote": {
+ "type": "http",
+ "url": "https://api.githubcopilot.com/mcp/",
+ "headers": {
+ "Authorization": "Bearer ${input:github_mcp_pat}"
+ }
+ }
+ },
+ "inputs": [
+ {
+ "type": "promptString",
+ "id": "github_mcp_pat",
+ "description": "GitHub Personal Access Token",
+ "password": true
+ }
+ ]
+}
+```
+
+ |
+
+
+
+### Usage in other MCP Hosts
+
+For MCP Hosts that are [Remote MCP-compatible](docs/host-integration.md), choose the appropriate JSON block from the examples below and add it to your host configuration:
+
+
+Using OAuth | Using a GitHub PAT |
+
+
+
+```json
+{
+ "mcpServers": {
+ "github-remote": {
+ "url": "https://api.githubcopilot.com/mcp/"
+ }
+ }
+}
+```
+
+ |
+
+
+```json
+{
+ "mcpServers": {
+ "github-remote": {
+ "url": "https://api.githubcopilot.com/mcp/",
+ "authorization_token": "Bearer "
+ }
+ }
+}
+```
+
+ |
+
+
+
+> **Note:** The exact configuration format may vary by host. Refer to your host's documentation for the correct syntax and location for remote MCP server setup.
+
+### Configuration
+
+See [Remote Server Documentation](docs/remote-server.md) on how to pass additional configuration settings to the remote GitHub MCP Server.
+
+---
+
+## Local GitHub MCP Server
+
+[](https://insiders.vscode.dev/redirect/mcp/install?name=github&inputs=%5B%7B%22id%22%3A%22github_token%22%2C%22type%22%3A%22promptString%22%2C%22description%22%3A%22GitHub%20Personal%20Access%20Token%22%2C%22password%22%3Atrue%7D%5D&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22-e%22%2C%22GITHUB_PERSONAL_ACCESS_TOKEN%22%2C%22ghcr.io%2Fgithub%2Fgithub-mcp-server%22%5D%2C%22env%22%3A%7B%22GITHUB_PERSONAL_ACCESS_TOKEN%22%3A%22%24%7Binput%3Agithub_token%7D%22%7D%7D) [](https://insiders.vscode.dev/redirect/mcp/install?name=github&inputs=%5B%7B%22id%22%3A%22github_token%22%2C%22type%22%3A%22promptString%22%2C%22description%22%3A%22GitHub%20Personal%20Access%20Token%22%2C%22password%22%3Atrue%7D%5D&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22-e%22%2C%22GITHUB_PERSONAL_ACCESS_TOKEN%22%2C%22ghcr.io%2Fgithub%2Fgithub-mcp-server%22%5D%2C%22env%22%3A%7B%22GITHUB_PERSONAL_ACCESS_TOKEN%22%3A%22%24%7Binput%3Agithub_token%7D%22%7D%7D&quality=insiders)
+
## Prerequisites
1. To run the server in a container, you will need to have [Docker](https://www.docker.com/) installed.
@@ -23,9 +137,11 @@ The MCP server can use many of the GitHub APIs, so enable the permissions that y
### Usage with VS Code
-For quick installation, use one of the one-click install buttons at the top of this README. Once you complete that flow, toggle Agent mode (located by the Copilot Chat text input) and the server will start.
+For quick installation, use one of the one-click install buttons. Once you complete that flow, toggle Agent mode (located by the Copilot Chat text input) and the server will start.
+
+### Usage in other MCP Hosts
-For manual installation, add the following JSON block to your User Settings (JSON) file in VS Code. You can do this by pressing `Ctrl + Shift + P` and typing `Preferences: Open User Settings (JSON)`.
+Add the following JSON block to your IDE MCP settings.
```json
{
@@ -141,19 +257,25 @@ If you don't have Docker, you can use `go build` to build the binary in the
The GitHub MCP Server supports enabling or disabling specific groups of functionalities via the `--toolsets` flag. This allows you to control which GitHub API capabilities are available to your AI tools. Enabling only the toolsets that you need can help the LLM with tool choice and reduce the context size.
+_Toolsets are not limited to Tools. Relevant MCP Resources and Prompts are also included where applicable._
+
### Available Toolsets
The following sets of tools are available (all are on by default):
| Toolset | Description |
| ----------------------- | ------------------------------------------------------------- |
-| `repos` | Repository-related tools (file operations, branches, commits) |
+| `context` | **Strongly recommended**: Tools that provide context about the current user and GitHub context you are operating in |
+| `code_security` | Code scanning alerts and security features |
| `issues` | Issue-related tools (create, read, update, comment) |
-| `users` | Anything relating to GitHub Users |
+| `notifications` | GitHub Notifications related tools |
| `pull_requests` | Pull request operations (create, merge, review) |
-| `code_security` | Code scanning alerts and security features |
+| `repos` | Repository-related tools (file operations, branches, commits) |
+| `secret_protection` | Secret protection related tools, such as GitHub Secret Scanning |
+| `users` | Anything relating to GitHub Users |
| `experiments` | Experimental features (not considered stable) |
+
#### Specifying Toolsets
To specify toolsets you want available to the LLM, you can pass an allow-list in two ways:
@@ -369,6 +491,14 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
- `page`: Page number (number, optional)
- `perPage`: Results per page (number, optional)
+- **assign_copilot_to_issue** - Assign Copilot to a specific issue in a GitHub repository
+
+ - `owner`: Repository owner (string, required)
+ - `repo`: Repository name (string, required)
+ - `issueNumber`: Issue number (number, required)
+ - _Note_: This tool can help with creating a Pull Request with source code changes to resolve the issue. More information can be found at [GitHub Copilot documentation](https://docs.github.com/en/copilot/using-github-copilot/using-copilot-coding-agent-to-work-on-tasks/about-assigning-tasks-to-copilot)
+
+
### Pull Requests
- **get_pull_request** - Get details of a specific pull request
@@ -427,6 +557,12 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
- `repo`: Repository name (string, required)
- `pullNumber`: Pull request number (number, required)
+- **get_pull_request_diff** - Get the diff of a pull request
+
+ - `owner`: Repository owner (string, required)
+ - `repo`: Repository name (string, required)
+ - `pullNumber`: Pull request number (number, required)
+
- **create_pull_request_review** - Create a review on a pull request review
- `owner`: Repository owner (string, required)
@@ -439,6 +575,53 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
- For inline comments: provide `path`, `position` (or `line`), and `body`
- For multi-line comments: provide `path`, `start_line`, `line`, optional `side`/`start_side`, and `body`
+- **create_pending_pull_request_review** - Create a pending review for a pull request that can be submitted later
+
+ - `owner`: Repository owner (string, required)
+ - `repo`: Repository name (string, required)
+ - `pullNumber`: Pull request number (number, required)
+ - `commitID`: SHA of commit to review (string, optional)
+
+- **add_pull_request_review_comment_to_pending_review** - Add a comment to the requester's latest pending pull request review
+
+ - `owner`: Repository owner (string, required)
+ - `repo`: Repository name (string, required)
+ - `pullNumber`: Pull request number (number, required)
+ - `path`: The relative path to the file that necessitates a comment (string, required)
+ - `body`: The text of the review comment (string, required)
+ - `subjectType`: The level at which the comment is targeted (string, required)
+ - Enum: "FILE", "LINE"
+ - `line`: The line of the blob in the pull request diff that the comment applies to (number, optional)
+ - `side`: The side of the diff to comment on (string, optional)
+ - Enum: "LEFT", "RIGHT"
+ - `startLine`: For multi-line comments, the first line of the range (number, optional)
+ - `startSide`: For multi-line comments, the starting side of the diff (string, optional)
+ - Enum: "LEFT", "RIGHT"
+
+- **submit_pending_pull_request_review** - Submit the requester's latest pending pull request review
+
+ - `owner`: Repository owner (string, required)
+ - `repo`: Repository name (string, required)
+ - `pullNumber`: Pull request number (number, required)
+ - `event`: The event to perform (string, required)
+ - Enum: "APPROVE", "REQUEST_CHANGES", "COMMENT"
+ - `body`: The text of the review comment (string, optional)
+
+- **delete_pending_pull_request_review** - Delete the requester's latest pending pull request review
+
+ - `owner`: Repository owner (string, required)
+ - `repo`: Repository name (string, required)
+ - `pullNumber`: Pull request number (number, required)
+
+- **create_and_submit_pull_request_review** - Create and submit a review for a pull request without review comments
+
+ - `owner`: Repository owner (string, required)
+ - `repo`: Repository name (string, required)
+ - `pullNumber`: Pull request number (number, required)
+ - `body`: Review comment text (string, required)
+ - `event`: Review action ('APPROVE', 'REQUEST_CHANGES', 'COMMENT') (string, required)
+ - `commitID`: SHA of commit to review (string, optional)
+
- **create_pull_request** - Create a new pull request
- `owner`: Repository owner (string, required)
@@ -494,6 +677,13 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
- `branch`: Branch name (string, optional)
- `sha`: File SHA if updating (string, optional)
+- **delete_file** - Delete a file from a GitHub repository
+ - `owner`: Repository owner (string, required)
+ - `repo`: Repository name (string, required)
+ - `path`: Path to the file to delete (string, required)
+ - `message`: Commit message (string, required)
+ - `branch`: Branch to delete the file from (string, required)
+
- **list_branches** - List branches in a GitHub repository
- `owner`: Repository owner (string, required)
- `repo`: Repository name (string, required)
@@ -552,6 +742,17 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
- `page`: Page number, for files in the commit (number, optional)
- `perPage`: Results per page, for files in the commit (number, optional)
+- **get_tag** - Get details about a specific git tag in a GitHub repository
+ - `owner`: Repository owner (string, required)
+ - `repo`: Repository name (string, required)
+ - `tag`: Tag name (string, required)
+
+- **list_tags** - List git tags in a GitHub repository
+ - `owner`: Repository owner (string, required)
+ - `repo`: Repository name (string, required)
+ - `page`: Page number (number, optional)
+ - `perPage`: Results per page (number, optional)
+
- **search_code** - Search for code across GitHub repositories
- `query`: Search query (string, required)
- `sort`: Sort field (string, optional)
@@ -610,7 +811,6 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
- `page`: Page number (number, optional)
- `perPage`: Results per page (number, optional)
-
- **get_notification_details** – Get detailed information for a specific GitHub notification
- `notificationID`: The ID of the notification (string, required)
diff --git a/docs/host-integration.md b/docs/host-integration.md
new file mode 100644
index 000000000..d9f6d9050
--- /dev/null
+++ b/docs/host-integration.md
@@ -0,0 +1,193 @@
+# GitHub Remote MCP Integration Guide for MCP Host Authors
+
+This guide outlines high-level considerations for MCP Host authors who want to allow installation of the Remote GitHub MCP server.
+
+The goal is to explain the architecture at a high-level, define key requirements, and provide guidance to get you started, while pointing to official documentation for deeper implementation details.
+
+---
+
+## Table of Contents
+
+- [Understanding MCP Architecture](#understanding-mcp-architecture)
+- [Connecting to the Remote GitHub MCP Server](#connecting-to-the-remote-github-mcp-server)
+ - [Authentication and Authorization](#authentication-and-authorization)
+ - [OAuth Support on GitHub](#oauth-support-on-github)
+ - [Create an OAuth-enabled App Using the GitHub UI](#create-an-oauth-enabled-app-using-the-github-ui)
+ - [Things to Consider](#things-to-consider)
+ - [Initiating the OAuth Flow from your Client Application](#initiating-the-oauth-flow-from-your-client-application)
+- [Handling Organization Access Restrictions](#handling-organization-access-restrictions)
+- [Essential Security Considerations](#essential-security-considerations)
+- [Additional Resources](#additional-resources)
+
+---
+
+## Understanding MCP Architecture
+
+The Model Context Protocol (MCP) enables seamless communication between your application and various external tools through an architecture defined by the [MCP Standard](https://modelcontextprotocol.io/).
+
+### High-level Architecture
+
+The diagram below illustrates how a single client application can connect to multiple MCP Servers, each providing access to a unique set of resources. Notice that some MCP Servers are running locally (side-by-side with the client application) while others are hosted remotely. GitHub's MCP offerings are available to run either locally or remotely.
+
+```mermaid
+flowchart LR
+ subgraph "Local Runtime Environment"
+ subgraph "Client Application (e.g., IDE)"
+ CLIENTAPP[Application Runtime]
+ CX["MCP Client (FileSystem)"]
+ CY["MCP Client (GitHub)"]
+ CZ["MCP Client (Other)"]
+ end
+
+ LOCALMCP[File System MCP Server]
+ end
+
+ subgraph "Internet"
+ GITHUBMCP[GitHub Remote MCP Server]
+ OTHERMCP[Other Remote MCP Server]
+ end
+
+ CLIENTAPP --> CX
+ CLIENTAPP --> CY
+ CLIENTAPP --> CZ
+
+ CX <-->|"stdio"| LOCALMCP
+ CY <-->|"OAuth 2.0 + HTTP/SSE"| GITHUBMCP
+ CZ <-->|"OAuth 2.0 + HTTP/SSE"| OTHERMCP
+```
+
+### Runtime Environment
+
+- **Application**: The user-facing application you are building. It instantiates one or more MCP clients and orchestrates tool calls.
+- **MCP Client**: A component within your client application that maintains a 1:1 connection with a single MCP server.
+- **MCP Server**: A service that provides access to a specific set of tools.
+ - **Local MCP Server**: An MCP Server running locally, side-by-side with the Application.
+ - **Remote MCP Server**: An MCP Server running remotely, accessed via the internet. Most Remote MCP Servers require authentication via OAuth.
+
+For more detail, see the [official MCP specification](https://modelcontextprotocol.io/specification/draft).
+
+> [!NOTE]
+> GitHub offers both a Local MCP Server and a Remote MCP Server.
+
+---
+
+## Connecting to the Remote GitHub MCP Server
+
+### Authentication and Authorization
+
+GitHub MCP Servers require a valid access token in the `Authorization` header. This is true for both the Local GitHub MCP Server and the Remote GitHub MCP Server.
+
+For the Remote GitHub MCP Server, the recommended way to obtain a valid access token is to ensure your client application supports [OAuth 2.1](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13). It should be noted, however, that you may also supply any valid access token. For example, you may supply a pre-generated Personal Access Token (PAT).
+
+
+> [!IMPORTANT]
+> The Remote GitHub MCP Server itself does not provide Authentication services.
+> Your client application must obtain valid GitHub access tokens through one of the supported methods.
+
+The expected flow for obtaining a valid access token via OAuth is depicted in the [MCP Specification](https://modelcontextprotocol.io/specification/draft/basic/authorization#authorization-flow-steps). For convenience, we've embedded a copy of the authorization flow below. Please study it carefully as the remainder of this document is written with this flow in mind.
+
+```mermaid
+sequenceDiagram
+ participant B as User-Agent (Browser)
+ participant C as Client
+ participant M as MCP Server (Resource Server)
+ participant A as Authorization Server
+
+ C->>M: MCP request without token
+ M->>C: HTTP 401 Unauthorized with WWW-Authenticate header
+ Note over C: Extract resource_metadata URL from WWW-Authenticate
+
+ C->>M: Request Protected Resource Metadata
+ M->>C: Return metadata
+
+ Note over C: Parse metadata and extract authorization server(s)
Client determines AS to use
+
+ C->>A: GET /.well-known/oauth-authorization-server
+ A->>C: Authorization server metadata response
+
+ alt Dynamic client registration
+ C->>A: POST /register
+ A->>C: Client Credentials
+ end
+
+ Note over C: Generate PKCE parameters
+ C->>B: Open browser with authorization URL + code_challenge
+ B->>A: Authorization request
+ Note over A: User authorizes
+ A->>B: Redirect to callback with authorization code
+ B->>C: Authorization code callback
+ C->>A: Token request + code_verifier
+ A->>C: Access token (+ refresh token)
+ C->>M: MCP request with access token
+ M-->>C: MCP response
+ Note over C,M: MCP communication continues with valid token
+```
+
+> [!NOTE]
+> Dynamic Client Registration is NOT supported by Remote GitHub MCP Server at this time.
+
+
+#### OAuth Support on GitHub
+
+GitHub offers two solutions for obtaining access tokens via OAuth: [**GitHub Apps**](https://docs.github.com/en/apps/using-github-apps/about-using-github-apps#about-github-apps) and [**OAuth Apps**](https://docs.github.com/en/apps/oauth-apps). These solutions are typically created, administered, and maintained by GitHub Organization administrators. Collaborate with a GitHub Organization administrator to configure either a **GitHub App** or an **OAuth App** to allow your client application to utilize GitHub OAuth support. Furthermore, be aware that it may be necessary for users of your client application to register your **GitHub App** or **OAuth App** within their own GitHub Organization in order to generate authorization tokens capable of accessing Organization's GitHub resources.
+
+> [!TIP]
+> Before proceeding, check whether your organization already supports one of these solutions. Administrators of your GitHub Organization can help you determine what **GitHub Apps** or **OAuth Apps** are already registered. If there's an existing **GitHub App** or **OAuth App** that fits your use case, consider reusing it for Remote MCP Authorization. That said, be sure to take heed of the following warning.
+
+> [!WARNING]
+> Both **GitHub Apps** and **OAuth Apps** require the client application to pass a "client secret" in order to initiate the OAuth flow. If your client application is designed to run in an uncontrolled environment (i.e. customer-provided hardware), end users will be able to discover your "client secret" and potentially exploit it for other purposes. In such cases, our recommendation is to register a new **GitHub App** (or **OAuth App**) exclusively dedicated to servicing OAuth requests from your client application.
+
+#### Create an OAuth-enabled App Using the GitHub UI
+
+Detailed instructions for creating a **GitHub App** can be found at ["Creating GitHub Apps"](https://docs.github.com/en/apps/creating-github-apps/about-creating-github-apps/about-creating-github-apps#building-a-github-app). (RECOMMENDED)
+Detailed instructions for creating an **OAuth App** can be found ["Creating an OAuth App"](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app).
+
+For guidance on which type of app to choose, see ["Differences Between GitHub Apps and OAuth Apps"](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/differences-between-github-apps-and-oauth-apps).
+
+#### Things to Consider:
+- Tokens provided by **GitHub Apps** are generally more secure because they:
+ - include an expiration
+ - include support for fine-grained permissions
+- **GitHub Apps** must be installed on a GitHub Organization before they can be used.
In general, installation must be approved by someone in the Organization with administrator permissions. For more details, see [this explanation](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/differences-between-github-apps-and-oauth-apps#who-can-install-github-apps-and-authorize-oauth-apps).
By contrast, **OAuth Apps** don't require installation and, typically, can be used immediately.
+- Members of an Organization may use the GitHub UI to [request that a GitHub App be installed](https://docs.github.com/en/apps/using-github-apps/requesting-a-github-app-from-your-organization-owner) organization-wide.
+- While not strictly necessary, if you expect that a wide range of users will use your MCP Server, consider publishing its corresponding **GitHub App** or **OAuth App** on the [GitHub App Marketplace](https://github.com/marketplace?type=apps) to ensure that it's discoverable by your audience.
+
+
+#### Initiating the OAuth Flow from your Client Application
+
+For **GitHub Apps**, details on initiating the OAuth flow from a client application are described in detail [here](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app#using-the-web-application-flow-to-generate-a-user-access-token).
+
+For **OAuth Apps**, details on initiating the OAuth flow from a client application are described in detail [here](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#web-application-flow).
+
+> [!IMPORTANT]
+> For endpoint discovery, be sure to honor the [`WWW-Authenticate` information provided](https://modelcontextprotocol.io/specification/draft/basic/authorization#authorization-server-location) by the Remote GitHub MCP Server rather than relying on hard-coded endpoints like `https://github.com/login/oauth/authorize`.
+
+
+### Handling Organization Access Restrictions
+Organizations may block **GitHub Apps** and **OAuth Apps** until explicitly approved. Within your client application code, you can provide actionable next steps for a smooth user experience in the event that OAuth-related calls fail due to your **GitHub App** or **OAuth App** being unavailable (i.e. not registered within the user's organization).
+
+1. Detect the specific error.
+2. Notify the user clearly.
+3. Depending on their GitHub organization privileges:
+ - Org Members: Prompt them to request approval from a GitHub organization admin, within the organization where access has not been approved.
+ - Org Admins: Link them to the corresponding GitHub organization’s App approval settings at `https://github.com/organizations/[ORG_NAME]/settings/oauth_application_policy`
+
+
+## Essential Security Considerations
+- **Token Storage**: Use secure platform APIs (e.g. keytar for Node.js).
+- **Input Validation**: Sanitize all tool arguments.
+- **HTTPS Only**: Never send requests over plaintext HTTP. Always use HTTPS in production.
+- **PKCE:** We strongly recommend implementing [PKCE](https://datatracker.ietf.org/doc/html/rfc7636) for all OAuth flows to prevent code interception, to prepare for upcoming PKCE support.
+
+## Additional Resources
+- [MCP Official Spec](https://modelcontextprotocol.io/specification/draft)
+- [MCP SDKs](https://modelcontextprotocol.io/sdk/java/mcp-overview)
+- [GitHub Docs on Creating GitHub Apps](https://docs.github.com/en/apps/creating-github-apps)
+- [GitHub Docs on Using GitHub Apps](https://docs.github.com/en/apps/using-github-apps/about-using-github-apps)
+- [GitHub Docs on Creating OAuth Apps](https://docs.github.com/en/apps/oauth-apps)
+- GitHub Docs on Installing OAuth Apps into a [Personal Account](https://docs.github.com/en/apps/oauth-apps/using-oauth-apps/installing-an-oauth-app-in-your-personal-account) and [Organization](https://docs.github.com/en/apps/oauth-apps/using-oauth-apps/installing-an-oauth-app-in-your-organization)
+- [Managing OAuth Apps at the Organization Level](https://docs.github.com/en/organizations/managing-oauth-access-to-your-organizations-data)
+- [Managing Programmatic Access at the GitHub Organization Level](https://docs.github.com/en/organizations/managing-programmatic-access-to-your-organization)
+- [Building Copilot Extensions](https://docs.github.com/en/copilot/building-copilot-extensions)
+- [Managing App/Extension Visibility](https://docs.github.com/en/copilot/building-copilot-extensions/managing-the-availability-of-your-copilot-extension) (including GitHub Marketplace information)
+- [Example Implementation in VS Code Repository](https://github.com/microsoft/vscode/blob/main/src/vs/workbench/api/common/extHostMcp.ts#L313)
diff --git a/docs/remote-server.md b/docs/remote-server.md
new file mode 100644
index 000000000..888caef43
--- /dev/null
+++ b/docs/remote-server.md
@@ -0,0 +1,35 @@
+# Remote GitHub MCP Server 🚀
+
+[](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2F%22%7D) [](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2F%22%7D&quality=insiders)
+
+Easily connect to the GitHub MCP Server using the hosted version – no local setup or runtime required.
+
+**URL:** https://api.githubcopilot.com/mcp/
+
+## About
+
+The remote GitHub MCP server is built using this repository as a library, and binding it into GitHub server infrastructure with an internal repository. You can open issues and propose changes in this repository, and we regularly update the remote server to include the latest version of this code.
+
+## Remote MCP Toolsets
+
+Below is a table of available toolsets for the remote GitHub MCP Server. Each toolset is provided as a distinct URL so you can mix and match to create the perfect combination of tools for your use-case. Add `/readonly` to the end of any URL to restrict the tools in the toolset to only those that enable read access. We also provide the option to use [headers](#headers) instead.
+
+
+| Name | Description | API URL | 1-Click Install (VS Code) | Read-only Link | 1-Click Read-only Install (VS Code) |
+|----------------|--------------------------------------------------|-------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| all | All available GitHub MCP tools | https://api.githubcopilot.com/mcp/ | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2F%22%7D) | [read-only](https://api.githubcopilot.com/mcp/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Freadonly%22%7D) |
+| code_security | Code security related tools, such as Code Scanning| https://api.githubcopilot.com/mcp/x/code_security | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-code_security&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fcode_security%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/code_security/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-code_security&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fcode_security%2Freadonly%22%7D)|
+| issues | GitHub Issues related tools | https://api.githubcopilot.com/mcp/x/issues | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-issues&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fissues%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/issues/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-issues&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fissues%2Freadonly%22%7D) |
+| notifications | GitHub Notifications related tools | https://api.githubcopilot.com/mcp/x/notifications | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-notifications&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fnotifications%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/notifications/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-notifications&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fnotifications%2Freadonly%22%7D)|
+| pull_requests | GitHub Pull Request related tools | https://api.githubcopilot.com/mcp/x/pull_requests | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-pull_requests&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fpull_requests%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/pull_requests/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-pull_requests&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fpull_requests%2Freadonly%22%7D)|
+| repos | GitHub Repository related tools | https://api.githubcopilot.com/mcp/x/repos | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-repos&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Frepos%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/repos/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-repos&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Frepos%2Freadonly%22%7D) |
+| secret_protection | Secret protection related tools, e.g. Secret Scanning | https://api.githubcopilot.com/mcp/x/secret_protection | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-secret_protection&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fsecret_protection%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/secret_protection/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-secret_protection&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fsecret_protection%2Freadonly%22%7D)|
+| users | GitHub User related tools | https://api.githubcopilot.com/mcp/x/users | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-users&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fusers%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/users/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-users&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fusers%2Freadonly%22%7D) |
+
+### Headers
+
+You can configure toolsets and readonly mode by providing HTTP headers in your server configuration.
+
+The headers are:
+- `X-MCP-Toolsets=,...`
+- `X-MCP-Readonly=true`
diff --git a/go.mod b/go.mod
index ab2302ed5..d2f28d7da 100644
--- a/go.mod
+++ b/go.mod
@@ -26,7 +26,7 @@ require (
require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
- github.com/go-viper/mapstructure/v2 v2.2.1
+ github.com/go-viper/mapstructure/v2 v2.3.0
github.com/google/go-github/v71 v71.0.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
diff --git a/go.sum b/go.sum
index e7f6794a7..a8a950e9c 100644
--- a/go.sum
+++ b/go.sum
@@ -13,8 +13,8 @@ github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrKU=
github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
-github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
-github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk=
+github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
diff --git a/internal/ghmcp/server.go b/internal/ghmcp/server.go
index 593411ae3..ca38e76b3 100644
--- a/internal/ghmcp/server.go
+++ b/internal/ghmcp/server.go
@@ -14,6 +14,7 @@ import (
"github.com/github/github-mcp-server/pkg/github"
mcplog "github.com/github/github-mcp-server/pkg/log"
+ "github.com/github/github-mcp-server/pkg/raw"
"github.com/github/github-mcp-server/pkg/translations"
gogithub "github.com/google/go-github/v72/github"
"github.com/mark3labs/mcp-go/mcp"
@@ -112,20 +113,24 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) {
return gqlClient, nil // closing over client
}
+ getRawClient := func(ctx context.Context) (*raw.Client, error) {
+ client, err := getClient(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+ }
+ return raw.NewClient(client, apiHost.rawURL), nil // closing over client
+ }
+
// Create default toolsets
- tsg := github.DefaultToolsetGroup(cfg.ReadOnly, getClient, getGQLClient, cfg.Translator)
+ tsg := github.DefaultToolsetGroup(cfg.ReadOnly, getClient, getGQLClient, getRawClient, cfg.Translator)
err = tsg.EnableToolsets(enabledToolsets)
if err != nil {
return nil, fmt.Errorf("failed to enable toolsets: %w", err)
}
- context := github.InitContextToolset(getClient, cfg.Translator)
- github.RegisterResources(ghServer, getClient, cfg.Translator)
-
- // Register the tools with the server
- tsg.RegisterTools(ghServer)
- context.RegisterTools(ghServer)
+ // Register all mcp functionality with the server
+ tsg.RegisterAll(ghServer)
if cfg.DynamicToolsets {
dynamic := github.InitDynamicToolset(ghServer, tsg, cfg.Translator)
@@ -241,6 +246,7 @@ type apiHost struct {
baseRESTURL *url.URL
graphqlURL *url.URL
uploadURL *url.URL
+ rawURL *url.URL
}
func newDotcomHost() (apiHost, error) {
@@ -259,10 +265,16 @@ func newDotcomHost() (apiHost, error) {
return apiHost{}, fmt.Errorf("failed to parse dotcom Upload URL: %w", err)
}
+ rawURL, err := url.Parse("https://raw.githubusercontent.com/")
+ if err != nil {
+ return apiHost{}, fmt.Errorf("failed to parse dotcom Raw URL: %w", err)
+ }
+
return apiHost{
baseRESTURL: baseRestURL,
graphqlURL: gqlURL,
uploadURL: uploadURL,
+ rawURL: rawURL,
}, nil
}
@@ -292,10 +304,16 @@ func newGHECHost(hostname string) (apiHost, error) {
return apiHost{}, fmt.Errorf("failed to parse GHEC Upload URL: %w", err)
}
+ rawURL, err := url.Parse(fmt.Sprintf("https://raw.%s/", u.Hostname()))
+ if err != nil {
+ return apiHost{}, fmt.Errorf("failed to parse GHEC Raw URL: %w", err)
+ }
+
return apiHost{
baseRESTURL: restURL,
graphqlURL: gqlURL,
uploadURL: uploadURL,
+ rawURL: rawURL,
}, nil
}
@@ -319,11 +337,16 @@ func newGHESHost(hostname string) (apiHost, error) {
if err != nil {
return apiHost{}, fmt.Errorf("failed to parse GHES Upload URL: %w", err)
}
+ rawURL, err := url.Parse(fmt.Sprintf("%s://%s/raw/", u.Scheme, u.Hostname()))
+ if err != nil {
+ return apiHost{}, fmt.Errorf("failed to parse GHES Raw URL: %w", err)
+ }
return apiHost{
baseRESTURL: restURL,
graphqlURL: gqlURL,
uploadURL: uploadURL,
+ rawURL: rawURL,
}, nil
}
diff --git a/pkg/github/helper_test.go b/pkg/github/helper_test.go
index 4b9a243de..bc1ae412f 100644
--- a/pkg/github/helper_test.go
+++ b/pkg/github/helper_test.go
@@ -132,6 +132,36 @@ func getTextResult(t *testing.T, result *mcp.CallToolResult) mcp.TextContent {
return textContent
}
+func getErrorResult(t *testing.T, result *mcp.CallToolResult) mcp.TextContent {
+ res := getTextResult(t, result)
+ require.True(t, result.IsError, "expected tool call result to be an error")
+ return res
+}
+
+// getTextResourceResult is a helper function that returns a text result from a tool call.
+func getTextResourceResult(t *testing.T, result *mcp.CallToolResult) mcp.TextResourceContents {
+ t.Helper()
+ assert.NotNil(t, result)
+ require.Len(t, result.Content, 2)
+ content := result.Content[1]
+ require.IsType(t, mcp.EmbeddedResource{}, content)
+ resource := content.(mcp.EmbeddedResource)
+ require.IsType(t, mcp.TextResourceContents{}, resource.Resource)
+ return resource.Resource.(mcp.TextResourceContents)
+}
+
+// getBlobResourceResult is a helper function that returns a blob result from a tool call.
+func getBlobResourceResult(t *testing.T, result *mcp.CallToolResult) mcp.BlobResourceContents {
+ t.Helper()
+ assert.NotNil(t, result)
+ require.Len(t, result.Content, 2)
+ content := result.Content[1]
+ require.IsType(t, mcp.EmbeddedResource{}, content)
+ resource := content.(mcp.EmbeddedResource)
+ require.IsType(t, mcp.BlobResourceContents{}, resource.Resource)
+ return resource.Resource.(mcp.BlobResourceContents)
+}
+
func TestOptionalParamOK(t *testing.T) {
tests := []struct {
name string
diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go
index 093e5fdcd..3475167b1 100644
--- a/pkg/github/repositories.go
+++ b/pkg/github/repositories.go
@@ -2,11 +2,15 @@ package github
import (
"context"
+ "encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
+ "net/url"
+ "strings"
+ "github.com/github/github-mcp-server/pkg/raw"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/google/go-github/v72/github"
"github.com/mark3labs/mcp-go/mcp"
@@ -409,7 +413,7 @@ func CreateRepository(getClient GetClientFn, t translations.TranslationHelperFun
}
// GetFileContents creates a tool to get the contents of a file or directory from a GitHub repository.
-func GetFileContents(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
+func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("get_file_contents",
mcp.WithDescription(t("TOOL_GET_FILE_CONTENTS_DESCRIPTION", "Get the contents of a file or directory from a GitHub repository")),
mcp.WithToolAnnotation(mcp.ToolAnnotation{
@@ -426,7 +430,7 @@ func GetFileContents(getClient GetClientFn, t translations.TranslationHelperFunc
),
mcp.WithString("path",
mcp.Required(),
- mcp.Description("Path to file/directory"),
+ mcp.Description("Path to file/directory (directories must end with a slash '/')"),
),
mcp.WithString("branch",
mcp.Description("Branch to get contents from"),
@@ -450,38 +454,92 @@ func GetFileContents(getClient GetClientFn, t translations.TranslationHelperFunc
return mcp.NewToolResultError(err.Error()), nil
}
- client, err := getClient(ctx)
- if err != nil {
- return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+ // If the path is (most likely) not to be a directory, we will first try to get the raw content from the GitHub raw content API.
+ if path != "" && !strings.HasSuffix(path, "/") {
+ rawOpts := &raw.RawContentOpts{}
+ if branch != "" {
+ rawOpts.Ref = "refs/heads/" + branch
+ }
+ rawClient, err := getRawClient(ctx)
+ if err != nil {
+ return mcp.NewToolResultError("failed to get GitHub raw content client"), nil
+ }
+ resp, err := rawClient.GetRawContent(ctx, owner, repo, path, rawOpts)
+ if err != nil {
+ return mcp.NewToolResultError("failed to get raw repository content"), nil
+ }
+ defer func() {
+ _ = resp.Body.Close()
+ }()
+
+ if resp.StatusCode != http.StatusOK {
+ // If the raw content is not found, we will fall back to the GitHub API (in case it is a directory)
+ } else {
+ // If the raw content is found, return it directly
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return mcp.NewToolResultError("failed to read response body"), nil
+ }
+ contentType := resp.Header.Get("Content-Type")
+
+ var resourceURI string
+ if branch == "" {
+ // do a safe url join
+ resourceURI, err = url.JoinPath("repo://", owner, repo, "contents", path)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create resource URI: %w", err)
+ }
+ } else {
+ resourceURI, err = url.JoinPath("repo://", owner, repo, "refs", "heads", branch, "contents", path)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create resource URI: %w", err)
+ }
+ }
+ if strings.HasPrefix(contentType, "application") || strings.HasPrefix(contentType, "text") {
+ return mcp.NewToolResultResource("successfully downloaded text file", mcp.TextResourceContents{
+ URI: resourceURI,
+ Text: string(body),
+ MIMEType: contentType,
+ }), nil
+ }
+
+ return mcp.NewToolResultResource("successfully downloaded binary file", mcp.BlobResourceContents{
+ URI: resourceURI,
+ Blob: base64.StdEncoding.EncodeToString(body),
+ MIMEType: contentType,
+ }), nil
+
+ }
}
- opts := &github.RepositoryContentGetOptions{Ref: branch}
- fileContent, dirContent, resp, err := client.Repositories.GetContents(ctx, owner, repo, path, opts)
+
+ client, err := getClient(ctx)
if err != nil {
- return nil, fmt.Errorf("failed to get file contents: %w", err)
+ return mcp.NewToolResultError("failed to get GitHub client"), nil
}
- defer func() { _ = resp.Body.Close() }()
- if resp.StatusCode != 200 {
- body, err := io.ReadAll(resp.Body)
+ if strings.HasSuffix(path, "/") {
+ opts := &github.RepositoryContentGetOptions{Ref: branch}
+ _, dirContent, resp, err := client.Repositories.GetContents(ctx, owner, repo, path, opts)
if err != nil {
- return nil, fmt.Errorf("failed to read response body: %w", err)
+ return mcp.NewToolResultError("failed to get file contents"), nil
}
- return mcp.NewToolResultError(fmt.Sprintf("failed to get file contents: %s", string(body))), nil
- }
+ defer func() { _ = resp.Body.Close() }()
- var result interface{}
- if fileContent != nil {
- result = fileContent
- } else {
- result = dirContent
- }
+ if resp.StatusCode != 200 {
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return mcp.NewToolResultError("failed to read response body"), nil
+ }
+ return mcp.NewToolResultError(fmt.Sprintf("failed to get file contents: %s", string(body))), nil
+ }
- r, err := json.Marshal(result)
- if err != nil {
- return nil, fmt.Errorf("failed to marshal response: %w", err)
+ r, err := json.Marshal(dirContent)
+ if err != nil {
+ return mcp.NewToolResultError("failed to marshal response"), nil
+ }
+ return mcp.NewToolResultText(string(r)), nil
}
-
- return mcp.NewToolResultText(string(r)), nil
+ return mcp.NewToolResultError("Failed to get file contents. The path does not point to a file or directory, or the file does not exist in the repository."), nil
}
}
diff --git a/pkg/github/repositories_test.go b/pkg/github/repositories_test.go
index f7924b2f9..c2585341e 100644
--- a/pkg/github/repositories_test.go
+++ b/pkg/github/repositories_test.go
@@ -2,13 +2,17 @@ package github
import (
"context"
+ "encoding/base64"
"encoding/json"
"net/http"
+ "net/url"
"testing"
"time"
+ "github.com/github/github-mcp-server/pkg/raw"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/google/go-github/v72/github"
+ "github.com/mark3labs/mcp-go/mcp"
"github.com/migueleliasweb/go-github-mock/src/mock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -17,7 +21,8 @@ import (
func Test_GetFileContents(t *testing.T) {
// Verify tool definition once
mockClient := github.NewClient(nil)
- tool, _ := GetFileContents(stubGetClientFn(mockClient), translations.NullTranslationHelper)
+ mockRawClient := raw.NewClient(mockClient, &url.URL{Scheme: "https", Host: "raw.githubusercontent.com", Path: "/"})
+ tool, _ := GetFileContents(stubGetClientFn(mockClient), stubGetRawClientFn(mockRawClient), translations.NullTranslationHelper)
assert.Equal(t, "get_file_contents", tool.Name)
assert.NotEmpty(t, tool.Description)
@@ -27,17 +32,8 @@ func Test_GetFileContents(t *testing.T) {
assert.Contains(t, tool.InputSchema.Properties, "branch")
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "path"})
- // Setup mock file content for success case
- mockFileContent := &github.RepositoryContent{
- Type: github.Ptr("file"),
- Name: github.Ptr("README.md"),
- Path: github.Ptr("README.md"),
- Content: github.Ptr("IyBUZXN0IFJlcG9zaXRvcnkKClRoaXMgaXMgYSB0ZXN0IHJlcG9zaXRvcnku"), // Base64 encoded "# Test Repository\n\nThis is a test repository."
- SHA: github.Ptr("abc123"),
- Size: github.Ptr(42),
- HTMLURL: github.Ptr("https://github.com/owner/repo/blob/main/README.md"),
- DownloadURL: github.Ptr("https://raw.githubusercontent.com/owner/repo/main/README.md"),
- }
+ // Mock response for raw content
+ mockRawContent := []byte("# Test Repository\n\nThis is a test repository.")
// Setup mock directory content for success case
mockDirContent := []*github.RepositoryContent{
@@ -65,17 +61,17 @@ func Test_GetFileContents(t *testing.T) {
expectError bool
expectedResult interface{}
expectedErrMsg string
+ expectStatus int
}{
{
- name: "successful file content fetch",
+ name: "successful text content fetch",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
- mock.GetReposContentsByOwnerByRepoByPath,
- expectQueryParams(t, map[string]string{
- "ref": "main",
- }).andThen(
- mockResponse(t, http.StatusOK, mockFileContent),
- ),
+ raw.GetRawReposContentsByOwnerByRepoByBranchByPath,
+ http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "text/markdown")
+ _, _ = w.Write(mockRawContent)
+ }),
),
),
requestArgs: map[string]interface{}{
@@ -84,8 +80,36 @@ func Test_GetFileContents(t *testing.T) {
"path": "README.md",
"branch": "main",
},
- expectError: false,
- expectedResult: mockFileContent,
+ expectError: false,
+ expectedResult: mcp.TextResourceContents{
+ URI: "repo://owner/repo/refs/heads/main/contents/README.md",
+ Text: "# Test Repository\n\nThis is a test repository.",
+ MIMEType: "text/markdown",
+ },
+ },
+ {
+ name: "successful file blob content fetch",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ raw.GetRawReposContentsByOwnerByRepoByBranchByPath,
+ http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "image/png")
+ _, _ = w.Write(mockRawContent)
+ }),
+ ),
+ ),
+ requestArgs: map[string]interface{}{
+ "owner": "owner",
+ "repo": "repo",
+ "path": "test.png",
+ "branch": "main",
+ },
+ expectError: false,
+ expectedResult: mcp.BlobResourceContents{
+ URI: "repo://owner/repo/refs/heads/main/contents/test.png",
+ Blob: base64.StdEncoding.EncodeToString(mockRawContent),
+ MIMEType: "image/png",
+ },
},
{
name: "successful directory content fetch",
@@ -96,11 +120,19 @@ func Test_GetFileContents(t *testing.T) {
mockResponse(t, http.StatusOK, mockDirContent),
),
),
+ mock.WithRequestMatchHandler(
+ raw.GetRawReposContentsByOwnerByRepoByPath,
+ expectQueryParams(t, map[string]string{
+ "branch": "main",
+ }).andThen(
+ mockResponse(t, http.StatusNotFound, nil),
+ ),
+ ),
),
requestArgs: map[string]interface{}{
"owner": "owner",
"repo": "repo",
- "path": "src",
+ "path": "src/",
},
expectError: false,
expectedResult: mockDirContent,
@@ -115,6 +147,13 @@ func Test_GetFileContents(t *testing.T) {
_, _ = w.Write([]byte(`{"message": "Not Found"}`))
}),
),
+ mock.WithRequestMatchHandler(
+ raw.GetRawReposContentsByOwnerByRepoByPath,
+ http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusNotFound)
+ _, _ = w.Write([]byte(`{"message": "Not Found"}`))
+ }),
+ ),
),
requestArgs: map[string]interface{}{
"owner": "owner",
@@ -122,8 +161,8 @@ func Test_GetFileContents(t *testing.T) {
"path": "nonexistent.md",
"branch": "main",
},
- expectError: true,
- expectedErrMsg: "failed to get file contents",
+ expectError: false,
+ expectedResult: mcp.NewToolResultError("Failed to get file contents. The path does not point to a file or directory, or the file does not exist in the repository."),
},
}
@@ -131,7 +170,8 @@ func Test_GetFileContents(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
client := github.NewClient(tc.mockedClient)
- _, handler := GetFileContents(stubGetClientFn(client), translations.NullTranslationHelper)
+ mockRawClient := raw.NewClient(client, &url.URL{Scheme: "https", Host: "raw.example.com", Path: "/"})
+ _, handler := GetFileContents(stubGetClientFn(client), stubGetRawClientFn(mockRawClient), translations.NullTranslationHelper)
// Create call request
request := createMCPRequest(tc.requestArgs)
@@ -147,20 +187,17 @@ func Test_GetFileContents(t *testing.T) {
}
require.NoError(t, err)
-
- // Parse the result and get the text content if no error
- textContent := getTextResult(t, result)
-
- // Verify based on expected type
+ // Use the correct result helper based on the expected type
switch expected := tc.expectedResult.(type) {
- case *github.RepositoryContent:
- var returnedContent github.RepositoryContent
- err = json.Unmarshal([]byte(textContent.Text), &returnedContent)
- require.NoError(t, err)
- assert.Equal(t, *expected.Name, *returnedContent.Name)
- assert.Equal(t, *expected.Path, *returnedContent.Path)
- assert.Equal(t, *expected.Type, *returnedContent.Type)
+ case mcp.TextResourceContents:
+ textResource := getTextResourceResult(t, result)
+ assert.Equal(t, expected, textResource)
+ case mcp.BlobResourceContents:
+ blobResource := getBlobResourceResult(t, result)
+ assert.Equal(t, expected, blobResource)
case []*github.RepositoryContent:
+ // Directory content fetch returns a text result (JSON array)
+ textContent := getTextResult(t, result)
var returnedContents []*github.RepositoryContent
err = json.Unmarshal([]byte(textContent.Text), &returnedContents)
require.NoError(t, err)
@@ -170,6 +207,9 @@ func Test_GetFileContents(t *testing.T) {
assert.Equal(t, *expected[i].Path, *content.Path)
assert.Equal(t, *expected[i].Type, *content.Type)
}
+ case mcp.TextContent:
+ textContent := getErrorResult(t, result)
+ require.Equal(t, textContent, expected)
}
})
}
diff --git a/pkg/github/repository_resource.go b/pkg/github/repository_resource.go
index 7e1ce51cc..fd2a04f89 100644
--- a/pkg/github/repository_resource.go
+++ b/pkg/github/repository_resource.go
@@ -9,8 +9,10 @@ import (
"mime"
"net/http"
"path/filepath"
+ "strconv"
"strings"
+ "github.com/github/github-mcp-server/pkg/raw"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/google/go-github/v72/github"
"github.com/mark3labs/mcp-go/mcp"
@@ -18,52 +20,52 @@ import (
)
// GetRepositoryResourceContent defines the resource template and handler for getting repository content.
-func GetRepositoryResourceContent(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) {
+func GetRepositoryResourceContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) {
return mcp.NewResourceTemplate(
"repo://{owner}/{repo}/contents{/path*}", // Resource template
t("RESOURCE_REPOSITORY_CONTENT_DESCRIPTION", "Repository Content"),
),
- RepositoryResourceContentsHandler(getClient)
+ RepositoryResourceContentsHandler(getClient, getRawClient)
}
// GetRepositoryResourceBranchContent defines the resource template and handler for getting repository content for a branch.
-func GetRepositoryResourceBranchContent(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) {
+func GetRepositoryResourceBranchContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) {
return mcp.NewResourceTemplate(
"repo://{owner}/{repo}/refs/heads/{branch}/contents{/path*}", // Resource template
t("RESOURCE_REPOSITORY_CONTENT_BRANCH_DESCRIPTION", "Repository Content for specific branch"),
),
- RepositoryResourceContentsHandler(getClient)
+ RepositoryResourceContentsHandler(getClient, getRawClient)
}
// GetRepositoryResourceCommitContent defines the resource template and handler for getting repository content for a commit.
-func GetRepositoryResourceCommitContent(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) {
+func GetRepositoryResourceCommitContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) {
return mcp.NewResourceTemplate(
"repo://{owner}/{repo}/sha/{sha}/contents{/path*}", // Resource template
t("RESOURCE_REPOSITORY_CONTENT_COMMIT_DESCRIPTION", "Repository Content for specific commit"),
),
- RepositoryResourceContentsHandler(getClient)
+ RepositoryResourceContentsHandler(getClient, getRawClient)
}
// GetRepositoryResourceTagContent defines the resource template and handler for getting repository content for a tag.
-func GetRepositoryResourceTagContent(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) {
+func GetRepositoryResourceTagContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) {
return mcp.NewResourceTemplate(
"repo://{owner}/{repo}/refs/tags/{tag}/contents{/path*}", // Resource template
t("RESOURCE_REPOSITORY_CONTENT_TAG_DESCRIPTION", "Repository Content for specific tag"),
),
- RepositoryResourceContentsHandler(getClient)
+ RepositoryResourceContentsHandler(getClient, getRawClient)
}
// GetRepositoryResourcePrContent defines the resource template and handler for getting repository content for a pull request.
-func GetRepositoryResourcePrContent(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) {
+func GetRepositoryResourcePrContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) {
return mcp.NewResourceTemplate(
"repo://{owner}/{repo}/refs/pull/{prNumber}/head/contents{/path*}", // Resource template
t("RESOURCE_REPOSITORY_CONTENT_PR_DESCRIPTION", "Repository Content for specific pull request"),
),
- RepositoryResourceContentsHandler(getClient)
+ RepositoryResourceContentsHandler(getClient, getRawClient)
}
// RepositoryResourceContentsHandler returns a handler function for repository content requests.
-func RepositoryResourceContentsHandler(getClient GetClientFn) func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
+func RepositoryResourceContentsHandler(getClient GetClientFn, getRawClient raw.GetRawClientFn) func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
return func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
// the matcher will give []string with one element
// https://github.com/mark3labs/mcp-go/pull/54
@@ -87,121 +89,104 @@ func RepositoryResourceContentsHandler(getClient GetClientFn) func(ctx context.C
}
opts := &github.RepositoryContentGetOptions{}
+ rawOpts := &raw.RawContentOpts{}
sha, ok := request.Params.Arguments["sha"].([]string)
if ok && len(sha) > 0 {
opts.Ref = sha[0]
+ rawOpts.SHA = sha[0]
}
branch, ok := request.Params.Arguments["branch"].([]string)
if ok && len(branch) > 0 {
opts.Ref = "refs/heads/" + branch[0]
+ rawOpts.Ref = "refs/heads/" + branch[0]
}
tag, ok := request.Params.Arguments["tag"].([]string)
if ok && len(tag) > 0 {
opts.Ref = "refs/tags/" + tag[0]
+ rawOpts.Ref = "refs/tags/" + tag[0]
}
prNumber, ok := request.Params.Arguments["prNumber"].([]string)
if ok && len(prNumber) > 0 {
- opts.Ref = "refs/pull/" + prNumber[0] + "/head"
+ // fetch the PR from the API to get the latest commit and use SHA
+ githubClient, err := getClient(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+ }
+ prNum, err := strconv.Atoi(prNumber[0])
+ if err != nil {
+ return nil, fmt.Errorf("invalid pull request number: %w", err)
+ }
+ pr, _, err := githubClient.PullRequests.Get(ctx, owner, repo, prNum)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get pull request: %w", err)
+ }
+ sha := pr.GetHead().GetSHA()
+ rawOpts.SHA = sha
+ opts.Ref = sha
}
-
- client, err := getClient(ctx)
- if err != nil {
- return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+ // if it's a directory
+ if path == "" || strings.HasSuffix(path, "/") {
+ return nil, fmt.Errorf("directories are not supported: %s", path)
}
- fileContent, directoryContent, _, err := client.Repositories.GetContents(ctx, owner, repo, path, opts)
+ rawClient, err := getRawClient(ctx)
+
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("failed to get GitHub raw content client: %w", err)
}
- if directoryContent != nil {
- var resources []mcp.ResourceContents
- for _, entry := range directoryContent {
- mimeType := "text/directory"
- if entry.GetType() == "file" {
- // this is system dependent, and a best guess
- ext := filepath.Ext(entry.GetName())
- mimeType = mime.TypeByExtension(ext)
- if ext == ".md" {
- mimeType = "text/markdown"
- }
- }
- resources = append(resources, mcp.TextResourceContents{
- URI: entry.GetHTMLURL(),
- MIMEType: mimeType,
- Text: entry.GetName(),
- })
-
+ resp, err := rawClient.GetRawContent(ctx, owner, repo, path, rawOpts)
+ defer func() {
+ _ = resp.Body.Close()
+ }()
+ // If the raw content is not found, we will fall back to the GitHub API (in case it is a directory)
+ switch {
+ case err != nil:
+ return nil, fmt.Errorf("failed to get raw content: %w", err)
+ case resp.StatusCode == http.StatusOK:
+ ext := filepath.Ext(path)
+ mimeType := resp.Header.Get("Content-Type")
+ if ext == ".md" {
+ mimeType = "text/markdown"
+ } else if mimeType == "" {
+ mimeType = mime.TypeByExtension(ext)
}
- return resources, nil
- }
- if fileContent != nil {
- if fileContent.Content != nil {
- // download the file content from fileContent.GetDownloadURL() and use the content-type header to determine the MIME type
- // and return the content as a blob unless it is a text file, where you can return the content as text
- req, err := http.NewRequest("GET", fileContent.GetDownloadURL(), nil)
- if err != nil {
- return nil, fmt.Errorf("failed to create request: %w", err)
- }
-
- resp, err := client.Client().Do(req)
- if err != nil {
- return nil, fmt.Errorf("failed to send request: %w", err)
- }
- defer func() { _ = resp.Body.Close() }()
-
- if resp.StatusCode != http.StatusOK {
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- return nil, fmt.Errorf("failed to read response body: %w", err)
- }
- return nil, fmt.Errorf("failed to fetch file content: %s", string(body))
- }
-
- ext := filepath.Ext(fileContent.GetName())
- mimeType := resp.Header.Get("Content-Type")
- if ext == ".md" {
- mimeType = "text/markdown"
- } else if mimeType == "" {
- // backstop to the file extension if the content type is not set
- mimeType = mime.TypeByExtension(filepath.Ext(fileContent.GetName()))
- }
-
- // if the content is a string, return it as text
- if strings.HasPrefix(mimeType, "text") {
- content, err := io.ReadAll(resp.Body)
- if err != nil {
- return nil, fmt.Errorf("failed to parse the response body: %w", err)
- }
-
- return []mcp.ResourceContents{
- mcp.TextResourceContents{
- URI: request.Params.URI,
- MIMEType: mimeType,
- Text: string(content),
- },
- }, nil
- }
- // otherwise, read the content and encode it as base64
- decodedContent, err := io.ReadAll(resp.Body)
- if err != nil {
- return nil, fmt.Errorf("failed to parse the response body: %w", err)
- }
+ content, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read file content: %w", err)
+ }
+ switch {
+ case strings.HasPrefix(mimeType, "text"), strings.HasPrefix(mimeType, "application"):
+ return []mcp.ResourceContents{
+ mcp.TextResourceContents{
+ URI: request.Params.URI,
+ MIMEType: mimeType,
+ Text: string(content),
+ },
+ }, nil
+ default:
return []mcp.ResourceContents{
mcp.BlobResourceContents{
URI: request.Params.URI,
MIMEType: mimeType,
- Blob: base64.StdEncoding.EncodeToString(decodedContent), // Encode content as Base64
+ Blob: base64.StdEncoding.EncodeToString(content),
},
}, nil
}
+ case resp.StatusCode != http.StatusNotFound:
+ // If we got a response but it is not 200 OK, we return an error
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read response body: %w", err)
+ }
+ return nil, fmt.Errorf("failed to fetch raw content: %s", string(body))
+ default:
+ // This should be unreachable because GetContents should return an error if neither file nor directory content is found.
+ return nil, errors.New("404 Not Found")
}
-
- // This should be unreachable because GetContents should return an error if neither file nor directory content is found.
- return nil, errors.New("no repository resource content found")
}
}
diff --git a/pkg/github/repository_resource_test.go b/pkg/github/repository_resource_test.go
index a99edb5cf..0e9f018e7 100644
--- a/pkg/github/repository_resource_test.go
+++ b/pkg/github/repository_resource_test.go
@@ -3,8 +3,10 @@ package github
import (
"context"
"net/http"
+ "net/url"
"testing"
+ "github.com/github/github-mcp-server/pkg/raw"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/google/go-github/v72/github"
"github.com/mark3labs/mcp-go/mcp"
@@ -12,82 +14,8 @@ import (
"github.com/stretchr/testify/require"
)
-var GetRawReposContentsByOwnerByRepoByPath mock.EndpointPattern = mock.EndpointPattern{
- Pattern: "/{owner}/{repo}/main/{path:.+}",
- Method: "GET",
-}
-
func Test_repositoryResourceContentsHandler(t *testing.T) {
- mockDirContent := []*github.RepositoryContent{
- {
- Type: github.Ptr("file"),
- Name: github.Ptr("README.md"),
- Path: github.Ptr("README.md"),
- SHA: github.Ptr("abc123"),
- Size: github.Ptr(42),
- HTMLURL: github.Ptr("https://github.com/owner/repo/blob/main/README.md"),
- DownloadURL: github.Ptr("https://raw.githubusercontent.com/owner/repo/main/README.md"),
- },
- {
- Type: github.Ptr("dir"),
- Name: github.Ptr("src"),
- Path: github.Ptr("src"),
- SHA: github.Ptr("def456"),
- HTMLURL: github.Ptr("https://github.com/owner/repo/tree/main/src"),
- DownloadURL: github.Ptr("https://raw.githubusercontent.com/owner/repo/main/src"),
- },
- }
- expectedDirContent := []mcp.TextResourceContents{
- {
- URI: "https://github.com/owner/repo/blob/main/README.md",
- MIMEType: "text/markdown",
- Text: "README.md",
- },
- {
- URI: "https://github.com/owner/repo/tree/main/src",
- MIMEType: "text/directory",
- Text: "src",
- },
- }
-
- mockTextContent := &github.RepositoryContent{
- Type: github.Ptr("file"),
- Name: github.Ptr("README.md"),
- Path: github.Ptr("README.md"),
- Content: github.Ptr("# Test Repository\n\nThis is a test repository."),
- SHA: github.Ptr("abc123"),
- Size: github.Ptr(42),
- HTMLURL: github.Ptr("https://github.com/owner/repo/blob/main/README.md"),
- DownloadURL: github.Ptr("https://raw.githubusercontent.com/owner/repo/main/README.md"),
- }
-
- mockFileContent := &github.RepositoryContent{
- Type: github.Ptr("file"),
- Name: github.Ptr("data.png"),
- Path: github.Ptr("data.png"),
- Content: github.Ptr("IyBUZXN0IFJlcG9zaXRvcnkKClRoaXMgaXMgYSB0ZXN0IHJlcG9zaXRvcnku"), // Base64 encoded "# Test Repository\n\nThis is a test repository."
- SHA: github.Ptr("abc123"),
- Size: github.Ptr(42),
- HTMLURL: github.Ptr("https://github.com/owner/repo/blob/main/data.png"),
- DownloadURL: github.Ptr("https://raw.githubusercontent.com/owner/repo/main/data.png"),
- }
-
- expectedFileContent := []mcp.BlobResourceContents{
- {
- Blob: "IyBUZXN0IFJlcG9zaXRvcnkKClRoaXMgaXMgYSB0ZXN0IHJlcG9zaXRvcnku",
- MIMEType: "image/png",
- URI: "",
- },
- }
-
- expectedTextContent := []mcp.TextResourceContents{
- {
- Text: "# Test Repository\n\nThis is a test repository.",
- MIMEType: "text/markdown",
- URI: "",
- },
- }
-
+ base, _ := url.Parse("https://raw.example.com/")
tests := []struct {
name string
mockedClient *http.Client
@@ -98,9 +26,14 @@ func Test_repositoryResourceContentsHandler(t *testing.T) {
{
name: "missing owner",
mockedClient: mock.NewMockedHTTPClient(
- mock.WithRequestMatch(
- mock.GetReposContentsByOwnerByRepoByPath,
- mockFileContent,
+ mock.WithRequestMatchHandler(
+ raw.GetRawReposContentsByOwnerByRepoByPath,
+ http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "image/png")
+ // as this is given as a png, it will return the content as a blob
+ _, err := w.Write([]byte("# Test Repository\n\nThis is a test repository."))
+ require.NoError(t, err)
+ }),
),
),
requestArgs: map[string]any{},
@@ -109,9 +42,14 @@ func Test_repositoryResourceContentsHandler(t *testing.T) {
{
name: "missing repo",
mockedClient: mock.NewMockedHTTPClient(
- mock.WithRequestMatch(
- mock.GetReposContentsByOwnerByRepoByPath,
- mockFileContent,
+ mock.WithRequestMatchHandler(
+ raw.GetRawReposContentsByOwnerByRepoByBranchByPath,
+ http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "image/png")
+ // as this is given as a png, it will return the content as a blob
+ _, err := w.Write([]byte("# Test Repository\n\nThis is a test repository."))
+ require.NoError(t, err)
+ }),
),
),
requestArgs: map[string]any{
@@ -122,38 +60,59 @@ func Test_repositoryResourceContentsHandler(t *testing.T) {
{
name: "successful blob content fetch",
mockedClient: mock.NewMockedHTTPClient(
- mock.WithRequestMatch(
- mock.GetReposContentsByOwnerByRepoByPath,
- mockFileContent,
- ),
mock.WithRequestMatchHandler(
- GetRawReposContentsByOwnerByRepoByPath,
+ raw.GetRawReposContentsByOwnerByRepoByPath,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "image/png")
- // as this is given as a png, it will return the content as a blob
_, err := w.Write([]byte("# Test Repository\n\nThis is a test repository."))
require.NoError(t, err)
}),
),
),
requestArgs: map[string]any{
- "owner": []string{"owner"},
- "repo": []string{"repo"},
- "path": []string{"data.png"},
- "branch": []string{"main"},
+ "owner": []string{"owner"},
+ "repo": []string{"repo"},
+ "path": []string{"data.png"},
},
- expectedResult: expectedFileContent,
+ expectedResult: []mcp.BlobResourceContents{{
+ Blob: "IyBUZXN0IFJlcG9zaXRvcnkKClRoaXMgaXMgYSB0ZXN0IHJlcG9zaXRvcnku",
+ MIMEType: "image/png",
+ URI: "",
+ }},
},
{
- name: "successful text content fetch",
+ name: "successful text content fetch (HEAD)",
mockedClient: mock.NewMockedHTTPClient(
- mock.WithRequestMatch(
- mock.GetReposContentsByOwnerByRepoByPath,
- mockTextContent,
+ mock.WithRequestMatchHandler(
+ raw.GetRawReposContentsByOwnerByRepoByPath,
+ http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "text/markdown")
+ _, err := w.Write([]byte("# Test Repository\n\nThis is a test repository."))
+ require.NoError(t, err)
+ }),
),
- mock.WithRequestMatch(
- GetRawReposContentsByOwnerByRepoByPath,
- []byte("# Test Repository\n\nThis is a test repository."),
+ ),
+ requestArgs: map[string]any{
+ "owner": []string{"owner"},
+ "repo": []string{"repo"},
+ "path": []string{"README.md"},
+ },
+ expectedResult: []mcp.TextResourceContents{{
+ Text: "# Test Repository\n\nThis is a test repository.",
+ MIMEType: "text/markdown",
+ URI: "",
+ }},
+ },
+ {
+ name: "successful text content fetch (branch)",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ raw.GetRawReposContentsByOwnerByRepoByBranchByPath,
+ http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "text/markdown")
+ _, err := w.Write([]byte("# Test Repository\n\nThis is a test repository."))
+ require.NoError(t, err)
+ }),
),
),
requestArgs: map[string]any{
@@ -162,37 +121,91 @@ func Test_repositoryResourceContentsHandler(t *testing.T) {
"path": []string{"README.md"},
"branch": []string{"main"},
},
- expectedResult: expectedTextContent,
+ expectedResult: []mcp.TextResourceContents{{
+ Text: "# Test Repository\n\nThis is a test repository.",
+ MIMEType: "text/markdown",
+ URI: "",
+ }},
},
{
- name: "successful directory content fetch",
+ name: "successful text content fetch (tag)",
mockedClient: mock.NewMockedHTTPClient(
- mock.WithRequestMatch(
- mock.GetReposContentsByOwnerByRepoByPath,
- mockDirContent,
+ mock.WithRequestMatchHandler(
+ raw.GetRawReposContentsByOwnerByRepoByTagByPath,
+ http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "text/markdown")
+ _, err := w.Write([]byte("# Test Repository\n\nThis is a test repository."))
+ require.NoError(t, err)
+ }),
),
),
requestArgs: map[string]any{
"owner": []string{"owner"},
"repo": []string{"repo"},
- "path": []string{"src"},
+ "path": []string{"README.md"},
+ "tag": []string{"v1.0.0"},
},
- expectedResult: expectedDirContent,
+ expectedResult: []mcp.TextResourceContents{{
+ Text: "# Test Repository\n\nThis is a test repository.",
+ MIMEType: "text/markdown",
+ URI: "",
+ }},
},
{
- name: "empty data",
+ name: "successful text content fetch (sha)",
mockedClient: mock.NewMockedHTTPClient(
- mock.WithRequestMatch(
- mock.GetReposContentsByOwnerByRepoByPath,
- []*github.RepositoryContent{},
+ mock.WithRequestMatchHandler(
+ raw.GetRawReposContentsByOwnerByRepoBySHAByPath,
+ http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "text/markdown")
+ _, err := w.Write([]byte("# Test Repository\n\nThis is a test repository."))
+ require.NoError(t, err)
+ }),
),
),
requestArgs: map[string]any{
"owner": []string{"owner"},
"repo": []string{"repo"},
- "path": []string{"src"},
+ "path": []string{"README.md"},
+ "sha": []string{"abc123"},
+ },
+ expectedResult: []mcp.TextResourceContents{{
+ Text: "# Test Repository\n\nThis is a test repository.",
+ MIMEType: "text/markdown",
+ URI: "",
+ }},
+ },
+ {
+ name: "successful text content fetch (pr)",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ mock.GetReposPullsByOwnerByRepoByPullNumber,
+ http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ _, err := w.Write([]byte(`{"head": {"sha": "abc123"}}`))
+ require.NoError(t, err)
+ }),
+ ),
+ mock.WithRequestMatchHandler(
+ raw.GetRawReposContentsByOwnerByRepoBySHAByPath,
+ http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "text/markdown")
+ _, err := w.Write([]byte("# Test Repository\n\nThis is a test repository."))
+ require.NoError(t, err)
+ }),
+ ),
+ ),
+ requestArgs: map[string]any{
+ "owner": []string{"owner"},
+ "repo": []string{"repo"},
+ "path": []string{"README.md"},
+ "prNumber": []string{"42"},
},
- expectedResult: nil,
+ expectedResult: []mcp.TextResourceContents{{
+ Text: "# Test Repository\n\nThis is a test repository.",
+ MIMEType: "text/markdown",
+ URI: "",
+ }},
},
{
name: "content fetch fails",
@@ -218,7 +231,8 @@ func Test_repositoryResourceContentsHandler(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := github.NewClient(tc.mockedClient)
- handler := RepositoryResourceContentsHandler((stubGetClientFn(client)))
+ mockRawClient := raw.NewClient(client, base)
+ handler := RepositoryResourceContentsHandler((stubGetClientFn(client)), stubGetRawClientFn(mockRawClient))
request := mcp.ReadResourceRequest{
Params: struct {
@@ -243,25 +257,24 @@ func Test_repositoryResourceContentsHandler(t *testing.T) {
}
func Test_GetRepositoryResourceContent(t *testing.T) {
- tmpl, _ := GetRepositoryResourceContent(nil, translations.NullTranslationHelper)
+ mockRawClient := raw.NewClient(github.NewClient(nil), &url.URL{})
+ tmpl, _ := GetRepositoryResourceContent(nil, stubGetRawClientFn(mockRawClient), translations.NullTranslationHelper)
require.Equal(t, "repo://{owner}/{repo}/contents{/path*}", tmpl.URITemplate.Raw())
}
func Test_GetRepositoryResourceBranchContent(t *testing.T) {
- tmpl, _ := GetRepositoryResourceBranchContent(nil, translations.NullTranslationHelper)
+ mockRawClient := raw.NewClient(github.NewClient(nil), &url.URL{})
+ tmpl, _ := GetRepositoryResourceBranchContent(nil, stubGetRawClientFn(mockRawClient), translations.NullTranslationHelper)
require.Equal(t, "repo://{owner}/{repo}/refs/heads/{branch}/contents{/path*}", tmpl.URITemplate.Raw())
}
func Test_GetRepositoryResourceCommitContent(t *testing.T) {
- tmpl, _ := GetRepositoryResourceCommitContent(nil, translations.NullTranslationHelper)
+ mockRawClient := raw.NewClient(github.NewClient(nil), &url.URL{})
+ tmpl, _ := GetRepositoryResourceCommitContent(nil, stubGetRawClientFn(mockRawClient), translations.NullTranslationHelper)
require.Equal(t, "repo://{owner}/{repo}/sha/{sha}/contents{/path*}", tmpl.URITemplate.Raw())
}
func Test_GetRepositoryResourceTagContent(t *testing.T) {
- tmpl, _ := GetRepositoryResourceTagContent(nil, translations.NullTranslationHelper)
+ mockRawClient := raw.NewClient(github.NewClient(nil), &url.URL{})
+ tmpl, _ := GetRepositoryResourceTagContent(nil, stubGetRawClientFn(mockRawClient), translations.NullTranslationHelper)
require.Equal(t, "repo://{owner}/{repo}/refs/tags/{tag}/contents{/path*}", tmpl.URITemplate.Raw())
}
-
-func Test_GetRepositoryResourcePrContent(t *testing.T) {
- tmpl, _ := GetRepositoryResourcePrContent(nil, translations.NullTranslationHelper)
- require.Equal(t, "repo://{owner}/{repo}/refs/pull/{prNumber}/head/contents{/path*}", tmpl.URITemplate.Raw())
-}
diff --git a/pkg/github/resources.go b/pkg/github/resources.go
deleted file mode 100644
index 774261e94..000000000
--- a/pkg/github/resources.go
+++ /dev/null
@@ -1,14 +0,0 @@
-package github
-
-import (
- "github.com/github/github-mcp-server/pkg/translations"
- "github.com/mark3labs/mcp-go/server"
-)
-
-func RegisterResources(s *server.MCPServer, getClient GetClientFn, t translations.TranslationHelperFunc) {
- s.AddResourceTemplate(GetRepositoryResourceContent(getClient, t))
- s.AddResourceTemplate(GetRepositoryResourceBranchContent(getClient, t))
- s.AddResourceTemplate(GetRepositoryResourceCommitContent(getClient, t))
- s.AddResourceTemplate(GetRepositoryResourceTagContent(getClient, t))
- s.AddResourceTemplate(GetRepositoryResourcePrContent(getClient, t))
-}
diff --git a/pkg/github/server_test.go b/pkg/github/server_test.go
index db0b0b237..3f00d7b24 100644
--- a/pkg/github/server_test.go
+++ b/pkg/github/server_test.go
@@ -8,6 +8,7 @@ import (
"net/http"
"testing"
+ "github.com/github/github-mcp-server/pkg/raw"
"github.com/google/go-github/v72/github"
"github.com/shurcooL/githubv4"
"github.com/stretchr/testify/assert"
@@ -37,6 +38,12 @@ func stubGetGQLClientFn(client *githubv4.Client) GetGQLClientFn {
}
}
+func stubGetRawClientFn(client *raw.Client) raw.GetRawClientFn {
+ return func(_ context.Context) (*raw.Client, error) {
+ return client, nil
+ }
+}
+
func badRequestHandler(msg string) http.HandlerFunc {
return func(w http.ResponseWriter, _ *http.Request) {
structuredErrorResponse := github.ErrorResponse{
diff --git a/pkg/github/tools.go b/pkg/github/tools.go
index 550adddd7..9569c4390 100644
--- a/pkg/github/tools.go
+++ b/pkg/github/tools.go
@@ -3,6 +3,7 @@ package github
import (
"context"
+ "github.com/github/github-mcp-server/pkg/raw"
"github.com/github/github-mcp-server/pkg/toolsets"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/google/go-github/v72/github"
@@ -15,7 +16,7 @@ type GetGQLClientFn func(context.Context) (*githubv4.Client, error)
var DefaultTools = []string{"all"}
-func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) *toolsets.ToolsetGroup {
+func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetGQLClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) *toolsets.ToolsetGroup {
tsg := toolsets.NewToolsetGroup(readOnly)
// Define all available features with their default state (disabled)
@@ -23,7 +24,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
repos := toolsets.NewToolset("repos", "GitHub Repository related tools").
AddReadTools(
toolsets.NewServerTool(SearchRepositories(getClient, t)),
- toolsets.NewServerTool(GetFileContents(getClient, t)),
+ toolsets.NewServerTool(GetFileContents(getClient, getRawClient, t)),
toolsets.NewServerTool(ListCommits(getClient, t)),
toolsets.NewServerTool(SearchCode(getClient, t)),
toolsets.NewServerTool(GetCommit(getClient, t)),
@@ -38,6 +39,13 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
toolsets.NewServerTool(CreateBranch(getClient, t)),
toolsets.NewServerTool(PushFiles(getClient, t)),
toolsets.NewServerTool(DeleteFile(getClient, t)),
+ ).
+ AddResourceTemplates(
+ toolsets.NewServerResourceTemplate(GetRepositoryResourceContent(getClient, getRawClient, t)),
+ toolsets.NewServerResourceTemplate(GetRepositoryResourceBranchContent(getClient, getRawClient, t)),
+ toolsets.NewServerResourceTemplate(GetRepositoryResourceCommitContent(getClient, getRawClient, t)),
+ toolsets.NewServerResourceTemplate(GetRepositoryResourceTagContent(getClient, getRawClient, t)),
+ toolsets.NewServerResourceTemplate(GetRepositoryResourcePrContent(getClient, getRawClient, t)),
)
issues := toolsets.NewToolset("issues", "GitHub Issues related tools").
AddReadTools(
@@ -106,7 +114,13 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
// Keep experiments alive so the system doesn't error out when it's always enabled
experiments := toolsets.NewToolset("experiments", "Experimental features that are not considered stable yet")
+ contextTools := toolsets.NewToolset("context", "Tools that provide context about the current user and GitHub context you are operating in").
+ AddReadTools(
+ toolsets.NewServerTool(GetMe(getClient, t)),
+ )
+
// Add toolsets to the group
+ tsg.AddToolset(contextTools)
tsg.AddToolset(repos)
tsg.AddToolset(issues)
tsg.AddToolset(users)
@@ -119,16 +133,6 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
return tsg
}
-func InitContextToolset(getClient GetClientFn, t translations.TranslationHelperFunc) *toolsets.Toolset {
- // Create a new context toolset
- contextTools := toolsets.NewToolset("context", "Tools that provide context about the current user and GitHub context you are operating in").
- AddReadTools(
- toolsets.NewServerTool(GetMe(getClient, t)),
- )
- contextTools.Enabled = true
- return contextTools
-}
-
// InitDynamicToolset creates a dynamic toolset that can be used to enable other toolsets, and so requires the server and toolset group as arguments
func InitDynamicToolset(s *server.MCPServer, tsg *toolsets.ToolsetGroup, t translations.TranslationHelperFunc) *toolsets.Toolset {
// Create a new dynamic toolset
diff --git a/pkg/raw/raw.go b/pkg/raw/raw.go
new file mode 100644
index 000000000..d604891b6
--- /dev/null
+++ b/pkg/raw/raw.go
@@ -0,0 +1,69 @@
+// Package raw provides a client for interacting with the GitHub raw file API
+package raw
+
+import (
+ "context"
+ "net/http"
+ "net/url"
+
+ gogithub "github.com/google/go-github/v72/github"
+)
+
+// GetRawClientFn is a function type that returns a RawClient instance.
+type GetRawClientFn func(context.Context) (*Client, error)
+
+// Client is a client for interacting with the GitHub raw content API.
+type Client struct {
+ url *url.URL
+ client *gogithub.Client
+}
+
+// NewClient creates a new instance of the raw API Client with the provided GitHub client and provided URL.
+func NewClient(client *gogithub.Client, rawURL *url.URL) *Client {
+ client = gogithub.NewClient(client.Client())
+ client.BaseURL = rawURL
+ return &Client{client: client, url: rawURL}
+}
+
+func (c *Client) newRequest(method string, urlStr string, body interface{}, opts ...gogithub.RequestOption) (*http.Request, error) {
+ req, err := c.client.NewRequest(method, urlStr, body, opts...)
+ return req, err
+}
+
+func (c *Client) refURL(owner, repo, ref, path string) string {
+ if ref == "" {
+ return c.url.JoinPath(owner, repo, "HEAD", path).String()
+ }
+ return c.url.JoinPath(owner, repo, ref, path).String()
+}
+
+func (c *Client) URLFromOpts(opts *RawContentOpts, owner, repo, path string) string {
+ if opts == nil {
+ opts = &RawContentOpts{}
+ }
+ if opts.SHA != "" {
+ return c.commitURL(owner, repo, opts.SHA, path)
+ }
+ return c.refURL(owner, repo, opts.Ref, path)
+}
+
+// BlobURL returns the URL for a blob in the raw content API.
+func (c *Client) commitURL(owner, repo, sha, path string) string {
+ return c.url.JoinPath(owner, repo, sha, path).String()
+}
+
+type RawContentOpts struct {
+ Ref string
+ SHA string
+}
+
+// GetRawContent fetches the raw content of a file from a GitHub repository.
+func (c *Client) GetRawContent(ctx context.Context, owner, repo, path string, opts *RawContentOpts) (*http.Response, error) {
+ url := c.URLFromOpts(opts, owner, repo, path)
+ req, err := c.newRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return c.client.Client().Do(req)
+}
diff --git a/pkg/raw/raw_mock.go b/pkg/raw/raw_mock.go
new file mode 100644
index 000000000..30c7759d3
--- /dev/null
+++ b/pkg/raw/raw_mock.go
@@ -0,0 +1,20 @@
+package raw
+
+import "github.com/migueleliasweb/go-github-mock/src/mock"
+
+var GetRawReposContentsByOwnerByRepoByPath mock.EndpointPattern = mock.EndpointPattern{
+ Pattern: "/{owner}/{repo}/HEAD/{path:.*}",
+ Method: "GET",
+}
+var GetRawReposContentsByOwnerByRepoByBranchByPath mock.EndpointPattern = mock.EndpointPattern{
+ Pattern: "/{owner}/{repo}/refs/heads/{branch}/{path:.*}",
+ Method: "GET",
+}
+var GetRawReposContentsByOwnerByRepoByTagByPath mock.EndpointPattern = mock.EndpointPattern{
+ Pattern: "/{owner}/{repo}/refs/tags/{tag}/{path:.*}",
+ Method: "GET",
+}
+var GetRawReposContentsByOwnerByRepoBySHAByPath mock.EndpointPattern = mock.EndpointPattern{
+ Pattern: "/{owner}/{repo}/{sha}/{path:.*}",
+ Method: "GET",
+}
diff --git a/pkg/raw/raw_test.go b/pkg/raw/raw_test.go
new file mode 100644
index 000000000..bb9b23a28
--- /dev/null
+++ b/pkg/raw/raw_test.go
@@ -0,0 +1,150 @@
+package raw
+
+import (
+ "context"
+ "net/http"
+ "net/url"
+ "testing"
+
+ "github.com/google/go-github/v72/github"
+ "github.com/migueleliasweb/go-github-mock/src/mock"
+ "github.com/stretchr/testify/require"
+)
+
+func TestGetRawContent(t *testing.T) {
+ base, _ := url.Parse("https://raw.example.com/")
+
+ tests := []struct {
+ name string
+ pattern mock.EndpointPattern
+ opts *RawContentOpts
+ owner, repo, path string
+ statusCode int
+ contentType string
+ body string
+ expectError string
+ }{
+ {
+ name: "HEAD fetch success",
+ pattern: GetRawReposContentsByOwnerByRepoByPath,
+ opts: nil,
+ owner: "octocat", repo: "hello", path: "README.md",
+ statusCode: 200,
+ contentType: "text/plain",
+ body: "# Test file",
+ },
+ {
+ name: "branch fetch success",
+ pattern: GetRawReposContentsByOwnerByRepoByBranchByPath,
+ opts: &RawContentOpts{Ref: "refs/heads/main"},
+ owner: "octocat", repo: "hello", path: "README.md",
+ statusCode: 200,
+ contentType: "text/plain",
+ body: "# Test file",
+ },
+ {
+ name: "tag fetch success",
+ pattern: GetRawReposContentsByOwnerByRepoByTagByPath,
+ opts: &RawContentOpts{Ref: "refs/tags/v1.0.0"},
+ owner: "octocat", repo: "hello", path: "README.md",
+ statusCode: 200,
+ contentType: "text/plain",
+ body: "# Test file",
+ },
+ {
+ name: "sha fetch success",
+ pattern: GetRawReposContentsByOwnerByRepoBySHAByPath,
+ opts: &RawContentOpts{SHA: "abc123"},
+ owner: "octocat", repo: "hello", path: "README.md",
+ statusCode: 200,
+ contentType: "text/plain",
+ body: "# Test file",
+ },
+ {
+ name: "not found",
+ pattern: GetRawReposContentsByOwnerByRepoByPath,
+ opts: nil,
+ owner: "octocat", repo: "hello", path: "notfound.txt",
+ statusCode: 404,
+ contentType: "application/json",
+ body: `{"message": "Not Found"}`,
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ mockedClient := mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ tc.pattern,
+ http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", tc.contentType)
+ w.WriteHeader(tc.statusCode)
+ _, err := w.Write([]byte(tc.body))
+ require.NoError(t, err)
+ }),
+ ),
+ )
+ ghClient := github.NewClient(mockedClient)
+ client := NewClient(ghClient, base)
+ resp, err := client.GetRawContent(context.Background(), tc.owner, tc.repo, tc.path, tc.opts)
+ defer func() {
+ _ = resp.Body.Close()
+ }()
+ if tc.expectError != "" {
+ require.Error(t, err)
+ return
+ }
+ require.NoError(t, err)
+ require.Equal(t, tc.statusCode, resp.StatusCode)
+ })
+ }
+}
+
+func TestUrlFromOpts(t *testing.T) {
+ base, _ := url.Parse("https://raw.example.com/")
+ ghClient := github.NewClient(nil)
+ client := NewClient(ghClient, base)
+
+ tests := []struct {
+ name string
+ opts *RawContentOpts
+ owner string
+ repo string
+ path string
+ want string
+ }{
+ {
+ name: "no opts (HEAD)",
+ opts: nil,
+ owner: "octocat", repo: "hello", path: "README.md",
+ want: "https://raw.example.com/octocat/hello/HEAD/README.md",
+ },
+ {
+ name: "ref branch",
+ opts: &RawContentOpts{Ref: "refs/heads/main"},
+ owner: "octocat", repo: "hello", path: "README.md",
+ want: "https://raw.example.com/octocat/hello/refs/heads/main/README.md",
+ },
+ {
+ name: "ref tag",
+ opts: &RawContentOpts{Ref: "refs/tags/v1.0.0"},
+ owner: "octocat", repo: "hello", path: "README.md",
+ want: "https://raw.example.com/octocat/hello/refs/tags/v1.0.0/README.md",
+ },
+ {
+ name: "sha",
+ opts: &RawContentOpts{SHA: "abc123"},
+ owner: "octocat", repo: "hello", path: "README.md",
+ want: "https://raw.example.com/octocat/hello/abc123/README.md",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := client.URLFromOpts(tt.opts, tt.owner, tt.repo, tt.path)
+ if got != tt.want {
+ t.Errorf("UrlFromOpts() = %q, want %q", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/pkg/toolsets/toolsets.go b/pkg/toolsets/toolsets.go
index fcb5e93b3..ad444c050 100644
--- a/pkg/toolsets/toolsets.go
+++ b/pkg/toolsets/toolsets.go
@@ -33,6 +33,20 @@ func NewServerTool(tool mcp.Tool, handler server.ToolHandlerFunc) server.ServerT
return server.ServerTool{Tool: tool, Handler: handler}
}
+func NewServerResourceTemplate(resourceTemplate mcp.ResourceTemplate, handler server.ResourceTemplateHandlerFunc) ServerResourceTemplate {
+ return ServerResourceTemplate{
+ resourceTemplate: resourceTemplate,
+ handler: handler,
+ }
+}
+
+// ServerResourceTemplate represents a resource template that can be registered with the MCP server.
+type ServerResourceTemplate struct {
+ resourceTemplate mcp.ResourceTemplate
+ handler server.ResourceTemplateHandlerFunc
+}
+
+// Toolset represents a collection of MCP functionality that can be enabled or disabled as a group.
type Toolset struct {
Name string
Description string
@@ -40,6 +54,9 @@ type Toolset struct {
readOnly bool
writeTools []server.ServerTool
readTools []server.ServerTool
+ // resources are not tools, but the community seems to be moving towards namespaces as a broader concept
+ // and in order to have multiple servers running concurrently, we want to avoid overlapping resources too.
+ resourceTemplates []ServerResourceTemplate
}
func (t *Toolset) GetActiveTools() []server.ServerTool {
@@ -73,6 +90,31 @@ func (t *Toolset) RegisterTools(s *server.MCPServer) {
}
}
+func (t *Toolset) AddResourceTemplates(templates ...ServerResourceTemplate) *Toolset {
+ t.resourceTemplates = append(t.resourceTemplates, templates...)
+ return t
+}
+
+func (t *Toolset) GetActiveResourceTemplates() []ServerResourceTemplate {
+ if !t.Enabled {
+ return nil
+ }
+ return t.resourceTemplates
+}
+
+func (t *Toolset) GetAvailableResourceTemplates() []ServerResourceTemplate {
+ return t.resourceTemplates
+}
+
+func (t *Toolset) RegisterResourcesTemplates(s *server.MCPServer) {
+ if !t.Enabled {
+ return
+ }
+ for _, resource := range t.resourceTemplates {
+ s.AddResourceTemplate(resource.resourceTemplate, resource.handler)
+ }
+}
+
func (t *Toolset) SetReadOnly() {
// Set the toolset to read-only
t.readOnly = true
@@ -179,9 +221,10 @@ func (tg *ToolsetGroup) EnableToolset(name string) error {
return nil
}
-func (tg *ToolsetGroup) RegisterTools(s *server.MCPServer) {
+func (tg *ToolsetGroup) RegisterAll(s *server.MCPServer) {
for _, toolset := range tg.Toolsets {
toolset.RegisterTools(s)
+ toolset.RegisterResourcesTemplates(s)
}
}
diff --git a/third-party-licenses.darwin.md b/third-party-licenses.darwin.md
index 5905f040c..e182c63c2 100644
--- a/third-party-licenses.darwin.md
+++ b/third-party-licenses.darwin.md
@@ -11,14 +11,17 @@ Some packages may only be included on certain architectures or operating systems
- [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))
- [github.com/go-openapi/jsonpointer](https://pkg.go.dev/github.com/go-openapi/jsonpointer) ([Apache-2.0](https://github.com/go-openapi/jsonpointer/blob/v0.19.5/LICENSE))
- [github.com/go-openapi/swag](https://pkg.go.dev/github.com/go-openapi/swag) ([Apache-2.0](https://github.com/go-openapi/swag/blob/v0.21.1/LICENSE))
- - [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))
+ - [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.3.0/LICENSE))
+ - [github.com/google/go-github/v71/github](https://pkg.go.dev/github.com/google/go-github/v71/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v71.0.0/LICENSE))
- [github.com/google/go-github/v72/github](https://pkg.go.dev/github.com/google/go-github/v72/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v72.0.0/LICENSE))
- [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))
- [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))
+ - [github.com/gorilla/mux](https://pkg.go.dev/github.com/gorilla/mux) ([BSD-3-Clause](https://github.com/gorilla/mux/blob/v1.8.0/LICENSE))
- [github.com/josephburnett/jd/v2](https://pkg.go.dev/github.com/josephburnett/jd/v2) ([MIT](https://github.com/josephburnett/jd/blob/v1.9.2/LICENSE))
- [github.com/josharian/intern](https://pkg.go.dev/github.com/josharian/intern) ([MIT](https://github.com/josharian/intern/blob/v1.0.0/license.md))
- [github.com/mailru/easyjson](https://pkg.go.dev/github.com/mailru/easyjson) ([MIT](https://github.com/mailru/easyjson/blob/v0.7.7/LICENSE))
- [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.31.0/LICENSE))
+ - [github.com/migueleliasweb/go-github-mock/src/mock](https://pkg.go.dev/github.com/migueleliasweb/go-github-mock/src/mock) ([MIT](https://github.com/migueleliasweb/go-github-mock/blob/v1.3.0/LICENSE))
- [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))
- [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.9.0/LICENSE))
- [github.com/shurcooL/githubv4](https://pkg.go.dev/github.com/shurcooL/githubv4) ([MIT](https://github.com/shurcooL/githubv4/blob/48295856cce7/LICENSE))
@@ -36,6 +39,7 @@ Some packages may only be included on certain architectures or operating systems
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/8a7402ab:LICENSE))
- [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))
- [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))
+ - [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.5.0:LICENSE))
- [gopkg.in/yaml.v2](https://pkg.go.dev/gopkg.in/yaml.v2) ([Apache-2.0](https://github.com/go-yaml/yaml/blob/v2.4.0/LICENSE))
- [gopkg.in/yaml.v3](https://pkg.go.dev/gopkg.in/yaml.v3) ([MIT](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE))
diff --git a/third-party-licenses.linux.md b/third-party-licenses.linux.md
index 5905f040c..e182c63c2 100644
--- a/third-party-licenses.linux.md
+++ b/third-party-licenses.linux.md
@@ -11,14 +11,17 @@ Some packages may only be included on certain architectures or operating systems
- [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))
- [github.com/go-openapi/jsonpointer](https://pkg.go.dev/github.com/go-openapi/jsonpointer) ([Apache-2.0](https://github.com/go-openapi/jsonpointer/blob/v0.19.5/LICENSE))
- [github.com/go-openapi/swag](https://pkg.go.dev/github.com/go-openapi/swag) ([Apache-2.0](https://github.com/go-openapi/swag/blob/v0.21.1/LICENSE))
- - [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))
+ - [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.3.0/LICENSE))
+ - [github.com/google/go-github/v71/github](https://pkg.go.dev/github.com/google/go-github/v71/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v71.0.0/LICENSE))
- [github.com/google/go-github/v72/github](https://pkg.go.dev/github.com/google/go-github/v72/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v72.0.0/LICENSE))
- [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))
- [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))
+ - [github.com/gorilla/mux](https://pkg.go.dev/github.com/gorilla/mux) ([BSD-3-Clause](https://github.com/gorilla/mux/blob/v1.8.0/LICENSE))
- [github.com/josephburnett/jd/v2](https://pkg.go.dev/github.com/josephburnett/jd/v2) ([MIT](https://github.com/josephburnett/jd/blob/v1.9.2/LICENSE))
- [github.com/josharian/intern](https://pkg.go.dev/github.com/josharian/intern) ([MIT](https://github.com/josharian/intern/blob/v1.0.0/license.md))
- [github.com/mailru/easyjson](https://pkg.go.dev/github.com/mailru/easyjson) ([MIT](https://github.com/mailru/easyjson/blob/v0.7.7/LICENSE))
- [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.31.0/LICENSE))
+ - [github.com/migueleliasweb/go-github-mock/src/mock](https://pkg.go.dev/github.com/migueleliasweb/go-github-mock/src/mock) ([MIT](https://github.com/migueleliasweb/go-github-mock/blob/v1.3.0/LICENSE))
- [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))
- [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.9.0/LICENSE))
- [github.com/shurcooL/githubv4](https://pkg.go.dev/github.com/shurcooL/githubv4) ([MIT](https://github.com/shurcooL/githubv4/blob/48295856cce7/LICENSE))
@@ -36,6 +39,7 @@ Some packages may only be included on certain architectures or operating systems
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/8a7402ab:LICENSE))
- [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))
- [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))
+ - [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.5.0:LICENSE))
- [gopkg.in/yaml.v2](https://pkg.go.dev/gopkg.in/yaml.v2) ([Apache-2.0](https://github.com/go-yaml/yaml/blob/v2.4.0/LICENSE))
- [gopkg.in/yaml.v3](https://pkg.go.dev/gopkg.in/yaml.v3) ([MIT](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE))
diff --git a/third-party-licenses.windows.md b/third-party-licenses.windows.md
index b5b5c112c..d8bfd4925 100644
--- a/third-party-licenses.windows.md
+++ b/third-party-licenses.windows.md
@@ -11,15 +11,18 @@ Some packages may only be included on certain architectures or operating systems
- [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))
- [github.com/go-openapi/jsonpointer](https://pkg.go.dev/github.com/go-openapi/jsonpointer) ([Apache-2.0](https://github.com/go-openapi/jsonpointer/blob/v0.19.5/LICENSE))
- [github.com/go-openapi/swag](https://pkg.go.dev/github.com/go-openapi/swag) ([Apache-2.0](https://github.com/go-openapi/swag/blob/v0.21.1/LICENSE))
- - [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))
+ - [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.3.0/LICENSE))
+ - [github.com/google/go-github/v71/github](https://pkg.go.dev/github.com/google/go-github/v71/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v71.0.0/LICENSE))
- [github.com/google/go-github/v72/github](https://pkg.go.dev/github.com/google/go-github/v72/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v72.0.0/LICENSE))
- [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))
- [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))
+ - [github.com/gorilla/mux](https://pkg.go.dev/github.com/gorilla/mux) ([BSD-3-Clause](https://github.com/gorilla/mux/blob/v1.8.0/LICENSE))
- [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))
- [github.com/josephburnett/jd/v2](https://pkg.go.dev/github.com/josephburnett/jd/v2) ([MIT](https://github.com/josephburnett/jd/blob/v1.9.2/LICENSE))
- [github.com/josharian/intern](https://pkg.go.dev/github.com/josharian/intern) ([MIT](https://github.com/josharian/intern/blob/v1.0.0/license.md))
- [github.com/mailru/easyjson](https://pkg.go.dev/github.com/mailru/easyjson) ([MIT](https://github.com/mailru/easyjson/blob/v0.7.7/LICENSE))
- [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.31.0/LICENSE))
+ - [github.com/migueleliasweb/go-github-mock/src/mock](https://pkg.go.dev/github.com/migueleliasweb/go-github-mock/src/mock) ([MIT](https://github.com/migueleliasweb/go-github-mock/blob/v1.3.0/LICENSE))
- [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))
- [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.9.0/LICENSE))
- [github.com/shurcooL/githubv4](https://pkg.go.dev/github.com/shurcooL/githubv4) ([MIT](https://github.com/shurcooL/githubv4/blob/48295856cce7/LICENSE))
@@ -37,6 +40,7 @@ Some packages may only be included on certain architectures or operating systems
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/8a7402ab:LICENSE))
- [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))
- [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))
+ - [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.5.0:LICENSE))
- [gopkg.in/yaml.v2](https://pkg.go.dev/gopkg.in/yaml.v2) ([Apache-2.0](https://github.com/go-yaml/yaml/blob/v2.4.0/LICENSE))
- [gopkg.in/yaml.v3](https://pkg.go.dev/gopkg.in/yaml.v3) ([MIT](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE))
diff --git a/third-party/github.com/google/go-github/v71/github/LICENSE b/third-party/github.com/google/go-github/v71/github/LICENSE
new file mode 100644
index 000000000..28b6486f0
--- /dev/null
+++ b/third-party/github.com/google/go-github/v71/github/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2013 The go-github AUTHORS. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/third-party/github.com/gorilla/mux/LICENSE b/third-party/github.com/gorilla/mux/LICENSE
new file mode 100644
index 000000000..6903df638
--- /dev/null
+++ b/third-party/github.com/gorilla/mux/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2012-2018 The Gorilla Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/third-party/github.com/migueleliasweb/go-github-mock/src/mock/LICENSE b/third-party/github.com/migueleliasweb/go-github-mock/src/mock/LICENSE
new file mode 100644
index 000000000..86d42717d
--- /dev/null
+++ b/third-party/github.com/migueleliasweb/go-github-mock/src/mock/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Miguel Elias dos Santos
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/third-party/golang.org/x/time/rate/LICENSE b/third-party/golang.org/x/time/rate/LICENSE
new file mode 100644
index 000000000..6a66aea5e
--- /dev/null
+++ b/third-party/golang.org/x/time/rate/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2009 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.