Skip to content

feat(mcp): allow MCP tools to specify a docker image / version for the connector#1020

Closed
Aaron ("AJ") Steers (aaronsteers) wants to merge 3 commits into
mainfrom
devin/1778018940-mcp-docker-image-override
Closed

feat(mcp): allow MCP tools to specify a docker image / version for the connector#1020
Aaron ("AJ") Steers (aaronsteers) wants to merge 3 commits into
mainfrom
devin/1778018940-mcp-docker-image-override

Conversation

@aaronsteers

@aaronsteers Aaron ("AJ") Steers (aaronsteers) commented May 5, 2026

Copy link
Copy Markdown
Member

Summary

Closes #1018.

The local MCP tools in airbyte/mcp/local.py previously accepted only a registry connector name (source-mssql) and silently resolved everything via the registry. As a result, MCP callers could not pin to a specific connector version, could not point at a custom-built image (e.g. airbyte/source-mssql:dev after docker build), and would always pull whatever the registry resolved at the moment of the call.

This PR exposes both compatible shapes from the issue, mirroring get_source(name=..., docker_image=..., version=...):

  1. Overloads connector_name — values containing a / are treated as docker image identifiers (e.g. airbyte/source-mssql, airbyte/source-mssql:4.4.2, airbyte/source-mssql:dev, ghcr.io/airbytehq/source-mssql:4.4.2). Image-shaped values bypass registry lookup for the executable, infer the registry name from the last path segment (with any :tag stripped), and force docker execution.
  2. Adds explicit docker_image: str | None and version: str | None keyword args — these mirror the get_source(...) parameters. When docker_image is set, docker execution is forced regardless of override_execution_mode. version pins the registry version when staying on the registry-name path and the docker image tag when the resolved image does not already include one.

Updated tools:

  • validate_connector_config
  • list_source_streams
  • get_source_stream_json_schema
  • read_source_stream_records
  • get_stream_previews
  • sync_source_to_cache

The existing destination_smoke_test already accepted docker_image, so it is unchanged.

The previous code path always passed docker_image=True to get_source(...) in docker mode, which forced the registry-name lookup before the docker override could take effect. The new _get_mcp_source resolves an explicit docker image first, decouples the registry name from the executable image, and forwards both docker_image and version through to get_source(...).

A small private helper _resolve_docker_image_and_name encapsulates the parsing rules and is unit-tested against the worked examples from the issue (registry passthrough, image identifiers with/without tags, multi-segment paths, explicit override precedence).

Review & Testing Checklist for Human

  • Verify image-shaped connector_name values still work for connectors whose registry name does NOT match the last path segment (e.g. private registries with renamed images). The current behavior treats the last path segment with any :tag stripped as the registry name; if that segment is not a registered connector, the registry lookup is silently skipped (matching get_source(docker_image=...) behavior).
  • Smoke-test the worked example from the issue: invoke validate_connector_config (and read_source_stream_records) with a pinned image like airbyte/source-mssql:4.4.2 and verify the source executes against that exact image without permission/path errors.
  • Confirm the new docker_image / version kwargs surface correctly in coral-mcp / FastMCP schemas (the _DOCKER_IMAGE_HELP / _VERSION_HELP field descriptions are what callers will see).

Notes

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

Summary by CodeRabbit

  • New Features

    • Support explicit Docker image overrides and version pinning for MCP data sources, enabling per-connector execution control.
    • MCP source tooling and endpoints now accept docker image and version parameters and surface updated field descriptions for clarity.
  • Tests

    • Added unit tests covering Docker image resolution and override precedence across connector naming scenarios.

@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 and CI monitoring

@github-actions

github-actions Bot commented May 5, 2026

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/1778018940-mcp-docker-image-override' pyairbyte --help

# Install PyAirbyte from this branch for development:
pip install 'git+https://github.com/airbytehq/PyAirbyte.git@devin/1778018940-mcp-docker-image-override'

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.

@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@devin-ai-integration[bot] has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 52 minutes and 36 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2f24c1a3-5776-4500-bafc-f0def115db44

📥 Commits

Reviewing files that changed from the base of the PR and between 406516d and f5d9c42.

📒 Files selected for processing (1)
  • airbyte/mcp/local.py
📝 Walkthrough

Walkthrough

