airbyte-internal-ops 0.1.11__py3-none-any.whl → 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. {airbyte_internal_ops-0.1.11.dist-info → airbyte_internal_ops-0.2.1.dist-info}/METADATA +2 -2
  2. {airbyte_internal_ops-0.1.11.dist-info → airbyte_internal_ops-0.2.1.dist-info}/RECORD +41 -40
  3. {airbyte_internal_ops-0.1.11.dist-info → airbyte_internal_ops-0.2.1.dist-info}/entry_points.txt +1 -0
  4. airbyte_ops_mcp/__init__.py +2 -2
  5. airbyte_ops_mcp/cli/cloud.py +264 -301
  6. airbyte_ops_mcp/cloud_admin/api_client.py +51 -26
  7. airbyte_ops_mcp/cloud_admin/auth.py +32 -0
  8. airbyte_ops_mcp/cloud_admin/connection_config.py +2 -2
  9. airbyte_ops_mcp/constants.py +18 -0
  10. airbyte_ops_mcp/github_actions.py +94 -5
  11. airbyte_ops_mcp/mcp/_http_headers.py +254 -0
  12. airbyte_ops_mcp/mcp/_mcp_utils.py +2 -2
  13. airbyte_ops_mcp/mcp/cloud_connector_versions.py +162 -52
  14. airbyte_ops_mcp/mcp/github.py +34 -1
  15. airbyte_ops_mcp/mcp/prod_db_queries.py +67 -24
  16. airbyte_ops_mcp/mcp/{live_tests.py → regression_tests.py} +165 -152
  17. airbyte_ops_mcp/mcp/server.py +84 -11
  18. airbyte_ops_mcp/prod_db_access/db_engine.py +15 -11
  19. airbyte_ops_mcp/prod_db_access/queries.py +27 -15
  20. airbyte_ops_mcp/prod_db_access/sql.py +17 -16
  21. airbyte_ops_mcp/{live_tests → regression_tests}/__init__.py +3 -3
  22. airbyte_ops_mcp/{live_tests → regression_tests}/cdk_secrets.py +1 -1
  23. airbyte_ops_mcp/{live_tests → regression_tests}/connection_secret_retriever.py +3 -3
  24. airbyte_ops_mcp/{live_tests → regression_tests}/connector_runner.py +1 -1
  25. airbyte_ops_mcp/{live_tests → regression_tests}/message_cache/__init__.py +3 -1
  26. airbyte_ops_mcp/{live_tests → regression_tests}/regression/__init__.py +1 -1
  27. airbyte_ops_mcp/{live_tests → regression_tests}/schema_generation.py +3 -1
  28. airbyte_ops_mcp/{live_tests → regression_tests}/validation/__init__.py +2 -2
  29. airbyte_ops_mcp/{live_tests → regression_tests}/validation/record_validators.py +4 -2
  30. {airbyte_internal_ops-0.1.11.dist-info → airbyte_internal_ops-0.2.1.dist-info}/WHEEL +0 -0
  31. /airbyte_ops_mcp/{live_tests → regression_tests}/ci_output.py +0 -0
  32. /airbyte_ops_mcp/{live_tests → regression_tests}/commons/__init__.py +0 -0
  33. /airbyte_ops_mcp/{live_tests → regression_tests}/config.py +0 -0
  34. /airbyte_ops_mcp/{live_tests → regression_tests}/connection_fetcher.py +0 -0
  35. /airbyte_ops_mcp/{live_tests → regression_tests}/evaluation_modes.py +0 -0
  36. /airbyte_ops_mcp/{live_tests → regression_tests}/http_metrics.py +0 -0
  37. /airbyte_ops_mcp/{live_tests → regression_tests}/message_cache/duckdb_cache.py +0 -0
  38. /airbyte_ops_mcp/{live_tests → regression_tests}/models.py +0 -0
  39. /airbyte_ops_mcp/{live_tests → regression_tests}/obfuscation.py +0 -0
  40. /airbyte_ops_mcp/{live_tests → regression_tests}/regression/comparators.py +0 -0
  41. /airbyte_ops_mcp/{live_tests → regression_tests}/validation/catalog_validators.py +0 -0
