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.
Files changed (52) hide show
  1. golf/__init__.py +1 -0
  2. golf/auth/__init__.py +277 -0
  3. golf/auth/api_key.py +73 -0
  4. golf/auth/factory.py +360 -0
  5. golf/auth/helpers.py +175 -0
  6. golf/auth/providers.py +586 -0
  7. golf/auth/registry.py +256 -0
  8. golf/cli/__init__.py +1 -0
  9. golf/cli/branding.py +191 -0
  10. golf/cli/main.py +377 -0
  11. golf/commands/__init__.py +5 -0
  12. golf/commands/build.py +81 -0
  13. golf/commands/init.py +290 -0
  14. golf/commands/run.py +137 -0
  15. golf/core/__init__.py +1 -0
  16. golf/core/builder.py +1884 -0
  17. golf/core/builder_auth.py +209 -0
  18. golf/core/builder_metrics.py +221 -0
  19. golf/core/builder_telemetry.py +99 -0
  20. golf/core/config.py +199 -0
  21. golf/core/parser.py +1085 -0
  22. golf/core/telemetry.py +492 -0
  23. golf/core/transformer.py +231 -0
  24. golf/examples/__init__.py +0 -0
  25. golf/examples/basic/.env.example +4 -0
  26. golf/examples/basic/README.md +133 -0
  27. golf/examples/basic/auth.py +76 -0
  28. golf/examples/basic/golf.json +5 -0
  29. golf/examples/basic/prompts/welcome.py +27 -0
  30. golf/examples/basic/resources/current_time.py +34 -0
  31. golf/examples/basic/resources/info.py +28 -0
  32. golf/examples/basic/resources/weather/city.py +46 -0
  33. golf/examples/basic/resources/weather/client.py +48 -0
  34. golf/examples/basic/resources/weather/current.py +36 -0
  35. golf/examples/basic/resources/weather/forecast.py +36 -0
  36. golf/examples/basic/tools/calculator.py +94 -0
  37. golf/examples/basic/tools/say/hello.py +65 -0
  38. golf/metrics/__init__.py +10 -0
  39. golf/metrics/collector.py +320 -0
  40. golf/metrics/registry.py +12 -0
  41. golf/telemetry/__init__.py +23 -0
  42. golf/telemetry/instrumentation.py +1402 -0
  43. golf/utilities/__init__.py +12 -0
  44. golf/utilities/context.py +53 -0
  45. golf/utilities/elicitation.py +170 -0
  46. golf/utilities/sampling.py +221 -0
  47. golf_mcp-0.2.16.dist-info/METADATA +262 -0
  48. golf_mcp-0.2.16.dist-info/RECORD +52 -0
  49. golf_mcp-0.2.16.dist-info/WHEEL +5 -0
  50. golf_mcp-0.2.16.dist-info/entry_points.txt +2 -0
  51. golf_mcp-0.2.16.dist-info/licenses/LICENSE +201 -0
  52. 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