Adds explicit docker image override and version pinning support for MCP sources, introduces user-facing help constants, extends internal source resolution to handle docker images, updates multiple public endpoints to accept/forward docker_image and version, and adds unit tests for docker-image/name resolution.

Changes

Docker Image Override and Version Pinning

Layer / File(s) Summary
Help Text Constants
airbyte/mcp/local.py
Add _CONNECTOR_NAME_HELP, _DOCKER_IMAGE_HELP, _VERSION_HELP to document connector name, docker image override, and version pinning semantics.
Name / Image Resolution
airbyte/mcp/local.py
Add _resolve_docker_image_and_name(connector_name, docker_image) to derive registry name and optional docker image from connector inputs.
Core Source Resolution / Routing
airbyte/mcp/local.py
Extend _get_mcp_source(...) to accept docker_image and version; resolve execution mode (auto/python/docker/yaml), compute final docker_image/version arguments (dropping version when image already has tag/digest), and pass parameters through to get_source.
Public Endpoint Signatures & Wiring
airbyte/mcp/local.py
Extend signatures and field descriptions to accept docker_image and version and forward them into _get_mcp_source for: validate_connector_config, list_source_streams, get_source_stream_json_schema, read_source_stream_records, get_stream_previews, sync_source_to_cache.
Unit Tests
tests/unit_tests/test_mcp_local.py
Add parametrized tests covering _resolve_docker_image_and_name across 10 scenarios (passthrough, explicit override, tags, digests, multi-segment paths, precedence).

🎯 3 (Moderate) | ⏱️ ~22 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main feature: enabling MCP tools to accept docker image and version parameters for connector execution, which is the core objective of the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 88.89% which is sufficient. The required threshold is 80.00%.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch devin/1778018940-mcp-docker-image-override

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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 and usage tips.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
airbyte/mcp/local.py (1)

803-813: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Could we resolve suggested streams with the normalized connector name instead, wdyt?

On Line 806 this still uses source_connector_name for get_connector_metadata(...). If the caller uses the new image-shaped path, registry lookup misses and we fall back to "*", so sync_source_to_cache() can sync every stream instead of the connector's suggested set.

Proposed fix
-            metadata = get_connector_metadata(
-                source_connector_name,
-            )
+            metadata = get_connector_metadata(source.name)
🤖 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/local.py` around lines 803 - 813, The call to
get_connector_metadata currently passes source_connector_name which can be the
image-shaped path and will miss registry metadata; change the call to use the
normalized connector name used elsewhere (e.g., normalized_source_connector_name
or the image reference variable source_connector_ref) so metadata lookup
succeeds; if no normalized variable exists, call the existing normalization
helper (e.g., normalize_connector_name(source_connector_name) or similar) and
pass that result to get_connector_metadata, leaving the rest of the
streams/metadata.suggested_streams logic unchanged.
🤖 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/local.py`:
- Around line 110-112: The parsing treats ":" too broadly and drops tags/digests
incorrectly; instead first strip any digest by splitting on "@" (e.g. base =
last_segment.split("@", 1)[0]) and then remove a tag by splitting on ":" only
from that digest-free base (name = base.split(":", 1)[0]); apply the same logic
when deriving the image/name from docker_image (the code paths around
last_segment, name and the branch that handles docker_image around lines
157-161) so digest-pinned refs (sha256:...) and registry:port prefixes are
preserved correctly.

---

Outside diff comments:
In `@airbyte/mcp/local.py`:
- Around line 803-813: The call to get_connector_metadata currently passes
source_connector_name which can be the image-shaped path and will miss registry
metadata; change the call to use the normalized connector name used elsewhere
(e.g., normalized_source_connector_name or the image reference variable
source_connector_ref) so metadata lookup succeeds; if no normalized variable
exists, call the existing normalization helper (e.g.,
normalize_connector_name(source_connector_name) or similar) and pass that result
to get_connector_metadata, leaving the rest of the
streams/metadata.suggested_streams logic unchanged.
🪄 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

Run ID: 340d7b04-857c-4c60-99df-d78ede7431f5

📥 Commits

Reviewing files that changed from the base of the PR and between 82bf1e4 and 44f018e.

📒 Files selected for processing (2)
  • airbyte/mcp/local.py
  • tests/unit_tests/test_mcp_local.py

Comment thread airbyte/mcp/local.py
@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown

PyTest Results (Fast Tests Only, No Creds)

