Skip to content

feat(mcp): add billing_status, subscription_status, and is_account_locked to check_airbyte_cloud_workspace MCP tool#965

Merged
Aaron ("AJ") Steers (aaronsteers) merged 16 commits intomainfrom
devin/1769624968-add-billing-status-to-mcp
Jan 28, 2026
Merged

feat(mcp): add billing_status, subscription_status, and is_account_locked to check_airbyte_cloud_workspace MCP tool#965
Aaron ("AJ") Steers (aaronsteers) merged 16 commits intomainfrom
devin/1769624968-add-billing-status-to-mcp

Conversation

@aaronsteers
Copy link
Contributor

@aaronsteers Aaron ("AJ") Steers (aaronsteers) commented Jan 28, 2026

feat(mcp): add billing status to check_airbyte_cloud_workspace tool

Summary

Enhances the check_airbyte_cloud_workspace MCP tool to expose billing status information, allowing the Coral Agent to detect when syncs are disabled due to unpaid invoices.

Adds three new fields to CloudWorkspaceResult:

  • payment_status: Organization payment status (e.g., 'okay', 'grace_period', 'disabled', 'locked')
  • subscription_status: Organization subscription status (e.g., 'pre_subscription', 'subscribed')
  • account_is_locked: Boolean indicating if the account is locked due to billing issues (defaults to False)

The billing data is fetched from the existing get_workspace_organization_info API endpoint. The payment_status and subscription_status fields gracefully degrade to None if billing info is unavailable (e.g., insufficient permissions), while account_is_locked defaults to False unless we have affirmative evidence of a locked state.

Updates since last revision

  • Renamed can_run_syncs to account_is_locked with inverted logic per code review feedback
  • account_is_locked is True only when payment_status in {'disabled', 'locked'} or subscription_status in {'unsubscribed'}
  • Changed default from None to False - assumes account is not locked unless we detect a locked state
  • Verified billing status strings match production values in OrganizationPaymentConfig.java enums

Review & Testing Checklist for Human

  • Verify account_is_locked logic is correct: Should be True when payment_status is 'disabled'/'locked' OR subscription_status is 'unsubscribed'. Defaults to False when billing info is unavailable.
  • Test with a workspace that has billing issues: Verify the tool correctly returns account_is_locked=True for a workspace with unpaid invoices (payment_status='disabled')
  • Test with workspace-scoped credentials: Verify graceful degradation when ORGANIZATION_READER permission is not available (billing fields should be None, account_is_locked should be False)

Suggested test plan: Use the MCP tool against a test workspace with known billing status and verify the returned values match expectations.

Notes

Summary by CodeRabbit

  • New Features

    • Workspace and organization views now include payment status, subscription status, and an "account locked" flag.
    • Organization objects support lazy on‑demand loading of billing and contact information.
  • Behavior

    • Sync eligibility now accounts for billing state; locked/disabled payments or unsubscribed status can block syncs.
    • Billing retrieval is tolerant of permission/API failures—unavailable billing fields remain blank without breaking views.
  • Documentation

    • Workspace docs updated to include billing and sync‑eligibility details.

✏️ Tip: You can customize this high-level summary in your review settings.

Enhances the check_airbyte_cloud_workspace MCP tool to include billing
status information in its response:
- payment_status: Organization payment status (okay, grace_period, disabled, locked)
- subscription_status: Organization subscription status (pre_subscription, subscribed)
- can_run_syncs: Boolean indicating if syncs can run based on billing status

This allows the Coral Agent to detect when syncs are disabled due to
unpaid invoices and provide appropriate context to users.

Co-Authored-By: AJ Steers <aj@airbyte.io>
@devin-ai-integration
Copy link
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
Copy link

👋 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/1769624968-add-billing-status-to-mcp' pyairbyte --help

# Install PyAirbyte from this branch for development:
pip install 'git+https://github.com/airbytehq/PyAirbyte.git@devin/1769624968-add-billing-status-to-mcp'

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
Copy link
Contributor

The intent here is to fail gracefully - if billing info can't be fetched (e.g., due to permissions or API issues), we want to continue with None values rather than breaking the tool.

I can add logging here for debuggability. The pattern already exists in this codebase (see airbyte/mcp/registry.py which uses logger = logging.getLogger("airbyte.mcp")).

Aaron ("AJ") Steers (@aaronsteers) - Would you like me to add logging here, or is the silent fallback acceptable for this use case? Happy to add it if you prefer the extra debuggability.

@aaronsteers Aaron ("AJ") Steers (aaronsteers) marked this pull request as ready for review January 28, 2026 18:34
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 28, 2026

