Skip to content

fix(mcp): honor client UI extension over stateless HTTP - #1097

Open
Aaron ("AJ") Steers (aaronsteers) wants to merge 3 commits into
mainfrom
devin/1785634148-mcp-ui-http-extensions
Open

fix(mcp): honor client UI extension over stateless HTTP#1097
Aaron ("AJ") Steers (aaronsteers) wants to merge 3 commits into
mainfrom
devin/1785634148-mcp-ui-http-extensions

Conversation

@aaronsteers

@aaronsteers Aaron ("AJ") Steers (aaronsteers) commented Aug 2, 2026

Copy link
Copy Markdown
Member

Closes #1096.

Requested by AJ Steers.

Summary

MCP Apps interactive-ui tools (show_connectors_list, show_workspace_sync_status, show_connection_sync_history) were visible to a UI-capable client over stdio but never over streamable HTTP. Reproduced: stdio 44 tools, HTTP 39.

Root cause: the hosted HTTP entrypoint runs stateless_http=True, and mcp's StreamableHTTPSessionManager builds a fresh ServerSession per request (mcp/server/streamable_http_manager.py, stateless branch). By the time tools/list is served, ServerSession._client_params is None, so FastMCP's client_supports_extension() — which only reads ClientCapabilities.extensions captured at initialize (fastmcp/server/low_level.py, unchanged through fastmcp 3.4.5) — returns False and airbyte_ui_support_filter hides the tools. Nothing about the client is wrong; the capability simply doesn't survive to the request that needs it.

The fix keeps the server stateless (statefulness would require LB session affinity and would only paper over the per-request gap) and instead lets an HTTP client re-declare its extensions per request:

def _client_supports_ui() -> bool:
    # session-declared capabilities: stdio and stateful HTTP
    context_supports_ui = _fastmcp_context_supports_ui(get_context())  # False if no context
    # per-request declaration: stateless HTTP
    return context_supports_ui or UI_EXTENSION_ID in _client_declared_extensions_from_headers()

X-MCP-Extensions is a comma-separated list of extension IDs, e.g. X-MCP-Extensions: io.modelcontextprotocol/ui. The filter never fails open: with no session capabilities and no header, the tools stay hidden.

Why a header, and why X--prefixed

There is no standardized MCP header for client capabilities. The spec-aligned stateless mechanism is per-request _meta under io.modelcontextprotocol/clientCapabilities (MCP 2026-07-28 / SEP-2575; CLIENT_CAPABILITIES_META_KEY in mcp-types 2.0.0). That path is not usable yet on our stack: with mcp 1.25 / fastmcp 3.2, request-params _meta is not propagated into the server RequestContext consulted by tool filters (mcp/server/lowlevel/server.py builds the context from transport message.request_meta, not params._meta), and get_context().request_context.meta is None during a stateless tools/list even when the client sends it. Rather than ship dead code, this PR documents that path as the future direction and uses the non-standard X--prefixed header as the interim escape hatch. When FastMCP surfaces per-request capabilities, _client_supports_ui() gains one more branch and the header can be retired.

Upstream-fix framing for FastMCP, if we want to file it: MiddlewareServerSession.client_supports_extension() should fall back to the current request's _meta[io.modelcontextprotocol/clientCapabilities] when _client_params is None, which would make capability declaration work in stateless mode without any vendor-specific header.

