Skip to content

fix(mcp): require the auth choice startMcpHttpServer serves under - #933

Merged
sroussey merged 1 commit into
mainfrom
claude/eloquent-gauss-y0d94j-mcp-auth
Sep 9, 2026
Merged

sroussey merged 1 commit into
mainfrom
claude/eloquent-gauss-y0d94j-mcp-auth

Conversation

@sroussey

@sroussey sroussey commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

What was wrong

StartMcpHttpServerArgs.token was readonly token?: string | undefined — optional, with no runtime default and no warning. authorizeBearer(header, undefined) returns "allowed" without reading the header (bearerAuth.ts:87). That is the correct answer for an explicit opt-out, and it is also exactly what a plain omission produces. Nothing logged, nothing threw.

The exposure is not abstract. createTaskMcpServer publishes every registered task outside Flow Control/Hidden as a callable tool, which for a typical host includes FetchUrlTask, JavaScriptTask, and FileGrepTask/FileSedTask/FileLoaderTask under registerCommonTasks({ fileSystemTasks: true }).

resolveAllowedHosts also returns undefined (no Host check at all) for a wildcard bind with no allowedHosts — deliberately, since nothing about 0.0.0.0 names the addresses a machine answers on. Combined with the optional token, this type-checked and ran:

await startMcpHttpServer({ port, host: "0.0.0.0", createServer });

…serving task execution to the network with neither a token nor a DNS-rebinding guard.

The concrete failure

A downstream host of @workglow/mcp/server — builder and embarc are the two named consumers — writes await startMcpHttpServer({ port, host, createServer }), forgets token, and ships. Every request is authorized. workglow mcp serve gets this right (examples/cli/src/commands/mcpServe.ts generates a token and requires --no-auth to drop it), so the "requires a bearer token by default" property held for exactly one caller — not for the downstream hosts the library exists to serve.

