airbyte-internal-ops 0.1.11__py3-none-any.whl → 0.2.1__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.1.11.dist-info → airbyte_internal_ops-0.2.1.dist-info}/METADATA +2 -2
  2. {airbyte_internal_ops-0.1.11.dist-info → airbyte_internal_ops-0.2.1.dist-info}/RECORD +41 -40
  3. {airbyte_internal_ops-0.1.11.dist-info → airbyte_internal_ops-0.2.1.dist-info}/entry_points.txt +1 -0
  4. airbyte_ops_mcp/__init__.py +2 -2
  5. airbyte_ops_mcp/cli/cloud.py +264 -301
  6. airbyte_ops_mcp/cloud_admin/api_client.py +51 -26
  7. airbyte_ops_mcp/cloud_admin/auth.py +32 -0
  8. airbyte_ops_mcp/cloud_admin/connection_config.py +2 -2
  9. airbyte_ops_mcp/constants.py +18 -0
  10. airbyte_ops_mcp/github_actions.py +94 -5
  11. airbyte_ops_mcp/mcp/_http_headers.py +254 -0
  12. airbyte_ops_mcp/mcp/_mcp_utils.py +2 -2
  13. airbyte_ops_mcp/mcp/cloud_connector_versions.py +162 -52
  14. airbyte_ops_mcp/mcp/github.py +34 -1
  15. airbyte_ops_mcp/mcp/prod_db_queries.py +67 -24
  16. airbyte_ops_mcp/mcp/{live_tests.py → regression_tests.py} +165 -152
  17. airbyte_ops_mcp/mcp/server.py +84 -11
  18. airbyte_ops_mcp/prod_db_access/db_engine.py +15 -11
  19. airbyte_ops_mcp/prod_db_access/queries.py +27 -15
  20. airbyte_ops_mcp/prod_db_access/sql.py +17 -16
  21. airbyte_ops_mcp/{live_tests → regression_tests}/__init__.py +3 -3
  22. airbyte_ops_mcp/{live_tests → regression_tests}/cdk_secrets.py +1 -1
  23. airbyte_ops_mcp/{live_tests → regression_tests}/connection_secret_retriever.py +3 -3
  24. airbyte_ops_mcp/{live_tests → regression_tests}/connector_runner.py +1 -1
  25. airbyte_ops_mcp/{live_tests → regression_tests}/message_cache/__init__.py +3 -1
  26. airbyte_ops_mcp/{live_tests → regression_tests}/regression/__init__.py +1 -1
  27. airbyte_ops_mcp/{live_tests → regression_tests}/schema_generation.py +3 -1
  28. airbyte_ops_mcp/{live_tests → regression_tests}/validation/__init__.py +2 -2
  29. airbyte_ops_mcp/{live_tests → regression_tests}/validation/record_validators.py +4 -2
  30. {airbyte_internal_ops-0.1.11.dist-info → airbyte_internal_ops-0.2.1.dist-info}/WHEEL +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,68 @@ class RunLiveConnectionTestsResponse(BaseModel):
255
302
  idempotent=False,
256
303
  open_world=True,
257
304
  )