📝 Walkthrough
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding billing status fields to the check_airbyte_cloud_workspace MCP tool.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch devin/1769624968-add-billing-status-to-mcp

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.

Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View issue and 3 additional flags in Devin Review.

Open in Devin Review

Fix bug where can_run_syncs was only computed when BOTH payment_status
and subscription_status were present. Now correctly computes the value
when either status is available, ensuring syncs are blocked if
payment_status is 'disabled'/'locked' even when subscription_status
is unavailable.

Co-Authored-By: AJ Steers <aj@airbyte.io>
@devin-ai-integration
Copy link
Contributor

Good catch! I've fixed this in commit 49c2e0f. The logic now correctly computes can_run_syncs when either status is available:

if payment_status or subscription_status:
    blocked_by_payment = payment_status in {"disabled", "locked"}
    blocked_by_subscription = subscription_status == "unsubscribed"
    can_run_syncs = not (blocked_by_payment or blocked_by_subscription)

This ensures that if payment_status='disabled' but subscription_status=None, can_run_syncs will correctly be False.

- Add LOCKED_PAYMENT_STATUSES and LOCKED_SUBSCRIPTION_STATUSES constants
- Simplify can_run_syncs logic per code review feedback
- Add comment to explain billing lookup failure handling

Co-Authored-By: AJ Steers <aj@airbyte.io>
@devin-ai-integration
Copy link
Contributor

Aaron ("AJ") Steers (@aaronsteers) Done! Simplified the logic with constants as you suggested:

LOCKED_PAYMENT_STATUSES: set[str] = {"disabled", "locked"}
LOCKED_SUBSCRIPTION_STATUSES: set[str] = {"unsubscribed"}

can_run_syncs = (
    payment_status
    and payment_status not in LOCKED_PAYMENT_STATUSES
) or (
    subscription_status
    and subscription_status not in LOCKED_SUBSCRIPTION_STATUSES
)

Also added a comment to the except block to address the CodeQL warning about empty except clauses.

Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View issue and 6 additional flags in Devin Review.

Open in Devin Review

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@airbyte/mcp/cloud.py`:
- Around line 576-580: The current can_run_syncs boolean uses OR and therefore
allows syncs if either payment_status or subscription_status is non-locked;
change the logic to require both statuses to be non-blocking by using AND:
ensure can_run_syncs evaluates to True only when (payment_status is missing or
not in LOCKED_PAYMENT_STATUSES) AND (subscription_status is missing or not in
LOCKED_SUBSCRIPTION_STATUSES); update the inline comment near can_run_syncs to
reflect that both payment and subscription must permit syncs.

Update docstring to accurately describe the OR logic behavior:
syncs can run if EITHER status is valid, and null status doesn't
block syncs on its own.

Co-Authored-By: AJ Steers <aj@airbyte.io>
@github-actions
Copy link

github-actions bot commented Jan 28, 2026

PyTest Results (Fast Tests Only, No Creds)

326 tests  ±0   326 ✅ ±0   5m 37s ⏱️ -1s
  1 suites ±0     0 💤 ±0 
  1 files   ±0     0 ❌ ±0 

Results for commit c564c9a. ± Comparison against base commit 8a43e06.

♻️ This comment has been updated with latest results.

- Remove try/except block
- Remove nested if statement for billing check
- Compute can_run_syncs unconditionally based on available status values

Co-Authored-By: AJ Steers <aj@airbyte.io>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@airbyte/mcp/cloud.py`:
- Around line 574-577: The computed can_run_syncs currently evaluates to False
when both payment_status and subscription_status are None, but the type allows
None to represent "unknown"; change the logic so can_run_syncs is set to None
when billing info is unavailable (both payment_status and subscription_status
are None), otherwise compute the boolean as before using the existing expression
that checks against LOCKED_PAYMENT_STATUSES and LOCKED_SUBSCRIPTION_STATUSES
(refer to the can_run_syncs variable and the payment_status/subscription_status
symbols).

Remove if block and use ternary expression with else {} as requested.

Co-Authored-By: AJ Steers <aj@airbyte.io>
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View issue and 3 additional flags in Devin Review.

Open in Devin Review

Use suppress(Exception) to handle potential API failures gracefully
when fetching organization info for billing status.

Co-Authored-By: AJ Steers <aj@airbyte.io>
Co-Authored-By: AJ Steers <aj@airbyte.io>
@github-actions
Copy link

github-actions bot commented Jan 28, 2026

PyTest Results (Full)