What changed

  • StartMcpHttpServerArgs.token is now readonly token: string | null. Omitting it is a type error; null is the written opt-out. null reads the same as before at the request layer (the handle's token and the internal request context stay string | undefined, so authorizeBearer is untouched).
  • New assertAuthChoice(token, host), called before the listener is created:
    • token === undefined throws. Unreachable through the types, checked anyway — this is a published package and an untyped caller still arrives with the field omitted, which used to mean "serve unauthenticated".
    • token === null together with a wildcard bind ("", 0.0.0.0, ::, [::]) throws. A wildcard is reachable under every name the machine answers to and resolveAllowedHosts derives no Host allow-list from it, so nothing at all would be left deciding who may run a task. Binding a named interface unauthenticated is still allowed — that is the deliberate escape, and it is stated in the error message.
  • examples/cli's resolveServeToken returns string | null; --no-auth reaches null instead of undefined. No behavior change for the CLI.
  • README snippet comment and the @workglow/mcp/server paragraph in .claude/CLAUDE.md updated, since the contract they describe changed.

Breaking change

This is a breaking API change to a published package. StartMcpHttpServerArgs.token is required. Callers pass their token, or null to serve unauthenticated on a named interface. The only in-repo caller (examples/cli) is updated here; downstream hosts will get a compile error, which is the point.

Verified

Run in a clean worktree after bun install, with bun run use-dist (real dist) for the type checks:

  • bunx vitest run packages/mcp/src/server/__tests__/62 passed.
  • The two new tests were run against the unfixed source first (fix stashed): both failed, as did the two existing assertions that depend on the null → undefined normalization — 4 failed / 17 passed. With the fix, all pass.
  • bunx vitest run examples/cli/src/commands/mcpServe.test.ts — 8 passed.
  • bunx tsc --noEmit in packages/mcp (src) and packages/mcp -p tsconfig.test.json (tests) and examples/cli — all clean.
  • bunx oxlint --type-aware packages/mcp/src examples/cli/src/commands/ — clean.
  • bunx oxfmt --check on the touched files — clean. (examples/cli/src/web/client/index.html reports a pre-existing format issue on main; untouched here.)

Not verified here

  • The full test suite and the full repo lint/typecheck were not run — only the slices above.
  • No downstream consumer (builder, embarc) was compiled against this change.

🤖 Generated with Claude Code

https://claude.ai/code/session_01T8DU24G9GWuRUJncc5TJFZ


Generated by Claude Code

`token` was optional with no default, so a host that omitted it published an
unauthenticated endpoint that executes every registered task — and nothing
logged or threw, because `authorizeBearer(header, undefined)` allows the
request without reading the header, which is also the right answer for a
deliberate opt-out. A wildcard bind made it worse: `resolveAllowedHosts`
returns no `Host` allow-list for one, so `{ port, host: "0.0.0.0",
createServer }` type-checked and served task execution to the network with
neither a token nor a rebinding guard.

`token` is now required and `string | null`: omitting it is a type error, an
untyped caller is refused at run time, and `null` — the written opt-out — is
refused outright together with a wildcard bind, where nothing else is left to
decide who may run a task.

BREAKING CHANGE: `StartMcpHttpServerArgs.token` is required. Pass the token, or
`null` to serve unauthenticated on a named interface.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T8DU24G9GWuRUJncc5TJFZ
@sroussey
sroussey merged commit fbabdbb into main Sep 9, 2026
15 checks passed
@sroussey
sroussey deleted the claude/eloquent-gauss-y0d94j-mcp-auth branch September 9, 2026 18:32
sroussey added a commit that referenced this pull request Sep 13, 2026
## @workglow/task-graph

### Features

#### task-graph,ai

- a tool call's whole life on the wire (#936)

## @workglow/web-search

### Bug Fixes

#### web-search

- reduce domain entries on every route, not just site: (#931)

## @workglow/ai

### Features

#### task-graph,ai

- a tool call's whole life on the wire (#936)

#### ai

- AgentTask: the tool-calling turn loop as a task, with the CLI and its console on it (#935)

### Bug Fixes

#### pricing

- stop reporting an unpriced rate as free (#930)

## workglow

### Updated Dependencies

- `tslog`: ^5.2.0

## @workglow/storage

### Bug Fixes

#### storage

- decide the join pushdown on enlistment, not the instance flag (#934)

## @workglow/mcp

### Breaking Changes

- **bug fixes(mcp)**: require the auth choice startMcpHttpServer serves under (#933)

### Bug Fixes

#### mcp

- require the auth choice startMcpHttpServer serves under (#933)

## @workglow/test

### Features

#### task-graph,ai

- a tool call's whole life on the wire (#936)

#### ai

- AgentTask: the tool-calling turn loop as a task, with the CLI and its console on it (#935)

### Bug Fixes

#### storage

- decide the join pushdown on enlistment, not the instance flag (#934)

#### pricing

- stop reporting an unpriced rate as free (#930)

### Updated Dependencies

- `miniflare`: ^5.20260911.0-alpha

## @workglow/postgres

### Bug Fixes

#### storage

- decide the join pushdown on enlistment, not the instance flag (#934)

## @workglow/openrouter

### Bug Fixes

#### pricing

- stop reporting an unpriced rate as free (#930)

## @workglow/eval

### Updated Dependencies

- `hyparquet`: ^1.30.1

## @workglow/cli

### Breaking Changes

- **bug fixes(mcp)**: require the auth choice startMcpHttpServer serves under (#933)

### Features

#### ai

- AgentTask: the tool-calling turn loop as a task, with the CLI and its console on it (#935)

### Bug Fixes

#### mcp

- require the auth choice startMcpHttpServer serves under (#933)

## @workglow/web

### Updated Dependencies

- `react-dom`: ^19.3.0
- `tailwind-merge`: ^3.7.0
- `@types/react-dom`: ^19.3.0
- `vite`: ^8.3.0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants