airbyte-internal-ops 0.1.7__py3-none-any.whl → 0.1.9__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: airbyte-internal-ops
3
- Version: 0.1.7
3
+ Version: 0.1.9
4
4
  Summary: MCP and API interfaces that let the agents do the admin work
5
5
  Author-email: Aaron Steers <aj@airbyte.io>
6
6
  Keywords: admin,airbyte,api,mcp
@@ -1,6 +1,7 @@
1
1
  airbyte_ops_mcp/__init__.py,sha256=HhzURuYr29_UIdMrnWYaZB8ENr_kFkBdm4uqeiIW3Vw,760
2
2
  airbyte_ops_mcp/_annotations.py,sha256=MO-SBDnbykxxHDESG7d8rviZZ4WlZgJKv0a8eBqcEzQ,1757
3
- airbyte_ops_mcp/constants.py,sha256=eDO262DXtlPbWENwpk9GTllmkz9S_xT7G4qX2-BcfkE,1743
3
+ airbyte_ops_mcp/constants.py,sha256=419-AlRfwbbxeEEV9lhmXhpTUjsSdzJpfcuL_MZZtXM,1982
4
+ airbyte_ops_mcp/gcp_auth.py,sha256=5k-k145ZoYhHLjyDES8nrA8f8BBihRI0ykrdD1IcfOs,3599
4
5
  airbyte_ops_mcp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
6
  airbyte_ops_mcp/_legacy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
7
  airbyte_ops_mcp/_legacy/airbyte_ci/README.md,sha256=qEYx4geDR8AEDjrcA303h7Nol-CMDLojxUyiGzQprM8,236
@@ -206,7 +207,7 @@ airbyte_ops_mcp/_legacy/airbyte_ci/connector_qa/checks/documentation/templates/s
206
207
  airbyte_ops_mcp/_legacy/airbyte_ci/connector_qa/checks/documentation/templates/template.md.j2,sha256=bYxLfOP3GDwQC9Q3PiVbxsL5GS0p5b10k1dwzfOrm1M,1962
207
208
  airbyte_ops_mcp/_legacy/airbyte_ci/connector_qa/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
208
209
  airbyte_ops_mcp/_legacy/airbyte_ci/connector_qa/templates/qa_checks.md.j2,sha256=v3NyYUAYkqRiYAFDQ2NgpgYK6PIDhDKrqfRgpX8454s,1946
209
- airbyte_ops_mcp/_legacy/airbyte_ci/metadata_models/README.md,sha256=9uGj0cTaGawoD-uMOT8ImT19XXozWz6-r_aFRjZhN8Y,3567
210
+ airbyte_ops_mcp/_legacy/airbyte_ci/metadata_models/README.md,sha256=CQtTgVzHH3UdgRCohA9GyNhxIiYxVZmSwhMKWnfbocE,3569
210
211
  airbyte_ops_mcp/_legacy/airbyte_ci/metadata_models/package-lock.json,sha256=uazyPocrveknTDfwu4DlcaDQ5HwkPNf1Q_vDNpGMp0s,2233
211
212
  airbyte_ops_mcp/_legacy/airbyte_ci/metadata_models/package.json,sha256=_JMByXIhM9iYRKlLqheCVWXWswfwDzuXc5-A1VRUdUM,307
212
213
  airbyte_ops_mcp/_legacy/airbyte_ci/metadata_models/transform.py,sha256=cx-u9y0aYKs0M7rL47I62WQfcx6uIyzbL2wWv5GERNM,1939
@@ -281,7 +282,7 @@ airbyte_ops_mcp/_legacy/airbyte_ci/metadata_service/constants.py,sha256=bSXsx-RQ
281
282
  airbyte_ops_mcp/_legacy/airbyte_ci/metadata_service/docker_hub.py,sha256=K-6-QjTiSEPeJUnVJIMyYfTtu6uFoh0vLX579b04LSs,5528
282
283
  airbyte_ops_mcp/_legacy/airbyte_ci/metadata_service/gcs_upload.py,sha256=_r1nh54U6QFglFiPWBpxrSylE6Guu0Gjk7Bq5NKK4EE,26506
283
284
  airbyte_ops_mcp/_legacy/airbyte_ci/metadata_service/registry.py,sha256=3okT639PCZGL0p6tlWJoTYy-xI2igeAj6DerxkMC1_Q,15130
284
- airbyte_ops_mcp/_legacy/airbyte_ci/metadata_service/registry_entry.py,sha256=sVq0fgAnTIihROn9Iy5OHMswT-fF2JDTPsRo-bNF0yA,26417
285
+ airbyte_ops_mcp/_legacy/airbyte_ci/metadata_service/registry_entry.py,sha256=QT-UopRiFGJpabvoya-B3F32qrlB2qluOiNKj56Pv4g,26489
285
286
  airbyte_ops_mcp/_legacy/airbyte_ci/metadata_service/registry_report.py,sha256=dp13BxBiXG7DgxhzfupMm-sTFdjUVn9TyYrvo6KShU0,12600
286
287
  airbyte_ops_mcp/_legacy/airbyte_ci/metadata_service/sentry.py,sha256=e1O6_FHrHvqNNQdb5TkXs5Bhv7_hZ8fMiOWJr8H5tkI,1806
287
288
  airbyte_ops_mcp/_legacy/airbyte_ci/metadata_service/spec_cache.py,sha256=rcsJtu6B8_bmfXFv9U-IVjsVpPDJWLJYdH8e_XbT7yk,4482
@@ -350,24 +351,24 @@ airbyte_ops_mcp/cli/__init__.py,sha256=XpL7FyVfgabfBF2JR7u7NwJ2krlYqjd_OwLcWf-Xc
350
351
  airbyte_ops_mcp/cli/_base.py,sha256=I8tWnyQf0ks4r3J8N8h-5GZxyn37T-55KsbuHnxYlcg,415
351
352
  airbyte_ops_mcp/cli/_shared.py,sha256=jg-xMyGzTCGPqKd8VTfE_3kGPIyO_3Kx5sQbG4rPc0Y,1311
352
353
  airbyte_ops_mcp/cli/app.py,sha256=SEdBpqFUG2O8zGV5ifwptxrLGFph_dLr66-MX9d69gQ,789
353
- airbyte_ops_mcp/cli/cloud.py,sha256=BMFYs5bTEgdOhxwzBrtSyYMKaHhXnMM_SGzK2hFDPBY,32076
354
+ airbyte_ops_mcp/cli/cloud.py,sha256=OTc8o_MIw5DGGPnQ2kiUT98GCXS2I95vvsSN9_d7LVo,32077
354
355
  airbyte_ops_mcp/cli/gh.py,sha256=91b1AxFXvHQCFyXhrrym-756ZjnMCqvxFdmwCtma1zI,2046
355
- airbyte_ops_mcp/cli/registry.py,sha256=OpON1p4_A-G-FSfIpr6UlKYTjcj_zyiprKOu7qxwuhc,5787
356
+ airbyte_ops_mcp/cli/registry.py,sha256=-yiLJWSslV_qGi6ImXZYfXOJSE4oJBO7yICkyA_RiUo,5792
356
357
  airbyte_ops_mcp/cli/repo.py,sha256=G1hoQpH0XYhUH3FFOsia9xabGB0LP9o3XcwBuqvFVo0,16331
357
358
  airbyte_ops_mcp/cloud_admin/__init__.py,sha256=cqE96Q10Kp6elhH9DAi6TVsIwSUy3sooDLLrxTaktGk,816
358
- airbyte_ops_mcp/cloud_admin/api_client.py,sha256=4vZv1J4S2Q8ETl6gIB20X1X6KHTVV-bx__b2Ax8oqyc,17358
359
+ airbyte_ops_mcp/cloud_admin/api_client.py,sha256=6PovHDwOzo4fxSyk6viwvnXjCRIiC4uPZo0pGMx0Bdk,17359
359
360
  airbyte_ops_mcp/cloud_admin/auth.py,sha256=j45pRR8fg6CLwVdn7Uu5KW_kTz_CjRP6ZJGUzqHj_Dk,2558
360
361
  airbyte_ops_mcp/cloud_admin/connection_config.py,sha256=UtbIwuB7CA3WJr9oYRwlKDsjciqd_9ewWdml2f8DuXw,4887
361
362
  airbyte_ops_mcp/cloud_admin/models.py,sha256=YZ3FbEW-tZa50khKTTl4Bzvy_LsGyyQd6qcpXo62jls,2670
362
363
  airbyte_ops_mcp/connection_config_retriever/__init__.py,sha256=Xoi-YvARrNPhECdpwEDDkdwEpnvj8zuUlwULpf4iRrU,800
363
364
  airbyte_ops_mcp/connection_config_retriever/audit_logging.py,sha256=GjT4dVa0TtvGDmiBz9qwzcYCnSf9hTo7UM6l7ubUNE8,2846
