Skip to content

feat(mcp): let agents discover their own Cloud org and workspace IDs - #1094

Open
Aaron ("AJ") Steers (aaronsteers) wants to merge 2 commits into
mainfrom
devin/1785180153-cloud-mcp-org-discovery
Open

feat(mcp): let agents discover their own Cloud org and workspace IDs#1094
Aaron ("AJ") Steers (aaronsteers) wants to merge 2 commits into
mainfrom
devin/1785180153-cloud-mcp-org-discovery

Conversation

@aaronsteers

@aaronsteers Aaron ("AJ") Steers (aaronsteers) commented Jul 27, 2026

Copy link
Copy Markdown
Member

Summary

Agents connecting to Cloud MCP with only client credentials had no way to learn their own organization or workspace ID, so they had to stop and ask the user for a UUID. The API could already answer the question — GET /organizations and GET /workspaces both succeed with no ID supplied — but the MCP surface never exposed it: list_cloud_workspaces hard-required an org selector and its docstring said it "will NOT list workspaces across all organizations", and there was no organization-listing tool at all.

This PR exposes the existing capability rather than adding new API calls:

  • New CloudClient.list_organizations() returning typed CloudOrganizations, backed by the existing api_util.list_organizations_for_user(). get_organization() now filters that list instead of duplicating the call — behavior is unchanged since CloudOrganization already scopes its own credentials to organization_id in its constructor.
  • New read-only list_cloud_organizations MCP tool taking no inputs.
  • list_cloud_workspaces now takes the org-less public-API path when neither organization_id nor organization_name is given, and keeps the Config API list_by_organization_id path when one is:
client.list_workspaces(
    organization_id=(
        _resolve_organization_id(...) if organization_id or organization_name else None
    ),
    ...
)

Both discovery tools return a result model with a message field rather than raising on the two expected dead ends — zero results, and 401/403 from under-permissioned (e.g. workspace-scoped) credentials that can't read organizations. Anything else still raises.

Guidance in MCP_SERVER_INSTRUCTIONS, CLOUD_AUTH_TIP_TEXT, and AirbyteMissingWorkspaceContextError now tells agents to discover first and to ask the user to choose when discovery returns multiple candidates — never auto-select. No session/selection state is added here; this is read-only discovery only.

Verified live against Airbyte Cloud with sandbox client credentials and no org/workspace configured: list_cloud_organizations({}) returned Airbyte Team, and list_cloud_workspaces({}) returned Devin's Sandbox.

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

Summary by CodeRabbit

  • New Features

    • Added authenticated Cloud organization discovery and improved organization retrieval.
    • Added structured discovery outputs for organizations and workspaces, including optional guidance messaging.
    • Enhanced workspace discovery to search across organizations when no organization is selected.
    • Updated Cloud MCP discovery flow and selection instructions to require user choice when multiple results appear.
  • Bug Fixes

    • Improved permission-denied handling during discovery by returning empty results with actionable guidance.
    • Enhanced missing workspace context guidance for hosted and non-hosted usage.
  • Tests

    • Expanded unit coverage for Cloud client and MCP discovery behavior and permission scenarios.

Co-Authored-By: AJ Steers <aj@airbyte.io>
Copilot AI review requested due to automatic review settings July 27, 2026 19:34
@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

@devin-ai-integration
devin-ai-integration Bot marked this pull request as ready for review July 27, 2026 19:34
@github-actions

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/1785180153-cloud-mcp-org-discovery' pyairbyte --help

# Install PyAirbyte from this branch for development:
pip install 'git+https://github.com/airbytehq/PyAirbyte.git@devin/1785180153-cloud-mcp-org-discovery'

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.

@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

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

This PR exposes Airbyte Cloud organization/workspace discovery to MCP agents so they can determine their own org/workspace IDs using existing Cloud API capabilities, instead of requiring users to manually provide UUIDs.

Changes:

  • Add CloudClient.list_organizations() and refactor get_organization() to reuse it.
  • Add a new read-only MCP tool list_cloud_organizations, and enhance list_cloud_workspaces to support org-less discovery plus structured {workspaces, message} results.
  • Update server/auth/workspace-missing guidance text and unit tests to direct agents to discovery-first flows and to avoid auto-selecting among multiple candidates.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
