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.
Files changed (41) hide show
  1. {airbyte_internal_ops-0.2.0.dist-info → airbyte_internal_ops-0.2.2.dist-info}/METADATA +19 -3
  2. {airbyte_internal_ops-0.2.0.dist-info → airbyte_internal_ops-0.2.2.dist-info}/RECORD +41 -41
  3. airbyte_ops_mcp/__init__.py +2 -2
  4. airbyte_ops_mcp/cli/cloud.py +207 -306
  5. airbyte_ops_mcp/cloud_admin/api_client.py +51 -26
  6. airbyte_ops_mcp/cloud_admin/connection_config.py +2 -2
  7. airbyte_ops_mcp/constants.py +61 -1
  8. airbyte_ops_mcp/github_actions.py +69 -1
  9. airbyte_ops_mcp/mcp/_http_headers.py +56 -0
  10. airbyte_ops_mcp/mcp/_mcp_utils.py +2 -2
  11. airbyte_ops_mcp/mcp/cloud_connector_versions.py +57 -43
  12. airbyte_ops_mcp/mcp/github.py +34 -1
  13. airbyte_ops_mcp/mcp/prerelease.py +3 -3
  14. airbyte_ops_mcp/mcp/prod_db_queries.py +293 -50
  15. airbyte_ops_mcp/mcp/{live_tests.py → regression_tests.py} +158 -176
  16. airbyte_ops_mcp/mcp/server.py +3 -3
  17. airbyte_ops_mcp/prod_db_access/db_engine.py +7 -11
  18. airbyte_ops_mcp/prod_db_access/queries.py +79 -0
  19. airbyte_ops_mcp/prod_db_access/sql.py +86 -0
  20. airbyte_ops_mcp/{live_tests → regression_tests}/__init__.py +3 -3
  21. airbyte_ops_mcp/{live_tests → regression_tests}/cdk_secrets.py +1 -1
  22. airbyte_ops_mcp/{live_tests → regression_tests}/connection_secret_retriever.py +3 -3
  23. airbyte_ops_mcp/{live_tests → regression_tests}/connector_runner.py +1 -1
  24. airbyte_ops_mcp/{live_tests → regression_tests}/message_cache/__init__.py +3 -1
  25. airbyte_ops_mcp/{live_tests → regression_tests}/regression/__init__.py +1 -1
  26. airbyte_ops_mcp/{live_tests → regression_tests}/schema_generation.py +3 -1
  27. airbyte_ops_mcp/{live_tests → regression_tests}/validation/__init__.py +2 -2
  28. airbyte_ops_mcp/{live_tests → regression_tests}/validation/record_validators.py +4 -2
  29. {airbyte_internal_ops-0.2.0.dist-info → airbyte_internal_ops-0.2.2.dist-info}/WHEEL +0 -0
  30. {airbyte_internal_ops-0.2.0.dist-info → airbyte_internal_ops-0.2.2.dist-info}/entry_points.txt +0 -0
  31. /airbyte_ops_mcp/{live_tests → regression_tests}/ci_output.py +0 -0
  32. /airbyte_ops_mcp/{live_tests → regression_tests}/commons/__init__.py +0 -0
  33. /airbyte_ops_mcp/{live_tests → regression_tests}/config.py +0 -0
  34. /airbyte_ops_mcp/{live_tests → regression_tests}/connection_fetcher.py +0 -0
  35. /airbyte_ops_mcp/{live_tests → regression_tests}/evaluation_modes.py +0 -0
  36. /airbyte_ops_mcp/{live_tests → regression_tests}/http_metrics.py +0 -0
  37. /airbyte_ops_mcp/{live_tests → regression_tests}/message_cache/duckdb_cache.py +0 -0
  38. /airbyte_ops_mcp/{live_tests → regression_tests}/models.py +0 -0
  39. /airbyte_ops_mcp/{live_tests → regression_tests}/obfuscation.py +0 -0
  40. /airbyte_ops_mcp/{live_tests → regression_tests}/regression/comparators.py +0 -0
  41. /airbyte_ops_mcp/{live_tests → regression_tests}/validation/catalog_validators.py +0 -0
@@ -1,9 +1,16 @@
1
1
  # Copyright (c) 2025 Airbyte, Inc., all rights reserved.
2
- """MCP tools for live connection tests.
2
+ """MCP tools for connector regression tests.
3
3
 