Changes

  • airbyte/mcp/_tool_utils.py: _client_supports_ui() falls back to _client_declared_extensions_from_headers(); generic parser returns the declared extension ID set (comma/whitespace tolerant, blank-safe, header name matched case-insensitively via FastMCP's lowercased get_http_headers()).
  • airbyte/constants.py: MCP_EXTENSIONS_HEADER = "X-MCP-Extensions".
  • Docs: airbyte/mcp/__init__.py and airbyte/mcp/http_main.py tell HTTP clients to send the header and explain the stateless capability loss.
  • tests/unit_tests/test_mcp_http.py: new regression coverage.

Test plan

tests/unit_tests/test_mcp_http.py asserts, against a real in-process ASGI streamable-HTTP server plus a real stdio subprocess server:

  • stdio, client declaring extensions: {"io.modelcontextprotocol/ui": {}} at initialize → all three show_ tools visible; non-declaring client → hidden.
  • stateless HTTP with X-MCP-Extensions: io.modelcontextprotocol/ui (sent mixed-case on the wire, asserted via an httpx event hook) → all three visible; no header or blank header → hidden.
  • parametrized unit cases for the header parser (single, comma-separated, unknown, blank, missing).

Local: ruff format, ruff check, pyrefly check clean; new tests 10 passed; fast suite 473 passed / 1 skipped. tests/integration_tests/test_install.py::test_install_failure_log_pypi fails identically on a clean origin/main worktree (pre-existing, unrelated).

Link to Devin session: https://app.devin.ai/sessions/26b42c83920e467ea4a8242915c17dc7
Requested by: Aaron ("AJ") Steers (@aaronsteers)

Summary by CodeRabbit

  • New Features

    • Added support for detecting MCP Apps interactive-ui capabilities through the X-MCP-Extensions request header.
    • Interactive UI tools are shown only when clients declare support, including comma- or whitespace-separated extension values.
    • Stateless HTTP clients can request UI tools by sending the extension header with each request.
  • Documentation

    • Added guidance for enabling interactive UI support in stateless HTTP connections.
  • Tests

    • Added coverage for supported, unsupported, missing, blank, and multiple extension declarations.

…ensions

Co-Authored-By: AJ Steers <aj@airbyte.io>
Copilot AI review requested due to automatic review settings August 2, 2026 01:29
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

👋 Greetings, Airbyte Team Member!

Here are some helpful tips and reminders for your convenience.

💡 Show Tips and Tricks

Testing This PyAirbyte Version

You can test this version of PyAirbyte using the following:

# Run PyAirbyte CLI from this branch:
uvx --from 'git+https://github.com/airbytehq/PyAirbyte.git@devin/1785634148-mcp-ui-http-extensions' pyairbyte --help

# Install PyAirbyte from this branch for development:
pip install 'git+https://github.com/airbytehq/PyAirbyte.git@devin/1785634148-mcp-ui-http-extensions'

PR Slash Commands

Airbyte Maintainers can execute the following slash commands on your PR:

  • /fix-pr - Fixes most formatting and linting issues
  • /uv-lock - Updates uv.lock file
  • /test-pr - Runs tests with the updated PyAirbyte
  • /prerelease - Builds and publishes a prerelease version to PyPI
📚 Show Repo Guidance

Helpful Resources

Community Support

Questions? Join the #pyairbyte channel in our Slack workspace.

📝 Edit this welcome message.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The MCP server now detects MCP Apps interactive-ui support from FastMCP client context or the X-MCP-Extensions request header. Tests cover stdio and stateless HTTP tool visibility, header parsing, and missing or blank declarations.

Changes

MCP Apps UI capability detection

Layer / File(s) Summary
UI extension header contract
airbyte/constants.py, airbyte/mcp/__init__.py, airbyte/mcp/http_main.py
Defines MCP_EXTENSIONS_HEADER and documents the stateless HTTP header workaround for interactive-ui tools.
Combined client capability detection
airbyte/mcp/_tool_utils.py
Combines FastMCP context detection with parsing of comma- or whitespace-separated X-MCP-Extensions values.
Transport and header validation
tests/unit_tests/test_mcp_http.py
Tests UI tool visibility over stdio and stateless HTTP for supported, unsupported, blank, missing, unknown, and separated declarations.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: copilot

Sequence Diagram(s)

sequenceDiagram
  participant MCPClient
  participant StatelessHTTP
  participant ToolFilter
  MCPClient->>StatelessHTTP: Send X-MCP-Extensions header
  StatelessHTTP->>ToolFilter: Provide request headers
  ToolFilter->>ToolFilter: Parse declared extension IDs
  ToolFilter-->>MCPClient: Return interactive-ui tools when supported
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address issue #1096 by restoring UI tool visibility over HTTP and adding regression coverage for stdio, HTTP, and header parsing.
Out of Scope Changes check ✅ Passed The constant, documentation, detection logic, and regression tests directly support the linked issue and PR objectives.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the fix for honoring client UI extension declarations over stateless HTTP.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch devin/1785634148-mcp-ui-http-extensions

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

coderabbitai[bot]

This comment was marked as resolved.

This comment was marked as resolved.

@github-code-quality

github-code-quality Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Overview

Languages: Python

Python / code-coverage/pytest-fast

The overall coverage in commit d7db9bd in the devin/1785634148-mcp... branch is 68%. The coverage in commit d9f652f in the main branch is 65%.

Show a code coverage summary of the most impacted files.
File main d9f652f devin/1785634148-mcp... d7db9bd +/-
airbyte/mcp/registry.py 53% 70% +17%
airbyte/mcp/_tool_utils.py 72% 91% +19%
airbyte/mcp/server.py 69% 88% +19%
airbyte/mcp/_arg_resolvers.py 13% 44% +31%
airbyte/mcp/int...c_history_ui.py 0% 36% +36%
airbyte/mcp/int...hared_models.py 0% 81% +81%
airbyte/cloud/models.py 0% 91% +91%
airbyte/mcp/int..._registry_ui.py 0% 92% +92%
airbyte/mcp/int...nc_status_ui.py 0% 97% +97%
airbyte/mcp/_guards.py 0% 100% +100%

Python / code-coverage/pytest-no-creds

The overall coverage in commit d7db9bd in the devin/1785634148-mcp... branch is 68%. The coverage in commit d9f652f in the main branch is 65%.

Show a code coverage summary of the most impacted files.
File main d9f652f devin/1785634148-mcp... d7db9bd +/-
airbyte/mcp/registry.py 53% 70% +17%
airbyte/mcp/_tool_utils.py 72% 91% +19%
airbyte/mcp/server.py 69% 88% +19%
airbyte/mcp/_arg_resolvers.py 13% 44% +31%
airbyte/mcp/int...c_history_ui.py 0% 36% +36%
airbyte/mcp/int...hared_models.py 0% 81% +81%
airbyte/cloud/models.py 0% 91% +91%
airbyte/mcp/int..._registry_ui.py 0% 92% +92%
airbyte/mcp/int...nc_status_ui.py 0% 97% +97%
airbyte/mcp/_guards.py 0% 100% +100%

Python / code-coverage/pytest

The overall coverage in commit d7db9bd in the devin/1785634148-mcp... branch is 73%. The coverage in commit d9f652f in the main branch is 71%.

Show a code coverage summary of the most impacted files.
File main d9f652f devin/1785634148-mcp... d7db9bd +/-
airbyte/mcp/registry.py 53% 70% +17%
airbyte/mcp/_tool_utils.py 72% 91% +19%
airbyte/mcp/server.py 69% 88% +19%
airbyte/mcp/_arg_resolvers.py 13% 44% +31%
airbyte/mcp/int...c_history_ui.py 0% 36% +36%
airbyte/mcp/int...hared_models.py 0% 81% +81%
airbyte/mcp/int..._registry_ui.py 0% 92% +92%
airbyte/cloud/models.py 0% 93% +93%
airbyte/mcp/int...nc_status_ui.py 0% 97% +97%
airbyte/mcp/_guards.py 0% 100% +100%

Updated August 02, 2026 02:11 UTC

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Runtime verification

Verified against real server processes — the actual airbyte-mcp-http console script over the network and a real python -m airbyte.mcp.server stdio subprocess — not the in-process ASGI harness the unit tests use. The bug was also reproduced on main via a git worktree, for a true before/after.

Transport / declaration Code tools/list show_* tools
stdio + extensions: io.modelcontextprotocol/ui PR 44 all 3
stdio, no extension PR 39 none
HTTP + X-MCP-Extensions: io.modelcontextprotocol/ui main (before) 39 none — issue reproduced
HTTP + X-MCP-Extensions: io.modelcontextprotocol/ui PR (after) 44 all 3
HTTP, no header PR 39 none

HTTP-with-header now matches stdio exactly, and the set diff vs. the no-header run adds only the three show_ tools — nothing else changed.

tools/call gating

With the header:

call: {"isError": false,
       "text_preview": "{\n  \"registry_url\": \"https://connectors.airbyte.com/files/registries/v0/oss_registry.json\",\n  \"connector_count\": 652, ...",
       "n_content": 1}

Without the header:

call: {"isError": true,
       "text_preview": "Tool 'show_connectors_list' is not available. It may be restricted based on your current session configuration.",
       "n_content": 1}

The gate holds at call time, not just in listing.

Header parser on the wire
Header value Result Verdict
x-mCp-ExTeNsIoNs: io.modelcontextprotocol/ui (mixed-case name) 44, all 3 pass
other, io.modelcontextprotocol/ui , another 44, all 3 pass
io.modelcontextprotocol/uix (near-miss) 39, none pass
, and `` (empty) 39, none pass

Note: a whitespace-padded value like " , " cannot be sent over a real connection — httpx/h11 raise LocalProtocolError: Illegal header value, since HTTP forbids leading/trailing whitespace in header values. That input is unreachable in practice and is covered by the unit tests with a mocked header getter.

Regression: browser landing page
HTTP/1.1 200 OK
content-type: text/html; charset=utf-8
content-length: 2063
...
<title>Airbyte MCP Server</title>
http://localhost:8080/mcp
docs.airbyte.com/ai-agents/

GET on the MCP path still serves the landing page; MCP POST traffic on the same path is unaffected.

No failures. This is a protocol/CLI change with no GUI surface, so the evidence is verbatim client output rather than screenshots.


Devin session

Co-Authored-By: AJ Steers <aj@airbyte.io>
Copilot AI review requested due to automatic review settings August 2, 2026 01:43

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 5 out of 5 changed files in this pull request and generated no new comments.

Suppressed comments (1)

airbyte/mcp/_tool_utils.py:546

  • _client_declared_extensions_from_headers() assumes get_http_headers() returns a dict keyed by the lowercased header name and does a direct .get(header_key). Elsewhere in this module (_resolve_transport_bearer_token) the code treats the returned header keys as potentially non-normalized and matches case-insensitively. To avoid coupling tool visibility to a specific get_http_headers() normalization behavior, make this lookup case-insensitive too (and keep the include optimization).
def _client_declared_extensions_from_headers() -> set[str]:
    """Return comma- or whitespace-separated extension IDs from HTTP headers."""
    header_key = MCP_EXTENSIONS_HEADER.lower()
    header_value = get_http_headers(include={header_key}).get(header_key, "")
    return set(header_value.replace(",", " ").split())

coderabbitai[bot]

This comment was marked as resolved.

Co-Authored-By: AJ Steers <aj@airbyte.io>
Copilot AI review requested due to automatic review settings August 2, 2026 01:47

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 5 out of 5 changed files in this pull request and generated no new comments.

@aaronsteers
Aaron ("AJ") Steers (aaronsteers) marked this pull request as ready for review August 2, 2026 05:27

@devin-ai-integration devin-ai-integration Bot 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.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

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.

fix(mcp): MCP Apps interactive tools are hidden over streamable HTTP but visible over stdio

2 participants