Skip to content

Fix query encoding, policy matching, scaffold deps; harden HTTP transports - #39

Merged
haasonsaas merged 1 commit into
mainfrom
fix/correctness-and-transport-hardening
Aug 4, 2026
Merged

Fix query encoding, policy matching, scaffold deps; harden HTTP transports#39
haasonsaas merged 1 commit into
mainfrom
fix/correctness-and-transport-hardening

Conversation

@haasonsaas

Copy link
Copy Markdown
Collaborator

What this fixes

Correctness

  • Query double-encoding (src/http.ts): values were encodeURIComponent-ed and then appended to URLSearchParams, which encodes again. { q: "a b&c=d" } reached the upstream API as q=a%2520b%2526c%253Dd. Values are now encoded exactly once; allowReserved parameters keep reserved characters raw.
  • Policy pattern matching: ? was not escaped when patterns were compiled to regex, so --deny-tools "op?name" matched more than intended. Matching moved to src/policy.ts (* is the only wildcard) with unit tests.
  • SSE session check: /messages accepted a mismatched sessionId whenever exactly one session was active. Now strict.
  • Unknown CLI flags now error. Previously --allow-host (typo of --allow-hosts) was silently ignored, which disabled the host allowlist without any signal.
  • OAuth env prefix: client-credentials lookup ignored the --auth-scope prefix and always read MCP_OPENAPI_*.
  • Scaffold dependency confusion: init/generate emitted "mcp-openapi": "latest". The npm package by that name is an unrelated third-party project (maintainer docat0209, v0.3.0), so generated projects installed someone else's code. Scaffolds now depend on github:evalops/mcp-openapi.
  • Response size caps now count UTF-8 bytes, not string length.

Transport hardening (MCP spec security guidance)

  • Web transports bind 127.0.0.1 by default; --host overrides. This is a behavior change for deployments that relied on binding all interfaces — pass --host 0.0.0.0 there.
  • Origin validation on /mcp, /sse, /messages: localhost origins allowed, --allow-origins adds more, requests without an Origin header (non-browser MCP clients) unaffected. Blocks DNS-rebinding.
  • Optional bearer auth: setting MCP_OPENAPI_HTTP_AUTH_TOKEN requires Authorization: Bearer <token> on the MCP endpoints (timing-safe compare).
  • CORS reflects only allowed origins instead of *.
  • --version flag; redactSecrets no longer mangles arrays; fixed literal \n in the SSE listen banner.

Testing

  • 10 new tests (42 total, all passing): single-encoding + allowReserved round-trips against a live HTTP server, policy matcher unit tests, unknown-flag rejection, --version, scaffold dependency assertion, streamable-http 403/401/success matrix, SSE origin rejection.
  • npm run check, build, test, smoke, mcp:inspect all pass locally.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LqBsGC7xLvihtBxhCdWKz5

…ports

Correctness:
- Query parameter values were passed through encodeURIComponent and then
  appended to URLSearchParams, which encodes again at serialization; values
  like "a b" reached the upstream API as "a%2520b". Values are now appended
  raw and encoded exactly once. allowReserved parameters bypass
  URLSearchParams so reserved characters survive as-is.
- Tool allow/deny patterns did not escape "?" when building the regex, so a
  literal "?" in a pattern acted as a regex optional quantifier. Pattern
  matching moved to src/policy.ts with "*" as the only wildcard, and is now
  unit-tested.
- The SSE /messages route fell back to "the only active session" when the
  sessionId did not match; the session id is now matched strictly.
- Unknown CLI arguments now fail with an error instead of being silently
  ignored (a typo like --allow-host previously disabled the host allowlist
  without any signal).
- Response size limits now measure UTF-8 bytes (Buffer.byteLength), not
  UTF-16 code units.
- OAuth2 client-credentials env lookup now honors the --auth-scope env
  prefix; previously it always read MCP_OPENAPI_* even for scoped tags.
- init/generate scaffolds depended on npm "mcp-openapi@latest", which is an
  unrelated third-party package; they now depend on
  github:evalops/mcp-openapi.

Transport hardening:
- Web transports bind to 127.0.0.1 by default; --host restores wider
  binding.
- /mcp, /sse, and /messages validate the Origin header (DNS-rebinding
  protection). Localhost origins are always allowed; --allow-origins adds
  more. Non-browser clients without an Origin header are unaffected.
- If MCP_OPENAPI_HTTP_AUTH_TOKEN is set, /mcp, /sse, and /messages require
  Authorization: Bearer <token>, compared timing-safe.
- CORS reflects only allowed origins instead of "*".
- redactSecrets now recurses into arrays instead of converting them to
  objects.
- Added --version; fixed literal "\n" in the SSE listen banner.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqBsGC7xLvihtBxhCdWKz5
@haasonsaas
haasonsaas merged commit fd41a76 into main Aug 4, 2026
4 checks passed
@haasonsaas
haasonsaas deleted the fix/correctness-and-transport-hardening branch August 4, 2026 02:50
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.

1 participant