Skip to content

feat(mcp-scan): add dual-stack stateless MCP support (RFC #2597) - #3188

Merged
MohammadHaroonAbuomar merged 12 commits into
microsoft:mainfrom
DhineshPonnarasan:feat/3130-mcp-dual-stack-scanner
Sep 15, 2026
Merged

MohammadHaroonAbuomar merged 12 commits into
microsoft:mainfrom
DhineshPonnarasan:feat/3130-mcp-dual-stack-scanner

Conversation

@DhineshPonnarasan

@DhineshPonnarasan Dhinesh Ponnarasan (DhineshPonnarasan) commented Jun 25, 2026 •

Copy link
Copy Markdown
Contributor

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/discover discovery with per-request _meta and 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.

  • Adds MCP_PROTOCOL_VERSION_STATELESS = "2026-07-28", MCP_METHOD_HEADER, MCP_NAME_HEADER, and _METHODS_REQUIRING_NAME constants; extends _SUPPORTED_PROTOCOL_VERSIONS.
  • Extends _jsonrpc_request to attach a per-request _meta payload when supplied.
  • Extends _streamable_http_call with a protocol_version parameter so every Streamable HTTP request carries Mcp-Method, sets Mcp-Name only for routable methods (currently server/discover), and never sets Mcp-Session-Id on the stateless path. The function also defensively returns None for the session id on the stateless path so a server-issued session id from response headers cannot leak into the scanner.
  • Extends _inspect_streamable_http_server with a labelled stateless fast-path at the top that tries server/discover first and falls through to the existing legacy initialize handshake 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 in inspect_remote_server, all work exactly as before.

Test coverage

test_mcp_scan_cli.py — extends the existing _StreamableHTTPMCPHandler to recognise server/discover, and adds two new tests:

  • test_inspect_streamable_http_server_prefers_server_discover_when_supported verifies the maintainer's required assertions on the stateless path: MCP-Protocol-Version: 2026-07-28 header set, Mcp-Method: server/discover header set, Mcp-Name: agent-os-mcp-scan header set on server/discover, Mcp-Session-Id absent on every stateless request, per-request _meta carrying clientInfo and capabilities, initialize and notifications/initialized never invoked.
  • test_inspect_streamable_http_server_falls_back_to_legacy_when_discover_unsupported verifies the scanner transparently reverts to the 2025-11-25 handshake when the peer returns method-not-found for server/discover, and that Mcp-Session-Id is then carried on the legacy list calls exactly as before.

The existing test_inspect_streamable_http_server_lists_tools_and_sends_spec_headers was updated to find the initialize request by method name (since server/discover is 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.py reports 131 passed, 0 failed (was 129 before this change). The full MCP test suite passes.

Backward compatibility

  • All existing public exports are unchanged. Existing CLI subcommands and the 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.
  • All 129 pre-existing tests in test_mcp_scan_cli.py continue 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.
  • Bundled JS MCP servers referenced by RFC RFC: Adopt a dual-stack migration for MCP 2026-07-28 #2597 (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!

@github-actions github-actions Bot added the tests label Jun 25, 2026
@github-actions

github-actions Bot commented Jun 25, 2026 •

Copy link
Copy Markdown
🤖 AI Agent: security-scanner — View details

AI-generated review output. Treat it as untrusted analysis and verify before acting.

No security issues found.

@github-actions

github-actions Bot commented Jun 25, 2026 •

Copy link
Copy Markdown
🤖 AI Agent: docs-sync-checker — Docs Sync

AI-generated review output. Treat it as untrusted analysis and verify before acting.

Docs Sync

  • README.md -- section describing MCP protocol versions and supported features needs update to include the new 2026-07-28 stateless protocol version.
  • CHANGELOG.md -- missing entry for the addition of dual-stack stateless MCP support and related behavioral changes.

@github-actions github-actions Bot added the size/M Medium PR (< 200 lines) label Jun 25, 2026
@github-actions

github-actions Bot commented Jun 25, 2026 •

Copy link
Copy Markdown
🤖 AI Agent: breaking-change-detector — API Compatibility

AI-generated review output. Treat it as untrusted analysis and verify before acting.

API Compatibility

Severity Change Impact
High _streamable_http_call now includes a protocol_version parameter. This change may break any existing code that calls _streamable_http_call without providing the new parameter, as it modifies the function's signature.
High _inspect_streamable_http_server introduces a new stateless fast-path for server/discover. This change alters the behavior of the function, potentially impacting users relying on the legacy handshake being the first probe.
Medium The order of the first wire request in test_inspect_streamable_http_server_lists_tools_and_sends_spec_headers has changed. This may affect users or tests that depend on the specific order of requests.

@github-actions

github-actions Bot commented Jun 25, 2026 •

Copy link
Copy Markdown
🤖 AI Agent: code-reviewer — View details

AI-generated review output. Treat it as untrusted analysis and verify before acting.

TL;DR: 0 blockers, 1 warning. The change is solid but requires follow-up for test coverage clarity.

# Sev Issue Where
1 Warn Test coverage for backward compatibility could be clearer test_mcp_scan_cli.py

Action items:

  • Ensure backward compatibility tests explicitly cover all legacy paths, including edge cases for initialize and notifications/initialized.

Warnings are fine as follow-up PRs.

@github-actions

Copy link
Copy Markdown

PR Review Summary

Check Status Details
🔍 Code Review ⚠️ Missing No current-run comment
🛡️ Security Scan ⚠️ Missing No current-run comment
🔄 Breaking Changes ⚠️ Missing No current-run comment
📝 Docs Sync ⚠️ Missing No current-run comment
🧪 Test Coverage ⚠️ Missing No current-run comment

Verdict: ⚠️ AI review incomplete; ready for human review

AI review comments are untrusted advisory output. The summary reports workflow-generated completion status only, not model-authored pass/fail claims.

@github-actions

Copy link
Copy Markdown

🟡 Contributor Check: MEDIUM

Check Result
Profile MEDIUM
Credential LOW
Overall MEDIUM

Automated check by AGT Contributor Check.

@github-actions

github-actions Bot commented Jun 30, 2026 •

Copy link
Copy Markdown
🤖 AI Agent: test-generator — `agent_os/cli/mcp_scan.py`

AI-generated review output. Treat it as untrusted analysis and verify before acting.

agent_os/cli/mcp_scan.py

  • test_inspect_streamable_http_server_handles_invalid_protocol_version -- Validate behavior when an unsupported protocol version is provided.
  • test_inspect_streamable_http_server_handles_missing_headers -- Ensure proper handling of requests missing required headers like Mcp-Method or Mcp-Name.
  • test_inspect_streamable_http_server_handles_invalid_meta_payload -- Test behavior when _meta payload contains invalid or unexpected data.
  • test_inspect_streamable_http_server_handles_unexpected_response_codes -- Verify fallback or error handling for unexpected HTTP response codes (e.g., 500, 403).
  • test_inspect_streamable_http_server_handles_partial_implementation -- Ensure correct behavior when the server supports some but not all stateless MCP features.

@liamcrumm

Copy link
Copy Markdown
Contributor

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 liamcrumm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Dhinesh Ponnarasan (DhineshPonnarasan) added a commit to DhineshPonnarasan/agent-governance-toolkit that referenced this pull request Jul 3, 2026
…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>
@github-actions github-actions Bot added size/L Large PR (< 500 lines) and removed size/M Medium PR (< 200 lines) labels Jul 3, 2026
@DhineshPonnarasan

Dhinesh Ponnarasan (DhineshPonnarasan) commented Jul 3, 2026 •

Copy link
Copy Markdown
Contributor Author

liamcrumm Thanks for the detailed review!

I have pushed a follow-up commit (fc856b98) addressing the feedback:

  • Wrapped the initial server/discover probe so HTTP 400/404/405 responses fall back to the legacy initialize handshake on the same endpoint.
  • Reused _validate_initialize_result to validate the stateless discovery response before treating it as successful.
  • Added regression tests covering the requested HTTP fallback scenarios, HTTP 500 behavior, and malformed discovery responses.

The updated test suite passes. I'd appreciate it if you could take another look when you have a chance. Thanks again!

@imran-siddique

Copy link
Copy Markdown
Collaborator

Dhinesh Ponnarasan (@DhineshPonnarasan) following up on liamcrumm's review from ~5 days ago. The blocking items:

  • Wrap the server/discover probe in try/except so an HTTP 400/404/405/5xx rejection falls through to the legacy initialize handshake instead of diverting to SSE or hard-failing. Today a compliant 2025-11-25 streamable-HTTP server that rejects Mcp-Protocol-Version: 2026-07-28 at the HTTP layer regresses and fails to scan where it worked before.
  • Fix the stateless-path skip liamcrumm flagged.
  • Add edge-case tests for the 400/405/500 fallback.

This closes #2597 and #3130. Can you address these, or flag if you'd like a hand?

Dhinesh Ponnarasan (DhineshPonnarasan) added a commit to DhineshPonnarasan/agent-governance-toolkit that referenced this pull request Jul 6, 2026
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>
@github-actions github-actions Bot added size/XL Extra large PR (500+ lines) and removed size/L Large PR (< 500 lines) labels Jul 6, 2026
@DhineshPonnarasan

Copy link
Copy Markdown
Contributor Author

Dhinesh Ponnarasan (@DhineshPonnarasan) following up on liamcrumm's review from ~5 days ago. The blocking items:

  • Wrap the server/discover probe in try/except so an HTTP 400/404/405/5xx rejection falls through to the legacy initialize handshake instead of diverting to SSE or hard-failing. Today a compliant 2025-11-25 streamable-HTTP server that rejects Mcp-Protocol-Version: 2026-07-28 at the HTTP layer regresses and fails to scan where it worked before.
  • Fix the stateless-path skip liamcrumm flagged.
  • Add edge-case tests for the 400/405/500 fallback.

This closes #2597 and #3130. Can you address these, or flag if you'd like a hand?


Hi Imran Siddique (@imran-siddique),

Thanks for the follow-up. I have pushed a follow-up commit (c51dd94) addressing the additional feedback.

The server/discover probe now falls back to the legacy initialize handshake for HTTP errors from the initial stateless probe, including 5xx responses, and I've updated the regression tests accordingly.

Whenever you have a chance, I'd really appreciate another look. Thank you!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@DhineshPonnarasan

Copy link
Copy Markdown
Contributor Author

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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@imran-siddique

Copy link
Copy Markdown
Collaborator

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>
Copilot AI review requested due to automatic review settings July 30, 2026 16:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-28 now in _SUPPORTED_PROTOCOL_VERSIONS, _validate_initialize_result() will currently warn that 2026-07-28 is "older" and that the "latest" is 2025-11-25 (because it only compares to MCP_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 its finally resets 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_COUNTER a 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>
Copilot AI review requested due to automatic review settings July 30, 2026 19:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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't MCP_PROTOCOL_VERSION (2025-11-25). With stateless 2026-07-28 now supported and used via server/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")

Copilot AI review requested due to automatic review settings July 30, 2026 20:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_call merges server.headers verbatim, so a user-provided remote config can still inject reserved MCP headers (notably Mcp-Session-Id), which violates the stateless requirement and can also force Mcp-Name/Mcp-Method on 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.headers is empty. Because _streamable_http_call merges server.headers, this won’t catch regressions where a user-configured header like Mcp-Session-Id accidentally leaks into stateless requests. Passing an injected Mcp-Session-Id here makes the existing assert "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
        )

@DhineshPonnarasan

Dhinesh Ponnarasan (DhineshPonnarasan) commented Jul 30, 2026 •

Copy link
Copy Markdown
Contributor Author

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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 _meta is now preserved via the resolved is not None fix.

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>
@DhineshPonnarasan

Copy link
Copy Markdown
Contributor Author

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 _meta is now preserved via the resolved is not None fix.

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.


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 MohammadHaroonAbuomar left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • 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>
@DhineshPonnarasan

Copy link
Copy Markdown
Contributor Author
  • 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.

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 MohammadHaroonAbuomar left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@MohammadHaroonAbuomar
MohammadHaroonAbuomar dismissed liamcrumm’s stale review September 15, 2026 15:34

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.

@MohammadHaroonAbuomar
MohammadHaroonAbuomar merged commit 14921d8 into microsoft:main Sep 15, 2026
125 checks passed
Yuvraj Singh (yuvrajsingh2428) pushed a commit to yuvrajsingh2428/agent-governance-toolkit that referenced this pull request Oct 1, 2026
) (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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-review:MEDIUM Contributor check flagged MEDIUM risk size/XL Extra large PR (500+ lines) tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mcp: add dual-stack scanner compatibility for 2026-07-28

5 participants