From 98d6668093e1d37d6ff0de06899a1ab75123e86b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2026 01:29:48 +0000 Subject: [PATCH 1/3] fix(mcp): return direct-access enablement link from get_agent_skill_docs Co-Authored-By: AJ Steers --- airbyte/cloud/connectors.py | 41 ++++---- airbyte/cloud/workspaces.py | 4 + airbyte/mcp/_docs_results.py | 3 + airbyte/mcp/cloud.py | 17 +++- airbyte/mcp/guidance.py | 22 ++++- tests/unit_tests/test_cloud_describe.py | 9 +- tests/unit_tests/test_cloud_direct_execute.py | 93 +++++++++++++++++++ tests/unit_tests/test_cloud_guidance.py | 7 ++ tests/unit_tests/test_mcp_cloud.py | 50 ++++++++++ 9 files changed, 219 insertions(+), 27 deletions(-) diff --git a/airbyte/cloud/connectors.py b/airbyte/cloud/connectors.py index 8db297888..282083f16 100644 --- a/airbyte/cloud/connectors.py +++ b/airbyte/cloud/connectors.py @@ -921,19 +921,16 @@ def _direct_access_guidance_id(self) -> str | None: def read_agent_skill_docs(self, *, section: str | None = None) -> DirectAccessGuidance: """Read this connector's skill docs from the Agents API without enrichment. - Sources are addressed by the `docs_skill_id` reported by Context Layer - `inspect`; SQL passthrough destinations by their conventional - `connector-destination:` skill ID. `section` is passed through unchanged - and Agents API errors (including not-enabled 403/404s) propagate. Other - destinations do not support direct access docs. + Sources are addressed by their `connector-source:` skill ID; SQL passthrough + destinations by their conventional `connector-destination:` skill ID. + `section` is passed through unchanged. Not-enabled 403/404s raise + `AirbyteExternalAccessNotEnabledError` with an `enablement_url`; when the + Context Layer API is unavailable, that error has no URL. Other errors + propagate. Other destinations do not support direct access docs. """ if self.connector_type == ConnectorType.SOURCE: - skill_id: str | None = self._direct_access_guidance_id() - if skill_id is None: - raise exc.AirbyteExternalAccessNotEnabledError( - connector_name=self.name, - connector_id=self.connector_id, - ) + self._require_context_layer_api() + skill_id = connector_docs.source_skill_id(self.connector_id) elif self.definition_id in _SQL_PASSTHROUGH_DESTINATION_DIALECTS: skill_id = connector_docs.destination_skill_id(self.connector_id) else: @@ -948,14 +945,22 @@ def read_agent_skill_docs(self, *, section: str | None = None) -> DirectAccessGu "definition_id": self.definition_id, }, ) - return DirectAccessGuidance.model_validate( - agents_api_util.read_cloud_skill_docs( - workspace_id=self.workspace.workspace_id, - skill_id=skill_id, - credentials=self.workspace._credentials, # noqa: SLF001 - section=section, + try: + return DirectAccessGuidance.model_validate( + agents_api_util.read_cloud_skill_docs( + workspace_id=self.workspace.workspace_id, + skill_id=skill_id, + credentials=self.workspace._credentials, # noqa: SLF001 + section=section, + ) ) - ) + except exc.AirbyteCloudError as error: + self._raise_if_feature_not_enabled( + error, + ConnectorFeature.DIRECT_ACCESS, + enablement_url=self._direct_access_enablement_url, + ) + raise def get_direct_access_guidance(self, *, section: str | None = None) -> DirectAccessGuidance: """Read this connector's direct-access docs, optionally scoped to a section. diff --git a/airbyte/cloud/workspaces.py b/airbyte/cloud/workspaces.py index de241ca6e..0e88b5c57 100644 --- a/airbyte/cloud/workspaces.py +++ b/airbyte/cloud/workspaces.py @@ -635,6 +635,10 @@ def get_agent_skill_docs( raise exc.AirbyteLibInputError( message="Provide exactly one of `docs_skill_id` or `connector_id`.", ) + if docs_skill_id.startswith(connector_docs.SOURCE_SKILL_PREFIX): + return self.get_source( + connector_docs.connector_id_from_skill_id(docs_skill_id) + ).read_agent_skill_docs(section=section) if docs_skill_id.startswith(connector_docs.DESTINATION_SKILL_PREFIX): destination = self.get_destination( connector_docs.connector_id_from_skill_id(docs_skill_id) diff --git a/airbyte/mcp/_docs_results.py b/airbyte/mcp/_docs_results.py index 50542a1d5..b578078f2 100644 --- a/airbyte/mcp/_docs_results.py +++ b/airbyte/mcp/_docs_results.py @@ -71,6 +71,9 @@ class AgentSkillDocsResult(BaseModel): warnings: list[str] = Field(default_factory=list) """Non-fatal issues reported while reading or rendering the docs.""" + errors: list[str] = Field(default_factory=list) + """Errors to relay to the user, for example when direct access is disabled for the connector.""" + def render_connector_docs_result(docs: DirectAccessGuidance) -> CloudConnectorDocsResult: """Render `DirectAccessGuidance` into a `CloudConnectorDocsResult`.""" diff --git a/airbyte/mcp/cloud.py b/airbyte/mcp/cloud.py index 1723055f1..7af690f55 100644 --- a/airbyte/mcp/cloud.py +++ b/airbyte/mcp/cloud.py @@ -1657,10 +1657,19 @@ def _direct_access_disabled_result( label = error.connector_name or f"{connector.connector_type} {connector.connector_id}" return ExternalApiExecuteResult( status="error", - errors=[ - f"Show the user this message as-is: Direct access is currently disabled for " - f"{label}. [Click here]({error.enablement_url}) to enable it." - ], + errors=[_direct_access_disabled_message(error, label=label)], + ) + + +def _direct_access_disabled_message( + error: AirbyteExternalAccessNotEnabledError, + *, + label: str, +) -> str: + """Return the user-facing message for disabled direct access.""" + return ( + f"Show the user this message as-is: Direct access is currently disabled for " + f"{label}. [Click here]({error.enablement_url}) to enable it." ) diff --git a/airbyte/mcp/guidance.py b/airbyte/mcp/guidance.py index 28638f5cb..ee588a747 100644 --- a/airbyte/mcp/guidance.py +++ b/airbyte/mcp/guidance.py @@ -28,11 +28,13 @@ from airbyte import exceptions as exc from airbyte._util.registry_spec import get_connector_spec_from_registry from airbyte.constants import is_hosted_mcp_mode +from airbyte.exceptions import AirbyteExternalAccessNotEnabledError from airbyte.mcp._docs_results import AgentSkillDocsResult, render_agent_skill_docs_result from airbyte.mcp._tool_utils import mcp_tool from airbyte.mcp.cloud import ( SKILL_DOCS_SECTION_HINT, WORKSPACE_ID_TIP_TEXT, + _direct_access_disabled_message, _get_cloud_workspace, ) from airbyte.registry import ( @@ -231,11 +233,25 @@ def get_agent_skill_docs( Exactly one of `connector_id` or `docs_skill_id` is required. `connector_id` must be the ID of a source or destination deployed in the workspace (for example from `list_cloud_connectors`), not a connector definition ID from the registry. + + When direct access is disabled for the connector, `errors` holds a message with an + enablement link to show the user. """ workspace: CloudWorkspace = _get_cloud_workspace(ctx, workspace_id) - return render_agent_skill_docs_result( - workspace.get_agent_skill_docs(docs_skill_id, connector_id=connector_id, section=section) - ) + try: + docs = workspace.get_agent_skill_docs( + docs_skill_id, connector_id=connector_id, section=section + ) + except AirbyteExternalAccessNotEnabledError as error: + if error.enablement_url is None: + raise + label = error.connector_name or f"connector {error.connector_id}" + return AgentSkillDocsResult( + skill_id=docs_skill_id, + content="", + errors=[_direct_access_disabled_message(error, label=label)], + ) + return render_agent_skill_docs_result(docs) class ConnectorInfo(BaseModel): diff --git a/tests/unit_tests/test_cloud_describe.py b/tests/unit_tests/test_cloud_describe.py index 49d551fd6..27b7da9b3 100644 --- a/tests/unit_tests/test_cloud_describe.py +++ b/tests/unit_tests/test_cloud_describe.py @@ -298,9 +298,10 @@ def test_direct_access_guidance_id_source_probe_not_enabled( monkeypatch: pytest.MonkeyPatch, probe_error: AirbyteCloudError, ) -> None: - """A 403/404 docs probe leaves no guidance ID; reads raise not-enabled.""" + """A 403/404 probe leaves no guidance ID and reads return an enablement URL.""" workspace = _make_workspace(monkeypatch) _patch_context_layer(monkeypatch) + monkeypatch.setattr(CloudConnector, "is_feature_enabled", lambda *_: False) monkeypatch.setattr( agents_api_util, "read_cloud_skill_docs", @@ -309,8 +310,12 @@ def test_direct_access_guidance_id_source_probe_not_enabled( source = _seed_source(workspace, "source-1", "GitHub") assert source._direct_access_guidance_id() is None # noqa: SLF001 - with pytest.raises(AirbyteExternalAccessNotEnabledError): + with pytest.raises(AirbyteExternalAccessNotEnabledError) as exc_info: source.get_direct_access_guidance() + assert ( + exc_info.value.enablement_url + == f"{source.connector_url}/enable-direct-access?skip-consent-screen=false" + ) @pytest.mark.parametrize( diff --git a/tests/unit_tests/test_cloud_direct_execute.py b/tests/unit_tests/test_cloud_direct_execute.py index 098acb1bc..d4f5be928 100644 --- a/tests/unit_tests/test_cloud_direct_execute.py +++ b/tests/unit_tests/test_cloud_direct_execute.py @@ -1655,6 +1655,99 @@ def fail(**_: Any) -> Any: # noqa: ANN401 assert "docs lookup failed" in warnings[0] +def test_read_agent_skill_docs_translates_disabled_access( + monkeypatch: pytest.MonkeyPatch, +) -> None: + workspace = _make_workspace(monkeypatch) + _patch_context_layer(monkeypatch) + source = _seed_source(workspace, "source-1", "GitHub Issues") + cloud_error = AirbyteCloudApiError(status_code=403) + + def fail_read_docs(**_: Any) -> Any: # noqa: ANN401 + raise cloud_error + + monkeypatch.setattr(agents_api_util, "read_cloud_skill_docs", fail_read_docs) + with _patch_direct_access(enabled=False): + with pytest.raises(AirbyteExternalAccessNotEnabledError) as exc_info: + source.read_agent_skill_docs() + + assert exc_info.value.connector_id == "source-1" + assert ( + exc_info.value.enablement_url + == f"{source.connector_url}/enable-direct-access?skip-consent-screen=false" + ) + + +def test_read_agent_skill_docs_propagates_cloud_error_when_enabled( + monkeypatch: pytest.MonkeyPatch, +) -> None: + workspace = _make_workspace(monkeypatch) + _patch_context_layer(monkeypatch) + source = _seed_source(workspace, "source-1", "GitHub Issues") + cloud_error = AirbyteCloudApiError(status_code=403) + + def fail_read_docs(**_: Any) -> Any: # noqa: ANN401 + raise cloud_error + + monkeypatch.setattr(agents_api_util, "read_cloud_skill_docs", fail_read_docs) + with _patch_direct_access(enabled=True): + with pytest.raises(AirbyteCloudApiError) as exc_info: + source.read_agent_skill_docs() + + assert exc_info.value is cloud_error + assert not isinstance(exc_info.value, AirbyteExternalAccessNotEnabledError) + + +def test_read_agent_skill_docs_without_context_layer_skips_docs_read( + monkeypatch: pytest.MonkeyPatch, +) -> None: + workspace = _make_workspace(monkeypatch) + _patch_context_layer(monkeypatch, available=False) + source = _seed_source(workspace, "source-1", "GitHub Issues") + calls: list[dict[str, Any]] = [] + + def read_docs(**kwargs: Any) -> dict[str, Any]: + calls.append(kwargs) + return {} + + monkeypatch.setattr(agents_api_util, "read_cloud_skill_docs", read_docs) + + with pytest.raises(AirbyteExternalAccessNotEnabledError) as exc_info: + source.read_agent_skill_docs() + + assert exc_info.value.connector_id == "source-1" + assert exc_info.value.enablement_url is None + assert calls == [] + + +def test_read_agent_skill_docs_uses_source_skill_id_and_section( + monkeypatch: pytest.MonkeyPatch, +) -> None: + workspace = _make_workspace(monkeypatch) + _patch_context_layer(monkeypatch) + source = _seed_source(workspace, "source-1", "GitHub Issues") + calls: list[dict[str, Any]] = [] + + def read_docs(**kwargs: Any) -> dict[str, Any]: + calls.append(kwargs) + return { + "metadata": { + "id": "connector-source:source-1", + "kind": "connector_source", + "title": "GitHub Issues", + }, + "content": [], + } + + monkeypatch.setattr(agents_api_util, "read_cloud_skill_docs", read_docs) + + docs = source.read_agent_skill_docs(section="actions.issues.list") + + assert docs.metadata.id == "connector-source:source-1" + assert calls[0]["skill_id"] == "connector-source:source-1" + assert calls[0]["section"] == "actions.issues.list" + + _BACKFILL_RESPONSE = { "streams": [ { diff --git a/tests/unit_tests/test_cloud_guidance.py b/tests/unit_tests/test_cloud_guidance.py index 2458e7d3f..2344856da 100644 --- a/tests/unit_tests/test_cloud_guidance.py +++ b/tests/unit_tests/test_cloud_guidance.py @@ -145,6 +145,13 @@ def fake_read_docs(**kwargs: Any) -> dict[str, Any]: @pytest.mark.parametrize( ("docs_skill_id", "connector_id", "lookup_method", "expected_lookup_id"), [ + pytest.param( + "connector-source:source-1", + None, + "get_source", + "source-1", + id="source_skill_id", + ), pytest.param( "connector-destination:destination-1", None, diff --git a/tests/unit_tests/test_mcp_cloud.py b/tests/unit_tests/test_mcp_cloud.py index 1077675d7..9b1a4b759 100644 --- a/tests/unit_tests/test_mcp_cloud.py +++ b/tests/unit_tests/test_mcp_cloud.py @@ -1840,6 +1840,56 @@ def test_get_agent_skill_docs_tool( assert "Hello" in result.content +def test_get_agent_skill_docs_tool_returns_direct_access_enablement_link( + monkeypatch: pytest.MonkeyPatch, +) -> None: + enablement_url = ( + "https://cloud.airbyte.com/x/enable-direct-access?skip-consent-screen=false" + ) + error = AirbyteExternalAccessNotEnabledError( + connector_name="GitHub", + connector_id="source-1", + enablement_url=enablement_url, + ) + workspace = SimpleNamespace(get_agent_skill_docs=MagicMock(side_effect=error)) + monkeypatch.setattr( + guidance_mcp, "_get_cloud_workspace", lambda _ctx, _id: workspace + ) + + result = guidance_mcp.get_agent_skill_docs( + None, + docs_skill_id="connector-source:source-1", + workspace_id=None, + ) + + assert result.errors == [ + "Show the user this message as-is: Direct access is currently disabled for " + "GitHub. [Click here](https://cloud.airbyte.com/x/enable-direct-access?" + "skip-consent-screen=false) to enable it." + ] + assert result.content == "" + + +def test_get_agent_skill_docs_tool_reraises_without_enablement_url( + monkeypatch: pytest.MonkeyPatch, +) -> None: + error = AirbyteExternalAccessNotEnabledError( + connector_name="GitHub", + connector_id="source-1", + ) + workspace = SimpleNamespace(get_agent_skill_docs=MagicMock(side_effect=error)) + monkeypatch.setattr( + guidance_mcp, "_get_cloud_workspace", lambda _ctx, _id: workspace + ) + + with pytest.raises(AirbyteExternalAccessNotEnabledError): + guidance_mcp.get_agent_skill_docs( + None, + docs_skill_id="connector-source:source-1", + workspace_id=None, + ) + + def test_get_agent_skill_docs_tool_connector_id( monkeypatch: pytest.MonkeyPatch, ) -> None: From f8a4610efc6b9362367ec2fda291749fc615fb42 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2026 01:47:17 +0000 Subject: [PATCH 2/3] fix(mcp): redact connector label in direct-access disabled message Co-Authored-By: AJ Steers --- airbyte/mcp/cloud.py | 2 +- tests/unit_tests/test_mcp_cloud.py | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/airbyte/mcp/cloud.py b/airbyte/mcp/cloud.py index 7af690f55..22e159a3e 100644 --- a/airbyte/mcp/cloud.py +++ b/airbyte/mcp/cloud.py @@ -1669,7 +1669,7 @@ def _direct_access_disabled_message( """Return the user-facing message for disabled direct access.""" return ( f"Show the user this message as-is: Direct access is currently disabled for " - f"{label}. [Click here]({error.enablement_url}) to enable it." + f"{redact_agent_text(label)}. [Click here]({error.enablement_url}) to enable it." ) diff --git a/tests/unit_tests/test_mcp_cloud.py b/tests/unit_tests/test_mcp_cloud.py index 9b1a4b759..d0d65cc41 100644 --- a/tests/unit_tests/test_mcp_cloud.py +++ b/tests/unit_tests/test_mcp_cloud.py @@ -1870,6 +1870,33 @@ def test_get_agent_skill_docs_tool_returns_direct_access_enablement_link( assert result.content == "" +def test_get_agent_skill_docs_tool_redacts_connector_name_and_preserves_enablement_url( + monkeypatch: pytest.MonkeyPatch, +) -> None: + enablement_url = ( + "https://cloud.airbyte.com/x/enable-direct-access?skip-consent-screen=false" + ) + error = AirbyteExternalAccessNotEnabledError( + connector_name="My source https://user:secret@host/x?token=abc", + connector_id="source-1", + enablement_url=enablement_url, + ) + workspace = SimpleNamespace(get_agent_skill_docs=MagicMock(side_effect=error)) + monkeypatch.setattr( + guidance_mcp, "_get_cloud_workspace", lambda _ctx, _id: workspace + ) + + result = guidance_mcp.get_agent_skill_docs( + None, + docs_skill_id="connector-source:source-1", + workspace_id=None, + ) + + assert "secret" not in result.errors[0] + assert "token=abc" not in result.errors[0] + assert enablement_url in result.errors[0] + + def test_get_agent_skill_docs_tool_reraises_without_enablement_url( monkeypatch: pytest.MonkeyPatch, ) -> None: From fe3f140b8da5bed3c75be34107654cdc35a61775 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2026 16:57:09 +0000 Subject: [PATCH 3/3] refactor(mcp): rename get_agent_skill_docs tool to get_external_access_skill_docs Co-Authored-By: AJ Steers --- airbyte/_direct_connectors/connector_docs.py | 2 +- airbyte/mcp/_docs_results.py | 14 ++++++----- airbyte/mcp/cloud.py | 16 ++++++------- airbyte/mcp/guidance.py | 13 ++++++---- tests/unit_tests/fixtures/arg_trace_plan.json | 12 +++++----- tests/unit_tests/test_cloud_describe.py | 6 ++--- tests/unit_tests/test_mcp_cloud.py | 24 +++++++++---------- tests/unit_tests/test_mcp_tool_utils.py | 2 +- 8 files changed, 47 insertions(+), 42 deletions(-) diff --git a/airbyte/_direct_connectors/connector_docs.py b/airbyte/_direct_connectors/connector_docs.py index d9dda8b28..33eb4b60e 100644 --- a/airbyte/_direct_connectors/connector_docs.py +++ b/airbyte/_direct_connectors/connector_docs.py @@ -4,7 +4,7 @@ Three paths serve destination docs: - `CloudWorkspace.get_agent_skill_docs` / `CloudConnector.read_agent_skill_docs` - (MCP tool `get_agent_skill_docs`) read destination skill docs straight from + (MCP tool `get_external_access_skill_docs`) read destination skill docs straight from the Agents API, with no local enrichment. - `CloudConnector.get_direct_access_guidance` (MCP tool `describe_cloud_connector` with `with_direct_access_guidance=True`) prepends a short local intro -- diff --git a/airbyte/mcp/_docs_results.py b/airbyte/mcp/_docs_results.py index b578078f2..06b8fabc3 100644 --- a/airbyte/mcp/_docs_results.py +++ b/airbyte/mcp/_docs_results.py @@ -1,7 +1,7 @@ # Copyright (c) 2025 Airbyte, Inc., all rights reserved. """Docs-result models and renderers for the Cloud MCP tools. -This module provides the `CloudConnectorDocsResult` and `AgentSkillDocsResult` +This module provides the `CloudConnectorDocsResult` and `ExternalAccessSkillDocsResult` result models plus the helpers that render `DirectAccessGuidance` into them. """ @@ -48,8 +48,8 @@ class CloudConnectorDocsResult(BaseModel): """Non-fatal issues reported while reading or rendering the docs.""" -class AgentSkillDocsResult(BaseModel): - """Docs for a single agent skill (skills for agents), returned by `get_agent_skill_docs`.""" +class ExternalAccessSkillDocsResult(BaseModel): + """Docs for a single external-access skill, returned by `get_external_access_skill_docs`.""" model_config = ConfigDict(extra="allow") @@ -87,9 +87,11 @@ def render_connector_docs_result(docs: DirectAccessGuidance) -> CloudConnectorDo ) -def render_agent_skill_docs_result(docs: DirectAccessGuidance) -> AgentSkillDocsResult: - """Render `DirectAccessGuidance` into an `AgentSkillDocsResult`.""" - return AgentSkillDocsResult( +def render_external_access_skill_docs_result( + docs: DirectAccessGuidance, +) -> ExternalAccessSkillDocsResult: + """Render `DirectAccessGuidance` into an `ExternalAccessSkillDocsResult`.""" + return ExternalAccessSkillDocsResult( skill_id=docs.metadata.id, title=docs.metadata.title, content=render_docs_content_markdown(docs.content), diff --git a/airbyte/mcp/cloud.py b/airbyte/mcp/cloud.py index 22e159a3e..0eeb75dd2 100644 --- a/airbyte/mcp/cloud.py +++ b/airbyte/mcp/cloud.py @@ -149,11 +149,11 @@ def _manifest_and_testing_values_hint() -> str | None: SKILL_DOCS_SECTION_HINT = """\ Skill doc sections for direct API queries and actions are named with the pattern `actions..`, matching the `entity_type` and `action` arguments of the -`execute_external_api_query` tool. Call `get_agent_skill_docs` with the deployed `connector_id` -(or a `docs_skill_id`) and no `section` to get the overview and list of available sections, then -retrieve the relevant section(s) before calling an action. E.g. for GitHub `entity_type='issues'`, -`action='list'`, the `'actions.issues.list'` section will teach that its expected -`api_args` keys are `owner`, `repo`, `states`, etc.\ +`execute_external_api_query` tool. Call `get_external_access_skill_docs` with the deployed +`connector_id` (or a `docs_skill_id`) and no `section` to get the overview and list of +available sections, then retrieve the relevant section(s) before calling an action. E.g. for +GitHub `entity_type='issues'`, `action='list'`, the `'actions.issues.list'` section will teach +that its expected `api_args` keys are `owner`, `repo`, `states`, etc.\ """ CONNECTOR_CHECK_FAILURE_FALLBACK = "Connector check failed without a failure message." DEFER_CREDENTIALS_TIP_TEXT = ( @@ -1693,7 +1693,7 @@ def execute_external_api_query( # noqa: PLR0913 # Explicit args mirror the con Field( description=( "The type of entity to query, for example 'issues'. Call " - "`get_agent_skill_docs` for supported entity types." + "`get_external_access_skill_docs` for supported entity types." ), ), TraceArg.VALUE, @@ -1818,7 +1818,7 @@ def _execute_external_api_action( # noqa: PLR0913 # Explicit args mirror the c Field( description=( "The type of entity to act on, for example 'issues'. Call " - "`get_agent_skill_docs` for supported entity types." + "`get_external_access_skill_docs` for supported entity types." ), ), ], @@ -1883,7 +1883,7 @@ def _execute_external_api_action( # noqa: PLR0913 # Explicit args mirror the c Creates, updates, or deletes data in the external system. - Before calling, read the connector's action docs with `get_agent_skill_docs` + Before calling, read the connector's action docs with `get_external_access_skill_docs` (or `describe_cloud_connector` with `with_direct_access_guidance=True`) to learn the entity types, actions, and required `api_args`; argument names differ per connector and are not guessable. diff --git a/airbyte/mcp/guidance.py b/airbyte/mcp/guidance.py index ee588a747..4e4e0dc76 100644 --- a/airbyte/mcp/guidance.py +++ b/airbyte/mcp/guidance.py @@ -29,7 +29,10 @@ from airbyte._util.registry_spec import get_connector_spec_from_registry from airbyte.constants import is_hosted_mcp_mode from airbyte.exceptions import AirbyteExternalAccessNotEnabledError -from airbyte.mcp._docs_results import AgentSkillDocsResult, render_agent_skill_docs_result +from airbyte.mcp._docs_results import ( + ExternalAccessSkillDocsResult, + render_external_access_skill_docs_result, +) from airbyte.mcp._tool_utils import mcp_tool from airbyte.mcp.cloud import ( SKILL_DOCS_SECTION_HINT, @@ -178,7 +181,7 @@ def test_my_tools_prompt( extra_help_text=SKILL_DOCS_SECTION_HINT, external_access=True, ) -def get_agent_skill_docs( +def get_external_access_skill_docs( ctx: Context, *, docs_skill_id: Annotated[ @@ -221,7 +224,7 @@ def get_agent_skill_docs( ), TraceArg.OMIT, ], -) -> AgentSkillDocsResult: +) -> ExternalAccessSkillDocsResult: """Read a deployed connector's direct-access docs (agent skill docs). These docs describe the entity types, actions, and arguments that @@ -246,12 +249,12 @@ def get_agent_skill_docs( if error.enablement_url is None: raise label = error.connector_name or f"connector {error.connector_id}" - return AgentSkillDocsResult( + return ExternalAccessSkillDocsResult( skill_id=docs_skill_id, content="", errors=[_direct_access_disabled_message(error, label=label)], ) - return render_agent_skill_docs_result(docs) + return render_external_access_skill_docs_result(docs) class ConnectorInfo(BaseModel): diff --git a/tests/unit_tests/fixtures/arg_trace_plan.json b/tests/unit_tests/fixtures/arg_trace_plan.json index 1a5b51832..ec92fabd2 100644 --- a/tests/unit_tests/fixtures/arg_trace_plan.json +++ b/tests/unit_tests/fixtures/arg_trace_plan.json @@ -110,12 +110,6 @@ "sql_dialect": "value str", "workspace_id": "omit" }, - "get_agent_skill_docs": { - "connector_id": "hash", - "docs_skill_id": "hash", - "section": "fingerprint", - "workspace_id": "omit" - }, "get_api_docs_urls": { "connector_name": "fingerprint" }, @@ -181,6 +175,12 @@ "workspace_id": "omit" }, "get_default_cloud_context": {}, + "get_external_access_skill_docs": { + "connector_id": "hash", + "docs_skill_id": "hash", + "section": "fingerprint", + "workspace_id": "omit" + }, "get_source_stream_json_schema": { "config": "presence", "config_file": "fingerprint", diff --git a/tests/unit_tests/test_cloud_describe.py b/tests/unit_tests/test_cloud_describe.py index 27b7da9b3..e703c17aa 100644 --- a/tests/unit_tests/test_cloud_describe.py +++ b/tests/unit_tests/test_cloud_describe.py @@ -15,7 +15,7 @@ from airbyte._direct_connectors import connector_docs from airbyte.cloud import workspaces as cloud_workspaces from airbyte.mcp._docs_results import ( - render_agent_skill_docs_result, + render_external_access_skill_docs_result, render_connector_docs_result, ) from airbyte.cloud.connections import CloudConnection @@ -422,7 +422,7 @@ def test_get_direct_access_guidance_destination_not_enabled_adds_notice( assert guidance.content[0] == {"type": "paragraph", "text": notice} assert guidance.metadata.warnings == [notice] assert render_connector_docs_result(guidance).warnings == [notice] - assert render_agent_skill_docs_result(guidance).warnings == [notice] + assert render_external_access_skill_docs_result(guidance).warnings == [notice] assert guidance.outline == [] assert any(block.get("type") == "table" for block in guidance.content) assert "execute_external_sql_query" not in _content_text(guidance) @@ -563,7 +563,7 @@ def test_get_direct_access_guidance_destination_no_context_layer_notices( assert guidance.content[0] == {"type": "paragraph", "text": notice} assert guidance.metadata.warnings == [notice] assert render_connector_docs_result(guidance).warnings == [notice] - assert render_agent_skill_docs_result(guidance).warnings == [notice] + assert render_external_access_skill_docs_result(guidance).warnings == [notice] assert guidance.outline == [] assert "execute_external_sql_query" not in _content_text(guidance) assert "SHOW TABLES" not in _content_text(guidance) diff --git a/tests/unit_tests/test_mcp_cloud.py b/tests/unit_tests/test_mcp_cloud.py index d0d65cc41..b1c7046a0 100644 --- a/tests/unit_tests/test_mcp_cloud.py +++ b/tests/unit_tests/test_mcp_cloud.py @@ -1789,7 +1789,7 @@ def test_list_custom_source_definitions_does_not_forward_organization_shared_opt ) -def test_get_agent_skill_docs_tool( +def test_get_external_access_skill_docs_tool( monkeypatch: pytest.MonkeyPatch, ) -> None: """The tool forwards `docs_skill_id`/`section` and renders the docs result.""" @@ -1818,14 +1818,14 @@ def test_get_agent_skill_docs_tool( guidance_mcp, "_get_cloud_workspace", lambda _ctx, _id: workspace ) - result = guidance_mcp.get_agent_skill_docs( + result = guidance_mcp.get_external_access_skill_docs( None, docs_skill_id="connector-source:source-1", section="setup", workspace_id=None, ) - assert isinstance(result, guidance_mcp.AgentSkillDocsResult) + assert isinstance(result, guidance_mcp.ExternalAccessSkillDocsResult) assert calls == [ { "skill_id": "connector-source:source-1", @@ -1840,7 +1840,7 @@ def test_get_agent_skill_docs_tool( assert "Hello" in result.content -def test_get_agent_skill_docs_tool_returns_direct_access_enablement_link( +def test_get_external_access_skill_docs_tool_returns_direct_access_enablement_link( monkeypatch: pytest.MonkeyPatch, ) -> None: enablement_url = ( @@ -1856,7 +1856,7 @@ def test_get_agent_skill_docs_tool_returns_direct_access_enablement_link( guidance_mcp, "_get_cloud_workspace", lambda _ctx, _id: workspace ) - result = guidance_mcp.get_agent_skill_docs( + result = guidance_mcp.get_external_access_skill_docs( None, docs_skill_id="connector-source:source-1", workspace_id=None, @@ -1870,7 +1870,7 @@ def test_get_agent_skill_docs_tool_returns_direct_access_enablement_link( assert result.content == "" -def test_get_agent_skill_docs_tool_redacts_connector_name_and_preserves_enablement_url( +def test_get_external_access_skill_docs_tool_redacts_connector_name_and_preserves_enablement_url( monkeypatch: pytest.MonkeyPatch, ) -> None: enablement_url = ( @@ -1886,7 +1886,7 @@ def test_get_agent_skill_docs_tool_redacts_connector_name_and_preserves_enableme guidance_mcp, "_get_cloud_workspace", lambda _ctx, _id: workspace ) - result = guidance_mcp.get_agent_skill_docs( + result = guidance_mcp.get_external_access_skill_docs( None, docs_skill_id="connector-source:source-1", workspace_id=None, @@ -1897,7 +1897,7 @@ def test_get_agent_skill_docs_tool_redacts_connector_name_and_preserves_enableme assert enablement_url in result.errors[0] -def test_get_agent_skill_docs_tool_reraises_without_enablement_url( +def test_get_external_access_skill_docs_tool_reraises_without_enablement_url( monkeypatch: pytest.MonkeyPatch, ) -> None: error = AirbyteExternalAccessNotEnabledError( @@ -1910,14 +1910,14 @@ def test_get_agent_skill_docs_tool_reraises_without_enablement_url( ) with pytest.raises(AirbyteExternalAccessNotEnabledError): - guidance_mcp.get_agent_skill_docs( + guidance_mcp.get_external_access_skill_docs( None, docs_skill_id="connector-source:source-1", workspace_id=None, ) -def test_get_agent_skill_docs_tool_connector_id( +def test_get_external_access_skill_docs_tool_connector_id( monkeypatch: pytest.MonkeyPatch, ) -> None: """The tool forwards `connector_id` to the workspace method.""" @@ -1940,13 +1940,13 @@ def test_get_agent_skill_docs_tool_connector_id( guidance_mcp, "_get_cloud_workspace", lambda _ctx, _id: workspace ) - result = guidance_mcp.get_agent_skill_docs( + result = guidance_mcp.get_external_access_skill_docs( None, connector_id="source-1", workspace_id=None, ) - assert isinstance(result, guidance_mcp.AgentSkillDocsResult) + assert isinstance(result, guidance_mcp.ExternalAccessSkillDocsResult) assert calls == [{"skill_id": None, "connector_id": "source-1", "section": None}] assert result.skill_id == "connector-source:source-1" diff --git a/tests/unit_tests/test_mcp_tool_utils.py b/tests/unit_tests/test_mcp_tool_utils.py index 1087696bc..25532465d 100644 --- a/tests/unit_tests/test_mcp_tool_utils.py +++ b/tests/unit_tests/test_mcp_tool_utils.py @@ -284,7 +284,7 @@ def test_server_tools_register_expected_policies() -> None: "execute_external_sql_query", "execute_external_search_query", "get_cloud_search_status", - "get_agent_skill_docs", + "get_external_access_skill_docs", ) for tool_name in external_tools: assert _TOOL_POLICIES[tool_name].external_access