4
- This module provides MCP tools for triggering live validation and regression tests
5
- on Airbyte Cloud connections via GitHub Actions workflows. Tests run asynchronously
6
- in GitHub Actions and results can be polled via workflow status.
4
+ This module provides MCP tools for triggering regression tests on Airbyte Cloud
5
+ connections via GitHub Actions workflows. Regression tests can run in two modes:
6
+ - Single version mode: Tests a connector version against a connection config
7
+ - Comparison mode: Compares a target version against a control (baseline) version
8
+
9
+ Tests run asynchronously in GitHub Actions and results can be polled via workflow status.
10
+
11
+ Note: The term "regression tests" encompasses all connector validation testing.
12
+ The term "live tests" is reserved for scenarios where actual Cloud connections
13
+ are pinned to pre-release versions for real-world validation.
7
14
  """
8
15
 
9
16
  from __future__ import annotations
@@ -17,6 +24,10 @@ from typing import Annotated, Any
17
24
  import requests
18
25
  from airbyte.cloud import CloudWorkspace
19
26
  from airbyte.cloud.auth import resolve_cloud_client_id, resolve_cloud_client_secret
27
+ from airbyte.exceptions import (
28
+ AirbyteMissingResourceError,
29
+ AirbyteWorkspaceMismatchError,
30
+ )
20
31
  from fastmcp import FastMCP
21
32
  from pydantic import BaseModel, Field
22
33
 
@@ -33,13 +44,49 @@ logger = logging.getLogger(__name__)
33
44
  # GitHub Workflow Configuration
34
45
  # =============================================================================
35
46
 
36
- LIVE_TEST_REPO_OWNER = "airbytehq"
37
- LIVE_TEST_REPO_NAME = "airbyte-ops-mcp"
38
- LIVE_TEST_DEFAULT_BRANCH = "main"
39
- LIVE_TEST_WORKFLOW_FILE = "connector-live-test.yml"
47
+ REGRESSION_TEST_REPO_OWNER = "airbytehq"
48
+ REGRESSION_TEST_REPO_NAME = "airbyte-ops-mcp"
49
+ REGRESSION_TEST_DEFAULT_BRANCH = "main"
50
+ # Unified regression test workflow (handles both single-version and comparison modes)
40
51
  REGRESSION_TEST_WORKFLOW_FILE = "connector-regression-test.yml"
41
52
 
42
53
 
54
+ # =============================================================================
55
+ # Workspace Validation Helpers
56
+ # =============================================================================
57
+
58
+
59
+ def validate_connection_workspace(
60
+ connection_id: str,
61
+ workspace_id: str,
62
+ ) -> None:
63
+ """Validate that a connection belongs to the expected workspace.
64
+
65
+ Uses PyAirbyte's CloudConnection.check_is_valid() method to verify that
66
+ the connection exists and belongs to the specified workspace.
67
+
68
+ Raises:
69
+ ValueError: If Airbyte Cloud credentials are missing.
70
+ AirbyteWorkspaceMismatchError: If connection belongs to a different workspace.
71
+ AirbyteMissingResourceError: If connection is not found.
72
+ """
73
+ client_id = resolve_cloud_client_id()
74
+ client_secret = resolve_cloud_client_secret()
75
+ if not client_id or not client_secret:
76
+ raise ValueError(
77
+ "Missing Airbyte Cloud credentials. "
78
+ "Set AIRBYTE_CLOUD_CLIENT_ID and AIRBYTE_CLOUD_CLIENT_SECRET env vars."
79
+ )
80
+
81
+ workspace = CloudWorkspace(
82
+ workspace_id=workspace_id,
83
+ client_id=client_id,
84
+ client_secret=client_secret,
85
+ )
86
+ connection = workspace.get_connection(connection_id)
87
+ connection.check_is_valid()
88
+
89
+
43
90
  def _get_workflow_run_status(
44
91
  owner: str,
45
92
  repo: str,
@@ -90,8 +137,8 @@ class TestRunStatus(str, Enum):
90
137
  FAILED = "failed"
91
138
 
92
139
 
93
- class TestPhaseStatus(str, Enum):
94
- """Status of a test phase (live or regression)."""
140
+ class TestOutcome(str, Enum):
141
+ """Outcome of a test (execution or comparison)."""
95
142
 
96
143
  PENDING = "pending"
97
144
  RUNNING = "running"
@@ -130,10 +177,10 @@ class StreamComparisonResultModel(BaseModel):
130
177
  message: str = Field(description="Human-readable comparison summary")
131
178
 
132
179
 
133
- class LivePhaseResult(BaseModel):
134
- """Results from the live test phase."""
180
+ class RegressionTestExecutionResult(BaseModel):
181
+ """Results from executing the connector (validations and record counts)."""
135
182
 
136
- status: TestPhaseStatus = Field(description="Status of the live phase")
183
+ outcome: TestOutcome = Field(description="Outcome of the execution")
137
184
  catalog_validations: list[ValidationResultModel] = Field(
138
185
  default_factory=list,
139
186
  description="Results of catalog validation checks",
@@ -148,14 +195,14 @@ class LivePhaseResult(BaseModel):
148
195
  )
149
196
  error_message: str | None = Field(
150
197
  default=None,
151
- description="Error message if the phase failed",
198
+ description="Error message if the execution failed",
152
199
  )
153
200
 
154
201
 
155
- class RegressionPhaseResult(BaseModel):
156
- """Results from the regression test phase."""
202
+ class RegressionTestComparisonResult(BaseModel):
203
+ """Results from comparing target vs control connector versions."""
157
204
 
158
- status: TestPhaseStatus = Field(description="Status of the regression phase")
205
+ outcome: TestOutcome = Field(description="Outcome of the comparison")
159
206
  baseline_version: str | None = Field(
160
207
  default=None,
161
208
  description="Version of the baseline (control) connector",
@@ -166,12 +213,12 @@ class RegressionPhaseResult(BaseModel):
166
213
  )
167
214
  error_message: str | None = Field(
168
215
  default=None,
169
- description="Error message if the phase failed",
216
+ description="Error message if the comparison failed",
170
217
  )
171
218
 
172
219
 
173
- class LiveConnectionTestResult(BaseModel):
174
- """Complete result of a live connection test run."""
220
+ class RegressionTestResult(BaseModel):
221
+ """Complete result of a regression test run."""
175
222
 
176
223
  run_id: str = Field(description="Unique identifier for this test run")
177
224
  connection_id: str = Field(description="The connection being tested")
@@ -183,23 +230,23 @@ class LiveConnectionTestResult(BaseModel):
183
230
  )
184
231
  baseline_version: str | None = Field(
185
232
  default=None,
186
- description="Version of the baseline connector (if regression ran)",
233
+ description="Version of the baseline connector (if comparison mode)",
187
234
  )
188
235
  evaluation_mode: str = Field(
189
236
  default="diagnostic",
190
237
  description="Evaluation mode used (diagnostic or strict)",
191
238
  )
192
- skip_regression_tests: bool = Field(
239
+ compare_versions: bool = Field(
193
240
  default=False,
194
- description="Whether regression tests were skipped by request",
241
+ description="Whether comparison mode was used (target vs control)",
195
242
  )
196
- live_phase: LivePhaseResult | None = Field(
243
+ execution_result: RegressionTestExecutionResult | None = Field(
197
244
  default=None,
198
- description="Results from the live test phase",
245
+ description="Results from executing the connector (validations and record counts)",
199
246
  )
200
- regression_phase: RegressionPhaseResult | None = Field(
247
+ comparison_result: RegressionTestComparisonResult | None = Field(
201
248
  default=None,
202
- description="Results from the regression test phase",
249
+ description="Results from comparing target vs control connector versions",
203
250
  )
204
251
  artifacts: dict[str, str] = Field(
205
252
  default_factory=dict,
@@ -223,8 +270,8 @@ class LiveConnectionTestResult(BaseModel):
223
270
  )
224
271
 
225
272
 
226
- class RunLiveConnectionTestsResponse(BaseModel):
227
- """Response from starting a live connection test via GitHub Actions workflow."""
273
+ class RunRegressionTestsResponse(BaseModel):
274
+ """Response from starting a regression test via GitHub Actions workflow."""
228
275
 
229
276
  run_id: str = Field(
230
277
  description="Unique identifier for the test run (internal tracking ID)"
@@ -255,66 +302,57 @@ class RunLiveConnectionTestsResponse(BaseModel):
255
302
  idempotent=False,
256
303
  open_world=True,
257
304
  )
258
- def run_live_connection_tests(
259
- connection_id: Annotated[str, "The Airbyte Cloud connection ID to test"],
260
- command: Annotated[
305
+ def run_regression_tests(
306
+ connector_name: Annotated[
261
307
  str,
262
- "Airbyte command to run: 'spec', 'check', 'discover', or 'read'",
263
- ] = "check",
264
- workspace_id: Annotated[
308
+ "Connector name to build from source (e.g., 'source-pokeapi'). Required.",
309
+ ],
310
+ pr: Annotated[
311
+ int,
312
+ "PR number from the airbyte monorepo to checkout and build from (e.g., 70847). Required.",
313
+ ],
314
+ connection_id: Annotated[
265
315
  str | None,
266
- "Optional Airbyte Cloud workspace ID. If provided, validates that the connection "
267
- "belongs to this workspace before triggering tests. If omitted, no validation is done.",
316
+ "Airbyte Cloud connection ID to fetch config/catalog from. "
317
+ "If not provided, uses GSM integration test secrets.",
268
318
  ] = None,
269
- skip_regression_tests: Annotated[
319
+ skip_compare: Annotated[
270
320
  bool,
271
- "If True, run only live tests (connector-live-test workflow). "
272
- "If False, run regression tests comparing target vs control versions "
273
- "(connector-regression-test workflow).",
274
- ] = True,
275
- connector_image: Annotated[
276
- str | None,
277
- "Optional connector image with tag for live tests (e.g., 'airbyte/source-github:1.0.0'). "
278
- "If not provided, auto-detected from connection. Only used when skip_regression_tests=True.",
279
- ] = None,
280
- target_image: Annotated[
321
+ "If True, skip comparison and run single-version tests only. "
322
+ "If False (default), run comparison tests (target vs control versions).",
323
+ ] = False,
324
+ skip_read_action: Annotated[
325
+ bool,
326
+ "If True, skip the read action (run only spec, check, discover). "
327
+ "If False (default), run all verbs including read.",
328
+ ] = False,
329
+ override_test_image: Annotated[
281
330
  str | None,
282
- "Target connector image (new version) with tag for regression tests "
283
- "(e.g., 'airbyte/source-github:2.0.0'). Optional if connector_name is provided. "
284
- "Only used when skip_regression_tests=False.",
331
+ "Override test connector image with tag (e.g., 'airbyte/source-github:1.0.0'). "
332
+ "Ignored if skip_compare=False.",
285
333
  ] = None,
286
- control_image: Annotated[
334
+ override_control_image: Annotated[
287
335
  str | None,
288
- "Control connector image (baseline version) with tag for regression tests "
289
- "(e.g., 'airbyte/source-github:1.0.0'). Optional if connection_id is provided "
290
- "(auto-detected from connection). Only used when skip_regression_tests=False.",
336
+ "Override control connector image (baseline version) with tag. "
337
+ "Ignored if skip_compare=True.",
291
338
  ] = None,
292
- connector_name: Annotated[
339
+ workspace_id: Annotated[
293
340
  str | None,
294
- "Connector name to build the connector image from source "
295
- "(e.g., 'source-pokeapi'). If provided, builds the image locally with tag 'dev'. "
296
- "For live tests, this builds the test image. For regression tests, this builds "
297
- "the target image while control is auto-detected from the connection.",
341
+ "Optional Airbyte Cloud workspace ID. If provided with connection_id, validates "
342
+ "that the connection belongs to this workspace before triggering tests.",
298
343
  ] = None,
299
- pr: Annotated[
300
- int | None,
301
- "PR number from the airbyte monorepo to checkout and build from "
302
- "(e.g., 70847). Only used when connector_name is provided. "
303
- "If not specified, builds from the default branch (master).",
304
- ] = None,
305
- ) -> RunLiveConnectionTestsResponse:
306
- """Start a live connection test run via GitHub Actions workflow.
344
+ ) -> RunRegressionTestsResponse:
345
+ """Start a regression test run via GitHub Actions workflow.
307
346
 
308
- This tool triggers either the live-test or regression-test workflow depending
309
- on the skip_regression_tests parameter:
347
+ This tool triggers the regression test workflow which builds the connector
348
+ from the specified PR and runs tests against it.
310
349
 
311
- - skip_regression_tests=True (default): Triggers connector-live-test workflow.
312
- Runs the specified command against the connection and validates the output.
350
+ - skip_compare=False (default): Comparison mode - compares the PR version
351
+ against the baseline (control) version.
352
+ - skip_compare=True: Single-version mode - runs tests without comparison.
313
353
 
314
- - skip_regression_tests=False: Triggers connector-regression-test workflow.
315
- Compares the target connector version against a control (baseline) version.
316
- For regression tests, provide either target_image or connector_name to specify
317
- the target version.
354
+ If connection_id is provided, config/catalog are fetched from Airbyte Cloud.
355
+ Otherwise, GSM integration test secrets are used.
318
356
 
319
357
  Returns immediately with a run_id and workflow URL. Check the workflow URL
320
358
  to monitor progress and view results.
@@ -329,133 +367,77 @@ def run_live_connection_tests(
329
367
  try:
330
368
  token = resolve_github_token()
331
369
  except ValueError as e:
332
- return RunLiveConnectionTestsResponse(
370
+ return RunRegressionTestsResponse(
333
371
  run_id=run_id,
334
372
  status=TestRunStatus.FAILED,
335
373
  message=str(e),
336
374
  workflow_url=None,
337
375
  )
338
376
 
339
- # Validate workspace membership if workspace_id is provided
340
- if workspace_id:
341
- client_id = resolve_cloud_client_id()
342
- client_secret = resolve_cloud_client_secret()
343
- if not client_id or not client_secret:
344
- return RunLiveConnectionTestsResponse(
345
- run_id=run_id,
346
- status=TestRunStatus.FAILED,
347
- message=(
348
- "Missing Airbyte Cloud credentials. "
349
- "Set AIRBYTE_CLOUD_CLIENT_ID and AIRBYTE_CLOUD_CLIENT_SECRET env vars."
350
- ),
351
- workflow_url=None,
352
- )
353
- workspace = CloudWorkspace(
354
- workspace_id=workspace_id,
355
- client_id=client_id,
356
- client_secret=client_secret,
357
- )
358
- connection = workspace.get_connection(connection_id)
359
- if connection is None:
360
- return RunLiveConnectionTestsResponse(
361
- run_id=run_id,
362
- status=TestRunStatus.FAILED,
363
- message=f"Connection {connection_id} not found in workspace {workspace_id}",
364
- workflow_url=None,
365
- )
366
-
367
- if skip_regression_tests:
368
- # Live test workflow
369
- workflow_inputs: dict[str, str] = {
370
- "connection_id": connection_id,
371
- "command": command,
372
- }
373
- if connector_image:
374
- workflow_inputs["connector_image"] = connector_image
375
- if connector_name:
376
- workflow_inputs["connector_name"] = connector_name
377
- if pr:
378
- workflow_inputs["pr"] = str(pr)
379
-
377
+ # Validate workspace membership if workspace_id and connection_id are provided
378
+ if workspace_id and connection_id:
380
379
  try:
381
- dispatch_result = trigger_workflow_dispatch(
382
- owner=LIVE_TEST_REPO_OWNER,
383
- repo=LIVE_TEST_REPO_NAME,
384
- workflow_file=LIVE_TEST_WORKFLOW_FILE,
385
- ref=LIVE_TEST_DEFAULT_BRANCH,
386
- inputs=workflow_inputs,
387
- token=token,
388
- )
389
- except Exception as e:
390
- logger.exception("Failed to trigger live test workflow")
391
- return RunLiveConnectionTestsResponse(
380
+ validate_connection_workspace(connection_id, workspace_id)
381
+ except (
382
+ ValueError,
383
+ AirbyteWorkspaceMismatchError,
384
+ AirbyteMissingResourceError,
385
+ ) as e:
386
+ return RunRegressionTestsResponse(
392
387
  run_id=run_id,
393
388
  status=TestRunStatus.FAILED,
394
- message=f"Failed to trigger live-test workflow: {e}",
389
+ message=str(e),
395
390
  workflow_url=None,
396
391
  )
397
392
 
398
- view_url = dispatch_result.run_url or dispatch_result.workflow_url
399
- return RunLiveConnectionTestsResponse(
400
- run_id=run_id,
401
- status=TestRunStatus.QUEUED,
402
- message=f"Live-test workflow triggered for connection {connection_id}. "
403
- f"View progress at: {view_url}",
404
- workflow_url=dispatch_result.workflow_url,
405
- github_run_id=dispatch_result.run_id,
406
- github_run_url=dispatch_result.run_url,
407
- )
408
-
409
- # Regression test workflow (skip_regression_tests=False)
410
- # Validate that we have enough info to run regression tests
411
- if not target_image and not connector_name:
412
- return RunLiveConnectionTestsResponse(
413
- run_id=run_id,
414
- status=TestRunStatus.FAILED,
415
- message=(
416
- "For regression tests (skip_regression_tests=False), provide either "
417
- "target_image or connector_name so the workflow can determine the target image."
418
- ),
419
- workflow_url=None,
420
- )
421
-
422
- workflow_inputs = {
423
- "connection_id": connection_id,
424
- "command": command,
393
+ # Build workflow inputs - connector_name and pr are required
394
+ workflow_inputs: dict[str, str] = {
395
+ "connector_name": connector_name,
396
+ "pr": str(pr),
425
397
  }
426
- if target_image:
427
- workflow_inputs["target_image"] = target_image
428
- if control_image:
429
- workflow_inputs["control_image"] = control_image
430
- if connector_name:
431
- workflow_inputs["connector_name"] = connector_name
432
- if pr:
433
- workflow_inputs["pr"] = str(pr)
434
398
 
399
+ # Add optional inputs
400
+ if connection_id:
401
+ workflow_inputs["connection_id"] = connection_id
402
+ if skip_compare:
403
+ workflow_inputs["skip_compare"] = "true"
404
+ if skip_read_action:
405
+ workflow_inputs["skip_read_action"] = "true"
406
+ if override_test_image:
407
+ workflow_inputs["override_test_image"] = override_test_image
408
+ if override_control_image:
409
+ workflow_inputs["override_control_image"] = override_control_image
410
+
411
+ mode_description = "single-version" if skip_compare else "comparison"
435
412
  try:
436
413
  dispatch_result = trigger_workflow_dispatch(
437
- owner=LIVE_TEST_REPO_OWNER,
438
- repo=LIVE_TEST_REPO_NAME,
414
+ owner=REGRESSION_TEST_REPO_OWNER,
415
+ repo=REGRESSION_TEST_REPO_NAME,
439
416
  workflow_file=REGRESSION_TEST_WORKFLOW_FILE,
440
- ref=LIVE_TEST_DEFAULT_BRANCH,
417
+ ref=REGRESSION_TEST_DEFAULT_BRANCH,
441
418
  inputs=workflow_inputs,
442
419
  token=token,
443
420
  )
444
421
  except Exception as e:
445
- logger.exception("Failed to trigger regression test workflow")
446
- return RunLiveConnectionTestsResponse(
422
+ logger.exception(
423
+ f"Failed to trigger {mode_description} regression test workflow"
424
+ )
425
+ return RunRegressionTestsResponse(
447
426
  run_id=run_id,
448
427
  status=TestRunStatus.FAILED,
449
- message=f"Failed to trigger regression-test workflow: {e}",
428
+ message=f"Failed to trigger {mode_description} regression test workflow: {e}",
450
429
  workflow_url=None,
451
430
  )
452
431
 
453
432
  view_url = dispatch_result.run_url or dispatch_result.workflow_url
454
- return RunLiveConnectionTestsResponse(
433
+ connection_info = f" for connection {connection_id}" if connection_id else ""
434
+ return RunRegressionTestsResponse(
455
435
  run_id=run_id,
456
436
  status=TestRunStatus.QUEUED,
457
- message=f"Regression-test workflow triggered for connection {connection_id}. "
458
- f"View progress at: {view_url}",
437
+ message=(
438
+ f"{mode_description.capitalize()} regression test workflow triggered "
439
+ f"for {connector_name} (PR #{pr}){connection_info}. View progress at: {view_url}"
440
+ ),
459
441
  workflow_url=dispatch_result.workflow_url,
460
442
  github_run_id=dispatch_result.run_id,
461
443
  github_run_url=dispatch_result.run_url,
@@ -467,8 +449,8 @@ def run_live_connection_tests(
467
449
  # =============================================================================
468
450
 
469
451
 
470
- def register_live_tests_tools(app: FastMCP) -> None:
471
- """Register live tests tools with the FastMCP app.
452
+ def register_regression_tests_tools(app: FastMCP) -> None:
453
+ """Register regression tests tools with the FastMCP app.
472
454
 
473
455
  Args:
474
456
  app: FastMCP application instance
@@ -26,10 +26,10 @@ from airbyte_ops_mcp.mcp.cloud_connector_versions import (
26
26
  )
27
27
  from airbyte_ops_mcp.mcp.github import register_github_tools
28
28
  from airbyte_ops_mcp.mcp.github_repo_ops import register_github_repo_ops_tools
29
- from airbyte_ops_mcp.mcp.live_tests import register_live_tests_tools
30
29
  from airbyte_ops_mcp.mcp.prerelease import register_prerelease_tools
31
30
  from airbyte_ops_mcp.mcp.prod_db_queries import register_prod_db_query_tools
32
31
  from airbyte_ops_mcp.mcp.prompts import register_prompts
32
+ from airbyte_ops_mcp.mcp.regression_tests import register_regression_tests_tools
33
33
  from airbyte_ops_mcp.mcp.server_info import register_server_info_resources
34
34
 
35
35
  # Default HTTP server configuration
@@ -47,7 +47,7 @@ def register_server_assets(app: FastMCP) -> None:
47
47
  - REPO: GitHub repository operations
48
48
  - CLOUD: Cloud connector version management
49
49
  - PROMPTS: Prompt templates for common workflows
50
- - LIVE_TESTS: Live connection validation and regression tests
50
+ - REGRESSION_TESTS: Connector regression tests (single-version and comparison)
51
51
  - REGISTRY: Connector registry operations (future)
52
52
  - METADATA: Connector metadata operations (future)
53
53
  - QA: Connector quality assurance (future)
@@ -63,7 +63,7 @@ def register_server_assets(app: FastMCP) -> None:
63
63
  register_cloud_connector_version_tools(app)
64
64
  register_prod_db_query_tools(app)
65
65
  register_prompts(app)
66
- register_live_tests_tools(app)
66
+ register_regression_tests_tools(app)
67
67
 
68
68
 
69
69
  register_server_assets(app)
@@ -166,24 +166,20 @@ def _get_connector() -> Connector:
166
166
 
167
167
 
168
168
  def _get_secret_value(
169
- gsm_client: secretmanager.SecretManagerServiceClient, secret_id: str
169
+ gsm_client: secretmanager.SecretManagerServiceClient,
170
+ secret_id: str,
170
171
  ) -> str:
171
- """Get the value of the enabled version of a secret.
172
+ """Get the value of the latest version of a secret.
172
173
 
173
174
  Args:
174
175
  gsm_client: GCP Secret Manager client
175
- secret_id: The id of the secret
176
+ secret_id: The full resource ID of the secret
177
+ (e.g., "projects/123/secrets/my-secret")
176
178
 
177
179
  Returns:
178
- The value of the enabled version of the secret
180
+ The value of the latest version of the secret
179
181
  """
