|
| 1 | +# Local Server OAuth Login (stdio) |
| 2 | + |
| 3 | +The local (stdio) GitHub MCP Server can log you in with OAuth instead of a |
| 4 | +Personal Access Token (PAT). On first use it walks you through GitHub's |
| 5 | +authorization flow in your browser and keeps the resulting token **in memory |
| 6 | +only** — nothing is written to disk. |
| 7 | + |
| 8 | +Official released binaries and the `ghcr.io/github/github-mcp-server` image ship |
| 9 | +with a registered GitHub OAuth application baked in, so on **github.com** you can |
| 10 | +start the server with no token and no client ID at all. To target a different |
| 11 | +host (GitHub Enterprise Server or `ghe.com`), or to use your own application, |
| 12 | +pass `--oauth-client-id` (see [Bring your own app](#bring-your-own-app)). |
| 13 | + |
| 14 | +> OAuth login applies to the **stdio** server only. The remote server and the |
| 15 | +> `http` command have their own authentication; see |
| 16 | +> [Remote Server](remote-server.md). |
| 17 | +
|
| 18 | +## Contents |
| 19 | + |
| 20 | +- [How it works](#how-it-works) |
| 21 | +- [Quick start](#quick-start) |
| 22 | +- [Configuration reference](#configuration-reference) |
| 23 | +- [Scope filtering](#scope-filtering) |
| 24 | +- [Running in Docker](#running-in-docker) |
| 25 | +- [Headless and device-code fallback](#headless-and-device-code-fallback) |
| 26 | +- [URL elicitation and the security advisory](#url-elicitation-and-the-security-advisory) |
| 27 | +- [Bring your own app](#bring-your-own-app) |
| 28 | +- [GitHub Enterprise Server and ghe.com](#github-enterprise-server-and-ghecom) |
| 29 | +- [Building from source with baked-in credentials](#building-from-source-with-baked-in-credentials) |
| 30 | + |
| 31 | +## How it works |
| 32 | + |
| 33 | +The server prefers the **authorization code flow with PKCE**: it starts a |
| 34 | +loopback callback server on your machine, opens GitHub's authorization page, and |
| 35 | +exchanges the returned code for a token. PKCE means the client secret is not |
| 36 | +required to complete the exchange, which is why a public, distributed client can |
| 37 | +ship without a confidential secret. |
| 38 | + |
| 39 | +To present the authorization URL, the server uses the most secure channel your |
| 40 | +MCP client offers, in order: |
| 41 | + |
| 42 | +1. **Open your browser automatically** (native runs). |
| 43 | +2. **URL elicitation** — the client prompts you with the link out of band, so the |
| 44 | + URL never enters the model's context. Requires a client that supports MCP |
| 45 | + elicitation (e.g. VS Code 1.101+). |
| 46 | +3. **A message in the first tool response** — a last resort for clients without |
| 47 | + elicitation. This includes a [security advisory](#url-elicitation-and-the-security-advisory). |
| 48 | + |
| 49 | +If the authorization-code flow can't be used — for example, a container with no |
| 50 | +published callback port — the server falls back to the |
| 51 | +[device-code flow](#headless-and-device-code-fallback). |
| 52 | + |
| 53 | +GitHub App tokens that expire are refreshed transparently using the refresh |
| 54 | +token, so long-running sessions keep working without re-authorizing. |
| 55 | + |
| 56 | +## Quick start |
| 57 | + |
| 58 | +**Native binary (recommended).** Best experience: a random loopback port is |
| 59 | +used and your browser opens automatically. On github.com with an official build, |
| 60 | +no flags are needed: |
| 61 | + |
| 62 | +```bash |
| 63 | +github-mcp-server stdio |
| 64 | +``` |
| 65 | + |
| 66 | +With your own application: |
| 67 | + |
| 68 | +```bash |
| 69 | +github-mcp-server stdio --oauth-client-id <YOUR_CLIENT_ID> |
| 70 | +``` |
| 71 | + |
| 72 | +VS Code (`.vscode/mcp.json`), using your own app: |
| 73 | + |
| 74 | +```json |
| 75 | +{ |
| 76 | + "servers": { |
| 77 | + "github": { |
| 78 | + "command": "/path/to/github-mcp-server", |
| 79 | + "args": ["stdio", "--oauth-client-id", "<YOUR_CLIENT_ID>"] |
| 80 | + } |
| 81 | + } |
| 82 | +} |
| 83 | +``` |
| 84 | + |
| 85 | +For Docker, see [Running in Docker](#running-in-docker) — containers need a fixed |
| 86 | +callback port. |
| 87 | + |
| 88 | +## Configuration reference |
| 89 | + |
| 90 | +OAuth login is configured with these stdio flags (each has an environment |
| 91 | +variable equivalent). Flags apply only to the `stdio` command. |
| 92 | + |
| 93 | +| Flag | Environment variable | Description | |
| 94 | +|------|----------------------|-------------| |
| 95 | +| `--oauth-client-id` | `GITHUB_OAUTH_CLIENT_ID` | OAuth App or GitHub App client ID. Enables OAuth login when no token is set. Defaults to the baked-in app on github.com for official builds. | |
| 96 | +| `--oauth-client-secret` | `GITHUB_OAUTH_CLIENT_SECRET` | Client secret, **if your app requires one**. For distributed clients this is a public, non-confidential credential. | |
| 97 | +| `--oauth-scopes` | `GITHUB_OAUTH_SCOPES` | Comma-separated scopes to request. Also [filters tools](#scope-filtering) to those scopes. Defaults to the full supported set. | |
| 98 | +| `--oauth-callback-port` | `GITHUB_OAUTH_CALLBACK_PORT` | Fixed local port for the callback server. Defaults to a random port; set a fixed port when mapping it through Docker. | |
| 99 | + |
| 100 | +A static token still takes precedence: if `GITHUB_PERSONAL_ACCESS_TOKEN` is set, |
| 101 | +the server uses it and skips OAuth entirely. |
| 102 | + |
| 103 | +## Scope filtering |
| 104 | + |
| 105 | +The scopes you request determine which tools are exposed. Requesting the full |
| 106 | +supported set (the default) hides no tools. Narrowing `--oauth-scopes` both |
| 107 | +narrows the token's grant **and** filters out tools that would need a scope you |
| 108 | +didn't request, so the tool list reflects what the token can actually do. |
| 109 | + |
| 110 | +For example, requesting only `repo,read:org` hides tools that require `gist`, |
| 111 | +`workflow`, `notifications`, and so on. |
| 112 | + |
| 113 | +## Running in Docker |
| 114 | + |
| 115 | +A container can't reach a random loopback port on your host, so Docker OAuth |
| 116 | +needs a **fixed** callback port that you publish into the container. Use port |
| 117 | +**8085** to match the official app's registered callback URL. |
| 118 | + |
| 119 | +```bash |
| 120 | +docker run -i --rm \ |
| 121 | + -p 127.0.0.1:8085:8085 \ |
| 122 | + -e GITHUB_OAUTH_CALLBACK_PORT=8085 \ |
| 123 | + ghcr.io/github/github-mcp-server |
| 124 | +``` |
| 125 | + |
| 126 | +VS Code (`.vscode/mcp.json`): |
| 127 | + |
| 128 | +```json |
| 129 | +{ |
| 130 | + "servers": { |
| 131 | + "github": { |
| 132 | + "command": "docker", |
| 133 | + "args": [ |
| 134 | + "run", "-i", "--rm", |
| 135 | + "-p", "127.0.0.1:8085:8085", |
| 136 | + "-e", "GITHUB_OAUTH_CALLBACK_PORT", |
| 137 | + "ghcr.io/github/github-mcp-server" |
| 138 | + ], |
| 139 | + "env": { "GITHUB_OAUTH_CALLBACK_PORT": "8085" } |
| 140 | + } |
| 141 | + } |
| 142 | +} |
| 143 | +``` |
| 144 | + |
| 145 | +Because the container can't open your host browser, the authorization URL |
| 146 | +arrives via [URL elicitation](#url-elicitation-and-the-security-advisory) or the |
| 147 | +tool-response message. After you authorize, your browser hits |
| 148 | +`localhost:8085`, which Docker forwards into the container's callback. |
| 149 | + |
| 150 | +If you bring your own app for Docker, register its callback URL as exactly |
| 151 | +`http://localhost:8085/callback`. |
| 152 | + |
| 153 | +> **Two safety properties to be aware of with a fixed port:** |
| 154 | +> |
| 155 | +> - **Publish to loopback only** (`-p 127.0.0.1:8085:8085`, not `-p 8085:8085`). |
| 156 | +> Inside a container the callback necessarily listens on all interfaces, so a |
| 157 | +> plain publish would expose the authorization code to your network. The |
| 158 | +> server logs a warning reminding you of this when it binds inside a container. |
| 159 | +> - **A busy port is fatal, by design.** With a fixed port, if the server can't |
| 160 | +> bind it (another process already holds it), it **stops with an error** rather |
| 161 | +> than silently falling back to the device flow. A port you didn't get could |
| 162 | +> belong to another user's process positioned to receive the redirect, so the |
| 163 | +> server refuses to continue. Free the port or choose a different |
| 164 | +> `--oauth-callback-port`. |
| 165 | +
|
| 166 | +## Headless and device-code fallback |
| 167 | + |
| 168 | +When there's no usable browser or callback — a remote shell, CI, or a container |
| 169 | +started without a published port — the server uses GitHub's **device-code |
| 170 | +flow**. You'll get a short code and a verification URL to open on any device: |
| 171 | + |
| 172 | +``` |
| 173 | +Visit https://github.com/login/device and enter the code WDJB-MJHT to authorize |
| 174 | +the GitHub MCP Server. |
| 175 | +``` |
| 176 | + |
| 177 | +The server polls GitHub until you finish authorizing, then continues. No |
| 178 | +callback port is involved, so this works anywhere. |
| 179 | + |
| 180 | +## URL elicitation and the security advisory |
| 181 | + |
| 182 | +URL elicitation lets your MCP client present the authorization URL to you |
| 183 | +directly, keeping it **out of the model's context** — the model never sees the |
| 184 | +link or any code embedded in it. This is the most secure way to hand off the |
| 185 | +authorization step. |
| 186 | + |
| 187 | +If your client doesn't support elicitation, the server falls back to placing the |
| 188 | +URL in a tool response and appends a short advisory: |
| 189 | + |
| 190 | +> Note: your MCP client does not appear to support secure URL elicitation. For |
| 191 | +> improved security, consider asking your agent, CLI, or IDE to add it (for |
| 192 | +> example, by opening an issue). |
| 193 | +
|
| 194 | +If you see this, your authorization still works — but consider asking your client |
| 195 | +vendor to add elicitation support. |
| 196 | + |
| 197 | +## Bring your own app |
| 198 | + |
| 199 | +You need your own application when targeting a non-github.com host, or when you'd |
| 200 | +rather not use the baked-in app. Either application type works: |
| 201 | + |
| 202 | +- **[Create an OAuth App](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app)** — |
| 203 | + simplest to set up. Grants the scopes you request. |
| 204 | +- **[Register a GitHub App](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app)** — |
| 205 | + finer-grained, per-resource permissions and short-lived tokens that refresh |
| 206 | + automatically. Enable **Device Flow** in the app settings if you want the |
| 207 | + [headless fallback](#headless-and-device-code-fallback). |
| 208 | + |
| 209 | +When registering, set the authorization callback URL: |
| 210 | + |
| 211 | +- **Native runs** use a random loopback port. For loopback redirects GitHub does |
| 212 | + not require the callback port to match, so registering |
| 213 | + `http://localhost/callback` is sufficient. |
| 214 | +- **Docker / fixed port** must match exactly: register |
| 215 | + `http://localhost:8085/callback` (or whichever port you publish). |
| 216 | + |
| 217 | +Then pass the client ID (and secret, only if your app requires one): |
| 218 | + |
| 219 | +```bash |
| 220 | +github-mcp-server stdio \ |
| 221 | + --oauth-client-id <YOUR_CLIENT_ID> \ |
| 222 | + --oauth-client-secret <YOUR_CLIENT_SECRET> |
| 223 | +``` |
| 224 | + |
| 225 | +## GitHub Enterprise Server and ghe.com |
| 226 | + |
| 227 | +The baked-in app is registered on github.com only, so it is **not** used when you |
| 228 | +set a custom host. GitHub Enterprise Server and `ghe.com` (Enterprise Cloud with |
| 229 | +data residency) users must **bring their own app** registered on that host and |
| 230 | +pass `--oauth-client-id`. |
| 231 | + |
| 232 | +Set the host with `--gh-host` / `GITHUB_HOST`; the server derives the OAuth |
| 233 | +authorization, token, and device endpoints from it, so login is directed at your |
| 234 | +instance's authorization server rather than github.com: |
| 235 | + |
| 236 | +```bash |
| 237 | +github-mcp-server stdio \ |
| 238 | + --gh-host https://github.example.com \ |
| 239 | + --oauth-client-id <YOUR_CLIENT_ID> |
| 240 | +``` |
| 241 | + |
| 242 | +- For GitHub Enterprise Server, prefix the host with `https://`. |
| 243 | +- For `ghe.com`, use `https://YOURSUBDOMAIN.ghe.com`. |
| 244 | + |
| 245 | +Register the app's callback URL on the same host (e.g. |
| 246 | +`http://localhost/callback` for native runs, or `http://localhost:8085/callback` |
| 247 | +for Docker). |
| 248 | + |
| 249 | +## Building from source with baked-in credentials |
| 250 | + |
| 251 | +Official builds embed the default OAuth client via linker flags at build time, so |
| 252 | +they are not present in the source tree. To produce your own build with embedded |
| 253 | +credentials, set them with `-ldflags`: |
| 254 | + |
| 255 | +```bash |
| 256 | +go build -ldflags "\ |
| 257 | + -X github.com/github/github-mcp-server/internal/buildinfo.OAuthClientID=<CLIENT_ID> \ |
| 258 | + -X github.com/github/github-mcp-server/internal/buildinfo.OAuthClientSecret=<CLIENT_SECRET>" \ |
| 259 | + ./cmd/github-mcp-server |
| 260 | +``` |
| 261 | + |
| 262 | +Without these, a source build simply has no baked-in app and expects |
| 263 | +`--oauth-client-id` (or a PAT) at runtime. |
0 commit comments