diff --git a/docs/mcp/cli-reference.md b/docs/mcp/cli-reference.md index 0b3e1c5..389d6c7 100644 --- a/docs/mcp/cli-reference.md +++ b/docs/mcp/cli-reference.md @@ -30,6 +30,12 @@ Connects to one server, runs `initialize` + `tools/list`, and prints the discovered tools with truncated input schemas. Exits non-zero on any failure (useful in CI). +For an `oauth` server this reads the token stored by `forge mcp login`, +honoring the same `mcp.token_store_path` (forge.yaml) / `MCP_TOKEN_STORE_PATH` +(env) override as `login`/`logout` — so a server logged in under a custom +store path resolves correctly. Run `forge mcp login ` first; +`test` does not perform interactive login. + Flags: - `--call ` — also invoke this tool after listing. - `--args ''` — JSON arguments for `--call`. Default `{}`. @@ -46,12 +52,23 @@ forge mcp test linear --call list_issues --args '{"first":5}' Runs the OAuth 2.1 PKCE flow against the named server. Opens a `127.0.0.1` loopback listener on a random port, opens the operator's -browser at the configured `authorize_url`, exchanges the returned -code for tokens, and persists them encrypted at +browser at the authorization endpoint, exchanges the returned code for +tokens, and persists them encrypted at `~/.forge/credentials/mcp_.json`. +**Discovery (#316):** when the server's `auth` block omits +`client_id` / `authorize_url` / `token_url`, login **discovers** them +from the server `url` (RFC 9728 → RFC 8414) and **dynamically registers** +a client (RFC 7591). The discovered endpoints + minted `client_id` are +persisted alongside the token as `mcp_reg_.json` and reused on +refresh — login never re-registers. Explicit config in `forge.yaml` +overrides discovery. See +[configuration.md](configuration.md#oauth-discovery--dynamic-client-registration-316). + Requires `auth.type: oauth` in the server's config. Fails fast for -bearer / static / no-auth servers. +bearer / static / no-auth servers, and fails closed with a clear message +if a server advertises no metadata / no registration endpoint and no +`client_id` is configured. For Kubernetes deployments: @@ -59,9 +76,12 @@ For Kubernetes deployments: # On your laptop: forge mcp login linear -# Bundle the credentials file into a Secret: +# Bundle the credentials into a Secret. For a discovery-based server, +# include the registration record too (mcp_reg_.json) — the +# runtime refresh path needs the discovered token_url + client_id: kubectl create secret generic mcp-tokens \ - --from-file=mcp_linear.json=$HOME/.forge/credentials/mcp_linear.json + --from-file=mcp_linear.json=$HOME/.forge/credentials/mcp_linear.json \ + --from-file=mcp_reg_linear.json=$HOME/.forge/credentials/mcp_reg_linear.json # In the pod spec: volumeMounts: @@ -75,7 +95,11 @@ env: ## `forge mcp logout ` -Deletes the stored OAuth token for the server. Idempotent. +Deletes the stored OAuth token **and** the discovery/registration record +(`mcp_reg_.json`) for the server. Idempotent. Clearing the +registration is the recovery path when an authorization server revokes +the dynamically-registered client: the next `forge mcp login` re-discovers +and re-registers from scratch. ```sh forge mcp logout linear diff --git a/docs/mcp/troubleshooting.md b/docs/mcp/troubleshooting.md index 808703e..8c0332b 100644 --- a/docs/mcp/troubleshooting.md +++ b/docs/mcp/troubleshooting.md @@ -23,6 +23,11 @@ rows. | `forge mcp login` opens browser but `state mismatch` after consent | Stale callback or malicious redirect attempt | Re-run; if persistent, check for browser extensions injecting URLs | | At runtime: `mcp: oauth token revoked` | Refresh token denied — usually expired or user revoked | `forge mcp login ` again; re-bundle the Secret | | At runtime: `mcp: no stored token for "" — run 'forge mcp login ...'` | Token store empty inside the pod | Mount the Secret correctly; set `HOME` so the store path resolves | +| `forge mcp test` reports `no stored token` right after a successful login | `test` read a different store than `login` wrote | Set the same `mcp.token_store_path` (forge.yaml) or `MCP_TOKEN_STORE_PATH` for both — `test` now honors it (fixed) | +| `could not discover an authorization server` at login | Server has no RFC 9728 metadata / wrong endpoint | Use the Streamable HTTP `/mcp` URL (not `/sse`); or set `authorize_url`/`token_url`/`client_id` explicitly | +| `advertises no registration_endpoint and no client_id` at login | Server doesn't support RFC 7591 dynamic registration | Configure `client_id` (+ `authorize_url`/`token_url`) explicitly for that server | +| `issued a confidential client (client_secret)` at login | Server returned a secret; Forge is public-PKCE only | Configure `client_id`/`authorize_url`/`token_url` explicitly (Forge won't store a client secret) | +| At runtime: `mcp: oauth token revoked` after the AS revoked the DCR client | Persisted registration is stale | `forge mcp logout ` (clears the registration record) then `forge mcp login ` to re-discover + re-register | ## Tool name conflicts @@ -37,6 +42,7 @@ rows. |------------------------------------------------------------------------------|---------------------------------------------------------|----------------------------------------------------------------| | Locally the agent works; in K8s it fails with `transport unavailable` | NetworkPolicy egress doesn't include the MCP server | Rebuild — `egress_stage.go` merges `MCPDomains` automatically; re-apply manifests | | OAuth refresh fails with `transport: dial` from the pod | `token_url` host blocked by NetworkPolicy | The build merges this too — verify the latest `egress_allowlist.json` | +| Discovery-based server: refresh blocked because the auth-server host isn't allowlisted | The build-time freeze can't know a *discovered* host | The runtime learns it from the login-time `mcp_reg_.json` and merges it — ensure that file is mounted alongside the token (see [cli-reference](cli-reference.md#forge-mcp-login-name)) | ## Runtime behavior