Commit 986641e
* fix: [#791][#792] support bearer-auth remote MCP servers (Microsoft Fabric, etc.)
Three coupled fixes that together let altimate-code connect to MCP servers
gated by short-lived bearer tokens — most prominently Microsoft Fabric Core
MCP — without a local proxy or per-hour config edits.
Issues addressed:
- #791 (dynamic auth for remote MCP servers): static `headers` only.
- #792 (external MCP servers not auto-activated at session start): mostly
caused by OAuth dynamic-client-registration probes pre-empting valid
bearer headers and ending the connect in a non-`connected` status.
- A third defect surfaced during this work: the strict
`ListToolsResultSchema` from `@modelcontextprotocol/sdk` rejects the
`null` annotation hints that Fabric returns, causing `listTools()` to
throw and the connection to be marked failed even after a successful
initialize. None of these are altimate-specific — they all reproduce on
upstream `opencode`.
Changes:
1. `McpRemote.headersCommand: Record<string, string[]>` — header values
produced by running an argv command (executed via `execFile`, not a
shell, so values aren't subject to shell injection). Resolved on every
connect so expiring tokens refresh automatically. Example:
"headersCommand": {
"Authorization": ["sh", "-c",
"printf 'Bearer %s' \"$(az account get-access-token ... -o tsv)\""]
}
2. OAuth provider is no longer attached when the user supplies an
explicit `Authorization` header (statically or via `headersCommand`)
and `oauth` is not specified. Prevents Entra ID's DCR rejection from
short-circuiting a bearer-authenticated connection. Behavior is
unchanged when `oauth` is explicitly configured.
3. `listTools()` is wrapped to retry with a permissive Zod schema on
schema-validation errors. The fast path is unchanged for compliant
servers; non-compliant servers (Fabric returns `null` for
`annotations.{readOnlyHint,destructiveHint,idempotentHint,openWorldHint}`)
no longer fail the connection.
4. `normalizeMcpConfig` was silently dropping `oauth` and (would have
dropped) `headersCommand` when reconstructing remote entries. Both
are now passed through. This was a pre-existing latent bug that made
`oauth: false` configurations no-op at runtime.
Tests:
- 22 new tests across `test/mcp/mcp-bearer-auth.test.ts` (lenient schema,
`headersCommand` resolution + execFile-not-shell semantics, Authorization
detection, schema round-trip) and `test/mcp/headers.test.ts` (OAuth
auto-disable on static bearer, on `headersCommand`, and explicit-OAuth
override). All pass.
- Zero regressions vs `origin/main` baseline on existing 185 MCP tests.
- Live-verified end-to-end against `https://api.fabric.microsoft.com/v1/mcp/core`
using `headersCommand` + `az account get-access-token`: server connected,
29 tools registered, no proxy required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix: [#791][#792] address review feedback on bearer-auth remote MCP
Follow-up to f6b2e2d resolving all bot review comments:
- `isSchemaError`: narrow to `name === ZodError|$ZodError` AND an `issues`
array, so unrelated errors carrying an `issues` property no longer trigger
the lenient retry (Copilot).
- `resolveHeadersCommand`: wrap `execFile` failures with the header key
(`headersCommand[<name>] failed: ...`) and append trimmed `stderr` so
`mcp list` points to the exact failing command (Copilot).
- Drop the now-redundant `headersCommand failed:` prefix at the call site
since the thrown message already names the header.
- Use Zod v4 `z.looseObject(...)` instead of deprecated `.loose()` for the
lenient `tools/list` schemas (CodeRabbit).
- `config.ts`: reword `headersCommand` comment to clarify it is argv via
`execFile` (no shell interpolation); document why `normalizeMcpConfig`
passes malformed array shapes through (schema rejects them with an
actionable `invalid_type` rather than silently dropping) (Copilot).
- Tests: remove unnecessary `as any` cast; assert against the production
`LenientListToolsResultSchema` via `MCP._testing` instead of a duplicated
copy; add an end-to-end `listToolsLenient` retry test using a real SDK
`$ZodError`, and a test that array-shaped headers are rejected loudly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: [#791][#792] address second-round review on bearer-auth MCP support
- `discover.ts`: preserve `headersCommand` / `oauth` in the discovery
normalizer (mirrors `normalizeMcpConfig`) so auto-discovered servers
from `.mcp.json` / `.gemini/settings.json` keep their bearer-token
config instead of silently connecting with no auth
- `supportsOAuth()`: mirror `create()`'s OAuth auto-disable — return
`false` when an `Authorization` header is present (statically or via
`headersCommand`) and `oauth` is not explicitly configured, so the
auth routes no longer start OAuth flows the connection ignores
- `mergeHeaders()`: merge static + dynamic headers case-insensitively
(HTTP header names are case-insensitive) so `headersCommand` overrides
a static header differing only in casing instead of sending duplicates
- `resolveHeadersCommand()`: run the composed failure message through
`Telemetry.maskString()` before it reaches logs / `status.error`, so a
token echoed in argv or printed to stderr by a verbose auth CLI is
redacted
- tests: discovery round-trip for `headersCommand`/`oauth`, `supportsOAuth`
matrix, case-insensitive merge (unit + transport-level), stderr masking
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: validate foreign `oauth`/`headersCommand` shapes at MCP discovery ingestion
Regression-review follow-up to 03f82f2. The discovery normalizer passed
`oauth` / `headersCommand` through unvalidated from foreign config files.
Gemini CLI's `settings.json` legitimately uses `oauth: { enabled: true }`,
which our strict `McpOAuth` schema rejects — harmless at runtime, but
`mcp-discover add` persists discovered entries to `opencode.json` without
re-validation, so the poisoned entry would fail `Info.safeParse` on every
subsequent config load and break the session.
- `discover.ts`: preserve `headersCommand` only when it is a record of
non-empty string argv arrays, and `oauth` only when it is `false` or a
strict object of optional string `clientId`/`clientSecret`/`scope`;
drop foreign dialects with a debug log (matching pre-PR behavior).
Validators mirror `McpRemote` — schemas can't be imported as values
here because config.ts dynamically imports this module and the Config
import is type-only to avoid a static cycle.
- `mcp-bearer-auth.test.ts`: stop importing `sdk/types.js` in the
`$ZodError` retry test — `mcp.test.ts` replaces that module via
`mock.module` (process-global in bun), leaving `ListToolsResultSchema`
undefined in full-suite runs. Replicate the SDK's strict annotation
typing with `zod/v4-mini` (the same zod build the SDK validates with)
so the test produces the identical `$ZodError` deterministically.
- `discover.test.ts`: new test — foreign `oauth`/`headersCommand`
dialects are dropped while the server itself is still discovered, and
schema-valid shapes are preserved.
Full `test/mcp` suite (220 tests): failure set is byte-identical to
`origin/main`'s 29 pre-existing environment failures; all 31 tests added
by this PR pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent f376fef commit 986641e
6 files changed
Lines changed: 903 additions & 17 deletions
File tree
- packages/opencode
- src
- config
- mcp
- test/mcp
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
652 | 652 | | |
653 | 653 | | |
654 | 654 | | |
| 655 | + | |
| 656 | + | |
| 657 | + | |
| 658 | + | |
| 659 | + | |
| 660 | + | |
| 661 | + | |
| 662 | + | |
| 663 | + | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
| 669 | + | |
655 | 670 | | |
656 | 671 | | |
657 | 672 | | |
658 | 673 | | |
659 | | - | |
| 674 | + | |
| 675 | + | |
| 676 | + | |
660 | 677 | | |
661 | 678 | | |
662 | 679 | | |
| |||
1451 | 1468 | | |
1452 | 1469 | | |
1453 | 1470 | | |
| 1471 | + | |
| 1472 | + | |
| 1473 | + | |
| 1474 | + | |
| 1475 | + | |
| 1476 | + | |
1454 | 1477 | | |
| 1478 | + | |
| 1479 | + | |
| 1480 | + | |
| 1481 | + | |
| 1482 | + | |
| 1483 | + | |
| 1484 | + | |
| 1485 | + | |
| 1486 | + | |
1455 | 1487 | | |
1456 | 1488 | | |
1457 | 1489 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
89 | 89 | | |
90 | 90 | | |
91 | 91 | | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
92 | 134 | | |
93 | 135 | | |
94 | 136 | | |
| |||
0 commit comments