Fix query encoding, policy matching, scaffold deps; harden HTTP transports - #39
Merged
Merged
Conversation
…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
This was referenced Aug 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this fixes
Correctness
src/http.ts): values wereencodeURIComponent-ed and then appended toURLSearchParams, which encodes again.{ q: "a b&c=d" }reached the upstream API asq=a%2520b%2526c%253Dd. Values are now encoded exactly once;allowReservedparameters keep reserved characters raw.?was not escaped when patterns were compiled to regex, so--deny-tools "op?name"matched more than intended. Matching moved tosrc/policy.ts(*is the only wildcard) with unit tests./messagesaccepted a mismatchedsessionIdwhenever exactly one session was active. Now strict.--allow-host(typo of--allow-hosts) was silently ignored, which disabled the host allowlist without any signal.--auth-scopeprefix and always readMCP_OPENAPI_*.init/generateemitted"mcp-openapi": "latest". The npm package by that name is an unrelated third-party project (maintainerdocat0209, v0.3.0), so generated projects installed someone else's code. Scaffolds now depend ongithub:evalops/mcp-openapi.Transport hardening (MCP spec security guidance)
127.0.0.1by default;--hostoverrides. This is a behavior change for deployments that relied on binding all interfaces — pass--host 0.0.0.0there./mcp,/sse,/messages: localhost origins allowed,--allow-originsadds more, requests without an Origin header (non-browser MCP clients) unaffected. Blocks DNS-rebinding.MCP_OPENAPI_HTTP_AUTH_TOKENrequiresAuthorization: Bearer <token>on the MCP endpoints (timing-safe compare).*.--versionflag;redactSecretsno longer mangles arrays; fixed literal\nin the SSE listen banner.Testing
allowReservedround-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:inspectall pass locally.🤖 Generated with Claude Code
https://claude.ai/code/session_01LqBsGC7xLvihtBxhCdWKz5