Skip to content

feat(mcp): custom HTTP headers for bring-your-own MCP servers - #2186

Merged
simple-agent-manager[bot] merged 18 commits into
mainfrom
sam/able-add-headers-mcp-0az2dh
Sep 29, 2026
Merged

simple-agent-manager[bot] merged 18 commits into
mainfrom
sam/able-add-headers-mcp-0az2dh

Conversation

@simple-agent-manager

@simple-agent-manager simple-agent-manager Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Bring-your-own MCP servers could only authenticate with a bearer token or a credential in the URL. Composio's MCP endpoints need a custom API-key header (x-api-key for single-toolkit MCP, x-consumer-api-key for Composio Connect), and the public guide listed custom headers as unsupported. This PR lets users manage custom HTTP headers on any MCP server, in both personal settings and project settings, and injects them into every agent harness.

  • Model. Headers are independent of authType (none | bearer). Composio is none plus x-api-key. A custom Authorization header is allowed only with none, for non-Bearer schemes.
  • Header names. Names match ^[A-Za-z0-9_-]{1,64}$, which is exactly what the Amp bridge mcp-remote@0.1.38 parses in --header name:value and what TOML accepts as a bare key. Names are unique case-insensitively, and transport-managed names (host, content-length, mcp-session-id, …) are reserved. The rule and the reserved list are pinned between TypeScript and Go by packages/shared/src/fixtures/mcp-server-name-contract.json.
  • Secrets. Header values are secrets: trimmed, non-empty, no control characters, capped at MCP_CONNECTION_HEADER_VALUE_MAX_BYTES (8192) bytes, and at most MAX_MCP_CONNECTION_HEADERS (10) per server.
    • They are stored as one AES-GCM ciphertext of the [{name,value}] list (D1 0178: encrypted_headers, headers_iv), plus a plaintext header_names display projection.
    • No response returns a value. The API exposes headerNames only.
  • Editing. PATCH treats headers as the full desired set, and an entry without a value keeps the stored one. The new inline Edit form can therefore add, remove or rotate one header without the browser ever holding the others. The update only writes if updated_at still matches the row it read (a lost race returns 409), so a header-only edit racing a switch to bearer can never store Authorization beside a bearer token.
  • Session start. Resolution decrypts and re-validates each row, and skips a bad row with a warning (rules 41/50). The vm-agent rejects a whole create-agent-session request over one bad header, so a bad row must never reach it.
  • vm-agent. McpServerEntry.Headers is additive and sent only when non-empty (rule 54). The vm-agent validates it (ValidateHeaders: charset, reserved names, duplicates, Authorization beside a token), persists it (SQLite migrateV18) and injects it per harness:
    • ACP HTTP header list (Claude Code, Gemini, OpenCode…).
    • Codex env_http_headers pointing at SAM_MCP_<NAME>_HEADER_<i>_SECRET, so values never land in config.toml.
    • Vibe headers table.
    • Amp mcp-remote --header name:${SAM_MCP_HEADER_<i>}, with values in the stdio env rather than argv.
  • Refactor. The first commit (refactor(vm-agent)) only moves code. It extracts the MCP/Codex/Vibe code from gateway.go, session_host.go, workspaces.go and store.go into dedicated files (rule 18), and the go-specialist verified it is behaviour-neutral line by line.

Task: tasks/archive/2026-09-29-mcp-connection-custom-headers.md (SAM task 01M3NTSE4PAGHVDJPKKZ0AZ2DH).

