Repository navigation
feat(mcp-scan): add dual-stack stateless MCP support (RFC #2597) - #3188
MohammadHaroonAbuomar merged 12 commits into
Conversation
🤖 AI Agent: security-scanner — View details
No security issues found. |
🤖 AI Agent: docs-sync-checker — Docs Sync
Docs Sync
|
🤖 AI Agent: breaking-change-detector — API Compatibility
API Compatibility
|
🤖 AI Agent: code-reviewer — View details
TL;DR: 0 blockers, 1 warning. The change is solid but requires follow-up for test coverage clarity.
Action items:
Warnings are fine as follow-up PRs. |
PR Review Summary
Verdict: AI review comments are untrusted advisory output. The summary reports workflow-generated completion status only, not model-authored pass/fail claims. |
|
🟡 Contributor Check: MEDIUM
Automated check by AGT Contributor Check. |
🤖 AI Agent: test-generator — `agent_os/cli/mcp_scan.py`
|
|
I'm happy with the dual-stack approach but I don't think the legacy fallback is implemented properly. The fallback is only triggering on an in-band JSON-=RPC -32601 at HTTP 200. The server/discover probe isn't wrapped, so an HTTP 400/404/405 rejection diverts to SSE and a 5xx/tiemout hard-fails, not triggering the legacy initialize handshake. Since the probe always sends Mcp-Protocol-Version: 2026-07-28 , a compliant 2025-11-25 streamable-HTTP server that rejects it at the HTTP layer now fails to scan where it worked before. Can you wrap the probe in a try/except and fall through to initialize and then add tests for edge cases like 400/405/500 fallbacks? Also the stateless path is skipping _validate_initialize_result right now so a malformed discover result will return ok with an empty inventory. |
liamcrumm
left a comment
There was a problem hiding this comment.
I'm happy with the dual-stack approach but I don't think the legacy fallback is implemented properly. The fallback is only triggering on an in-band JSON-=RPC -32601 at HTTP 200. The server/discover probe isn't wrapped, so an HTTP 400/404/405 rejection diverts to SSE and a 5xx/tiemout hard-fails, not triggering the legacy initialize handshake. Since the probe always sends Mcp-Protocol-Version: 2026-07-28 , a compliant 2025-11-25 streamable-HTTP server that rejects it at the HTTP layer now fails to scan where it worked before. Can you wrap the probe in a try/except and fall through to initialize and then add tests for edge cases like 400/405/500 fallbacks? Also the stateless path is skipping _validate_initialize_result right now so a malformed discover result will return ok with an empty inventory.
…d HTTP-rejected responses Review feedback on the dual-stack MCP scanner PR (microsoft#3188): 1. Wrap the initial server/discover probe in try/except so HTTP-level errors on the stateless path are handled explicitly instead of leaking out as uncaught exceptions. 2. On HTTP 400/404/405 from the stateless probe, fall through to the legacy initialize handshake on the SAME Streamable HTTP endpoint rather than letting inspect_remote_server's outer handler divert to SSE. SSE fallback is reserved for transport-level mismatches in the initialize handshake itself, not for stateless-probe rejections. 3. Validate malformed server/discover responses via the existing _validate_initialize_result helper (the same validator the legacy initialize handshake uses). On RuntimeError, fall through to the legacy handshake instead of returning a malformed inspection. 4. Add coverage: - test_inspect_streamable_http_server_discover_http_400/404/405 _falls_back_to_legacy_initialize: HTTP-level rejection of server/discover falls through to initialize on the same endpoint. - test_inspect_streamable_http_server_discover_http_500_does_not_fall_back: 5xx propagates to inspect_remote_server and returns ok=False (no SSE fallback because 500 is outside the {400, 404, 405} stateless fallback set). - test_inspect_streamable_http_server_discover_malformed_falls_back_to_legacy_initialize: malformed discover (missing capabilities) is rejected by _validate_initialize_result and the scanner falls through. The legacy SSE handler's auto-reset of reject_post_initialize is also removed so that test_url_only_remote_falls_back_to_legacy_sse_on_streamable_http_405 continues to test SSE fallback: a server that 405s server/discover will also 405 initialize, propagating to inspect_remote_server's outer handler. Signed-off-by: Dhinesh Ponnarasan <dhineshponnarasan@gmail.com>
|
liamcrumm Thanks for the detailed review! I have pushed a follow-up commit (
The updated test suite passes. I'd appreciate it if you could take another look when you have a chance. Thanks again! |
|
Dhinesh Ponnarasan (@DhineshPonnarasan) following up on liamcrumm's review from ~5 days ago. The blocking items:
This closes #2597 and #3130. Can you address these, or flag if you'd like a hand? |
Per maintainer follow-up on PR microsoft#3188, HTTP 5xx from the initial server/discover probe should also fall through to the legacy initialize handshake on the same endpoint, not hard-fail. - Remove the {400, 404, 405} guard so any HTTP-level rejection (4xx, 5xx) from the stateless probe falls through to legacy initialize. - Update code comments to reflect the broader fallback policy. - Replace test_inspect_streamable_http_server_discover_http_500 _does_not_fall_back with _falls_back_to_legacy_initialize, delegating to the shared assertion helper. Signed-off-by: Dhinesh Ponnarasan <dhineshponnarasan@gmail.com>
Hi Imran Siddique (@imran-siddique), Thanks for the follow-up. I have pushed a follow-up commit ( The Whenever you have a chance, I'd really appreciate another look. Thank you! |
Imran Siddique (imran-siddique)
left a comment
There was a problem hiding this comment.
Reviewed the dual-stack migration for stateless correctness, session/state confusion, and backward compatibility. This is a careful, well-tested change to mcp_scan.py.
Stateless correctness — verified. _streamable_http_call gates on is_stateless = protocol_version == MCP_PROTOCOL_VERSION_STATELESS: on the stateless path it never sets Mcp-Session-Id (the session_id and not is_stateless guard), and it defensively returns None for the session id so a server-issued session id in the response headers cannot leak back into the scanner. Mcp-Method is set on every request; Mcp-Name only for methods in _METHODS_REQUIRING_NAME (server/discover), sourced from _client_info()["name"] so it stays consistent with _meta.clientInfo.name. Per-request _meta is attached correctly.
No session/state confusion. The stateless call closure holds no nonlocal session_id and threads no session between requests — each tools/list / resources/list / prompts/list carries its own _meta and no session header. The legacy closure keeps its nonlocal session_id exactly as before.
Backward compatibility preserved. server/discover is tried first; any HTTPError / URLError / parse failure, a non-2026-07-28 protocolVersion, or a malformed discover result (rejected by the same _validate_initialize_result the legacy handshake uses) falls through to the untouched initialize -> notifications/initialized -> session-bound listing path, and the existing 400/404/405 -> SSE fallback in inspect_remote_server still applies. The test matrix is thorough and asserts exactly the right things: discover-first ordering, initialize/notifications/initialized never sent on the stateless path, no Mcp-Session-Id on any stateless request, per-request _meta, and legacy fallback on -32601 / HTTP 400/404/405/500 / malformed-body, with Mcp-Session-Id correctly reappearing on the legacy list calls.
Minor: the diff carries a large amount of unrelated ruff/black reformatting churn (allowlist expansion, argument wrapping) that inflates the line count and makes the substantive delta harder to isolate — not a blocker, but worth keeping formatting-only changes out of a feature PR in future.
131 tests passing, all CI checks green. Approving.
Thanks for the thorough review, Imran Siddique (@imran-siddique). I really appreciate the detailed feedback and the approval. I'll keep the note about avoiding unrelated formatting changes in mind for future PRs. |
Imran Siddique (imran-siddique)
left a comment
There was a problem hiding this comment.
The dual-stack logic is correct: the discover probe is wrapped so HTTP 4xx/5xx and malformed results fall through to the legacy handshake, _validate_initialize_result gates the stateless result, and the stateless path never carries or leaks a session id. Two required checks (Spell-check, No Stubs/TODOs) are failing on HEAD from base drift rather than your changed lines, so rebase onto main and rerun to get CI green before merge. Consider splitting the file-wide reformatting churn out of the feature diff next time.
|
Hi Dhinesh Ponnarasan (@DhineshPonnarasan), this PR has requested changes that are still open, with no update for a while. Could you address the review feedback when you have a chance? If there is no activity within one week we will close this to keep the review queue manageable, and you are welcome to reopen once you are ready. Thanks for the contribution. |
) Extends the Streamable HTTP inspection flow in agent_os/cli/mcp_scan.py to prefer the new stateless MCP 2026-07-28 flow (server/discover discovery with per-request _meta and required Streamable HTTP headers) while preserving the legacy 2025-11-25 handshake as a transparent fallback. Changes in mcp_scan.py: - Add MCP_PROTOCOL_VERSION_STATELESS, MCP_METHOD_HEADER, MCP_NAME_HEADER, and _METHODS_REQUIRING_NAME constants; extend _SUPPORTED_PROTOCOL_VERSIONS. - Extend _jsonrpc_request to attach per-request _meta when supplied. - Extend _streamable_http_call with a protocol_version parameter so every request carries Mcp-Method, Mcp-Name when required, and never Mcp-Session-Id on the stateless path. - Extend _inspect_streamable_http_server with a stateless fast-path that tries server/discover first and falls through to the existing initialize handshake on any error or non-2026-07-28 response. The legacy code path is unchanged: initialize, notifications/initialized, session-bound list calls, and the SSE fallback in inspect_remote_server continue to work exactly as before. Changes in test_mcp_scan_cli.py: - Extend _StreamableHTTPMCPHandler to recognize server/discover. - Add test_inspect_streamable_http_server_prefers_server_discover_when_supported covering Mcp-Protocol-Version, Mcp-Method, Mcp-Name, and Mcp-Session-Id absence for stateless requests plus per-request _meta with clientInfo and capabilities. - Add test_inspect_streamable_http_server_falls_back_to_legacy_when_discover_unsupported verifying the scanner transparently reverts to the 2025-11-25 handshake when the peer does not honor server/discover. Closes microsoft#3130. Signed-off-by: Dhinesh Ponnarasan <dhineshponnarasan@gmail.com>
…d HTTP-rejected responses Review feedback on the dual-stack MCP scanner PR (microsoft#3188): 1. Wrap the initial server/discover probe in try/except so HTTP-level errors on the stateless path are handled explicitly instead of leaking out as uncaught exceptions. 2. On HTTP 400/404/405 from the stateless probe, fall through to the legacy initialize handshake on the SAME Streamable HTTP endpoint rather than letting inspect_remote_server's outer handler divert to SSE. SSE fallback is reserved for transport-level mismatches in the initialize handshake itself, not for stateless-probe rejections. 3. Validate malformed server/discover responses via the existing _validate_initialize_result helper (the same validator the legacy initialize handshake uses). On RuntimeError, fall through to the legacy handshake instead of returning a malformed inspection. 4. Add coverage: - test_inspect_streamable_http_server_discover_http_400/404/405 _falls_back_to_legacy_initialize: HTTP-level rejection of server/discover falls through to initialize on the same endpoint. - test_inspect_streamable_http_server_discover_http_500_does_not_fall_back: 5xx propagates to inspect_remote_server and returns ok=False (no SSE fallback because 500 is outside the {400, 404, 405} stateless fallback set). - test_inspect_streamable_http_server_discover_malformed_falls_back_to_legacy_initialize: malformed discover (missing capabilities) is rejected by _validate_initialize_result and the scanner falls through. The legacy SSE handler's auto-reset of reject_post_initialize is also removed so that test_url_only_remote_falls_back_to_legacy_sse_on_streamable_http_405 continues to test SSE fallback: a server that 405s server/discover will also 405 initialize, propagating to inspect_remote_server's outer handler. Signed-off-by: Dhinesh Ponnarasan <dhineshponnarasan@gmail.com>
Per maintainer follow-up on PR microsoft#3188, HTTP 5xx from the initial server/discover probe should also fall through to the legacy initialize handshake on the same endpoint, not hard-fail. - Remove the {400, 404, 405} guard so any HTTP-level rejection (4xx, 5xx) from the stateless probe falls through to legacy initialize. - Update code comments to reflect the broader fallback policy. - Replace test_inspect_streamable_http_server_discover_http_500 _does_not_fall_back with _falls_back_to_legacy_initialize, delegating to the shared assertion helper. Signed-off-by: Dhinesh Ponnarasan <dhineshponnarasan@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (3)
agent-governance-python/agent-os/src/agent_os/cli/mcp_scan.py:1031
- With stateless
2026-07-28now in_SUPPORTED_PROTOCOL_VERSIONS,_validate_initialize_result()will currently warn that2026-07-28is "older" and that the "latest" is2025-11-25(because it only compares toMCP_PROTOCOL_VERSION). This produces a misleading warning for stateless peers. Consider only warning for protocol versions older than the legacy/staless versions.
capabilities = result.get("capabilities")
if not isinstance(capabilities, Mapping):
raise RuntimeError("initialize result did not advertise capabilities")
agent-governance-python/agent-os/tests/test_mcp_scan_cli.py:150
- The autouse handler reset fixture earlier in this file only clears request/response lists. With the new class-level
discover_*knobs on_StreamableHTTPMCPHandler, those values can leak across tests if a test fails before itsfinallyresets them. Resetting them in the fixture makes the suite more robust and order-independent.
# Test load_config
# ============================================================================
class TestLoadConfig:
agent-governance-python/agent-os/src/agent_os/cli/mcp_scan.py:1154
- There are two consecutive initializations of
_REQUEST_ID_COUNTERa few lines above_next_request_id(). This is redundant and makes it unclear which initialization is intended; it should be defined once.
def _next_request_id() -> int:
return next(_REQUEST_ID_COUNTER)
Signed-off-by: Dhinesh Ponnarasan <dhineshponnarasan@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
agent-governance-python/agent-os/src/agent_os/cli/mcp_scan.py:1033
_validate_initialize_result()warns for any protocol version that isn'tMCP_PROTOCOL_VERSION(2025-11-25). With stateless2026-07-28now supported and used viaserver/discover, this emits a misleading warning (it calls 2026-07-28 “older” and claims 2025-11-25 is the latest). The warning condition/message should exclude the stateless version and point to the preferred version instead.
capabilities = result.get("capabilities")
if not isinstance(capabilities, Mapping):
raise RuntimeError("initialize result did not advertise capabilities")
if not any(
isinstance(capabilities.get(key), Mapping) for key in ("tools", "resources", "prompts")
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
agent-governance-python/agent-os/src/agent_os/cli/mcp_scan.py:1347
_streamable_http_callmergesserver.headersverbatim, so a user-provided remote config can still inject reserved MCP headers (notablyMcp-Session-Id), which violates the stateless requirement and can also forceMcp-Name/Mcp-Methodon methods where the scanner intends to control them. This makes the “stateless flow MUST NOT set Mcp-Session-Id” guarantee false when the header is supplied via config.
method = payload.get("method", "")
headers: dict[str, str] = {
**server.headers,
"Accept": "application/json, text/event-stream",
"Mcp-Protocol-Version": protocol_version,
MCP_METHOD_HEADER: str(method),
}
agent-governance-python/agent-os/tests/test_mcp_scan_cli.py:1097
- The stateless tests currently only cover the default case where
RemoteMCPServerConfig.headersis empty. Because_streamable_http_callmergesserver.headers, this won’t catch regressions where a user-configured header likeMcp-Session-Idaccidentally leaks into stateless requests. Passing an injectedMcp-Session-Idhere makes the existingassert "Mcp-Session-Id" not in ...checks act as a regression test for that guarantee.
inspection = inspect_remote_server(
RemoteMCPServerConfig("http", "streamable-http", url), timeout=2
)
|
MohammadHaroonAbuomar , Thank you so much for your review. I had rebased onto main and added TimeoutError + RuntimeError to the discover-probe except list in 4f44bb5. |
Imran Siddique (imran-siddique)
left a comment
There was a problem hiding this comment.
Re-reviewed current head 78698d18 after the rebase. The dual-stack behavior remains sound:
- stateless discovery sends the required protocol/method/name headers and per-request
_meta; - no session ID is sent or retained on the stateless path;
- malformed or unsupported discovery responses and HTTP/transport failures fall through to the legacy initialize flow;
- discovery responses use the existing validation boundary before inventory is accepted;
- legacy session-bound listing and SSE fallback remain covered;
- explicitly supplied empty
_metais now preserved via the resolvedis not Nonefix.
The focused test matrix covers discover-first success, malformed discovery, HTTP 400/405/500 fallback, protocol mismatch, stateless session isolation, and legacy behavior. Content approved.
GitHub currently reports the branch as not mergeable, so any current base conflict/check gate must still be resolved before merge.
Signed-off-by: Dhinesh Ponnarasan <dhineshponnarasan@gmail.com>
Signed-off-by: Dhinesh Ponnarasan <dhineshponnarasan@gmail.com>
Imran Siddique (@imran-siddique) ,Thanks for the review! Branch is re-synced with main, and the two things blocking the merge are fixed: the No Stubs/TODOs check (a pass # comment in the new fallback code — comment moved above the pass) and a misleading "older protocol" warning on the stateless path (2026-07-28 is newer than 2025-11-25, so it was reporting a downgrade that wasn't happening) |
MohammadHaroonAbuomar
left a comment
There was a problem hiding this comment.
- CI 'Spell-check changed files' fails on this PR's own added lines: Spell-check changed files: unknown word recwarn in added lines; Please add the terms to .cspell-repo-terms.txt (or reword) so the gate passes.
Signed-off-by: Dhinesh Ponnarasan <dhineshponnarasan@gmail.com>
MohammadHaroonAbuomar I've addressed the spell-check failure by adding recwarn to .cspell-repo-terms.txt and pushed the fix in b37d04b. The change is limited to the spell-check terms file. Thanks! |
MohammadHaroonAbuomar
left a comment
There was a problem hiding this comment.
Verified at b37d04b: the recwarn spell fix passes a faithful local run of the spell-check job, the new merge of main is mechanical, and no scanner code or test changed since the last review. All earlier asks hold at this head (probe wrapped in try/except with 400/404/405/500 and malformed-body fallbacks, stateless result validated, TimeoutError and RuntimeError handled, empty _meta preserved). 136 mcp-scan tests pass locally, 8 single-parent commits signed off.
Addressed at head b37d04b: the server/discover probe is wrapped in try/except with fallback tests for 400, 404, 405, 500 and malformed bodies, the stateless initialize result is validated, and the scanner falls through to legacy initialize on any unexpected answer. Verified locally (136 mcp-scan tests) and by adversarial probes against the test server.
) (microsoft#3188) * feat(mcp-scan): add dual-stack stateless MCP support (RFC microsoft#2597) Extends the Streamable HTTP inspection flow in agent_os/cli/mcp_scan.py to prefer the new stateless MCP 2026-07-28 flow (server/discover discovery with per-request _meta and required Streamable HTTP headers) while preserving the legacy 2025-11-25 handshake as a transparent fallback. Changes in mcp_scan.py: - Add MCP_PROTOCOL_VERSION_STATELESS, MCP_METHOD_HEADER, MCP_NAME_HEADER, and _METHODS_REQUIRING_NAME constants; extend _SUPPORTED_PROTOCOL_VERSIONS. - Extend _jsonrpc_request to attach per-request _meta when supplied. - Extend _streamable_http_call with a protocol_version parameter so every request carries Mcp-Method, Mcp-Name when required, and never Mcp-Session-Id on the stateless path. - Extend _inspect_streamable_http_server with a stateless fast-path that tries server/discover first and falls through to the existing initialize handshake on any error or non-2026-07-28 response. The legacy code path is unchanged: initialize, notifications/initialized, session-bound list calls, and the SSE fallback in inspect_remote_server continue to work exactly as before. Changes in test_mcp_scan_cli.py: - Extend _StreamableHTTPMCPHandler to recognize server/discover. - Add test_inspect_streamable_http_server_prefers_server_discover_when_supported covering Mcp-Protocol-Version, Mcp-Method, Mcp-Name, and Mcp-Session-Id absence for stateless requests plus per-request _meta with clientInfo and capabilities. - Add test_inspect_streamable_http_server_falls_back_to_legacy_when_discover_unsupported verifying the scanner transparently reverts to the 2025-11-25 handshake when the peer does not honor server/discover. Closes microsoft#3130. Signed-off-by: Dhinesh Ponnarasan <dhineshponnarasan@gmail.com> * feat(mcp-scan): harden dual-stack discover probe against malformed and HTTP-rejected responses Review feedback on the dual-stack MCP scanner PR (microsoft#3188): 1. Wrap the initial server/discover probe in try/except so HTTP-level errors on the stateless path are handled explicitly instead of leaking out as uncaught exceptions. 2. On HTTP 400/404/405 from the stateless probe, fall through to the legacy initialize handshake on the SAME Streamable HTTP endpoint rather than letting inspect_remote_server's outer handler divert to SSE. SSE fallback is reserved for transport-level mismatches in the initialize handshake itself, not for stateless-probe rejections. 3. Validate malformed server/discover responses via the existing _validate_initialize_result helper (the same validator the legacy initialize handshake uses). On RuntimeError, fall through to the legacy handshake instead of returning a malformed inspection. 4. Add coverage: - test_inspect_streamable_http_server_discover_http_400/404/405 _falls_back_to_legacy_initialize: HTTP-level rejection of server/discover falls through to initialize on the same endpoint. - test_inspect_streamable_http_server_discover_http_500_does_not_fall_back: 5xx propagates to inspect_remote_server and returns ok=False (no SSE fallback because 500 is outside the {400, 404, 405} stateless fallback set). - test_inspect_streamable_http_server_discover_malformed_falls_back_to_legacy_initialize: malformed discover (missing capabilities) is rejected by _validate_initialize_result and the scanner falls through. The legacy SSE handler's auto-reset of reject_post_initialize is also removed so that test_url_only_remote_falls_back_to_legacy_sse_on_streamable_http_405 continues to test SSE fallback: a server that 405s server/discover will also 405 initialize, propagating to inspect_remote_server's outer handler. Signed-off-by: Dhinesh Ponnarasan <dhineshponnarasan@gmail.com> * fix(mcp-scan): expand discover-probe HTTPError fallback to cover 5xx Per maintainer follow-up on PR microsoft#3188, HTTP 5xx from the initial server/discover probe should also fall through to the legacy initialize handshake on the same endpoint, not hard-fail. - Remove the {400, 404, 405} guard so any HTTP-level rejection (4xx, 5xx) from the stateless probe falls through to legacy initialize. - Update code comments to reflect the broader fallback policy. - Replace test_inspect_streamable_http_server_discover_http_500 _does_not_fall_back with _falls_back_to_legacy_initialize, delegating to the shared assertion helper. Signed-off-by: Dhinesh Ponnarasan <dhineshponnarasan@gmail.com> * fix(mcp-scan): add TimeoutError and RuntimeError to discover-probe except list Per MohammadHaroonAbuomar's review on PR microsoft#3188, a legacy peer that hangs (TimeoutError) or mis-identifies the server/discover response (RuntimeError) should fall through to the legacy initialize handshake instead of hard-failing the inspection. Both exceptions are added to the existing fall-through except tuple (urllib.error.URLError, ValueError, json.JSONDecodeError). Signed-off-by: Dhinesh Ponnarasan <dhineshponnarasan@gmail.com> * fix: handle empty meta dict correctly in _jsonrpc_request Signed-off-by: Dhinesh Ponnarasan <dhineshponnarasan@gmail.com> * fix(mcp-scan): satisfy no-stubs gate in stateless probe fallback Signed-off-by: Dhinesh Ponnarasan <dhineshponnarasan@gmail.com> * fix(mcp-scan): don't report stateless 2026-07-28 protocol as older Signed-off-by: Dhinesh Ponnarasan <dhineshponnarasan@gmail.com> * fix(mcp-scan): add recwarn to spell-check terms Signed-off-by: Dhinesh Ponnarasan <dhineshponnarasan@gmail.com> --------- Signed-off-by: Dhinesh Ponnarasan <dhineshponnarasan@gmail.com> Signed-off-by: yuvrajsingh2428 <offcyuvi2428@gmail.com>
Summary
Implements the scanner/client half of RFC #2597's MCP dual-stack migration (issue #3130) by extending the existing Streamable HTTP inspection flow in
agent_os/cli/mcp_scan.py. The scanner now prefers the new stateless MCP 2026-07-28 flow (server/discoverdiscovery with per-request_metaand required Streamable HTTP headers) while preserving the legacy 2025-11-25 handshake as a transparent fallback.What changed
mcp_scan.py— extends four existing sites; no new top-level functions, no new modules, no new files.MCP_PROTOCOL_VERSION_STATELESS = "2026-07-28",MCP_METHOD_HEADER,MCP_NAME_HEADER, and_METHODS_REQUIRING_NAMEconstants; extends_SUPPORTED_PROTOCOL_VERSIONS._jsonrpc_requestto attach a per-request_metapayload when supplied._streamable_http_callwith aprotocol_versionparameter so every Streamable HTTP request carriesMcp-Method, setsMcp-Nameonly for routable methods (currentlyserver/discover), and never setsMcp-Session-Idon the stateless path. The function also defensively returnsNonefor the session id on the stateless path so a server-issued session id from response headers cannot leak into the scanner._inspect_streamable_http_serverwith a labelled stateless fast-path at the top that triesserver/discoverfirst and falls through to the existing legacyinitializehandshake on any error or non-2026-07-28 response.The legacy code path is unchanged:
initialize->notifications/initialized-> session-bound list calls, plus the existing 400/404/405 -> legacy SSE fallback ininspect_remote_server, all work exactly as before.Test coverage
test_mcp_scan_cli.py— extends the existing_StreamableHTTPMCPHandlerto recogniseserver/discover, and adds two new tests:test_inspect_streamable_http_server_prefers_server_discover_when_supportedverifies the maintainer's required assertions on the stateless path:MCP-Protocol-Version: 2026-07-28header set,Mcp-Method: server/discoverheader set,Mcp-Name: agent-os-mcp-scanheader set onserver/discover,Mcp-Session-Idabsent on every stateless request, per-request_metacarryingclientInfoandcapabilities,initializeandnotifications/initializednever invoked.test_inspect_streamable_http_server_falls_back_to_legacy_when_discover_unsupportedverifies the scanner transparently reverts to the 2025-11-25 handshake when the peer returns method-not-found forserver/discover, and thatMcp-Session-Idis then carried on the legacy list calls exactly as before.The existing
test_inspect_streamable_http_server_lists_tools_and_sends_spec_headerswas updated to find theinitializerequest by method name (sinceserver/discoveris now the first probe) and explicitly asserts the new probe was sent before falling through. All original assertions still hold.pytest tests/test_mcp_scan_cli.pyreports 131 passed, 0 failed (was 129 before this change). The full MCP test suite passes.Backward compatibility
inspect_remote_server/inspect_stdio_server/parse_remote_mcp_servers/parse_stdio_mcp_servers/run_security_scan/cmd_*APIs all keep their existing behavior for legacy 2025-11-25 peers.test_mcp_scan_cli.pycontinue to pass; the one test that asserted the order of the first wire request was updated to reflect the new dual-stack probe behaviour, with a preserving comment explaining the change.agt-mcp.mjs,server.mjs) are not in this tree; tracked separately per the RFC.Closes #3130
Hi Imran Siddique (@imran-siddique), Could you please review this whenever you have some time, I'd really appreciate your feedback.
Thanks in advance!