You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(mcp): add Connector Builder project tools for hosted connector building - #1407
👉 TL;DR: Lets an agent build and test a custom low-code source entirely through hosted Cloud MCP. The draft and the test credentials live in an Airbyte Cloud Connector Builder project, and test reads run on Airbyte Cloud, so nothing runs on the user's machine and secrets never pass through the agent.
Specifically, this adds CloudConnectorBuilderProject (airbyte/cloud/connector_builder/), CloudWorkspace.create_connector_builder_project() / get_connector_builder_project(), and 9 MCP tools: create_connector_builder_project, update_connector_builder_manifest_text, set_connector_builder_testing_values, run_connector_builder_stream_test_read, get_connector_builder_resolved_manifest, run_connector_builder_readiness_checks (readiness checks + per-stream test reads, optional full sync), list_connector_builder_published_versions, get_connector_builder_published_manifest, revert_connector_builder_published_version. All 9 are insiders_only (hidden unless AIRBYTE_MCP_INSIDERS / X-MCP-Insiders is on) until they graduate.
Changes
airbyte/_util/api_util.py: Config API helpers for connector_builder_projects/create, read_stream, full_resolve, publish, get_with_manifest (version), and declarative_source_definitions/list_manifests.
airbyte/cloud/connector_builder/ (new package; private _models.py result models and _util.py helpers, project.py the class; __init__.py exports the class and the 3 return types of its public methods: ConnectorBuilderStreamReadResult, ConnectorBuilderPageSummary, ConnectorBuilderResolvedManifest). Public CloudConnectorBuilderProject surface: builder_project_id, name, connector_builder_project_url, source_definition_id, draft_manifest_dict, draft_manifest_yaml, published_manifest_dict, replace_draft_manifest(), replace_testing_values(), run_stream_test_read(), get_resolved_manifest(), publish(pre_validate=..., description=None) (description defaults to Published by PyAirbyte at <UTC>; republishing uses Config API declarative_source_definitions/create_manifest so the description is kept), list_published_versions() → ConnectorBuilderPublishedVersion(version, is_active, description), get_published_manifest_dict(version=None) / get_published_manifest_yaml(version=None), revert_to_published_version(version, *, as_draft=True, description=None) (as_draft=True saves the version as the pending draft; False publishes it as a new active version and refuses if the draft has unpublished edits). Private (MCP-only for now, can graduate later): _get_info(), _update_draft_manifest_yaml(), _run_readiness_checks(), _get_or_create_default_test_source(), _run_full_sync(), and the readiness models (ConnectorBuilderReadinessCheck(description, result, detail) with result in passed/failed/unknown, ...).
airbyte/cloud/workspaces.py: create_connector_builder_project(), get_connector_builder_project(builder_project_id | definition_id), and a builder_project_id option on publish_custom_source_definition().
airbyte/cloud/connectors.py: private CustomCloudSourceDefinition._connector_builder_project (keeps the owning workspace for shared definitions); _unwrap_builder_manifest moved to the new module.
airbyte/mcp/connector_builder.py (new MCP module, register_connector_builder_tools, a Cloud module for policy filters): the 6 new tools, plus get_connector_builder_draft_manifest (moved from cloud.py), which now takes definition_id or builder_project_id and returns draft_manifest_yaml; publish_custom_source_definition takes an optional builder_project_id. Tools only parse args, call the project methods above, and return typed Pydantic result models (ConnectorBuilderDraftManifestResult, ConnectorBuilderProjectCreateResult, ConnectorBuilderStreamTestReadToolResult, ConnectorBuilderResolvedManifestResult, ConnectorBuilderReadinessReportResult). secret_reference:: guidance and testing_values_secret_name appear only with trusted execution (also on publish_custom_source_definition / update_custom_source_definition), the same way manifest_yaml_file does. Tools that mutate an existing project enforce the created-in-session guard (project ID or definition ID); test read, resolve and readiness report are external_access=True.
create_connector_builder_project(create_test_source=True): publishes v1, then CloudConnectorBuilderProject._get_or_create_default_test_source() creates <name> (Connector Builder test) on the definition with the non-secret testing values (deferred credentials when the spec has airbyte_secret fields or a required field without a value or default). The spec comes from the published manifest when the first publish cleared the draft. Returns definition_id, test_source_id, test_source_settings_url, so the user enters source credentials before the agent's heavy lifting.
run_connector_builder_readiness_checks(run_full_sync, test_source_id) → CloudConnectorBuilderProject._run_full_sync(): when nothing failed/pending/not-attempted (or no streams were named), checks the test source (an explicit test_source_id must be on this project's definition) and finds or creates the dev-null Connector Builder Test (No-op) destination (a same-name non-dev-null destination fails), all before publishing. Then publishes the draft (no draft: syncs the published version), selects every stream the test source discovers (streams.get_stream_properties, ignore_cache=True), reuses or creates the <name> (Connector Builder full sync) connection, and starts the sync without waiting. Returns full_sync (source_id, destination_id, destination_created, connection_id, connection_created, job_id, job_url). An on_resource_created callback registers each created destination/connection in the MCP session as soon as it exists; poll with get_cloud_sync_status.
Tests: tests/unit_tests/test_cloud_connector_builder.py (new); arg-trace fixture and trusted-execution schema test cover the new tools.
Review Spotlight
Reviewers with limited time, please review first:
apply_manifest_text_edit(): the replace_full / replace_text (exactly one match) / replace_all semantics.
CloudConnectorBuilderProject.publish(): first publish goes through Config API publish (creates v1, clears the draft); an already-published project adds a new active version via the Public API instead.
ConnectorBuilderStreamReadiness.from_stream_read(): a stream passes when it returns records with no HTTP >= 400 pages and no ERROR/FATAL logs (WARN is reported but doesn't fail).
CloudConnectorBuilderProject._run_readiness_checks(stream_names, timeout_seconds=None): reads any number of named streams in parallel (5 at a time, input order kept) at Cloud's max caps (10,000 records, 20 pages per slice, 20 slices). Each stream reports read_to_end (not test_read_limit_reached). The verdict covers only the named streams: passed means each one passed, was read to the end, and finished before the timeout. The report lists passed_streams, failed_streams, pending_streams (still reading at the timeout; results dropped, Cloud may still finish them; each read's HTTP timeout ends at the deadline) and not_attempted_streams (never started), plus checks (readiness checks derived from one project read, never stored) and next_steps (fix failures, resume with not-attempted streams, then publish and full-sync). The whole connector is validated by the full sync, not this report. After publishing, the readiness checks and tool description point to a full sync into a no-op destination (deploy_noop_destination_to_cloud, deploy_connector_to_cloud, create_connection_on_cloud, run_cloud_sync).
_redact_stream_diagnostics() in airbyte/mcp/connector_builder.py: test-read and readiness results are successful tool data, so the error middleware doesn't see them; page URLs, error bodies, logs and errors go through redact_agent_text() here.
New MCP tools in airbyte/mcp/connector_builder.py: names, arg names, and descriptions are proposals for review.
Design: one noun per state
Draft tools take builder_project_id or definition_id (exactly one). Every response returns both IDs; definition_id is null until the project is published. "Connector Builder project" is the draft workspace; "custom source definition" is the published artifact.
Risks
Config API dependency: drafts, testing values, test reads and resolve only exist on the Config API (cloud.airbyte.com/api/v1), which the platform treats as subject to change. PyAirbyte already depends on it for get_with_manifest and update_testing_values.
Draft text is a normalized YAML rendering: Cloud stores the parsed manifest, so comments and original formatting are not preserved (the Builder UI behaves the same way).
The agent and the Builder UI write the same single draft slot; concurrent edits overwrite each other.
Cloud test reads stop at 20 pages per slice and 20 slices, so large streams can't be read to the end on a draft; the report says so per stream (read_to_end: false) and won't pass. The full sync after publishing is the real end-to-end check.
Parallel reads (5 at a time) against the Cloud builder server: throughput and rate limits not measured yet.
Test source: users enter source credentials separately from Builder testing values (no Cloud route copies them). The deferred-source path for a custom definition hasn't been run live.
Not yet tested against a live Cloud workspace; only unit tests with mocked API calls.
Follow-ups
End-to-end check against a real Cloud workspace (create → edit → testing values → test read → readiness checks → publish).
Port connector-builder-mcp's YAML guidance content behind the existing docs tools.
Test plan
uv run pytest tests/unit_tests/test_cloud_connector_builder.py tests/unit_tests/test_mcp_cloud.py tests/unit_tests/test_mcp_arg_trace_parity.py tests/unit_tests/test_mcp_tool_annotations.py (touched tests pass on latest push).
ruff check, ruff format, pyrefly check, ./scripts/check_docstring_markdown.uv
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/1792350894-connector-builder-tools' pyairbyte --help
# Install PyAirbyte from this branch for development:
pip install 'git+https://github.com/airbytehq/PyAirbyte.git@devin/1792350894-connector-builder-tools'
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
/evals - Runs the Cloud MCP evals that use the MCP tools this PR changed (/evals full=true runs the full suite)
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Cross-API publishing and credential workflows need live Cloud validation and human review beyond the mocked tests.
No open issues 🎉
3 previously missed issues
In code that hasn't changed since last review
Convert documentation and OAuth metadata to API field names
airbyte/cloud/connector_builder.py:143
First publication sends this object as initialDeclarativeManifest.spec, but only connection_specification is converted to the API's camelCase keys. documentation_url and advanced_auth therefore do not populate documentationUrl and advancedAuth, leaving documentation and OAuth metadata missing from the published spec. Reuse the conversion in _builder_testing_values_spec(), which also matches the existing testing-values conversion in connectors.py.
Accept input-free connection schemas with empty properties
airbyte/cloud/connector_builder.py:666
A valid input-free connector can define connection_specification: {type: object, properties: {}}, as the test fixture does. Requiring nonempty properties marks that spec as missing and keeps asking for it in next_steps. Check whether the schema is defined instead of whether it has user inputs.
Treat empty testing configuration as valid for input-free connectors
airbyte/cloud/connector_builder.py:675
For an input-free connector, {} is a valid testing configuration. Even after set_testing_values({}) succeeds, this truthiness check marks the step incomplete and keeps telling the agent to enter testing values or secrets. Treat the step as satisfied or not applicable when the defined connection schema accepts an empty configuration; retain the missing-values check when configuration is required.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
👉 TL;DR: Lets an agent build and test a custom low-code source entirely through hosted Cloud MCP. The draft and the test credentials live in an Airbyte Cloud Connector Builder project, and test reads run on Airbyte Cloud, so nothing runs on the user's machine and secrets never pass through the agent.
Specifically, this adds
CloudConnectorBuilderProject(airbyte/cloud/connector_builder/),CloudWorkspace.create_connector_builder_project()/get_connector_builder_project(), and 9 MCP tools:create_connector_builder_project,update_connector_builder_manifest_text,set_connector_builder_testing_values,run_connector_builder_stream_test_read,get_connector_builder_resolved_manifest,run_connector_builder_readiness_checks(readiness checks + per-stream test reads, optional full sync),list_connector_builder_published_versions,get_connector_builder_published_manifest,revert_connector_builder_published_version. All 9 areinsiders_only(hidden unlessAIRBYTE_MCP_INSIDERS/X-MCP-Insidersis on) until they graduate.Changes
airbyte/_util/api_util.py: Config API helpers forconnector_builder_projects/create,read_stream,full_resolve,publish,get_with_manifest(version), anddeclarative_source_definitions/list_manifests.airbyte/cloud/connector_builder/(new package; private_models.pyresult models and_util.pyhelpers,project.pythe class;__init__.pyexports the class and the 3 return types of its public methods:ConnectorBuilderStreamReadResult,ConnectorBuilderPageSummary,ConnectorBuilderResolvedManifest). PublicCloudConnectorBuilderProjectsurface:builder_project_id,name,connector_builder_project_url,source_definition_id,draft_manifest_dict,draft_manifest_yaml,published_manifest_dict,replace_draft_manifest(),replace_testing_values(),run_stream_test_read(),get_resolved_manifest(),publish(pre_validate=..., description=None)(description defaults toPublished by PyAirbyte at <UTC>; republishing uses Config APIdeclarative_source_definitions/create_manifestso the description is kept),list_published_versions()→ConnectorBuilderPublishedVersion(version, is_active, description),get_published_manifest_dict(version=None)/get_published_manifest_yaml(version=None),revert_to_published_version(version, *, as_draft=True, description=None)(as_draft=Truesaves the version as the pending draft;Falsepublishes it as a new active version and refuses if the draft has unpublished edits). Private (MCP-only for now, can graduate later):_get_info(),_update_draft_manifest_yaml(),_run_readiness_checks(),_get_or_create_default_test_source(),_run_full_sync(), and the readiness models (ConnectorBuilderReadinessCheck(description, result, detail)withresultinpassed/failed/unknown, ...).airbyte/cloud/workspaces.py:create_connector_builder_project(),get_connector_builder_project(builder_project_id | definition_id), and abuilder_project_idoption onpublish_custom_source_definition().airbyte/cloud/connectors.py: privateCustomCloudSourceDefinition._connector_builder_project(keeps the owning workspace for shared definitions);_unwrap_builder_manifestmoved to the new module.airbyte/mcp/connector_builder.py(new MCP module,register_connector_builder_tools, a Cloud module for policy filters): the 6 new tools, plusget_connector_builder_draft_manifest(moved fromcloud.py), which now takesdefinition_idorbuilder_project_idand returnsdraft_manifest_yaml;publish_custom_source_definitiontakes an optionalbuilder_project_id. Tools only parse args, call the project methods above, and return typed Pydantic result models (ConnectorBuilderDraftManifestResult,ConnectorBuilderProjectCreateResult,ConnectorBuilderStreamTestReadToolResult,ConnectorBuilderResolvedManifestResult,ConnectorBuilderReadinessReportResult).secret_reference::guidance andtesting_values_secret_nameappear only with trusted execution (also onpublish_custom_source_definition/update_custom_source_definition), the same waymanifest_yaml_filedoes. Tools that mutate an existing project enforce the created-in-session guard (project ID or definition ID); test read, resolve and readiness report areexternal_access=True.create_connector_builder_project(create_test_source=True): publishes v1, thenCloudConnectorBuilderProject._get_or_create_default_test_source()creates<name> (Connector Builder test)on the definition with the non-secret testing values (deferred credentials when the spec hasairbyte_secretfields or a required field without a value or default). The spec comes from the published manifest when the first publish cleared the draft. Returnsdefinition_id,test_source_id,test_source_settings_url, so the user enters source credentials before the agent's heavy lifting.run_connector_builder_readiness_checks(run_full_sync, test_source_id)→CloudConnectorBuilderProject._run_full_sync(): when nothing failed/pending/not-attempted (or no streams were named), checks the test source (an explicittest_source_idmust be on this project's definition) and finds or creates the dev-nullConnector Builder Test (No-op)destination (a same-name non-dev-null destination fails), all before publishing. Then publishes the draft (no draft: syncs the published version), selects every stream the test source discovers (streams.get_stream_properties,ignore_cache=True), reuses or creates the<name> (Connector Builder full sync)connection, and starts the sync without waiting. Returnsfull_sync(source_id,destination_id,destination_created,connection_id,connection_created,job_id,job_url). Anon_resource_createdcallback registers each created destination/connection in the MCP session as soon as it exists; poll withget_cloud_sync_status.tests/unit_tests/test_cloud_connector_builder.py(new); arg-trace fixture and trusted-execution schema test cover the new tools.Review Spotlight
Reviewers with limited time, please review first:
apply_manifest_text_edit(): thereplace_full/replace_text(exactly one match) /replace_allsemantics.CloudConnectorBuilderProject.publish(): first publish goes through Config APIpublish(creates v1, clears the draft); an already-published project adds a new active version via the Public API instead.ConnectorBuilderStreamReadiness.from_stream_read(): a stream passes when it returns records with no HTTP >= 400 pages and no ERROR/FATAL logs (WARN is reported but doesn't fail).CloudConnectorBuilderProject._run_readiness_checks(stream_names, timeout_seconds=None): reads any number of named streams in parallel (5 at a time, input order kept) at Cloud's max caps (10,000 records, 20 pages per slice, 20 slices). Each stream reportsread_to_end(not test_read_limit_reached). The verdict covers only the named streams:passedmeans each one passed, was read to the end, and finished before the timeout. The report listspassed_streams,failed_streams,pending_streams(still reading at the timeout; results dropped, Cloud may still finish them; each read's HTTP timeout ends at the deadline) andnot_attempted_streams(never started), pluschecks(readiness checks derived from one project read, never stored) andnext_steps(fix failures, resume with not-attempted streams, then publish and full-sync). The whole connector is validated by the full sync, not this report. After publishing, the readiness checks and tool description point to a full sync into a no-op destination (deploy_noop_destination_to_cloud,deploy_connector_to_cloud,create_connection_on_cloud,run_cloud_sync)._redact_stream_diagnostics()inairbyte/mcp/connector_builder.py: test-read and readiness results are successful tool data, so the error middleware doesn't see them; page URLs, error bodies, logs and errors go throughredact_agent_text()here.airbyte/mcp/connector_builder.py: names, arg names, and descriptions are proposals for review.Design: one noun per state
Draft tools take
builder_project_idordefinition_id(exactly one). Every response returns both IDs;definition_idis null until the project is published. "Connector Builder project" is the draft workspace; "custom source definition" is the published artifact.Risks
cloud.airbyte.com/api/v1), which the platform treats as subject to change. PyAirbyte already depends on it forget_with_manifestandupdate_testing_values.read_to_end: false) and won't pass. The full sync after publishing is the real end-to-end check.Follow-ups
Test plan
uv run pytest tests/unit_tests/test_cloud_connector_builder.py tests/unit_tests/test_mcp_cloud.py tests/unit_tests/test_mcp_arg_trace_parity.py tests/unit_tests/test_mcp_tool_annotations.py(touched tests pass on latest push).ruff check,ruff format,pyrefly check,./scripts/check_docstring_markdown.uvLink to Devin session: https://app.devin.ai/sessions/1c8e32703d37402a9c8db8aaffe27db7
Open in Devin Desktop: https://app.devin.ai/desktop/session/1c8e32703d37402a9c8db8aaffe27db7?variant=devin
Requested by: Aaron ("AJ") Steers (@aaronsteers)