Validation

  • pnpm lint — pnpm check:fast exit 0 (format ratchet, oxlint shadow, ESLint workspace, type-boundary ratchet)
  • pnpm typecheck — api / web / shared tsc --noEmit clean; go vet clean on internal/acp, internal/persistence, internal/server
  • pnpm test
    • web: 321 files / 3,878 tests pass.
    • api: 787 files / 11,008 tests. The 8 failures in the full run were cold-import timeouts under a load average of ~18 on 8 cores, in 7 files this PR does not touch. On rerun, 3 of those files pass at default timeouts and the other 4 (40 tests) pass with longer timeouts.
    • Go: internal/acp, internal/persistence and internal/server pass in full.
  • Additional validation
    • pnpm build exit 0, quality:migration-safety exit 0, quality:file-sizes exit 0.
    • Playwright visual audit: 80/80 pass.
    • 25+ mutation checks: each new guard was deleted and its intended test went red. This covers 13 Go guards, 12 API guards (including the updated_at predicate, the strictly-increasing timestamp, the resolution backstop and both env-limit wirings), the wire tag and the reserved-name contract.
  • N/A: no sweep/cron/alarm candidate selection changed.
  • Secret Scan: Gitleaks' generic-api-key rule flagged a test literal that paired "x-api-key" with an environment-variable name. It is not a credential.
    • 82674ca69 removes that literal from the current tree; the test now builds the map in a loop and was mutation-checked.
    • 655038210 baselines the one historical PR-range occurrence as an exact, expiring synthetic-test-fixture digest.
    • A local current-tree scan shows 0 new findings.

Staging Verification (REQUIRED for all code changes — merge-blocking)

  • Staging deployment green — Deploy Staging run 36558820009 at c1c9084a4: Validate Configuration, Deploy to Cloudflare and smoke-tests all green
  • Live app verified via Playwright — logged into app.sammy.party with the staging smoke user (token-login) and drove the real UI
  • Existing workflows confirmed working — /dashboard, /projects, /settings/mcp-servers and a project's Runtime settings at 1280 and 375 px: pages load, no horizontal overflow, zero console errors, zero failed requests
  • New feature/fix verified on staging — two real agent harnesses called a header-gated MCP server through servers configured in the UI (details below)
  • Infrastructure verification completed — a real VM was provisioned for the test session. It heartbeat as healthy running this branch's vm-agent, served the workspace, ran the agent, and was then deleted.
  • Mobile and desktop verification notes added for UI changes

Staging Verification Evidence