airbyte/cloud/client.py Adds list_organizations() and reuses it in get_organization().
airbyte/mcp/cloud.py Introduces result models for discovery, adds list_cloud_organizations, and updates list_cloud_workspaces behavior + auth degradation.
airbyte/mcp/server.py Updates MCP server instructions to recommend discovery-first and explicit user selection.
airbyte/exceptions.py Updates missing-workspace guidance to instruct agents to run discovery tools first.
tests/unit_tests/test_cloud_credentials.py Adds unit coverage for organization discovery and new workspace discovery behavior/error degradation.
tests/unit_tests/test_exceptions.py Updates assertions to match the revised guidance strings.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread airbyte/mcp/cloud.py
Comment on lines 1420 to 1424
CloudWorkspaceResult(
workspace_id=ws.workspace_id,
workspace_name=ws.name,
organization_id=ws.organization_id or "",
)

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.

Agreed, fixing. Making CloudWorkspaceResult.organization_id nullable and returning None when the org is unknown, rather than coercing to "". An independent end-to-end run against Cloud flagged the same thing: the no-arg public-API path returned organization_id: "" while the org-scoped path returned the real ID, which is exactly the ambiguity you describe.

Comment thread airbyte/mcp/cloud.py Outdated
Comment on lines +1412 to +1416
message=(
"Workspace discovery is unavailable because these credentials do not "
"have permission to list workspaces. Provide a workspace ID or use "
"credentials with workspace access."
),

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.

Good catch, fixing. That except block does wrap _resolve_organization_id(), so an org-resolution 403 currently reports as a workspace-permission failure. Broadening the message to cover both organization and workspace discovery, and folding it into a shared helper so the two discovery tools can't drift.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 571892df-c1f4-41f6-9e52-7343c618c4bd

📥 Commits

Reviewing files that changed from the base of the PR and between 2096a36 and 7c9d9a7.

📒 Files selected for processing (3)
  • airbyte/mcp/cloud.py
  • airbyte/mcp/server.py
  • tests/unit_tests/test_cloud_credentials.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • airbyte/mcp/server.py
  • tests/unit_tests/test_cloud_credentials.py
  • airbyte/mcp/cloud.py

📝 Walkthrough

Walkthrough

CloudClient now exposes typed organization discovery. MCP organization and workspace tools return structured results with selection guidance and permission-error messages, while server instructions and workspace-context errors require explicit discovery before choosing a workspace.

Changes

Cloud discovery flow

Layer / File(s) Summary
Cloud organization client listing
airbyte/cloud/client.py, tests/unit_tests/test_cloud_credentials.py
CloudClient.list_organizations() maps API responses to CloudOrganization objects, and get_organization() reuses those objects.
MCP organization and workspace discovery
airbyte/mcp/cloud.py, tests/unit_tests/test_cloud_credentials.py
Organization and workspace tools return typed result models, support discovery without organization filters, and report permission failures through messages.
Discovery instructions and context guidance
airbyte/exceptions.py, airbyte/mcp/server.py, tests/unit_tests/test_exceptions.py
Hosted and non-hosted guidance requires organization/workspace discovery and explicit selection when multiple workspaces are found.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MCPClient
  participant CloudClient
  participant OrganizationsTool
  participant WorkspacesTool
  MCPClient->>OrganizationsTool: discover organizations
  OrganizationsTool->>CloudClient: list_organizations()
  CloudClient-->>OrganizationsTool: CloudOrganization objects
  OrganizationsTool-->>MCPClient: organizations and optional message
  MCPClient->>WorkspacesTool: discover workspaces
  WorkspacesTool->>CloudClient: list workspaces across organizations
  CloudClient-->>WorkspacesTool: workspace results or permission error
  WorkspacesTool-->>MCPClient: workspaces and selection guidance
Loading

Possibly related PRs

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: MCP now lets agents discover Cloud organization and workspace IDs.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch devin/1785180153-cloud-mcp-org-discovery

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 coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (2)
airbyte/mcp/cloud.py (1)

1406-1417: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate permission-error handling between the two discovery tools.

list_cloud_workspaces and list_cloud_organizations repeat the same except AirbyteError / status-code check / empty-result construction pattern. Might be worth extracting a small shared helper (e.g. returning the permission message or None) to avoid re-copying this for future discovery tools — wdyt?