353 tests  +10   353 ✅ +10   6m 1s ⏱️ -31s
  1 suites ± 0     0 💤 ± 0 
  1 files   ± 0     0 ❌ ± 0 

Results for commit f5d9c42. ± Comparison against base commit 82bf1e4.

♻️ This comment has been updated with latest results.

- Strip @sha256:... digest before stripping :tag in
  _resolve_docker_image_and_name, so refs like
  ghcr.io/org/source@sha256:abc resolve name='source'.
- Inspect only the last path segment when deciding whether to drop the
  caller-provided version, so untagged registry-port refs like
  localhost:5000/airbyte/source-mssql keep version pinning.
- Use source.name (the parsed registry name) for get_connector_metadata
  in sync_source_to_cache so suggested-stream resolution works when the
  caller passes an image-shaped connector_name.
- Add regression tests for digest, registry-port (untagged), and
  registry-port + tag image references.
@devin-ai-integration

Copy link
Copy Markdown
Contributor

Both CodeRabbit findings addressed in 406516d:

  1. Inline (_resolve_docker_image_and_name parsing of :): now strips @digest first, then :tag, and the "drop version if image is tagged" check inspects only the last /-segment so untagged registry-port refs like localhost:5000/airbyte/source-mssql keep version pinning. Replied in thread.
  2. Outside-diff (sync_source_to_cache metadata lookup): get_connector_metadata(source_connector_name) would miss when the caller passed an image-shaped connector_name, falling back to syncing every stream. Switched to get_connector_metadata(source.name) (the parsed registry name).

Regression tests added for @sha256:... digest, localhost:5000/... (untagged), and localhost:5000/...:tag (tagged) image references. Local ruff check, ruff format, pyrefly check, and the 10-case test_resolve_docker_image_and_name parametrization all pass.


Devin session

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

🧹 Nitpick comments (3)
airbyte/mcp/local.py (3)

812-820: 💤 Low value

Switching to source.name for the registry lookup looks right — small follow-up question on the failure mode, wdyt?

Using source.name here is the right move so that an image-shaped source_connector_name like airbyte/source-mssql:4.4.2 still resolves to the registry name (source-mssql) for get_connector_metadata. One tiny thing: the except Exception: branch at Line 816-817 sets streams = "*", but streams was already set to "*" on Line 813, so this is a no-op. Worth dropping to reduce confusion, or replacing with a pass/log so it's obvious the fallback is intentional?

Optional simplification
         try:
             metadata = get_connector_metadata(source.name)
         except Exception:
-            streams = "*"  # Fallback to all streams if suggested streams fail.
+            pass  # Fallback to all streams (already set above).
         else:
             if metadata is not None:
                 streams = metadata.suggested_streams or "*"
🤖 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/local.py` around lines 812 - 820, The code sets streams = "*"
before calling get_connector_metadata(source.name) and then redundantly resets
streams = "*" again in the except block; remove the no-op assignment in the
except branch and either replace it with a pass or a brief log to make the
fallback explicit. Update the try/except around
get_connector_metadata(source.name) (referenced symbols: streams,
get_connector_metadata, source.name) so the except block no longer reassigns
streams to "*" but instead leaves streams as-is (or logs the error) to avoid
confusion.

67-84: ⚡ Quick win

Help text says "mutually exclusive" but the implementation silently drops version — should we soften the wording, wdyt?

_DOCKER_IMAGE_HELP claims docker_image is "Mutually exclusive with a :tag already embedded in the image and an explicit version", but _get_mcp_source just silently sets docker_version = None when the image already has a :tag or @digest rather than raising. From a tool-author UX angle, "mutually exclusive" usually implies a hard error. Could we either raise on conflict or reword to something like "If the image already includes a :tag or @digest, the explicit version is ignored", to keep doc and behavior aligned?

🤖 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/local.py` around lines 67 - 84, The help text for
_DOCKER_IMAGE_HELP currently claims mutual exclusivity with an embedded
tag/digest and an explicit version, but _get_mcp_source silently sets
docker_version = None when the image already includes a :tag or `@digest`; update
the phrasing in _DOCKER_IMAGE_HELP (instead of asserting “Mutually exclusive…”)
to state that if the docker image already includes a :tag or `@digest`, any
explicitly provided version will be ignored (or alternatively, change
_get_mcp_source to raise on that conflict); reference the _DOCKER_IMAGE_HELP
string and the docker_version handling in _get_mcp_source when making the change
so documentation and behavior are aligned.

