Skip to content

Commit 992b4da

Browse files
committed
find_invalid_x_mcp_header: handle non-str type; bump client conformance timeout
- find_invalid_x_mcp_header: a JSON-Schema array type (e.g. ["string", "null"]) raised TypeError on the frozenset membership check, crashing list_tools() on modern sessions against untrusted server input. Guard with isinstance(str) so any non-scalar type cleanly maps to the not-primitive reason. Covers dict/list type values via two new parametrize cases. - conformance.yml: bump --timeout to 60s on the client legs. The harness runs all scenarios via unbounded Promise.all; 40 parallel python processes on a 2-core runner starve sse-retry (the only scenario with a real-time SSE reconnect wait) past the 30s default. - everything-server _unseal_state: normalise base64/utf-8 decode errors to the same INVALID_PARAMS as the HMAC mismatch path.
1 parent e418756 commit 992b4da

4 files changed

Lines changed: 14 additions & 2 deletions

File tree

.github/workflows/conformance.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,20 @@ jobs:
6767
node-version: 24
6868
- run: uv sync --frozen --all-extras --package mcp
6969
- name: Run client conformance (all suite)
70+
# The harness runs all scenarios via unbounded Promise.all; with 40
71+
# scenarios on a 2-core runner the slowest one (sse-retry, which has a
72+
# real-time SSE reconnect wait) needs more than the 30s default budget.
7073
run: >-
7174
npx --yes "$CONFORMANCE_PKG" client
7275
--command 'uv run --frozen python .github/actions/conformance/client.py'
7376
--suite all
77+
--timeout 60000
7478
--expected-failures ./.github/actions/conformance/expected-failures.yml
7579
- name: Run client conformance (2026-07-28 wire, all suite)
7680
run: >-
7781
npx --yes "$CONFORMANCE_PKG" client
7882
--command 'uv run --frozen python .github/actions/conformance/client.py'
7983
--suite all
84+
--timeout 60000
8085
--spec-version 2026-07-28
8186
--expected-failures ./.github/actions/conformance/expected-failures.2026-07-28.yml

examples/servers/everything-server/mcp_everything_server/server.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import asyncio
88
import base64
9+
import binascii
910
import hashlib
1011
import hmac
1112
import json
@@ -493,7 +494,10 @@ def _unseal_state(state: str) -> str:
493494
expected = hmac.new(_STATE_HMAC_KEY, encoded.encode(), hashlib.sha256).hexdigest()
494495
if not sig or not hmac.compare_digest(sig, expected):
495496
raise MCPError(code=INVALID_PARAMS, message="requestState failed integrity verification")
496-
return base64.urlsafe_b64decode(encoded).decode()
497+
try:
498+
return base64.urlsafe_b64decode(encoded).decode()
499+
except (binascii.Error, UnicodeDecodeError) as e:
500+
raise MCPError(code=INVALID_PARAMS, message="requestState failed integrity verification") from e
497501

498502

499503
@mcp.tool()

src/mcp/shared/inbound.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,8 @@ def find_invalid_x_mcp_header(input_schema: Any) -> str | None:
141141
header = prop_schema[X_MCP_HEADER_KEY]
142142
if not isinstance(header, str) or not _RFC9110_TOKEN.fullmatch(header):
143143
return f"property {prop_name!r}: {X_MCP_HEADER_KEY} {header!r} is not an RFC 9110 token"
144-
if prop_schema.get("type") not in _X_MCP_HEADER_PRIMITIVE_TYPES:
144+
prop_type = prop_schema.get("type")
145+
if not isinstance(prop_type, str) or prop_type not in _X_MCP_HEADER_PRIMITIVE_TYPES:
145146
return f"property {prop_name!r}: {X_MCP_HEADER_KEY} is only permitted on primitive-typed properties"
146147
lower = header.lower()
147148
if lower in seen:

tests/shared/test_inbound.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,8 @@ def test_find_invalid_x_mcp_header_accepts_valid_or_absent_annotations(input_sch
383383
pytest.param(_schema(a={"type": "object", "x-mcp-header": "Data"}), id="on-object"),
384384
pytest.param(_schema(a={"type": "array", "x-mcp-header": "Items"}), id="on-array"),
385385
pytest.param(_schema(a={"type": "null", "x-mcp-header": "Nil"}), id="on-null"),
386+
pytest.param(_schema(a={"type": ["string", "null"], "x-mcp-header": "Maybe"}), id="array-type"),
387+
pytest.param(_schema(a={"type": {"not": "valid"}, "x-mcp-header": "Bad"}), id="dict-type"),
386388
pytest.param(_schema(a={"x-mcp-header": "NoType"}), id="missing-type"),
387389
pytest.param(
388390
_schema(a={"type": "string", "x-mcp-header": "Region"}, b={"type": "string", "x-mcp-header": "Region"}),

0 commit comments

Comments
 (0)