Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions airbyte/mcp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@
disabled. Running and cancelling syncs are also disabled.

Set the environment variable `AIRBYTE_CLOUD_MCP_READONLY_MODE=1` to enable read-only mode.
HTTP clients that cannot send custom headers can use the shared standard filter's
`?read-only=true` URL parameter, for example
`https://mcp.airbyte.com/mcp?read-only=true`.

### Local Demo Mode

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ dependencies = [
"typing-extensions",
"uuid7>=0.1.0,<1.0",
"fastmcp>=4.0.9,<5.0",
"fastmcp-extensions>=0.35.0,<1.0.0",
"fastmcp-extensions>=0.37.0,<1.0.0",
"starlette", # Transitive dependency of fastmcp, imported directly
"httpx", # Transitive dependency of fastmcp, imported directly
"mcp", # Transitive dependency of fastmcp, imported directly
Expand Down
66 changes: 64 additions & 2 deletions tests/unit_tests/test_mcp_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,15 @@
from mcp.client.streamable_http import streamable_http_client
from starlette.middleware import Middleware

from airbyte.constants import MCP_EXTENSIONS_HEADER
from airbyte.constants import (
MCP_EXTENSIONS_HEADER,
MCP_READONLY_MODE_ENV_VAR,
)
from airbyte.mcp.server import app
from fastmcp_extensions import CapabilityTokenMiddleware
from fastmcp_extensions import DEFAULT_EXTENSIONS_HEADER
from fastmcp_extensions.capability_tokens import decode_capability_token
from fastmcp_extensions.tool_filters import ENV_READONLY_MODE


UI_EXTENSION = {"io.modelcontextprotocol/ui": {}}
Expand Down Expand Up @@ -143,6 +147,7 @@ async def _http_session(
*,
headers: dict[str, str] | None,
configure_client_capabilities: Any,
url: str = "http://testserver/mcp",
ui: bool = False,
wire_headers: list[list[tuple[bytes, bytes]]] | None = None,
response_headers: list[list[tuple[bytes, bytes]]] | None = None,
Expand All @@ -169,7 +174,7 @@ async def capture_response(response: httpx.Response) -> None:
},
) as http_client:
async with streamable_http_client(
"http://testserver/mcp",
url,
http_client=http_client,
) as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
Expand All @@ -183,6 +188,63 @@ async def _tool_names(session_factory: Any) -> set[str]:
return {tool.name for tool in result.tools}


def _http_tool_names(
*,
headers: dict[str, str] | None,
configure_client_capabilities: Any,
url: str = "http://testserver/mcp",
) -> set[str]:
return asyncio.run(
_tool_names(
_http_session(
headers=headers,
configure_client_capabilities=configure_client_capabilities,
url=url,
)
)
)


def test_http_read_only_url_param_hides_non_read_only_tools(
monkeypatch: pytest.MonkeyPatch,
configure_client_capabilities: Any,
) -> None:
"""The shared read-only filter applies to HTTP `tools/list` requests."""
monkeypatch.delenv(MCP_READONLY_MODE_ENV_VAR, raising=False)

unrestricted_names = _http_tool_names(
headers={},
configure_client_capabilities=configure_client_capabilities,
)
read_only_names = _http_tool_names(
headers={},
configure_client_capabilities=configure_client_capabilities,
url="http://testserver/mcp?read-only=true",
)

assert "run_connection_sync" in unrestricted_names
assert "run_connection_sync" not in read_only_names
assert "list_workspaces" in read_only_names
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Resolved


def test_http_read_only_url_param_cannot_disable_server_read_only_mode(
monkeypatch: pytest.MonkeyPatch,
configure_client_capabilities: Any,
) -> None:
"""A request cannot disable read-only mode enforced by the server."""
monkeypatch.setenv(ENV_READONLY_MODE, "1")
monkeypatch.delenv(MCP_READONLY_MODE_ENV_VAR, raising=False)

names = _http_tool_names(
headers={},
configure_client_capabilities=configure_client_capabilities,
url="http://testserver/mcp?read-only=false",
)

assert "run_connection_sync" not in names
assert "list_workspaces" in names


@pytest.mark.parametrize("ui", [True, False], ids=["ui-client", "non-ui-client"])
def test_stdio_ui_tool_visibility(
ui: bool,
Expand Down
8 changes: 4 additions & 4 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading