diff --git a/airbyte/mcp/_arg_resolvers.py b/airbyte/mcp/_arg_resolvers.py index 52f600937..85a2a597c 100644 --- a/airbyte/mcp/_arg_resolvers.py +++ b/airbyte/mcp/_arg_resolvers.py @@ -113,9 +113,27 @@ def resolve_manifest_yaml(manifest_yaml: str | Path | None) -> str | Path | None if is_trusted_execution_enabled(): return Path(manifest_yaml) + if _looks_like_file_path(manifest_yaml): + raise AirbyteLibInputError( + message="`manifest_yaml` looks like a file path, which this server cannot read.", + guidance=( + "This MCP server does not run on your machine and cannot read local files. " + "Pass the manifest content inline as the `manifest_yaml` value." + ), + context={"manifest_yaml": manifest_yaml}, + ) + return manifest_yaml +def _looks_like_file_path(value: str) -> bool: + """Return True for a single-line value shaped like a manifest file path.""" + stripped = value.strip() + return stripped.lower().endswith((".yaml", ".yml", ".json")) or stripped.startswith( + ("/", "~", "./", "../") + ) + + def resolve_connector_config( # noqa: PLR0912 config: dict | str | None = None, config_file: str | Path | None = None, diff --git a/airbyte/mcp/cloud.py b/airbyte/mcp/cloud.py index e9bfb4ffd..bc1fb73d3 100644 --- a/airbyte/mcp/cloud.py +++ b/airbyte/mcp/cloud.py @@ -2991,8 +2991,8 @@ def publish_custom_source_definition( Field( description=( "The Low-code CDK manifest as inline YAML or a file path. File paths are " - "honored only when trusted execution is enabled; otherwise, the value is " - "treated as inline YAML. Required for YAML connectors." + "honored only when trusted execution is enabled; otherwise, pass the manifest " + "content inline (path-like values are rejected). Required for YAML connectors." ), default=None, ), @@ -3234,7 +3234,8 @@ def update_custom_source_definition( Field( description=( "New manifest as inline YAML or a file path. File paths are honored only when " - "trusted execution is enabled; otherwise, the value is treated as inline YAML. " + "trusted execution is enabled; otherwise, pass the manifest content inline " + "(path-like values are rejected). " "Optional; omit to update only testing values." ), default=None, diff --git a/tests/unit_tests/test_mcp_trusted_execution.py b/tests/unit_tests/test_mcp_trusted_execution.py index 75ea06a5d..1ff1fc401 100644 --- a/tests/unit_tests/test_mcp_trusted_execution.py +++ b/tests/unit_tests/test_mcp_trusted_execution.py @@ -178,7 +178,7 @@ def test_resolve_manifest_yaml_rejects_path_when_untrusted( @pytest.mark.parametrize( "manifest_yaml", [ - pytest.param("manifest.yaml", id="single-line-text"), + pytest.param("not-a-manifest", id="single-line-text"), pytest.param( '{"version": "0.1.0", "type": "DeclarativeSource"}', id="inline-json" ), @@ -196,6 +196,27 @@ def test_resolve_manifest_yaml_preserves_inline_values_when_untrusted( assert resolved == manifest_yaml +@pytest.mark.parametrize( + "manifest_yaml", + [ + pytest.param("manifest.yaml", id="relative-file"), + pytest.param("/private/tmp/session/bill_manifest.yaml", id="absolute-yaml"), + pytest.param("~/connectors/manifest.YML", id="home-yml"), + pytest.param("./manifest", id="dot-relative"), + ], +) +def test_resolve_manifest_yaml_rejects_path_like_text_when_untrusted( + monkeypatch: MonkeyPatch, + manifest_yaml: str, +) -> None: + """Untrusted execution rejects single-line path-like text and asks for inline content.""" + _set_trusted(monkeypatch, enabled=False) + with pytest.raises(AirbyteLibInputError) as exc_info: + resolve_manifest_yaml(manifest_yaml) + assert exc_info.value.guidance is not None + assert "inline" in exc_info.value.guidance + + def test_resolve_manifest_yaml_preserves_existing_path_when_trusted( monkeypatch: MonkeyPatch, tmp_path: Path, @@ -228,13 +249,13 @@ def test_resolve_manifest_yaml_does_not_read_file_when_untrusted( monkeypatch: MonkeyPatch, tmp_path: Path, ) -> None: - """A path string remains inline YAML when trusted execution is disabled.""" + """A path string is rejected, not read, when trusted execution is disabled.""" _set_trusted(monkeypatch, enabled=False) manifest_path = tmp_path / "manifest.yaml" manifest_path.write_text("version: 0.1.0\ntype: DeclarativeSource\n") - resolved = resolve_manifest_yaml(str(manifest_path)) - assert isinstance(resolved, str) - assert resolved == str(manifest_path) + with pytest.raises(AirbyteLibInputError) as exc_info: + resolve_manifest_yaml(str(manifest_path)) + assert "version: 0.1.0" not in str(exc_info.value) _UNTRUSTED_LOCAL_HELPERS: list[Callable[[], object]] = [