Staging is shared, so I waited in line behind three other agents' staging verifications and coordinated handoffs by durable message. While waiting I renumbered this PR's D1 migration twice, 0175 → 0177 → 0178, before any environment had applied it: main claimed 0175 (#2181), and open sibling branches had already put 0176 and 0177 into staging's ledger. Staging's ledger now shows 0178_mcp_connection_headers.sql (id 206) on top of them.

Test endpoint. A minimal streamable-HTTP MCP server that answers only when x-api-key matches, as Composio does, and otherwise returns 401.

  • Its probe tool returns a per-run nonce and echoes any x-probe-* headers it received.
  • It logs header names only, never values.
  • It was reachable over HTTPS through a Cloudflare quick tunnel.
  • Checked first with the official MCP SDK client: tools/call succeeds with the header and is refused (401) without it.
  1. Add (project scope, real UI). In Project Settings → Runtime → MCP servers I added header-probe with authentication "None" and header x-api-key.
    • POST returned 201 with headerNames: ["x-api-key"]; the value was not echoed.
    • Staging D1 holds header_names = ["x-api-key"], a 108-byte encrypted_headers and a 16-byte headers_iv. instr() finds the key in neither column.
    • Screenshots: add form added
  2. Claude Code on a VM (profile "Claude Code VM Chat").
    • Node 01M3PE6VWYS2DWWRRKSHGB6PEE was created at 11:18:43Z and first heartbeat as healthy at 11:21:19Z. Its agent_version is a089e14f…, the last commit on this branch that touched packages/vm-agent. The workspace was ready at 11:23:20Z.
    • The probe logged server/discover → initialize → tools/list → tools/call, and every request was authorized by x-api-key.
    • The agent replied PROBE RESULT: probe-ok nonce-db0f0b13 extra-headers=[].
    • Screenshots: claude vm chat desktop claude vm chat mobile
  3. Edit (real UI). I added x-probe-mode = edited and left the saved x-api-key blank.
    • The browser sent {"headers":[{"name":"x-api-key"},{"name":"x-probe-mode","value":"edited"}]}, so it never held the stored value.
    • The response was 200 with headerNames: ["x-api-key","x-probe-mode"].
    • D1 now holds a 164-byte ciphertext; neither value appears in plaintext.
    • Screenshots: edit form list after edit desktop list after edit mobile
  4. Codex on Instant (container runtime) (profile "Instant Codex").
    • The probe logged initialize → tools/list → tools/call, each carrying both headers.
    • The reply was PROBE RESULT: probe-ok nonce-db0f0b13 extra-headers=[x-probe-mode=edited]. This shows the value kept through the edit still authorizes, and the Codex env_http_headers path works in the container runtime.
    • Screenshots: codex chat desktop codex chat mobile
  5. Personal scope (real UI). In Settings → MCP servers, add returned 201 (headerNames: ["x-api-key"], no value echoed) and the server was listed. Deleting it from the mobile view returned 200, and the D1 row is gone.
    • Screenshots: personal desktop personal mobile
  6. No regressions. Every UI step above recorded zero console errors and zero failed requests. The smoke pages at both widths had no overflow.
    • Screenshots: dashboard projects mobile runtime mobile
  7. Cleanup. Both sessions are stopped and their workspaces deleted. The VM node is deleted, /api/nodes returns [] and D1 has 0 active nodes. The test MCP connections are deleted and the tunnel is stopped. Staging was then handed to the next agent in line.

Staging ran c1c9084a4. Since then:

The Go acp/server/persistence suites, the TS typechecks, the MCP suites (175/175), migration ordering and the Gitleaks current-tree scan were all re-run green on the rebased head.

UI Compliance Checklist (Required for UI changes)

  • Mobile-first layout verified
    • Header rows are grouped cards at 375px.
    • Server actions sit below the text on phones.
    • The audit compares measured coordinates to prove each name/value/remove row stays inside its card.
  • Accessibility checks completed
    • Every header input and button has an accessible name (Header N name, <name> value, Remove <name>, Edit <server>, Delete <server>).
    • Forms are labelled regions (Add MCP server / Edit <server>).
  • Shared UI components used (Button, Input, Select from @simple-agent-manager/ui; the app's ConfirmDialog for delete).
  • Playwright visual audit run locally: apps/web/tests/playwright/mcp-servers-audit.spec.ts, 80/80 pass, with assertNoOverflow (including the clipped-overflow walk) in every scenario.
  • Desktop and mobile screenshots for every changed UI surface are linked below
  • Screenshots reviewed for quality control

UI Screenshot Evidence

All screenshots below were taken with Playwright by the local audit (apps/web/tests/playwright/mcp-servers-audit.spec.ts) against mocked API data. The staging screenshots under Staging Verification Evidence were taken with Playwright against app.sammy.party.

Surface: Settings → MCP servers — server list with header names

  • Desktop evidence: long header names desktop normal desktop 30 servers desktop special characters desktop empty desktop error desktop

  • Mobile evidence: long header names mobile normal mobile 30 servers mobile special characters mobile empty mobile error mobile

  • Mock/stress data used: Playwright mock data with long text (a row with five header names, two of them 64 unbroken characters; hosts with 90+ character subdomains), many items (30 servers, every fifth with two header names), special characters (a punycode emoji host, a <script> host, a Unicode host), and the empty and error (API 500) states.

  • Screenshot quality review: I reviewed every screenshot at both viewports for layout quality, overflow, clipping, readability and responsive behavior. The first audit found three mobile issues, all fixed in b63461d3f before handoff:

    • Header rows read as one blob.
    • The host was squeezed beside the actions.
    • The auth option was truncated.

    Long header names now wrap inside the card, and there is no horizontal overflow anywhere.

Surface: MCP server add form — headers editor

  • Desktop evidence: add form with headers desktop
  • Mobile evidence: add form with headers mobile
  • Mock/stress data used: Playwright mock data pushing long text: two header rows, one with a 64-character unbroken name, and the authentication select on its longest option, "None (credential in URL or headers)".
  • Screenshot quality review: I reviewed both screenshots for layout quality, overflow, clipping, readability and responsive behavior. The name rule is stated up front, values are masked, and each row groups name, value and remove on a phone. The audit measures that every row's controls stay inside the viewport. Result: no visual issues found.

Surface: MCP server edit form — inline edit that keeps saved secrets

  • Desktop evidence: edit form desktop
  • Mobile evidence: edit form mobile
  • Mock/stress data used: Playwright mock data with long text: editing the five-header server whose names include two 64-character unbroken names, with the saved URL, token and header values empty behind "Leave blank to keep" placeholders.
  • Screenshot quality review: I reviewed both screenshots for layout quality, overflow, clipping, readability and responsive behavior. The form states that a saved header keeps its value unless retyped, and shows the saved URL host only. Result: no visual issues found.

Surface: Project Settings → Runtime → MCP servers (shared)

  • Desktop evidence: project runtime desktop
  • Mobile evidence: project runtime mobile
  • Mock/stress data used: Playwright mock data with mixed edge cases: a bearer server, a localhost server (http://127.0.0.1:4788), a no-auth server carrying x-api-key, and a disabled server, below the runtime env-var and file sections.
  • Screenshot quality review: I reviewed both screenshots for layout quality, overflow, clipping, readability and responsive behavior. The shared server shows Headers: x-api-key and the Edit/Delete actions. Result: no visual issues found.

End-to-End Verification (Required for multi-component changes)

  • Data flow traced from user input to final outcome with code path citations
  • Capability test exercises the complete happy path across system boundaries
    • mcp-connection-headers-injection.test.ts saves a connection through the service, resolves it for a session, and calls a live mock MCP server that answers only with x-api-key.
    • The shared wire fixture mcp-server-entry-wire.json is serialized by node-agent.ts and posted to the real Go handleCreateAgentSession handler.
    • Go tests parse the generated Codex/Vibe TOML and the mcp-remote argv.
  • All spec/doc assumptions about existing behavior verified against code
    • The mcp-remote header regex comes from the 0.1.38 tarball source.
    • Codex env_http_headers comes from the Codex config reference.
    • isSecretEnvVar suffix handling was read and tested.
  • Gap between automated coverage and full E2E covered by the staging run below (real harnesses sending the header to a real endpoint).

Data Flow Trace

  1. UI: McpServerForm → toCreateRequest / toUpdateRequest (apps/web/src/components/mcp-servers/mcp-server-form-state.ts) → createMcpConnection / updateMcpConnection (apps/web/src/lib/api/mcp-connections.ts).
  2. API: userMcpConnectionRoutes / projectMcpConnectionRoutes (apps/api/src/routes/mcp-connections.ts, Valibot CreateMcpConnectionSchema / UpdateMcpConnectionSchema) → createMcpConnection / updateMcpConnection (apps/api/src/services/mcp-connections.ts) → validateMcpConnectionHeaders + sealMcpConnectionHeaders (apps/api/src/services/mcp-connection-headers.ts) → D1 mcp_connections.header_names, encrypted_headers, headers_iv.
  3. Session start: agent-session-bootstrap.ts → buildSessionMcpServers → resolveMcpServersForSession → toEntry → openMcpConnectionHeaders (apps/api/src/services/mcp-connection-resolution.ts) → McpServerEntry.headers.
  4. Control plane → vm-agent: serializeMcpServers (apps/api/src/services/node-agent.ts) → handleCreateAgentSession (packages/vm-agent/internal/server/workspaces.go) → normalizeMcpServers + McpServerEntry.ValidateHeaders (internal/server/mcp_servers.go, internal/acp/mcp_servers.go) → registerSessionMcpServers → UpsertSessionMcpServers (internal/persistence/session_mcp_servers.go).
  5. Harness: buildAcpMcpServers / buildAmpMcpServer (internal/acp/mcp_servers.go), generateCodexMcpConfig (internal/acp/codex_config.go), generateVibeConfig (internal/acp/vibe_config.go) → the MCP server receives the header.

Untested Gaps

  • Amp and Vibe are covered by generated-config tests (argv/env and parsed TOML), not by a live staging session. Claude Code (VM) and Codex (Instant) were exercised live on staging.

Post-Mortem (Required for bug fix PRs)

N/A: not a bug fix (new capability).

Specialist Review Evidence (Required for agent-authored PRs)

  • All local reviewers completed and findings addressed before merge
  • If any reviewer did NOT complete: needs-human-review label added and merge deferred to human — N/A, all completed
Reviewer Status Outcome
task-completion-validator ADDRESSED PASS. MEDIUM: stray vm-agent test .db files were committed; removed and gitignored. MEDIUM: acceptance criteria were left unticked; ticked with evidence. LOW: reserved-name scope; commented. Fixed in 1daf06264, 6f2bbec46.
go-specialist ADDRESSED The pure-move refactor was verified behaviour-neutral. Fixed in bbacb6cff (5 Go mutations red):
  • HIGH: a repeated header name made the whole Codex/Vibe TOML unparseable. Duplicates, and Authorization beside a bearer token, are now rejected.
  • MEDIUM: reserved names are now enforced in Go and pinned by the contract fixture.
  • MEDIUM: GetSessionMcpServers now skips one corrupt row instead of dropping every server.
Accepted and documented: MEDIUM, Vibe writes header values into ~/.vibe/config.toml. This is the existing bearer-token pattern: the directory is 0700 and the file is excluded from snapshots. Declined: LOW, mirroring the operator-configurable limits in Go.
cloudflare-specialist ADDRESSED HIGH TOCTOU: updateMcpConnection derives every field from the row it read, so an edit racing a switch to bearer could persist Authorization beside a bearer token. Fixed in bbacb6cff with an updated_at compare-and-set (409 on a lost race), strictly increasing timestamps, and a resolution-time backstop. All three were mutation-proven under a frozen clock.
security-auditor ADDRESSED No CRITICAL, HIGH or MEDIUM findings. LOW: stray .db files; fixed and gitignored. Info: vm-agent SQLite stores values like the existing token.
ui-ux-specialist ADDRESSED The header-name rule is shown up front, the edit form states keep-by-default behaviour, and server names use the card-title type style. Fixed in fd5c1b18d; Playwright 80/80 and screenshots re-reviewed.
test-engineer ADDRESSED Added in bbacb6cff and 9ae91f735: route-level POST/PATCH tests on real SQLite; env-driven header count/size limits (hardcoding either in the route turns a test red); Vibe TOML escaping; per-row persistence skip; authType switch keeping a non-conflicting header; bearer + x-api-key; update-time limit; UTF-8 byte limit.
doc-sync-validator PASS No stale docs.
env-validator PASS Info: the new limits follow the existing MCP_CONNECTION_* pattern (optional Worker vars, not synced from GitHub Environment), which is pre-existing for all five.
constitution-validator ADDRESSED No Principle XI violations; both limits are env-configurable with DEFAULT_* constants. LOW: stray artifacts; removed.
architecture-reviewer PASS MEDIUM deferred with justification: gateway.go, session_host.go and workspaces.go stay over 800 lines. They are on the check-file-sizes EXEMPT list, and this PR only shrinks them. LOW: consolidate other byte-length helpers onto lib/utf8.ts, tracked as SAM idea 01M3P26DV2N1FF59AZ021TX6MQ.
security-auditor (Secret Scan follow-up, run by PR Shepherd task 01M3PHPH90P58AXT4326PRQ6WE) PASS The sole historical finding was independently verified as a non-secret test fixture. The digest is exact and unique, expires after 90 days, contains no match bytes, and cannot suppress changed values or locations. No findings.
go-specialist (CI follow-up, run by PR Shepherd task 01M3PHPH90P58AXT4326PRQ6WE) PASS The remediation changes Go comments only. TestShutdown_CancelsSizeFallbackRequest is outside this PR's diff and is byte-identical to main; the exact test passed 10/10 locally under Go 1.26.6, while the affected ACP/persistence/server packages also passed.
task-completion-validator (final follow-up, run by PR Shepherd task 01M3PHPH90P58AXT4326PRQ6WE) PASS Checks A–F pass: research/checklist/diff/criteria/UI propagation/multi-resource/vertical-slice coverage are complete. All four UI surfaces, official references, staging proof, and cleanup are documented.

CodeRabbit Review Evidence (Required for agent-authored PRs)

  • coderabbit-review label applied after local review, staging, and CI gates passed
  • All CodeRabbit findings implemented or explicitly reviewed and closed/resolved: CodeRabbit produced no review, so there are no findings
  • Incremental CodeRabbit review completed after final pushed fixes, or no fixes were needed: waived under project policy 7da78e9f (see notes)
  • Latest CodeRabbit review has no unresolved feedback: no review exists, and there are no unresolved CodeRabbit threads

CodeRabbit Notes

  • The coderabbit-review label was applied at 13:00:58Z, after CI went fully green: CI run 36568729320, attempt 2.
    • Attempt 1 hit an unrelated flake in TestShutdown_CancelsSizeFallbackRequest, which is outside this diff, byte-identical to main, and passed 10/10 locally.
  • The trusted coderabbit-bot-review.yml run 36572061511 posted @coderabbitai review through the human-identity bridge at 13:01:09Z.
  • Before that, CodeRabbit's only activity was its automatic "Review skipped: bot user detected" status comment.
  • A second trusted dispatch, coderabbit-bot-review.yml run 36576317519 from main, posted @coderabbitai review again at 13:36:24Z.
  • Outcome: no review was produced. CodeRabbit posted no review, no inline comments and no reply to either command. Its only activity is the auto-generated status comment, which reads "Review skipped: bot user detected" and, after the 13:50Z rebase push, "Review skipped: auto reviews are disabled".
  • Waiver: per project policy 7da78e9f ("Do not block indefinitely on absent CodeRabbit reviews"), the gate is waived and recorded here. The trusted request path was used twice with no review. This waiver covers only the absent review; any CodeRabbit finding that arrives later will be triaged.

Exceptions (If any)

  • Scope: packages/vm-agent/internal/acp/gateway.go, internal/acp/session_host.go, internal/server/workspaces.go remain over 800 lines.
  • Rationale: they are pre-existing entries on the check-file-sizes EXEMPT list. This PR moves MCP, Codex and Vibe code out of them (net shrink) and adds nothing to them.
  • Expiration: tracked by the existing file-size debt list.

Agent Preflight (Required)

  • Preflight completed before code changes

Classification

  • external-api-change
  • cross-component-change
  • business-logic-change
  • public-surface-change
  • docs-sync-change
  • security-sensitive-change
  • ui-change
  • infra-change

External References

Official documentation and primary sources consulted before coding:

  • Composio MCP authentication: x-api-key (single-toolkit MCP) and x-consumer-api-key (Composio Connect). Sources: https://docs.composio.dev/docs/single-toolkit-mcp and https://composio.dev/toolkits/composio/framework/codex.
  • Codex configuration reference: mcp_servers.<id>.env_http_headers (map of header name to env var name) and bearer_token_env_var. Source: https://learn.chatgpt.com/docs/config-file/config-reference.
  • mcp-remote@0.1.38 (the Amp bridge), read from the published tarball:
    • --header parsing uses /^([A-Za-z0-9_-]+):\s*(.*)$/ (dist/chunk-65X3S4HB.js:20713); a non-matching name is silently dropped.
    • ${ENV} is expanded in values (:20851).
  • ACP Go SDK acpsdk.HttpHeader for inline HTTP MCP servers.
  • TOML 1.0 bare-key charset [A-Za-z0-9_-] and the duplicate-key rule, checked with pelletier/go-toml/v2 (the pinned parser).

Codebase Impact Analysis

  • packages/shared: MCP connection types and header constants, McpServerEntrySchema.headers, contract fixtures.
  • apps/api:
    • D1 migration 0178 (renumbered past main's 0175 and the staged sibling migrations 0176/0177).
    • services/mcp-connection-headers.ts (new), services/mcp-connections.ts (create/update and the concurrency guard), services/mcp-connection-resolution.ts, services/node-agent.ts (serializer).
    • Schemas, routes, limits and env.
  • packages/vm-agent:
    • internal/acp (entry, validation, ACP/Amp/Codex/Vibe builders).
    • internal/server (normalize, register, converters, agent_ws prefetch).
    • internal/persistence (migrateV18, per-row isolation).
  • apps/web: McpServersManager, the new McpServerForm, McpServerHeadersField, mcp-server-form-state.ts, lib/api/mcp-connections.ts, Settings help copy.

Documentation & Specs

  • apps/www/src/content/docs/docs/guides/mcp-servers.md: headers, the Composio row, editing, and the per-harness note. The "custom headers not supported" limitation is removed.
  • apps/www/src/content/docs/docs/reference/configuration.md and apps/api/.env.example: MAX_MCP_CONNECTION_HEADERS and MCP_CONNECTION_HEADER_VALUE_MAX_BYTES.
  • .claude/skills/changelog/SKILL.md: the mcp-connection-custom-headers entry.
  • No spec directory applies.

Constitution & Risk Check

  • Principle XI: both new limits are env-configurable with DEFAULT_* constants in packages/shared. The 64-character name length is a fixed safety ceiling, like the server-name length.
  • Secrets: values are encrypted at rest, never returned, never logged, and absent from error strings. The Go errors carry only an index or a name.
  • Rollout skew (rule 54):
    • The field is additive and sent only when non-empty.
    • The web UI tolerates a pre-headers API during the deploy window (headerNames ?? []).
    • An old vm-agent ignores headers, so no request is rejected. Placement's agent-version gate routes new sessions to current nodes.
  • Risk: Vibe writes header values into its own config file. This is the same exposure as the bearer token today: the directory is 0700 and the file is excluded from snapshots.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository: raphaeltm/simple-agent-manager/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 021d351b-7826-456c-aa05-c34b90a202e8

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codspeed

codspeed Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 6 untouched benchmarks


Comparing sam/able-add-headers-mcp-0az2dh (6550382) with main (eaa0178)

Open in CodSpeed

@simple-agent-manager simple-agent-manager Bot added the coderabbit-review Trigger CodeRabbit review for opt-in PRs label Sep 29, 2026
@raphaeltm

Copy link
Copy Markdown
Owner

@coderabbitai review

1 similar comment
@raphaeltm

Copy link
Copy Markdown
Owner

@coderabbitai review

raphaeltm and others added 18 commits September 29, 2026 13:37
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…to dedicated files

Pure moves, no behaviour change. Prepares for custom MCP headers without growing
files already over the rule-18 ceiling:
- acp/mcp_servers.go: McpServerEntry + ACP/Amp server builders (from gateway.go, session_host.go)
- acp/codex_config.go: Codex config.toml generation and writers (from gateway.go)
- acp/vibe_config.go: Vibe config.toml generation and writer (from gateway.go)
- server/mcp_servers.go: normalizeMcpServers + registerSessionMcpServers (from workspaces.go)
- persistence/session_mcp_servers.go: session_mcp_servers table + CRUD (from store.go)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MCP connections can now carry custom HTTP headers alongside (or instead of)
a bearer token — Composio requires x-api-key / x-consumer-api-key.

Control plane:
- D1 0175 adds header_names (display projection) + encrypted_headers/headers_iv
  (AES-GCM sealed [{name,value}] list). Values are never returned; responses
  expose headerNames only.
- services/mcp-connection-headers.ts owns the rules: [A-Za-z0-9_-]{1,64} names
  (mcp-remote + TOML bare-key safe), case-insensitive uniqueness, transport
  headers reserved, Authorization only when authType is none, trimmed values
  without control characters, configurable MAX_MCP_CONNECTION_HEADERS and
  MCP_CONNECTION_HEADER_VALUE_MAX_BYTES.
- PATCH headers is the full desired set; an entry without a value keeps the
  stored value, so one header can be rotated without re-sending the others.
- Resolution re-validates decrypted headers and skips a bad row (rules 41/50);
  node-agent sends headers only when present (rule 54).

vm-agent:
- McpServerEntry.Headers, validated in normalizeMcpServers, persisted
  (migrateV18) and restored on restart through one pair of converters.
- ACP HTTP header list, Codex env_http_headers with SAM_MCP_<NAME>_HEADER_<i>_SECRET
  env vars, Vibe headers table, Amp mcp-remote --header name:${ENV}. Secret
  values never reach config.toml (Codex) or argv (Amp).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…agent

- API: validation, encryption at rest, never-returned values, PATCH keep/rotate/
  remove/clear semantics, bearer vs Authorization conflict, unreadable-ciphertext
  recovery, rule-50 list tolerance (real SQLite).
- Vertical slice: a connection saved through the write path authorizes against a
  live x-api-key MCP server using only what session resolution injects; corrupt
  or vm-agent-unsafe rows are skipped without logging secrets.
- Shared wire fixture mcp-server-entry-wire.json: node-agent must serialize it
  exactly; the real vm-agent create-agent-session handler must accept it.
- vm-agent: ACP/Amp/Codex/Vibe output (TOML parsed, mcp-remote regex pinned),
  env var collision + secret classification, boundary validation, full
  normalize -> SQLite -> restart backfill round trip, migrateV18 upgrade.
- Every guard verified discriminating by mutation (15 mutations, all red).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- McpServerForm (add + inline Edit) and McpServerHeadersField replace the
  inline create form in McpServersManager, which keeps list/toggle/delete.
- Headers are name + masked value rows. A saved header shows its name and a
  blank value that keeps the saved one (the API never returns values);
  editing likewise keeps the URL and token unless new ones are typed.
- Rows list their header names; one form is open at a time.
- Payloads are built in mcp-server-form-state.ts: create omits empty headers,
  update always sends the full desired header set (value-less = keep).
- Unit tests drive the real form: Composio-shape create, unsaved-row removal,
  edit keep/rotate/remove/add, bearer token requirement, rejected save keeps
  the editor. Playwright audit adds header data, add/edit form scenarios and a
  measured-coordinate check of the responsive header row.
- Docs: MCP servers guide (headers, Composio, editing, Amp note), limits in
  configuration reference and .env.example, changelog skill entry.
- Formatting: prettier on the new files and on the MCP-owned files touched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Visual audit findings, fixed:
- header rows are bordered cards below sm so each value visibly belongs to its
  name (they read as one undifferentiated stack before)
- server row actions wrap under the text on phones instead of squeezing long
  hosts into a ~24-character column now that there are three of them
- the None auth option no longer truncates at 375px
- Settings help card: Composio takes an x-api-key header, not only a
  pre-signed URL
Audit: long header-name row and Project Settings -> Runtime scenarios added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… deploy

- The wire-contract test drove the real create-agent-session handler, whose
  message reporter wrote messages-ws.db* into the package directory, and those
  files were committed. Point the test's persistence path at a temp dir and
  remove them (found by the constitution-validator review).
- The deploy publishes the web UI before the API Worker, so listMcpConnections
  defaults a missing headerNames to [] for that window instead of crashing the
  MCP settings page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…scope

- gitignore packages/vm-agent/**/*.db{,-shm,-wal} so a test run cannot commit
  message-reporter databases again (task-completion + security review).
- ValidateMcpHeaders: say that policy rules (reserved names, duplicates,
  Authorization vs bearer, limits) live in the control plane, the only writer.
- Task file: acceptance criteria 1-6 and the Playwright audit ticked with the
  tests that prove them; staging stays open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ames

- updateMcpConnection writes only if updated_at still matches the row it
  read (409 otherwise); nextUpdatedAt keeps the column strictly increasing,
  so a header-only edit racing a switch to bearer can no longer persist an
  Authorization header beside a bearer token. Resolution refuses that pair
  as a backstop.
- vm-agent McpServerEntry.ValidateHeaders rejects reserved transport names,
  case-insensitive duplicates and Authorization beside a bearer token: one
  repeated key made the whole Codex/Vibe TOML unparseable, sam-mcp included.
  The reserved list is pinned TS<->Go by headerNames.reserved.
- GetSessionMcpServers skips a row whose headers cannot be decoded instead
  of failing the read, which the restore path treated as no MCP servers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… routes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… front

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main claimed 0175 in #2181, and a sibling branch's 0176 is already in
staging's D1 ledger. This file had never been applied anywhere, so it
moves to the next free prefix before its first staging deploy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Open sibling branches have already applied 0176 and 0177 to staging, so
this migration takes the next prefix no open branch uses, before its
first staging deploy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A literal "x-api-key": "SAM_MCP_..._SECRET" pair reads as an API key
assignment to Gitleaks; the values are environment variable names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@simple-agent-manager
simple-agent-manager Bot force-pushed the sam/able-add-headers-mcp-0az2dh branch from f096d7e to 6550382 Compare September 29, 2026 13:40
@simple-agent-manager
simple-agent-manager Bot merged commit 27e8bdd into main Sep 29, 2026
29 checks passed
@simple-agent-manager
simple-agent-manager Bot deleted the sam/able-add-headers-mcp-0az2dh branch September 29, 2026 13:58
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

coderabbit-review Trigger CodeRabbit review for opt-in PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant