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
@@ -0,0 +1,209 @@
1
+ """Authentication integration for the Golf MCP build process.
2
+
3
+ This module adds support for injecting authentication configuration
4
+ into the generated FastMCP application during the build process using
5
+ FastMCP 2.11+ built-in auth providers.
6
+ """
7
+
8
+ from golf.auth import get_auth_config, is_auth_configured
9
+ from golf.auth.api_key import get_api_key_config
10
+ from golf.auth.providers import AuthConfig
11
+
12
+
13
+ def generate_auth_code(
14
+ server_name: str,
15
+ host: str = "localhost",
16
+ port: int = 3000,
17
+ https: bool = False,
18
+ opentelemetry_enabled: bool = False,
19
+ transport: str = "streamable-http",
20
+ ) -> dict:
21
+ """Generate authentication components for the FastMCP app using modern
22
+ auth providers.
23
+
24
+ Returns a dictionary with:
25
+ - imports: List of import statements
26
+ - setup_code: Auth setup code (provider configuration, etc.)
27
+ - fastmcp_args: Dict of arguments to add to FastMCP constructor
28
+ - has_auth: Whether auth is configured
29
+ """
30
+ # Check for API key configuration first
31
+ api_key_config = get_api_key_config()
32
+ if api_key_config:
33
+ return generate_api_key_auth_components(server_name, opentelemetry_enabled, transport)
34
+
35
+ # Check for modern auth configuration
36
+ auth_config = get_auth_config()
37
+ if not auth_config:
38
+ # If no auth config, return empty components
39
+ return {"imports": [], "setup_code": [], "fastmcp_args": {}, "has_auth": False}
40
+
41
+ # Validate that we have a modern auth config
42
+ if not isinstance(auth_config, AuthConfig):
43
+ raise ValueError(
44
+ f"Invalid auth configuration type: {type(auth_config).__name__}. "
45
+ "Golf 0.2.x requires modern auth configurations (JWTAuthConfig, "
46
+ "StaticTokenConfig, OAuthServerConfig, or RemoteAuthConfig). "
47
+ "Please update your auth.py file."
48
+ )
49
+
50
+ # Generate modern auth components with embedded configuration
51
+ auth_imports = [
52
+ "import os",
53
+ "import sys",
54
+ "from golf.auth.factory import create_auth_provider",
55
+ "from golf.auth.providers import RemoteAuthConfig, JWTAuthConfig, StaticTokenConfig, OAuthServerConfig, OAuthProxyConfig",
56
+ ]
57
+
58
+ # Embed the auth configuration directly in the generated code
59
+ # Convert the auth config to its string representation for embedding
60
+ auth_config_repr = repr(auth_config)
61
+
62
+ setup_code_lines = [
63
+ "# Modern FastMCP 2.11+ authentication setup with embedded configuration",
64
+ f"auth_config = {auth_config_repr}",
65
+ "try:",
66
+ " auth_provider = create_auth_provider(auth_config)",
67
+ " # Authentication configured with {auth_config.provider_type} provider",
68
+ "except Exception as e:",
69
+ " print(f'Authentication setup failed: {e}', file=sys.stderr)",
70
+ " auth_provider = None",
71
+ "",
72
+ ]
73
+
74
+ # FastMCP constructor arguments - FastMCP 2.11+ uses auth parameter
75
+ fastmcp_args = {"auth": "auth_provider"}
76
+
77
+ return {
78
+ "imports": auth_imports,
79
+ "setup_code": setup_code_lines,
80
+ "fastmcp_args": fastmcp_args,
81
+ "has_auth": True,
82
+ }
83
+
84
+
85
+ def generate_api_key_auth_components(
86
+ server_name: str,
87
+ opentelemetry_enabled: bool = False,
88
+ transport: str = "streamable-http",
89
+ ) -> dict:
90
+ """Generate authentication components for API key authentication.
91
+
92
+ Returns a dictionary with:
93
+ - imports: List of import statements
94
+ - setup_code: Auth setup code (middleware setup)
95
+ - fastmcp_args: Dict of arguments to add to FastMCP constructor
96
+ - has_auth: Whether auth is configured
97
+ """
98
+ api_key_config = get_api_key_config()
99
+ if not api_key_config:
100
+ return {"imports": [], "setup_code": [], "fastmcp_args": {}, "has_auth": False}
101
+
102
+ auth_imports = [
103
+ "# API key authentication setup",
104
+ "from golf.auth.api_key import get_api_key_config, configure_api_key",
105
+ "from golf.auth import set_api_key",
106
+ "from starlette.middleware.base import BaseHTTPMiddleware",
107
+ "from starlette.requests import Request",
108
+ "from starlette.responses import JSONResponse",
109
+ "import os",
110
+ ]
111
+
112
+ setup_code_lines = [
113
+ "# Recreate API key configuration from auth.py",
114
+ "configure_api_key(",
115
+ f" header_name={repr(api_key_config.header_name)},",
116
+ f" header_prefix={repr(api_key_config.header_prefix)},",
117
+ f" required={repr(api_key_config.required)}",
118
+ ")",
119
+ "",
120
+ "# Simplified API key middleware that validates presence",
121
+ "class ApiKeyMiddleware(BaseHTTPMiddleware):",
122
+ " async def dispatch(self, request: Request, call_next):",
123
+ " # Debug mode from environment",
124
+ " debug = os.environ.get('API_KEY_DEBUG', '').lower() == 'true'",
125
+ " ",
126
+ " # Skip auth for monitoring endpoints",
127
+ " path = request.url.path",
128
+ " if path in ['/metrics', '/health']:",
129
+ " return await call_next(request)",
130
+ " ",
131
+ " api_key_config = get_api_key_config()",
132
+ " ",
133
+ " if api_key_config:",
134
+ " # Extract API key from the configured header",
135
+ " header_name = api_key_config.header_name",
136
+ " header_prefix = api_key_config.header_prefix",
137
+ " ",
138
+ " # Case-insensitive header lookup",
139
+ " api_key = None",
140
+ " for k, v in request.headers.items():",
141
+ " if k.lower() == header_name.lower():",
142
+ " api_key = v",
143
+ " break",
144
+ " ",
145
+ " # Process the API key if found",
146
+ " if api_key:",
147
+ " # Strip prefix if configured",
148
+ " if header_prefix and api_key.startswith(header_prefix):",
149
+ " api_key = api_key[len(header_prefix):]",
150
+ " ",
151
+ " # Store the API key in request state for tools to access",
152
+ " request.state.api_key = api_key",
153
+ " ",
154
+ " # Also store in context variable for tools",
155
+ " set_api_key(api_key)",
156
+ " ",
157
+ " # Check if API key is required but missing",
158
+ " if api_key_config.required and not api_key:",
159
+ " return JSONResponse(",
160
+ " {'error': 'unauthorized', "
161
+ "'detail': f'Missing required {header_name} header'},"
162
+ " status_code=401,",
163
+ " headers={'WWW-Authenticate': f'{header_name} realm=\"MCP Server\"'}",
164
+ " )",
165
+ " ",
166
+ " # Continue with the request",
167
+ " return await call_next(request)",
168
+ "",
169
+ ]
170
+
171
+ # API key auth is handled via middleware, not FastMCP constructor args
172
+ fastmcp_args = {}
173
+
174
+ return {
175
+ "imports": auth_imports,
176
+ "setup_code": setup_code_lines,
177
+ "fastmcp_args": fastmcp_args,
178
+ "has_auth": True,
179
+ }
180
+
181
+
182
+ def generate_auth_routes() -> str:
183
+ """Generate code for auth routes in the FastMCP app.
184
+
185
+ Auth providers (RemoteAuthProvider, OAuthProvider) provide OAuth metadata routes
186
+ that need to be added to the server.
187
+ """
188
+ # API key auth doesn't need special routes
189
+ api_key_config = get_api_key_config()
190
+ if api_key_config:
191
+ return ""
192
+
193
+ # Check if auth is configured
194
+ if not is_auth_configured():
195
+ return ""
196
+
197
+ # Auth providers provide OAuth metadata routes that need to be added to the server
198
+ return """
199
+ # Add OAuth metadata routes from auth provider
200
+ if auth_provider and hasattr(auth_provider, 'get_routes'):
201
+ auth_routes = auth_provider.get_routes()
202
+ if auth_routes:
203
+ # Add routes to FastMCP's additional HTTP routes list
204
+ try:
205
+ mcp._additional_http_routes.extend(auth_routes)
206
+ # Added {len(auth_routes)} OAuth metadata routes
207
+ except Exception as e:
208
+ print(f"Warning: Failed to add OAuth routes: {e}")
209
+ """
@@ -0,0 +1,221 @@
1
+ """Metrics integration for the GolfMCP build process.
2
+
3
+ This module provides functions for generating Prometheus metrics initialization
4
+ and collection code for FastMCP servers built with GolfMCP.
5
+ """
6
+
7
+
8
+ def generate_metrics_imports() -> list[str]:
9
+ """Generate import statements for metrics collection.
10
+
11
+ Returns:
12
+ List of import statements for metrics
13
+ """
14
+ return [
15
+ "# Prometheus metrics imports",
16
+ "from golf.metrics import init_metrics, get_metrics_collector",
17
+ "from prometheus_client import generate_latest, CONTENT_TYPE_LATEST",
18
+ "from starlette.responses import Response",
19
+ "from starlette.middleware.base import BaseHTTPMiddleware",
20
+ "from starlette.requests import Request",
21
+ "import time",
22
+ ]
23
+
24
+
25
+ def generate_metrics_initialization(server_name: str) -> list[str]:
26
+ """Generate metrics initialization code.
27
+
28
+ Args:
29
+ server_name: Name of the MCP server
30
+
31
+ Returns:
32
+ List of code lines for metrics initialization
33
+ """
34
+ return [
35
+ "# Initialize metrics collection",
36
+ "init_metrics(enabled=True)",
37
+ "",
38
+ ]
39
+
40
+
41
+ def generate_metrics_route(metrics_path: str) -> list[str]:
42
+ """Generate the metrics endpoint route code.
43
+
44
+ Args:
45
+ metrics_path: Path for the metrics endpoint (e.g., "/metrics")
46
+
47
+ Returns:
48
+ List of code lines for the metrics route
49
+ """
50
+ return [
51
+ "# Add metrics endpoint",
52
+ f'@mcp.custom_route("{metrics_path}", methods=["GET"])',
53
+ "async def metrics_endpoint(request):",
54
+ ' """Prometheus metrics endpoint for monitoring."""',
55
+ " # Update uptime before returning metrics",
56
+ " update_uptime()",
57
+ " return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)",
58
+ "",
59
+ ]
60
+
61
+
62
+ def generate_metrics_instrumentation() -> list[str]:
63
+ """Generate metrics instrumentation wrapper functions.
64
+
65
+ Returns:
66
+ List of code lines for metrics instrumentation
67
+ """
68
+ return [
69
+ "# Metrics instrumentation wrapper functions",
70
+ "import time",
71
+ "import functools",
72
+ "from typing import Any, Callable",
73
+ "",
74
+ "def instrument_tool(func: Callable, tool_name: str) -> Callable:",
75
+ ' """Wrap a tool function with metrics collection."""',
76
+ " @functools.wraps(func)",
77
+ " async def wrapper(*args, **kwargs) -> Any:",
78
+ " collector = get_metrics_collector()",
79
+ " start_time = time.time()",
80
+ " status = 'success'",
81
+ " try:",
82
+ " result = await func(*args, **kwargs)",
83
+ " return result",
84
+ " except Exception as e:",
85
+ " status = 'error'",
86
+ " collector.increment_error('tool', type(e).__name__)",
87
+ " raise",
88
+ " finally:",
89
+ " duration = time.time() - start_time",
90
+ " collector.increment_tool_execution(tool_name, status)",
91
+ " collector.record_tool_duration(tool_name, duration)",
92
+ " return wrapper",
93
+ "",
94
+ "def instrument_resource(func: Callable, resource_name: str) -> Callable:",
95
+ ' """Wrap a resource function with metrics collection."""',
96
+ " @functools.wraps(func)",
97
+ " async def wrapper(*args, **kwargs) -> Any:",
98
+ " collector = get_metrics_collector()",
99
+ " try:",
100
+ " result = await func(*args, **kwargs)",
101
+ " # Extract URI from args if available for resource_reads metric",
102
+ " if args and len(args) > 0:",
103
+ " uri = str(args[0]) if args[0] else resource_name",
104
+ " else:",
105
+ " uri = resource_name",
106
+ " collector.increment_resource_read(uri)",
107
+ " return result",
108
+ " except Exception as e:",
109
+ " collector.increment_error('resource', type(e).__name__)",
110
+ " raise",
111
+ " return wrapper",
112
+ "",
113
+ "def instrument_prompt(func: Callable, prompt_name: str) -> Callable:",
114
+ ' """Wrap a prompt function with metrics collection."""',
115
+ " @functools.wraps(func)",
116
+ " async def wrapper(*args, **kwargs) -> Any:",
117
+ " collector = get_metrics_collector()",
118
+ " try:",
119
+ " result = await func(*args, **kwargs)",
120
+ " collector.increment_prompt_generation(prompt_name)",
121
+ " return result",
122
+ " except Exception as e:",
123
+ " collector.increment_error('prompt', type(e).__name__)",
124
+ " raise",
125
+ " return wrapper",
126
+ "",
127
+ "# HTTP Request Metrics Middleware",
128
+ "class MetricsMiddleware(BaseHTTPMiddleware):",
129
+ ' """Middleware to collect HTTP request metrics."""',
130
+ "",
131
+ " async def dispatch(self, request: Request, call_next):",
132
+ " collector = get_metrics_collector()",
133
+ " start_time = time.time()",
134
+ " ",
135
+ " # Extract path and method",
136
+ " method = request.method",
137
+ " path = request.url.path",
138
+ " ",
139
+ " try:",
140
+ " response = await call_next(request)",
141
+ " status_code = response.status_code",
142
+ " except Exception as e:",
143
+ " status_code = 500",
144
+ " collector.increment_error('http', type(e).__name__)",
145
+ " raise",
146
+ " finally:",
147
+ " duration = time.time() - start_time",
148
+ " collector.increment_http_request(method, status_code, path)",
149
+ " collector.record_http_duration(method, path, duration)",
150
+ " ",
151
+ " return response",
152
+ "",
153
+ "# Session tracking helpers",
154
+ "import atexit",
155
+ "from contextlib import asynccontextmanager",
156
+ "",
157
+ "# Global server start time for uptime tracking",
158
+ "_server_start_time = time.time()",
159
+ "",
160
+ "def track_session_start():",
161
+ ' """Track when a new session starts."""',
162
+ " collector = get_metrics_collector()",
163
+ " collector.increment_session()",
164
+ "",
165
+ "def track_session_end(start_time: float):",
166
+ ' """Track when a session ends."""',
167
+ " collector = get_metrics_collector()",
168
+ " duration = time.time() - start_time",
169
+ " collector.record_session_duration(duration)",
170
+ "",
171
+ "def update_uptime():",
172
+ ' """Update the uptime metric."""',
173
+ " collector = get_metrics_collector()",
174
+ " uptime = time.time() - _server_start_time",
175
+ " collector.set_uptime(uptime)",
176
+ "",
177
+ "# Initialize uptime tracking",
178
+ "update_uptime()",
179
+ "",
180
+ ]
181
+
182
+
183
+ def generate_session_tracking() -> list[str]:
184
+ """Generate session tracking integration code.
185
+
186
+ Returns:
187
+ List of code lines for session tracking
188
+ """
189
+ return [
190
+ "# Session tracking integration",
191
+ "import asyncio",
192
+ "from typing import Dict",
193
+ "",
194
+ "# Track active sessions",
195
+ "_active_sessions: Dict[str, float] = {}",
196
+ "",
197
+ "# Hook into FastMCP's session lifecycle if available",
198
+ "try:",
199
+ " from fastmcp.server import SessionManager",
200
+ " ",
201
+ " # Monkey patch session creation if possible",
202
+ " _original_create_session = getattr(mcp, '_create_session', None)",
203
+ " if _original_create_session:",
204
+ " async def _patched_create_session(*args, **kwargs):",
205
+ " session_id = str(id(args)) if args else 'unknown'",
206
+ " _active_sessions[session_id] = time.time()",
207
+ " track_session_start()",
208
+ " try:",
209
+ " return await _original_create_session(*args, **kwargs)",
210
+ " except Exception:",
211
+ " # If session creation fails, clean up",
212
+ " if session_id in _active_sessions:",
213
+ " del _active_sessions[session_id]",
214
+ " raise",
215
+ " ",
216
+ " mcp._create_session = _patched_create_session",
217
+ "except (ImportError, AttributeError):",
218
+ " # Fallback: track sessions via request patterns",
219
+ " pass",
220
+ "",
221
+ ]
@@ -0,0 +1,99 @@
1
+ """OpenTelemetry integration for the GolfMCP build process.
2
+
3
+ This module provides functions for generating OpenTelemetry initialization
4
+ and instrumentation code for FastMCP servers built with GolfMCP.
5
+ """
6
+
7
+
8
+ def generate_telemetry_imports() -> list[str]:
9
+ """Generate import statements for telemetry instrumentation.
10
+
11
+ Returns:
12
+ List of import statements for telemetry
13
+ """
14
+ return [
15
+ "# OpenTelemetry instrumentation imports",
16
+ "from golf.telemetry import (",
17
+ " instrument_tool,",
18
+ " instrument_resource,",
19
+ " instrument_prompt,",
20
+ " telemetry_lifespan,",
21
+ ")",
22
+ ]
23
+
24
+
25
+ def generate_component_registration_with_telemetry(
26
+ component_type: str,
27
+ component_name: str,
28
+ module_path: str,
29
+ entry_function: str,
30
+ docstring: str = "",
31
+ uri_template: str = None,
32
+ is_template: bool = False,
33
+ ) -> str:
34
+ """Generate component registration code with telemetry instrumentation.
35
+
36
+ Args:
37
+ component_type: Type of component ('tool', 'resource', 'prompt')
38
+ component_name: Name of the component
39
+ module_path: Full module path to the component
40
+ entry_function: Entry function name
41
+ docstring: Component description
42
+ uri_template: URI template for resources (optional)
43
+ is_template: Whether the resource is a template (has URI parameters)
44
+
45
+ Returns:
46
+ Python code string for registering the component with instrumentation
47
+ """
48
+ func_ref = f"{module_path}.{entry_function}"
49
+ escaped_docstring = docstring.replace('"', '\\"') if docstring else ""
50
+
51
+ if component_type == "tool":
52
+ wrapped_func = f"instrument_tool({func_ref}, '{component_name}')"
53
+ return (
54
+ f"_tool = Tool.from_function({wrapped_func}, "
55
+ f'name="{component_name}", description="{escaped_docstring}")\n'
56
+ f"mcp.add_tool(_tool)"
57
+ )
58
+
59
+ elif component_type == "resource":
60
+ wrapped_func = f"instrument_resource({func_ref}, '{uri_template}')"
61
+ if is_template:
62
+ return (
63
+ f"_resource = ResourceTemplate.from_function({wrapped_func}, "
64
+ f'uri_template="{uri_template}", name="{component_name}", '
65
+ f'description="{escaped_docstring}")\n'
66
+ f"mcp.add_template(_resource)"
67
+ )
68
+ else:
69
+ return (
70
+ f"_resource = Resource.from_function({wrapped_func}, "
71
+ f'uri="{uri_template}", name="{component_name}", '
72
+ f'description="{escaped_docstring}")\n'
73
+ f"mcp.add_resource(_resource)"
74
+ )
75
+
76
+ elif component_type == "prompt":
77
+ wrapped_func = f"instrument_prompt({func_ref}, '{component_name}')"
78
+ return (
79
+ f"_prompt = Prompt.from_function({wrapped_func}, "
80
+ f'name="{component_name}", description="{escaped_docstring}")\n'
81
+ f"mcp.add_prompt(_prompt)"
82
+ )
83
+
84
+ else:
85
+ raise ValueError(f"Unknown component type: {component_type}")
86
+
87
+
88
+ def get_otel_dependencies() -> list[str]:
89
+ """Get list of OpenTelemetry dependencies to add to pyproject.toml.
90
+
91
+ Returns:
92
+ List of package requirements strings
93
+ """
94
+ return [
95
+ "opentelemetry-api>=1.18.0",
96
+ "opentelemetry-sdk>=1.18.0",
97
+ "opentelemetry-instrumentation-asgi>=0.40b0",
98
+ "opentelemetry-exporter-otlp-proto-http>=0.40b0",
99
+ ]