396 tests  ±0   379 ✅ ±0   23m 24s ⏱️ -52s
  1 suites ±0    17 💤 ±0 
  1 files   ±0     0 ❌ ±0 

Results for commit c564c9a. ± Comparison against base commit 8a43e06.

♻️ This comment has been updated with latest results.

devin-ai-integration bot and others added 2 commits January 28, 2026 20:33
@aaronsteers Aaron ("AJ") Steers (aaronsteers) changed the title feat(mcp): add billing status to check_airbyte_cloud_workspace tool feat(mcp): add billing_status, subscription_status, and is_account_locked to check_airbyte_cloud_workspace MCP tool Jan 28, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@airbyte/mcp/cloud.py`:
- Around line 171-182: Docstring for subscription_status omits the
'unsubscribed' value even though account_is_locked logic treats 'unsubscribed'
as locking; update the subscription_status docstring to include 'unsubscribed'
(e.g., "pre_subscription', 'subscribed', 'unsubscribed'") and ensure the
account_is_locked docstring references the same values consistently (mentioning
that 'unsubscribed' contributes to a locked state alongside payment_status
values like 'disabled'/'locked').
🧹 Nitpick comments (1)
airbyte/mcp/cloud.py (1)

42-44: Clarify the tuple rationale comment?

The “unhashable types” note feels a bit off for string constants. Would you consider rewording it to emphasize immutability or just drop the note for clarity, wdyt?

devin-ai-integration bot and others added 2 commits January 28, 2026 20:47
Co-Authored-By: AJ Steers <aj@airbyte.io>
devin-ai-integration bot and others added 2 commits January 28, 2026 21:05
…ribe_cloud_organization

- Move LOCKED_PAYMENT_STATUSES and LOCKED_SUBSCRIPTION_STATUSES constants to api_util.py
- Add is_account_locked() utility function to api_util.py
- Add get_organization_info() function to api_util.py for direct org billing lookup
- Add billing fields (payment_status, subscription_status, account_is_locked) to CloudOrganizationResult
- Update describe_cloud_organization to fetch and return billing info
- Update check_airbyte_cloud_workspace to use common is_account_locked() utility

Co-Authored-By: AJ Steers <aj@airbyte.io>
- Convert CloudOrganization from dataclass to class with lazy loading
- Add payment_status, subscription_status, account_is_locked properties
- Update CloudWorkspace.get_organization() to pass credentials to CloudOrganization
- Update _resolve_organization to return CloudOrganization with credentials
- Update describe_cloud_organization MCP tool to use CloudOrganization properties directly

Co-Authored-By: AJ Steers <aj@airbyte.io>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVED

- Add _organization_info_fetch_failed flag to remember when fetching fails
- Return empty dict on failure instead of retrying on each property access
- Simplify property implementations by removing redundant try/except blocks
- Properties now return None gracefully when billing info is unavailable

Co-Authored-By: AJ Steers <aj@airbyte.io>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@airbyte/cloud/workspaces.py`:
- Around line 134-139: The early-return checks block re-fetching when
_organization_info_fetch_failed is True even if force_refresh is requested;
update the logic in the method that uses force_refresh,
_organization_info_fetch_failed, and _organization_info so that when
force_refresh is True you clear or reset _organization_info_fetch_failed (or run
the fetch path regardless) before returning; in short, check force_refresh first
(or set self._organization_info_fetch_failed = False when force_refresh is True)
so a forced refresh will actually attempt to re-fetch rather than immediately
returning {}.
🧹 Nitpick comments (1)
airbyte/cloud/workspaces.py (1)

149-152: Consider adding debug logging when fetch fails?

The silent failure is great for graceful degradation, but it might make debugging tricky when something goes wrong. Would adding a debug-level log message help with troubleshooting, wdyt?

except Exception as e:
    # Consider: import logging; logger = logging.getLogger(__name__)
    # logger.debug("Failed to fetch organization info: %s", e)
    self._organization_info_fetch_failed = True
    return {}

When force_refresh=True is passed to _fetch_organization_info(), reset
the _organization_info_fetch_failed flag so that a forced refresh will
actually attempt to re-fetch rather than returning empty dict.

Co-Authored-By: AJ Steers <aj@airbyte.io>
@aaronsteers Aaron ("AJ") Steers (aaronsteers) merged commit cd5e6a0 into main Jan 28, 2026
21 checks passed
@aaronsteers Aaron ("AJ") Steers (aaronsteers) deleted the devin/1769624968-add-billing-status-to-mcp branch January 28, 2026 22:51
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