golf-mcp 0.2.16__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.
- golf/__init__.py +1 -0
- golf/auth/__init__.py +277 -0
- golf/auth/api_key.py +73 -0
- golf/auth/factory.py +360 -0
- golf/auth/helpers.py +175 -0
- golf/auth/providers.py +586 -0
- golf/auth/registry.py +256 -0
- golf/cli/__init__.py +1 -0
- golf/cli/branding.py +191 -0
- golf/cli/main.py +377 -0
- golf/commands/__init__.py +5 -0
- golf/commands/build.py +81 -0
- golf/commands/init.py +290 -0
- golf/commands/run.py +137 -0
- golf/core/__init__.py +1 -0
- golf/core/builder.py +1884 -0
- golf/core/builder_auth.py +209 -0
- golf/core/builder_metrics.py +221 -0
- golf/core/builder_telemetry.py +99 -0
- golf/core/config.py +199 -0
- golf/core/parser.py +1085 -0
- golf/core/telemetry.py +492 -0
- golf/core/transformer.py +231 -0
- golf/examples/__init__.py +0 -0
- golf/examples/basic/.env.example +4 -0
- golf/examples/basic/README.md +133 -0
- golf/examples/basic/auth.py +76 -0
- golf/examples/basic/golf.json +5 -0
- golf/examples/basic/prompts/welcome.py +27 -0
- golf/examples/basic/resources/current_time.py +34 -0
- golf/examples/basic/resources/info.py +28 -0
- golf/examples/basic/resources/weather/city.py +46 -0
- golf/examples/basic/resources/weather/client.py +48 -0
- golf/examples/basic/resources/weather/current.py +36 -0
- golf/examples/basic/resources/weather/forecast.py +36 -0
- golf/examples/basic/tools/calculator.py +94 -0
- golf/examples/basic/tools/say/hello.py +65 -0
- golf/metrics/__init__.py +10 -0
- golf/metrics/collector.py +320 -0
- golf/metrics/registry.py +12 -0
- golf/telemetry/__init__.py +23 -0
- golf/telemetry/instrumentation.py +1402 -0
- golf/utilities/__init__.py +12 -0
- golf/utilities/context.py +53 -0
- golf/utilities/elicitation.py +170 -0
- golf/utilities/sampling.py +221 -0
- golf_mcp-0.2.16.dist-info/METADATA +262 -0
- golf_mcp-0.2.16.dist-info/RECORD +52 -0
- golf_mcp-0.2.16.dist-info/WHEEL +5 -0
- golf_mcp-0.2.16.dist-info/entry_points.txt +2 -0
- golf_mcp-0.2.16.dist-info/licenses/LICENSE +201 -0
- golf_mcp-0.2.16.dist-info/top_level.txt +1 -0
golf/auth/factory.py
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""Factory functions for creating FastMCP authentication providers."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
# Import these at runtime to avoid import errors during Golf installation
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from fastmcp.server.auth.auth import AuthProvider
|
|
11
|
+
from fastmcp.server.auth import JWTVerifier, StaticTokenVerifier
|
|
12
|
+
from mcp.server.auth.settings import RevocationOptions
|
|
13
|
+
|
|
14
|
+
from .providers import (
|
|
15
|
+
AuthConfig,
|
|
16
|
+
JWTAuthConfig,
|
|
17
|
+
StaticTokenConfig,
|
|
18
|
+
OAuthServerConfig,
|
|
19
|
+
RemoteAuthConfig,
|
|
20
|
+
OAuthProxyConfig,
|
|
21
|
+
)
|
|
22
|
+
from .registry import (
|
|
23
|
+
get_provider_registry,
|
|
24
|
+
create_auth_provider_from_registry,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def create_auth_provider(config: AuthConfig) -> "AuthProvider":
|
|
29
|
+
"""Create a FastMCP AuthProvider from Golf auth configuration.
|
|
30
|
+
|
|
31
|
+
This function uses the provider registry system to allow extensibility.
|
|
32
|
+
Built-in providers are automatically registered, and custom providers
|
|
33
|
+
can be added via the registry system.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
config: Golf authentication configuration
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Configured FastMCP AuthProvider instance
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
ValueError: If configuration is invalid
|
|
43
|
+
ImportError: If required dependencies are missing
|
|
44
|
+
KeyError: If provider type is not registered
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
return create_auth_provider_from_registry(config)
|
|
48
|
+
except KeyError:
|
|
49
|
+
# Fall back to legacy dispatch for backward compatibility
|
|
50
|
+
# This ensures existing code continues to work during transition
|
|
51
|
+
if config.provider_type == "jwt":
|
|
52
|
+
return _create_jwt_provider(config)
|
|
53
|
+
elif config.provider_type == "static":
|
|
54
|
+
return _create_static_provider(config)
|
|
55
|
+
elif config.provider_type == "oauth_server":
|
|
56
|
+
return _create_oauth_server_provider(config)
|
|
57
|
+
elif config.provider_type == "remote":
|
|
58
|
+
return _create_remote_provider(config)
|
|
59
|
+
elif config.provider_type == "oauth_proxy":
|
|
60
|
+
return _create_oauth_proxy_provider(config)
|
|
61
|
+
else:
|
|
62
|
+
raise ValueError(f"Unknown provider type: {config.provider_type}") from None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _create_jwt_provider(config: JWTAuthConfig) -> "JWTVerifier":
|
|
66
|
+
"""Create JWT token verifier from configuration."""
|
|
67
|
+
# Resolve runtime values from environment variables
|
|
68
|
+
public_key = config.public_key
|
|
69
|
+
if config.public_key_env_var:
|
|
70
|
+
env_value = os.environ.get(config.public_key_env_var)
|
|
71
|
+
if env_value:
|
|
72
|
+
public_key = env_value
|
|
73
|
+
|
|
74
|
+
jwks_uri = config.jwks_uri
|
|
75
|
+
if config.jwks_uri_env_var:
|
|
76
|
+
env_value = os.environ.get(config.jwks_uri_env_var)
|
|
77
|
+
if env_value:
|
|
78
|
+
jwks_uri = env_value
|
|
79
|
+
|
|
80
|
+
issuer = config.issuer
|
|
81
|
+
if config.issuer_env_var:
|
|
82
|
+
env_value = os.environ.get(config.issuer_env_var)
|
|
83
|
+
if env_value:
|
|
84
|
+
issuer = env_value
|
|
85
|
+
|
|
86
|
+
audience = config.audience
|
|
87
|
+
if config.audience_env_var:
|
|
88
|
+
env_value = os.environ.get(config.audience_env_var)
|
|
89
|
+
if env_value:
|
|
90
|
+
# Handle both string and comma-separated list
|
|
91
|
+
if "," in env_value:
|
|
92
|
+
audience = [s.strip() for s in env_value.split(",")]
|
|
93
|
+
else:
|
|
94
|
+
audience = env_value
|
|
95
|
+
|
|
96
|
+
# Validate configuration
|
|
97
|
+
if not public_key and not jwks_uri:
|
|
98
|
+
raise ValueError("Either public_key or jwks_uri must be provided for JWT verification")
|
|
99
|
+
|
|
100
|
+
if public_key and jwks_uri:
|
|
101
|
+
raise ValueError("Provide either public_key or jwks_uri, not both")
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
from fastmcp.server.auth import JWTVerifier
|
|
105
|
+
except ImportError as e:
|
|
106
|
+
raise ImportError("JWTVerifier not available. Please install fastmcp>=2.11.0") from e
|
|
107
|
+
|
|
108
|
+
return JWTVerifier(
|
|
109
|
+
public_key=public_key,
|
|
110
|
+
jwks_uri=jwks_uri,
|
|
111
|
+
issuer=issuer,
|
|
112
|
+
audience=audience,
|
|
113
|
+
algorithm=config.algorithm,
|
|
114
|
+
required_scopes=config.required_scopes,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _create_static_provider(config: StaticTokenConfig) -> "StaticTokenVerifier":
|
|
119
|
+
"""Create static token verifier from configuration."""
|
|
120
|
+
if not config.tokens:
|
|
121
|
+
raise ValueError("Static token provider requires at least one token")
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
from fastmcp.server.auth import StaticTokenVerifier
|
|
125
|
+
except ImportError as e:
|
|
126
|
+
raise ImportError("StaticTokenVerifier not available. Please install fastmcp>=2.11.0") from e
|
|
127
|
+
|
|
128
|
+
return StaticTokenVerifier(
|
|
129
|
+
tokens=config.tokens,
|
|
130
|
+
required_scopes=config.required_scopes,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _create_oauth_server_provider(config: OAuthServerConfig) -> "AuthProvider":
|
|
135
|
+
"""Create OAuth authorization server provider from configuration."""
|
|
136
|
+
try:
|
|
137
|
+
from fastmcp.server.auth import OAuthProvider
|
|
138
|
+
except ImportError as e:
|
|
139
|
+
raise ImportError(
|
|
140
|
+
"OAuthProvider not available in this FastMCP version. Please upgrade to FastMCP 2.11.0 or later."
|
|
141
|
+
) from e
|
|
142
|
+
|
|
143
|
+
# Resolve runtime values from environment variables with validation
|
|
144
|
+
base_url = config.base_url
|
|
145
|
+
if config.base_url_env_var:
|
|
146
|
+
env_value = os.environ.get(config.base_url_env_var)
|
|
147
|
+
if env_value:
|
|
148
|
+
# Apply the same validation as the config field to env var value
|
|
149
|
+
try:
|
|
150
|
+
from urllib.parse import urlparse
|
|
151
|
+
|
|
152
|
+
env_value = env_value.strip()
|
|
153
|
+
parsed = urlparse(env_value)
|
|
154
|
+
|
|
155
|
+
if not parsed.scheme or not parsed.netloc:
|
|
156
|
+
raise ValueError(
|
|
157
|
+
f"Invalid base URL from environment variable {config.base_url_env_var}: '{env_value}'"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if parsed.scheme not in ("http", "https"):
|
|
161
|
+
raise ValueError(f"Base URL from environment must use http/https: '{env_value}'")
|
|
162
|
+
|
|
163
|
+
# Production HTTPS check
|
|
164
|
+
is_production = (
|
|
165
|
+
os.environ.get("GOLF_ENV", "").lower() in ("prod", "production")
|
|
166
|
+
or os.environ.get("NODE_ENV", "").lower() == "production"
|
|
167
|
+
or os.environ.get("ENVIRONMENT", "").lower() in ("prod", "production")
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if is_production and parsed.scheme == "http":
|
|
171
|
+
raise ValueError(f"Base URL must use HTTPS in production: '{env_value}'")
|
|
172
|
+
|
|
173
|
+
base_url = env_value
|
|
174
|
+
|
|
175
|
+
except Exception as e:
|
|
176
|
+
raise ValueError(f"Invalid base URL from environment variable {config.base_url_env_var}: {e}") from e
|
|
177
|
+
|
|
178
|
+
# Additional security validations before creating provider
|
|
179
|
+
from urllib.parse import urlparse
|
|
180
|
+
|
|
181
|
+
# Validate final base_url
|
|
182
|
+
parsed_base = urlparse(base_url)
|
|
183
|
+
if not parsed_base.scheme or not parsed_base.netloc:
|
|
184
|
+
raise ValueError(f"Invalid base URL: '{base_url}'")
|
|
185
|
+
|
|
186
|
+
# Security check: prevent localhost in production
|
|
187
|
+
is_production = (
|
|
188
|
+
os.environ.get("GOLF_ENV", "").lower() in ("prod", "production")
|
|
189
|
+
or os.environ.get("NODE_ENV", "").lower() == "production"
|
|
190
|
+
or os.environ.get("ENVIRONMENT", "").lower() in ("prod", "production")
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if is_production and parsed_base.hostname in ("localhost", "127.0.0.1", "0.0.0.0"):
|
|
194
|
+
raise ValueError(f"Cannot use localhost/loopback addresses in production: '{base_url}'")
|
|
195
|
+
|
|
196
|
+
# Client registration options - always disabled for security
|
|
197
|
+
client_reg_options = None
|
|
198
|
+
|
|
199
|
+
# Create revocation options
|
|
200
|
+
revocation_options = None
|
|
201
|
+
if config.allow_token_revocation:
|
|
202
|
+
revocation_options = RevocationOptions(enabled=True)
|
|
203
|
+
|
|
204
|
+
return OAuthProvider(
|
|
205
|
+
base_url=base_url,
|
|
206
|
+
issuer_url=config.issuer_url,
|
|
207
|
+
service_documentation_url=config.service_documentation_url,
|
|
208
|
+
client_registration_options=client_reg_options,
|
|
209
|
+
revocation_options=revocation_options,
|
|
210
|
+
required_scopes=config.required_scopes,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _create_remote_provider(config: RemoteAuthConfig) -> "AuthProvider":
|
|
215
|
+
"""Create remote auth provider from configuration."""
|
|
216
|
+
try:
|
|
217
|
+
from fastmcp.server.auth import RemoteAuthProvider
|
|
218
|
+
except ImportError as e:
|
|
219
|
+
raise ImportError(
|
|
220
|
+
"RemoteAuthProvider not available in this FastMCP version. Please upgrade to FastMCP 2.11.0 or later."
|
|
221
|
+
) from e
|
|
222
|
+
|
|
223
|
+
# Resolve runtime values from environment variables
|
|
224
|
+
authorization_servers = config.authorization_servers
|
|
225
|
+
if config.authorization_servers_env_var:
|
|
226
|
+
env_value = os.environ.get(config.authorization_servers_env_var)
|
|
227
|
+
if env_value:
|
|
228
|
+
# Split comma-separated values and strip whitespace
|
|
229
|
+
authorization_servers = [s.strip() for s in env_value.split(",")]
|
|
230
|
+
|
|
231
|
+
resource_server_url = config.resource_server_url
|
|
232
|
+
if config.resource_server_url_env_var:
|
|
233
|
+
env_value = os.environ.get(config.resource_server_url_env_var)
|
|
234
|
+
if env_value:
|
|
235
|
+
resource_server_url = env_value
|
|
236
|
+
|
|
237
|
+
# Create the underlying token verifier
|
|
238
|
+
token_verifier = create_auth_provider(config.token_verifier_config)
|
|
239
|
+
|
|
240
|
+
# Ensure it's actually a TokenVerifier
|
|
241
|
+
if not hasattr(token_verifier, "verify_token"):
|
|
242
|
+
raise ValueError(f"Remote auth provider requires a TokenVerifier, got {type(token_verifier).__name__}")
|
|
243
|
+
|
|
244
|
+
# Update token verifier's required_scopes to match our scopes_supported for PRM
|
|
245
|
+
# RemoteAuthProvider uses token_verifier.required_scopes for scopes_supported in PRM
|
|
246
|
+
if config.scopes_supported and hasattr(token_verifier, "required_scopes"):
|
|
247
|
+
token_verifier.required_scopes = list(config.scopes_supported)
|
|
248
|
+
|
|
249
|
+
return RemoteAuthProvider(
|
|
250
|
+
token_verifier=token_verifier,
|
|
251
|
+
authorization_servers=authorization_servers,
|
|
252
|
+
resource_server_url=resource_server_url,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _create_oauth_proxy_provider(config: OAuthProxyConfig) -> "AuthProvider":
|
|
257
|
+
"""Create OAuth proxy provider - requires enterprise package."""
|
|
258
|
+
try:
|
|
259
|
+
# Try to import from enterprise package
|
|
260
|
+
from golf_enterprise import create_oauth_proxy_provider
|
|
261
|
+
|
|
262
|
+
return create_oauth_proxy_provider(config)
|
|
263
|
+
except ImportError as e:
|
|
264
|
+
raise ImportError(
|
|
265
|
+
"OAuth Proxy requires golf-mcp-enterprise package. "
|
|
266
|
+
"This feature provides OAuth proxy functionality for non-DCR providers "
|
|
267
|
+
"(GitHub, Google, Okta Web Apps, etc.). "
|
|
268
|
+
"Contact sales@golf.dev for enterprise licensing."
|
|
269
|
+
) from e
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def create_simple_jwt_provider(
|
|
273
|
+
*,
|
|
274
|
+
jwks_uri: str | None = None,
|
|
275
|
+
public_key: str | None = None,
|
|
276
|
+
issuer: str | None = None,
|
|
277
|
+
audience: str | list[str] | None = None,
|
|
278
|
+
required_scopes: list[str] | None = None,
|
|
279
|
+
) -> "JWTVerifier":
|
|
280
|
+
"""Create a simple JWT provider for common use cases.
|
|
281
|
+
|
|
282
|
+
This is a convenience function for creating JWT providers without
|
|
283
|
+
having to construct the full configuration objects.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
jwks_uri: JWKS URI for key fetching
|
|
287
|
+
public_key: Static public key (PEM format)
|
|
288
|
+
issuer: Expected issuer claim
|
|
289
|
+
audience: Expected audience claim(s)
|
|
290
|
+
required_scopes: Required scopes for all requests
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Configured JWTVerifier instance
|
|
294
|
+
"""
|
|
295
|
+
config = JWTAuthConfig(
|
|
296
|
+
jwks_uri=jwks_uri,
|
|
297
|
+
public_key=public_key,
|
|
298
|
+
issuer=issuer,
|
|
299
|
+
audience=audience,
|
|
300
|
+
required_scopes=required_scopes or [],
|
|
301
|
+
)
|
|
302
|
+
return _create_jwt_provider(config)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def create_dev_token_provider(
|
|
306
|
+
tokens: dict[str, Any] | None = None,
|
|
307
|
+
required_scopes: list[str] | None = None,
|
|
308
|
+
) -> "StaticTokenVerifier":
|
|
309
|
+
"""Create a static token provider for development.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
tokens: Token dictionary or None for default dev tokens
|
|
313
|
+
required_scopes: Required scopes for all requests
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
Configured StaticTokenVerifier instance
|
|
317
|
+
"""
|
|
318
|
+
if tokens is None:
|
|
319
|
+
# Default development tokens
|
|
320
|
+
tokens = {
|
|
321
|
+
"dev-token-123": {
|
|
322
|
+
"client_id": "dev-client",
|
|
323
|
+
"scopes": ["read", "write"],
|
|
324
|
+
},
|
|
325
|
+
"admin-token-456": {
|
|
326
|
+
"client_id": "admin-client",
|
|
327
|
+
"scopes": ["read", "write", "admin"],
|
|
328
|
+
},
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
config = StaticTokenConfig(
|
|
332
|
+
tokens=tokens,
|
|
333
|
+
required_scopes=required_scopes or [],
|
|
334
|
+
)
|
|
335
|
+
return _create_static_provider(config)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def register_builtin_providers() -> None:
|
|
339
|
+
"""Register built-in authentication providers in the registry.
|
|
340
|
+
|
|
341
|
+
This function registers the standard Golf authentication providers:
|
|
342
|
+
- jwt: JWT token verification
|
|
343
|
+
- static: Static token verification (development)
|
|
344
|
+
- oauth_server: Full OAuth authorization server
|
|
345
|
+
- remote: Remote authorization server integration
|
|
346
|
+
|
|
347
|
+
Note: oauth_proxy provider is registered by the golf-mcp-enterprise package
|
|
348
|
+
"""
|
|
349
|
+
registry = get_provider_registry()
|
|
350
|
+
|
|
351
|
+
# Register built-in provider factories
|
|
352
|
+
registry.register_factory("jwt", _create_jwt_provider)
|
|
353
|
+
registry.register_factory("static", _create_static_provider)
|
|
354
|
+
registry.register_factory("oauth_server", _create_oauth_server_provider)
|
|
355
|
+
registry.register_factory("remote", _create_remote_provider)
|
|
356
|
+
# oauth_proxy is registered by golf-mcp-enterprise package when installed
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
# Register built-in providers when module is imported
|
|
360
|
+
register_builtin_providers()
|
golf/auth/helpers.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Helper functions for working with authentication in MCP context."""
|
|
2
|
+
|
|
3
|
+
from contextvars import ContextVar
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Context variable to store the current request's API key
|
|
7
|
+
_current_api_key: ContextVar[str | None] = ContextVar("current_api_key", default=None)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def extract_token_from_header(auth_header: str) -> str | None:
|
|
11
|
+
"""Extract bearer token from Authorization header.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
auth_header: Authorization header value
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Bearer token or None if not present/valid
|
|
18
|
+
"""
|
|
19
|
+
if not auth_header:
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
parts = auth_header.split()
|
|
23
|
+
if len(parts) != 2 or parts[0].lower() != "bearer":
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
return parts[1]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def set_api_key(api_key: str | None) -> None:
|
|
30
|
+
"""Set the API key for the current request context.
|
|
31
|
+
|
|
32
|
+
This is an internal function used by the middleware.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
api_key: The API key to store in the context
|
|
36
|
+
"""
|
|
37
|
+
_current_api_key.set(api_key)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_api_key() -> str | None:
|
|
41
|
+
"""Get the API key from the current request context.
|
|
42
|
+
|
|
43
|
+
This function should be used in tools to retrieve the API key
|
|
44
|
+
that was sent in the request headers.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The API key if available, None otherwise
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
# In a tool file
|
|
51
|
+
from golf.auth import get_api_key
|
|
52
|
+
|
|
53
|
+
async def call_api():
|
|
54
|
+
api_key = get_api_key()
|
|
55
|
+
if not api_key:
|
|
56
|
+
return {"error": "No API key provided"}
|
|
57
|
+
|
|
58
|
+
# Use the API key in your request
|
|
59
|
+
headers = {"Authorization": f"Bearer {api_key}"}
|
|
60
|
+
...
|
|
61
|
+
"""
|
|
62
|
+
# Try to get directly from HTTP request if available (FastMCP pattern)
|
|
63
|
+
try:
|
|
64
|
+
# This follows the FastMCP pattern for accessing HTTP requests
|
|
65
|
+
from fastmcp.server.dependencies import get_http_request
|
|
66
|
+
|
|
67
|
+
request = get_http_request()
|
|
68
|
+
|
|
69
|
+
if request and hasattr(request, "state") and hasattr(request.state, "api_key"):
|
|
70
|
+
api_key = request.state.api_key
|
|
71
|
+
return api_key
|
|
72
|
+
|
|
73
|
+
# Get the API key configuration
|
|
74
|
+
from golf.auth.api_key import get_api_key_config
|
|
75
|
+
|
|
76
|
+
api_key_config = get_api_key_config()
|
|
77
|
+
|
|
78
|
+
if api_key_config and request:
|
|
79
|
+
# Extract API key from headers
|
|
80
|
+
header_name = api_key_config.header_name
|
|
81
|
+
header_prefix = api_key_config.header_prefix
|
|
82
|
+
|
|
83
|
+
# Case-insensitive header lookup
|
|
84
|
+
api_key = None
|
|
85
|
+
for k, v in request.headers.items():
|
|
86
|
+
if k.lower() == header_name.lower():
|
|
87
|
+
api_key = v
|
|
88
|
+
break
|
|
89
|
+
|
|
90
|
+
# Strip prefix if configured
|
|
91
|
+
if api_key and header_prefix and api_key.startswith(header_prefix):
|
|
92
|
+
api_key = api_key[len(header_prefix) :]
|
|
93
|
+
|
|
94
|
+
if api_key:
|
|
95
|
+
return api_key
|
|
96
|
+
except (ImportError, RuntimeError):
|
|
97
|
+
# FastMCP not available or not in HTTP context
|
|
98
|
+
pass
|
|
99
|
+
except Exception:
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
# Final fallback: environment variable (for development/testing)
|
|
103
|
+
import os
|
|
104
|
+
|
|
105
|
+
env_api_key = os.environ.get("API_KEY")
|
|
106
|
+
if env_api_key:
|
|
107
|
+
return env_api_key
|
|
108
|
+
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def get_auth_token() -> str | None:
|
|
113
|
+
"""Get the authorization token from the current request context.
|
|
114
|
+
|
|
115
|
+
This function should be used in tools to retrieve the authorization token
|
|
116
|
+
(typically a JWT or OAuth token) that was sent in the request headers.
|
|
117
|
+
|
|
118
|
+
Unlike get_api_key(), this function extracts the raw token from the Authorization
|
|
119
|
+
header without stripping any prefix, making it suitable for passing through
|
|
120
|
+
to upstream APIs that expect the full Authorization header value.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
The authorization token if available, None otherwise
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
# In a tool file
|
|
127
|
+
from golf.auth import get_auth_token
|
|
128
|
+
|
|
129
|
+
async def call_upstream_api():
|
|
130
|
+
auth_token = get_auth_token()
|
|
131
|
+
if not auth_token:
|
|
132
|
+
return {"error": "No authorization token provided"}
|
|
133
|
+
|
|
134
|
+
# Use the full token in upstream request
|
|
135
|
+
headers = {"Authorization": f"Bearer {auth_token}"}
|
|
136
|
+
async with httpx.AsyncClient() as client:
|
|
137
|
+
response = await client.get("https://api.example.com/data", headers=headers)
|
|
138
|
+
...
|
|
139
|
+
"""
|
|
140
|
+
# Try to get directly from HTTP request if available (FastMCP pattern)
|
|
141
|
+
try:
|
|
142
|
+
# This follows the FastMCP pattern for accessing HTTP requests
|
|
143
|
+
from fastmcp.server.dependencies import get_http_request
|
|
144
|
+
|
|
145
|
+
request = get_http_request()
|
|
146
|
+
|
|
147
|
+
if request and hasattr(request, "state") and hasattr(request.state, "auth_token"):
|
|
148
|
+
return request.state.auth_token
|
|
149
|
+
|
|
150
|
+
if request:
|
|
151
|
+
# Extract authorization token from Authorization header
|
|
152
|
+
auth_header = None
|
|
153
|
+
for k, v in request.headers.items():
|
|
154
|
+
if k.lower() == "authorization":
|
|
155
|
+
auth_header = v
|
|
156
|
+
break
|
|
157
|
+
|
|
158
|
+
if auth_header:
|
|
159
|
+
# Extract the token part (everything after "Bearer ")
|
|
160
|
+
token = extract_token_from_header(auth_header)
|
|
161
|
+
if token:
|
|
162
|
+
return token
|
|
163
|
+
|
|
164
|
+
# If not Bearer format, return the whole header value minus "Bearer " prefix if present
|
|
165
|
+
if auth_header.lower().startswith("bearer "):
|
|
166
|
+
return auth_header[7:] # Remove "Bearer " prefix
|
|
167
|
+
return auth_header
|
|
168
|
+
|
|
169
|
+
except (ImportError, RuntimeError):
|
|
170
|
+
# FastMCP not available or not in HTTP context
|
|
171
|
+
pass
|
|
172
|
+
except Exception:
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
return None
|