airbyte-internal-ops 0.2.0__py3-none-any.whl → 0.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {airbyte_internal_ops-0.2.0.dist-info → airbyte_internal_ops-0.2.2.dist-info}/METADATA +19 -3
- {airbyte_internal_ops-0.2.0.dist-info → airbyte_internal_ops-0.2.2.dist-info}/RECORD +41 -41
- airbyte_ops_mcp/__init__.py +2 -2
- airbyte_ops_mcp/cli/cloud.py +207 -306
- airbyte_ops_mcp/cloud_admin/api_client.py +51 -26
- airbyte_ops_mcp/cloud_admin/connection_config.py +2 -2
- airbyte_ops_mcp/constants.py +61 -1
- airbyte_ops_mcp/github_actions.py +69 -1
- airbyte_ops_mcp/mcp/_http_headers.py +56 -0
- airbyte_ops_mcp/mcp/_mcp_utils.py +2 -2
- airbyte_ops_mcp/mcp/cloud_connector_versions.py +57 -43
- airbyte_ops_mcp/mcp/github.py +34 -1
- airbyte_ops_mcp/mcp/prerelease.py +3 -3
- airbyte_ops_mcp/mcp/prod_db_queries.py +293 -50
- airbyte_ops_mcp/mcp/{live_tests.py → regression_tests.py} +158 -176
- airbyte_ops_mcp/mcp/server.py +3 -3
- airbyte_ops_mcp/prod_db_access/db_engine.py +7 -11
- airbyte_ops_mcp/prod_db_access/queries.py +79 -0
- airbyte_ops_mcp/prod_db_access/sql.py +86 -0
- airbyte_ops_mcp/{live_tests → regression_tests}/__init__.py +3 -3
- airbyte_ops_mcp/{live_tests → regression_tests}/cdk_secrets.py +1 -1
- airbyte_ops_mcp/{live_tests → regression_tests}/connection_secret_retriever.py +3 -3
- airbyte_ops_mcp/{live_tests → regression_tests}/connector_runner.py +1 -1
- airbyte_ops_mcp/{live_tests → regression_tests}/message_cache/__init__.py +3 -1
- airbyte_ops_mcp/{live_tests → regression_tests}/regression/__init__.py +1 -1
- airbyte_ops_mcp/{live_tests → regression_tests}/schema_generation.py +3 -1
- airbyte_ops_mcp/{live_tests → regression_tests}/validation/__init__.py +2 -2
- airbyte_ops_mcp/{live_tests → regression_tests}/validation/record_validators.py +4 -2
- {airbyte_internal_ops-0.2.0.dist-info → airbyte_internal_ops-0.2.2.dist-info}/WHEEL +0 -0
- {airbyte_internal_ops-0.2.0.dist-info → airbyte_internal_ops-0.2.2.dist-info}/entry_points.txt +0 -0
- /airbyte_ops_mcp/{live_tests → regression_tests}/ci_output.py +0 -0
- /airbyte_ops_mcp/{live_tests → regression_tests}/commons/__init__.py +0 -0
- /airbyte_ops_mcp/{live_tests → regression_tests}/config.py +0 -0
- /airbyte_ops_mcp/{live_tests → regression_tests}/connection_fetcher.py +0 -0
- /airbyte_ops_mcp/{live_tests → regression_tests}/evaluation_modes.py +0 -0
- /airbyte_ops_mcp/{live_tests → regression_tests}/http_metrics.py +0 -0
- /airbyte_ops_mcp/{live_tests → regression_tests}/message_cache/duckdb_cache.py +0 -0
- /airbyte_ops_mcp/{live_tests → regression_tests}/models.py +0 -0
- /airbyte_ops_mcp/{live_tests → regression_tests}/obfuscation.py +0 -0
- /airbyte_ops_mcp/{live_tests → regression_tests}/regression/comparators.py +0 -0
- /airbyte_ops_mcp/{live_tests → regression_tests}/validation/catalog_validators.py +0 -0
airbyte_ops_mcp/mcp/github.py
CHANGED
|
@@ -14,12 +14,27 @@ import requests
|
|
|
14
14
|
from fastmcp import FastMCP
|
|
15
15
|
from pydantic import BaseModel, Field
|
|
16
16
|
|
|
17
|
-
from airbyte_ops_mcp.github_actions import
|
|
17
|
+
from airbyte_ops_mcp.github_actions import (
|
|
18
|
+
GITHUB_API_BASE,
|
|
19
|
+
get_workflow_jobs,
|
|
20
|
+
resolve_github_token,
|
|
21
|
+
)
|
|
18
22
|
from airbyte_ops_mcp.mcp._mcp_utils import mcp_tool, register_mcp_tools
|
|
19
23
|
|
|
20
24
|
DOCKERHUB_API_BASE = "https://hub.docker.com/v2"
|
|
21
25
|
|
|
22
26
|
|
|
27
|
+
class JobInfo(BaseModel):
|
|
28
|
+
"""Information about a single job in a workflow run."""
|
|
29
|
+
|
|
30
|
+
job_id: int
|
|
31
|
+
name: str
|
|
32
|
+
status: str
|
|
33
|
+
conclusion: str | None = None
|
|
34
|
+
started_at: str | None = None
|
|
35
|
+
completed_at: str | None = None
|
|
36
|
+
|
|
37
|
+
|
|
23
38
|
class WorkflowRunStatus(BaseModel):
|
|
24
39
|
"""Response model for check_workflow_status MCP tool."""
|
|
25
40
|
|
|
@@ -34,6 +49,7 @@ class WorkflowRunStatus(BaseModel):
|
|
|
34
49
|
updated_at: str
|
|
35
50
|
run_started_at: str | None = None
|
|
36
51
|
jobs_url: str
|
|
52
|
+
jobs: list[JobInfo] = []
|
|
37
53
|
|
|
38
54
|
|
|
39
55
|
def _parse_workflow_url(url: str) -> tuple[str, str, int]:
|
|
@@ -148,6 +164,22 @@ def check_workflow_status(
|
|
|
148
164
|
# Get workflow run details
|
|
149
165
|
run_data = _get_workflow_run(owner, repo, run_id, token)
|
|
150
166
|
|
|
167
|
+
# Get jobs for the workflow run (uses upstream function that resolves its own token)
|
|
168
|
+
workflow_jobs = get_workflow_jobs(owner, repo, run_id)
|
|
169
|
+
|
|
170
|
+
# Convert dataclass objects to Pydantic models for the response
|
|
171
|
+
jobs = [
|
|
172
|
+
JobInfo(
|
|
173
|
+
job_id=job.job_id,
|
|
174
|
+
name=job.name,
|
|
175
|
+
status=job.status,
|
|
176
|
+
conclusion=job.conclusion,
|
|
177
|
+
started_at=job.started_at,
|
|
178
|
+
completed_at=job.completed_at,
|
|
179
|
+
)
|
|
180
|
+
for job in workflow_jobs
|
|
181
|
+
]
|
|
182
|
+
|
|
151
183
|
return WorkflowRunStatus(
|
|
152
184
|
run_id=run_data["id"],
|
|
153
185
|
status=run_data["status"],
|
|
@@ -160,6 +192,7 @@ def check_workflow_status(
|
|
|
160
192
|
updated_at=run_data["updated_at"],
|
|
161
193
|
run_started_at=run_data.get("run_started_at"),
|
|
162
194
|
jobs_url=run_data["jobs_url"],
|
|
195
|
+
jobs=jobs,
|
|
163
196
|
)
|
|
164
197
|
|
|
165
198
|
|
|
@@ -228,17 +228,17 @@ def publish_connector_to_airbyte_registry(
|
|
|
228
228
|
# Guard: Check for required token
|
|
229
229
|
token = resolve_github_token(PRERELEASE_TOKEN_ENV_VARS)
|
|
230
230
|
|
|
231
|
-
# Get the PR's head
|
|
231
|
+
# Get the PR's head SHA for computing the docker image tag
|
|
232
|
+
# Note: We no longer pass gitref to the workflow - it derives the ref from PR number
|
|
232
233
|
head_info = _get_pr_head_info(
|
|
233
234
|
DEFAULT_REPO_OWNER, DEFAULT_REPO_NAME, pr_number, token
|
|
234
235
|
)
|
|
235
236
|
|
|
236
237
|
# Prepare workflow inputs
|
|
237
|
-
# The workflow
|
|
238
|
+
# The workflow uses refs/pull/{pr}/head directly - no gitref needed
|
|
238
239
|
# Note: The workflow auto-detects modified connectors from the PR
|
|
239
240
|
workflow_inputs = {
|
|
240
241
|
"repo": f"{DEFAULT_REPO_OWNER}/{DEFAULT_REPO_NAME}",
|
|
241
|
-
"gitref": head_info.ref,
|
|
242
242
|
"pr": str(pr_number),
|
|
243
243
|
}
|
|
244
244
|
|
|
@@ -7,28 +7,78 @@ airbyte_ops_mcp.prod_db_access.queries for use by AI agents.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
from datetime import datetime
|
|
10
11
|
from typing import Annotated, Any
|
|
11
12
|
|
|
12
13
|
import requests
|
|
13
14
|
from airbyte.exceptions import PyAirbyteInputError
|
|
14
15
|
from fastmcp import FastMCP
|
|
15
|
-
from pydantic import Field
|
|
16
|
+
from pydantic import BaseModel, Field
|
|
16
17
|
|
|
18
|
+
from airbyte_ops_mcp.constants import OrganizationAliasEnum
|
|
17
19
|
from airbyte_ops_mcp.mcp._mcp_utils import mcp_tool, register_mcp_tools
|
|
18
20
|
from airbyte_ops_mcp.prod_db_access.queries import (
|
|
19
21
|
query_actors_pinned_to_version,
|
|
20
22
|
query_connections_by_connector,
|
|
23
|
+
query_connections_by_destination_connector,
|
|
21
24
|
query_connector_versions,
|
|
22
25
|
query_dataplanes_list,
|
|
23
26
|
query_failed_sync_attempts_for_connector,
|
|
24
27
|
query_new_connector_releases,
|
|
25
28
|
query_sync_results_for_version,
|
|
26
29
|
query_workspace_info,
|
|
30
|
+
query_workspaces_by_email_domain,
|
|
27
31
|
)
|
|
28
32
|
|
|
29
33
|
# Cloud UI base URL for building connection URLs
|
|
30
34
|
CLOUD_UI_BASE_URL = "https://cloud.airbyte.com"
|
|
31
35
|
|
|
36
|
+
|
|
37
|
+
# =============================================================================
|
|
38
|
+
# Pydantic Models for MCP Tool Responses
|
|
39
|
+
# =============================================================================
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class WorkspaceInfo(BaseModel):
|
|
43
|
+
"""Information about a workspace found by email domain search."""
|
|
44
|
+
|
|
45
|
+
organization_id: str = Field(description="The organization UUID")
|
|
46
|
+
workspace_id: str = Field(description="The workspace UUID")
|
|
47
|
+
workspace_name: str = Field(description="The name of the workspace")
|
|
48
|
+
slug: str | None = Field(
|
|
49
|
+
default=None, description="The workspace slug (URL-friendly identifier)"
|
|
50
|
+
)
|
|
51
|
+
email: str | None = Field(
|
|
52
|
+
default=None, description="The email address associated with the workspace"
|
|
53
|
+
)
|
|
54
|
+
dataplane_group_id: str | None = Field(
|
|
55
|
+
default=None, description="The dataplane group UUID (region)"
|
|
56
|
+
)
|
|
57
|
+
dataplane_name: str | None = Field(
|
|
58
|
+
default=None, description="The name of the dataplane (e.g., 'US', 'EU')"
|
|
59
|
+
)
|
|
60
|
+
created_at: datetime | None = Field(
|
|
61
|
+
default=None, description="When the workspace was created"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class WorkspacesByEmailDomainResult(BaseModel):
|
|
66
|
+
"""Result of looking up workspaces by email domain."""
|
|
67
|
+
|
|
68
|
+
email_domain: str = Field(
|
|
69
|
+
description="The email domain that was searched for (e.g., 'motherduck.com')"
|
|
70
|
+
)
|
|
71
|
+
total_workspaces_found: int = Field(
|
|
72
|
+
description="Total number of workspaces matching the email domain"
|
|
73
|
+
)
|
|
74
|
+
unique_organization_ids: list[str] = Field(
|
|
75
|
+
description="List of unique organization IDs found"
|
|
76
|
+
)
|
|
77
|
+
workspaces: list[WorkspaceInfo] = Field(
|
|
78
|
+
description="List of workspaces matching the email domain"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
32
82
|
# Cloud registry URL for resolving canonical names
|
|
33
83
|
CLOUD_REGISTRY_URL = (
|
|
34
84
|
"https://connectors.airbyte.com/files/registries/v0/cloud_registry.json"
|
|
@@ -36,13 +86,18 @@ CLOUD_REGISTRY_URL = (
|
|
|
36
86
|
|
|
37
87
|
|
|
38
88
|
def _resolve_canonical_name_to_definition_id(canonical_name: str) -> str:
|
|
39
|
-
"""Resolve a canonical
|
|
89
|
+
"""Resolve a canonical connector name to a definition ID.
|
|
90
|
+
|
|
91
|
+
Auto-detects whether the connector is a source or destination based on the
|
|
92
|
+
canonical name prefix ("source-" or "destination-"). If no prefix is present,
|
|
93
|
+
searches both sources and destinations.
|
|
40
94
|
|
|
41
95
|
Args:
|
|
42
|
-
canonical_name: Canonical
|
|
96
|
+
canonical_name: Canonical connector name (e.g., 'source-youtube-analytics',
|
|
97
|
+
'destination-duckdb', 'YouTube Analytics', 'DuckDB').
|
|
43
98
|
|
|
44
99
|
Returns:
|
|
45
|
-
The
|
|
100
|
+
The connector definition ID (UUID).
|
|
46
101
|
|
|
47
102
|
Raises:
|
|
48
103
|
PyAirbyteInputError: If the canonical name cannot be resolved.
|
|
@@ -56,31 +111,65 @@ def _resolve_canonical_name_to_definition_id(canonical_name: str) -> str:
|
|
|
56
111
|
)
|
|
57
112
|
|
|
58
113
|
data = response.json()
|
|
59
|
-
sources = data.get("sources", [])
|
|
60
|
-
|
|
61
|
-
# Normalize the canonical name for matching
|
|
62
114
|
normalized_input = canonical_name.lower().strip()
|
|
63
115
|
|
|
64
|
-
#
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
116
|
+
# Determine which registries to search based on prefix
|
|
117
|
+
is_source = normalized_input.startswith("source-")
|
|
118
|
+
is_destination = normalized_input.startswith("destination-")
|
|
119
|
+
|
|
120
|
+
# Search sources if it looks like a source or has no prefix
|
|
121
|
+
if is_source or not is_destination:
|
|
122
|
+
sources = data.get("sources", [])
|
|
123
|
+
for source in sources:
|
|
124
|
+
source_name = source.get("name", "").lower()
|
|
125
|
+
if source_name == normalized_input:
|
|
126
|
+
return source["sourceDefinitionId"]
|
|
127
|
+
slugified = source_name.replace(" ", "-")
|
|
128
|
+
if (
|
|
129
|
+
slugified == normalized_input
|
|
130
|
+
or f"source-{slugified}" == normalized_input
|
|
131
|
+
):
|
|
132
|
+
return source["sourceDefinitionId"]
|
|
133
|
+
|
|
134
|
+
# Search destinations if it looks like a destination or has no prefix
|
|
135
|
+
if is_destination or not is_source:
|
|
136
|
+
destinations = data.get("destinations", [])
|
|
137
|
+
for destination in destinations:
|
|
138
|
+
destination_name = destination.get("name", "").lower()
|
|
139
|
+
if destination_name == normalized_input:
|
|
140
|
+
return destination["destinationDefinitionId"]
|
|
141
|
+
slugified = destination_name.replace(" ", "-")
|
|
142
|
+
if (
|
|
143
|
+
slugified == normalized_input
|
|
144
|
+
or f"destination-{slugified}" == normalized_input
|
|
145
|
+
):
|
|
146
|
+
return destination["destinationDefinitionId"]
|
|
147
|
+
|
|
148
|
+
# Build appropriate error message based on what was searched
|
|
149
|
+
if is_source:
|
|
150
|
+
connector_type = "source"
|
|
151
|
+
hint = (
|
|
152
|
+
"Use the exact canonical name (e.g., 'source-youtube-analytics') "
|
|
153
|
+
"or display name (e.g., 'YouTube Analytics')."
|
|
154
|
+
)
|
|
155
|
+
elif is_destination:
|
|
156
|
+
connector_type = "destination"
|
|
157
|
+
hint = (
|
|
158
|
+
"Use the exact canonical name (e.g., 'destination-duckdb') "
|
|
159
|
+
"or display name (e.g., 'DuckDB')."
|
|
160
|
+
)
|
|
161
|
+
else:
|
|
162
|
+
connector_type = "connector"
|
|
163
|
+
hint = (
|
|
164
|
+
"Use the exact canonical name (e.g., 'source-youtube-analytics', "
|
|
165
|
+
"'destination-duckdb') or display name (e.g., 'YouTube Analytics', 'DuckDB')."
|
|
166
|
+
)
|
|
77
167
|
|
|
78
168
|
raise PyAirbyteInputError(
|
|
79
|
-
message=f"Could not find
|
|
169
|
+
message=f"Could not find {connector_type} definition for canonical name: {canonical_name}",
|
|
80
170
|
context={
|
|
81
|
-
"hint":
|
|
82
|
-
"
|
|
83
|
-
"You can list available sources using the connector registry tools.",
|
|
171
|
+
"hint": hint
|
|
172
|
+
+ " You can list available connectors using the connector registry tools.",
|
|
84
173
|
"searched_for": canonical_name,
|
|
85
174
|
},
|
|
86
175
|
)
|
|
@@ -275,11 +364,12 @@ def query_prod_failed_sync_attempts_for_connector(
|
|
|
275
364
|
),
|
|
276
365
|
] = None,
|
|
277
366
|
organization_id: Annotated[
|
|
278
|
-
str | None,
|
|
367
|
+
str | OrganizationAliasEnum | None,
|
|
279
368
|
Field(
|
|
280
369
|
description=(
|
|
281
|
-
"Optional organization ID (UUID) to filter results. "
|
|
282
|
-
"If provided, only failed attempts from this organization will be returned."
|
|
370
|
+
"Optional organization ID (UUID) or alias to filter results. "
|
|
371
|
+
"If provided, only failed attempts from this organization will be returned. "
|
|
372
|
+
"Accepts '@airbyte-internal' as an alias for the Airbyte internal org."
|
|
283
373
|
),
|
|
284
374
|
default=None,
|
|
285
375
|
),
|
|
@@ -327,9 +417,12 @@ def query_prod_failed_sync_attempts_for_connector(
|
|
|
327
417
|
else:
|
|
328
418
|
resolved_definition_id = source_definition_id # type: ignore[assignment]
|
|
329
419
|
|
|
420
|
+
# Resolve organization ID alias
|
|
421
|
+
resolved_organization_id = OrganizationAliasEnum.resolve(organization_id)
|
|
422
|
+
|
|
330
423
|
return query_failed_sync_attempts_for_connector(
|
|
331
424
|
connector_definition_id=resolved_definition_id,
|
|
332
|
-
organization_id=
|
|
425
|
+
organization_id=resolved_organization_id,
|
|
333
426
|
days=days,
|
|
334
427
|
limit=limit,
|
|
335
428
|
)
|
|
@@ -346,7 +439,8 @@ def query_prod_connections_by_connector(
|
|
|
346
439
|
Field(
|
|
347
440
|
description=(
|
|
348
441
|
"Source connector definition ID (UUID) to search for. "
|
|
349
|
-
"Exactly one of
|
|
442
|
+
"Exactly one of source_definition_id, source_canonical_name, "
|
|
443
|
+
"destination_definition_id, or destination_canonical_name is required. "
|
|
350
444
|
"Example: 'afa734e4-3571-11ec-991a-1e0031268139' for YouTube Analytics."
|
|
351
445
|
),
|
|
352
446
|
default=None,
|
|
@@ -357,18 +451,44 @@ def query_prod_connections_by_connector(
|
|
|
357
451
|
Field(
|
|
358
452
|
description=(
|
|
359
453
|
"Canonical source connector name to search for. "
|
|
360
|
-
"Exactly one of
|
|
454
|
+
"Exactly one of source_definition_id, source_canonical_name, "
|
|
455
|
+
"destination_definition_id, or destination_canonical_name is required. "
|
|
361
456
|
"Examples: 'source-youtube-analytics', 'YouTube Analytics'."
|
|
362
457
|
),
|
|
363
458
|
default=None,
|
|
364
459
|
),
|
|
365
460
|
] = None,
|
|
366
|
-
|
|
461
|
+
destination_definition_id: Annotated[
|
|
367
462
|
str | None,
|
|
368
463
|
Field(
|
|
369
464
|
description=(
|
|
370
|
-
"
|
|
371
|
-
"
|
|
465
|
+
"Destination connector definition ID (UUID) to search for. "
|
|
466
|
+
"Exactly one of source_definition_id, source_canonical_name, "
|
|
467
|
+
"destination_definition_id, or destination_canonical_name is required. "
|
|
468
|
+
"Example: 'e5c8e66c-a480-4a5e-9c0e-e8e5e4c5c5c5' for DuckDB."
|
|
469
|
+
),
|
|
470
|
+
default=None,
|
|
471
|
+
),
|
|
472
|
+
] = None,
|
|
473
|
+
destination_canonical_name: Annotated[
|
|
474
|
+
str | None,
|
|
475
|
+
Field(
|
|
476
|
+
description=(
|
|
477
|
+
"Canonical destination connector name to search for. "
|
|
478
|
+
"Exactly one of source_definition_id, source_canonical_name, "
|
|
479
|
+
"destination_definition_id, or destination_canonical_name is required. "
|
|
480
|
+
"Examples: 'destination-duckdb', 'DuckDB'."
|
|
481
|
+
),
|
|
482
|
+
default=None,
|
|
483
|
+
),
|
|
484
|
+
] = None,
|
|
485
|
+
organization_id: Annotated[
|
|
486
|
+
str | OrganizationAliasEnum | None,
|
|
487
|
+
Field(
|
|
488
|
+
description=(
|
|
489
|
+
"Optional organization ID (UUID) or alias to filter results. "
|
|
490
|
+
"If provided, only connections in this organization will be returned. "
|
|
491
|
+
"Accepts '@airbyte-internal' as an alias for the Airbyte internal org."
|
|
372
492
|
),
|
|
373
493
|
default=None,
|
|
374
494
|
),
|
|
@@ -378,38 +498,88 @@ def query_prod_connections_by_connector(
|
|
|
378
498
|
Field(description="Maximum number of results (default: 1000)", default=1000),
|
|
379
499
|
] = 1000,
|
|
380
500
|
) -> list[dict[str, Any]]:
|
|
381
|
-
"""Search for all connections using a specific source connector type.
|
|
501
|
+
"""Search for all connections using a specific source or destination connector type.
|
|
382
502
|
|
|
383
503
|
This tool queries the Airbyte Cloud Prod DB Replica directly for fast results.
|
|
384
|
-
It finds all connections where the source connector matches the
|
|
385
|
-
regardless of how the
|
|
504
|
+
It finds all connections where the source or destination connector matches the
|
|
505
|
+
specified type, regardless of how the connector is named by users.
|
|
386
506
|
|
|
387
507
|
Optionally filter by organization_id to limit results to a specific organization.
|
|
508
|
+
Use '@airbyte-internal' as an alias for the Airbyte internal organization.
|
|
388
509
|
|
|
389
510
|
Returns a list of connection dicts with workspace context and clickable Cloud UI URLs.
|
|
390
|
-
|
|
511
|
+
For source queries, returns: connection_id, connection_name, connection_url, source_id,
|
|
391
512
|
source_name, source_definition_id, workspace_id, workspace_name, organization_id,
|
|
392
513
|
dataplane_group_id, dataplane_name.
|
|
514
|
+
For destination queries, returns: connection_id, connection_name, connection_url,
|
|
515
|
+
destination_id, destination_name, destination_definition_id, workspace_id,
|
|
516
|
+
workspace_name, organization_id, dataplane_group_id, dataplane_name.
|
|
393
517
|
"""
|
|
394
|
-
# Validate that exactly one of the
|
|
395
|
-
|
|
518
|
+
# Validate that exactly one of the four connector parameters is provided
|
|
519
|
+
provided_params = [
|
|
520
|
+
source_definition_id,
|
|
521
|
+
source_canonical_name,
|
|
522
|
+
destination_definition_id,
|
|
523
|
+
destination_canonical_name,
|
|
524
|
+
]
|
|
525
|
+
num_provided = sum(p is not None for p in provided_params)
|
|
526
|
+
if num_provided != 1:
|
|
396
527
|
raise PyAirbyteInputError(
|
|
397
528
|
message=(
|
|
398
|
-
"Exactly one of source_definition_id
|
|
399
|
-
"must be provided
|
|
529
|
+
"Exactly one of source_definition_id, source_canonical_name, "
|
|
530
|
+
"destination_definition_id, or destination_canonical_name must be provided."
|
|
400
531
|
),
|
|
401
532
|
)
|
|
402
533
|
|
|
403
|
-
#
|
|
534
|
+
# Determine if this is a source or destination query and resolve the definition ID
|
|
535
|
+
is_source_query = (
|
|
536
|
+
source_definition_id is not None or source_canonical_name is not None
|
|
537
|
+
)
|
|
404
538
|
resolved_definition_id: str
|
|
539
|
+
|
|
405
540
|
if source_canonical_name:
|
|
406
541
|
resolved_definition_id = _resolve_canonical_name_to_definition_id(
|
|
407
542
|
canonical_name=source_canonical_name,
|
|
408
543
|
)
|
|
544
|
+
elif source_definition_id:
|
|
545
|
+
resolved_definition_id = source_definition_id
|
|
546
|
+
elif destination_canonical_name:
|
|
547
|
+
resolved_definition_id = _resolve_canonical_name_to_definition_id(
|
|
548
|
+
canonical_name=destination_canonical_name,
|
|
549
|
+
)
|
|
409
550
|
else:
|
|
410
|
-
resolved_definition_id =
|
|
411
|
-
|
|
412
|
-
#
|
|
551
|
+
resolved_definition_id = destination_definition_id # type: ignore[assignment]
|
|
552
|
+
|
|
553
|
+
# Resolve organization ID alias
|
|
554
|
+
resolved_organization_id = OrganizationAliasEnum.resolve(organization_id)
|
|
555
|
+
|
|
556
|
+
# Query the database based on connector type
|
|
557
|
+
if is_source_query:
|
|
558
|
+
return [
|
|
559
|
+
{
|
|
560
|
+
"organization_id": str(row.get("organization_id", "")),
|
|
561
|
+
"workspace_id": str(row["workspace_id"]),
|
|
562
|
+
"workspace_name": row.get("workspace_name", ""),
|
|
563
|
+
"connection_id": str(row["connection_id"]),
|
|
564
|
+
"connection_name": row.get("connection_name", ""),
|
|
565
|
+
"connection_url": (
|
|
566
|
+
f"{CLOUD_UI_BASE_URL}/workspaces/{row['workspace_id']}"
|
|
567
|
+
f"/connections/{row['connection_id']}/status"
|
|
568
|
+
),
|
|
569
|
+
"source_id": str(row["source_id"]),
|
|
570
|
+
"source_name": row.get("source_name", ""),
|
|
571
|
+
"source_definition_id": str(row["source_definition_id"]),
|
|
572
|
+
"dataplane_group_id": str(row.get("dataplane_group_id", "")),
|
|
573
|
+
"dataplane_name": row.get("dataplane_name", ""),
|
|
574
|
+
}
|
|
575
|
+
for row in query_connections_by_connector(
|
|
576
|
+
connector_definition_id=resolved_definition_id,
|
|
577
|
+
organization_id=resolved_organization_id,
|
|
578
|
+
limit=limit,
|
|
579
|
+
)
|
|
580
|
+
]
|
|
581
|
+
|
|
582
|
+
# Destination query
|
|
413
583
|
return [
|
|
414
584
|
{
|
|
415
585
|
"organization_id": str(row.get("organization_id", "")),
|
|
@@ -421,20 +591,93 @@ def query_prod_connections_by_connector(
|
|
|
421
591
|
f"{CLOUD_UI_BASE_URL}/workspaces/{row['workspace_id']}"
|
|
422
592
|
f"/connections/{row['connection_id']}/status"
|
|
423
593
|
),
|
|
424
|
-
"
|
|
425
|
-
"
|
|
426
|
-
"
|
|
594
|
+
"destination_id": str(row["destination_id"]),
|
|
595
|
+
"destination_name": row.get("destination_name", ""),
|
|
596
|
+
"destination_definition_id": str(row["destination_definition_id"]),
|
|
427
597
|
"dataplane_group_id": str(row.get("dataplane_group_id", "")),
|
|
428
598
|
"dataplane_name": row.get("dataplane_name", ""),
|
|
429
599
|
}
|
|
430
|
-
for row in
|
|
600
|
+
for row in query_connections_by_destination_connector(
|
|
431
601
|
connector_definition_id=resolved_definition_id,
|
|
432
|
-
organization_id=
|
|
602
|
+
organization_id=resolved_organization_id,
|
|
433
603
|
limit=limit,
|
|
434
604
|
)
|
|
435
605
|
]
|
|
436
606
|
|
|
437
607
|
|
|
608
|
+
@mcp_tool(
|
|
609
|
+
read_only=True,
|
|
610
|
+
idempotent=True,
|
|
611
|
+
)
|
|
612
|
+
def query_prod_workspaces_by_email_domain(
|
|
613
|
+
email_domain: Annotated[
|
|
614
|
+
str,
|
|
615
|
+
Field(
|
|
616
|
+
description=(
|
|
617
|
+
"Email domain to search for (e.g., 'motherduck.com', 'fivetran.com'). "
|
|
618
|
+
"Do not include the '@' symbol. This will find workspaces where users "
|
|
619
|
+
"have email addresses with this domain."
|
|
620
|
+
),
|
|
621
|
+
),
|
|
622
|
+
],
|
|
623
|
+
limit: Annotated[
|
|
624
|
+
int,
|
|
625
|
+
Field(
|
|
626
|
+
description="Maximum number of workspaces to return (default: 100)",
|
|
627
|
+
default=100,
|
|
628
|
+
),
|
|
629
|
+
] = 100,
|
|
630
|
+
) -> WorkspacesByEmailDomainResult:
|
|
631
|
+
"""Find workspaces by email domain.
|
|
632
|
+
|
|
633
|
+
This tool searches for workspaces where users have email addresses matching
|
|
634
|
+
the specified domain. This is useful for identifying workspaces belonging to
|
|
635
|
+
specific companies - for example, searching for "motherduck.com" will find
|
|
636
|
+
workspaces belonging to MotherDuck employees.
|
|
637
|
+
|
|
638
|
+
Use cases:
|
|
639
|
+
- Finding partner organization connections for testing connector fixes
|
|
640
|
+
- Identifying internal test accounts for specific integrations
|
|
641
|
+
- Locating workspaces belonging to technology partners
|
|
642
|
+
|
|
643
|
+
The returned organization IDs can be used with other tools like
|
|
644
|
+
`query_prod_connections_by_connector` to find connections within
|
|
645
|
+
those organizations for safe testing.
|
|
646
|
+
"""
|
|
647
|
+
# Strip leading @ if provided
|
|
648
|
+
clean_domain = email_domain.lstrip("@")
|
|
649
|
+
|
|
650
|
+
# Query the database
|
|
651
|
+
rows = query_workspaces_by_email_domain(email_domain=clean_domain, limit=limit)
|
|
652
|
+
|
|
653
|
+
# Convert rows to Pydantic models
|
|
654
|
+
workspaces = [
|
|
655
|
+
WorkspaceInfo(
|
|
656
|
+
organization_id=str(row["organization_id"]),
|
|
657
|
+
workspace_id=str(row["workspace_id"]),
|
|
658
|
+
workspace_name=row.get("workspace_name", ""),
|
|
659
|
+
slug=row.get("slug"),
|
|
660
|
+
email=row.get("email"),
|
|
661
|
+
dataplane_group_id=str(row["dataplane_group_id"])
|
|
662
|
+
if row.get("dataplane_group_id")
|
|
663
|
+
else None,
|
|
664
|
+
dataplane_name=row.get("dataplane_name"),
|
|
665
|
+
created_at=row.get("created_at"),
|
|
666
|
+
)
|
|
667
|
+
for row in rows
|
|
668
|
+
]
|
|
669
|
+
|
|
670
|
+
# Extract unique organization IDs
|
|
671
|
+
unique_org_ids = list(dict.fromkeys(w.organization_id for w in workspaces))
|
|
672
|
+
|
|
673
|
+
return WorkspacesByEmailDomainResult(
|
|
674
|
+
email_domain=clean_domain,
|
|
675
|
+
total_workspaces_found=len(workspaces),
|
|
676
|
+
unique_organization_ids=unique_org_ids,
|
|
677
|
+
workspaces=workspaces,
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
|
|
438
681
|
def register_prod_db_query_tools(app: FastMCP) -> None:
|
|
439
682
|
"""Register prod DB query tools with the FastMCP app."""
|
|
440
683
|
register_mcp_tools(app, domain=__name__)
|