> ## Documentation Index
> Fetch the complete documentation index at: https://docs.formal.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# GitHub

> Explore repositories, issues, pull requests, and code with governed access.

Developer Tools Publisher: GitHub

Route GitHub MCP traffic through Formal to control and log what AI agents do in GitHub:

* **Block** risky tools, such as `create_or_update_file`.
* **Mask** sensitive data in tool results, such as emails in `list_commits` responses.
* **Keep credentials at the Connector**, so users and agents never see GitHub tokens.
* **Log** every tool call with the user, the MCP client and its parameters.

Cloud agents

Laptop / VM

Endpoint

[https://api.githubcopilot.com/mcp/](https://api.githubcopilot.com/mcp/)

Connector

![](https://docs.formal.ai/static/brands/github.svg)

## 0. Prepare your Connector

The Connector needs an MCP listener and a hostname with wildcard DNS and TLS.
Set them up once by following [Deploy the Connector](https://docs.formal.ai/docs/guides/core-concepts/connectors/mcp-gateway.md#deploy-the-connector). Every MCP server reuses them.

## 1. Add GitHub MCP server to your Resources

> [tab] UI
>
> 1. Open the [Admin Catalog](https://app.formal.ai/catalog) and click **Add MCP**.
> 2. Select the **GitHub** template.
> 3. Enter a DNS-label **Name**, such as `github`.
> 4. Choose the **Space** containing your Connector, such as `default`.
> 5. Configure upstream authentication as described below, then save.
>
> If no Connector is in the selected Space, the **Configuration** tab shows **MCP clients can't connect directly yet**.
> The reason reads: `No Connector is in this server's Space.`
>
> Adding GitHub to the Admin Catalog

> [tab] Terraform
>
> Set `space_id` to the ID of the Space containing your Connector.
>
> ```hcl theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
> resource "formal_resource" "github" {
>   name                    = "github"
>   technology              = "mcp"
>   hostname                = "api.githubcopilot.com"
>   space_id                = "<SPACE_ID>"
>   native_users_v3_enabled = true
>   port                    = 443
>   tags = {
>     "mcp-endpoint" = "/mcp/"
>   }
> }
>
> resource "formal_resource_oauth_settings" "github" {
>   resource_id = formal_resource.github.id
> }
> ```

> [tab] Pulumi
>
> Set `spaceId` to the ID of the Space containing your Connector.
>
> ```ts theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
> import * as formal from "@formalco/pulumi";
>
> const resource = new formal.Resource("mcp", {
>   name: "github",
>   technology: "mcp",
>   hostname: "api.githubcopilot.com",
>   port: 443,
>   spaceId: "<SPACE_ID>",
>   tags: { "mcp-endpoint": "/mcp/" },
>   nativeUsersV3Enabled: true,
> });
>
> new formal.ResourceOauthSettings("mcpOAuth", {
>   resourceId: resource.id,
> });
> ```

> [tab] API
>
> Create a Resource with the MCP endpoint and its Space ID.
>
> ```bash theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
> export FORMAL_API_KEY="<YOUR_API_KEY>"
> RESOURCE_RESPONSE=$(curl --fail --silent --show-error \
>   --request POST \
>   https://api.formal.ai/core.v1.ResourceService/CreateResource \
>   --header "x-api-key: ${FORMAL_API_KEY}" \
>   --header "Content-Type: application/json" \
>   --data-binary @- <<JSON
>   {
>     "name": "github",
>     "hostname": "api.githubcopilot.com",
>     "port": 443,
>     "technology": "mcp",
>     "spaceId": "<SPACE_ID>",
>     "tags": [
>       {
>         "key": "mcp-endpoint",
>         "value": "/mcp/"
>       }
>     ]
>   }
> JSON
> )
> RESOURCE_ID=$(printf '%s' "${RESOURCE_RESPONSE}" | jq -r '.resource.id')
> curl --fail --silent --show-error \
>   --request POST \
>   https://api.formal.ai/core.v1.ResourceService/CreateResourceOAuthSettings \
>   --header "x-api-key: ${FORMAL_API_KEY}" \
>   --header "Content-Type: application/json" \
>   --data-binary @- <<JSON
>   {
>     "resourceId": "${RESOURCE_ID}"
>   }
> JSON
> ```

## 2. Setup Authentication

> [tab] Linked Accounts (Recommended)
>
> Each user links their own GitHub account once in the Employee Catalog, and the Connector sends that user's token to GitHub.
>
> ![Linked Accounts selected under How does Formal authenticate to this resource?](https://mintcdn.com/formal/vzPMpIcsF4BYmdx8/assets/images/mcp-servers/auth-linked-accounts.png?fit=max\&auto=format\&n=vzPMpIcsF4BYmdx8\&q=85\&s=dc448db2a373faa6e1bf49fd5ffaf7c4)
>
> Under **OAuth app**, choose one of the two options.
>
> #### 1. Formal's App (default)
>
> Formal has a registered OAuth app with GitHub, so you register nothing.
> Users see Formal on GitHub's consent screen.
>
> #### 2. Your OAuth App
>
> Users see your app's name on GitHub's consent screen.
>
> > [accordion] Set up your own GitHub OAuth app
> >
> > See [GitHub OAuth documentation](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app) and [register an app](https://github.com/settings/applications/new).
> >
> > **1. Register an OAuth app**
> >
> > Go to **Settings** > **Developer settings** > **OAuth apps** and click [**New OAuth App**](https://github.com/settings/applications/new).
> >
> > **2. Describe the app**
> >
> > Enter an **Application name** and a **Homepage URL**. Use an organization's **Developer settings** for an org-owned app.
> >
> > **3. Add the redirect URI**
> >
> > Paste Formal's **Redirect URI** into **Authorization callback URL**, then click **Register application**.
> >
> > **4. Review scopes**
> >
> > OAuth apps have no scope settings. Scopes like `repo` and `read:org` are requested at sign-in.
> >
> > **5. Copy the credentials**
> >
> > Copy the **Client ID**. Click **Generate a new client secret** and copy the **Client secret**.
> >
> > **6. Enter the app in Formal**
> >
> > > [tab] UI
> > >
> > > Back in the Admin Catalog, enter the **Client ID** and **Client secret**, then save.
> > > Users then link their accounts on the GitHub server's page in the [Employee Catalog](https://catalog.formal.ai).
> > >
> > > ![Your OAuth App selected in the Admin Catalog, with the Redirect URI, Client ID and Client secret fields](https://mintcdn.com/formal/R-3CwVgs8IU6liji/assets/images/mcp-servers/your-oauth-app.png?fit=max\&auto=format\&n=R-3CwVgs8IU6liji\&q=85\&s=00584678f4f2696805678de060eaea0a)
> >
> > > [tab] Terraform
> > >
> > > Use these OAuth settings instead of the ones from step 1.
> > >
> > > ```hcl theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
> > > variable "github_oauth_client_secret" {
> > >   type      = string
> > >   sensitive = true
> > > }
> > >
> > > resource "formal_resource_oauth_settings" "github" {
> > >   resource_id              = formal_resource.github.id
> > >   upstream_oauth_mode      = "pre_registered"
> > >   client_id                = "<YOUR_CLIENT_ID>"
> > >   client_secret_wo         = var.github_oauth_client_secret
> > >   client_secret_wo_version = 1
> > > }
> > > ```
> >
> > > [tab] Pulumi
> > >
> > > Use these OAuth settings instead of the ones from step 1.
> > > Store the secret with `pulumi config set --secret githubOauthClientSecret`.
> > >
> > > ```ts theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
> > > import * as pulumi from "@pulumi/pulumi";
> > >
> > > const config = new pulumi.Config();
> > >
> > > new formal.ResourceOauthSettings("mcpOAuth", {
> > >   resourceId: resource.id,
> > >   upstreamOauthMode: "pre_registered",
> > >   clientId: "<YOUR_CLIENT_ID>",
> > >   clientSecret: config.requireSecret("githubOauthClientSecret"),
> > > });
> > > ```
> >
> > > [tab] API
> > >
> > > Use this call instead of the `CreateResourceOAuthSettings` call from step 1.
> > > The OAuth app is fixed at creation.
> > >
> > > ```bash theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
> > > curl --fail --silent --show-error \
> > >   --request POST \
> > >   https://api.formal.ai/core.v1.ResourceService/CreateResourceOAuthSettings \
> > >   --header "x-api-key: ${FORMAL_API_KEY}" \
> > >   --header "Content-Type: application/json" \
> > >   --data-binary @- <<JSON
> > >   {
> > >     "resourceId": "${RESOURCE_ID}",
> > >     "upstreamOauthMode": "pre_registered",
> > >     "clientId": "<YOUR_CLIENT_ID>",
> > >     "clientSecret": "<YOUR_CLIENT_SECRET>"
> > >   }
> > > JSON
> > > ```
> >
> > > [!WARNING]
> > >
> > > Switching between Formal's App and your OAuth app unlinks every account. Users must link again.

> [tab] Shared Credentials
>
> An administrator stores one GitHub token in Formal, and the Connector sends it on every user's requests without showing it to them.
>
> ![Shared Credentials selected under How does Formal authenticate to this resource?](https://mintcdn.com/formal/R-3CwVgs8IU6liji/assets/images/mcp-servers/auth-shared-credentials.png?fit=max\&auto=format\&n=R-3CwVgs8IU6liji\&q=85\&s=91032e202518088292ee1cf39451b9ef)
>
> GitHub's MCP endpoint accepts a [personal access token](https://github.com/github/github-mcp-server/blob/main/README.md) as a Bearer token.
> Create one at [github.com/settings/personal-access-tokens/new](https://github.com/settings/personal-access-tokens/new).
> Grant only the repositories and permissions the agents need; use read-only permissions for read-only use.
>
> 1. Open this server from the [Admin Catalog](https://app.formal.ai/catalog).
> 2. Select **Configuration**, then **Authentication**.
> 3. Choose **Shared Credentials**, then select **Add credential**.
> 4. Enter the credential, make it the default, and select **Save authentication**.
>
> See [Native Users](https://docs.formal.ai/docs/guides/core-concepts/resources/native-users.md).
>
> For HTTP Bearer authentication, enter the raw token. The Connector adds the `Bearer ` prefix.
>
> In the Terraform tab, set `native_users_v3_enabled = true` on the Resource and `upstream_oauth_mode = "none"` on its OAuth settings.
>
> ```hcl theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
> variable "mcp_shared_credential" {
>   type      = string
>   sensitive = true
> }
>
> resource "formal_native_user_v3" "mcp_shared_credential" {
>   resource_id = formal_resource.github.id
>   label       = "shared-credential"
>
>   http_bearer {
>     header = "Authorization"
>     token {
>       literal_wo         = var.mcp_shared_credential
>       literal_wo_version = 1
>     }
>   }
> }
>
> resource "formal_resource_native_user_selection" "mcp_shared_credential" {
>   resource_id = formal_resource.github.id
>   cel         = "\"${formal_native_user_v3.mcp_shared_credential.id}\""
> }
> ```

> [tab] Pass Through
>
> Each user sends their own GitHub sign-in or token from their MCP client, and Formal forwards it unchanged without storing it.
>
> ![Pass Through selected under How does Formal authenticate to this resource?](https://mintcdn.com/formal/R-3CwVgs8IU6liji/assets/images/mcp-servers/auth-pass-through.png?fit=max\&auto=format\&n=R-3CwVgs8IU6liji\&q=85\&s=c14ae61de2dba8b567626cba03aae2cf)
>
> Nothing in the request identifies the Formal user. Formal attributes these requests only when they come through the [Formal Endpoint](https://docs.formal.ai/docs/guides/core-concepts/connectors/mcp-gateway.md#identity-headers). Pass-through servers have no Connector address, so cloud agents can't use them.
>
> For OAuth, each MCP client signs in to GitHub itself.

## 3. Route Endpoint traffic

Auth is managed at the Connector level when you use Linked Accounts or Shared Credentials, so the Endpoint needs to forward the MCP traffic to it.
Laptops and VMs with the Formal Endpoint keep using `https://api.githubcopilot.com/mcp/`, and a network rule sends that traffic to the Connector.

> [tab] UI
>
> 1. Go to [Network Rules](https://app.formal.ai/network-rules) and create a rule named `github-mcp`.
> 2. Paste this expression, set the rule to **Active**, then save:
>
> ```javascript theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
> {
>   "condition": {
>     "pre_tcp": process.is_agent,
>     // Every MCP server added to Formal: process.is_agent && resource.technology == "mcp"
>     "pre_tls": process.is_agent && hostname == "api.githubcopilot.com",
>     "post_tls": true
>   },
>   "outputs": {
>     "forward_to_connector": true
>   }
> }
> ```

> [tab] Terraform
>
> ```hcl theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
> resource "formal_network_rule" "github_mcp" {
>   name        = "github-mcp"
>   description = "Forward GitHub MCP traffic from agents to the Connector"
>   status      = "active"
>
>   # Every MCP server added to Formal: set pre_tls to process.is_agent && resource.technology == "mcp"
>   cel_expression = <<-EOT
>     {
>       "condition": {
>         "pre_tcp": process.is_agent,
>         "pre_tls": process.is_agent && hostname == "api.githubcopilot.com",
>         "post_tls": true
>       },
>       "outputs": {
>         "forward_to_connector": true
>       }
>     }
>   EOT
> }
> ```

> [tab] Pulumi
>
> ```ts theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
> import * as formal from "@formalco/pulumi";
>
> new formal.NetworkRule("githubMcp", {
>   name: "github-mcp",
>   description: "Forward GitHub MCP traffic from agents to the Connector",
>   status: "active",
>   // Every MCP server added to Formal: set pre_tls to process.is_agent && resource.technology == "mcp"
>   celExpression: `
>     {
>       "condition": {
>         "pre_tcp": process.is_agent,
>         "pre_tls": process.is_agent && hostname == "api.githubcopilot.com",
>         "post_tls": true
>       },
>       "outputs": {
>         "forward_to_connector": true
>       }
>     }
>   `,
> });
> ```

> [tab] API
>
> ```bash theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
> export FORMAL_API_KEY="<YOUR_API_KEY>"
> # Every MCP server added to Formal: set pre_tls to process.is_agent && resource.technology == "mcp"
> CEL_EXPRESSION=$(cat <<'CEL'
> {
>   "condition": {
>     "pre_tcp": process.is_agent,
>     "pre_tls": process.is_agent && hostname == "api.githubcopilot.com",
>     "post_tls": true
>   },
>   "outputs": {
>     "forward_to_connector": true
>   }
> }
> CEL
> )
> jq -n --arg cel "${CEL_EXPRESSION}" '{
>   name: "github-mcp",
>   description: "Forward GitHub MCP traffic from agents to the Connector",
>   status: "active",
>   celExpression: $cel
> }' | curl --fail --silent --show-error \
>   --request POST \
>   https://api.formal.ai/core.v1.DesktopService/CreateDesktopRoutingRule \
>   --header "x-api-key: ${FORMAL_API_KEY}" \
>   --header "Content-Type: application/json" \
>   --data-binary @-
> ```

The rule matches [known agents](https://docs.formal.ai/docs/guides/network-rules.md#known-agent-identities) only.
See [Identity headers](https://docs.formal.ai/docs/guides/core-concepts/connectors/mcp-gateway.md#identity-headers).

Cloud agents such as Claude, ChatGPT and Cursor Cloud Agents don't run the Endpoint.
Point them at this server's Connector address, `https://github.<connector-hostname>/mcp`.
Find it in the [Admin Catalog](https://app.formal.ai/catalog): open GitHub, then copy **Connector address** on the **Configuration** tab.

## 4. Create policies

### Tools

The Admin Catalog lists these tools for GitHub:

| Tool | Description |
| - | - |
| `search_code` | Search code across accessible repositories. |
| `search_repositories` | Search GitHub repositories. |
| `search_issues` | Search issues and pull requests. |
| `get_file_contents` | Read a file or directory from a repository. |
| `create_or_update_file` | Create or update a repository file. |
| `push_files` | Commit multiple files to a repository. |
| `create_repository` | Create a GitHub repository. |
| `fork_repository` | Fork a repository. |
| `create_branch` | Create a branch from a repository ref. |
| `list_commits` | List commits for a branch or repository. |
| `get_commit` | Retrieve a commit and its changed files. |
| `list_issues` | List repository issues. |
| `get_issue` | Retrieve an issue. |
| `create_issue` | Create an issue. |
| `update_issue` | Update an issue. |
| `add_issue_comment` | Add a comment to an issue or pull request. |
| `list_pull_requests` | List repository pull requests. |
| `get_pull_request` | Retrieve pull request details. |
| `create_pull_request` | Create a pull request. |
| `update_pull_request` | Update a pull request. |
| `merge_pull_request` | Merge a pull request. |
| `get_pull_request_files` | List files changed by a pull request. |
| `get_pull_request_status` | Retrieve combined status checks for a pull request. |
| `get_pull_request_comments` | List review comments on a pull request. |
| `create_pull_request_review` | Submit a pull request review. |

### Policies examples

> [tab] Block a tool
>
> This policy blocks the tools that change data, so agents can only read GitHub.
>
> ```rego theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
> package formal.v2
>
> import future.keywords.if
> import future.keywords.in
>
> write_tools := {
>   "create_or_update_file",
>   "push_files",
>   "create_repository",
>   "fork_repository",
>   "create_branch",
>   "create_issue",
>   "update_issue",
>   "add_issue_comment",
>   "create_pull_request",
>   "update_pull_request",
>   "merge_pull_request",
>   "create_pull_request_review",
> }
>
> request := {
>   "action": "block",
>   "type": "block_with_custom_message",
>   "message": "Writing to GitHub is not allowed.",
> } if {
>   input.resource.name == "github"
>   input.mcp.method == "tools/call"
>   input.mcp.tool_name in write_tools
> }
> ```

> [tab] Mask a tool's output
>
> This policy replaces email addresses in results returned by `list_commits` and `get_commit`.
>
> ```rego theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
> package formal.v2
>
> import future.keywords.if
> import future.keywords.in
>
> email_pattern := `[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}`
>
> response := {
>   "action": "rewrite",
>   "body": regex.replace(input.http.body, email_pattern, "[REDACTED-EMAIL]"),
>   "reason": "Mask emails in GitHub results",
> } if {
>   input.resource.name == "github"
>   input.mcp.tool_name in {"list_commits", "get_commit"}
>   regex.match(email_pattern, input.http.body)
> }
> ```

See [MCP resources](https://docs.formal.ai/docs/guides/policies/evaluation.md#mcp-resources) for the `input.mcp` fields and [Rewrite Action](https://docs.formal.ai/docs/guides/policies/enforcement.md#rewrite-action) for body rewrites.

## 5. Check the logs

Open [Logs](https://app.formal.ai/logs) and search for `resource.name:github`.
Each GitHub tool call is one `request` log with the user, the MCP client and the tool.
Open a log to see its tool parameters and the policies it triggered.
A blocked write shows the block policy and its message.
A masked search shows the rewrite policy.

GitHub tool calls in Logs, including a masked response and a blocked write

See [Logs](https://docs.formal.ai/docs/guides/observability/logs.md) for search syntax and filters.

## Troubleshooting

> [accordion] The agent says Formal has no current sign-in
>
> The agent relays: `Formal has no current github sign-in for you. Link or re-link your github account in Formal…`
>
> **Cause:** The user never linked GitHub, or the token expired and couldn't be refreshed.
>
> **Fix:** Open the server in the [Employee Catalog](https://catalog.formal.ai) and link GitHub again.

> [accordion] The agent says GitHub rejected your credentials
>
> The agent relays: `github rejected your credentials. Link or re-link your github account in Formal…`
>
> **Cause:** GitHub no longer accepts the linked token, for example after the user revoked access in GitHub.
>
> **Fix:** Link GitHub again from the [Employee Catalog](https://catalog.formal.ai).
>
> With Shared Credentials, the message asks the user to contact an administrator. Replace the token in the server's **Shared Credentials**.

> [accordion] Laptop traffic doesn't appear in Logs
>
> **Cause:** The Endpoint isn't running, or the network rule doesn't match the client.
>
> **Fix:** Check the Endpoint is connected. The rule above only matches [known agents](https://docs.formal.ai/docs/guides/network-rules.md#known-agent-identities). Other clients, such as a browser, reach GitHub directly unless the rule also matches them. See [Network rules](https://docs.formal.ai/docs/guides/network-rules.md).

> [accordion] MCP clients can't connect directly yet
>
> **Cause:** The **Configuration** tab lists the reason, most often `No Connector is in this server's Space.`
>
> **Fix:** Edit the server and choose the Space containing its Connector.

## Next Steps

> [card] [MCP Gateway](https://docs.formal.ai/docs/guides/core-concepts/connectors/mcp-gateway.md)
>
> Configure Connector routing and downstream access

> [card] [All MCP servers](https://docs.formal.ai/docs/guides/core-concepts/connectors/mcp-servers/index.md)
>
> Browse every MCP server template


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.