airbyte-internal-ops 0.1.7__py3-none-any.whl → 0.1.8__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.8
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
@@ -361,13 +362,13 @@ airbyte_ops_mcp/cloud_admin/connection_config.py,sha256=UtbIwuB7CA3WJr9oYRwlKDsj
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=keB5_LUs6OULXr4Ukg-gzJfeDiC_QvBXSh56yTD9kSQ,7625
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=RTEq9uguaREeZ9BqC2In3abdGuqg2IUZZ43KNoYj66Y,9444
397
+ airbyte_ops_mcp/mcp/prod_db_queries.py,sha256=RV909zZX156WKXzwtu68Sd071tmaiJaCJz9wNHg3j0I,12399
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/queries.py,sha256=jUGJErgDmaKf7mswgVEGin8bjsUFAxegSAOB47XzT9k,8724
405
406
  airbyte_ops_mcp/prod_db_access/sql.py,sha256=zHPucNuMlfxz3aU8vYo1ziiGk0lIncG9XmblEoRDd4c,12725
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.8.dist-info/METADATA,sha256=BSNP1wsy5PbIFtyq6RbvCGPHgY3C8vC5UNsAzjqZAOk,5282
411
+ airbyte_internal_ops-0.1.8.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
412
+ airbyte_internal_ops-0.1.8.dist-info/entry_points.txt,sha256=eUgJ9xIy9PlR-CgRbqRMsh1NVp6jz08v9bul9vCYlU4,111
413
+ airbyte_internal_ops-0.1.8.dist-info/RECORD,,
@@ -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,
@@ -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,
@@ -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"
@@ -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,
@@ -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,7 +14,7 @@ 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,
@@ -86,7 +86,6 @@ def _resolve_canonical_name_to_definition_id(canonical_name: str) -> str:
86
86
 
87
87
 
88
88
  @mcp_tool(
89
- ToolDomain.CLOUD_ADMIN,
90
89
  read_only=True,
91
90
  idempotent=True,
92
91
  )
@@ -104,7 +103,6 @@ def query_prod_dataplanes() -> list[dict[str, Any]]:
104
103
 
105
104
 
106
105
  @mcp_tool(
107
- ToolDomain.CLOUD_ADMIN,
108
106
  read_only=True,
109
107
  idempotent=True,
110
108
  )
@@ -128,7 +126,6 @@ def query_prod_workspace_info(
128
126
 
129
127
 
130
128
  @mcp_tool(
131
- ToolDomain.CLOUD_ADMIN,
132
129
  read_only=True,
133
130
  idempotent=True,
134
131
  )
@@ -151,7 +148,6 @@ def query_prod_connector_versions(
151
148
 
152
149
 
153
150
  @mcp_tool(
154
- ToolDomain.CLOUD_ADMIN,
155
151
  read_only=True,
156
152
  idempotent=True,
157
153
  )
@@ -179,7 +175,6 @@ def query_prod_new_connector_releases(
179
175
 
180
176
 
181
177
  @mcp_tool(
182
- ToolDomain.CLOUD_ADMIN,
183
178
  read_only=True,
184
179
  idempotent=True,
185
180
  )
@@ -205,7 +200,6 @@ def query_prod_actors_by_connector_version(
205
200
 
206
201
 
207
202
  @mcp_tool(
208
- ToolDomain.CLOUD_ADMIN,
209
203
  read_only=True,
210
204
  idempotent=True,
211
205
  )
@@ -252,7 +246,6 @@ def query_prod_connector_version_sync_results(
252
246
 
253
247
 
254
248
  @mcp_tool(
255
- ToolDomain.CLOUD_ADMIN,
256
249
  read_only=True,
257
250
  idempotent=True,
258
251
  open_world=True,
@@ -354,4 +347,4 @@ def query_prod_connections_by_connector(
354
347
 
355
348
  def register_prod_db_query_tools(app: FastMCP) -> None:
356
349
  """Register prod DB query tools with the FastMCP app."""
357
- register_mcp_tools(app, ToolDomain.CLOUD_ADMIN)
350
+ 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,6 +16,7 @@ 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,
@@ -52,7 +53,7 @@ def _run_sql_query(
52
53
  List of row dicts from the query result
53
54
  """
54
55
  if gsm_client is None:
55
- gsm_client = secretmanager.SecretManagerServiceClient()
56
+ gsm_client = get_secret_manager_client()
56
57
  pool = get_pool(gsm_client)
57
58
  start = perf_counter()
58
59
  with pool.connect() as conn: