mcp-multiplexer 0.3.1

One MCP server fronting many: 7 meta-tools instead of every tool schema in context
Documentation
# OAuth

mcp-multiplexer brokers OAuth 2.1 for remote MCP servers so the model (and
you) never touches tokens. Set `"oauth": true` on a `url` server and the first
use walks you through a browser login; after that, tokens refresh themselves.

Standards implemented (via [rmcp](https://crates.io/crates/rmcp)): OAuth 2.1
with PKCE (S256), authorization-server metadata discovery (RFC 8414),
protected-resource metadata (RFC 9728), dynamic client registration
(RFC 7591), and resource indicators (RFC 8707).

## Quick start

```json
{
  "mcpServers": {
    "gitlab": {
      "url": "https://gitlab.com/api/v4/mcp",
      "oauth": true
    }
  }
}
```

1. Call any tool (or `authorize_server`) — you get an authorization URL.
2. Open it, log in, approve. A temporary `127.0.0.1` listener catches the
   redirect and completes the exchange automatically.
3. Retry the call. Done — no re-login on restart, tokens auto-refresh.

The model can drive this itself: the `authorize_server` meta-tool returns the
URL with instructions, and its result tells the model what to do next.

## How it works

```
1. call_tool(gitlab, …)
2. mux: no token → discover provider metadata
   (WWW-Authenticate → .well-known/oauth-protected-resource
    → .well-known/oauth-authorization-server)
3. mux: register a client dynamically (skipped if you set oauth_client_id)
4. mux → you: authorization URL (scope auto-discovered, PKCE challenge,
   resource indicator)
5. browser: you approve → redirect to http://127.0.0.1:<port>/callback
6. mux: code exchange → tokens.json → retry succeeds
```

Scope is discovered from the provider's protected-resource metadata — you
normally don't set `oauth_scopes` at all.

## Configuration reference

| Key | Type | Default | Purpose |
|---|---|---|---|
| `oauth` | bool | `false` | Enable OAuth for this `url` server |
| `oauth_client_id` | string | — | Pre-registered client ID. Skips dynamic registration — needed for providers that don't support RFC 7591 |
| `oauth_scopes` | string[] | auto | Override scopes when auto-discovery picks wrong ones or the provider has no metadata |
| `oauth_redirect_port` | number | ephemeral | Fix the callback port. Needed when your pre-registered OAuth app requires an exact redirect URI (`http://127.0.0.1:<port>/callback`) |

Static `headers` and OAuth can coexist; the OAuth Bearer token wins on the
`Authorization` header.

## Headless (SSH, Docker, no local browser)

The callback listener can't catch a redirect on a machine without a browser,
so paste instead:

1. `authorize_server` (no `pasted_url`) → open the URL **anywhere**.
2. Approve. Your browser tries to load `http://127.0.0.1:…/callback?code=…`
   and fails — that's fine.
3. Copy that final URL from the address bar and call
   `authorize_server` with `pasted_url` set to it.

## Token storage

- Location: `~/.cache/mcp-multiplexer/tokens.json` (`$XDG_CACHE_HOME` aware),
  mode `0600`, one entry per server.
- Contains access + refresh tokens and the dynamically-registered client ID
  (reused across restarts so providers don't accumulate clients; if a provider
  forgets the registration, mux discards it and re-registers automatically).
- Refresh is automatic and transparent; a revoked refresh token simply
  triggers the authorization flow again on next use.
- **Log out:** delete the server's entry from `tokens.json`. Also revoke the
  grant at the provider if you want it fully dead.

## Provider notes

### GitLab (`https://gitlab.com/api/v4/mcp`)

Validated end-to-end against gitlab.com (2026-09, GitLab 19.4): dynamic
registration, scope discovery, refresh all work.

- **Available on all tiers including Free**, but an **owner must enable it per
  top-level group**: Group → **Settings → General → Access and permissions** →
  "Allow access to the MCP server". Your personal namespace does not count —
  it must be a group. If you have none, create a free one.
- Symptom when not enabled: `HTTP 404` on the MCP endpoint *after* successful
  authorization (confusingly, the OAuth flow itself works fine).

## Troubleshooting

| Symptom | Cause | Fix |
|---|---|---|
| `Auth required, when send initialize request` | server needs OAuth but `oauth` isn't set — common when migrating from a client (e.g. Claude Code) that had its own cached OAuth tokens | set `"oauth": true` and authorize |
| `HTTP 404` after authorizing (GitLab) | MCP not enabled on any top-level group | see Provider notes above |
| `invalid URL, scheme is not http` | build without TLS (bug in v0.1.0) | upgrade to ≥ v0.1.1 |
| Provider rejects the redirect URI | pre-registered app requires an exact URI | set `oauth_redirect_port` and register `http://127.0.0.1:<port>/callback` |
| `oauth init … metadata discovery failed` | provider has no RFC 8414/9728 metadata, or wrong URL | check the `url`; if the provider is non-standard, file an issue |
| Dynamic registration refused | provider doesn't support RFC 7591 | create an OAuth app manually, set `oauth_client_id` (+ `oauth_redirect_port`) |
| `Authorization server issuer mismatch` | provider delegates auth to a different domain than the MCP host (declared `issuer` lives on another domain) | upgrade mux — current versions trust the issuer the discovery document declares |
| `invalid_client: Invalid client_id` on refresh | provider forgot the dynamic registration (non-persistent DCR) | nothing — mux clears the stale registration, re-registers, and asks you to re-authorize once |
| Redirect-URI mismatch naming an unknown client ID | provider fronts a fixed first-party OAuth app that forbids loopback redirects (hosted first-party connectors do this) | not mux-fixable — use the vendor's own connector |
| Constantly asked to re-authorize | refresh token revoked/expired | expected: re-approve once; check provider's token TTL settings if it recurs |
| Want to force re-login | — | delete the server's entry in `tokens.json` and retry |