Also applies to: 1450-1461

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@airbyte/mcp/cloud.py` around lines 1406 - 1417, Extract the duplicated
AirbyteError permission handling from list_cloud_workspaces and
list_cloud_organizations into a small shared helper that checks for
HTTPStatus.UNAUTHORIZED or HTTPStatus.FORBIDDEN and returns the common
permission-unavailable result or message. Update both discovery tools to reuse
that helper while preserving re-raising for other AirbyteError statuses and the
existing empty-result response.
airbyte/mcp/server.py (1)

116-119: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Minor: awkward phrasing "and then ... first".

Small wording nit in agent-facing instructions — could read more cleanly. wdyt?

✏️ Suggested rewording
-  AIRBYTE_CLOUD_WORKSPACE_ID for local or stdio connections). When no organization
-  or workspace ID is configured, call list_cloud_organizations and then
-  list_cloud_workspaces first. If multiple organizations or workspaces are
-  returned, ask the user to choose explicitly; never select automatically.
+  AIRBYTE_CLOUD_WORKSPACE_ID for local or stdio connections). When no organization
+  or workspace ID is configured, first call list_cloud_organizations, then
+  list_cloud_workspaces. If multiple organizations or workspaces are
+  returned, ask the user to choose explicitly; never select automatically.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@airbyte/mcp/server.py` around lines 116 - 119, Rewrite the agent-facing
instruction around list_cloud_organizations and list_cloud_workspaces to state
clearly that when neither ID is configured, call list_cloud_organizations
followed by list_cloud_workspaces. Preserve the requirement to ask the user when
multiple results exist and never select automatically.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@airbyte/mcp/cloud.py`:
- Around line 1384-1389: Update the docstring for the workspace-listing tool to
avoid promising cross-organization results whenever organization arguments are
omitted. Clarify that omitted arguments use the client's configured default
organization when one exists, while the public cross-organization listing is
used only when neither explicit nor default organization configuration is
present. Keep the existing behavior and references to
CloudClient.list_workspaces() unchanged.

---

Nitpick comments:
In `@airbyte/mcp/cloud.py`:
- Around line 1406-1417: Extract the duplicated AirbyteError permission handling
from list_cloud_workspaces and list_cloud_organizations into a small shared
helper that checks for HTTPStatus.UNAUTHORIZED or HTTPStatus.FORBIDDEN and
returns the common permission-unavailable result or message. Update both
discovery tools to reuse that helper while preserving re-raising for other
AirbyteError statuses and the existing empty-result response.

In `@airbyte/mcp/server.py`:
- Around line 116-119: Rewrite the agent-facing instruction around
list_cloud_organizations and list_cloud_workspaces to state clearly that when
neither ID is configured, call list_cloud_organizations followed by
list_cloud_workspaces. Preserve the requirement to ask the user when multiple
results exist and never select automatically.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 58bc6da3-791f-4587-bb90-677f28723264

📥 Commits

Reviewing files that changed from the base of the PR and between 173e779 and 2096a36.

📒 Files selected for processing (6)
  • airbyte/cloud/client.py
  • airbyte/exceptions.py
  • airbyte/mcp/cloud.py
  • airbyte/mcp/server.py
  • tests/unit_tests/test_cloud_credentials.py
  • tests/unit_tests/test_exceptions.py

Comment thread airbyte/mcp/cloud.py
@devin-ai-integration

devin-ai-integration Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

End-to-end MCP verification (headless, real server over stdio)

Ran the actual MCP server (uv run airbyte-mcp, stdio) as a subprocess and drove it over the MCP protocol with a fastmcp client, with only AIRBYTE_CLOUD_CLIENT_ID/_SECRET set and every other AIRBYTE_* var stripped from the child env (no workspace id, no org id). Read-only Cloud tools only; no Cloud resources were created, modified, or deleted.

Bare credentials → org → workspace → workspace-scoped call, no human input
tools/list: 40 tools (main: 39)
list_cloud_organizations present: True   required: []   properties: []

CALL list_cloud_organizations {} -> OK
{"organizations":[{"id":"664c690e-5263-49ba-b01f-4a6759b3330a","name":"Airbyte Team",
  "email":"helpdesk@airbyte.io","is_account_locked":false}],"message":null}

CALL list_cloud_workspaces {} -> OK           # this errored before this PR
{"workspaces":[{"workspace_id":"266ebdfe-0d7b-4540-9817-de7e4505ba61",
  "workspace_name":"Devin's Sandbox"}],"message":null}

CALL check_airbyte_cloud_workspace {"workspace_id":"266ebdfe-0d7b-4540-9817-de7e4505ba61"} -> OK
{"workspace_name":"Devin's Sandbox","organization_id":"664c690e-5263-49ba-b01f-4a6759b3330a",
 "organization_name":"Airbyte Team","subscription_status":"subscribed"}
Baseline contrast on origin/main (proves it's not a no-op)
BASELINE total tools: 39
BASELINE list_cloud_organizations present: False
BASELINE list_cloud_workspaces({}) -> ERROR
  PyAirbyteInputError: Organization ID or organization name is required.
BASELINE missing-workspace guidance (old):
  "Set the `AIRBYTE_CLOUD_WORKSPACE_ID` environment variable, or pass the `workspace_id` parameter."
Org-scoped path + new error guidance + sanity gate
CALL list_cloud_workspaces {"organization_id":"664c690e-..."} -> OK
{"workspaces":[{"workspace_id":"266ebdfe-...","workspace_name":"Devin's Sandbox",
  "organization_id":"664c690e-5263-49ba-b01f-4a6759b3330a"}]}   # Config API path intact

CALL list_deployed_cloud_connections {} -> AirbyteMissingWorkspaceContextError
  "Call `list_cloud_organizations` and `list_cloud_workspaces` first. If discovery returns
   exactly one workspace, set its ID in `AIRBYTE_CLOUD_WORKSPACE_ID` or pass the
   `workspace_id` parameter; otherwise ask the user to choose."

uv run ruff check .            -> All checks passed!
uv run pytest tests/unit_tests -> 448 passed, 1 skipped

The run independently flagged the organization_id: "" ambiguity that Copilot also raised; that is being fixed by making the field nullable.

Not covered: the hosted/HTTP transport (airbyte-mcp-http + X-Airbyte-Cloud-* headers), and the multi-org / multi-workspace and 401/403 degradation branches — the sandbox credentials see exactly one org and one workspace.


Devin session

@github-code-quality

github-code-quality Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Overview

Languages: Python

Python / code-coverage/pytest-fast

The overall coverage in commit 7c9d9a7 in the devin/1785180153-clo... 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/1785180153-clo... 7c9d9a7 +/-
airbyte/mcp/cloud.py 52% 55% +3%
airbyte/mcp/_tool_utils.py 72% 84% +12%
airbyte/mcp/registry.py 53% 70% +17%
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%

Python / code-coverage/pytest-no-creds

The overall coverage in commit 7c9d9a7 in the devin/1785180153-clo... 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/1785180153-clo... 7c9d9a7 +/-
airbyte/mcp/cloud.py 52% 55% +3%
airbyte/mcp/_tool_utils.py 72% 84% +12%
airbyte/mcp/registry.py 53% 70% +17%
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%

Python / code-coverage/pytest

The overall coverage in commit 7c9d9a7 in the devin/1785180153-clo... 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/1785180153-clo... 7c9d9a7 +/-
airbyte/mcp/cloud.py 52% 55% +3%
airbyte/mcp/_tool_utils.py 72% 84% +12%
airbyte/mcp/registry.py 53% 70% +17%
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%

Updated July 27, 2026 20:07 UTC

Co-Authored-By: AJ Steers <aj@airbyte.io>
Copilot AI review requested due to automatic review settings July 27, 2026 19:42

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

Comments suppressed due to low confidence (3)

airbyte/mcp/cloud.py:261

  • CloudWorkspaceListResult.message is used both when discovery returns no results and when discovery is unavailable due to 401/403 (permission) errors, but the field docstring only mentions the "no results" case. Update the docstring so it matches actual behavior and helps tool consumers interpret permission-denied responses.
    message: str | None = None
    """Additional guidance when discovery returns no results."""

airbyte/mcp/cloud.py:237

  • Now that CloudWorkspaceResult.organization_id is nullable, returning placeholder strings (e.g. in check_airbyte_cloud_workspace it currently uses "[unavailable - requires ORGANIZATION_READER permission]") conflicts with the field’s meaning and can look like a real org ID to agents/consumers. Consider returning None when the org is unavailable and using message/separate fields for the unavailability reason.
    organization_id: str | None
    """ID of the organization, if known and available."""

airbyte/mcp/cloud.py:88

  • In _handle_discovery_permission_error, using raise error will re-raise the exception but can lose the original traceback context compared to a bare raise. Prefer raise here so unexpected errors preserve the original stack trace for debugging.
    status_code = (error.context or {}).get("status_code")
    if status_code not in {HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN}:
        raise error

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.

2 participants