airbyte-internal-ops 0.1.4__py3-none-any.whl → 0.1.6__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.1.4.dist-info → airbyte_internal_ops-0.1.6.dist-info}/METADATA +70 -1
- {airbyte_internal_ops-0.1.4.dist-info → airbyte_internal_ops-0.1.6.dist-info}/RECORD +30 -31
- airbyte_ops_mcp/__init__.py +30 -2
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_pipelines/airbyte_ci/connectors/pipeline.py +2 -8
- airbyte_ops_mcp/airbyte_repo/list_connectors.py +176 -4
- airbyte_ops_mcp/airbyte_repo/utils.py +5 -3
- airbyte_ops_mcp/cli/cloud.py +35 -36
- airbyte_ops_mcp/cli/registry.py +90 -1
- airbyte_ops_mcp/cli/repo.py +15 -0
- airbyte_ops_mcp/connection_config_retriever/__init__.py +26 -0
- airbyte_ops_mcp/{live_tests/_connection_retriever → connection_config_retriever}/audit_logging.py +5 -6
- airbyte_ops_mcp/{live_tests/_connection_retriever → connection_config_retriever}/retrieval.py +8 -22
- airbyte_ops_mcp/{live_tests/_connection_retriever → connection_config_retriever}/secrets_resolution.py +8 -42
- airbyte_ops_mcp/constants.py +35 -0
- airbyte_ops_mcp/live_tests/connection_secret_retriever.py +1 -1
- airbyte_ops_mcp/mcp/github_repo_ops.py +10 -0
- airbyte_ops_mcp/mcp/live_tests.py +21 -6
- airbyte_ops_mcp/mcp/prod_db_queries.py +357 -0
- airbyte_ops_mcp/mcp/server.py +2 -0
- airbyte_ops_mcp/mcp/server_info.py +2 -2
- airbyte_ops_mcp/prod_db_access/__init__.py +34 -0
- airbyte_ops_mcp/prod_db_access/db_engine.py +127 -0
- airbyte_ops_mcp/prod_db_access/py.typed +0 -0
- airbyte_ops_mcp/prod_db_access/queries.py +272 -0
- airbyte_ops_mcp/prod_db_access/sql.py +353 -0
- airbyte_ops_mcp/registry/__init__.py +34 -0
- airbyte_ops_mcp/registry/models.py +63 -0
- airbyte_ops_mcp/registry/publish.py +368 -0
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_pipelines/airbyte_ci/connectors/publish/__init__.py +0 -3
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_pipelines/airbyte_ci/connectors/publish/commands.py +0 -242
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_pipelines/airbyte_ci/connectors/publish/context.py +0 -175
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_pipelines/airbyte_ci/connectors/publish/pipeline.py +0 -1056
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_pipelines/airbyte_ci/poetry/publish/__init__.py +0 -3
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_pipelines/airbyte_ci/poetry/publish/commands.py +0 -127
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_pipelines/airbyte_ci/steps/python_registry.py +0 -238
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_pipelines/models/contexts/python_registry_publish.py +0 -119
- airbyte_ops_mcp/live_tests/_connection_retriever/__init__.py +0 -35
- airbyte_ops_mcp/live_tests/_connection_retriever/consts.py +0 -33
- airbyte_ops_mcp/live_tests/_connection_retriever/db_access.py +0 -82
- {airbyte_internal_ops-0.1.4.dist-info → airbyte_internal_ops-0.1.6.dist-info}/WHEEL +0 -0
- {airbyte_internal_ops-0.1.4.dist-info → airbyte_internal_ops-0.1.6.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
|
|
2
|
+
"""MCP tools for querying the Airbyte Cloud Prod DB Replica.
|
|
3
|
+
|
|
4
|
+
This module provides MCP tools that wrap the query functions from
|
|
5
|
+
airbyte_ops_mcp.prod_db_access.queries for use by AI agents.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Annotated, Any
|
|
11
|
+
|
|
12
|
+
import requests
|
|
13
|
+
from airbyte.exceptions import PyAirbyteInputError
|
|
14
|
+
from fastmcp import FastMCP
|
|
15
|
+
from pydantic import Field
|
|
16
|
+
|
|
17
|
+
from airbyte_ops_mcp.mcp._mcp_utils import ToolDomain, mcp_tool, register_mcp_tools
|
|
18
|
+
from airbyte_ops_mcp.prod_db_access.queries import (
|
|
19
|
+
query_actors_pinned_to_version,
|
|
20
|
+
query_connections_by_connector,
|
|
21
|
+
query_connector_versions,
|
|
22
|
+
query_dataplanes_list,
|
|
23
|
+
query_new_connector_releases,
|
|
24
|
+
query_sync_results_for_version,
|
|
25
|
+
query_workspace_info,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Cloud UI base URL for building connection URLs
|
|
29
|
+
CLOUD_UI_BASE_URL = "https://cloud.airbyte.com"
|
|
30
|
+
|
|
31
|
+
# Cloud registry URL for resolving canonical names
|
|
32
|
+
CLOUD_REGISTRY_URL = (
|
|
33
|
+
"https://connectors.airbyte.com/files/registries/v0/cloud_registry.json"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _resolve_canonical_name_to_definition_id(canonical_name: str) -> str:
|
|
38
|
+
"""Resolve a canonical source name to a definition ID.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
canonical_name: Canonical source name (e.g., 'source-youtube-analytics').
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
The source definition ID (UUID).
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
PyAirbyteInputError: If the canonical name cannot be resolved.
|
|
48
|
+
"""
|
|
49
|
+
response = requests.get(CLOUD_REGISTRY_URL, timeout=60)
|
|
50
|
+
|
|
51
|
+
if response.status_code != 200:
|
|
52
|
+
raise PyAirbyteInputError(
|
|
53
|
+
message=f"Failed to fetch connector registry: {response.status_code}",
|
|
54
|
+
context={"response": response.text},
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
data = response.json()
|
|
58
|
+
sources = data.get("sources", [])
|
|
59
|
+
|
|
60
|
+
# Normalize the canonical name for matching
|
|
61
|
+
normalized_input = canonical_name.lower().strip()
|
|
62
|
+
|
|
63
|
+
# Try exact match on name field
|
|
64
|
+
for source in sources:
|
|
65
|
+
source_name = source.get("name", "").lower()
|
|
66
|
+
# The registry returns names like "YouTube Analytics"
|
|
67
|
+
# So we need to handle both formats
|
|
68
|
+
if source_name == normalized_input:
|
|
69
|
+
return source["sourceDefinitionId"]
|
|
70
|
+
|
|
71
|
+
# Also try matching against a slugified version
|
|
72
|
+
# e.g., "YouTube Analytics" -> "youtube-analytics"
|
|
73
|
+
slugified = source_name.replace(" ", "-")
|
|
74
|
+
if slugified == normalized_input or f"source-{slugified}" == normalized_input:
|
|
75
|
+
return source["sourceDefinitionId"]
|
|
76
|
+
|
|
77
|
+
raise PyAirbyteInputError(
|
|
78
|
+
message=f"Could not find source definition for canonical name: {canonical_name}",
|
|
79
|
+
context={
|
|
80
|
+
"hint": "Use the exact canonical name (e.g., 'source-youtube-analytics') "
|
|
81
|
+
"or display name (e.g., 'YouTube Analytics'). "
|
|
82
|
+
"You can list available sources using the connector registry tools.",
|
|
83
|
+
"searched_for": canonical_name,
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@mcp_tool(
|
|
89
|
+
ToolDomain.CLOUD_ADMIN,
|
|
90
|
+
read_only=True,
|
|
91
|
+
idempotent=True,
|
|
92
|
+
)
|
|
93
|
+
def query_prod_dataplanes() -> list[dict[str, Any]]:
|
|
94
|
+
"""List all dataplane groups with workspace counts.
|
|
95
|
+
|
|
96
|
+
Returns information about all active dataplane groups in Airbyte Cloud,
|
|
97
|
+
including the number of workspaces in each. Useful for understanding
|
|
98
|
+
the distribution of workspaces across regions (US, US-Central, EU).
|
|
99
|
+
|
|
100
|
+
Returns list of dicts with keys: dataplane_group_id, dataplane_name,
|
|
101
|
+
organization_id, enabled, tombstone, created_at, workspace_count
|
|
102
|
+
"""
|
|
103
|
+
return query_dataplanes_list()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@mcp_tool(
|
|
107
|
+
ToolDomain.CLOUD_ADMIN,
|
|
108
|
+
read_only=True,
|
|
109
|
+
idempotent=True,
|
|
110
|
+
)
|
|
111
|
+
def query_prod_workspace_info(
|
|
112
|
+
workspace_id: Annotated[
|
|
113
|
+
str,
|
|
114
|
+
Field(description="Workspace UUID to look up"),
|
|
115
|
+
],
|
|
116
|
+
) -> dict[str, Any] | None:
|
|
117
|
+
"""Get workspace information including dataplane group.
|
|
118
|
+
|
|
119
|
+
Returns details about a specific workspace, including which dataplane
|
|
120
|
+
(region) it belongs to. Useful for determining if a workspace is in
|
|
121
|
+
the EU region for filtering purposes.
|
|
122
|
+
|
|
123
|
+
Returns dict with keys: workspace_id, workspace_name, slug, organization_id,
|
|
124
|
+
dataplane_group_id, dataplane_name, created_at, tombstone
|
|
125
|
+
Or None if workspace not found.
|
|
126
|
+
"""
|
|
127
|
+
return query_workspace_info(workspace_id)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@mcp_tool(
|
|
131
|
+
ToolDomain.CLOUD_ADMIN,
|
|
132
|
+
read_only=True,
|
|
133
|
+
idempotent=True,
|
|
134
|
+
)
|
|
135
|
+
def query_prod_connector_versions(
|
|
136
|
+
connector_definition_id: Annotated[
|
|
137
|
+
str,
|
|
138
|
+
Field(description="Connector definition UUID to list versions for"),
|
|
139
|
+
],
|
|
140
|
+
) -> list[dict[str, Any]]:
|
|
141
|
+
"""List all versions for a connector definition.
|
|
142
|
+
|
|
143
|
+
Returns all published versions of a connector, ordered by last_published
|
|
144
|
+
date descending. Useful for understanding version history and finding
|
|
145
|
+
specific version IDs for pinning or rollout monitoring.
|
|
146
|
+
|
|
147
|
+
Returns list of dicts with keys: version_id, docker_image_tag, docker_repository,
|
|
148
|
+
release_stage, support_level, cdk_version, language, last_published, release_date
|
|
149
|
+
"""
|
|
150
|
+
return query_connector_versions(connector_definition_id)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@mcp_tool(
|
|
154
|
+
ToolDomain.CLOUD_ADMIN,
|
|
155
|
+
read_only=True,
|
|
156
|
+
idempotent=True,
|
|
157
|
+
)
|
|
158
|
+
def query_prod_new_connector_releases(
|
|
159
|
+
days: Annotated[
|
|
160
|
+
int,
|
|
161
|
+
Field(description="Number of days to look back (default: 7)", default=7),
|
|
162
|
+
] = 7,
|
|
163
|
+
limit: Annotated[
|
|
164
|
+
int,
|
|
165
|
+
Field(description="Maximum number of results (default: 100)", default=100),
|
|
166
|
+
] = 100,
|
|
167
|
+
) -> list[dict[str, Any]]:
|
|
168
|
+
"""List recently published connector versions.
|
|
169
|
+
|
|
170
|
+
Returns connector versions published within the specified number of days.
|
|
171
|
+
Uses last_published timestamp which reflects when the version was actually
|
|
172
|
+
deployed to the registry (not the changelog date).
|
|
173
|
+
|
|
174
|
+
Returns list of dicts with keys: version_id, connector_definition_id, docker_repository,
|
|
175
|
+
docker_image_tag, last_published, release_date, release_stage, support_level,
|
|
176
|
+
cdk_version, language, created_at
|
|
177
|
+
"""
|
|
178
|
+
return query_new_connector_releases(days=days, limit=limit)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@mcp_tool(
|
|
182
|
+
ToolDomain.CLOUD_ADMIN,
|
|
183
|
+
read_only=True,
|
|
184
|
+
idempotent=True,
|
|
185
|
+
)
|
|
186
|
+
def query_prod_actors_by_connector_version(
|
|
187
|
+
connector_version_id: Annotated[
|
|
188
|
+
str,
|
|
189
|
+
Field(description="Connector version UUID to find pinned instances for"),
|
|
190
|
+
],
|
|
191
|
+
) -> list[dict[str, Any]]:
|
|
192
|
+
"""List actors (sources/destinations) pinned to a specific connector version.
|
|
193
|
+
|
|
194
|
+
Returns all actors that have been explicitly pinned to a specific
|
|
195
|
+
connector version via scoped_configuration. Useful for monitoring
|
|
196
|
+
rollouts and understanding which customers are affected by version pins.
|
|
197
|
+
|
|
198
|
+
The actor_id field is the actor ID (superset of source_id/destination_id).
|
|
199
|
+
|
|
200
|
+
Returns list of dicts with keys: actor_id, connector_definition_id, origin_type,
|
|
201
|
+
origin, description, created_at, expires_at, actor_name, workspace_id,
|
|
202
|
+
workspace_name, organization_id, dataplane_group_id, dataplane_name
|
|
203
|
+
"""
|
|
204
|
+
return query_actors_pinned_to_version(connector_version_id)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@mcp_tool(
|
|
208
|
+
ToolDomain.CLOUD_ADMIN,
|
|
209
|
+
read_only=True,
|
|
210
|
+
idempotent=True,
|
|
211
|
+
)
|
|
212
|
+
def query_prod_connector_version_sync_results(
|
|
213
|
+
connector_version_id: Annotated[
|
|
214
|
+
str,
|
|
215
|
+
Field(description="Connector version UUID to find sync results for"),
|
|
216
|
+
],
|
|
217
|
+
days: Annotated[
|
|
218
|
+
int,
|
|
219
|
+
Field(description="Number of days to look back (default: 7)", default=7),
|
|
220
|
+
] = 7,
|
|
221
|
+
limit: Annotated[
|
|
222
|
+
int,
|
|
223
|
+
Field(description="Maximum number of results (default: 100)", default=100),
|
|
224
|
+
] = 100,
|
|
225
|
+
successful_only: Annotated[
|
|
226
|
+
bool,
|
|
227
|
+
Field(
|
|
228
|
+
description="If True, only return successful syncs (default: False)",
|
|
229
|
+
default=False,
|
|
230
|
+
),
|
|
231
|
+
] = False,
|
|
232
|
+
) -> list[dict[str, Any]]:
|
|
233
|
+
"""List sync job results for actors pinned to a specific connector version.
|
|
234
|
+
|
|
235
|
+
Returns sync job results for connections using actors that are pinned
|
|
236
|
+
to the specified version. Useful for monitoring rollout health and
|
|
237
|
+
identifying issues with specific connector versions.
|
|
238
|
+
|
|
239
|
+
The actor_id field is the actor ID (superset of source_id/destination_id).
|
|
240
|
+
|
|
241
|
+
Returns list of dicts with keys: job_id, connection_id, job_status, started_at,
|
|
242
|
+
job_updated_at, connection_name, actor_id, actor_name, connector_definition_id,
|
|
243
|
+
pin_origin_type, pin_origin, workspace_id, workspace_name, organization_id,
|
|
244
|
+
dataplane_group_id, dataplane_name
|
|
245
|
+
"""
|
|
246
|
+
return query_sync_results_for_version(
|
|
247
|
+
connector_version_id,
|
|
248
|
+
days=days,
|
|
249
|
+
limit=limit,
|
|
250
|
+
successful_only=successful_only,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@mcp_tool(
|
|
255
|
+
ToolDomain.CLOUD_ADMIN,
|
|
256
|
+
read_only=True,
|
|
257
|
+
idempotent=True,
|
|
258
|
+
open_world=True,
|
|
259
|
+
)
|
|
260
|
+
def query_prod_connections_by_connector(
|
|
261
|
+
source_definition_id: Annotated[
|
|
262
|
+
str | None,
|
|
263
|
+
Field(
|
|
264
|
+
description=(
|
|
265
|
+
"Source connector definition ID (UUID) to search for. "
|
|
266
|
+
"Exactly one of this or source_canonical_name is required. "
|
|
267
|
+
"Example: 'afa734e4-3571-11ec-991a-1e0031268139' for YouTube Analytics."
|
|
268
|
+
),
|
|
269
|
+
default=None,
|
|
270
|
+
),
|
|
271
|
+
] = None,
|
|
272
|
+
source_canonical_name: Annotated[
|
|
273
|
+
str | None,
|
|
274
|
+
Field(
|
|
275
|
+
description=(
|
|
276
|
+
"Canonical source connector name to search for. "
|
|
277
|
+
"Exactly one of this or source_definition_id is required. "
|
|
278
|
+
"Examples: 'source-youtube-analytics', 'YouTube Analytics'."
|
|
279
|
+
),
|
|
280
|
+
default=None,
|
|
281
|
+
),
|
|
282
|
+
] = None,
|
|
283
|
+
organization_id: Annotated[
|
|
284
|
+
str | None,
|
|
285
|
+
Field(
|
|
286
|
+
description=(
|
|
287
|
+
"Optional organization ID (UUID) to filter results. "
|
|
288
|
+
"If provided, only connections in this organization will be returned."
|
|
289
|
+
),
|
|
290
|
+
default=None,
|
|
291
|
+
),
|
|
292
|
+
] = None,
|
|
293
|
+
limit: Annotated[
|
|
294
|
+
int,
|
|
295
|
+
Field(description="Maximum number of results (default: 1000)", default=1000),
|
|
296
|
+
] = 1000,
|
|
297
|
+
) -> list[dict[str, Any]]:
|
|
298
|
+
"""Search for all connections using a specific source connector type.
|
|
299
|
+
|
|
300
|
+
This tool queries the Airbyte Cloud Prod DB Replica directly for fast results.
|
|
301
|
+
It finds all connections where the source connector matches the specified type,
|
|
302
|
+
regardless of how the source is named by users.
|
|
303
|
+
|
|
304
|
+
Optionally filter by organization_id to limit results to a specific organization.
|
|
305
|
+
|
|
306
|
+
Returns a list of connection dicts with workspace context and clickable Cloud UI URLs.
|
|
307
|
+
Each dict contains: connection_id, connection_name, connection_url, source_id,
|
|
308
|
+
source_name, source_definition_id, workspace_id, workspace_name, organization_id,
|
|
309
|
+
dataplane_group_id, dataplane_name.
|
|
310
|
+
"""
|
|
311
|
+
# Validate that exactly one of the two parameters is provided
|
|
312
|
+
if (source_definition_id is None) == (source_canonical_name is None):
|
|
313
|
+
raise PyAirbyteInputError(
|
|
314
|
+
message=(
|
|
315
|
+
"Exactly one of source_definition_id or source_canonical_name "
|
|
316
|
+
"must be provided, but not both."
|
|
317
|
+
),
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
# Resolve canonical name to definition ID if needed
|
|
321
|
+
resolved_definition_id: str
|
|
322
|
+
if source_canonical_name:
|
|
323
|
+
resolved_definition_id = _resolve_canonical_name_to_definition_id(
|
|
324
|
+
canonical_name=source_canonical_name,
|
|
325
|
+
)
|
|
326
|
+
else:
|
|
327
|
+
resolved_definition_id = source_definition_id # type: ignore[assignment]
|
|
328
|
+
|
|
329
|
+
# Query the database and transform rows to include connection URLs
|
|
330
|
+
return [
|
|
331
|
+
{
|
|
332
|
+
"organization_id": str(row.get("organization_id", "")),
|
|
333
|
+
"workspace_id": str(row["workspace_id"]),
|
|
334
|
+
"workspace_name": row.get("workspace_name", ""),
|
|
335
|
+
"connection_id": str(row["connection_id"]),
|
|
336
|
+
"connection_name": row.get("connection_name", ""),
|
|
337
|
+
"connection_url": (
|
|
338
|
+
f"{CLOUD_UI_BASE_URL}/workspaces/{row['workspace_id']}"
|
|
339
|
+
f"/connections/{row['connection_id']}/status"
|
|
340
|
+
),
|
|
341
|
+
"source_id": str(row["source_id"]),
|
|
342
|
+
"source_name": row.get("source_name", ""),
|
|
343
|
+
"source_definition_id": str(row["source_definition_id"]),
|
|
344
|
+
"dataplane_group_id": str(row.get("dataplane_group_id", "")),
|
|
345
|
+
"dataplane_name": row.get("dataplane_name", ""),
|
|
346
|
+
}
|
|
347
|
+
for row in query_connections_by_connector(
|
|
348
|
+
connector_definition_id=resolved_definition_id,
|
|
349
|
+
organization_id=organization_id,
|
|
350
|
+
limit=limit,
|
|
351
|
+
)
|
|
352
|
+
]
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def register_prod_db_query_tools(app: FastMCP) -> None:
|
|
356
|
+
"""Register prod DB query tools with the FastMCP app."""
|
|
357
|
+
register_mcp_tools(app, ToolDomain.CLOUD_ADMIN)
|
airbyte_ops_mcp/mcp/server.py
CHANGED
|
@@ -19,6 +19,7 @@ from airbyte_ops_mcp.mcp.github import register_github_tools
|
|
|
19
19
|
from airbyte_ops_mcp.mcp.github_repo_ops import register_github_repo_ops_tools
|
|
20
20
|
from airbyte_ops_mcp.mcp.live_tests import register_live_tests_tools
|
|
21
21
|
from airbyte_ops_mcp.mcp.prerelease import register_prerelease_tools
|
|
22
|
+
from airbyte_ops_mcp.mcp.prod_db_queries import register_prod_db_query_tools
|
|
22
23
|
from airbyte_ops_mcp.mcp.prompts import register_prompts
|
|
23
24
|
from airbyte_ops_mcp.mcp.server_info import register_server_info_resources
|
|
24
25
|
|
|
@@ -47,6 +48,7 @@ def register_server_assets(app: FastMCP) -> None:
|
|
|
47
48
|
register_github_tools(app)
|
|
48
49
|
register_prerelease_tools(app)
|
|
49
50
|
register_cloud_connector_version_tools(app)
|
|
51
|
+
register_prod_db_query_tools(app)
|
|
50
52
|
register_prompts(app)
|
|
51
53
|
register_live_tests_tools(app)
|
|
52
54
|
|
|
@@ -19,7 +19,7 @@ from airbyte_ops_mcp.mcp._mcp_utils import (
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
@lru_cache(maxsize=1)
|
|
22
|
-
def _get_version_info() -> dict[str, str | None]:
|
|
22
|
+
def _get_version_info() -> dict[str, str | list[str] | None]:
|
|
23
23
|
"""Get version information for the MCP server.
|
|
24
24
|
|
|
25
25
|
Returns:
|
|
@@ -66,7 +66,7 @@ def _get_version_info() -> dict[str, str | None]:
|
|
|
66
66
|
mime_type="application/json",
|
|
67
67
|
domain=ToolDomain.SERVER_INFO,
|
|
68
68
|
)
|
|
69
|
-
def mcp_server_info() -> dict[str, str | None]:
|
|
69
|
+
def mcp_server_info() -> dict[str, str | list[str] | None]:
|
|
70
70
|
"""Resource that returns information for the MCP server.
|
|
71
71
|
|
|
72
72
|
This includes package version, release history, help URLs, as well as other information.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
|
|
2
|
+
"""Prod DB Access module for querying Airbyte Cloud Prod DB Replica.
|
|
3
|
+
|
|
4
|
+
This module provides:
|
|
5
|
+
- sql.py: SQL query templates and schema documentation
|
|
6
|
+
- db_engine.py: Database connection and engine management
|
|
7
|
+
- queries.py: Query execution functions
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from airbyte_ops_mcp.prod_db_access.db_engine import get_pool
|
|
11
|
+
from airbyte_ops_mcp.prod_db_access.sql import (
|
|
12
|
+
SELECT_ACTORS_PINNED_TO_VERSION,
|
|
13
|
+
SELECT_CONNECTIONS_BY_CONNECTOR,
|
|
14
|
+
SELECT_CONNECTOR_VERSIONS,
|
|
15
|
+
SELECT_DATAPLANES_LIST,
|
|
16
|
+
SELECT_NEW_CONNECTOR_RELEASES,
|
|
17
|
+
SELECT_ORG_WORKSPACES,
|
|
18
|
+
SELECT_SUCCESSFUL_SYNCS_FOR_VERSION,
|
|
19
|
+
SELECT_SYNC_RESULTS_FOR_VERSION,
|
|
20
|
+
SELECT_WORKSPACE_INFO,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"SELECT_ACTORS_PINNED_TO_VERSION",
|
|
25
|
+
"SELECT_CONNECTIONS_BY_CONNECTOR",
|
|
26
|
+
"SELECT_CONNECTOR_VERSIONS",
|
|
27
|
+
"SELECT_DATAPLANES_LIST",
|
|
28
|
+
"SELECT_NEW_CONNECTOR_RELEASES",
|
|
29
|
+
"SELECT_ORG_WORKSPACES",
|
|
30
|
+
"SELECT_SUCCESSFUL_SYNCS_FOR_VERSION",
|
|
31
|
+
"SELECT_SYNC_RESULTS_FOR_VERSION",
|
|
32
|
+
"SELECT_WORKSPACE_INFO",
|
|
33
|
+
"get_pool",
|
|
34
|
+
]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
|
|
2
|
+
"""Database engine and connection management for Airbyte Cloud Prod DB Replica.
|
|
3
|
+
|
|
4
|
+
This module provides connection pooling and engine management for querying
|
|
5
|
+
the Airbyte Cloud production database replica.
|
|
6
|
+
|
|
7
|
+
For SQL query templates and schema documentation, see sql.py.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import traceback
|
|
15
|
+
from typing import Any, Callable
|
|
16
|
+
|
|
17
|
+
import sqlalchemy
|
|
18
|
+
from google.cloud import secretmanager
|
|
19
|
+
from google.cloud.sql.connector import Connector
|
|
20
|
+
from google.cloud.sql.connector.enums import IPTypes
|
|
21
|
+
|
|
22
|
+
# Secret ID for database connection details
|
|
23
|
+
CONNECTION_RETRIEVER_PG_CONNECTION_DETAILS_SECRET_ID = (
|
|
24
|
+
"projects/587336813068/secrets/CONNECTION_RETRIEVER_PG_CONNECTION_DETAILS"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
PG_DRIVER = "pg8000"
|
|
28
|
+
|
|
29
|
+
# Lazy-initialized to avoid import-time GCP auth
|
|
30
|
+
_connector: Connector | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _get_connector() -> Connector:
|
|
34
|
+
"""Get the Cloud SQL connector, initializing lazily on first use."""
|
|
35
|
+
global _connector
|
|
36
|
+
if _connector is None:
|
|
37
|
+
_connector = Connector()
|
|
38
|
+
return _connector
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _get_secret_value(
|
|
42
|
+
gsm_client: secretmanager.SecretManagerServiceClient, secret_id: str
|
|
43
|
+
) -> str:
|
|
44
|
+
"""Get the value of the enabled version of a secret.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
gsm_client: GCP Secret Manager client
|
|
48
|
+
secret_id: The id of the secret
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
The value of the enabled version of the secret
|
|
52
|
+
"""
|
|
53
|
+
response = gsm_client.list_secret_versions(
|
|
54
|
+
request={"parent": secret_id, "filter": "state:ENABLED"}
|
|
55
|
+
)
|
|
56
|
+
if len(response.versions) == 0:
|
|
57
|
+
raise ValueError(f"No enabled version of secret {secret_id} found")
|
|
58
|
+
enabled_version = response.versions[0]
|
|
59
|
+
response = gsm_client.access_secret_version(name=enabled_version.name)
|
|
60
|
+
return response.payload.data.decode("UTF-8")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_database_creator(pg_connection_details: dict) -> Callable:
|
|
64
|
+
"""Create a database connection creator function."""
|
|
65
|
+
|
|
66
|
+
def creator() -> Any:
|
|
67
|
+
return _get_connector().connect(
|
|
68
|
+
pg_connection_details["database_address"],
|
|
69
|
+
PG_DRIVER,
|
|
70
|
+
user=pg_connection_details["pg_user"],
|
|
71
|
+
password=pg_connection_details["pg_password"],
|
|
72
|
+
db=pg_connection_details["database_name"],
|
|
73
|
+
ip_type=IPTypes.PRIVATE,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return creator
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_pool(
|
|
80
|
+
gsm_client: secretmanager.SecretManagerServiceClient,
|
|
81
|
+
) -> sqlalchemy.Engine:
|
|
82
|
+
"""Get a SQLAlchemy connection pool for the Airbyte Cloud database.
|
|
83
|
+
|
|
84
|
+
This function supports two connection modes:
|
|
85
|
+
1. Direct connection via Cloud SQL Python Connector (default, requires VPC access)
|
|
86
|
+
2. Connection via Cloud SQL Auth Proxy (when CI or USE_CLOUD_SQL_PROXY env var is set)
|
|
87
|
+
|
|
88
|
+
For proxy mode, start the proxy with:
|
|
89
|
+
cloud-sql-proxy prod-ab-cloud-proj:us-west3:prod-pgsql-replica --port=<port>
|
|
90
|
+
|
|
91
|
+
Environment variables:
|
|
92
|
+
CI: If set, uses proxy connection mode
|
|
93
|
+
USE_CLOUD_SQL_PROXY: If set, uses proxy connection mode
|
|
94
|
+
DB_PORT: Port for proxy connection (default: 5432)
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
gsm_client: GCP Secret Manager client for retrieving credentials
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
SQLAlchemy Engine connected to the Prod DB Replica
|
|
101
|
+
"""
|
|
102
|
+
pg_connection_details = json.loads(
|
|
103
|
+
_get_secret_value(
|
|
104
|
+
gsm_client, CONNECTION_RETRIEVER_PG_CONNECTION_DETAILS_SECRET_ID
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if os.getenv("CI") or os.getenv("USE_CLOUD_SQL_PROXY"):
|
|
109
|
+
# Connect via Cloud SQL Auth Proxy, running on localhost
|
|
110
|
+
# Port can be configured via DB_PORT env var (default: 5432)
|
|
111
|
+
host = "127.0.0.1"
|
|
112
|
+
port = os.getenv("DB_PORT", "5432")
|
|
113
|
+
try:
|
|
114
|
+
return sqlalchemy.create_engine(
|
|
115
|
+
f"postgresql+{PG_DRIVER}://{pg_connection_details['pg_user']}:{pg_connection_details['pg_password']}@{host}:{port}/{pg_connection_details['database_name']}",
|
|
116
|
+
)
|
|
117
|
+
except Exception as e:
|
|
118
|
+
raise AssertionError(
|
|
119
|
+
f"sqlalchemy.create_engine exception; could not connect to the proxy at {host}:{port}. "
|
|
120
|
+
f"Error: {traceback.format_exception(e)}"
|
|
121
|
+
) from e
|
|
122
|
+
|
|
123
|
+
# Default: Connect via Cloud SQL Python Connector (requires VPC access)
|
|
124
|
+
return sqlalchemy.create_engine(
|
|
125
|
+
f"postgresql+{PG_DRIVER}://",
|
|
126
|
+
creator=get_database_creator(pg_connection_details),
|
|
127
|
+
)
|
|
File without changes
|