120-156: ⚡ Quick win

Quick sanity check: when both manifest_path and docker_image are passed, manifest wins silently — intended, wdyt?

The docstring on Line 138-139 says "when [docker_image] is set, override_execution_mode is forced to 'docker'", but at Line 153 if manifest_path: later flips it to "yaml" and the resolved docker image is never used in the yaml branch. That means a caller pinning to a specific built image while also passing a manifest will silently get yaml mode and lose the image override. Is that the desired precedence, or would it be friendlier to either raise on conflict or flip the order so an explicit docker_image wins over manifest_path?

🤖 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/local.py` around lines 120 - 156, The function _get_mcp_source
silently prefers manifest_path over docker_image (it forces
override_execution_mode="yaml" after resolving a docker image), so fix by
detecting the conflict between manifest_path and docker_image early in
_get_mcp_source (after calling _resolve_docker_image_and_name) and either raise
a clear ValueError when both are provided or explicitly document/implement the
desired precedence; e.g., if you choose to make docker_image win, set
override_execution_mode="docker" (and keep resolved_docker_image) before the
manifest_path check, or if you choose to forbid the combination, raise
ValueError mentioning manifest_path and docker_image to force the caller to
disambiguate.
🤖 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.

Nitpick comments:
In `@airbyte/mcp/local.py`:
- Around line 812-820: The code sets streams = "*" before calling
get_connector_metadata(source.name) and then redundantly resets streams = "*"
again in the except block; remove the no-op assignment in the except branch and
either replace it with a pass or a brief log to make the fallback explicit.
Update the try/except around get_connector_metadata(source.name) (referenced
symbols: streams, get_connector_metadata, source.name) so the except block no
longer reassigns streams to "*" but instead leaves streams as-is (or logs the
error) to avoid confusion.
- Around line 67-84: The help text for _DOCKER_IMAGE_HELP currently claims
mutual exclusivity with an embedded tag/digest and an explicit version, but
_get_mcp_source silently sets docker_version = None when the image already
includes a :tag or `@digest`; update the phrasing in _DOCKER_IMAGE_HELP (instead
of asserting “Mutually exclusive…”) to state that if the docker image already
includes a :tag or `@digest`, any explicitly provided version will be ignored (or
alternatively, change _get_mcp_source to raise on that conflict); reference the
_DOCKER_IMAGE_HELP string and the docker_version handling in _get_mcp_source
when making the change so documentation and behavior are aligned.
- Around line 120-156: The function _get_mcp_source silently prefers
manifest_path over docker_image (it forces override_execution_mode="yaml" after
resolving a docker image), so fix by detecting the conflict between
manifest_path and docker_image early in _get_mcp_source (after calling
_resolve_docker_image_and_name) and either raise a clear ValueError when both
are provided or explicitly document/implement the desired precedence; e.g., if
you choose to make docker_image win, set override_execution_mode="docker" (and
keep resolved_docker_image) before the manifest_path check, or if you choose to
forbid the combination, raise ValueError mentioning manifest_path and
docker_image to force the caller to disambiguate.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ef5ad448-f2e5-41c6-a8ed-41bdb8420087

📥 Commits

Reviewing files that changed from the base of the PR and between 44f018e and 406516d.

📒 Files selected for processing (2)
  • airbyte/mcp/local.py
  • tests/unit_tests/test_mcp_local.py

- _get_mcp_source now raises ValueError when both manifest_path and a
  docker image (via image-shaped connector_name or explicit docker_image)
  are supplied. Previously manifest_path silently won and the image
  override was dropped.
- Update _DOCKER_IMAGE_HELP to match actual behavior: an embedded :tag
  or @digest causes any explicit version to be ignored (not raised), and
  docker_image cannot be combined with manifest_path.
@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown

PyTest Results (Full)

423 tests  +10   405 ✅ +10   23m 36s ⏱️ - 2m 39s
  1 suites ± 0    18 💤 ± 0 
  1 files   ± 0     0 ❌ ± 0 

Results for commit f5d9c42. ± Comparison against base commit 82bf1e4.

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.

feat(mcp): allow MCP tools to specify a docker image / version for the connector (currently only registry-name lookup)

1 participant