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
|
@@ -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
|
+
]
|