@@ -48,21 +48,36 @@ class _OriginType(str, Enum):
48
48
 
49
49
 
50
50
  def _get_access_token(
51
- client_id: str,
52
- client_secret: str,
51
+ client_id: str | None = None,
52
+ client_secret: str | None = None,
53
+ bearer_token: str | None = None,
53
54
  ) -> str:
54
55
  """Get an access token for Airbyte Cloud API.
55
56
 
57
+ If a bearer_token is provided, it is returned directly (already an access token).
58
+ Otherwise, exchanges client_id/client_secret for an access token.
59
+
56
60
  Args:
57
- client_id: The Airbyte Cloud client ID
58
- client_secret: The Airbyte Cloud client secret
61
+ client_id: The Airbyte Cloud client ID (required if no bearer_token)
62
+ client_secret: The Airbyte Cloud client secret (required if no bearer_token)
63
+ bearer_token: Pre-existing bearer token (takes precedence over client credentials)
59
64
 
60
65
  Returns:
61
66
  Access token string
62
67
 
63
68
  Raises:
64
- PyAirbyteInputError: If authentication fails
69
+ PyAirbyteInputError: If authentication fails or no valid credentials provided
65
70
  """
71
+ # If bearer token provided, use it directly
72
+ if bearer_token:
73
+ return bearer_token
74
+
75
+ # Otherwise, exchange client credentials for access token
76
+ if not client_id or not client_secret:
77
+ raise PyAirbyteInputError(
78
+ message="Either bearer_token or both client_id and client_secret must be provided",
79
+ )
80
+
66
81
  # Always authenticate via the public API endpoint
67
82
  auth_url = f"{constants.CLOUD_API_ROOT}/applications/token"