180
- response = gsm_client.list_secret_versions(
181
- request={"parent": secret_id, "filter": "state:ENABLED"}
182
- )
183
- if len(response.versions) == 0:
184
- raise ValueError(f"No enabled version of secret {secret_id} found")
185
- enabled_version = response.versions[0]
186
- response = gsm_client.access_secret_version(name=enabled_version.name)
182
+ response = gsm_client.access_secret_version(name=f"{secret_id}/versions/latest")
187
183
  return response.payload.data.decode("UTF-8")
188
184
 
189
185
 
@@ -22,6 +22,8 @@ from airbyte_ops_mcp.prod_db_access.sql import (
22
22
  SELECT_ACTORS_PINNED_TO_VERSION,
23
23
  SELECT_CONNECTIONS_BY_CONNECTOR,
24
24
  SELECT_CONNECTIONS_BY_CONNECTOR_AND_ORG,
25
+ SELECT_CONNECTIONS_BY_DESTINATION_CONNECTOR,
26
+ SELECT_CONNECTIONS_BY_DESTINATION_CONNECTOR_AND_ORG,
25
27
  SELECT_CONNECTOR_VERSIONS,
26
28
  SELECT_DATAPLANES_LIST,
27
29
  SELECT_FAILED_SYNC_ATTEMPTS_FOR_CONNECTOR,
@@ -30,6 +32,7 @@ from airbyte_ops_mcp.prod_db_access.sql import (
30
32
  SELECT_SUCCESSFUL_SYNCS_FOR_VERSION,
31
33
  SELECT_SYNC_RESULTS_FOR_VERSION,
32
34
  SELECT_WORKSPACE_INFO,
35
+ SELECT_WORKSPACES_BY_EMAIL_DOMAIN,
33
36
  )
34
37
 
35
38
  logger = logging.getLogger(__name__)
@@ -113,6 +116,48 @@ def query_connections_by_connector(
113
116
  )
114
117
 
115
118
 
119
+ def query_connections_by_destination_connector(
120
+ connector_definition_id: str,
121
+ organization_id: str | None = None,
122
+ limit: int = 1000,
123
+ *,
124
+ gsm_client: secretmanager.SecretManagerServiceClient | None = None,
125
+ ) -> list[dict[str, Any]]:
126
+ """Query connections by destination connector type, optionally filtered by organization.
127
+
128
+ Args:
129
+ connector_definition_id: Destination connector definition UUID to filter by
130
+ organization_id: Optional organization UUID to search within
131
+ limit: Maximum number of results (default: 1000)
132
+ gsm_client: GCP Secret Manager client. If None, a new client will be instantiated.
133
+
134
+ Returns:
135
+ List of connection records with workspace and dataplane info
136
+ """
137
+ # Use separate queries to avoid pg8000 NULL parameter type issues
138
+ if organization_id is None:
139
+ return _run_sql_query(
140
+ SELECT_CONNECTIONS_BY_DESTINATION_CONNECTOR,
141
+ parameters={
142
+ "connector_definition_id": connector_definition_id,
143
+ "limit": limit,
144
+ },
145
+ query_name="SELECT_CONNECTIONS_BY_DESTINATION_CONNECTOR",
146
+ gsm_client=gsm_client,
147
+ )
148
+
149
+ return _run_sql_query(
150
+ SELECT_CONNECTIONS_BY_DESTINATION_CONNECTOR_AND_ORG,
151
+ parameters={
152
+ "connector_definition_id": connector_definition_id,
153
+ "organization_id": organization_id,
154
+ "limit": limit,
155
+ },
156
+ query_name="SELECT_CONNECTIONS_BY_DESTINATION_CONNECTOR_AND_ORG",
157
+ gsm_client=gsm_client,
158
+ )
159
+
160
+
116
161
  def query_connector_versions(
117
162
  connector_definition_id: str,
118
163
  *,
@@ -337,3 +382,37 @@ def query_org_workspaces(
337
382
  query_name="SELECT_ORG_WORKSPACES",
338
383
  gsm_client=gsm_client,
339
384
  )
385
+
386
+
387
+ def query_workspaces_by_email_domain(
388
+ email_domain: str,
389
+ limit: int = 100,
390
+ *,
391
+ gsm_client: secretmanager.SecretManagerServiceClient | None = None,
392
+ ) -> list[dict[str, Any]]:
393
+ """Query workspaces by email domain.
394
+
395
+ This is useful for identifying workspaces based on user email domains.
396
+ For example, searching for "motherduck.com" will find workspaces where users have
397
+ @motherduck.com email addresses, which may belong to partner accounts.
398
+
399
+ Args:
400
+ email_domain: Email domain to search for (e.g., "motherduck.com", "fivetran.com").
401
+ Do not include the "@" symbol.
402
+ limit: Maximum number of results (default: 100)
403
+ gsm_client: GCP Secret Manager client. If None, a new client will be instantiated.
404
+
405
+ Returns:
406
+ List of workspace records with organization_id, workspace_id, workspace_name,
407
+ slug, email, dataplane_group_id, dataplane_name, and created_at.
408
+ Results are ordered by organization_id and workspace_name.
409
+ """
410
+ # Strip leading @ if provided
411
+ clean_domain = email_domain.lstrip("@")
412
+
413
+ return _run_sql_query(
414
+ SELECT_WORKSPACES_BY_EMAIL_DOMAIN,
415
+ parameters={"email_domain": clean_domain, "limit": limit},
416
+ query_name="SELECT_WORKSPACES_BY_EMAIL_DOMAIN",
417
+ gsm_client=gsm_client,
418
+ )