Control agent access

How to give an AI agent read-only GitHub access

Set up GitHub's MCP server so an agent can read issues and pull requests but can't merge, push or close anything.

By
Dan Flanagan
Updated
October 14, 2026
Tested with
github-mcp-server 2.0.2 (Docker image), tool lists checked over stdio, October 2026
Reading time
4 min

New to MCP? What is an MCP server? covers the basics first.

There are three ways to limit what an agent can do on GitHub: the token, the server's read-only mode, and per-tool grants in a gateway. Most of the work is knowing which one fits when the agent needs a single write, like leaving a comment.

Why this matters, including what happens when issue text talks an agent into a merge, is in Your triage agent can also merge pull requests.

Prerequisites

  • A GitHub account with access to the repositories the agent should read.
  • Docker, to run the server locally. The steps below were checked against the ghcr.io/github/github-mcp-server image, version 2.0.2.
  • An MCP client such as Claude Code, Cursor or VS Code. Adding a server to each one is covered in How to add MCP servers to Claude Code and How to set up MCP servers in Cursor.

Step 1: Create a read-only token

Start with the credential. GitHub enforces it no matter which server or client you use.

Create a fine-grained personal access token:

  1. Go to Settings → Developer settings → Personal access tokens → Fine-grained tokens and click Generate new token.
  2. Give it a name that says which agent it is for, such as triage-agent-read, and an expiration.
  3. Under Repository access, choose Only select repositories and pick the repositories the agent should see. Nothing else.
  4. Under Repository permissions, set Contents, Issues and Pull requests to Read-only. Metadata is added automatically. Leave everything else at No access, and leave account permissions empty.
  5. Click Generate token and copy it. GitHub shows it once.

Make one token per agent. Then you can revoke one agent's token and the others keep working.

Step 2: Run the server in read-only mode

Read-only mode stops the server from offering write tools at all, so the agent never sees merge_pull_request in its tool list.

Locally, set GITHUB_READ_ONLY when you start the container:

bash
docker run -i --rm \
  -e GITHUB_PERSONAL_ACCESS_TOKEN=<your-read-only-token> \
  -e GITHUB_READ_ONLY=1 \
  ghcr.io/github/github-mcp-server

If you run the binary directly, the flag is --read-only. You can also cut the tool list down to the areas the agent needs with toolsets. For a triage agent that is issues and pull requests:

bash
docker run -i --rm \
  -e GITHUB_PERSONAL_ACCESS_TOKEN=<your-read-only-token> \
  -e GITHUB_READ_ONLY=1 \
  -e GITHUB_TOOLSETS=issues,pull_requests \
  ghcr.io/github/github-mcp-server

I started the image with the issues and pull_requests toolsets and asked for its tools, with and without read-only mode:

ModeTools offered
Normal20, including merge_pull_request, create_pull_request, issue_write and add_issue_comment
Read-only9: issue_read, list_issues, search_issues, list_pull_requests, pull_request_read, search_pull_requests, get_label, list_issue_types, list_issue_fields

If you use GitHub's hosted server instead of running your own, the same switch is a header. Add X-MCP-Readonly: true to the request, or put /readonly on the end of the URL. This is the shape for a client that takes a URL and headers:

json
{
  "type": "http",
  "url": "https://api.githubcopilot.com/mcp/",
  "headers": {
    "Authorization": "Bearer <your-read-only-token>",
    "X-MCP-Toolsets": "issues,pull_requests",
    "X-MCP-Readonly": "true"
  }
}

Step 3: Connect your client and check the tool list

Add the server to your client using the command from Step 2. In Claude Code, for example:

bash
claude mcp add github-read \
  -e GITHUB_PERSONAL_ACCESS_TOKEN=<your-read-only-token> \
  -- docker run -i --rm -e GITHUB_PERSONAL_ACCESS_TOKEN -e GITHUB_READ_ONLY=1 \
  -e GITHUB_TOOLSETS=issues,pull_requests ghcr.io/github/github-mcp-server

Start a new session, because clients read their servers once at startup. Then ask the agent what GitHub tools it has. You should see the nine read tools above and nothing that writes. Ask it to merge a pull request and it should tell you it has no tool for that.

For an agent that only reads, you're done.

Step 4: Decide what to do when the agent needs one write

Most real agents need one small write. A triage agent reads an issue and then leaves a comment or a label, and neither GitHub control handles that well:

  • Read-only mode removes the comment tool too. add_issue_comment is a write, so it is one of the eleven tools read-only mode drops.
  • The token cannot tell a comment from a close. Commenting needs Issues: Read and write, and that same permission lets the token edit and close issues.

The server has one more option: an explicit tool list. Start it with GITHUB_TOOLS (or --tools) and it offers exactly those tools and no others:

bash
docker run -i --rm \
  -e GITHUB_PERSONAL_ACCESS_TOKEN=<token-with-issues-write> \
  -e GITHUB_TOOLS=list_issues,issue_read,search_issues,add_issue_comment \
  ghcr.io/github/github-mcp-server

I checked this too: the server offers those four tools and nothing else. Read-only mode wins over the list, so combining them drops add_issue_comment again. The list is also fixed when the server starts and applies to every client connected to that process. If two agents need different tools, you run two copies of the server, each with its own token and list.

That works for one agent on one machine. With several agents, every combination is another process and another copy of the token.

Step 5: Put the server behind a gateway with per-tool grants

A gateway checks each call against who is making it, so one GitHub server can serve agents with different permissions. In Statio:

  1. Install GitHub from the catalog. It connects to GitHub's hosted MCP server, and you paste the token once under Credentials. It is stored encrypted and is never sent to the agent.
  2. Create the agent under Settings → Agents. It gets its own client ID and secret, separate from yours.
  3. On the server's permissions, grant the agent the tools it needs: list_issues, issue_read, search_issues and add_issue_comment. Nothing is granted by default, so every other tool stays closed to it.
  4. Connect the agent's client through Statio, as described in Connect a client.

The agent's tool list now shows only what it was granted. If it calls merge_pull_request anyway, the gateway refuses the call before it reaches GitHub, and the refusal is recorded in the audit log next to the calls that went through. Another agent can use the same server with a different set of grants, and you change a grant without restarting anything.

Keep the read-only token for agents that never write. The token limits what GitHub accepts, and the grants limit what each agent can ask for.

Conclusion

When each control is enough:

  • A read-only fine-grained token for any agent that only reads. GitHub enforces it whatever else goes wrong.
  • Read-only mode or a tool list on the server to keep write tools out of the agent's view, when one configuration serves everyone connected to that server.
  • Per-tool grants in a gateway when agents need different small writes on the same server, and you want a record of every call, including the refused ones.

Next tutorials: