golf-mcp 0.2.0__tar.gz → 0.2.3__tar.gz
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.
Potentially problematic release.
This version of golf-mcp might be problematic. Click here for more details.
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/MANIFEST.in +1 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/PKG-INFO +1 -1
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/pyproject.toml +2 -2
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/__init__.py +1 -1
- golf_mcp-0.2.3/src/golf/_endpoints.py.in +6 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/auth/__init__.py +48 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/auth/factory.py +27 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/auth/providers.py +219 -1
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/core/builder_auth.py +3 -3
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf_mcp.egg-info/PKG-INFO +1 -1
- golf_mcp-0.2.3/src/golf_mcp.egg-info/SOURCES.txt +63 -0
- golf_mcp-0.2.0/src/golf/examples/basic/.coverage +0 -0
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/.gitignore +0 -2
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/class_index.html +0 -547
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/coverage_html_cb_6fb7b396.js +0 -733
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/favicon_32_cb_58284776.png +0 -0
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/function_index.html +0 -2091
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/index.html +0 -349
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/keybd_closed_cb_ce680311.png +0 -0
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/status.json +0 -1
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/style_cb_8e611ae1.css +0 -337
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_1c9a91c0e91c8496___init___py.html +0 -323
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_1c9a91c0e91c8496_api_key_py.html +0 -170
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_1c9a91c0e91c8496_factory_py.html +0 -430
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_1c9a91c0e91c8496_helpers_py.html +0 -288
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_1c9a91c0e91c8496_providers_py.html +0 -493
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_1c9a91c0e91c8496_registry_py.html +0 -353
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_3ec3b3f490dc0950___init___py.html +0 -120
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_3ec3b3f490dc0950_instrumentation_py.html +0 -1535
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_4b8b9dd4ccccc5db___init___py.html +0 -98
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_4b8b9dd4ccccc5db_branding_py.html +0 -289
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_4b8b9dd4ccccc5db_main_py.html +0 -476
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_5a6c4e6bcc86fb2f___init___py.html +0 -97
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_6cadab9ec0df475d___init___py.html +0 -102
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_6cadab9ec0df475d_build_py.html +0 -178
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_6cadab9ec0df475d_init_py.html +0 -387
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_6cadab9ec0df475d_run_py.html +0 -222
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_6fcdee0582ba84e4___init___py.html +0 -106
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_6fcdee0582ba84e4__endpoints_fallback_py.html +0 -107
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_7ba499ed22986217___init___py.html +0 -98
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_7ba499ed22986217_builder_auth_py.html +0 -306
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_7ba499ed22986217_builder_metrics_py.html +0 -329
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_7ba499ed22986217_builder_py.html +0 -1471
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_7ba499ed22986217_builder_telemetry_py.html +0 -186
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_7ba499ed22986217_config_py.html +0 -315
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_7ba499ed22986217_parser_py.html +0 -1149
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_7ba499ed22986217_platform_py.html +0 -279
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_7ba499ed22986217_telemetry_py.html +0 -589
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_7ba499ed22986217_transformer_py.html +0 -286
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_7d7da37693a43688___init___py.html +0 -107
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_7d7da37693a43688_collector_py.html +0 -417
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_7d7da37693a43688_registry_py.html +0 -109
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_abe733142b40ad4e___init___py.html +0 -109
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_abe733142b40ad4e_context_py.html +0 -150
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_abe733142b40ad4e_elicitation_py.html +0 -267
- golf_mcp-0.2.0/src/golf/examples/basic/htmlcov/z_abe733142b40ad4e_sampling_py.html +0 -318
- golf_mcp-0.2.0/src/golf_mcp.egg-info/SOURCES.txt +0 -107
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/.docs/docs.md +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/.docs/fastmcp-diff.md +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/.docs/mcp.md +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/LICENSE +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/README.md +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/setup.cfg +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/setup.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/_endpoints_fallback.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/auth/api_key.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/auth/helpers.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/auth/registry.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/cli/__init__.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/cli/branding.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/cli/main.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/commands/__init__.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/commands/build.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/commands/init.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/commands/run.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/core/__init__.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/core/builder.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/core/builder_metrics.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/core/builder_telemetry.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/core/config.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/core/parser.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/core/platform.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/core/telemetry.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/core/transformer.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/examples/__init__.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/examples/basic/.env.example +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/examples/basic/README.md +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/examples/basic/auth.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/examples/basic/golf.json +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/examples/basic/prompts/welcome.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/examples/basic/resources/current_time.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/examples/basic/resources/info.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/examples/basic/resources/weather/city.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/examples/basic/resources/weather/common.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/examples/basic/resources/weather/current.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/examples/basic/resources/weather/forecast.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/examples/basic/tools/calculator.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/examples/basic/tools/say/hello.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/metrics/__init__.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/metrics/collector.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/metrics/registry.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/telemetry/__init__.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/telemetry/instrumentation.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/utilities/__init__.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/utilities/context.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/utilities/elicitation.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf/utilities/sampling.py +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf_mcp.egg-info/dependency_links.txt +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf_mcp.egg-info/entry_points.txt +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf_mcp.egg-info/requires.txt +0 -0
- {golf_mcp-0.2.0 → golf_mcp-0.2.3}/src/golf_mcp.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "golf-mcp"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.3"
|
|
8
8
|
description = "Framework for building MCP servers"
|
|
9
9
|
authors = [
|
|
10
10
|
{name = "Antoni Gmitruk", email = "antoni@golf.dev"}
|
|
@@ -66,7 +66,7 @@ golf = ["examples/**/*"]
|
|
|
66
66
|
|
|
67
67
|
[tool.poetry]
|
|
68
68
|
name = "golf-mcp"
|
|
69
|
-
version = "0.
|
|
69
|
+
version = "0.2.3"
|
|
70
70
|
description = "Framework for building MCP servers with zero boilerplate"
|
|
71
71
|
authors = ["Antoni Gmitruk <antoni@golf.dev>"]
|
|
72
72
|
license = "Apache-2.0"
|
|
@@ -14,6 +14,7 @@ from .providers import (
|
|
|
14
14
|
StaticTokenConfig,
|
|
15
15
|
OAuthServerConfig,
|
|
16
16
|
RemoteAuthConfig,
|
|
17
|
+
OAuthProxyConfig,
|
|
17
18
|
)
|
|
18
19
|
from .factory import (
|
|
19
20
|
create_auth_provider,
|
|
@@ -44,6 +45,7 @@ __all__ = [
|
|
|
44
45
|
"configure_auth",
|
|
45
46
|
"configure_jwt_auth",
|
|
46
47
|
"configure_dev_auth",
|
|
48
|
+
"configure_oauth_proxy",
|
|
47
49
|
"get_auth_config",
|
|
48
50
|
# Provider configurations
|
|
49
51
|
"AuthConfig",
|
|
@@ -51,6 +53,7 @@ __all__ = [
|
|
|
51
53
|
"StaticTokenConfig",
|
|
52
54
|
"OAuthServerConfig",
|
|
53
55
|
"RemoteAuthConfig",
|
|
56
|
+
"OAuthProxyConfig",
|
|
54
57
|
# Factory functions
|
|
55
58
|
"create_auth_provider",
|
|
56
59
|
"create_simple_jwt_provider",
|
|
@@ -191,6 +194,51 @@ def configure_dev_auth(
|
|
|
191
194
|
configure_auth(config)
|
|
192
195
|
|
|
193
196
|
|
|
197
|
+
def configure_oauth_proxy(
|
|
198
|
+
upstream_authorization_endpoint: str,
|
|
199
|
+
upstream_token_endpoint: str,
|
|
200
|
+
upstream_client_id: str,
|
|
201
|
+
upstream_client_secret: str,
|
|
202
|
+
base_url: str,
|
|
203
|
+
token_verifier_config: JWTAuthConfig | StaticTokenConfig,
|
|
204
|
+
scopes_supported: list[str] | None = None,
|
|
205
|
+
upstream_revocation_endpoint: str | None = None,
|
|
206
|
+
redirect_path: str = "/oauth/callback",
|
|
207
|
+
) -> None:
|
|
208
|
+
"""Configure OAuth proxy authentication for non-DCR providers.
|
|
209
|
+
|
|
210
|
+
This sets up an OAuth proxy that bridges MCP clients (expecting DCR) with
|
|
211
|
+
traditional OAuth providers like GitHub, Google, Okta Web Apps that use
|
|
212
|
+
fixed client credentials.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
upstream_authorization_endpoint: Provider's authorization URL
|
|
216
|
+
upstream_token_endpoint: Provider's token endpoint URL
|
|
217
|
+
upstream_client_id: Your client ID registered with the provider
|
|
218
|
+
upstream_client_secret: Your client secret from the provider
|
|
219
|
+
base_url: This proxy server's public URL
|
|
220
|
+
token_verifier_config: JWT or static token config for token verification
|
|
221
|
+
scopes_supported: Scopes to advertise to MCP clients
|
|
222
|
+
upstream_revocation_endpoint: Optional token revocation endpoint
|
|
223
|
+
redirect_path: OAuth callback path (default: /oauth/callback)
|
|
224
|
+
|
|
225
|
+
Note:
|
|
226
|
+
Requires golf-mcp-enterprise package for implementation.
|
|
227
|
+
"""
|
|
228
|
+
config = OAuthProxyConfig(
|
|
229
|
+
upstream_authorization_endpoint=upstream_authorization_endpoint,
|
|
230
|
+
upstream_token_endpoint=upstream_token_endpoint,
|
|
231
|
+
upstream_client_id=upstream_client_id,
|
|
232
|
+
upstream_client_secret=upstream_client_secret,
|
|
233
|
+
upstream_revocation_endpoint=upstream_revocation_endpoint,
|
|
234
|
+
base_url=base_url,
|
|
235
|
+
redirect_path=redirect_path,
|
|
236
|
+
scopes_supported=scopes_supported or [],
|
|
237
|
+
token_verifier_config=token_verifier_config,
|
|
238
|
+
)
|
|
239
|
+
configure_auth(config)
|
|
240
|
+
|
|
241
|
+
|
|
194
242
|
def get_auth_config() -> AuthConfig | None:
|
|
195
243
|
"""Get the current auth configuration.
|
|
196
244
|
|
|
@@ -17,6 +17,7 @@ from .providers import (
|
|
|
17
17
|
StaticTokenConfig,
|
|
18
18
|
OAuthServerConfig,
|
|
19
19
|
RemoteAuthConfig,
|
|
20
|
+
OAuthProxyConfig,
|
|
20
21
|
)
|
|
21
22
|
from .registry import (
|
|
22
23
|
get_provider_registry,
|
|
@@ -55,6 +56,8 @@ def create_auth_provider(config: AuthConfig) -> "AuthProvider":
|
|
|
55
56
|
return _create_oauth_server_provider(config)
|
|
56
57
|
elif config.provider_type == "remote":
|
|
57
58
|
return _create_remote_provider(config)
|
|
59
|
+
elif config.provider_type == "oauth_proxy":
|
|
60
|
+
return _create_oauth_proxy_provider(config)
|
|
58
61
|
else:
|
|
59
62
|
raise ValueError(f"Unknown provider type: {config.provider_type}") from None
|
|
60
63
|
|
|
@@ -238,6 +241,11 @@ def _create_remote_provider(config: RemoteAuthConfig) -> "AuthProvider":
|
|
|
238
241
|
if not hasattr(token_verifier, "verify_token"):
|
|
239
242
|
raise ValueError(f"Remote auth provider requires a TokenVerifier, got {type(token_verifier).__name__}")
|
|
240
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
|
+
|
|
241
249
|
return RemoteAuthProvider(
|
|
242
250
|
token_verifier=token_verifier,
|
|
243
251
|
authorization_servers=authorization_servers,
|
|
@@ -245,6 +253,22 @@ def _create_remote_provider(config: RemoteAuthConfig) -> "AuthProvider":
|
|
|
245
253
|
)
|
|
246
254
|
|
|
247
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
|
+
|
|
248
272
|
def create_simple_jwt_provider(
|
|
249
273
|
*,
|
|
250
274
|
jwks_uri: str | None = None,
|
|
@@ -319,6 +343,8 @@ def register_builtin_providers() -> None:
|
|
|
319
343
|
- static: Static token verification (development)
|
|
320
344
|
- oauth_server: Full OAuth authorization server
|
|
321
345
|
- remote: Remote authorization server integration
|
|
346
|
+
|
|
347
|
+
Note: oauth_proxy provider is registered by the golf-mcp-enterprise package
|
|
322
348
|
"""
|
|
323
349
|
registry = get_provider_registry()
|
|
324
350
|
|
|
@@ -327,6 +353,7 @@ def register_builtin_providers() -> None:
|
|
|
327
353
|
registry.register_factory("static", _create_static_provider)
|
|
328
354
|
registry.register_factory("oauth_server", _create_oauth_server_provider)
|
|
329
355
|
registry.register_factory("remote", _create_remote_provider)
|
|
356
|
+
# oauth_proxy is registered by golf-mcp-enterprise package when installed
|
|
330
357
|
|
|
331
358
|
|
|
332
359
|
# Register built-in providers when module is imported
|
|
@@ -299,6 +299,12 @@ class RemoteAuthConfig(BaseModel):
|
|
|
299
299
|
# This server's URL
|
|
300
300
|
resource_server_url: str = Field(..., description="URL of this resource server")
|
|
301
301
|
|
|
302
|
+
# Scopes this resource supports (advertised via /.well-known/oauth-protected-resource)
|
|
303
|
+
scopes_supported: list[str] = Field(
|
|
304
|
+
default_factory=list,
|
|
305
|
+
description="Scopes this resource supports (advertised via /.well-known/oauth-protected-resource)",
|
|
306
|
+
)
|
|
307
|
+
|
|
302
308
|
# Token verification (delegate to another config)
|
|
303
309
|
token_verifier_config: JWTAuthConfig | StaticTokenConfig = Field(
|
|
304
310
|
..., description="Configuration for the underlying token verifier"
|
|
@@ -362,6 +368,45 @@ class RemoteAuthConfig(BaseModel):
|
|
|
362
368
|
|
|
363
369
|
return url
|
|
364
370
|
|
|
371
|
+
@field_validator("scopes_supported")
|
|
372
|
+
@classmethod
|
|
373
|
+
def validate_scopes_supported(cls, v: list[str]) -> list[str]:
|
|
374
|
+
"""Validate scopes_supported format and security."""
|
|
375
|
+
if not v:
|
|
376
|
+
return v
|
|
377
|
+
|
|
378
|
+
cleaned_scopes = []
|
|
379
|
+
for scope in v:
|
|
380
|
+
scope = scope.strip()
|
|
381
|
+
if not scope:
|
|
382
|
+
raise ValueError("Scopes cannot be empty or whitespace-only")
|
|
383
|
+
|
|
384
|
+
# OAuth 2.0 scope format validation (RFC 6749)
|
|
385
|
+
if not all(32 < ord(c) < 127 and c not in ' "\\' for c in scope):
|
|
386
|
+
raise ValueError(
|
|
387
|
+
f"Invalid scope format: '{scope}' - must be ASCII printable without spaces, quotes, or backslashes"
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
# Reasonable length limit to prevent abuse
|
|
391
|
+
if len(scope) > 128:
|
|
392
|
+
raise ValueError(f"Scope too long: '{scope}' - maximum 128 characters")
|
|
393
|
+
|
|
394
|
+
# Warn about potentially dangerous scope names
|
|
395
|
+
dangerous_scopes = {"admin", "root", "superuser", "system", "*", "all"}
|
|
396
|
+
if scope.lower() in dangerous_scopes:
|
|
397
|
+
import warnings
|
|
398
|
+
|
|
399
|
+
warnings.warn(
|
|
400
|
+
f"Potentially dangerous scope detected: '{scope}'. "
|
|
401
|
+
"Consider using more specific, principle-of-least-privilege scopes.",
|
|
402
|
+
UserWarning,
|
|
403
|
+
stacklevel=2,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
cleaned_scopes.append(scope)
|
|
407
|
+
|
|
408
|
+
return cleaned_scopes
|
|
409
|
+
|
|
365
410
|
@model_validator(mode="after")
|
|
366
411
|
def validate_token_verifier_compatibility(self) -> "RemoteAuthConfig":
|
|
367
412
|
"""Validate that the token verifier config is compatible with token verification."""
|
|
@@ -389,8 +434,181 @@ class RemoteAuthConfig(BaseModel):
|
|
|
389
434
|
if isinstance(config, StaticTokenConfig) and not config.tokens:
|
|
390
435
|
raise ValueError("Static token verifier config must provide at least one token")
|
|
391
436
|
|
|
437
|
+
# Convenience: if user didn't set scopes_supported, default to verifier.required_scopes
|
|
438
|
+
if not self.scopes_supported:
|
|
439
|
+
if hasattr(config, "required_scopes") and config.required_scopes:
|
|
440
|
+
self.scopes_supported = list(config.required_scopes)
|
|
441
|
+
|
|
442
|
+
return self
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
class OAuthProxyConfig(BaseModel):
|
|
446
|
+
"""Configuration for OAuth proxy functionality (requires golf-mcp-enterprise).
|
|
447
|
+
|
|
448
|
+
This configuration enables bridging MCP clients (which expect Dynamic Client
|
|
449
|
+
Registration) with OAuth providers that use fixed client credentials like
|
|
450
|
+
GitHub Apps, Google Cloud Console apps, Okta Web Applications, etc.
|
|
451
|
+
|
|
452
|
+
The proxy acts as a DCR-capable authorization server to MCP clients while
|
|
453
|
+
using your fixed upstream client credentials with the actual OAuth provider.
|
|
454
|
+
|
|
455
|
+
Note: This class provides configuration only. The actual implementation
|
|
456
|
+
requires the golf-mcp-enterprise package.
|
|
457
|
+
"""
|
|
458
|
+
|
|
459
|
+
provider_type: Literal["oauth_proxy"] = "oauth_proxy"
|
|
460
|
+
|
|
461
|
+
# Upstream OAuth provider configuration
|
|
462
|
+
upstream_authorization_endpoint: str = Field(..., description="Upstream provider's authorization endpoint URL")
|
|
463
|
+
upstream_token_endpoint: str = Field(..., description="Upstream provider's token endpoint URL")
|
|
464
|
+
upstream_client_id: str = Field(..., description="Your registered client ID with the upstream provider")
|
|
465
|
+
upstream_client_secret: str = Field(..., description="Your registered client secret with the upstream provider")
|
|
466
|
+
upstream_revocation_endpoint: str | None = Field(None, description="Optional upstream token revocation endpoint")
|
|
467
|
+
|
|
468
|
+
# This proxy server configuration
|
|
469
|
+
base_url: str = Field(..., description="Public URL of this OAuth proxy server")
|
|
470
|
+
redirect_path: str = Field("/oauth/callback", description="OAuth callback path (must match provider registration)")
|
|
471
|
+
|
|
472
|
+
# Scopes and token verification
|
|
473
|
+
scopes_supported: list[str] = Field(default_factory=list, description="Scopes supported by this proxy")
|
|
474
|
+
token_verifier_config: JWTAuthConfig | StaticTokenConfig = Field(
|
|
475
|
+
..., description="Token verifier configuration for validating upstream tokens"
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
# Environment variable names for runtime configuration
|
|
479
|
+
upstream_authorization_endpoint_env_var: str | None = Field(
|
|
480
|
+
None, description="Environment variable name for upstream authorization endpoint"
|
|
481
|
+
)
|
|
482
|
+
upstream_token_endpoint_env_var: str | None = Field(
|
|
483
|
+
None, description="Environment variable name for upstream token endpoint"
|
|
484
|
+
)
|
|
485
|
+
upstream_client_id_env_var: str | None = Field(None, description="Environment variable name for upstream client ID")
|
|
486
|
+
upstream_client_secret_env_var: str | None = Field(
|
|
487
|
+
None, description="Environment variable name for upstream client secret"
|
|
488
|
+
)
|
|
489
|
+
upstream_revocation_endpoint_env_var: str | None = Field(
|
|
490
|
+
None, description="Environment variable name for upstream revocation endpoint"
|
|
491
|
+
)
|
|
492
|
+
base_url_env_var: str | None = Field(None, description="Environment variable name for base URL")
|
|
493
|
+
|
|
494
|
+
@field_validator("upstream_authorization_endpoint", "upstream_token_endpoint", "base_url")
|
|
495
|
+
@classmethod
|
|
496
|
+
def validate_required_urls(cls, v: str) -> str:
|
|
497
|
+
"""Validate required URLs are properly formatted."""
|
|
498
|
+
if not v or not v.strip():
|
|
499
|
+
raise ValueError("URL cannot be empty")
|
|
500
|
+
|
|
501
|
+
url = v.strip()
|
|
502
|
+
try:
|
|
503
|
+
from urllib.parse import urlparse
|
|
504
|
+
|
|
505
|
+
parsed = urlparse(url)
|
|
506
|
+
if not parsed.scheme or not parsed.netloc:
|
|
507
|
+
raise ValueError(f"Invalid URL format: '{url}' - must include scheme and netloc")
|
|
508
|
+
if parsed.scheme not in ("http", "https"):
|
|
509
|
+
raise ValueError(f"URL must use http or https scheme: '{url}'")
|
|
510
|
+
except Exception as e:
|
|
511
|
+
if isinstance(e, ValueError):
|
|
512
|
+
raise
|
|
513
|
+
raise ValueError(f"Invalid URL '{url}': {e}") from e
|
|
514
|
+
|
|
515
|
+
return url
|
|
516
|
+
|
|
517
|
+
@field_validator("upstream_revocation_endpoint")
|
|
518
|
+
@classmethod
|
|
519
|
+
def validate_optional_url(cls, v: str | None) -> str | None:
|
|
520
|
+
"""Validate optional URLs are properly formatted."""
|
|
521
|
+
if not v:
|
|
522
|
+
return v
|
|
523
|
+
|
|
524
|
+
url = v.strip()
|
|
525
|
+
if not url:
|
|
526
|
+
return None
|
|
527
|
+
|
|
528
|
+
try:
|
|
529
|
+
from urllib.parse import urlparse
|
|
530
|
+
|
|
531
|
+
parsed = urlparse(url)
|
|
532
|
+
if not parsed.scheme or not parsed.netloc:
|
|
533
|
+
raise ValueError(f"Invalid URL format: '{url}' - must include scheme and netloc")
|
|
534
|
+
if parsed.scheme not in ("http", "https"):
|
|
535
|
+
raise ValueError(f"URL must use http or https scheme: '{url}'")
|
|
536
|
+
except Exception as e:
|
|
537
|
+
if isinstance(e, ValueError):
|
|
538
|
+
raise
|
|
539
|
+
raise ValueError(f"Invalid URL '{url}': {e}") from e
|
|
540
|
+
|
|
541
|
+
return url
|
|
542
|
+
|
|
543
|
+
@field_validator("scopes_supported")
|
|
544
|
+
@classmethod
|
|
545
|
+
def validate_scopes_supported(cls, v: list[str]) -> list[str]:
|
|
546
|
+
"""Validate scopes_supported format and security."""
|
|
547
|
+
if not v:
|
|
548
|
+
return v
|
|
549
|
+
|
|
550
|
+
cleaned_scopes = []
|
|
551
|
+
for scope in v:
|
|
552
|
+
scope = scope.strip()
|
|
553
|
+
if not scope:
|
|
554
|
+
raise ValueError("Scopes cannot be empty or whitespace-only")
|
|
555
|
+
|
|
556
|
+
# OAuth 2.0 scope format validation (RFC 6749)
|
|
557
|
+
if not all(32 < ord(c) < 127 and c not in ' "\\' for c in scope):
|
|
558
|
+
raise ValueError(
|
|
559
|
+
f"Invalid scope format: '{scope}' - must be ASCII printable without spaces, quotes, or backslashes"
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
# Reasonable length limit to prevent abuse
|
|
563
|
+
if len(scope) > 128:
|
|
564
|
+
raise ValueError(f"Scope too long: '{scope}' - maximum 128 characters")
|
|
565
|
+
|
|
566
|
+
cleaned_scopes.append(scope)
|
|
567
|
+
|
|
568
|
+
return cleaned_scopes
|
|
569
|
+
|
|
570
|
+
@model_validator(mode="after")
|
|
571
|
+
def validate_oauth_proxy_config(self) -> "OAuthProxyConfig":
|
|
572
|
+
"""Validate OAuth proxy configuration consistency."""
|
|
573
|
+
# Validate token verifier config is compatible
|
|
574
|
+
if not isinstance(self.token_verifier_config, JWTAuthConfig | StaticTokenConfig):
|
|
575
|
+
raise ValueError(
|
|
576
|
+
f"token_verifier_config must be JWTAuthConfig or StaticTokenConfig, got {type(self.token_verifier_config).__name__}"
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
# Warn about HTTPS requirements in production
|
|
580
|
+
is_production = (
|
|
581
|
+
os.environ.get("GOLF_ENV", "").lower() in ("prod", "production")
|
|
582
|
+
or os.environ.get("NODE_ENV", "").lower() == "production"
|
|
583
|
+
or os.environ.get("ENVIRONMENT", "").lower() in ("prod", "production")
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
if is_production:
|
|
587
|
+
from urllib.parse import urlparse
|
|
588
|
+
|
|
589
|
+
urls_to_check = [
|
|
590
|
+
("base_url", self.base_url),
|
|
591
|
+
("upstream_authorization_endpoint", self.upstream_authorization_endpoint),
|
|
592
|
+
("upstream_token_endpoint", self.upstream_token_endpoint),
|
|
593
|
+
]
|
|
594
|
+
|
|
595
|
+
if self.upstream_revocation_endpoint:
|
|
596
|
+
urls_to_check.append(("upstream_revocation_endpoint", self.upstream_revocation_endpoint))
|
|
597
|
+
|
|
598
|
+
for field_name, url in urls_to_check:
|
|
599
|
+
parsed = urlparse(url)
|
|
600
|
+
if parsed.scheme == "http":
|
|
601
|
+
import warnings
|
|
602
|
+
|
|
603
|
+
warnings.warn(
|
|
604
|
+
f"OAuth proxy {field_name} '{url}' uses HTTP in production environment. "
|
|
605
|
+
"HTTPS is strongly recommended for OAuth endpoints to prevent token interception.",
|
|
606
|
+
UserWarning,
|
|
607
|
+
stacklevel=2,
|
|
608
|
+
)
|
|
609
|
+
|
|
392
610
|
return self
|
|
393
611
|
|
|
394
612
|
|
|
395
613
|
# Union type for all auth configurations
|
|
396
|
-
AuthConfig = JWTAuthConfig | StaticTokenConfig | OAuthServerConfig | RemoteAuthConfig
|
|
614
|
+
AuthConfig = JWTAuthConfig | StaticTokenConfig | OAuthServerConfig | RemoteAuthConfig | OAuthProxyConfig
|
|
@@ -52,7 +52,7 @@ def generate_auth_code(
|
|
|
52
52
|
"import os",
|
|
53
53
|
"import sys",
|
|
54
54
|
"from golf.auth.factory import create_auth_provider",
|
|
55
|
-
"from golf.auth.providers import RemoteAuthConfig, JWTAuthConfig, StaticTokenConfig, OAuthServerConfig",
|
|
55
|
+
"from golf.auth.providers import RemoteAuthConfig, JWTAuthConfig, StaticTokenConfig, OAuthServerConfig, OAuthProxyConfig",
|
|
56
56
|
]
|
|
57
57
|
|
|
58
58
|
# Embed the auth configuration directly in the generated code
|
|
@@ -64,7 +64,7 @@ def generate_auth_code(
|
|
|
64
64
|
f"auth_config = {auth_config_repr}",
|
|
65
65
|
"try:",
|
|
66
66
|
" auth_provider = create_auth_provider(auth_config)",
|
|
67
|
-
"
|
|
67
|
+
" # Authentication configured with {auth_config.provider_type} provider",
|
|
68
68
|
"except Exception as e:",
|
|
69
69
|
" print(f'Authentication setup failed: {e}', file=sys.stderr)",
|
|
70
70
|
" auth_provider = None",
|
|
@@ -203,7 +203,7 @@ if auth_provider and hasattr(auth_provider, 'get_routes'):
|
|
|
203
203
|
# Add routes to FastMCP's additional HTTP routes list
|
|
204
204
|
try:
|
|
205
205
|
mcp._additional_http_routes.extend(auth_routes)
|
|
206
|
-
|
|
206
|
+
# Added {len(auth_routes)} OAuth metadata routes
|
|
207
207
|
except Exception as e:
|
|
208
208
|
print(f"Warning: Failed to add OAuth routes: {e}")
|
|
209
209
|
"""
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
setup.py
|
|
6
|
+
.docs/docs.md
|
|
7
|
+
.docs/fastmcp-diff.md
|
|
8
|
+
.docs/mcp.md
|
|
9
|
+
src/golf/__init__.py
|
|
10
|
+
src/golf/_endpoints.py.in
|
|
11
|
+
src/golf/_endpoints_fallback.py
|
|
12
|
+
src/golf/auth/__init__.py
|
|
13
|
+
src/golf/auth/api_key.py
|
|
14
|
+
src/golf/auth/factory.py
|
|
15
|
+
src/golf/auth/helpers.py
|
|
16
|
+
src/golf/auth/providers.py
|
|
17
|
+
src/golf/auth/registry.py
|
|
18
|
+
src/golf/cli/__init__.py
|
|
19
|
+
src/golf/cli/branding.py
|
|
20
|
+
src/golf/cli/main.py
|
|
21
|
+
src/golf/commands/__init__.py
|
|
22
|
+
src/golf/commands/build.py
|
|
23
|
+
src/golf/commands/init.py
|
|
24
|
+
src/golf/commands/run.py
|
|
25
|
+
src/golf/core/__init__.py
|
|
26
|
+
src/golf/core/builder.py
|
|
27
|
+
src/golf/core/builder_auth.py
|
|
28
|
+
src/golf/core/builder_metrics.py
|
|
29
|
+
src/golf/core/builder_telemetry.py
|
|
30
|
+
src/golf/core/config.py
|
|
31
|
+
src/golf/core/parser.py
|
|
32
|
+
src/golf/core/platform.py
|
|
33
|
+
src/golf/core/telemetry.py
|
|
34
|
+
src/golf/core/transformer.py
|
|
35
|
+
src/golf/examples/__init__.py
|
|
36
|
+
src/golf/examples/basic/.env.example
|
|
37
|
+
src/golf/examples/basic/README.md
|
|
38
|
+
src/golf/examples/basic/auth.py
|
|
39
|
+
src/golf/examples/basic/golf.json
|
|
40
|
+
src/golf/examples/basic/prompts/welcome.py
|
|
41
|
+
src/golf/examples/basic/resources/current_time.py
|
|
42
|
+
src/golf/examples/basic/resources/info.py
|
|
43
|
+
src/golf/examples/basic/resources/weather/city.py
|
|
44
|
+
src/golf/examples/basic/resources/weather/common.py
|
|
45
|
+
src/golf/examples/basic/resources/weather/current.py
|
|
46
|
+
src/golf/examples/basic/resources/weather/forecast.py
|
|
47
|
+
src/golf/examples/basic/tools/calculator.py
|
|
48
|
+
src/golf/examples/basic/tools/say/hello.py
|
|
49
|
+
src/golf/metrics/__init__.py
|
|
50
|
+
src/golf/metrics/collector.py
|
|
51
|
+
src/golf/metrics/registry.py
|
|
52
|
+
src/golf/telemetry/__init__.py
|
|
53
|
+
src/golf/telemetry/instrumentation.py
|
|
54
|
+
src/golf/utilities/__init__.py
|
|
55
|
+
src/golf/utilities/context.py
|
|
56
|
+
src/golf/utilities/elicitation.py
|
|
57
|
+
src/golf/utilities/sampling.py
|
|
58
|
+
src/golf_mcp.egg-info/PKG-INFO
|
|
59
|
+
src/golf_mcp.egg-info/SOURCES.txt
|
|
60
|
+
src/golf_mcp.egg-info/dependency_links.txt
|
|
61
|
+
src/golf_mcp.egg-info/entry_points.txt
|
|
62
|
+
src/golf_mcp.egg-info/requires.txt
|
|
63
|
+
src/golf_mcp.egg-info/top_level.txt
|
|
Binary file
|