68
83
  response = requests.post(
@@ -90,16 +105,18 @@ def _get_access_token(
90
105
  def get_user_id_by_email(
91
106
  email: str,
92
107
  api_root: str,
93
- client_id: str,
94
- client_secret: str,
108
+ client_id: str | None = None,
109
+ client_secret: str | None = None,
110
+ bearer_token: str | None = None,
95
111
  ) -> str:
96
112
  """Get user ID from email address.
97
113
 
98
114
  Args:
99
115
  email: The user's email address
100
116
  api_root: The API root URL
101
- client_id: The Airbyte Cloud client ID
102
- client_secret: The Airbyte Cloud client secret
117
+ client_id: The Airbyte Cloud client ID (required if no bearer_token)
118
+ client_secret: The Airbyte Cloud client secret (required if no bearer_token)
119
+ bearer_token: Pre-existing bearer token (takes precedence over client credentials)
103
120
 
104
121
  Returns:
105
122
  User ID (UUID string)
@@ -107,7 +124,7 @@ def get_user_id_by_email(
107
124
  Raises:
108
125
  PyAirbyteInputError: If user not found or API request fails
109
126
  """
110
- access_token = _get_access_token(client_id, client_secret)
127
+ access_token = _get_access_token(client_id, client_secret, bearer_token)
111
128
 
112
129
  endpoint = f"{api_root}/users/list_instance_admin"
113
130
  response = requests.post(
@@ -152,8 +169,9 @@ def resolve_connector_version_id(
152
169
  connector_type: Literal["source", "destination"],
153
170
  version: str,
154
171
  api_root: str,
155
- client_id: str,
156
- client_secret: str,
172
+ client_id: str | None = None,
173
+ client_secret: str | None = None,
174
+ bearer_token: str | None = None,
157
175
  ) -> str:
158
176
  """Resolve a version string to an actor_definition_version_id.
159
177
 
@@ -162,8 +180,9 @@ def resolve_connector_version_id(
162
180
  connector_type: Either "source" or "destination"
163
181
  version: The version string (e.g., "0.1.47-preview.abe7cb4")
164
182
  api_root: The API root URL
165
- client_id: The Airbyte Cloud client ID
166
- client_secret: The Airbyte Cloud client secret
183
+ client_id: The Airbyte Cloud client ID (required if no bearer_token)
184
+ client_secret: The Airbyte Cloud client secret (required if no bearer_token)
185
+ bearer_token: Pre-existing bearer token (takes precedence over client credentials)
167
186
 
168
187
  Returns:
169
188
  Version ID (UUID string)
@@ -171,7 +190,7 @@ def resolve_connector_version_id(
171
190
  Raises:
172
191
  PyAirbyteInputError: If version cannot be resolved or API request fails
173
192
  """
174
- access_token = _get_access_token(client_id, client_secret)
193
+ access_token = _get_access_token(client_id, client_secret, bearer_token)
175
194
 
176
195
  endpoint = f"{api_root}/actor_definition_versions/resolve"
177
196
  payload = {
@@ -223,8 +242,9 @@ def get_connector_version(
223
242
  connector_id: str,
224
243
  connector_type: Literal["source", "destination"],
225
244
  api_root: str,
226
- client_id: str,
227
- client_secret: str,
245
+ client_id: str | None = None,
246
+ client_secret: str | None = None,
247
+ bearer_token: str | None = None,
228
248
  ) -> dict[str, Any]:
229
249
  """Get version information for a deployed connector.
230
250
 
@@ -232,8 +252,9 @@ def get_connector_version(
232
252
  connector_id: The ID of the deployed connector (source or destination)
233
253
  connector_type: Either "source" or "destination"
234
254
  api_root: The API root URL
235
- client_id: The Airbyte Cloud client ID
236
- client_secret: The Airbyte Cloud client secret
255
+ client_id: The Airbyte Cloud client ID (required if no bearer_token)
256
+ client_secret: The Airbyte Cloud client secret (required if no bearer_token)
257
+ bearer_token: Pre-existing bearer token (takes precedence over client credentials)
237
258
 
238
259
  Returns:
239
260
  Dictionary containing:
@@ -243,7 +264,7 @@ def get_connector_version(
243
264
  Raises:
244
265
  PyAirbyteInputError: If the API request fails
245
266
  """
246
- access_token = _get_access_token(client_id, client_secret)
267
+ access_token = _get_access_token(client_id, client_secret, bearer_token)
247
268
 
248
269
  # Determine endpoint based on connector type
249
270
  # api_root already includes /v1
@@ -294,14 +315,15 @@ def set_connector_version_override(
294
315
  connector_id: str,
295
316
  connector_type: Literal["source", "destination"],
296
317
  api_root: str,
297
- client_id: str,
298
- client_secret: str,
299
- workspace_id: str,
318
+ client_id: str | None = None,
319
+ client_secret: str | None = None,
320
+ workspace_id: str | None = None,
300
321
  version: str | None = None,
301
322
  unset: bool = False,
302
323
  override_reason: str | None = None,
303
324
  override_reason_reference_url: str | None = None,
304
325
  user_email: str | None = None,
326
+ bearer_token: str | None = None,
305
327
  ) -> bool:
306
328
  """Set or clear a version override for a deployed connector.
307
329
 
@@ -309,14 +331,15 @@ def set_connector_version_override(
309
331
  connector_id: The ID of the deployed connector
310
332
  connector_type: Either "source" or "destination"
311
333
  api_root: The API root URL
312
- client_id: The Airbyte Cloud client ID
313
- client_secret: The Airbyte Cloud client secret
334
+ client_id: The Airbyte Cloud client ID (required if no bearer_token)
335
+ client_secret: The Airbyte Cloud client secret (required if no bearer_token)
314
336
  workspace_id: The workspace ID
315
337
  version: The version to pin to (e.g., "0.1.0"), or None to unset
316
338
  unset: If True, removes any existing override
317
339
  override_reason: Required when setting. Explanation for the override
318
340
  override_reason_reference_url: Optional URL with more context
319
341
  user_email: Email of user creating the override
342
+ bearer_token: Pre-existing bearer token (takes precedence over client credentials)
320
343
 
321
344
  Returns:
322
345
  True if operation succeeded, False if no override existed (unset only)
@@ -335,7 +358,7 @@ def set_connector_version_override(
335
358
  message="override_reason is required when setting a version and must be at least 10 characters",
336
359
  )
337
360
 
338
- access_token = _get_access_token(client_id, client_secret)
361
+ access_token = _get_access_token(client_id, client_secret, bearer_token)
339
362
 
340
363
  # Build the scoped configuration
341
364
  scope_type = _ScopeType.ACTOR
@@ -479,6 +502,7 @@ def set_connector_version_override(
479
502
  api_root=api_root,
480
503
  client_id=client_id,
481
504
  client_secret=client_secret,
505
+ bearer_token=bearer_token,
482
506
  )
483
507
 
484
508
  # Get user ID from email if provided
@@ -489,6 +513,7 @@ def set_connector_version_override(
489
513
  api_root=api_root,
490
514
  client_id=client_id,
491
515
  client_secret=client_secret,
516
+ bearer_token=bearer_token,
492
517
  )
493
518
 
494
519
  # Create the override with correct schema
@@ -41,6 +41,20 @@ def check_internal_admin_flag() -> bool:
41
41
  return bool(admin_user and EXPECTED_ADMIN_EMAIL_DOMAIN in admin_user)
42
42
 
43
43
 
44
+ def check_internal_admin_flag_only() -> bool:
45
+ """Check if internal admin flag is set (without requiring user email env var).
46
+
47
+ This is a lighter check that only validates AIRBYTE_INTERNAL_ADMIN_FLAG,
48
+ allowing the admin user email to be provided as a parameter instead of
49
+ an environment variable.
50
+
51
+ Returns:
52
+ True if AIRBYTE_INTERNAL_ADMIN_FLAG is set correctly, False otherwise
53
+ """
54
+ admin_flag = os.environ.get(ENV_AIRBYTE_INTERNAL_ADMIN_FLAG)
55
+ return admin_flag == EXPECTED_ADMIN_FLAG_VALUE
56
+
57
+
44
58
  def require_internal_admin() -> None:
45
59
  """Require internal admin access for the current operation.
46
60
 
@@ -59,6 +73,24 @@ def require_internal_admin() -> None:
59
73
  )
60
74
 
61
75
 
76
+ def require_internal_admin_flag_only() -> None:
77
+ """Require internal admin flag for the current operation.
78
+
79
+ This is a lighter check that only validates AIRBYTE_INTERNAL_ADMIN_FLAG,
80
+ allowing the admin user email to be provided as a parameter instead of
81
+ an environment variable.
82
+
83
+ Raises:
84
+ CloudAuthError: If AIRBYTE_INTERNAL_ADMIN_FLAG is not properly configured
85
+ """
86
+ if not check_internal_admin_flag_only():
87
+ raise CloudAuthError(
88
+ "This operation requires internal admin access. "
89
+ f"Set {ENV_AIRBYTE_INTERNAL_ADMIN_FLAG}={EXPECTED_ADMIN_FLAG_VALUE} "
90
+ "environment variable."
91
+ )
92
+
93
+
62
94
  def get_admin_user_email() -> str:
63
95
  """Get the admin user email from environment.
64
96
 
@@ -13,8 +13,8 @@ from pathlib import Path
13
13
 
14
14
  from pydantic import BaseModel, Field
15
15
 
16
- from airbyte_ops_mcp.live_tests.connection_fetcher import fetch_connection_data
17
- from airbyte_ops_mcp.live_tests.connection_secret_retriever import (
16
+ from airbyte_ops_mcp.regression_tests.connection_fetcher import fetch_connection_data
17
+ from airbyte_ops_mcp.regression_tests.connection_secret_retriever import (
18
18
  retrieve_unmasked_config,
19
19
  )
20
20
 
@@ -20,6 +20,24 @@ ENV_GCP_PROD_DB_ACCESS_CREDENTIALS = "GCP_PROD_DB_ACCESS_CREDENTIALS"
20
20
  EXPECTED_ADMIN_FLAG_VALUE = "airbyte.io"
21
21
  EXPECTED_ADMIN_EMAIL_DOMAIN = "@airbyte.io"
22
22
 
23
+ # =============================================================================
24
+ # HTTP Header Names for Airbyte Cloud Authentication
25
+ # =============================================================================
26
+ # These headers follow the PyAirbyte convention for passing credentials
27
+ # via HTTP when running as an MCP HTTP server.
28
+
29
+ HEADER_AIRBYTE_CLOUD_CLIENT_ID = "X-Airbyte-Cloud-Client-Id"
30
+ """HTTP header for OAuth client ID."""
31
+
32
+ HEADER_AIRBYTE_CLOUD_CLIENT_SECRET = "X-Airbyte-Cloud-Client-Secret"
33
+ """HTTP header for OAuth client secret."""
34
+
35
+ HEADER_AIRBYTE_CLOUD_WORKSPACE_ID = "X-Airbyte-Cloud-Workspace-Id"
36
+ """HTTP header for default workspace ID."""
37
+
38
+ HEADER_AIRBYTE_CLOUD_API_URL = "X-Airbyte-Cloud-Api-Url"
39
+ """HTTP header for API root URL override."""
40
+
23
41
  # =============================================================================
24
42
  # GCP and Prod DB Constants (from connection-retriever)
25
43
  # =============================================================================
@@ -9,6 +9,8 @@ are used by MCP tools but are not MCP-specific.
9
9
  from __future__ import annotations
10
10
 
11
11
  import os
12
+ import shutil
13
+ import subprocess
12
14
  import time
13
15
  from dataclasses import dataclass
14
16
  from datetime import datetime, timedelta
@@ -19,10 +21,11 @@ GITHUB_API_BASE = "https://api.github.com"
19
21
 
20
22
 
21
23
  def resolve_github_token(preferred_env_vars: list[str] | None = None) -> str:
22
- """Resolve GitHub token from environment variables.
24
+ """Resolve GitHub token from environment variables or gh CLI.
23
25
 
24
26
  Checks environment variables in order of preference, returning the first
25
- non-empty value found.
27
+ non-empty value found. If no environment variables are set, attempts to
28
+ get a token from the gh CLI tool using 'gh auth token'.
26
29
 
27
30
  Args:
28
31
  preferred_env_vars: List of environment variable names to check in order.
@@ -32,19 +35,37 @@ def resolve_github_token(preferred_env_vars: list[str] | None = None) -> str:
32
35
  GitHub token string.
33
36
 
34
37
  Raises:
35
- ValueError: If no GitHub token is found in any of the specified env vars.
38
+ ValueError: If no GitHub token is found in env vars or gh CLI.
36
39
  """
37
40
  if preferred_env_vars is None:
38
41
  preferred_env_vars = ["GITHUB_CI_WORKFLOW_TRIGGER_PAT", "GITHUB_TOKEN"]
39
42
 
43
+ # Check environment variables first
40
44
  for env_var in preferred_env_vars:
41
45
  token = os.getenv(env_var)
42
46
  if token:
43
47
  return token
44
48
 
49
+ # Fall back to gh CLI if available
50
+ gh_path = shutil.which("gh")
51
+ if gh_path:
52
+ try:
53
+ result = subprocess.run(
54
+ [gh_path, "auth", "token"],
55
+ capture_output=True,
56
+ text=True,
57
+ timeout=5,
58
+ check=False,
59
+ )
60
+ if result.returncode == 0 and result.stdout.strip():
61
+ return result.stdout.strip()
62
+ except (subprocess.TimeoutExpired, subprocess.SubprocessError):
63
+ pass
64
+
45
65
  env_var_list = ", ".join(preferred_env_vars)
46
66
  raise ValueError(
47
- f"No GitHub token found. Set one of: {env_var_list} environment variable."
67
+ f"No GitHub token found. Set one of: {env_var_list} environment variable, "
68
+ "or authenticate with 'gh auth login'."
48
69
  )
49
70
 
50
71
 
@@ -62,6 +83,74 @@ class WorkflowDispatchResult:
62
83
  """Direct URL to the workflow run, if discovered"""
63
84
 
64
85
 
86
+ @dataclass
87
+ class WorkflowJobInfo:
88
+ """Information about a single job in a workflow run."""
89
+
90
+ job_id: int
91
+ """GitHub job ID (use with git_ci_job_logs to retrieve logs)"""
92
+
93
+ name: str
94
+ """Job name as defined in the workflow"""
95
+
96
+ status: str
97
+ """Job status: queued, in_progress, completed"""
98
+
99
+ conclusion: str | None = None
100
+ """Job conclusion: success, failure, cancelled, skipped (only set when completed)"""
101
+
102
+ started_at: str | None = None
103
+ """ISO 8601 timestamp when the job started"""
104
+
105
+ completed_at: str | None = None
106
+ """ISO 8601 timestamp when the job completed"""
107
+
108
+
109
+ def get_workflow_jobs(
110
+ owner: str,
111
+ repo: str,
112
+ run_id: int,
113
+ ) -> list[WorkflowJobInfo]:
114
+ """Get jobs for a workflow run from GitHub API.
115
+
116
+ Args:
117
+ owner: Repository owner (e.g., "airbytehq")
118
+ repo: Repository name (e.g., "airbyte")
119
+ run_id: Workflow run ID
120
+
121
+ Returns:
122
+ List of WorkflowJobInfo objects for each job in the workflow run.
123
+
124
+ Raises:
125
+ ValueError: If no GitHub token is found.
126
+ requests.HTTPError: If API request fails.
127
+ """
128
+ token = resolve_github_token()
129
+
130
+ url = f"{GITHUB_API_BASE}/repos/{owner}/{repo}/actions/runs/{run_id}/jobs"
131
+ headers = {
132
+ "Authorization": f"Bearer {token}",
133
+ "Accept": "application/vnd.github+json",
134
+ "X-GitHub-Api-Version": "2022-11-28",
135
+ }
136
+
137
+ response = requests.get(url, headers=headers, timeout=30)
138
+ response.raise_for_status()
139
+
140
+ jobs_data = response.json()
141
+ return [
142
+ WorkflowJobInfo(
143
+ job_id=job["id"],
144
+ name=job["name"],
145
+ status=job["status"],
146
+ conclusion=job.get("conclusion"),
147
+ started_at=job.get("started_at"),
148
+ completed_at=job.get("completed_at"),
149
+ )
150
+ for job in jobs_data.get("jobs", [])
151
+ ]
152
+
153
+
65
154
  def find_workflow_run(
66
155
  owner: str,
67
156
  repo: str,
@@ -146,7 +235,7 @@ def trigger_workflow_dispatch(
146
235
  Args:
147
236
  owner: Repository owner (e.g., "airbytehq")
148
237
  repo: Repository name (e.g., "airbyte-ops-mcp")
149
- workflow_file: Workflow file name (e.g., "connector-live-test.yml")
238
+ workflow_file: Workflow file name (e.g., "connector-regression-test.yml")
150
239
  ref: Git ref to run the workflow on (branch name)
151
240
  inputs: Workflow inputs dictionary
152
241
  token: GitHub API token
@@ -0,0 +1,254 @@
1
+ # Copyright (c) 2025 Airbyte, Inc., all rights reserved.
2
+ """HTTP header extraction for Airbyte Cloud credentials.
3
+
4
+ This module provides internal helper functions for extracting Airbyte Cloud
5
+ authentication credentials from HTTP headers when running as an MCP HTTP server.
6
+ This enables per-request credential passing from upstream services like coral-agents.
7
+
8
+ The resolution order for credentials is:
9
+ 1. HTTP headers (when running as MCP HTTP server)
10
+ 2. Environment variables (fallback)
11
+
12
+ Note: This module is prefixed with "_" to indicate it is internal helper logic
13
+ for the MCP module and should not be imported directly by external code.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import os
19
+
20
+ from airbyte.cloud.auth import (
21
+ resolve_cloud_api_url,
22
+ resolve_cloud_client_id,
23
+ resolve_cloud_client_secret,
24
+ resolve_cloud_workspace_id,
25
+ )
26
+ from airbyte.secrets.base import SecretString
27
+ from fastmcp.server.dependencies import get_http_headers
28
+
29
+ from airbyte_ops_mcp.constants import (
30
+ HEADER_AIRBYTE_CLOUD_API_URL,
31
+ HEADER_AIRBYTE_CLOUD_CLIENT_ID,
32
+ HEADER_AIRBYTE_CLOUD_CLIENT_SECRET,
33
+ HEADER_AIRBYTE_CLOUD_WORKSPACE_ID,
34
+ )
35
+
36
+
37
+ def _get_header_value(headers: dict[str, str], header_name: str) -> str | None:
38
+ """Get a header value from a headers dict, case-insensitively.
39
+
40
+ Args:
41
+ headers: Dictionary of HTTP headers.
42
+ header_name: The header name to look for (case-insensitive).
43
+
44
+ Returns:
45
+ The header value if found, None otherwise.
46
+ """
47
+ header_name_lower = header_name.lower()
48
+ for key, value in headers.items():
49
+ if key.lower() == header_name_lower:
50
+ return value
51
+ return None
52
+
53
+
54
+ def get_bearer_token_from_headers() -> SecretString | None:
55
+ """Extract bearer token from HTTP Authorization header.
56
+
57
+ This function extracts the bearer token from the standard HTTP
58
+ `Authorization: Bearer <token>` header when running as an MCP HTTP server.
59
+
60
+ Returns:
61
+ The bearer token as a SecretString, or None if not found or not in HTTP context.
62
+ """
63
+ headers = get_http_headers()
64
+ if not headers:
65
+ return None
66
+
67
+ auth_header = _get_header_value(headers, "Authorization")
68
+ if not auth_header:
69
+ return None
70
+
71
+ # Parse "Bearer <token>" format (case-insensitive prefix check)
72
+ bearer_prefix = "bearer "
73
+ if auth_header.lower().startswith(bearer_prefix):
74
+ token = auth_header[len(bearer_prefix) :].strip()
75
+ if token:
76
+ return SecretString(token)
77
+
78
+ return None
79
+
80
+
81
+ def get_client_id_from_headers() -> SecretString | None:
82
+ """Extract client ID from HTTP headers.
83
+
84
+ Returns:
85
+ The client ID as a SecretString, or None if not found or not in HTTP context.
86
+ """
87
+ headers = get_http_headers()
88
+ if not headers:
89
+ return None
90
+
91
+ value = _get_header_value(headers, HEADER_AIRBYTE_CLOUD_CLIENT_ID)
92
+ if value:
93
+ return SecretString(value)
94
+ return None
95
+
96
+
97
+ def get_client_secret_from_headers() -> SecretString | None:
98
+ """Extract client secret from HTTP headers.
99
+
100
+ Returns:
101
+ The client secret as a SecretString, or None if not found or not in HTTP context.
102
+ """
103
+ headers = get_http_headers()
104
+ if not headers:
105
+ return None
106
+
107
+ value = _get_header_value(headers, HEADER_AIRBYTE_CLOUD_CLIENT_SECRET)
108
+ if value:
109
+ return SecretString(value)
110
+ return None
111
+
112
+
113
+ def get_workspace_id_from_headers() -> str | None:
114
+ """Extract workspace ID from HTTP headers.
115
+
116
+ Returns:
117
+ The workspace ID, or None if not found or not in HTTP context.
118
+ """
119
+ headers = get_http_headers()
120
+ if not headers:
121
+ return None
122
+
123
+ return _get_header_value(headers, HEADER_AIRBYTE_CLOUD_WORKSPACE_ID)
124
+
125
+
126
+ def get_api_url_from_headers() -> str | None:
127
+ """Extract API URL from HTTP headers.
128
+
129
+ Returns:
130
+ The API URL, or None if not found or not in HTTP context.
131
+ """
132
+ headers = get_http_headers()
133
+ if not headers:
134
+ return None
135
+
136
+ return _get_header_value(headers, HEADER_AIRBYTE_CLOUD_API_URL)
137
+
138
+
139
+ def resolve_client_id() -> SecretString:
140
+ """Resolve client ID from HTTP headers or environment variables.
141
+
142
+ Resolution order:
143
+ 1. HTTP header X-Airbyte-Cloud-Client-Id
144
+ 2. Environment variable AIRBYTE_CLOUD_CLIENT_ID (via PyAirbyte)
145
+
146
+ Returns:
147
+ The resolved client ID as a SecretString.
148
+
149
+ Raises:
150
+ PyAirbyteSecretNotFoundError: If no client ID can be resolved.
151
+ """
152
+ header_value = get_client_id_from_headers()
153
+ if header_value:
154
+ return header_value
155
+
156
+ return resolve_cloud_client_id()
157
+
158
+
159
+ def resolve_client_secret() -> SecretString:
160
+ """Resolve client secret from HTTP headers or environment variables.
161
+
162
+ Resolution order:
163
+ 1. HTTP header X-Airbyte-Cloud-Client-Secret
164
+ 2. Environment variable AIRBYTE_CLOUD_CLIENT_SECRET (via PyAirbyte)
165
+
166
+ Returns:
167
+ The resolved client secret as a SecretString.
168
+
169
+ Raises:
170
+ PyAirbyteSecretNotFoundError: If no client secret can be resolved.
171
+ """
172
+ header_value = get_client_secret_from_headers()
173
+ if header_value:
174
+ return header_value
175
+
176
+ return resolve_cloud_client_secret()
177
+
178
+
179
+ def resolve_workspace_id(workspace_id: str | None = None) -> str:
180
+ """Resolve workspace ID from multiple sources.
181
+
182
+ Resolution order:
183
+ 1. Explicit workspace_id parameter (if provided)
184
+ 2. HTTP header X-Airbyte-Cloud-Workspace-Id
185
+ 3. Environment variable AIRBYTE_CLOUD_WORKSPACE_ID (via PyAirbyte)
186
+
187
+ Args:
188
+ workspace_id: Optional explicit workspace ID.
189
+
190
+ Returns:
191
+ The resolved workspace ID.
192
+
193
+ Raises:
194
+ PyAirbyteSecretNotFoundError: If no workspace ID can be resolved.
195
+ """
196
+ if workspace_id is not None:
197
+ return workspace_id
198
+
199
+ header_value = get_workspace_id_from_headers()
200
+ if header_value:
201
+ return header_value
202
+
203
+ return resolve_cloud_workspace_id()
204
+
205
+
206
+ def resolve_api_url(api_url: str | None = None) -> str:
207
+ """Resolve API URL from multiple sources.
208
+
209
+ Resolution order:
210
+ 1. Explicit api_url parameter (if provided)
211
+ 2. HTTP header X-Airbyte-Cloud-Api-Url
212
+ 3. Environment variable / default (via PyAirbyte)
213
+
214
+ Args:
215
+ api_url: Optional explicit API URL.
216
+
217
+ Returns:
218
+ The resolved API URL.
219
+ """
220
+ if api_url is not None:
221
+ return api_url
222
+
223
+ header_value = get_api_url_from_headers()
224
+ if header_value:
225
+ return header_value
226
+
227
+ return resolve_cloud_api_url()
228
+
229
+
230
+ def resolve_bearer_token() -> SecretString | None:
231
+ """Resolve bearer token from HTTP headers or environment variables.
232
+
233
+ Resolution order:
234
+ 1. HTTP Authorization header (Bearer <token>)
235
+ 2. Environment variable AIRBYTE_CLOUD_BEARER_TOKEN
236
+
237
+ Returns:
238
+ The resolved bearer token as a SecretString, or None if not found.
239
+
240
+ Note:
241
+ Unlike resolve_client_id/resolve_client_secret, this function returns
242
+ None instead of raising an exception if no bearer token is found,
243
+ since bearer token auth is optional (can fall back to client credentials).
244
+ """
245
+ header_value = get_bearer_token_from_headers()
246
+ if header_value:
247
+ return header_value
248
+
249
+ # Try env var directly
250
+ env_value = os.environ.get("AIRBYTE_CLOUD_BEARER_TOKEN")
251
+ if env_value:
252
+ return SecretString(env_value)
253
+
254
+ return None
@@ -76,8 +76,8 @@ class ToolDomain(str, Enum):
76
76
  PROMPTS = "prompts"
77
77
  """Prompt templates for common workflows"""
78
78
 
79
- LIVE_TESTS = "live_tests"
80
- """Live tests for connector validation and regression testing"""
79
+ REGRESSION_TESTS = "regression_tests"
80
+ """Regression tests for connector validation and comparison testing"""
81
81
 
82
82
 
83
83
  _REGISTERED_TOOLS: list[tuple[Callable[..., Any], dict[str, Any]]] = []