364
- airbyte_ops_mcp/connection_config_retriever/retrieval.py,sha256=Bpbl1qoR0z1F8dgQOj-dB95UxqzGTXbkW4nHMPBIO-g,12112
365
+ airbyte_ops_mcp/connection_config_retriever/retrieval.py,sha256=s6yeCyrboWkUd6KdaheEo87x-rLtQNTL8XeR8O9z2HI,12160
365
366
  airbyte_ops_mcp/connection_config_retriever/secrets_resolution.py,sha256=12g0lZzhCzAPl4Iv4eMW6d76mvXjIBGspOnNhywzks4,3644
366
367
  airbyte_ops_mcp/live_tests/__init__.py,sha256=qJac67dt6DQCqif39HqeiG3Tr9xrxfP-ala8HsLZKis,1020
367
368
  airbyte_ops_mcp/live_tests/ci_output.py,sha256=NQATOGid0OCbLEl2NOtGK4cHLL5OxXhjmtanBjIlCyE,11369
368
369
  airbyte_ops_mcp/live_tests/config.py,sha256=dwWeY0tatdbwl9BqbhZ7EljoZDCtKmGO5fvOAIxeXmA,5873
369
370
  airbyte_ops_mcp/live_tests/connection_fetcher.py,sha256=5wIiA0VvCFNEc-fr6Po18gZMX3E5fyPOGf2SuVOqv5U,12799
370
- airbyte_ops_mcp/live_tests/connection_secret_retriever.py,sha256=C3yGoE8JSlkypKlpGYoI-5oczFexHyl5Z3avkDKyqQc,6445
371
+ airbyte_ops_mcp/live_tests/connection_secret_retriever.py,sha256=DtZYB4Y8CXfUXTFhmzrqzjuEFoICzz5Md3Ol_y9HCq0,4861
371
372
  airbyte_ops_mcp/live_tests/connector_runner.py,sha256=fGE_TCii9zhC3pbyBupJ3JVkuxOWB59Q1DgigcF3q04,9707
372
373
  airbyte_ops_mcp/live_tests/evaluation_modes.py,sha256=lAL6pEDmy_XCC7_m4_NXjt_f6Z8CXeAhMkc0FU8bm_M,1364
373
374
  airbyte_ops_mcp/live_tests/http_metrics.py,sha256=oTD7f2MnQOvx4plOxHop2bInQ0-whvuToSsrC7TIM-M,12469
@@ -384,29 +385,29 @@ airbyte_ops_mcp/live_tests/validation/catalog_validators.py,sha256=jqqVAMOk0mtdP
384
385
  airbyte_ops_mcp/live_tests/validation/record_validators.py,sha256=-7Ir2LWGCrtadK2JLuBgppSyk0RFJX6Nsy0lrabtwrs,7411
385
386
  airbyte_ops_mcp/mcp/__init__.py,sha256=QqkNkxzdXlg-W03urBAQ3zmtOKFPf35rXgO9ceUjpng,334
386
387
  airbyte_ops_mcp/mcp/_guidance.py,sha256=48tQSnDnxqXtyGJxxgjz0ZiI814o_7Fj7f6R8jpQ7so,2375
387
- airbyte_ops_mcp/mcp/_mcp_utils.py,sha256=Im3J3yCtXPbW_Kp-oh5b11Z2187V3-Zsos5LBgdvQrc,9111
388
- airbyte_ops_mcp/mcp/cloud_connector_versions.py,sha256=G8mVzhvSCmrTEqDseV57-wwL0s3oWdydgyvgMr027tU,10443
388
+ airbyte_ops_mcp/mcp/_mcp_utils.py,sha256=nhztHcoc-_ASPpJfoDBjxjjqEvQM6_QIrhp7F2UCrQk,11494
389
+ airbyte_ops_mcp/mcp/cloud_connector_versions.py,sha256=XxaS6WBP0sJPRwT7TTPhVH2PzhPqVWMNU5fVdWdxLLk,10361
389
390
  airbyte_ops_mcp/mcp/connector_analysis.py,sha256=OC4KrOSkMkKPkOisWnSv96BDDE5TQYHq-Jxa2vtjJpo,298
390
391
  airbyte_ops_mcp/mcp/connector_qa.py,sha256=aImpqdnqBPDrz10BS0owsV4kuIU2XdalzgbaGZsbOL0,258
391
- airbyte_ops_mcp/mcp/github.py,sha256=5ZPsSTy4-gummS96xGoG-n2RwCgyg3-UWAvmEmxd5x4,7686
392
- airbyte_ops_mcp/mcp/github_repo_ops.py,sha256=pE6RyATBfUKBOffQ6iyNJhHfCLjwq0pzVmmFYUg39UQ,5375
393
- airbyte_ops_mcp/mcp/live_tests.py,sha256=KnxZLuUNmm_3Clt0DU8H9rJ01zOKefnL_wqdSCMDjkE,17992
392
+ airbyte_ops_mcp/mcp/github.py,sha256=opmVWRwjNs_jeWejv8wHOtb-3J09hOlqxg98GCzmFLo,7627
393
+ airbyte_ops_mcp/mcp/github_repo_ops.py,sha256=PiERpt8abo20Gz4CfXhrDNlVM4o4FOt5sweZJND2a0s,5314
394
+ airbyte_ops_mcp/mcp/live_tests.py,sha256=eHfgNkEcY1OKzYiJmkxwOluLPiFHIdP4nm_H1H0MXDg,17940
394
395
  airbyte_ops_mcp/mcp/metadata.py,sha256=fwGW97WknR5lfKcQnFtK6dU87aA6TmLj1NkKyqDAV9g,270
395
- airbyte_ops_mcp/mcp/prerelease.py,sha256=2Mr0LdCLhEc9Q7CEtmganJXHGHCLCXODKlkSapLsSsY,9484
396
- airbyte_ops_mcp/mcp/prod_db_queries.py,sha256=L0p7LlF7GV5mXoH_2GwgQp04sXL2QA5v-RiuLsTb-yA,12614
397
- airbyte_ops_mcp/mcp/prompts.py,sha256=6opN4ZweQxfSdtoK0gL6wTrlxkRvxTQvH1VTmAuhoBE,1645
396
+ airbyte_ops_mcp/mcp/prerelease.py,sha256=08G6ogRqEauQyDxFLirUVaYeU3exAJd-DJn_8fXCNXg,9450
397
+ airbyte_ops_mcp/mcp/prod_db_queries.py,sha256=RkBVISfkbwML3grWONxYsULRnFEYdqDZVBZIyo6W8xE,14311
398
+ airbyte_ops_mcp/mcp/prompts.py,sha256=mJld9mdPECXYZffWXGSvNs4Xevx3rxqUGNlzGKVC2_s,1599
398
399
  airbyte_ops_mcp/mcp/registry.py,sha256=PW-VYUj42qx2pQ_apUkVaoUFq7VgB9zEU7-aGrkSCCw,290
399
400
  airbyte_ops_mcp/mcp/server.py,sha256=7zi91xioVTx1q-bEleekZH2c2JnbzDQt_6zxdEwnLbg,2958
400
- airbyte_ops_mcp/mcp/server_info.py,sha256=4yNBA_N_vUyLwVJqp7abyFuzZkcnv6-ck_Beb2SXqTE,2426
401
+ airbyte_ops_mcp/mcp/server_info.py,sha256=Yi4B1auW64QZGBDas5mro_vwTjvrP785TFNSBP7GhRg,2361
401
402
  airbyte_ops_mcp/prod_db_access/__init__.py,sha256=5pxouMPY1beyWlB0UwPnbaLTKTHqU6X82rbbgKY2vYU,1069
402
403
  airbyte_ops_mcp/prod_db_access/db_engine.py,sha256=ia1KcuQOXi3Qhy_MnxYmccCBJ4rAt_d4nVDjcyzje6o,4289
403
404
  airbyte_ops_mcp/prod_db_access/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
404
- airbyte_ops_mcp/prod_db_access/queries.py,sha256=bZZntlTXzvI2uSAFMkOfb_LcfCOlxyTuOddRVw4yU2M,8676
405
- airbyte_ops_mcp/prod_db_access/sql.py,sha256=zHPucNuMlfxz3aU8vYo1ziiGk0lIncG9XmblEoRDd4c,12725
405
+ airbyte_ops_mcp/prod_db_access/queries.py,sha256=q7PcI15EGh6jFS9MVB_gZt1a56YvrZV5hnwa5lgU2q0,10844
406
+ airbyte_ops_mcp/prod_db_access/sql.py,sha256=tWQAwMk8DzG8HpLIYglljlReI2oeYulQPsV31ocUJSw,16251
406
407
  airbyte_ops_mcp/registry/__init__.py,sha256=iEaPlt9GrnlaLbc__98TguNeZG8wuQu7S-_2QkhHcbA,858
407
408
  airbyte_ops_mcp/registry/models.py,sha256=B4L4TKr52wo0xs0CqvCBrpowqjShzVnZ5eTr2-EyhNs,2346
408
409
  airbyte_ops_mcp/registry/publish.py,sha256=VoPxsM2_0zJ829orzCRN-kjgcJtuBNyXgW4I9J680ro,12717
409
- airbyte_internal_ops-0.1.7.dist-info/METADATA,sha256=lIb2MpFriSDAUAYqwerfgA6a3zz3YOrwzDY17cyIZno,5282
410
- airbyte_internal_ops-0.1.7.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
411
- airbyte_internal_ops-0.1.7.dist-info/entry_points.txt,sha256=eUgJ9xIy9PlR-CgRbqRMsh1NVp6jz08v9bul9vCYlU4,111
412
- airbyte_internal_ops-0.1.7.dist-info/RECORD,,
410
+ airbyte_internal_ops-0.1.9.dist-info/METADATA,sha256=1tBlf96RtcJNsKARaZt4711QiVISWcAkex4OLBlUmjk,5282
411
+ airbyte_internal_ops-0.1.9.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
412
+ airbyte_internal_ops-0.1.9.dist-info/entry_points.txt,sha256=eUgJ9xIy9PlR-CgRbqRMsh1NVp6jz08v9bul9vCYlU4,111
413
+ airbyte_internal_ops-0.1.9.dist-info/RECORD,,
@@ -75,7 +75,7 @@ This will copy the specified connector version to your development bucket. This
75
75
  _💡 Note: A prerequisite is you have [gsutil](https://cloud.google.com/storage/docs/gsutil) installed and have run `gsutil auth login`_
76
76
 
77
77
  ```bash
78
- TARGET_BUCKET=<YOUR-DEV_BUCKET> CONNECTOR="airbyte/source-stripe" VERSION="3.17.0-dev.ea013c8741" poetry run poe copy-connector-from-prod
78
+ TARGET_BUCKET=<YOUR-DEV_BUCKET> CONNECTOR="airbyte/source-stripe" VERSION="3.17.0-preview.ea013c8" poetry run poe copy-connector-from-prod
79
79
  ```
80
80
 
81
81
  ### Promote Connector Version to Latest
@@ -87,5 +87,5 @@ _💡 Note: A prerequisite is you have [gsutil](https://cloud.google.com/storage
87
87
  _⚠️ Warning: Its important to know that this will remove ANY existing files in the latest folder that are not in the versioned folder as it calls `gsutil rsync` with `-d` enabled._
88
88
 
89
89
  ```bash
90
- TARGET_BUCKET=<YOUR-DEV_BUCKET> CONNECTOR="airbyte/source-stripe" VERSION="3.17.0-dev.ea013c8741" poetry run poe promote-connector-to-latest
90
+ TARGET_BUCKET=<YOUR-DEV_BUCKET> CONNECTOR="airbyte/source-stripe" VERSION="3.17.0-preview.ea013c8" poetry run poe promote-connector-to-latest
91
91
  ```
@@ -526,7 +526,7 @@ def generate_and_persist_registry_entry(
526
526
  bucket_name (str): The name of the GCS bucket.
527
527
  repo_metadata_file_path (pathlib.Path): The path to the spec file.
528
528
  registry_type (str): The registry type.
529
- docker_image_tag (str): The docker image tag associated with this release. Typically a semver string (e.g. '1.2.3'), possibly with a suffix (e.g. '1.2.3-dev.abcde12345')
529
+ docker_image_tag (str): The docker image tag associated with this release. Typically a semver string (e.g. '1.2.3'), possibly with a suffix (e.g. '1.2.3-preview.abcde12')
530
530
  is_prerelease (bool): Whether this is a prerelease, or a main release.
531
531
  """
532
532
  # Read the repo metadata dict to bootstrap ourselves. We need the docker repository,
@@ -536,7 +536,7 @@ def generate_and_persist_registry_entry(
536
536
 
537
537
  try:
538
538
  # Now that we have the docker repo, read the appropriate versioned metadata from GCS.
539
- # This metadata will differ in a few fields (e.g. in prerelease mode, dockerImageTag will contain the actual prerelease tag `1.2.3-dev.abcde12345`),
539
+ # This metadata will differ in a few fields (e.g. in prerelease mode, dockerImageTag will contain the actual prerelease tag `1.2.3-preview.abcde12`),
540
540
  # so we'll treat this as the source of truth (ish. See below for how we handle the registryOverrides field.)
541
541
  gcs_client = get_gcs_storage_client(gcs_creds=os.environ.get("GCS_CREDENTIALS"))
542
542
  bucket = gcs_client.bucket(bucket_name)
@@ -645,6 +645,7 @@ def generate_and_persist_registry_entry(
645
645
  if (
646
646
  "-rc" not in metadata_dict["data"]["dockerImageTag"]
647
647
  and "-dev" not in metadata_dict["data"]["dockerImageTag"]
648
+ and "-preview" not in metadata_dict["data"]["dockerImageTag"]
648
649
  ) and not metadata_dict["data"]["registryOverrides"][registry_type]["enabled"]:
649
650
  logger.info(
650
651
  f"{registry_type} is not enabled: deleting existing {registry_type} registry entry for {metadata_dict['data']['dockerRepository']} at latest path."
@@ -117,7 +117,7 @@ def set_version_override(
117
117
  version: Annotated[
118
118
  str,
119
119
  Parameter(
120
- help="The semver version string to pin to (e.g., '2.1.5-dev.abc1234567')."
120
+ help="The semver version string to pin to (e.g., '2.1.5-preview.abc1234')."
121
121
  ),
122
122
  ],
123
123
  reason: Annotated[
@@ -63,7 +63,7 @@ def publish_prerelease(
63
63
  """Publish a connector prerelease to the Airbyte registry.
64
64
 
65
65
  Triggers the publish-connectors-prerelease workflow in the airbytehq/airbyte
66
- repository. Pre-release versions are tagged with format: {version}-dev.{git-sha}
66
+ repository. Pre-release versions are tagged with format: {version}-preview.{git-sha}
67
67
 
68
68
  Requires GITHUB_CONNECTOR_PUBLISHING_PAT or GITHUB_TOKEN environment variable
69
69
  with 'actions:write' permission.
@@ -162,7 +162,7 @@ def inspect_image(
162
162
  ],
163
163
  tag: Annotated[
164
164
  str,
165
- Parameter(help="Image tag (e.g., '2.1.5-dev.abc1234567')."),
165
+ Parameter(help="Image tag (e.g., '2.1.5-preview.abc1234')."),
166
166
  ],
167
167
  ) -> None:
168
168
  """Check if a Docker image exists on DockerHub.
@@ -160,7 +160,7 @@ def resolve_connector_version_id(
160
160
  Args:
161
161
  actor_definition_id: The actor definition ID
162
162
  connector_type: Either "source" or "destination"
163
- version: The version string (e.g., "0.1.47-dev.abe7cb4ddb")
163
+ version: The version string (e.g., "0.1.47-preview.abe7cb4")
164
164
  api_root: The API root URL
165
165
  client_id: The Airbyte Cloud client ID
166
166
  client_secret: The Airbyte Cloud client secret
@@ -24,6 +24,7 @@ from airbyte_ops_mcp.connection_config_retriever.secrets_resolution import (
24
24
  get_resolved_config,
25
25
  )
26
26
  from airbyte_ops_mcp.constants import CLOUD_REGISTRY_URL, ConnectionObject
27
+ from airbyte_ops_mcp.gcp_auth import get_secret_manager_client
27
28
  from airbyte_ops_mcp.prod_db_access.db_engine import get_pool
28
29
 
29
30
  LOGGER = logging.getLogger(__name__)
@@ -306,7 +307,7 @@ def retrieve_objects(
306
307
  """
307
308
  connection_candidates = [TestingCandidate(connection_id=connection_id)]
308
309
 
309
- secret_manager_client = secretmanager.SecretManagerServiceClient()
310
+ secret_manager_client = get_secret_manager_client()
310
311
  connection_pool = get_pool(secret_manager_client)
311
312
 
312
313
  with connection_pool.connect() as db_conn:
@@ -12,6 +12,10 @@ MCP_SERVER_NAME = "airbyte-internal-ops"
12
12
  ENV_AIRBYTE_INTERNAL_ADMIN_FLAG = "AIRBYTE_INTERNAL_ADMIN_FLAG"
13
13
  ENV_AIRBYTE_INTERNAL_ADMIN_USER = "AIRBYTE_INTERNAL_ADMIN_USER"
14
14
 
15
+ # Environment variable for GCP credentials (JSON content, not file path)
16
+ ENV_GCP_PROD_DB_ACCESS_CREDENTIALS = "GCP_PROD_DB_ACCESS_CREDENTIALS"
17
+ """Environment variable containing GCP service account JSON credentials for prod DB access."""
18
+
15
19
  # Expected values for internal admin authentication
16
20
  EXPECTED_ADMIN_FLAG_VALUE = "airbyte.io"
17
21
  EXPECTED_ADMIN_EMAIL_DOMAIN = "@airbyte.io"
@@ -0,0 +1,99 @@
1
+ # Copyright (c) 2025 Airbyte, Inc., all rights reserved.
2
+ """Centralized GCP authentication utilities.
3
+
4
+ This module provides a single code path for GCP credential handling across
5
+ the airbyte-ops-mcp codebase. It supports both standard Application Default
6
+ Credentials (ADC) and the GCP_PROD_DB_ACCESS_CREDENTIALS environment variable
7
+ used internally at Airbyte.
8
+
9
+ Usage:
10
+ from airbyte_ops_mcp.gcp_auth import get_secret_manager_client
11
+
12
+ # Get a properly authenticated Secret Manager client
13
+ client = get_secret_manager_client()
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ import os
20
+ import tempfile
21
+ from pathlib import Path
22
+
23
+ from google.cloud import secretmanager
24
+
25
+ from airbyte_ops_mcp.constants import ENV_GCP_PROD_DB_ACCESS_CREDENTIALS
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ # Environment variable name (internal to GCP libraries)
30
+ ENV_GOOGLE_APPLICATION_CREDENTIALS = "GOOGLE_APPLICATION_CREDENTIALS"
31
+
32
+ # Module-level cache for the credentials file path
33
+ _credentials_file_path: str | None = None
34
+
35
+
36
+ def ensure_adc_credentials() -> str | None:
37
+ """Ensure GCP Application Default Credentials are available.
38
+
39
+ If GOOGLE_APPLICATION_CREDENTIALS is not set but GCP_PROD_DB_ACCESS_CREDENTIALS is,
40
+ write the JSON credentials to a temp file and set GOOGLE_APPLICATION_CREDENTIALS
41
+ to point to that file. This provides a fallback for internal employees who use
42
+ GCP_PROD_DB_ACCESS_CREDENTIALS as their standard credential source.
43
+
44
+ Note: GOOGLE_APPLICATION_CREDENTIALS must be a file path, not JSON content.
45
+ The GCP_PROD_DB_ACCESS_CREDENTIALS env var contains the JSON content directly,
46
+ so we write it to a temp file first.
47
+
48
+ This function is idempotent and safe to call multiple times.
49
+
50
+ Returns:
51
+ The path to the credentials file if one was created, or None if
52
+ GOOGLE_APPLICATION_CREDENTIALS was already set.
53
+ """
54
+ global _credentials_file_path
55
+
56
+ # If GOOGLE_APPLICATION_CREDENTIALS is already set, nothing to do
57
+ if ENV_GOOGLE_APPLICATION_CREDENTIALS in os.environ:
58
+ return None
59
+
60
+ # Check if we have the fallback credentials
61
+ gsm_creds = os.getenv(ENV_GCP_PROD_DB_ACCESS_CREDENTIALS)
62
+ if not gsm_creds:
63
+ return None
64
+
65
+ # Reuse the same file path if we've already written credentials and file still exists
66
+ if _credentials_file_path is not None and Path(_credentials_file_path).exists():
67
+ os.environ[ENV_GOOGLE_APPLICATION_CREDENTIALS] = _credentials_file_path
68
+ return _credentials_file_path
69
+
70
+ # Write credentials to a temp file
71
+ # Use a unique filename based on PID to avoid collisions between processes
72
+ creds_file = Path(tempfile.gettempdir()) / f"gcp_prod_db_creds_{os.getpid()}.json"
73
+ creds_file.write_text(gsm_creds)
74
+
75
+ # Set restrictive permissions (owner read/write only)
76
+ creds_file.chmod(0o600)
77
+
78
+ _credentials_file_path = str(creds_file)
79
+ os.environ[ENV_GOOGLE_APPLICATION_CREDENTIALS] = _credentials_file_path
80
+
81
+ logger.debug(
82
+ f"Wrote {ENV_GCP_PROD_DB_ACCESS_CREDENTIALS} to {creds_file} and set "
83
+ f"{ENV_GOOGLE_APPLICATION_CREDENTIALS}"
84
+ )
85
+
86
+ return _credentials_file_path
87
+
88
+
89
+ def get_secret_manager_client() -> secretmanager.SecretManagerServiceClient:
90
+ """Get a Secret Manager client with proper credential handling.
91
+
92
+ This function ensures GCP credentials are available (supporting the
93
+ GCP_PROD_DB_ACCESS_CREDENTIALS fallback) before creating the client.
94
+
95
+ Returns:
96
+ A configured SecretManagerServiceClient instance.
97
+ """
98
+ ensure_adc_credentials()
99
+ return secretmanager.SecretManagerServiceClient()
@@ -32,15 +32,14 @@ from __future__ import annotations
32
32
 
33
33
  import logging
34
34
  import os
35
- import tempfile
36
35
  from dataclasses import replace
37
- from pathlib import Path
38
36
  from typing import TYPE_CHECKING
39
37
 
40
38
  from airbyte_ops_mcp.connection_config_retriever import (
41
39
  ConnectionObject,
42
40
  retrieve_objects,
43
41
  )
42
+ from airbyte_ops_mcp.gcp_auth import ensure_adc_credentials
44
43
 
45
44
  if TYPE_CHECKING:
46
45
  from airbyte_ops_mcp.live_tests.connection_fetcher import ConnectionData
@@ -50,39 +49,6 @@ logger = logging.getLogger(__name__)
50
49
  # Environment variable to enable secret retrieval
51
50
  ENV_USE_SECRET_RETRIEVER = "USE_CONNECTION_SECRET_RETRIEVER"
52
51
 
53
- # GCP credential environment variables
54
- ENV_GOOGLE_APPLICATION_CREDENTIALS = "GOOGLE_APPLICATION_CREDENTIALS"
55
- ENV_GCP_PROD_DB_ACCESS_CREDENTIALS = "GCP_PROD_DB_ACCESS_CREDENTIALS"
56
-
57
-
58
- def _ensure_gcp_credentials_env() -> None:
59
- """Ensure GCP credentials are available via standard env var.
60
-
61
- If GOOGLE_APPLICATION_CREDENTIALS is not set but GCP_PROD_DB_ACCESS_CREDENTIALS is,
62
- write the JSON credentials to a temp file and set GOOGLE_APPLICATION_CREDENTIALS
63
- to point to that file. This provides a fallback for internal employees who use
64
- GCP_PROD_DB_ACCESS_CREDENTIALS as their standard credential source for prod
65
- database access.
66
-
67
- Note: GOOGLE_APPLICATION_CREDENTIALS must be a file path, not JSON content.
68
- The GCP_PROD_DB_ACCESS_CREDENTIALS env var contains the JSON content directly,
69
- so we write it to a temp file first.
70
-
71
- This function is idempotent and safe to call multiple times.
72
- """
73
- if ENV_GOOGLE_APPLICATION_CREDENTIALS not in os.environ:
74
- gsm_creds = os.getenv(ENV_GCP_PROD_DB_ACCESS_CREDENTIALS)
75
- if gsm_creds:
76
- # Write credentials to a temp file since GOOGLE_APPLICATION_CREDENTIALS
77
- # expects a file path, not JSON content
78
- creds_file = Path(tempfile.gettempdir()) / "gcp_prod_db_creds.json"
79
- creds_file.write_text(gsm_creds)
80
- os.environ[ENV_GOOGLE_APPLICATION_CREDENTIALS] = str(creds_file)
81
- logger.debug(
82
- f"Wrote {ENV_GCP_PROD_DB_ACCESS_CREDENTIALS} to {creds_file} and set "
83
- f"{ENV_GOOGLE_APPLICATION_CREDENTIALS}"
84
- )
85
-
86
52
 
87
53
  def is_secret_retriever_enabled() -> bool:
88
54
  """Check if secret retrieval is enabled via environment variable.
@@ -120,7 +86,7 @@ def retrieve_unmasked_config(
120
86
  The unmasked source config dict, or None if retrieval fails.
121
87
  """
122
88
  # Ensure GCP credentials are available (supports GCP_PROD_DB_ACCESS_CREDENTIALS fallback)
123
- _ensure_gcp_credentials_env()
89
+ ensure_adc_credentials()
124
90
 
125
91
  # Only request the source config - that's all we need for secrets
126
92
  requested_objects = [ConnectionObject.SOURCE_CONFIG]
@@ -2,14 +2,17 @@
2
2
  """Deferred MCP capability registration for tools, prompts, and resources.
3
3
 
4
4
  This module provides a decorator to tag tool functions with MCP annotations
5
- for deferred registration.
5
+ for deferred registration. The domain for each tool is automatically derived
6
+ from the file stem of the module where the tool is defined.
6
7
  """
7
8
 
8
9
  from __future__ import annotations
9
10
 
11
+ import inspect
10
12
  from collections.abc import Callable
11
13
  from dataclasses import dataclass
12
14
  from enum import Enum
15
+ from pathlib import Path
13
16
  from typing import Any, TypeVar
14
17
 
15
18
  from fastmcp import FastMCP
@@ -94,8 +97,39 @@ def should_register_tool(annotations: dict[str, Any]) -> bool:
94
97
  return True
95
98
 
96
99
 
100
+ def _get_caller_file_stem() -> str:
101
+ """Get the file stem of the caller's module.
102
+
103
+ Walks up the call stack to find the first frame outside this module,
104
+ then returns the stem of that file (e.g., "github" for "github.py").
105
+
106
+ Returns:
107
+ The file stem of the calling module.
108
+ """
109
+ for frame_info in inspect.stack():
110
+ # Skip frames from this module
111
+ if frame_info.filename != __file__:
112
+ return Path(frame_info.filename).stem
113
+ return "unknown"
114
+
115
+
116
+ def _normalize_domain(domain: str) -> str:
117
+ """Normalize a domain string to its simple form.
118
+
119
+ Handles both file stems (e.g., "github") and module names
120
+ (e.g., "airbyte_ops_mcp.mcp.github") by extracting the last segment.
121
+
122
+ Args:
123
+ domain: A domain string, either a simple name or a dotted module path.
124
+
125
+ Returns:
126
+ The normalized domain (last segment of a dotted path, or the input if no dots).
127
+ """
128
+ return domain.rsplit(".", 1)[-1]
129
+
130
+
97
131
  def mcp_tool(
98
- domain: ToolDomain | str,
132
+ domain: ToolDomain | str | None = None,
99
133
  *,
100
134
  read_only: bool = False,
101
135
  destructive: bool = False,
@@ -108,8 +142,12 @@ def mcp_tool(
108
142
  This decorator stores the annotations on the function for later use during
109
143
  deferred registration. It does not register the tool immediately.
110
144
 
145
+ The domain is automatically derived from the file stem of the module where
146
+ the tool is defined (e.g., tools in "github.py" get domain "github").
147
+
111
148
  Args:
112
- domain: The domain this tool belongs to (e.g., ToolDomain.REGISTRY, "registry")
149
+ domain: Optional explicit domain override. If not provided, the domain
150
+ is automatically derived from the caller's file stem.
113
151
  read_only: If True, tool only reads without making changes (default: False)
114
152
  destructive: If True, tool modifies/deletes existing data (default: False)
115
153
  idempotent: If True, repeated calls have same effect (default: False)
@@ -121,11 +159,18 @@ def mcp_tool(
121
159
  Decorator function that tags the tool with annotations
122
160
 
123
161
  Example:
124
- @mcp_tool(ToolDomain.REGISTRY, read_only=True, idempotent=True)
162
+ @mcp_tool(read_only=True, idempotent=True)
125
163
  def list_connectors_in_repo():
126
164
  ...
127
165
  """
128
- domain_str = domain.value if isinstance(domain, ToolDomain) else domain
166
+ # Auto-derive domain from caller's file stem if not provided
167
+ if domain is None:
168
+ domain_str = _get_caller_file_stem()
169
+ elif isinstance(domain, ToolDomain):
170
+ domain_str = domain.value
171
+ else:
172
+ domain_str = domain
173
+
129
174
  annotations: dict[str, Any] = {
130
175
  "domain": domain_str,
131
176
  READ_ONLY_HINT: read_only,
@@ -156,7 +201,8 @@ def mcp_prompt(
156
201
  Args:
157
202
  name: Unique name for the prompt
158
203
  description: Human-readable description of the prompt
159
- domain: Optional domain for filtering (e.g., ToolDomain.PROMPTS)
204
+ domain: Optional domain for filtering. If not provided, automatically
205
+ derived from the caller's file stem.
160
206
 
161
207
  Returns:
162
208
  Decorator function that registers the prompt
@@ -164,15 +210,20 @@ def mcp_prompt(
164
210
  Raises:
165
211
  ValueError: If a prompt with the same name is already registered
166
212
  """
213
+ # Auto-derive domain from caller's file stem if not provided
214
+ if domain is None:
215
+ domain_str = _get_caller_file_stem()
216
+ elif isinstance(domain, ToolDomain):
217
+ domain_str = domain.value
218
+ else:
219
+ domain_str = domain
167
220
 
168
221
  def decorator(func: Callable[..., list[dict[str, str]]]):
169
- domain_str = domain.value if isinstance(domain, ToolDomain) else domain
170
222
  annotations = {
171
223
  "name": name,
172
224
  "description": description,
225
+ "domain": domain_str,
173
226
  }
174
- if domain_str is not None:
175
- annotations["domain"] = domain_str
176
227
  _REGISTERED_PROMPTS.append((func, annotations))
177
228
  return func
178
229
 
@@ -191,7 +242,8 @@ def mcp_resource(
191
242
  uri: Unique URI for the resource
192
243
  description: Human-readable description of the resource
193
244
  mime_type: MIME type of the resource content
194
- domain: Optional domain for filtering (e.g., ToolDomain.SERVER_INFO)
245
+ domain: Optional domain for filtering. If not provided, automatically
246
+ derived from the caller's file stem.
195
247
 
196
248
  Returns:
197
249
  Decorator function that registers the resource
@@ -199,16 +251,21 @@ def mcp_resource(
199
251
  Raises:
200
252
  ValueError: If a resource with the same URI is already registered
201
253
  """
254
+ # Auto-derive domain from caller's file stem if not provided
255
+ if domain is None:
256
+ domain_str = _get_caller_file_stem()
257
+ elif isinstance(domain, ToolDomain):
258
+ domain_str = domain.value
259
+ else:
260
+ domain_str = domain
202
261
 
203
262
  def decorator(func: Callable[..., Any]):
204
- domain_str = domain.value if isinstance(domain, ToolDomain) else domain
205
263
  annotations = {
206
264
  "uri": uri,
207
265
  "description": description,
208
266
  "mime_type": mime_type,
267
+ "domain": domain_str,
209
268
  }
210
- if domain_str is not None:
211
- annotations["domain"] = domain_str
212
269
  _REGISTERED_RESOURCES.append((func, annotations))
213
270
  return func
214
271
 
@@ -226,11 +283,14 @@ def _register_mcp_callables(
226
283
 
227
284
  Args:
228
285
  app: The FastMCP app instance
229
- domain: The domain to register tools for (e.g., ToolDomain.REGISTRY, "registry")
286
+ domain: The domain to register tools for. Can be a simple name (e.g., "github")
287
+ or a full module path (e.g., "airbyte_ops_mcp.mcp.github" from __name__).
230
288
  resource_list: List of (callable, annotations) tuples to register
231
289
  register_fn: Function to call for each registration
232
290
  """
233
291
  domain_str = domain.value if isinstance(domain, ToolDomain) else domain
292
+ # Normalize to handle both file stems and __name__ module paths
293
+ domain_str = _normalize_domain(domain_str)
234
294
 
235
295
  filtered_callables = [
236
296
  (func, ann) for func, ann in resource_list if ann.get("domain") == domain_str
@@ -242,14 +302,17 @@ def _register_mcp_callables(
242
302
 
243
303
  def register_mcp_tools(
244
304
  app: FastMCP,
245
- domain: ToolDomain | str,
305
+ domain: ToolDomain | str | None = None,
246
306
  ) -> None:
247
307
  """Register tools with the FastMCP app, filtered by domain.
248
308
 
249
309
  Args:
250
310
  app: The FastMCP app instance
251
- domain: The domain to register for (e.g., ToolDomain.REGISTRY, "registry")
311
+ domain: The domain to register for. If not provided, automatically
312
+ derived from the caller's file stem.
252
313
  """
314
+ if domain is None:
315
+ domain = _get_caller_file_stem()
253
316
 
254
317
  def _register_fn(
255
318
  app: FastMCP,
@@ -271,14 +334,17 @@ def register_mcp_tools(
271
334
 
272
335
  def register_mcp_prompts(
273
336
  app: FastMCP,
274
- domain: ToolDomain | str,
337
+ domain: ToolDomain | str | None = None,
275
338
  ) -> None:
276
339
  """Register prompt callables with the FastMCP app, filtered by domain.
277
340
 
278
341
  Args:
279
342
  app: The FastMCP app instance
280
- domain: The domain to register for (e.g., ToolDomain.PROMPTS, "prompts")
343
+ domain: The domain to register for. If not provided, automatically
344
+ derived from the caller's file stem.
281
345
  """
346
+ if domain is None:
347
+ domain = _get_caller_file_stem()
282
348
 
283
349
  def _register_fn(
284
350
  app: FastMCP,
@@ -300,14 +366,17 @@ def register_mcp_prompts(
300
366
 
301
367
  def register_mcp_resources(
302
368
  app: FastMCP,
303
- domain: ToolDomain | str,
369
+ domain: ToolDomain | str | None = None,
304
370
  ) -> None:
305
371
  """Register resource callables with the FastMCP app, filtered by domain.
306
372
 
307
373
  Args:
308
374
  app: The FastMCP app instance
309
- domain: The domain to register for (e.g., ToolDomain.SERVER_INFO, "server_info")
375
+ domain: The domain to register for. If not provided, automatically
376
+ derived from the caller's file stem.
310
377
  """
378
+ if domain is None:
379
+ domain = _get_caller_file_stem()
311
380
 
312
381
  def _register_fn(
313
382
  app: FastMCP,
@@ -33,7 +33,7 @@ from airbyte_ops_mcp.cloud_admin.models import (
33
33
  ConnectorVersionInfo,
34
34
  VersionOverrideOperationResult,
35
35
  )
36
- from airbyte_ops_mcp.mcp._mcp_utils import ToolDomain, mcp_tool, register_mcp_tools
36
+ from airbyte_ops_mcp.mcp._mcp_utils import mcp_tool, register_mcp_tools
37
37
 
38
38
 
39
39
  def _get_workspace(workspace_id: str) -> CloudWorkspace:
@@ -63,7 +63,6 @@ def _get_workspace(workspace_id: str) -> CloudWorkspace:
63
63
 
64
64
 
65
65
  @mcp_tool(
66
- ToolDomain.CLOUD_ADMIN,
67
66
  read_only=True,
68
67
  idempotent=True,
69
68
  open_world=True,
@@ -117,7 +116,6 @@ def get_cloud_connector_version(
117
116
 
118
117
 
119
118
  @mcp_tool(
120
- ToolDomain.CLOUD_ADMIN,
121
119
  destructive=True,
122
120
  idempotent=False,
123
121
  open_world=True,
@@ -297,4 +295,4 @@ def register_cloud_connector_version_tools(app: FastMCP) -> None:
297
295
  Args:
298
296
  app: FastMCP application instance
299
297
  """
300
- register_mcp_tools(app, domain=ToolDomain.CLOUD_ADMIN)
298
+ register_mcp_tools(app, domain=__name__)
@@ -15,7 +15,7 @@ import requests
15
15
  from fastmcp import FastMCP
16
16
  from pydantic import BaseModel, Field
17
17
 
18
- from airbyte_ops_mcp.mcp._mcp_utils import ToolDomain, mcp_tool, register_mcp_tools
18
+ from airbyte_ops_mcp.mcp._mcp_utils import mcp_tool, register_mcp_tools
19
19
 
20
20
  GITHUB_API_BASE = "https://api.github.com"
21
21
  DOCKERHUB_API_BASE = "https://hub.docker.com/v2"
@@ -115,7 +115,6 @@ def _get_workflow_run(
115
115
 
116
116
 
117
117
  @mcp_tool(
118
- ToolDomain.REPO,
119
118
  read_only=True,
120
119
  idempotent=True,
121
120
  open_world=True,
@@ -205,7 +204,7 @@ def _check_dockerhub_image(
205
204
 
206
205
  Args:
207
206
  image: Docker image name (e.g., "airbyte/source-github")
208
- tag: Image tag (e.g., "2.1.5-dev.abc1234567")
207
+ tag: Image tag (e.g., "2.1.5-preview.abc1234")
209
208
 
210
209
  Returns:
211
210
  Tag data dictionary if found, None if not found.
@@ -222,7 +221,6 @@ def _check_dockerhub_image(
222
221
 
223
222
 
224
223
  @mcp_tool(
225
- ToolDomain.REPO,
226
224
  read_only=True,
227
225
  idempotent=True,
228
226
  open_world=True,
@@ -234,7 +232,7 @@ def get_docker_image_info(
234
232
  ],
235
233
  tag: Annotated[
236
234
  str,
237
- Field(description="Image tag (e.g., '2.1.5-dev.abc1234567')"),
235
+ Field(description="Image tag (e.g., '2.1.5-preview.abc1234')"),
238
236
  ],
239
237
  ) -> DockerImageInfo:
240
238
  """Check if a Docker image exists on DockerHub.
@@ -276,4 +274,4 @@ def register_github_tools(app: FastMCP) -> None:
276
274
  Args:
277
275
  app: FastMCP application instance
278
276
  """
279
- register_mcp_tools(app, domain=ToolDomain.REPO)
277
+ register_mcp_tools(app, domain=__name__)
@@ -15,7 +15,7 @@ from pydantic import BaseModel
15
15
  from airbyte_ops_mcp.airbyte_repo.bump_version import bump_connector_version
16
16
  from airbyte_ops_mcp.airbyte_repo.list_connectors import list_connectors
17
17
  from airbyte_ops_mcp.airbyte_repo.utils import resolve_diff_range
18
- from airbyte_ops_mcp.mcp._mcp_utils import ToolDomain, mcp_tool, register_mcp_tools
18
+ from airbyte_ops_mcp.mcp._mcp_utils import mcp_tool, register_mcp_tools
19
19
 
20
20
 
21
21
  class ConnectorListResponse(BaseModel):
@@ -36,7 +36,6 @@ class BumpVersionResponse(BaseModel):
36
36
 
37
37
 
38
38
  @mcp_tool(
39
- ToolDomain.REPO,
40
39
  read_only=True,
41
40
  idempotent=True,
42
41
  open_world=False,
@@ -102,7 +101,6 @@ def list_connectors_in_repo(
102
101
 
103
102
 
104
103
  @mcp_tool(
105
- ToolDomain.REPO,
106
104
  read_only=False,
107
105
  idempotent=False,
108
106
  open_world=False,
@@ -164,4 +162,4 @@ def register_github_repo_ops_tools(app: FastMCP) -> None:
164
162
  Args:
165
163
  app: FastMCP application instance
166
164
  """
167
- register_mcp_tools(app, domain=ToolDomain.REPO)
165
+ register_mcp_tools(app, domain=__name__)
@@ -21,7 +21,7 @@ from airbyte.cloud.auth import resolve_cloud_client_id, resolve_cloud_client_sec
21
21
  from fastmcp import FastMCP
22
22
  from pydantic import BaseModel, Field
23
23
 
24
- from airbyte_ops_mcp.mcp._mcp_utils import ToolDomain, mcp_tool, register_mcp_tools
24
+ from airbyte_ops_mcp.mcp._mcp_utils import mcp_tool, register_mcp_tools
25
25
 
26
26
  logger = logging.getLogger(__name__)
27
27
 
@@ -308,7 +308,6 @@ class RunLiveConnectionTestsResponse(BaseModel):
308
308
 
309
309
 
310
310
  @mcp_tool(
311
- ToolDomain.LIVE_TESTS,
312
311
  read_only=False,
313
312
  idempotent=False,
314
313
  open_world=True,
@@ -512,4 +511,4 @@ def register_live_tests_tools(app: FastMCP) -> None:
512
511
  Args:
513
512
  app: FastMCP application instance
514
513
  """
515
- register_mcp_tools(app, domain=ToolDomain.LIVE_TESTS)
514
+ register_mcp_tools(app, domain=__name__)
@@ -16,7 +16,7 @@ import yaml
16
16
  from fastmcp import FastMCP
17
17
  from pydantic import BaseModel, Field
18
18
 
19
- from airbyte_ops_mcp.mcp._mcp_utils import ToolDomain, mcp_tool, register_mcp_tools
19
+ from airbyte_ops_mcp.mcp._mcp_utils import mcp_tool, register_mcp_tools
20
20
 
21
21
  GITHUB_API_BASE = "https://api.github.com"
22
22
  DEFAULT_REPO_OWNER = "airbytehq"
@@ -111,7 +111,7 @@ def _get_pr_head_info(
111
111
  return PRHeadInfo(
112
112
  ref=pr_data["head"]["ref"],
113
113
  sha=sha,
114
- short_sha=sha[:10],
114
+ short_sha=sha[:7],
115
115
  )
116
116
 
117
117
 
@@ -205,7 +205,6 @@ def _trigger_workflow_dispatch(
205
205
 
206
206
 
207
207
  @mcp_tool(
208
- ToolDomain.REPO,
209
208
  read_only=False,
210
209
  destructive=False,
211
210
  idempotent=False,
@@ -236,7 +235,7 @@ def publish_connector_to_airbyte_registry(
236
235
  publish-connectors-prerelease workflow in the airbytehq/airbyte repository,
237
236
  which publishes a pre-release version of the specified connector from the PR branch.
238
237
 
239
- Pre-release versions are tagged with the format: {version}-dev.{10-char-git-sha}
238
+ Pre-release versions are tagged with the format: {version}-preview.{7-char-git-sha}
240
239
  These versions are available for version pinning via the scoped_configuration API.
241
240
 
242
241
  Requires GITHUB_CONNECTOR_PUBLISHING_PAT or GITHUB_TOKEN environment variable
@@ -291,7 +290,7 @@ def publish_connector_to_airbyte_registry(
291
290
  docker_image = data.get("dockerRepository")
292
291
  base_version = data.get("dockerImageTag")
293
292
  if base_version:
294
- docker_image_tag = f"{base_version}-dev.{head_info.short_sha}"
293
+ docker_image_tag = f"{base_version}-preview.{head_info.short_sha}"
295
294
 
296
295
  return PrereleaseWorkflowResult(
297
296
  success=True,
@@ -310,4 +309,4 @@ def register_prerelease_tools(app: FastMCP) -> None:
310
309
  Args:
311
310
  app: FastMCP application instance
312
311
  """
313
- register_mcp_tools(app, domain=ToolDomain.REPO)
312
+ register_mcp_tools(app, domain=__name__)
@@ -14,12 +14,13 @@ from airbyte.exceptions import PyAirbyteInputError
14
14
  from fastmcp import FastMCP
15
15
  from pydantic import Field
16
16
 
17
- from airbyte_ops_mcp.mcp._mcp_utils import ToolDomain, mcp_tool, register_mcp_tools
17
+ from airbyte_ops_mcp.mcp._mcp_utils import mcp_tool, register_mcp_tools
18
18
  from airbyte_ops_mcp.prod_db_access.queries import (
19
19
  query_actors_pinned_to_version,
20
20
  query_connections_by_connector,
21
21
  query_connector_versions,
22
22
  query_dataplanes_list,
23
+ query_failed_sync_attempts_for_version,
23
24
  query_new_connector_releases,
24
25
  query_sync_results_for_version,
25
26
  query_workspace_info,
@@ -86,7 +87,6 @@ def _resolve_canonical_name_to_definition_id(canonical_name: str) -> str:
86
87
 
87
88
 
88
89
  @mcp_tool(
89
- ToolDomain.CLOUD_ADMIN,
90
90
  read_only=True,
91
91
  idempotent=True,
92
92
  )
@@ -104,7 +104,6 @@ def query_prod_dataplanes() -> list[dict[str, Any]]:
104
104
 
105
105
 
106
106
  @mcp_tool(
107
- ToolDomain.CLOUD_ADMIN,
108
107
  read_only=True,
109
108
  idempotent=True,
110
109
  )
@@ -128,7 +127,6 @@ def query_prod_workspace_info(
128
127
 
129
128
 
130
129
  @mcp_tool(
131
- ToolDomain.CLOUD_ADMIN,
132
130
  read_only=True,
133
131
  idempotent=True,
134
132
  )
@@ -151,7 +149,6 @@ def query_prod_connector_versions(
151
149
 
152
150
 
153
151
  @mcp_tool(
154
- ToolDomain.CLOUD_ADMIN,
155
152
  read_only=True,
156
153
  idempotent=True,
157
154
  )
@@ -179,7 +176,6 @@ def query_prod_new_connector_releases(
179
176
 
180
177
 
181
178
  @mcp_tool(
182
- ToolDomain.CLOUD_ADMIN,
183
179
  read_only=True,
184
180
  idempotent=True,
185
181
  )
@@ -205,7 +201,6 @@ def query_prod_actors_by_connector_version(
205
201
 
206
202
 
207
203
  @mcp_tool(
208
- ToolDomain.CLOUD_ADMIN,
209
204
  read_only=True,
210
205
  idempotent=True,
211
206
  )
@@ -252,7 +247,52 @@ def query_prod_connector_version_sync_results(
252
247
 
253
248
 
254
249
  @mcp_tool(
255
- ToolDomain.CLOUD_ADMIN,
250
+ read_only=True,
251
+ idempotent=True,
252
+ )
253
+ def query_prod_failed_sync_attempts_for_version(
254
+ connector_version_id: Annotated[
255
+ str,
256
+ Field(description="Connector version UUID to find failed sync attempts for"),
257
+ ],
258
+ days: Annotated[
259
+ int,
260
+ Field(description="Number of days to look back (default: 7)", default=7),
261
+ ] = 7,
262
+ limit: Annotated[
263
+ int,
264
+ Field(description="Maximum number of results (default: 100)", default=100),
265
+ ] = 100,
266
+ ) -> list[dict[str, Any]]:
267
+ """List failed sync attempts with failure details for actors pinned to a connector version.
268
+
269
+ Returns failed attempt records for connections using actors pinned to the specified
270
+ version. Includes failure_summary from the attempts table for debugging.
271
+
272
+ Key fields:
273
+ - latest_job_attempt_status: Final job status after all retries ('succeeded' means
274
+ the job eventually succeeded despite this failed attempt)
275
+ - failed_attempt_number: Which attempt this was (0-indexed)
276
+ - failure_summary: JSON containing failure details including failureType and messages
277
+
278
+ Note: May return multiple rows per job (one per failed attempt). Results ordered by
279
+ job_updated_at DESC, then failed_attempt_number DESC.
280
+
281
+ Returns list of dicts with keys: job_id, connection_id, latest_job_attempt_status,
282
+ job_started_at, job_updated_at, connection_name, actor_id, actor_name,
283
+ actor_definition_id, pin_origin_type, pin_origin, workspace_id, workspace_name,
284
+ organization_id, dataplane_group_id, dataplane_name, failed_attempt_id,
285
+ failed_attempt_number, failed_attempt_status, failed_attempt_created_at,
286
+ failed_attempt_ended_at, failure_summary, processing_task_queue
287
+ """
288
+ return query_failed_sync_attempts_for_version(
289
+ connector_version_id,
290
+ days=days,
291
+ limit=limit,
292
+ )
293
+
294
+
295
+ @mcp_tool(
256
296
  read_only=True,
257
297
  idempotent=True,
258
298
  open_world=True,
@@ -354,4 +394,4 @@ def query_prod_connections_by_connector(
354
394
 
355
395
  def register_prod_db_query_tools(app: FastMCP) -> None:
356
396
  """Register prod DB query tools with the FastMCP app."""
357
- register_mcp_tools(app, ToolDomain.CLOUD_ADMIN)
397
+ register_mcp_tools(app, domain=__name__)
@@ -13,13 +13,12 @@ from fastmcp import FastMCP
13
13
  from pydantic import Field
14
14
 
15
15
  from airbyte_ops_mcp.mcp._guidance import TEST_MY_TOOLS_GUIDANCE
16
- from airbyte_ops_mcp.mcp._mcp_utils import ToolDomain, mcp_prompt, register_mcp_prompts
16
+ from airbyte_ops_mcp.mcp._mcp_utils import mcp_prompt, register_mcp_prompts
17
17
 
18
18
 
19
19
  @mcp_prompt(
20
20
  name="test-my-tools",
21
21
  description="Test all available MCP tools to confirm they are working properly",
22
- domain=ToolDomain.PROMPTS,
23
22
  )
24
23
  def test_my_tools_prompt(
25
24
  scope: Annotated[
@@ -57,4 +56,4 @@ def register_prompts(app: FastMCP) -> None:
57
56
  Args:
58
57
  app: FastMCP application instance
59
58
  """
60
- register_mcp_prompts(app, ToolDomain.PROMPTS)
59
+ register_mcp_prompts(app, domain=__name__)
@@ -12,7 +12,6 @@ from fastmcp import FastMCP
12
12
 
13
13
  from airbyte_ops_mcp.constants import MCP_SERVER_NAME
14
14
  from airbyte_ops_mcp.mcp._mcp_utils import (
15
- ToolDomain,
16
15
  mcp_resource,
17
16
  register_mcp_resources,
18
17
  )
@@ -64,7 +63,6 @@ def _get_version_info() -> dict[str, str | list[str] | None]:
64
63
  uri=f"{MCP_SERVER_NAME}://server/info",
65
64
  description="Server information for the Airbyte Admin MCP server",
66
65
  mime_type="application/json",
67
- domain=ToolDomain.SERVER_INFO,
68
66
  )
69
67
  def mcp_server_info() -> dict[str, str | list[str] | None]:
70
68
  """Resource that returns information for the MCP server.
@@ -83,4 +81,4 @@ def register_server_info_resources(app: FastMCP) -> None:
83
81
  Args:
84
82
  app: FastMCP application instance
85
83
  """
86
- register_mcp_resources(app, domain=ToolDomain.SERVER_INFO)
84
+ register_mcp_resources(app, domain=__name__)
@@ -16,12 +16,15 @@ from typing import Any
16
16
  import sqlalchemy
17
17
  from google.cloud import secretmanager
18
18
 
19
+ from airbyte_ops_mcp.gcp_auth import get_secret_manager_client
19
20
  from airbyte_ops_mcp.prod_db_access.db_engine import get_pool
20
21
  from airbyte_ops_mcp.prod_db_access.sql import (
21
22
  SELECT_ACTORS_PINNED_TO_VERSION,
22
23
  SELECT_CONNECTIONS_BY_CONNECTOR,
24
+ SELECT_CONNECTIONS_BY_CONNECTOR_AND_ORG,
23
25
  SELECT_CONNECTOR_VERSIONS,
24
26
  SELECT_DATAPLANES_LIST,
27
+ SELECT_FAILED_SYNC_ATTEMPTS_FOR_VERSION,
25
28
  SELECT_NEW_CONNECTOR_RELEASES,
26
29
  SELECT_ORG_WORKSPACES,
27
30
  SELECT_SUCCESSFUL_SYNCS_FOR_VERSION,
@@ -52,7 +55,7 @@ def _run_sql_query(
52
55
  List of row dicts from the query result
53
56
  """
54
57
  if gsm_client is None:
55
- gsm_client = secretmanager.SecretManagerServiceClient()
58
+ gsm_client = get_secret_manager_client()
56
59
  pool = get_pool(gsm_client)
57
60
  start = perf_counter()
58
61
  with pool.connect() as conn:
@@ -84,14 +87,28 @@ def query_connections_by_connector(
84
87
  Returns:
85
88
  List of connection records with workspace and dataplane info
86
89
  """
90
+ # Use separate queries to avoid pg8000 NULL parameter type issues
91
+ # pg8000 cannot determine the type of NULL parameters in patterns like
92
+ # "(:param IS NULL OR column = :param)"
93
+ if organization_id is None:
94
+ return _run_sql_query(
95
+ SELECT_CONNECTIONS_BY_CONNECTOR,
96
+ parameters={
97
+ "connector_definition_id": connector_definition_id,
98
+ "limit": limit,
99
+ },
100
+ query_name="SELECT_CONNECTIONS_BY_CONNECTOR",
101
+ gsm_client=gsm_client,
102
+ )
103
+
87
104
  return _run_sql_query(
88
- SELECT_CONNECTIONS_BY_CONNECTOR,
105
+ SELECT_CONNECTIONS_BY_CONNECTOR_AND_ORG,
89
106
  parameters={
90
107
  "connector_definition_id": connector_definition_id,
91
108
  "organization_id": organization_id,
92
109
  "limit": limit,
93
110
  },
94
- query_name="SELECT_CONNECTIONS_BY_CONNECTOR",
111
+ query_name="SELECT_CONNECTIONS_BY_CONNECTOR_AND_ORG",
95
112
  gsm_client=gsm_client,
96
113
  )
97
114
 
@@ -208,6 +225,44 @@ def query_sync_results_for_version(
208
225
  )
209
226
 
210
227
 
228
+ def query_failed_sync_attempts_for_version(
229
+ connector_version_id: str,
230
+ days: int = 7,
231
+ limit: int = 100,
232
+ *,
233
+ gsm_client: secretmanager.SecretManagerServiceClient | None = None,
234
+ ) -> list[dict[str, Any]]:
235
+ """Query failed sync job results with attempt details for actors pinned to a version.
236
+
237
+ This query joins to the attempts table to include failure_summary and other
238
+ attempt-level details useful for debugging. Date filters are applied to both
239
+ jobs and attempts tables to optimize join performance.
240
+
241
+ Note: This may return multiple rows per job (one per attempt). Results are
242
+ ordered by job_updated_at DESC, then attempt_number DESC.
243
+
244
+ Args:
245
+ connector_version_id: Connector version UUID to filter by
246
+ days: Number of days to look back (default: 7)
247
+ limit: Maximum number of results (default: 100)
248
+ gsm_client: GCP Secret Manager client. If None, a new client will be instantiated.
249
+
250
+ Returns:
251
+ List of failed sync job results with attempt details including failure_summary
252
+ """
253
+ cutoff_date = datetime.now(timezone.utc) - timedelta(days=days)
254
+ return _run_sql_query(
255
+ SELECT_FAILED_SYNC_ATTEMPTS_FOR_VERSION,
256
+ parameters={
257
+ "actor_definition_version_id": connector_version_id,
258
+ "cutoff_date": cutoff_date,
259
+ "limit": limit,
260
+ },
261
+ query_name="SELECT_FAILED_SYNC_ATTEMPTS_FOR_VERSION",
262
+ gsm_client=gsm_client,
263
+ )
264
+
265
+
211
266
  def query_dataplanes_list(
212
267
  *,
213
268
  gsm_client: secretmanager.SecretManagerServiceClient | None = None,
@@ -74,7 +74,9 @@ import sqlalchemy
74
74
  # Connection Queries
75
75
  # =============================================================================
76
76
 
77
- # Query connections by connector type, optionally filtered by organization
77
+ # Query connections by connector type (no organization filter)
78
+ # Note: pg8000 cannot determine the type of NULL parameters in patterns like
79
+ # "(:param IS NULL OR column = :param)", so we use separate queries instead
78
80
  SELECT_CONNECTIONS_BY_CONNECTOR = sqlalchemy.text(
79
81
  """
80
82
  SELECT
@@ -97,7 +99,34 @@ SELECT_CONNECTIONS_BY_CONNECTOR = sqlalchemy.text(
97
99
  ON workspace.dataplane_group_id = dataplane_group.id
98
100
  WHERE
99
101
  source_actor.actor_definition_id = :connector_definition_id
100
- AND (:organization_id IS NULL OR workspace.organization_id = :organization_id)
102
+ LIMIT :limit
103
+ """
104
+ )
105
+
106
+ # Query connections by connector type, filtered by organization
107
+ SELECT_CONNECTIONS_BY_CONNECTOR_AND_ORG = sqlalchemy.text(
108
+ """
109
+ SELECT
110
+ connection.id AS connection_id,
111
+ connection.name AS connection_name,
112
+ connection.source_id,
113
+ workspace.id AS workspace_id,
114
+ workspace.name AS workspace_name,
115
+ workspace.organization_id,
116
+ workspace.dataplane_group_id,
117
+ dataplane_group.name AS dataplane_name,
118
+ source_actor.actor_definition_id AS source_definition_id,
119
+ source_actor.name AS source_name
120
+ FROM connection
121
+ JOIN actor AS source_actor
122
+ ON connection.source_id = source_actor.id
123
+ JOIN workspace
124
+ ON source_actor.workspace_id = workspace.id
125
+ LEFT JOIN dataplane_group
126
+ ON workspace.dataplane_group_id = dataplane_group.id
127
+ WHERE
128
+ source_actor.actor_definition_id = :connector_definition_id
129
+ AND workspace.organization_id = :organization_id
101
130
  LIMIT :limit
102
131
  """
103
132
  )
@@ -276,6 +305,66 @@ SELECT_SUCCESSFUL_SYNCS_FOR_VERSION = sqlalchemy.text(
276
305
  """
277
306
  )
278
307
 
308
+ # Get failed attempt results for actors pinned to a specific connector definition VERSION ID
309
+ # Includes attempt details (failure_summary, etc.) for research/debugging
310
+ # Filters on attempts.status = 'failed' to capture all failed attempts, including those
311
+ # from jobs that eventually succeeded via retry. The latest_job_attempt_status field
312
+ # indicates whether the job eventually succeeded or remained failed.
313
+ # Query starts from attempts table to leverage indexed columns (ended_at, status)
314
+ # Note: attempts.ended_at and attempts.status are indexed (btree)
315
+ SELECT_FAILED_SYNC_ATTEMPTS_FOR_VERSION = sqlalchemy.text(
316
+ """
317
+ SELECT
318
+ jobs.id AS job_id,
319
+ jobs.scope AS connection_id,
320
+ jobs.status AS latest_job_attempt_status,
321
+ jobs.started_at AS job_started_at,
322
+ jobs.updated_at AS job_updated_at,
323
+ connection.name AS connection_name,
324
+ actor.id AS actor_id,
325
+ actor.name AS actor_name,
326
+ actor.actor_definition_id,
327
+ scoped_configuration.origin_type AS pin_origin_type,
328
+ scoped_configuration.origin AS pin_origin,
329
+ workspace.id AS workspace_id,
330
+ workspace.name AS workspace_name,
331
+ workspace.organization_id,
332
+ workspace.dataplane_group_id,
333
+ dataplane_group.name AS dataplane_name,
334
+ attempts.id AS failed_attempt_id,
335
+ attempts.attempt_number AS failed_attempt_number,
336
+ attempts.status AS failed_attempt_status,
337
+ attempts.created_at AS failed_attempt_created_at,
338
+ attempts.ended_at AS failed_attempt_ended_at,
339
+ attempts.failure_summary,
340
+ attempts.processing_task_queue
341
+ FROM attempts
342
+ JOIN jobs
343
+ ON jobs.id = attempts.job_id
344
+ AND jobs.config_type = 'sync'
345
+ AND jobs.updated_at >= :cutoff_date
346
+ JOIN connection
347
+ ON jobs.scope = connection.id::text
348
+ JOIN actor
349
+ ON connection.source_id = actor.id
350
+ JOIN scoped_configuration
351
+ ON scoped_configuration.scope_id = actor.id
352
+ AND scoped_configuration.key = 'connector_version'
353
+ AND scoped_configuration.scope_type = 'actor'
354
+ AND scoped_configuration.value = :actor_definition_version_id
355
+ JOIN workspace
356
+ ON actor.workspace_id = workspace.id
357
+ LEFT JOIN dataplane_group
358
+ ON workspace.dataplane_group_id = dataplane_group.id
359
+ WHERE
360
+ attempts.ended_at >= :cutoff_date
361
+ AND attempts.status = 'failed'
362
+ ORDER BY
363
+ attempts.ended_at DESC
364
+ LIMIT :limit
365
+ """
366
+ )
367
+
279
368
  # =============================================================================
280
369
  # Dataplane and Workspace Queries
281
370
  # =============================================================================