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.
- {airbyte_internal_ops-0.1.11.dist-info → airbyte_internal_ops-0.2.1.dist-info}/METADATA +2 -2
- {airbyte_internal_ops-0.1.11.dist-info → airbyte_internal_ops-0.2.1.dist-info}/RECORD +41 -40
- {airbyte_internal_ops-0.1.11.dist-info → airbyte_internal_ops-0.2.1.dist-info}/entry_points.txt +1 -0
- airbyte_ops_mcp/__init__.py +2 -2
- airbyte_ops_mcp/cli/cloud.py +264 -301
- airbyte_ops_mcp/cloud_admin/api_client.py +51 -26
- airbyte_ops_mcp/cloud_admin/auth.py +32 -0
- airbyte_ops_mcp/cloud_admin/connection_config.py +2 -2
- airbyte_ops_mcp/constants.py +18 -0
- airbyte_ops_mcp/github_actions.py +94 -5
- airbyte_ops_mcp/mcp/_http_headers.py +254 -0
- airbyte_ops_mcp/mcp/_mcp_utils.py +2 -2
- airbyte_ops_mcp/mcp/cloud_connector_versions.py +162 -52
- airbyte_ops_mcp/mcp/github.py +34 -1
- airbyte_ops_mcp/mcp/prod_db_queries.py +67 -24
- airbyte_ops_mcp/mcp/{live_tests.py → regression_tests.py} +165 -152
- airbyte_ops_mcp/mcp/server.py +84 -11
- airbyte_ops_mcp/prod_db_access/db_engine.py +15 -11
- airbyte_ops_mcp/prod_db_access/queries.py +27 -15
- airbyte_ops_mcp/prod_db_access/sql.py +17 -16
- 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.1.11.dist-info → airbyte_internal_ops-0.2.1.dist-info}/WHEEL +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
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
|
|
2
|
-
"""MCP tools for
|
|
2
|
+
"""MCP tools for connector regression tests.
|
|
3
3
|
|
|
4
|
-
This module provides MCP tools for triggering
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
94
|
-
"""
|
|
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
|
|
134
|
-
"""Results from the
|
|
180
|
+
class RegressionTestExecutionResult(BaseModel):
|
|
181
|
+
"""Results from executing the connector (validations and record counts)."""
|
|
135
182
|
|
|
136
|
-
|
|
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
|
|
198
|
+
description="Error message if the execution failed",
|
|
152
199
|
)
|
|
153
200
|
|
|
154
201
|
|
|
155
|
-
class
|
|
156
|
-
"""Results from
|
|
202
|
+
class RegressionTestComparisonResult(BaseModel):
|
|
203
|
+
"""Results from comparing target vs control connector versions."""
|
|
157
204
|
|
|
158
|
-
|
|
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
|
|
216
|
+
description="Error message if the comparison failed",
|
|
170
217
|
)
|
|
171
218
|
|
|
172
219
|
|
|
173
|
-
class
|
|
174
|
-
"""Complete result of a
|
|
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
|
|
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
|
-
|
|
239
|
+
compare_versions: bool = Field(
|
|
193
240
|
default=False,
|
|
194
|
-
description="Whether
|
|
241
|
+
description="Whether comparison mode was used (target vs control)",
|
|
195
242
|
)
|
|
196
|
-
|
|
243
|
+
execution_result: RegressionTestExecutionResult | None = Field(
|
|
197
244
|
default=None,
|
|
198
|
-
description="Results from the
|
|
245
|
+
description="Results from executing the connector (validations and record counts)",
|
|
199
246
|
)
|
|
200
|
-
|
|
247
|
+
comparison_result: RegressionTestComparisonResult | None = Field(
|
|
201
248
|
default=None,
|
|
202
|
-
description="Results from
|
|
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
|
|
227
|
-
"""Response from starting a
|
|
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
|
|
305
|
+
def run_regression_tests(
|
|
259
306
|
connection_id: Annotated[str, "The Airbyte Cloud connection ID to test"],
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
"
|
|
263
|
-
|
|
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
|
-
|
|
317
|
+
skip_compare: Annotated[
|
|
270
318
|
bool,
|
|
271
|
-
"If True,
|
|
272
|
-
"If False, run
|
|
273
|
-
|
|
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
|
|
278
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
297
|
-
"
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
"
|
|
302
|
-
"(e.g.,
|
|
303
|
-
"
|
|
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
|
-
) ->
|
|
306
|
-
"""Start a
|
|
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
|
-
|
|
312
|
-
|
|
357
|
+
This tool triggers either the comparison or single-version regression test
|
|
358
|
+
workflow depending on the skip_compare parameter:
|
|
313
359
|
|
|
314
|
-
-
|
|
360
|
+
- skip_compare=False (default): Triggers comparison regression test workflow.
|
|
315
361
|
Compares the target connector version against a control (baseline) version.
|
|
316
|
-
|
|
317
|
-
|
|
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
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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=
|
|
400
|
+
message=str(e),
|
|
364
401
|
workflow_url=None,
|
|
365
402
|
)
|
|
366
403
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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=
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
|
431
|
-
workflow_inputs["
|
|
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=
|
|
436
|
-
repo=
|
|
444
|
+
owner=REGRESSION_TEST_REPO_OWNER,
|
|
445
|
+
repo=REGRESSION_TEST_REPO_NAME,
|
|
437
446
|
workflow_file=REGRESSION_TEST_WORKFLOW_FILE,
|
|
438
|
-
ref=
|
|
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(
|
|
444
|
-
|
|
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
|
|
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
|
|
463
|
+
return RunRegressionTestsResponse(
|
|
453
464
|
run_id=run_id,
|
|
454
465
|
status=TestRunStatus.QUEUED,
|
|
455
|
-
message=
|
|
456
|
-
|
|
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
|
|
469
|
-
"""Register
|
|
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
|
airbyte_ops_mcp/mcp/server.py
CHANGED
|
@@ -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
|
-
-
|
|
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
|
-
|
|
66
|
+
register_regression_tests_tools(app)
|
|
54
67
|
|
|
55
68
|
|
|
56
69
|
register_server_assets(app)
|
|
57
70
|
|
|
58
71
|
|
|
59
|
-
def
|
|
60
|
-
"""
|
|
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
|
-
|
|
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__":
|