airbyte_ops_mcp.mcp.organization_admin
MCP tools for organization administration: is_agentic flag, payment config, and customer-tier lookup/cache.
MCP reference
MCP primitives registered by the organization_admin module of the airbyte-internal-ops server: 7 tool(s), 0 prompt(s), 0 resource(s).
Tools (7)
get_customer_tier_cache_stats
Hints: read-only · idempotent
Get current statistics about the customer tier cache.
Returns cache size, age, and file paths for both the tier cache (org -> tier) and workspace cache (workspace -> org + region).
Useful for checking cache freshness and diagnosing issues.
Parameters:
_No parameters._
Show input JSON schema
{
"additionalProperties": false,
"properties": {},
"type": "object"
}
Show output JSON schema
{
"description": "Statistics about the tier cache state.",
"properties": {
"tier_cache_size": {
"description": "Number of orgs in the tier cache",
"type": "integer"
},
"workspace_cache_size": {
"description": "Number of workspaces in the workspace cache",
"type": "integer"
},
"tier_cache_age_seconds": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "Age of the tier cache in seconds (None if not cached)"
},
"tier_export_age_seconds": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "Age of the underlying tier export in seconds"
},
"tier_export_row_count": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "Number of organization rows in the tier export"
},
"workspace_cache_age_seconds": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "Age of the workspace cache in seconds (None if not cached)"
},
"tier_cache_path": {
"description": "Path to the tier cache file",
"type": "string"
},
"workspace_cache_path": {
"description": "Path to the workspace cache file",
"type": "string"
}
},
"required": [
"tier_cache_size",
"workspace_cache_size",
"tier_cache_path",
"workspace_cache_path"
],
"type": "object"
}
get_organization_agentic_flag
Hints: idempotent · open-world
Get the current is_agentic flag for one or more organizations.
Parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
organization_ids |
string | array<string> |
yes | — | One organization UUID, or a list of organization UUIDs. |
config_api_root |
string | null |
no | null |
Optional Config API root URL override. When omitted, the tool reads from the Prod DB replica. |
Show input JSON schema
{
"additionalProperties": false,
"properties": {
"organization_ids": {
"anyOf": [
{
"type": "string"
},
{
"items": {
"type": "string"
},
"type": "array"
}
],
"description": "One organization UUID, or a list of organization UUIDs."
},
"config_api_root": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Optional Config API root URL override. When omitted, the tool reads from the Prod DB replica."
}
},
"required": [
"organization_ids"
],
"type": "object"
}
Show output JSON schema
{
"description": "Current managed agentic organization status for one or more organizations.",
"properties": {
"organizations": {
"description": "Organizations that were found.",
"items": {
"description": "Current managed agentic organization status for an organization.",
"properties": {
"organization_id": {
"description": "The organization UUID",
"type": "string"
},
"organization_name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "The display name of the organization"
},
"email": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Organization contact email (may be absent)"
},
"tombstone": {
"description": "Whether the organization is tombstoned",
"type": "boolean"
},
"is_agentic": {
"description": "Whether the organization is managed by the Airbyte Agents product. `False` means a standard Cloud org; `True` means an org managed via the app.agents.ai interfaces.",
"type": "boolean"
},
"customer_tier": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Customer tier of the organization (TIER_0, TIER_1, TIER_2, UNKNOWN)"
},
"tier_warning": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Warning message if the organization is a sensitive customer tier"
},
"warnings": {
"description": "Warnings raised while resolving this result.",
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"organization_id",
"tombstone",
"is_agentic"
],
"type": "object"
},
"type": "array"
},
"missing_organization_ids": {
"description": "Requested organization IDs that were not found.",
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"organizations"
],
"type": "object"
}
get_organization_payment_config
Hints: read-only · idempotent · open-world
Get the current payment configuration for an organization.
Returns payment status, subscription status, grace period info,
usage category override, and current Orb billing plan (when
ORB_API_KEY is configured). No PII or sensitive payment details
are included in the response.
Authentication credentials are resolved in priority order:
- Bearer token (Authorization header or AIRBYTE_CLOUD_BEARER_TOKEN env var)
- HTTP headers: X-Airbyte-Cloud-Client-Id, X-Airbyte-Cloud-Client-Secret
- Environment variables: AIRBYTE_CLOUD_CLIENT_ID, AIRBYTE_CLOUD_CLIENT_SECRET
Parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
organization_id |
string |
yes | — | The organization UUID. |
config_api_root |
string | null |
no | null |
Optional Config API root URL override. Defaults to Airbyte Cloud (https://cloud.airbyte.com/api/v1). |
Show input JSON schema
{
"additionalProperties": false,
"properties": {
"organization_id": {
"description": "The organization UUID.",
"type": "string"
},
"config_api_root": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Optional Config API root URL override. Defaults to Airbyte Cloud (`https://cloud.airbyte.com/api/v1`)."
}
},
"required": [
"organization_id"
],
"type": "object"
}
Show output JSON schema
{
"description": "Current payment configuration for an organization.\n\nReturned by the `GET /api/v1/organization_payment_config/{organizationId}` endpoint.",
"properties": {
"organization_id": {
"description": "The organization UUID",
"type": "string"
},
"payment_status": {
"description": "Payment status: `uninitialized`, `okay`, `grace_period`, `disabled`, `locked`, or `manual`",
"type": "string"
},
"subscription_status": {
"description": "Subscription status: `pre_subscription`, `subscribed`, or `unsubscribed`",
"type": "string"
},
"payment_provider_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "External payment provider ID (e.g. Stripe customer ID)"
},
"grace_period_end_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "ISO 8601 datetime when the grace period ends (if active)"
},
"usage_category_overwrite": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Usage category override: `free` or `internal` (if set)"
},
"customer_tier": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Customer tier of the organization (TIER_0, TIER_1, TIER_2, UNKNOWN)"
},
"tier_warning": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Warning message if the organization is a sensitive customer tier"
},
"warnings": {
"description": "Warnings raised while resolving this result.",
"items": {
"type": "string"
},
"type": "array"
},
"orb_subscription": {
"anyOf": [
{
"description": "Summary of an Orb billing subscription for an organization.",
"properties": {
"subscription_id": {
"description": "The Orb subscription ID",
"type": "string"
},
"status": {
"description": "Subscription status (e.g. `active`, `ended`)",
"type": "string"
},
"plan_name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Display name of the Orb plan (e.g. `Airbyte Partner`)"
},
"plan_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Orb internal plan ID"
},
"external_plan_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "External plan ID configured in Orb"
},
"start_date": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "ISO 8601 date when the subscription started"
},
"end_date": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "ISO 8601 date when the subscription ends (if applicable)"
},
"orb_customer_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Orb internal customer ID"
}
},
"required": [
"subscription_id",
"status"
],
"type": "object"
},
{
"type": "null"
}
],
"default": null,
"description": "Current Orb billing subscription info (if `ORB_API_KEY` is configured)"
}
},
"required": [
"organization_id",
"payment_status",
"subscription_status"
],
"type": "object"
}
lookup_customer_tiers
Hints: read-only · idempotent
Look up customer tier classification for organizations, workspaces, and/or connections.
Accepts mixed lists of organization IDs, workspace IDs, and connection IDs. Resolves each to its organization and maps to a customer tier (TIER_0, TIER_1, TIER_2, or UNKNOWN).
The GCS export contains one row per Cloud organization, including explicit Tier 2 rows. Any org not in the export resolves to UNKNOWN.
Returns enriched entries with tier, region (EU/US), and a summary of the distribution.
Parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
organization_ids |
array<string> | null |
no | null |
List of organization UUIDs to look up tiers for. Example: ['664c690e-5263-49ba-b01f-4a6759b3330a'] |
workspace_ids |
array<string> | null |
no | null |
List of workspace UUIDs to look up tiers for. Each workspace will be resolved to its organization and tier. Example: ['266ebdfe-0d7b-4540-9817-de7e4505ba61'] |
connection_ids |
array<string> | null |
no | null |
List of connection UUIDs to look up tiers for. Each connection will be resolved to its workspace, organization, and tier. |
Show input JSON schema
{
"additionalProperties": false,
"properties": {
"organization_ids": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"description": "List of organization UUIDs to look up tiers for. Example: ['664c690e-5263-49ba-b01f-4a6759b3330a']"
},
"workspace_ids": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"description": "List of workspace UUIDs to look up tiers for. Each workspace will be resolved to its organization and tier. Example: ['266ebdfe-0d7b-4540-9817-de7e4505ba61']"
},
"connection_ids": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"description": "List of connection UUIDs to look up tiers for. Each connection will be resolved to its workspace, organization, and tier."
}
},
"type": "object"
}
Show output JSON schema
{
"description": "Result of a customer tier lookup across multiple IDs.",
"properties": {
"entries": {
"description": "Individual tier lookup results for each input ID",
"items": {
"description": "A single resolved tier entry for an org, workspace, or connection.",
"properties": {
"input_id": {
"description": "The original ID that was looked up",
"type": "string"
},
"input_type": {
"description": "Type of the input ID: 'organization', 'workspace', or 'connection'",
"type": "string"
},
"organization_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Resolved organization UUID"
},
"workspace_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Workspace UUID (if input was workspace or connection)"
},
"connection_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Connection UUID (if input was connection)"
},
"customer_tier": {
"description": "Resolved tier: TIER_0, TIER_1, TIER_2, or UNKNOWN",
"enum": [
"TIER_0",
"TIER_1",
"TIER_2",
"UNKNOWN"
],
"type": "string"
},
"dataplane_name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Dataplane region name (e.g., 'US', 'EU')"
},
"is_eu": {
"default": false,
"description": "Whether the entity is in the EU region",
"type": "boolean"
},
"resolved": {
"default": true,
"description": "Whether the ID was successfully resolved",
"type": "boolean"
},
"warnings": {
"description": "Warnings raised while resolving this result.",
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"input_id",
"input_type",
"customer_tier"
],
"type": "object"
},
"type": "array"
},
"summary": {
"description": "Tier distribution summary across all resolved entries",
"properties": {
"tier_0_count": {
"default": 0,
"description": "Number of TIER_0 entries",
"type": "integer"
},
"tier_1_count": {
"default": 0,
"description": "Number of TIER_1 entries",
"type": "integer"
},
"tier_2_count": {
"default": 0,
"description": "Number of TIER_2 entries",
"type": "integer"
},
"unknown_count": {
"default": 0,
"description": "Number of UNKNOWN entries",
"type": "integer"
},
"total": {
"default": 0,
"description": "Total number of entries",
"type": "integer"
},
"warnings": {
"description": "Warnings raised while building this summary.",
"items": {
"type": "string"
},
"type": "array"
}
},
"type": "object"
},
"summary_text": {
"description": "Human-readable tier distribution summary",
"type": "string"
},
"warnings": {
"description": "Warnings raised while resolving this result.",
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"entries",
"summary",
"summary_text"
],
"type": "object"
}
refresh_customer_tier_cache
Hints: idempotent
Force-refresh the customer tier cache from the GCS export.
The tier cache is automatically refreshed every 24 hours. Use this tool to manually trigger a refresh if you need the latest tier data immediately (e.g., after a Salesforce update).
Returns cache statistics after the refresh.
Parameters:
_No parameters._
Show input JSON schema
{
"additionalProperties": false,
"properties": {},
"type": "object"
}
Show output JSON schema
{
"description": "Result of refreshing the tier cache.",
"properties": {
"stats": {
"description": "Cache statistics after refresh",
"properties": {
"tier_cache_size": {
"description": "Number of orgs in the tier cache",
"type": "integer"
},
"workspace_cache_size": {
"description": "Number of workspaces in the workspace cache",
"type": "integer"
},
"tier_cache_age_seconds": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "Age of the tier cache in seconds (None if not cached)"
},
"tier_export_age_seconds": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "Age of the underlying tier export in seconds"
},
"tier_export_row_count": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "Number of organization rows in the tier export"
},
"workspace_cache_age_seconds": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"description": "Age of the workspace cache in seconds (None if not cached)"
},
"tier_cache_path": {
"description": "Path to the tier cache file",
"type": "string"
},
"workspace_cache_path": {
"description": "Path to the workspace cache file",
"type": "string"
}
},
"required": [
"tier_cache_size",
"workspace_cache_size",
"tier_cache_path",
"workspace_cache_path"
],
"type": "object"
},
"message": {
"description": "Human-readable result message",
"type": "string"
}
},
"required": [
"stats",
"message"
],
"type": "object"
}
update_organization_agentic_flag
Hints: destructive · idempotent · open-world
Update the is_agentic flag for one or more organizations.
All updates require human-in-the-loop approval via escalate_to_human.
The tool validates each organization identity before writing the flag.
Parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
organization_ids |
string | array<string> |
yes | — | One organization UUID, or a list of organization UUIDs. |
is_agentic |
boolean |
yes | — | The desired is_agentic value. |
approval_comment_url |
string |
yes | — | URL to the Slack approval record. Obtain this by calling escalate_to_human with approval_requested=True; the backend delivers the approval record URL when a human clicks Approve. |
organization_name |
string | null |
no | null |
Confirmation of the target organization. Accepts the organization name, email address, or email domain. Required when updating a single organization. Ignored for multi-org updates; use organization_names instead. |
organization_names |
object | null |
no | null |
Per-organization confirmation for multi-org updates. Keys are organization UUIDs; values may be organization names, email addresses, or email domains. |
customer_tier_filter |
enum("TIER_0", "TIER_1", "TIER_2", "UNKNOWN", "ALL") |
no | "TIER_2" |
Required tier filter: 'TIER_0', 'TIER_1', 'TIER_2', 'UNKNOWN', or 'ALL'. The operation is rejected if the actual customer tier does not match. Use 'ALL' to proceed regardless of tier after human approval. |
Show input JSON schema
{
"additionalProperties": false,
"properties": {
"organization_ids": {
"anyOf": [
{
"type": "string"
},
{
"items": {
"type": "string"
},
"type": "array"
}
],
"description": "One organization UUID, or a list of organization UUIDs."
},
"is_agentic": {
"description": "The desired `is_agentic` value.",
"type": "boolean"
},
"approval_comment_url": {
"description": "URL to the Slack approval record. Obtain this by calling `escalate_to_human` with `approval_requested=True`; the backend delivers the approval record URL when a human clicks Approve.",
"type": "string"
},
"organization_name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Confirmation of the target organization. Accepts the organization name, email address, or email domain. Required when updating a single organization. Ignored for multi-org updates; use `organization_names` instead."
},
"organization_names": {
"anyOf": [
{
"additionalProperties": {
"type": "string"
},
"type": "object"
},
{
"type": "null"
}
],
"default": null,
"description": "Per-organization confirmation for multi-org updates. Keys are organization UUIDs; values may be organization names, email addresses, or email domains."
},
"customer_tier_filter": {
"default": "TIER_2",
"description": "Required tier filter: 'TIER_0', 'TIER_1', 'TIER_2', 'UNKNOWN', or 'ALL'. The operation is rejected if the actual customer tier does not match. Use 'ALL' to proceed regardless of tier after human approval.",
"enum": [
"TIER_0",
"TIER_1",
"TIER_2",
"UNKNOWN",
"ALL"
],
"type": "string"
}
},
"required": [
"organization_ids",
"is_agentic",
"approval_comment_url"
],
"type": "object"
}
Show output JSON schema
{
"description": "Result of updating one or more managed agentic organization statuses.",
"properties": {
"success": {
"description": "Whether every requested update succeeded",
"type": "boolean"
},
"message": {
"description": "Human-readable message describing the result",
"type": "string"
},
"results": {
"description": "Per-organization update results.",
"items": {
"description": "Result of a managed agentic organization status update.",
"properties": {
"success": {
"description": "Whether the operation succeeded",
"type": "boolean"
},
"message": {
"description": "Human-readable message describing the result",
"type": "string"
},
"organization_id": {
"description": "The organization UUID",
"type": "string"
},
"organization_name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "The display name of the organization"
},
"email": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Organization contact email (may be absent)"
},
"previous_is_agentic": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"default": null,
"description": "The managed agentic organization status before the update."
},
"new_is_agentic": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"default": null,
"description": "The managed agentic organization status after the update."
},
"customer_tier": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Customer tier of the organization (TIER_0, TIER_1, TIER_2, UNKNOWN)"
},
"tier_warning": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Warning message if the organization is a sensitive customer tier"
},
"warnings": {
"description": "Warnings raised by this operation.",
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"success",
"message",
"organization_id"
],
"type": "object"
},
"type": "array"
}
},
"required": [
"success",
"message",
"results"
],
"type": "object"
}
update_organization_payment_config
Hints: destructive · open-world
Update the payment configuration for an organization.
All updates require human-in-the-loop approval via escalate_to_human.
Use set_grace_period to start, extend, or cancel a grace period.
If the org is not already in manual status, the tool automatically
transitions to manual first before setting the grace period.
Use set_permanent_waiver_type to mark an organization as a partner (free)
or internal (internal) account. This is mutually exclusive with
set_grace_period — only one may be provided per call. Setting the waiver
type to free or internal also changes the Orb billing plan (free →
Airbyte Partner, internal → Airbyte Internal). The ORB_API_KEY
environment variable must be configured for waiver type changes.
The organization_name parameter is a safety check: the tool looks up the
organization via the Config API and verifies that the provided name, email, or
email domain matches. If omitted or mismatched, the tool returns the valid
identifiers so the caller can verify and retry.
Parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
organization_id |
string |
yes | — | The organization UUID. |
approval_comment_url |
string |
yes | — | URL to the Slack approval record. Obtain this by calling escalate_to_human with approval_requested=True; the backend delivers the approval record URL when a human clicks Approve. |
organization_name |
string | null |
no | null |
Confirmation of the target organization. Accepts the organization name, email address, or email domain. Required to prevent accidental modifications to the wrong organization. If omitted or mismatched, the tool returns an error with the valid identifiers so you can verify and retry. |
set_grace_period |
string | null |
no | null |
Set or modify the grace period. Accepts three forms: (1) A date in YYYY-MM-DD format — grace period ends at 11:59 PM Pacific on that date. (2) An integer number of days (1-90) from today (Pacific Time) — grace period ends at 11:59 PM Pacific on the resulting date. (3) 'cancel' to terminate the current grace period (sets status to manual). Requires set_grace_period_reason when setting or extending. |
set_grace_period_reason |
string | null |
no | null |
Reason for starting, extending, or canceling the grace period. Required when set_grace_period is a date or number of days. |
set_permanent_waiver_type |
enum("free", "internal", "none") | null |
no | null |
Set a permanent billing waiver for the organization. Use 'free' for partner accounts that should not be billed, 'internal' for Airbyte-internal organizations, or 'none' to remove an existing waiver. Mutually exclusive with set_grace_period. |
set_permanent_waiver_reason |
string | null |
no | null |
Reason for setting the permanent billing waiver. Required when set_permanent_waiver_type is provided. |
config_api_root |
string | null |
no | null |
Optional Config API root URL override. Defaults to Airbyte Cloud (https://cloud.airbyte.com/api/v1). |
Show input JSON schema
{
"additionalProperties": false,
"properties": {
"organization_id": {
"description": "The organization UUID.",
"type": "string"
},
"approval_comment_url": {
"description": "URL to the Slack approval record. Obtain this by calling `escalate_to_human` with `approval_requested=True`; the backend delivers the approval record URL when a human clicks Approve.",
"type": "string"
},
"organization_name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Confirmation of the target organization. Accepts the organization name, email address, or email domain. Required to prevent accidental modifications to the wrong organization. If omitted or mismatched, the tool returns an error with the valid identifiers so you can verify and retry."
},
"set_grace_period": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Set or modify the grace period. Accepts three forms: (1) A date in `YYYY-MM-DD` format \u2014 grace period ends at 11:59 PM Pacific on that date. (2) An integer number of days (1-90) from today (Pacific Time) \u2014 grace period ends at 11:59 PM Pacific on the resulting date. (3) `'cancel'` to terminate the current grace period (sets status to `manual`). Requires `set_grace_period_reason` when setting or extending."
},
"set_grace_period_reason": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Reason for starting, extending, or canceling the grace period. Required when `set_grace_period` is a date or number of days."
},
"set_permanent_waiver_type": {
"anyOf": [
{
"enum": [
"free",
"internal",
"none"
],
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Set a permanent billing waiver for the organization. Use `'free'` for partner accounts that should not be billed, `'internal'` for Airbyte-internal organizations, or `'none'` to remove an existing waiver. Mutually exclusive with `set_grace_period`."
},
"set_permanent_waiver_reason": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Reason for setting the permanent billing waiver. Required when `set_permanent_waiver_type` is provided."
},
"config_api_root": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Optional Config API root URL override. Defaults to Airbyte Cloud (`https://cloud.airbyte.com/api/v1`)."
}
},
"required": [
"organization_id",
"approval_comment_url"
],
"type": "object"
}
Show output JSON schema
{
"description": "Result of an organization payment config update operation.",
"properties": {
"success": {
"description": "Whether the operation succeeded",
"type": "boolean"
},
"message": {
"description": "Human-readable message describing the result",
"type": "string"
},
"organization_id": {
"description": "The organization UUID",
"type": "string"
},
"payment_status": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "The payment status after the update"
},
"grace_period_end_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "The grace period end datetime after the update (if applicable)"
},
"permanent_waiver_type": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Permanent billing waiver after the update: `free`, `internal`, or `None`"
},
"customer_tier": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Customer tier of the organization (TIER_0, TIER_1, TIER_2, UNKNOWN)"
},
"tier_warning": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Warning message if the organization is a sensitive customer tier"
},
"warnings": {
"description": "Warnings raised by this operation.",
"items": {
"type": "string"
},
"type": "array"
},
"orb_plan_change": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Result of the Orb plan change (e.g. `Changed to Airbyte Partner`), or `None` if no Orb plan change was attempted"
},
"entitlement_plan_change": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Result of the Stigg entitlement plan change (e.g. `Changed to PARTNER`), or `None` if not attempted"
}
},
"required": [
"success",
"message",
"organization_id"
],
"type": "object"
}
1# Copyright (c) 2025 Airbyte, Inc., all rights reserved. 2"""MCP tools for organization administration: is_agentic flag, payment config, and customer-tier lookup/cache. 3 4## MCP reference 5 6.. include:: ../../../docs/mcp-generated/organization_admin.md 7 :start-line: 2 8""" 9 10# NOTE: We intentionally do NOT use `from __future__ import annotations` here. 11# FastMCP has issues resolving forward references when PEP 563 deferred annotations 12# are used. See: https://github.com/jlowin/fastmcp/issues/905 13# Python 3.12+ supports modern type hint syntax natively, so this is not needed. 14 15__all__: list[str] = [] 16 17import logging 18import re 19from datetime import date, datetime, timedelta, timezone 20from typing import Annotated, Any, Literal 21 22import requests 23from airbyte import constants 24from fastmcp import Context, FastMCP 25from fastmcp_extensions import get_mcp_config, mcp_tool, register_mcp_tools 26from pydantic import BaseModel, Field 27from zoneinfo import ZoneInfo 28 29from airbyte_ops_mcp.approval_resolution import ( 30 ApprovalResolutionError, 31 resolve_admin_email_from_approval, 32) 33from airbyte_ops_mcp.cloud_admin.auth import ( 34 CloudAuthError, 35 require_internal_admin_flag_only, 36) 37from airbyte_ops_mcp.cloud_admin.entitlements import ( 38 WAIVER_TYPE_TO_ENTITLEMENT_PLAN, 39 EntitlementAPIError, 40 update_entitlement_plan, 41) 42from airbyte_ops_mcp.cloud_admin.models import ( 43 OrbSubscriptionInfo, 44 OrganizationAgenticFlagBatchInfo, 45 OrganizationAgenticFlagBatchUpdateResult, 46 OrganizationAgenticFlagInfo, 47 OrganizationAgenticFlagUpdateResult, 48 OrganizationInfo, 49 OrganizationPaymentConfigInfo, 50 OrganizationPaymentConfigUpdateResult, 51) 52from airbyte_ops_mcp.cloud_admin.orb_billing import ( 53 OrbAPIError, 54 _get_orb_api_key, 55 _resolve_plan_id, 56 extract_subscription_summary, 57 get_active_subscription, 58 schedule_plan_change, 59) 60from airbyte_ops_mcp.cloud_admin.organization_agentic_flag import ( 61 OrganizationAgenticFlagAPIError, 62) 63from airbyte_ops_mcp.cloud_admin.organization_agentic_flag import ( 64 get_organization_info as _get_organization_info, 65) 66from airbyte_ops_mcp.cloud_admin.organization_agentic_flag import ( 67 set_organization_agentic_status as _set_organization_agentic_status, 68) 69from airbyte_ops_mcp.cloud_admin.payment_config import ( 70 PaymentConfigAPIError, 71 get_organization_info, 72) 73from airbyte_ops_mcp.cloud_admin.payment_config import ( 74 get_organization_payment_config as _get_organization_payment_config, 75) 76from airbyte_ops_mcp.cloud_admin.payment_config import ( 77 update_organization_payment_config as _update_organization_payment_config, 78) 79from airbyte_ops_mcp.cloud_admin.version_overrides import validate_tier_filter 80from airbyte_ops_mcp.constants import ServerConfigKey 81from airbyte_ops_mcp.prod_db_access.queries import ( 82 query_connection_workspace_details, 83 query_organization_agentic_flags, 84) 85from airbyte_ops_mcp.tier_cache import ( 86 CustomerTier, 87 TierCacheStats, 88 TierFilter, 89 TierSourceHealth, 90 TierSummary, 91 build_tier_summary, 92 get_cache_stats, 93 get_org_tier, 94 get_org_tiers, 95 refresh_tier_cache, 96 resolve_workspaces, 97 tier_source_warnings, 98) 99 100 101def _normalize_organization_ids(organization_ids: str | list[str]) -> list[str]: 102 """Normalize one organization ID or a list of organization IDs.""" 103 ids = [organization_ids] if isinstance(organization_ids, str) else organization_ids 104 105 normalized = [org_id.strip() for org_id in ids if org_id and org_id.strip()] 106 return list(dict.fromkeys(normalized)) 107 108 109def _build_update_failure_results( 110 organization_ids: list[str], 111 message: str, 112) -> list[OrganizationAgenticFlagUpdateResult]: 113 """Build per-org failure results for request-level validation failures.""" 114 return [ 115 OrganizationAgenticFlagUpdateResult( 116 success=False, 117 message=message, 118 organization_id=organization_id, 119 ) 120 for organization_id in organization_ids 121 ] 122 123 124def _resolve_cloud_auth(ctx: Context) -> tuple[str | None, str | None, str | None]: 125 """Resolve auth credentials, returning `(bearer_token, client_id, client_secret)`.""" 126 bearer_token = get_mcp_config(ctx, ServerConfigKey.BEARER_TOKEN) 127 if bearer_token: 128 return bearer_token, None, None 129 130 client_id = get_mcp_config(ctx, ServerConfigKey.CLIENT_ID) 131 client_secret = get_mcp_config(ctx, ServerConfigKey.CLIENT_SECRET) 132 return None, client_id, client_secret 133 134 135def _org_info_to_flag_info( 136 org_info: OrganizationInfo, 137 customer_tier: str | None = None, 138 tier_warning: str | None = None, 139 source_health: TierSourceHealth | None = None, 140) -> OrganizationAgenticFlagInfo: 141 """Convert Config API organization info into agentic flag info.""" 142 return OrganizationAgenticFlagInfo( 143 organization_id=org_info.organization_id, 144 organization_name=org_info.organization_name, 145 email=org_info.email, 146 tombstone=False, 147 is_agentic=org_info.is_agentic or False, 148 customer_tier=customer_tier, 149 tier_warning=tier_warning, 150 warnings=tier_source_warnings(source_health), 151 ) 152 153 154def _row_to_flag_info( 155 row: dict[str, object], 156 customer_tier: str | None = None, 157 tier_warning: str | None = None, 158 source_health: TierSourceHealth | None = None, 159) -> OrganizationAgenticFlagInfo: 160 """Convert a DB row into agentic flag info.""" 161 return OrganizationAgenticFlagInfo( 162 organization_id=str(row["organization_id"]), 163 organization_name=str(row["organization_name"]) 164 if row["organization_name"] is not None 165 else None, 166 email=str(row["email"]) if row["email"] is not None else None, 167 tombstone=bool(row["tombstone"]), 168 is_agentic=bool(row["is_agentic"]), 169 customer_tier=customer_tier, 170 tier_warning=tier_warning, 171 warnings=tier_source_warnings(source_health), 172 ) 173 174 175@mcp_tool( 176 destructive=False, 177 idempotent=True, 178 open_world=True, 179) 180def get_organization_agentic_flag( 181 organization_ids: Annotated[ 182 str | list[str], 183 Field( 184 description="One organization UUID, or a list of organization UUIDs.", 185 ), 186 ], 187 config_api_root: Annotated[ 188 str | None, 189 Field( 190 description="Optional Config API root URL override. When omitted, " 191 "the tool reads from the Prod DB replica.", 192 default=None, 193 ), 194 ] = None, 195 *, 196 ctx: Context, 197) -> OrganizationAgenticFlagBatchInfo: 198 """Get the current `is_agentic` flag for one or more organizations.""" 199 normalized_ids = _normalize_organization_ids(organization_ids) 200 if not normalized_ids: 201 return OrganizationAgenticFlagBatchInfo( 202 organizations=[], 203 missing_organization_ids=[], 204 ) 205 206 if config_api_root is None: 207 rows = query_organization_agentic_flags(normalized_ids) 208 rows_by_org_id = {str(row["organization_id"]): row for row in rows} 209 organizations: list[OrganizationAgenticFlagInfo] = [] 210 for organization_id in normalized_ids: 211 row = rows_by_org_id.get(organization_id) 212 if row is None: 213 continue 214 tier_result = get_org_tier( 215 organization_id=organization_id, 216 allow_degraded=True, 217 ) 218 organizations.append( 219 _row_to_flag_info( 220 row, 221 customer_tier=tier_result.customer_tier, 222 tier_warning=_build_tier_warning(tier_result.customer_tier), 223 source_health=tier_result.source_health, 224 ) 225 ) 226 return OrganizationAgenticFlagBatchInfo( 227 organizations=organizations, 228 missing_organization_ids=[ 229 organization_id 230 for organization_id in normalized_ids 231 if organization_id not in rows_by_org_id 232 ], 233 ) 234 235 resolved_api_root = config_api_root or constants.CLOUD_CONFIG_API_ROOT 236 bearer_token, client_id, client_secret = _resolve_cloud_auth(ctx) 237 238 organizations: list[OrganizationAgenticFlagInfo] = [] 239 missing_organization_ids: list[str] = [] 240 for organization_id in normalized_ids: 241 try: 242 org_info = _get_organization_info( 243 organization_id=organization_id, 244 config_api_root=resolved_api_root, 245 client_id=client_id, 246 client_secret=client_secret, 247 bearer_token=bearer_token, 248 ) 249 except OrganizationAgenticFlagAPIError: 250 missing_organization_ids.append(organization_id) 251 continue 252 if org_info is None: 253 missing_organization_ids.append(organization_id) 254 continue 255 256 tier_result = get_org_tier( 257 organization_id=organization_id, 258 allow_degraded=True, 259 ) 260 organizations.append( 261 _org_info_to_flag_info( 262 org_info, 263 customer_tier=tier_result.customer_tier, 264 tier_warning=_build_tier_warning(tier_result.customer_tier), 265 source_health=tier_result.source_health, 266 ) 267 ) 268 269 return OrganizationAgenticFlagBatchInfo( 270 organizations=organizations, 271 missing_organization_ids=missing_organization_ids, 272 ) 273 274 275@mcp_tool( 276 destructive=True, 277 idempotent=True, 278 open_world=True, 279) 280def update_organization_agentic_flag( 281 organization_ids: Annotated[ 282 str | list[str], 283 Field( 284 description="One organization UUID, or a list of organization UUIDs.", 285 ), 286 ], 287 is_agentic: Annotated[ 288 bool, 289 Field(description="The desired `is_agentic` value."), 290 ], 291 approval_comment_url: Annotated[ 292 str, 293 Field( 294 description="URL to the Slack approval record. Obtain this by calling " 295 "`escalate_to_human` with `approval_requested=True`; the backend " 296 "delivers the approval record URL when a human clicks Approve.", 297 ), 298 ], 299 organization_name: Annotated[ 300 str | None, 301 Field( 302 description="Confirmation of the target organization. Accepts the " 303 "organization name, email address, or email domain. Required when " 304 "updating a single organization. Ignored for multi-org updates; use " 305 "`organization_names` instead.", 306 default=None, 307 ), 308 ] = None, 309 organization_names: Annotated[ 310 dict[str, str] | None, 311 Field( 312 description="Per-organization confirmation for multi-org updates. " 313 "Keys are organization UUIDs; values may be organization names, " 314 "email addresses, or email domains.", 315 default=None, 316 ), 317 ] = None, 318 customer_tier_filter: Annotated[ 319 TierFilter, 320 Field( 321 description=( 322 "Required tier filter: 'TIER_0', 'TIER_1', 'TIER_2', 'UNKNOWN', or 'ALL'. " 323 "The operation is rejected if the actual customer tier does not match. " 324 "Use 'ALL' to proceed regardless of tier after human approval." 325 ), 326 ), 327 ] = "TIER_2", 328 *, 329 ctx: Context, 330) -> OrganizationAgenticFlagBatchUpdateResult: 331 """Update the `is_agentic` flag for one or more organizations. 332 333 All updates require human-in-the-loop approval via `escalate_to_human`. 334 The tool validates each organization identity before writing the flag. 335 """ 336 normalized_ids = _normalize_organization_ids(organization_ids) 337 if not normalized_ids: 338 return OrganizationAgenticFlagBatchUpdateResult( 339 success=False, 340 message="No organization IDs provided.", 341 results=[], 342 ) 343 344 try: 345 require_internal_admin_flag_only() 346 except CloudAuthError as e: 347 return OrganizationAgenticFlagBatchUpdateResult( 348 success=False, 349 message=f"Admin authentication failed: {e}", 350 results=_build_update_failure_results( 351 normalized_ids, f"Admin authentication failed: {e}" 352 ), 353 ) 354 355 try: 356 resolve_admin_email_from_approval( 357 approval_comment_url=approval_comment_url, 358 ) 359 except ApprovalResolutionError as e: 360 return OrganizationAgenticFlagBatchUpdateResult( 361 success=False, 362 message=str(e), 363 results=_build_update_failure_results(normalized_ids, str(e)), 364 ) 365 366 bearer_token, client_id, client_secret = _resolve_cloud_auth(ctx) 367 368 results: list[OrganizationAgenticFlagUpdateResult] = [] 369 for organization_id in normalized_ids: 370 rows = query_organization_agentic_flags([organization_id]) 371 if not rows: 372 results.append( 373 OrganizationAgenticFlagUpdateResult( 374 success=False, 375 message=f"Organization {organization_id} not found.", 376 organization_id=organization_id, 377 ) 378 ) 379 continue 380 381 current_info = _row_to_flag_info(rows[0]) 382 if current_info.tombstone: 383 results.append( 384 OrganizationAgenticFlagUpdateResult( 385 success=False, 386 message=f"Organization {organization_id} is tombstoned.", 387 organization_id=organization_id, 388 organization_name=current_info.organization_name, 389 email=current_info.email, 390 previous_is_agentic=current_info.is_agentic, 391 ) 392 ) 393 continue 394 395 org_info = OrganizationInfo.model_validate( 396 { 397 "organizationId": current_info.organization_id, 398 "organizationName": current_info.organization_name or "", 399 "email": current_info.email, 400 "isAgentic": current_info.is_agentic, 401 } 402 ) 403 expected_name = ( 404 organization_names.get(organization_id) 405 if organization_names is not None 406 else organization_name 407 ) 408 name_ok, name_error = _validate_organization_name( 409 organization_id, expected_name, org_info 410 ) 411 if not name_ok: 412 results.append( 413 OrganizationAgenticFlagUpdateResult( 414 success=False, 415 message=name_error or "Organization name validation failed.", 416 organization_id=organization_id, 417 organization_name=current_info.organization_name, 418 email=current_info.email, 419 previous_is_agentic=current_info.is_agentic, 420 ) 421 ) 422 continue 423 424 tier_result = get_org_tier( 425 organization_id=organization_id, 426 allow_degraded=True, 427 ) 428 customer_tier = tier_result.customer_tier 429 tier_warning = _build_tier_warning(customer_tier) 430 warnings = tier_source_warnings(tier_result.source_health) 431 tier_ok, tier_error = validate_tier_filter( 432 customer_tier, 433 customer_tier_filter, 434 source_health=tier_result.source_health, 435 organization_id=organization_id, 436 ) 437 if not tier_ok: 438 results.append( 439 OrganizationAgenticFlagUpdateResult( 440 success=False, 441 message=tier_error or "Customer tier validation failed.", 442 organization_id=organization_id, 443 organization_name=current_info.organization_name, 444 email=current_info.email, 445 previous_is_agentic=current_info.is_agentic, 446 customer_tier=customer_tier, 447 tier_warning=tier_warning, 448 warnings=warnings, 449 ) 450 ) 451 continue 452 previous_is_agentic = current_info.is_agentic 453 454 if previous_is_agentic == is_agentic: 455 results.append( 456 OrganizationAgenticFlagUpdateResult( 457 success=True, 458 message=( 459 f"Organization {organization_id} already has managed agentic " 460 f"org status {is_agentic}." 461 ), 462 organization_id=organization_id, 463 organization_name=current_info.organization_name, 464 email=current_info.email, 465 previous_is_agentic=previous_is_agentic, 466 new_is_agentic=is_agentic, 467 customer_tier=customer_tier, 468 tier_warning=tier_warning, 469 warnings=warnings, 470 ) 471 ) 472 continue 473 474 try: 475 updated = _set_organization_agentic_status( 476 organization_id=organization_id, 477 is_agentic=is_agentic, 478 config_api_root=constants.CLOUD_CONFIG_API_ROOT, 479 client_id=client_id, 480 client_secret=client_secret, 481 bearer_token=bearer_token, 482 ) 483 except Exception as e: 484 results.append( 485 OrganizationAgenticFlagUpdateResult( 486 success=False, 487 message=str(e), 488 organization_id=organization_id, 489 organization_name=current_info.organization_name, 490 email=current_info.email, 491 previous_is_agentic=previous_is_agentic, 492 customer_tier=customer_tier, 493 tier_warning=tier_warning, 494 warnings=warnings, 495 ) 496 ) 497 continue 498 499 updated_info = _org_info_to_flag_info( 500 updated, 501 customer_tier=customer_tier, 502 tier_warning=tier_warning, 503 ) 504 results.append( 505 OrganizationAgenticFlagUpdateResult( 506 success=True, 507 message=( 508 f"Organization {organization_id} updated managed agentic org " 509 f"status to {updated_info.is_agentic}." 510 ), 511 organization_id=organization_id, 512 organization_name=updated_info.organization_name, 513 email=updated_info.email, 514 previous_is_agentic=previous_is_agentic, 515 new_is_agentic=updated_info.is_agentic, 516 customer_tier=customer_tier, 517 tier_warning=tier_warning, 518 warnings=warnings, 519 ) 520 ) 521 522 success = all(result.success for result in results) 523 return OrganizationAgenticFlagBatchUpdateResult( 524 success=success, 525 message=( 526 f"Updated {sum(result.success for result in results)} of " 527 f"{len(results)} organization agentic flag request(s)." 528 ), 529 results=results, 530 ) 531 532 533logger = logging.getLogger(__name__) 534 535_DATE_PATTERN = re.compile(r"^\d{4}-\d{2}-\d{2}$") 536 537_DAYS_PATTERN = re.compile(r"^\d+$") 538 539_PACIFIC = ZoneInfo("America/Los_Angeles") 540 541 542def _build_tier_warning(customer_tier: str) -> str | None: 543 """Build a warning message for sensitive customer tiers.""" 544 if customer_tier == "TIER_0": 545 return ( 546 "WARNING: This is a TIER_0 (highest-value) customer. " 547 "Proceed with extreme caution." 548 ) 549 if customer_tier == "TIER_1": 550 return "WARNING: This is a TIER_1 (high-value) customer. Proceed with caution." 551 return None 552 553 554def _fetch_orb_subscription_info( 555 organization_id: str, 556) -> OrbSubscriptionInfo | None: 557 """Try to fetch the active Orb subscription for an organization. 558 559 Returns `None` silently if the Orb API key is not configured or if no 560 active subscription is found. Logs warnings on API errors but does not 561 raise — Orb data is supplemental, not required. 562 """ 563 orb_api_key = _get_orb_api_key() 564 if not orb_api_key: 565 return None 566 567 try: 568 active_sub = get_active_subscription(organization_id, orb_api_key) 569 except (OrbAPIError, requests.RequestException): 570 logger.warning( 571 "Failed to fetch Orb subscription for org %s", 572 organization_id, 573 exc_info=True, 574 ) 575 return None 576 577 if active_sub is None: 578 return None 579 580 summary = extract_subscription_summary(active_sub) 581 return OrbSubscriptionInfo( 582 subscription_id=summary["subscription_id"], 583 status=summary["status"], 584 plan_name=summary.get("plan_name"), 585 plan_id=summary.get("plan_id"), 586 external_plan_id=summary.get("external_plan_id"), 587 start_date=summary.get("start_date"), 588 end_date=summary.get("end_date"), 589 orb_customer_id=summary.get("orb_customer_id"), 590 ) 591 592 593def _validate_organization_name( 594 organization_id: str, 595 organization_name: str | None, 596 org_info: OrganizationInfo, 597) -> tuple[bool, str | None]: 598 """Validate `organization_name` against the org record from the Config API. 599 600 Accepts the org's literal name, email, or email domain as valid inputs. 601 602 Returns `(ok, error_message)`. On success `error_message` is `None`. 603 """ 604 org_db_name: str = org_info.organization_name or "" 605 org_db_email: str = org_info.email or "" 606 org_db_domain: str = org_db_email.split("@", 1)[-1] if "@" in org_db_email else "" 607 608 valid_identifiers = [ 609 v 610 for v in [org_db_name.lower(), org_db_email.lower(), org_db_domain.lower()] 611 if v 612 ] 613 614 identifier_parts = [] 615 if org_db_name: 616 identifier_parts.append(f"'{org_db_name}' (org name)") 617 if org_db_email: 618 identifier_parts.append(f"'{org_db_email}' (email)") 619 if org_db_domain: 620 identifier_parts.append(f"'{org_db_domain}' (email domain)") 621 622 if not identifier_parts: 623 return ( 624 False, 625 f"Organization {organization_id} has no name or email on record.", 626 ) 627 628 hint_message = ( 629 f"To confirm, resend with `organization_name` set to one of: " 630 f"{', '.join(identifier_parts)}. " 631 f"Double-check that this is the correct organization before retrying. " 632 f"If there is any doubt, confirm with your user." 633 ) 634 635 if organization_name is None: 636 return ( 637 False, 638 ( 639 f"`organization_name` is required to confirm the target organization. " 640 f"{hint_message}" 641 ), 642 ) 643 644 if organization_name.strip().lower().lstrip("@") not in valid_identifiers: 645 return ( 646 False, 647 ( 648 f"Organization name mismatch: '{organization_name}' does not match " 649 f"organization {organization_id}. {hint_message}" 650 ), 651 ) 652 653 return True, None 654 655 656def _format_grace_period_end(target_date: date) -> str: 657 """Build a backend-compatible datetime string for 11:59 PM Pacific on `target_date`. 658 659 The result is converted to UTC and formatted as 660 `yyyy-MM-dd'T'HH:mm:ss.SSS+0000` to match the Java `@JsonFormat` annotation. 661 """ 662 end_pacific = datetime( 663 target_date.year, 664 target_date.month, 665 target_date.day, 666 23, 667 59, 668 59, 669 tzinfo=_PACIFIC, 670 ) 671 end_utc = end_pacific.astimezone(timezone.utc) 672 return end_utc.strftime("%Y-%m-%dT%H:%M:%S.000+0000") 673 674 675def _parse_grace_period_value( 676 value: str, 677) -> tuple[str | None, str | None]: 678 """Parse `set_grace_period` into an ISO 8601 datetime string or an action keyword. 679 680 Dates and day offsets resolve to 11:59 PM Pacific Time on the target date. 681 Day offsets use the current date in Pacific Time as the starting point. 682 683 Returns `(iso_datetime_or_action, error_message)`. 684 On success `error_message` is `None`. 685 """ 686 value = value.strip() 687 688 if value.lower() == "cancel": 689 return "cancel", None 690 691 today_pacific = datetime.now(tz=_PACIFIC).date() 692 693 if _DATE_PATTERN.match(value): 694 try: 695 parsed = date.fromisoformat(value) 696 except ValueError: 697 return None, f"Invalid date: {value}. Use YYYY-MM-DD format." 698 if parsed < today_pacific: 699 return None, f"Grace period end date {value} is in the past." 700 if (parsed - today_pacific).days > 90: 701 return None, ( 702 f"Grace period end date {value} is more than 90 days in the future. " 703 "Maximum grace period is 90 days." 704 ) 705 return _format_grace_period_end(parsed), None 706 707 if _DAYS_PATTERN.match(value): 708 days = int(value) 709 if days < 1 or days > 90: 710 return None, f"Days must be between 1 and 90, got {days}." 711 target_date = today_pacific + timedelta(days=days) 712 return _format_grace_period_end(target_date), None 713 714 return None, ( 715 f"Invalid `set_grace_period` value: '{value}'. " 716 "Expected a date (YYYY-MM-DD), an integer number of days, or 'cancel'." 717 ) 718 719 720@mcp_tool( 721 read_only=True, 722 idempotent=True, 723 open_world=True, 724) 725def get_organization_payment_config( 726 organization_id: Annotated[ 727 str, 728 "The organization UUID.", 729 ], 730 config_api_root: Annotated[ 731 str | None, 732 Field( 733 description="Optional Config API root URL override. " 734 "Defaults to Airbyte Cloud (`https://cloud.airbyte.com/api/v1`).", 735 default=None, 736 ), 737 ] = None, 738 *, 739 ctx: Context, 740) -> OrganizationPaymentConfigInfo: 741 """Get the current payment configuration for an organization. 742 743 Returns payment status, subscription status, grace period info, 744 usage category override, and current Orb billing plan (when 745 `ORB_API_KEY` is configured). No PII or sensitive payment details 746 are included in the response. 747 748 Authentication credentials are resolved in priority order: 749 1. Bearer token (Authorization header or AIRBYTE_CLOUD_BEARER_TOKEN env var) 750 2. HTTP headers: X-Airbyte-Cloud-Client-Id, X-Airbyte-Cloud-Client-Secret 751 3. Environment variables: AIRBYTE_CLOUD_CLIENT_ID, AIRBYTE_CLOUD_CLIENT_SECRET 752 """ 753 resolved_api_root = config_api_root or constants.CLOUD_CONFIG_API_ROOT 754 bearer_token, client_id, client_secret = _resolve_cloud_auth(ctx) 755 756 data = _get_organization_payment_config( 757 organization_id=organization_id, 758 config_api_root=resolved_api_root, 759 client_id=client_id, 760 client_secret=client_secret, 761 bearer_token=bearer_token, 762 ) 763 764 # Enrich with tier info 765 tier_result = get_org_tier( 766 organization_id=organization_id, 767 allow_degraded=True, 768 ) 769 tier_warning = _build_tier_warning(tier_result.customer_tier) 770 771 # Enrich with Orb subscription info (best-effort) 772 orb_subscription = _fetch_orb_subscription_info(organization_id) 773 774 return OrganizationPaymentConfigInfo( 775 organization_id=data["organizationId"], 776 payment_status=data["paymentStatus"], 777 subscription_status=data["subscriptionStatus"], 778 payment_provider_id=data.get("paymentProviderId"), 779 grace_period_end_at=data.get("gracePeriodEndAt"), 780 usage_category_overwrite=data.get("usageCategoryOverwrite"), 781 customer_tier=tier_result.customer_tier, 782 tier_warning=tier_warning, 783 warnings=tier_source_warnings(tier_result.source_health), 784 orb_subscription=orb_subscription, 785 ) 786 787 788@mcp_tool( 789 destructive=True, 790 idempotent=False, 791 open_world=True, 792) 793def update_organization_payment_config( 794 organization_id: Annotated[ 795 str, 796 "The organization UUID.", 797 ], 798 approval_comment_url: Annotated[ 799 str, 800 Field( 801 description="URL to the Slack approval record. Obtain this by calling " 802 "`escalate_to_human` with `approval_requested=True`; the backend " 803 "delivers the approval record URL when a human clicks Approve.", 804 ), 805 ], 806 organization_name: Annotated[ 807 str | None, 808 Field( 809 description="Confirmation of the target organization. Accepts the " 810 "organization name, email address, or email domain. Required to " 811 "prevent accidental modifications to the wrong organization. " 812 "If omitted or mismatched, the tool returns an error with the valid " 813 "identifiers so you can verify and retry.", 814 default=None, 815 ), 816 ] = None, 817 set_grace_period: Annotated[ 818 str | None, 819 Field( 820 description="Set or modify the grace period. Accepts three forms: " 821 "(1) A date in `YYYY-MM-DD` format — grace period ends at 11:59 PM Pacific on that date. " 822 "(2) An integer number of days (1-90) from today (Pacific Time) — " 823 "grace period ends at 11:59 PM Pacific on the resulting date. " 824 "(3) `'cancel'` to terminate the current grace period " 825 "(sets status to `manual`). " 826 "Requires `set_grace_period_reason` when setting or extending.", 827 default=None, 828 ), 829 ] = None, 830 set_grace_period_reason: Annotated[ 831 str | None, 832 Field( 833 description="Reason for starting, extending, or canceling the grace " 834 "period. Required when `set_grace_period` is a date or number of days.", 835 default=None, 836 ), 837 ] = None, 838 set_permanent_waiver_type: Annotated[ 839 Literal["free", "internal", "none"] | None, 840 Field( 841 description="Set a permanent billing waiver for the organization. " 842 "Use `'free'` for partner accounts that should not be billed, " 843 "`'internal'` for Airbyte-internal organizations, " 844 "or `'none'` to remove an existing waiver. " 845 "Mutually exclusive with `set_grace_period`.", 846 default=None, 847 ), 848 ] = None, 849 set_permanent_waiver_reason: Annotated[ 850 str | None, 851 Field( 852 description="Reason for setting the permanent billing waiver. " 853 "Required when `set_permanent_waiver_type` is provided.", 854 default=None, 855 ), 856 ] = None, 857 config_api_root: Annotated[ 858 str | None, 859 Field( 860 description="Optional Config API root URL override. " 861 "Defaults to Airbyte Cloud (`https://cloud.airbyte.com/api/v1`).", 862 default=None, 863 ), 864 ] = None, 865 *, 866 ctx: Context, 867) -> OrganizationPaymentConfigUpdateResult: 868 """Update the payment configuration for an organization. 869 870 All updates require human-in-the-loop approval via `escalate_to_human`. 871 872 Use `set_grace_period` to start, extend, or cancel a grace period. 873 If the org is not already in `manual` status, the tool automatically 874 transitions to `manual` first before setting the grace period. 875 876 Use `set_permanent_waiver_type` to mark an organization as a partner (`free`) 877 or internal (`internal`) account. This is mutually exclusive with 878 `set_grace_period` — only one may be provided per call. Setting the waiver 879 type to `free` or `internal` also changes the Orb billing plan (`free` → 880 Airbyte Partner, `internal` → Airbyte Internal). The `ORB_API_KEY` 881 environment variable must be configured for waiver type changes. 882 883 The `organization_name` parameter is a safety check: the tool looks up the 884 organization via the Config API and verifies that the provided name, email, or 885 email domain matches. If omitted or mismatched, the tool returns the valid 886 identifiers so the caller can verify and retry. 887 """ 888 # --- Validate that an action was specified --- 889 if set_grace_period is None and set_permanent_waiver_type is None: 890 return OrganizationPaymentConfigUpdateResult( 891 success=False, 892 message="No action specified. Provide `set_grace_period` with a date " 893 "(YYYY-MM-DD), number of days (1-90), or 'cancel'; or " 894 "`set_permanent_waiver_type` with 'free' (partner) or 'internal'.", 895 organization_id=organization_id, 896 ) 897 898 # --- Validate mutual exclusivity --- 899 if set_grace_period is not None and set_permanent_waiver_type is not None: 900 return OrganizationPaymentConfigUpdateResult( 901 success=False, 902 message="`set_grace_period` and `set_permanent_waiver_type` are " 903 "mutually exclusive. Provide only one per call.", 904 organization_id=organization_id, 905 ) 906 907 # --- Validate waiver reason is provided when waiver type is set --- 908 if set_permanent_waiver_type is not None and not set_permanent_waiver_reason: 909 return OrganizationPaymentConfigUpdateResult( 910 success=False, 911 message="`set_permanent_waiver_reason` is required when " 912 "`set_permanent_waiver_type` is provided.", 913 organization_id=organization_id, 914 ) 915 916 # --- Validate ORB_API_KEY is configured for waiver type changes --- 917 if set_permanent_waiver_type in ("free", "internal") and not _get_orb_api_key(): 918 return OrganizationPaymentConfigUpdateResult( 919 success=False, 920 message="`ORB_API_KEY` environment variable is not configured. " 921 "It is required when setting `set_permanent_waiver_type` to " 922 "'free' or 'internal' because the Orb billing plan must also " 923 "be changed.", 924 organization_id=organization_id, 925 ) 926 927 # --- Validate admin access --- 928 try: 929 require_internal_admin_flag_only() 930 except CloudAuthError as e: 931 return OrganizationPaymentConfigUpdateResult( 932 success=False, 933 message=f"Admin authentication failed: {e}", 934 organization_id=organization_id, 935 ) 936 937 # --- Resolve approval --- 938 try: 939 resolve_admin_email_from_approval( 940 approval_comment_url=approval_comment_url, 941 ) 942 except ApprovalResolutionError as e: 943 return OrganizationPaymentConfigUpdateResult( 944 success=False, 945 message=str(e), 946 organization_id=organization_id, 947 ) 948 949 # --- Resolve auth --- 950 resolved_api_root = config_api_root or constants.CLOUD_CONFIG_API_ROOT 951 bearer_token, client_id, client_secret = _resolve_cloud_auth(ctx) 952 953 # --- Look up organization info via Config API --- 954 try: 955 org_info = get_organization_info( 956 organization_id=organization_id, 957 config_api_root=resolved_api_root, 958 client_id=client_id, 959 client_secret=client_secret, 960 bearer_token=bearer_token, 961 ) 962 except PaymentConfigAPIError as e: 963 return OrganizationPaymentConfigUpdateResult( 964 success=False, 965 message=f"Failed to fetch organization info: {e}", 966 organization_id=organization_id, 967 ) 968 if org_info is None: 969 return OrganizationPaymentConfigUpdateResult( 970 success=False, 971 message=f"Organization {organization_id} not found.", 972 organization_id=organization_id, 973 ) 974 975 # --- Validate organization name (safety check) --- 976 name_ok, name_error = _validate_organization_name( 977 organization_id, organization_name, org_info 978 ) 979 if not name_ok: 980 return OrganizationPaymentConfigUpdateResult( 981 success=False, 982 message=name_error or "Organization name validation failed.", 983 organization_id=organization_id, 984 ) 985 986 # --- Enrich with tier info --- 987 tier_result = get_org_tier( 988 organization_id=organization_id, 989 allow_degraded=True, 990 ) 991 customer_tier = tier_result.customer_tier 992 tier_warning = _build_tier_warning(customer_tier) 993 warnings = tier_source_warnings(tier_result.source_health) 994 995 # --- Permanent-waiver-only path (no grace period change) --- 996 # 997 # Statuses that the API cannot set back: uninitialized, okay, disabled. 998 # If the org is in one of these, we transition to 'manual' first (same 999 # pattern the grace-period path uses). 1000 _api_nonsettable_statuses = ("uninitialized", "okay", "disabled") 1001 1002 if set_grace_period is None: 1003 # Only set_permanent_waiver_type was requested 1004 assert set_permanent_waiver_type is not None 1005 try: 1006 current_config = _get_organization_payment_config( 1007 organization_id=organization_id, 1008 config_api_root=resolved_api_root, 1009 client_id=client_id, 1010 client_secret=client_secret, 1011 bearer_token=bearer_token, 1012 ) 1013 except PaymentConfigAPIError as e: 1014 return OrganizationPaymentConfigUpdateResult( 1015 success=False, 1016 message=f"Failed to fetch current config: {e}", 1017 organization_id=organization_id, 1018 customer_tier=customer_tier, 1019 tier_warning=tier_warning, 1020 warnings=warnings, 1021 ) 1022 current_status = current_config["paymentStatus"] 1023 1024 # Transition to 'manual' if current status is not API-settable 1025 target_status = current_status 1026 if current_status in _api_nonsettable_statuses: 1027 try: 1028 _update_organization_payment_config( 1029 organization_id=organization_id, 1030 payment_status="manual", 1031 config_api_root=resolved_api_root, 1032 client_id=client_id, 1033 client_secret=client_secret, 1034 bearer_token=bearer_token, 1035 new_grace_period_reason=( 1036 f"Transitioned to manual for permanent waiver: " 1037 f"{set_permanent_waiver_reason}" 1038 ), 1039 ) 1040 target_status = "manual" 1041 except PaymentConfigAPIError as e: 1042 return OrganizationPaymentConfigUpdateResult( 1043 success=False, 1044 message=f"Failed to transition from '{current_status}' " 1045 f"to 'manual': {e}", 1046 organization_id=organization_id, 1047 payment_status=current_status, 1048 customer_tier=customer_tier, 1049 tier_warning=tier_warning, 1050 warnings=warnings, 1051 ) 1052 1053 try: 1054 data = _update_organization_payment_config( 1055 organization_id=organization_id, 1056 payment_status=target_status, 1057 config_api_root=resolved_api_root, 1058 client_id=client_id, 1059 client_secret=client_secret, 1060 bearer_token=bearer_token, 1061 usage_category_overwrite=( 1062 set_permanent_waiver_type 1063 if set_permanent_waiver_type != "none" 1064 else "" 1065 ), 1066 ) 1067 except PaymentConfigAPIError as e: 1068 return OrganizationPaymentConfigUpdateResult( 1069 success=False, 1070 message=f"Failed to set permanent waiver type: {e}", 1071 organization_id=organization_id, 1072 payment_status=target_status, 1073 customer_tier=customer_tier, 1074 tier_warning=tier_warning, 1075 warnings=warnings, 1076 ) 1077 parts = [ 1078 f"Permanent waiver type set to '{set_permanent_waiver_type}' " 1079 f"for org {organization_id}.", 1080 ] 1081 if current_status in _api_nonsettable_statuses: 1082 parts.append( 1083 f"Payment status transitioned from '{current_status}' to 'manual'." 1084 ) 1085 1086 # --- Orb plan change (required for "free" / "internal") --- 1087 orb_plan_change_result: str | None = None 1088 if set_permanent_waiver_type in ("free", "internal"): 1089 # ORB_API_KEY is validated at the top of the function, so this 1090 # is guaranteed to be non-None here. 1091 orb_api_key = _get_orb_api_key() 1092 assert orb_api_key, "ORB_API_KEY should have been validated earlier" 1093 try: 1094 active_sub = get_active_subscription(organization_id, orb_api_key) 1095 if active_sub is None: 1096 parts.append( 1097 "Orb plan change skipped: no active subscription " 1098 "found for this organization in Orb." 1099 ) 1100 orb_plan_change_result = "Skipped: no active Orb subscription" 1101 else: 1102 target_plan_id = _resolve_plan_id(set_permanent_waiver_type) 1103 current_plan_id = (active_sub.get("plan") or {}).get("id") 1104 current_plan_name = (active_sub.get("plan") or {}).get( 1105 "name", current_plan_id or "unknown" 1106 ) 1107 if current_plan_id == target_plan_id: 1108 orb_plan_change_result = ( 1109 f"Already on plan '{current_plan_name}'" 1110 ) 1111 parts.append(f"Orb plan already set to '{current_plan_name}'.") 1112 else: 1113 schedule_plan_change( 1114 subscription_id=active_sub["id"], 1115 plan_id=target_plan_id, 1116 api_key=orb_api_key, 1117 ) 1118 orb_plan_change_result = ( 1119 f"Changed from '{current_plan_name}' to '{target_plan_id}'" 1120 ) 1121 parts.append(f"Orb plan changed to '{target_plan_id}'.") 1122 except (OrbAPIError, requests.RequestException) as e: 1123 parts.append(f"Orb plan change failed: {e}") 1124 orb_plan_change_result = f"Failed: {e}" 1125 1126 # --- Entitlement plan update (Stigg) --- 1127 entitlement_plan_change_result: str | None = None 1128 target_entitlement_plan = WAIVER_TYPE_TO_ENTITLEMENT_PLAN.get( 1129 set_permanent_waiver_type 1130 ) 1131 if target_entitlement_plan: 1132 try: 1133 update_entitlement_plan( 1134 organization_id=organization_id, 1135 plan_name=target_entitlement_plan, 1136 config_api_root=resolved_api_root, 1137 client_id=client_id, 1138 client_secret=client_secret, 1139 bearer_token=bearer_token, 1140 ) 1141 entitlement_plan_change_result = f"Changed to {target_entitlement_plan}" 1142 parts.append( 1143 f"Entitlement plan updated to '{target_entitlement_plan}'." 1144 ) 1145 except EntitlementAPIError as e: 1146 parts.append(f"Entitlement plan update failed: {e}") 1147 entitlement_plan_change_result = f"Failed: {e}" 1148 1149 return OrganizationPaymentConfigUpdateResult( 1150 success=True, 1151 message=" ".join(parts), 1152 organization_id=organization_id, 1153 payment_status=data["paymentStatus"], 1154 grace_period_end_at=data.get("gracePeriodEndAt"), 1155 permanent_waiver_type=data.get("usageCategoryOverwrite"), 1156 customer_tier=customer_tier, 1157 tier_warning=tier_warning, 1158 warnings=warnings, 1159 orb_plan_change=orb_plan_change_result, 1160 entitlement_plan_change=entitlement_plan_change_result, 1161 ) 1162 1163 # --- Parse grace period value --- 1164 parsed_value, parse_error = _parse_grace_period_value(set_grace_period) 1165 if parse_error is not None: 1166 return OrganizationPaymentConfigUpdateResult( 1167 success=False, 1168 message=parse_error, 1169 organization_id=organization_id, 1170 customer_tier=customer_tier, 1171 tier_warning=tier_warning, 1172 warnings=warnings, 1173 ) 1174 1175 assert parsed_value is not None 1176 1177 # --- Fetch current payment config (needed for both cancel and set paths) --- 1178 try: 1179 current_config = _get_organization_payment_config( 1180 organization_id=organization_id, 1181 config_api_root=resolved_api_root, 1182 client_id=client_id, 1183 client_secret=client_secret, 1184 bearer_token=bearer_token, 1185 ) 1186 except PaymentConfigAPIError as e: 1187 return OrganizationPaymentConfigUpdateResult( 1188 success=False, 1189 message=f"Failed to fetch current config: {e}", 1190 organization_id=organization_id, 1191 customer_tier=customer_tier, 1192 tier_warning=tier_warning, 1193 warnings=warnings, 1194 ) 1195 1196 current_status = current_config["paymentStatus"] 1197 1198 if parsed_value == "cancel": 1199 # Only allow cancel when org is actually in grace_period (or manual with 1200 # an active grace period end date). 1201 if current_status not in ("grace_period", "manual"): 1202 return OrganizationPaymentConfigUpdateResult( 1203 success=False, 1204 message=f"Cannot cancel grace period: organization is in " 1205 f"'{current_status}' status, not 'grace_period'.", 1206 organization_id=organization_id, 1207 payment_status=current_status, 1208 customer_tier=customer_tier, 1209 tier_warning=tier_warning, 1210 warnings=warnings, 1211 ) 1212 1213 # Cancel grace period by setting status to manual 1214 try: 1215 data = _update_organization_payment_config( 1216 organization_id=organization_id, 1217 payment_status="manual", 1218 config_api_root=resolved_api_root, 1219 client_id=client_id, 1220 client_secret=client_secret, 1221 bearer_token=bearer_token, 1222 new_grace_period_reason=( 1223 set_grace_period_reason or "Grace period canceled via MCP tool" 1224 ), 1225 ) 1226 except PaymentConfigAPIError as e: 1227 return OrganizationPaymentConfigUpdateResult( 1228 success=False, 1229 message=str(e), 1230 organization_id=organization_id, 1231 customer_tier=customer_tier, 1232 tier_warning=tier_warning, 1233 warnings=warnings, 1234 ) 1235 1236 return OrganizationPaymentConfigUpdateResult( 1237 success=True, 1238 message=f"Grace period canceled for org {organization_id}. " 1239 f"New status: {data['paymentStatus']}.", 1240 organization_id=organization_id, 1241 payment_status=data["paymentStatus"], 1242 grace_period_end_at=data.get("gracePeriodEndAt"), 1243 permanent_waiver_type=data.get("usageCategoryOverwrite"), 1244 customer_tier=customer_tier, 1245 tier_warning=tier_warning, 1246 warnings=warnings, 1247 ) 1248 1249 # --- Setting/extending grace period --- 1250 if not set_grace_period_reason: 1251 return OrganizationPaymentConfigUpdateResult( 1252 success=False, 1253 message="`set_grace_period_reason` is required when setting or " 1254 "extending a grace period.", 1255 organization_id=organization_id, 1256 customer_tier=customer_tier, 1257 tier_warning=tier_warning, 1258 warnings=warnings, 1259 ) 1260 1261 # The API only allows setting grace_period from manual status. 1262 # If not already in manual, transition to manual first. 1263 transitioned_to_manual = False 1264 if current_status != "manual": 1265 logger.info( 1266 "Org %s is in '%s' status; transitioning to 'manual' first.", 1267 organization_id, 1268 current_status, 1269 ) 1270 try: 1271 _update_organization_payment_config( 1272 organization_id=organization_id, 1273 payment_status="manual", 1274 config_api_root=resolved_api_root, 1275 client_id=client_id, 1276 client_secret=client_secret, 1277 bearer_token=bearer_token, 1278 ) 1279 transitioned_to_manual = True 1280 except PaymentConfigAPIError as e: 1281 return OrganizationPaymentConfigUpdateResult( 1282 success=False, 1283 message=f"Failed to transition to 'manual' from '{current_status}': {e}", 1284 organization_id=organization_id, 1285 customer_tier=customer_tier, 1286 tier_warning=tier_warning, 1287 warnings=warnings, 1288 ) 1289 1290 # Now set the grace period 1291 try: 1292 data = _update_organization_payment_config( 1293 organization_id=organization_id, 1294 payment_status="grace_period", 1295 config_api_root=resolved_api_root, 1296 client_id=client_id, 1297 client_secret=client_secret, 1298 bearer_token=bearer_token, 1299 grace_period_end_at=parsed_value, 1300 new_grace_period_reason=set_grace_period_reason, 1301 ) 1302 except PaymentConfigAPIError as e: 1303 if transitioned_to_manual: 1304 msg = ( 1305 f"Failed to set grace period after transitioning to 'manual' " 1306 f"from '{current_status}'. The organization is now in 'manual' " 1307 f"status. Original error: {e}" 1308 ) 1309 else: 1310 msg = str(e) 1311 return OrganizationPaymentConfigUpdateResult( 1312 success=False, 1313 message=msg, 1314 organization_id=organization_id, 1315 payment_status="manual" if transitioned_to_manual else None, 1316 customer_tier=customer_tier, 1317 tier_warning=tier_warning, 1318 warnings=warnings, 1319 ) 1320 1321 return OrganizationPaymentConfigUpdateResult( 1322 success=True, 1323 message=f"Grace period set for org {organization_id}. " 1324 f"New status: {data['paymentStatus']}.", 1325 organization_id=organization_id, 1326 payment_status=data["paymentStatus"], 1327 grace_period_end_at=data.get("gracePeriodEndAt"), 1328 permanent_waiver_type=data.get("usageCategoryOverwrite"), 1329 customer_tier=customer_tier, 1330 tier_warning=tier_warning, 1331 warnings=warnings, 1332 ) 1333 1334 1335class TierLookupEntry(BaseModel): 1336 """A single resolved tier entry for an org, workspace, or connection.""" 1337 1338 input_id: str = Field(description="The original ID that was looked up") 1339 input_type: str = Field( 1340 description="Type of the input ID: 'organization', 'workspace', or 'connection'" 1341 ) 1342 organization_id: str | None = Field( 1343 default=None, description="Resolved organization UUID" 1344 ) 1345 workspace_id: str | None = Field( 1346 default=None, 1347 description="Workspace UUID (if input was workspace or connection)", 1348 ) 1349 connection_id: str | None = Field( 1350 default=None, description="Connection UUID (if input was connection)" 1351 ) 1352 customer_tier: CustomerTier = Field( 1353 description="Resolved tier: TIER_0, TIER_1, TIER_2, or UNKNOWN" 1354 ) 1355 dataplane_name: str | None = Field( 1356 default=None, description="Dataplane region name (e.g., 'US', 'EU')" 1357 ) 1358 is_eu: bool = Field( 1359 default=False, description="Whether the entity is in the EU region" 1360 ) 1361 resolved: bool = Field( 1362 default=True, description="Whether the ID was successfully resolved" 1363 ) 1364 warnings: list[str] = Field( 1365 default_factory=list, 1366 description="Warnings raised while resolving this result.", 1367 ) 1368 1369 1370class TierLookupResult(BaseModel): 1371 """Result of a customer tier lookup across multiple IDs.""" 1372 1373 entries: list[TierLookupEntry] = Field( 1374 description="Individual tier lookup results for each input ID" 1375 ) 1376 summary: TierSummary = Field( 1377 description="Tier distribution summary across all resolved entries" 1378 ) 1379 summary_text: str = Field(description="Human-readable tier distribution summary") 1380 warnings: list[str] = Field( 1381 default_factory=list, 1382 description="Warnings raised while resolving this result.", 1383 ) 1384 1385 1386class TierCacheRefreshResult(BaseModel): 1387 """Result of refreshing the tier cache.""" 1388 1389 stats: TierCacheStats = Field(description="Cache statistics after refresh") 1390 message: str = Field(description="Human-readable result message") 1391 1392 1393@mcp_tool( 1394 read_only=True, 1395 idempotent=True, 1396) 1397def lookup_customer_tiers( 1398 organization_ids: Annotated[ 1399 list[str] | None, 1400 Field( 1401 description=( 1402 "List of organization UUIDs to look up tiers for. " 1403 "Example: ['664c690e-5263-49ba-b01f-4a6759b3330a']" 1404 ), 1405 default=None, 1406 ), 1407 ] = None, 1408 workspace_ids: Annotated[ 1409 list[str] | None, 1410 Field( 1411 description=( 1412 "List of workspace UUIDs to look up tiers for. " 1413 "Each workspace will be resolved to its organization and tier. " 1414 "Example: ['266ebdfe-0d7b-4540-9817-de7e4505ba61']" 1415 ), 1416 default=None, 1417 ), 1418 ] = None, 1419 connection_ids: Annotated[ 1420 list[str] | None, 1421 Field( 1422 description=( 1423 "List of connection UUIDs to look up tiers for. " 1424 "Each connection will be resolved to its workspace, organization, and tier." 1425 ), 1426 default=None, 1427 ), 1428 ] = None, 1429) -> TierLookupResult: 1430 """Look up customer tier classification for organizations, workspaces, and/or connections. 1431 1432 Accepts mixed lists of organization IDs, workspace IDs, and connection IDs. 1433 Resolves each to its organization and maps to a customer tier 1434 (TIER_0, TIER_1, TIER_2, or UNKNOWN). 1435 1436 The GCS export contains one row per Cloud organization, including explicit 1437 Tier 2 rows. Any org not in the export resolves to UNKNOWN. 1438 1439 Returns enriched entries with tier, region (EU/US), and a summary of the distribution. 1440 """ 1441 entries: list[TierLookupEntry] = [] 1442 1443 # Resolve organization IDs directly 1444 org_ids = organization_ids or [] 1445 if org_ids: 1446 org_results = get_org_tiers( 1447 organization_ids=org_ids, 1448 allow_degraded=True, 1449 ) 1450 for result in org_results: 1451 entries.append( 1452 TierLookupEntry( 1453 input_id=result.organization_id, 1454 input_type="organization", 1455 organization_id=result.organization_id, 1456 customer_tier=result.customer_tier, 1457 warnings=tier_source_warnings(result.source_health), 1458 resolved=True, 1459 ) 1460 ) 1461 1462 # Resolve workspace IDs -> org -> tier 1463 ws_ids = workspace_ids or [] 1464 if ws_ids: 1465 ws_results = resolve_workspaces( 1466 workspace_ids=ws_ids, 1467 allow_degraded=True, 1468 ) 1469 for result in ws_results: 1470 entries.append( 1471 TierLookupEntry( 1472 input_id=result.workspace_id, 1473 input_type="workspace", 1474 organization_id=result.organization_id, 1475 workspace_id=result.workspace_id, 1476 customer_tier=result.customer_tier, 1477 warnings=tier_source_warnings(result.source_health), 1478 dataplane_name=result.dataplane_name, 1479 is_eu=result.is_eu, 1480 resolved=result.resolved, 1481 ) 1482 ) 1483 1484 # Resolve connection IDs -> workspace -> org -> tier 1485 conn_ids = connection_ids or [] 1486 if conn_ids: 1487 conn_details = query_connection_workspace_details(conn_ids) 1488 conn_map: dict[str, dict[str, Any]] = { 1489 str(row["connection_id"]): row for row in conn_details 1490 } 1491 1492 for conn_id in conn_ids: 1493 row = conn_map.get(conn_id) 1494 if row is None: 1495 entries.append( 1496 TierLookupEntry( 1497 input_id=conn_id, 1498 input_type="connection", 1499 connection_id=conn_id, 1500 customer_tier="UNKNOWN", 1501 resolved=False, 1502 ) 1503 ) 1504 continue 1505 1506 org_id = str(row["organization_id"]) 1507 dataplane_name = row.get("dataplane_name") or "US" 1508 org_result = get_org_tier( 1509 organization_id=org_id, 1510 allow_degraded=True, 1511 ) 1512 1513 entries.append( 1514 TierLookupEntry( 1515 input_id=conn_id, 1516 input_type="connection", 1517 organization_id=org_id, 1518 workspace_id=str(row["workspace_id"]), 1519 connection_id=conn_id, 1520 customer_tier=org_result.customer_tier, 1521 warnings=tier_source_warnings(org_result.source_health), 1522 dataplane_name=dataplane_name, 1523 is_eu=dataplane_name == "EU", 1524 resolved=True, 1525 ) 1526 ) 1527 1528 # Build summary from enriched entries 1529 summary_rows = [{"customer_tier": e.customer_tier} for e in entries if e.resolved] 1530 summary = build_tier_summary(summary_rows) 1531 warnings = sorted({warning for entry in entries for warning in entry.warnings}) 1532 1533 return TierLookupResult( 1534 entries=entries, 1535 summary=summary, 1536 summary_text=str(summary), 1537 warnings=warnings, 1538 ) 1539 1540 1541@mcp_tool( 1542 read_only=False, 1543 idempotent=True, 1544) 1545def refresh_customer_tier_cache() -> TierCacheRefreshResult: 1546 """Force-refresh the customer tier cache from the GCS export. 1547 1548 The tier cache is automatically refreshed every 24 hours. Use this tool to 1549 manually trigger a refresh if you need the latest tier data immediately 1550 (e.g., after a Salesforce update). 1551 1552 Returns cache statistics after the refresh. 1553 """ 1554 stats = refresh_tier_cache() 1555 return TierCacheRefreshResult( 1556 stats=stats, 1557 message=( 1558 f"Tier cache refreshed: {stats.tier_cache_size} orgs cached, " 1559 f"{stats.workspace_cache_size} workspaces cached." 1560 ), 1561 ) 1562 1563 1564@mcp_tool( 1565 read_only=True, 1566 idempotent=True, 1567) 1568def get_customer_tier_cache_stats() -> TierCacheStats: 1569 """Get current statistics about the customer tier cache. 1570 1571 Returns cache size, age, and file paths for both the tier cache 1572 (org -> tier) and workspace cache (workspace -> org + region). 1573 1574 Useful for checking cache freshness and diagnosing issues. 1575 """ 1576 return get_cache_stats() 1577 1578 1579def register_organization_admin_tools(app: FastMCP) -> None: 1580 """Register organization_admin tools with the FastMCP app.""" 1581 register_mcp_tools(app, mcp_module=__name__)