258
- def run_live_connection_tests(
305
+ def run_regression_tests(
259
306
  connection_id: Annotated[str, "The Airbyte Cloud connection ID to test"],
260
- command: Annotated[
261
- str,
262
- "Airbyte command to run: 'spec', 'check', 'discover', or 'read'",
263
- ] = "check",
307
+ skip_read_action: Annotated[
308
+ bool,
309
+ "If True, skip the read action (run only spec, check, discover). "
310
+ "If False (default), run all verbs including read.",
311
+ ] = False,
264
312
  workspace_id: Annotated[
265
313
  str | None,
266
314
  "Optional Airbyte Cloud workspace ID. If provided, validates that the connection "
267
315
  "belongs to this workspace before triggering tests. If omitted, no validation is done.",
268
316
  ] = None,
269
- skip_regression_tests: Annotated[
317
+ skip_compare: Annotated[
270
318
  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,
319
+ "If True, skip comparison and run single-version tests only. "
320
+ "If False (default), run comparison tests (target vs control versions).",
321
+ ] = False,
275
322
  connector_image: Annotated[
276
323
  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.",
324
+ "Optional connector image with tag for single-version tests "
325
+ "(e.g., 'airbyte/source-github:1.0.0'). "
326
+ "If not provided, auto-detected from connection. Only used when skip_compare=True.",
279
327
  ] = None,
280
328
  target_image: Annotated[
281
329
  str | None,
282
- "Target connector image (new version) with tag for regression tests "
330
+ "Target connector image (new version) with tag for comparison tests "
283
331
  "(e.g., 'airbyte/source-github:2.0.0'). Optional if connector_name is provided. "
284
- "Only used when skip_regression_tests=False.",
332
+ "Only used when skip_compare=False (default).",
285
333
  ] = None,
286
334
  control_image: Annotated[
287
335
  str | None,
288
- "Control connector image (baseline version) with tag for regression tests "
336
+ "Control connector image (baseline version) with tag for comparison tests "
289
337
  "(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.",
338
+ "(auto-detected from connection). Only used when skip_compare=False (default).",
291
339
  ] = None,
292
340
  connector_name: Annotated[
293
341
  str | None,
294
342
  "Connector name to build the connector image from source "
295
343
  "(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.",
344
+ "For comparison tests (default), this builds the target image while control is "
345
+ "auto-detected from the connection. For single-version tests (skip_compare=True), "
346
+ "this builds the test image.",
298
347
  ] = None,
299
- airbyte_ref: Annotated[
300
- str | None,
301
- "Git ref or PR number to checkout from the airbyte monorepo "
302
- "(e.g., 'master', '70847', 'refs/pull/70847/head'). "
303
- "Only used when connector_name is provided. Defaults to 'master' if not specified.",
348
+ pr: Annotated[
349
+ int | None,
350
+ "PR number from the airbyte monorepo to checkout and build from "
351
+ "(e.g., 70847). Only used when connector_name is provided. "
352
+ "If not specified, builds from the default branch (master).",
304
353
  ] = None,
305
- ) -> RunLiveConnectionTestsResponse:
306
- """Start a live connection test run via GitHub Actions workflow.
307
-
308
- This tool triggers either the live-test or regression-test workflow depending
309
- on the skip_regression_tests parameter:
354
+ ) -> RunRegressionTestsResponse:
355
+ """Start a regression test run via GitHub Actions workflow.
310
356
 
311
- - skip_regression_tests=True (default): Triggers connector-live-test workflow.
312
- Runs the specified command against the connection and validates the output.
357
+ This tool triggers either the comparison or single-version regression test
358
+ workflow depending on the skip_compare parameter:
313
359
 
314
- - skip_regression_tests=False: Triggers connector-regression-test workflow.
360
+ - skip_compare=False (default): Triggers comparison regression test workflow.
315
361
  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.
362
+ Provide either target_image or connector_name to specify the target version.
363
+
364
+ - skip_compare=True: Triggers single-version regression test workflow.
365
+ Runs the specified command against the connection and validates the output.
366
+ No comparison is performed.
318
367
 
319
368
  Returns immediately with a run_id and workflow URL. Check the workflow URL
320
369
  to monitor progress and view results.
@@ -329,7 +378,7 @@ def run_live_connection_tests(
329
378
  try:
330
379
  token = resolve_github_token()
331
380
  except ValueError as e:
332
- return RunLiveConnectionTestsResponse(
381
+ return RunRegressionTestsResponse(
333
382
  run_id=run_id,
334
383
  status=TestRunStatus.FAILED,
335
384
  message=str(e),
@@ -338,122 +387,86 @@ def run_live_connection_tests(
338
387
 
339
388
  # Validate workspace membership if workspace_id is provided
340
389
  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(
390
+ try:
391
+ validate_connection_workspace(connection_id, workspace_id)
392
+ except (
393
+ ValueError,
394
+ AirbyteWorkspaceMismatchError,
395
+ AirbyteMissingResourceError,
396
+ ) as e:
397
+ return RunRegressionTestsResponse(
361
398
  run_id=run_id,
362
399
  status=TestRunStatus.FAILED,
363
- message=f"Connection {connection_id} not found in workspace {workspace_id}",
400
+ message=str(e),
364
401
  workflow_url=None,
365
402
  )
366
403
 
367
- if skip_regression_tests:
368
- # Live test workflow
369
- workflow_inputs: dict[str, str] = {
370
- "connection_id": connection_id,
371
- "command": command,
372
- }
404
+ # Build workflow inputs for the unified regression test workflow
405
+ workflow_inputs: dict[str, str] = {
406
+ "connection_id": connection_id,
407
+ }
408
+
409
+ if skip_compare:
410
+ # Single-version mode
411
+ workflow_inputs["skip_compare"] = "true"
373
412
  if connector_image:
374
413
  workflow_inputs["connector_image"] = connector_image
375
- if connector_name:
376
- workflow_inputs["connector_name"] = connector_name
377
-
378
- try:
379
- dispatch_result = trigger_workflow_dispatch(
380
- owner=LIVE_TEST_REPO_OWNER,
381
- repo=LIVE_TEST_REPO_NAME,
382
- workflow_file=LIVE_TEST_WORKFLOW_FILE,
383
- ref=LIVE_TEST_DEFAULT_BRANCH,
384
- inputs=workflow_inputs,
385
- token=token,
386
- )
387
- except Exception as e:
388
- logger.exception("Failed to trigger live test workflow")
389
- return RunLiveConnectionTestsResponse(
414
+ else:
415
+ # Comparison mode (default): validate that we have enough info
416
+ if not target_image and not connector_name:
417
+ return RunRegressionTestsResponse(
390
418
  run_id=run_id,
391
419
  status=TestRunStatus.FAILED,
392
- message=f"Failed to trigger live-test workflow: {e}",
420
+ message=(
421
+ "For comparison regression tests (skip_compare=False, the default), "
422
+ "provide either target_image or connector_name so the workflow can "
423
+ "determine the target image."
424
+ ),
393
425
  workflow_url=None,
394
426
  )
395
-
396
- view_url = dispatch_result.run_url or dispatch_result.workflow_url
397
- return RunLiveConnectionTestsResponse(
398
- run_id=run_id,
399
- status=TestRunStatus.QUEUED,
400
- message=f"Live-test workflow triggered for connection {connection_id}. "
401
- f"View progress at: {view_url}",
402
- workflow_url=dispatch_result.workflow_url,
403
- github_run_id=dispatch_result.run_id,
404
- github_run_url=dispatch_result.run_url,
405
- )
406
-
407
- # Regression test workflow (skip_regression_tests=False)
408
- # Validate that we have enough info to run regression tests
409
- if not target_image and not connector_name:
410
- return RunLiveConnectionTestsResponse(
411
- run_id=run_id,
412
- status=TestRunStatus.FAILED,
413
- message=(
414
- "For regression tests (skip_regression_tests=False), provide either "
415
- "target_image or connector_name so the workflow can determine the target image."
416
- ),
417
- workflow_url=None,
418
- )
419
-
420
- workflow_inputs = {
421
- "connection_id": connection_id,
422
- "command": command,
423
- }
424
- if target_image:
425
- workflow_inputs["target_image"] = target_image
426
- if control_image:
427
- workflow_inputs["control_image"] = control_image
427
+ workflow_inputs["skip_compare"] = "false"
428
+ if target_image:
429
+ workflow_inputs["target_image"] = target_image
430
+ if control_image:
431
+ workflow_inputs["control_image"] = control_image
432
+
433
+ # Common inputs for both modes
434
+ if skip_read_action:
435
+ workflow_inputs["skip_read_action"] = "true"
428
436
  if connector_name:
429
437
  workflow_inputs["connector_name"] = connector_name
430
- if airbyte_ref:
431
- workflow_inputs["airbyte_ref"] = airbyte_ref
438
+ if pr:
439
+ workflow_inputs["pr"] = str(pr)
432
440
 
441
+ mode_description = "single-version" if skip_compare else "comparison"
433
442
  try:
434
443
  dispatch_result = trigger_workflow_dispatch(
435
- owner=LIVE_TEST_REPO_OWNER,
436
- repo=LIVE_TEST_REPO_NAME,
444
+ owner=REGRESSION_TEST_REPO_OWNER,
445
+ repo=REGRESSION_TEST_REPO_NAME,
437
446
  workflow_file=REGRESSION_TEST_WORKFLOW_FILE,
438
- ref=LIVE_TEST_DEFAULT_BRANCH,
447
+ ref=REGRESSION_TEST_DEFAULT_BRANCH,
439
448
  inputs=workflow_inputs,
440
449
  token=token,
441
450
  )
442
451
  except Exception as e:
443
- logger.exception("Failed to trigger regression test workflow")
444
- return RunLiveConnectionTestsResponse(
452
+ logger.exception(
453
+ f"Failed to trigger {mode_description} regression test workflow"
454
+ )
455
+ return RunRegressionTestsResponse(
445
456
  run_id=run_id,
446
457
  status=TestRunStatus.FAILED,
447
- message=f"Failed to trigger regression-test workflow: {e}",
458
+ message=f"Failed to trigger {mode_description} regression test workflow: {e}",
448
459
  workflow_url=None,
449
460
  )
450
461
 
451
462
  view_url = dispatch_result.run_url or dispatch_result.workflow_url
452
- return RunLiveConnectionTestsResponse(
463
+ return RunRegressionTestsResponse(
453
464
  run_id=run_id,
454
465
  status=TestRunStatus.QUEUED,
455
- message=f"Regression-test workflow triggered for connection {connection_id}. "
456
- f"View progress at: {view_url}",
466
+ message=(
467
+ f"{mode_description.capitalize()} regression test workflow triggered for "
468
+ f"connection {connection_id}. View progress at: {view_url}"
469
+ ),
457
470
  workflow_url=dispatch_result.workflow_url,
458
471
  github_run_id=dispatch_result.run_id,
459
472
  github_run_url=dispatch_result.run_url,
@@ -465,8 +478,8 @@ def run_live_connection_tests(
465
478
  # =============================================================================
466
479
 
467
480
 
468
- def register_live_tests_tools(app: FastMCP) -> None:
469
- """Register live tests tools with the FastMCP app.
481
+ def register_regression_tests_tools(app: FastMCP) -> None:
482
+ """Register regression tests tools with the FastMCP app.
470
483
 
471
484
  Args:
472
485
  app: FastMCP application instance
@@ -2,9 +2,18 @@
2
2
  """Airbyte Admin MCP server implementation.
3
3
 
4
4
  This module provides the main MCP server for Airbyte admin operations.
5
+
6
+ The server can run in two modes:
7
+ - **stdio mode** (default): For direct MCP client connections via stdin/stdout
8
+ - **HTTP mode**: For HTTP-based MCP connections, useful for containerized deployments
9
+
10
+ Environment Variables:
11
+ MCP_HTTP_HOST: Host to bind HTTP server to (default: 127.0.0.1)
12
+ MCP_HTTP_PORT: Port for HTTP server (default: 8082)
5
13
  """
6
14
 
7
15
  import asyncio
16
+ import os
8
17
  import sys
9
18
  from pathlib import Path
10
19
 
@@ -17,12 +26,16 @@ from airbyte_ops_mcp.mcp.cloud_connector_versions import (
17
26
  )
18
27
  from airbyte_ops_mcp.mcp.github import register_github_tools
19
28
  from airbyte_ops_mcp.mcp.github_repo_ops import register_github_repo_ops_tools
20
- from airbyte_ops_mcp.mcp.live_tests import register_live_tests_tools
21
29
  from airbyte_ops_mcp.mcp.prerelease import register_prerelease_tools
22
30
  from airbyte_ops_mcp.mcp.prod_db_queries import register_prod_db_query_tools
23
31
  from airbyte_ops_mcp.mcp.prompts import register_prompts
32
+ from airbyte_ops_mcp.mcp.regression_tests import register_regression_tests_tools
24
33
  from airbyte_ops_mcp.mcp.server_info import register_server_info_resources
25
34
 
35
+ # Default HTTP server configuration
36
+ DEFAULT_HTTP_HOST = "127.0.0.1"
37
+ DEFAULT_HTTP_PORT = 8082
38
+
26
39
  app: FastMCP = FastMCP(MCP_SERVER_NAME)
27
40
 
28
41
 
@@ -34,7 +47,7 @@ def register_server_assets(app: FastMCP) -> None:
34
47
  - REPO: GitHub repository operations
35
48
  - CLOUD: Cloud connector version management
36
49
  - PROMPTS: Prompt templates for common workflows
37
- - LIVE_TESTS: Live connection validation and regression tests
50
+ - REGRESSION_TESTS: Connector regression tests (single-version and comparison)
38
51
  - REGISTRY: Connector registry operations (future)
39
52
  - METADATA: Connector metadata operations (future)
40
53
  - QA: Connector quality assurance (future)
@@ -50,33 +63,93 @@ def register_server_assets(app: FastMCP) -> None:
50
63
  register_cloud_connector_version_tools(app)
51
64
  register_prod_db_query_tools(app)
52
65
  register_prompts(app)
53
- register_live_tests_tools(app)
66
+ register_regression_tests_tools(app)
54
67
 
55
68
 
56
69
  register_server_assets(app)
57
70
 
58
71
 
59
- def main() -> None:
60
- """Main entry point for the Airbyte Admin MCP server."""
61
- # Load environment variables from .env file in current working directory
72
+ def _load_env() -> None:
73
+ """Load environment variables from .env file if present."""
62
74
  env_file = Path.cwd() / ".env"
63
75
  if env_file.exists():
64
76
  load_dotenv(env_file)
65
77
  print(f"Loaded environment from: {env_file}", flush=True, file=sys.stderr)
66
78
 
79
+
80
+ def main() -> None:
81
+ """Main entry point for the Airbyte Admin MCP server (stdio mode).
82
+
83
+ This is the default entry point that runs the server in stdio mode,
84
+ suitable for direct MCP client connections.
85
+ """
86
+ _load_env()
87
+
67
88
  print("=" * 60, flush=True, file=sys.stderr)
68
- print("Starting Airbyte Admin MCP server.", file=sys.stderr)
89
+ print("Starting Airbyte Admin MCP server (stdio mode).", file=sys.stderr)
69
90
  try:
70
91
  asyncio.run(app.run_stdio_async(show_banner=False))
71
92
  except KeyboardInterrupt:
72
93
  print("Airbyte Admin MCP server interrupted by user.", file=sys.stderr)
73
- except Exception as ex:
74
- print(f"Error running Airbyte Admin MCP server: {ex}", file=sys.stderr)
75
- sys.exit(1)
76
94
 
77
95
  print("Airbyte Admin MCP server stopped.", file=sys.stderr)
78
96
  print("=" * 60, flush=True, file=sys.stderr)
79
- sys.exit(0)
97
+
98
+
99
+ def _parse_port(port_str: str | None, default: int) -> int:
100
+ """Parse and validate a port number from string.
101
+
102
+ Args:
103
+ port_str: Port string from environment variable, or None if not set
104
+ default: Default port to use if port_str is None
105
+
106
+ Returns:
107
+ Validated port number
108
+
109
+ Raises:
110
+ ValueError: If port_str is not a valid integer or out of range
111
+ """
112
+ if port_str is None:
113
+ return default
114
+
115
+ port_str = port_str.strip()
116
+ if not port_str.isdecimal():
117
+ raise ValueError(f"MCP_HTTP_PORT must be a valid integer, got: {port_str!r}")
118
+
119
+ port = int(port_str)
120
+ if not 1 <= port <= 65535:
121
+ raise ValueError(f"MCP_HTTP_PORT must be between 1 and 65535, got: {port}")
122
+
123
+ return port
124
+
125
+
126
+ def main_http() -> None:
127
+ """HTTP entry point for the Airbyte Admin MCP server.
128
+
129
+ This entry point runs the server in HTTP mode, suitable for containerized
130
+ deployments where the server needs to be accessible over HTTP.
131
+
132
+ Environment Variables:
133
+ MCP_HTTP_HOST: Host to bind to (default: 127.0.0.1)
134
+ MCP_HTTP_PORT: Port to listen on (default: 8082)
135
+ """
136
+ _load_env()
137
+
138
+ host = os.getenv("MCP_HTTP_HOST", DEFAULT_HTTP_HOST)
139
+ port = _parse_port(os.getenv("MCP_HTTP_PORT"), DEFAULT_HTTP_PORT)
140
+
141
+ print("=" * 60, flush=True, file=sys.stderr)
142
+ print(
143
+ f"Starting Airbyte Admin MCP server (HTTP mode) on {host}:{port}",
144
+ file=sys.stderr,
145
+ )
146
+ try:
147
+ app.run(transport="http", host=host, port=port)
148
+ except KeyboardInterrupt:
149
+ print("Airbyte Admin MCP server interrupted by user.", file=sys.stderr)
150
+
151
+ print("Airbyte Admin MCP server stopped.", file=sys.stderr)
152
+ print("=" * 60, flush=True, file=sys.stderr)
80
153
 
81
154
 
82
155
  if __name__ == "__main__":