golf-mcp 0.1.16__py3-none-any.whl → 0.1.18__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.
Potentially problematic release.
This version of golf-mcp might be problematic. Click here for more details.
- golf/__init__.py +1 -1
- golf/cli/main.py +13 -2
- golf/commands/init.py +63 -1
- golf/core/builder.py +220 -59
- golf/core/builder_auth.py +5 -0
- golf/core/builder_metrics.py +232 -0
- golf/core/config.py +12 -0
- golf/core/parser.py +531 -32
- golf/core/platform.py +180 -0
- golf/core/telemetry.py +28 -8
- golf/examples/api_key/.env.example +1 -5
- golf/examples/api_key/README.md +10 -10
- golf/examples/api_key/golf.json +1 -5
- golf/examples/basic/.env.example +3 -4
- golf/examples/basic/golf.json +1 -5
- golf/metrics/__init__.py +10 -0
- golf/metrics/collector.py +239 -0
- golf/metrics/registry.py +12 -0
- golf/telemetry/instrumentation.py +177 -144
- {golf_mcp-0.1.16.dist-info → golf_mcp-0.1.18.dist-info}/METADATA +10 -3
- {golf_mcp-0.1.16.dist-info → golf_mcp-0.1.18.dist-info}/RECORD +25 -20
- {golf_mcp-0.1.16.dist-info → golf_mcp-0.1.18.dist-info}/WHEEL +0 -0
- {golf_mcp-0.1.16.dist-info → golf_mcp-0.1.18.dist-info}/entry_points.txt +0 -0
- {golf_mcp-0.1.16.dist-info → golf_mcp-0.1.18.dist-info}/licenses/LICENSE +0 -0
- {golf_mcp-0.1.16.dist-info → golf_mcp-0.1.18.dist-info}/top_level.txt +0 -0
|
@@ -29,6 +29,15 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> TracerProvider | No
|
|
|
29
29
|
"""
|
|
30
30
|
global _provider
|
|
31
31
|
|
|
32
|
+
# Check for Golf platform integration first
|
|
33
|
+
golf_api_key = os.environ.get("GOLF_API_KEY")
|
|
34
|
+
if golf_api_key and not os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT"):
|
|
35
|
+
# Auto-configure for Golf platform
|
|
36
|
+
os.environ["OTEL_TRACES_EXPORTER"] = "otlp_http"
|
|
37
|
+
os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = "http://localhost:8000/api/v1/otel"
|
|
38
|
+
os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = f"X-Golf-Key={golf_api_key}"
|
|
39
|
+
print("[INFO] Auto-configured OpenTelemetry for Golf platform ingestion")
|
|
40
|
+
|
|
32
41
|
# Check for required environment variables based on exporter type
|
|
33
42
|
exporter_type = os.environ.get("OTEL_TRACES_EXPORTER", "console").lower()
|
|
34
43
|
|
|
@@ -37,7 +46,8 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> TracerProvider | No
|
|
|
37
46
|
endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT")
|
|
38
47
|
if not endpoint:
|
|
39
48
|
print(
|
|
40
|
-
"[WARNING] OpenTelemetry tracing is disabled:
|
|
49
|
+
"[WARNING] OpenTelemetry tracing is disabled: "
|
|
50
|
+
"OTEL_EXPORTER_OTLP_ENDPOINT is not set for OTLP HTTP exporter"
|
|
41
51
|
)
|
|
42
52
|
return None
|
|
43
53
|
|
|
@@ -47,6 +57,14 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> TracerProvider | No
|
|
|
47
57
|
"service.version": os.environ.get("SERVICE_VERSION", "1.0.0"),
|
|
48
58
|
"service.instance.id": os.environ.get("SERVICE_INSTANCE_ID", "default"),
|
|
49
59
|
}
|
|
60
|
+
|
|
61
|
+
# Add Golf-specific attributes if available
|
|
62
|
+
if golf_api_key:
|
|
63
|
+
golf_server_id = os.environ.get("GOLF_SERVER_ID")
|
|
64
|
+
if golf_server_id:
|
|
65
|
+
resource_attributes["golf.server.id"] = golf_server_id
|
|
66
|
+
resource_attributes["golf.platform.enabled"] = "true"
|
|
67
|
+
|
|
50
68
|
resource = Resource.create(resource_attributes)
|
|
51
69
|
|
|
52
70
|
# Create provider
|
|
@@ -71,6 +89,10 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> TracerProvider | No
|
|
|
71
89
|
exporter = OTLPSpanExporter(
|
|
72
90
|
endpoint=endpoint, headers=header_dict if header_dict else None
|
|
73
91
|
)
|
|
92
|
+
|
|
93
|
+
# Log successful configuration for Golf platform
|
|
94
|
+
if golf_api_key:
|
|
95
|
+
print(f"[INFO] OpenTelemetry configured for Golf platform: {endpoint}")
|
|
74
96
|
else:
|
|
75
97
|
# Default to console exporter
|
|
76
98
|
exporter = ConsoleSpanExporter(out=sys.stderr)
|
|
@@ -113,21 +135,6 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> TracerProvider | No
|
|
|
113
135
|
traceback.print_exc()
|
|
114
136
|
raise
|
|
115
137
|
|
|
116
|
-
# Create a test span to verify everything is working
|
|
117
|
-
try:
|
|
118
|
-
test_tracer = provider.get_tracer("golf.telemetry.test", "1.0.0")
|
|
119
|
-
with test_tracer.start_as_current_span("startup.test") as span:
|
|
120
|
-
span.set_attribute("test", True)
|
|
121
|
-
span.set_attribute("service.name", service_name)
|
|
122
|
-
span.set_attribute("exporter.type", exporter_type)
|
|
123
|
-
span.set_attribute(
|
|
124
|
-
"endpoint", os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", "not set")
|
|
125
|
-
)
|
|
126
|
-
except Exception:
|
|
127
|
-
import traceback
|
|
128
|
-
|
|
129
|
-
traceback.print_exc()
|
|
130
|
-
|
|
131
138
|
return provider
|
|
132
139
|
|
|
133
140
|
|
|
@@ -154,14 +161,12 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
154
161
|
|
|
155
162
|
tracer = get_tracer()
|
|
156
163
|
|
|
157
|
-
# Add debug logging
|
|
158
|
-
print(
|
|
159
|
-
f"[TELEMETRY DEBUG] Instrumenting tool: {tool_name} (function: {func.__name__})"
|
|
160
|
-
)
|
|
161
|
-
|
|
162
164
|
@functools.wraps(func)
|
|
163
165
|
async def async_wrapper(*args, **kwargs):
|
|
164
|
-
|
|
166
|
+
# Record metrics timing
|
|
167
|
+
import time
|
|
168
|
+
|
|
169
|
+
start_time = time.time()
|
|
165
170
|
|
|
166
171
|
# Create a more descriptive span name
|
|
167
172
|
span_name = f"mcp.tool.{tool_name}.execute"
|
|
@@ -205,52 +210,9 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
205
210
|
if session_id_from_baggage:
|
|
206
211
|
span.set_attribute("mcp.session.id", session_id_from_baggage)
|
|
207
212
|
|
|
208
|
-
# Add tool arguments as span attributes (be careful with sensitive data)
|
|
209
|
-
for i, arg in enumerate(args):
|
|
210
|
-
if isinstance(arg, str | int | float | bool) or arg is None:
|
|
211
|
-
span.set_attribute(f"mcp.tool.arg.{i}", str(arg))
|
|
212
|
-
elif hasattr(arg, "__dict__"):
|
|
213
|
-
# For objects, just record the type
|
|
214
|
-
span.set_attribute(f"mcp.tool.arg.{i}.type", type(arg).__name__)
|
|
215
|
-
|
|
216
|
-
# Add named arguments with better naming
|
|
217
|
-
for key, value in kwargs.items():
|
|
218
|
-
if key != "ctx":
|
|
219
|
-
if value is None:
|
|
220
|
-
span.set_attribute(f"mcp.tool.input.{key}", "null")
|
|
221
|
-
elif isinstance(value, str | int | float | bool):
|
|
222
|
-
span.set_attribute(f"mcp.tool.input.{key}", str(value))
|
|
223
|
-
elif isinstance(value, list | tuple):
|
|
224
|
-
span.set_attribute(f"mcp.tool.input.{key}.count", len(value))
|
|
225
|
-
span.set_attribute(f"mcp.tool.input.{key}.type", "array")
|
|
226
|
-
elif isinstance(value, dict):
|
|
227
|
-
span.set_attribute(f"mcp.tool.input.{key}.count", len(value))
|
|
228
|
-
span.set_attribute(f"mcp.tool.input.{key}.type", "object")
|
|
229
|
-
# Only show first few keys to avoid exceeding attribute limits
|
|
230
|
-
if len(value) > 0 and len(value) <= 5:
|
|
231
|
-
keys_list = list(value.keys())[:5]
|
|
232
|
-
# Limit key length and join
|
|
233
|
-
truncated_keys = [
|
|
234
|
-
str(k)[:20] + "..." if len(str(k)) > 20 else str(k)
|
|
235
|
-
for k in keys_list
|
|
236
|
-
]
|
|
237
|
-
span.set_attribute(
|
|
238
|
-
f"mcp.tool.input.{key}.sample_keys",
|
|
239
|
-
",".join(truncated_keys),
|
|
240
|
-
)
|
|
241
|
-
else:
|
|
242
|
-
# For other types, at least record the type
|
|
243
|
-
span.set_attribute(
|
|
244
|
-
f"mcp.tool.input.{key}.type", type(value).__name__
|
|
245
|
-
)
|
|
246
|
-
|
|
247
213
|
# Add event for tool execution start
|
|
248
214
|
span.add_event("tool.execution.started", {"tool.name": tool_name})
|
|
249
215
|
|
|
250
|
-
print(
|
|
251
|
-
f"[TELEMETRY DEBUG] Tool span created: {span_name} (span_id: {span.get_span_context().span_id:016x})"
|
|
252
|
-
)
|
|
253
|
-
|
|
254
216
|
try:
|
|
255
217
|
result = await func(*args, **kwargs)
|
|
256
218
|
span.set_status(Status(StatusCode.OK))
|
|
@@ -258,6 +220,19 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
258
220
|
# Add event for successful completion
|
|
259
221
|
span.add_event("tool.execution.completed", {"tool.name": tool_name})
|
|
260
222
|
|
|
223
|
+
# Record metrics for successful execution
|
|
224
|
+
try:
|
|
225
|
+
from golf.metrics import get_metrics_collector
|
|
226
|
+
|
|
227
|
+
metrics_collector = get_metrics_collector()
|
|
228
|
+
metrics_collector.increment_tool_execution(tool_name, "success")
|
|
229
|
+
metrics_collector.record_tool_duration(
|
|
230
|
+
tool_name, time.time() - start_time
|
|
231
|
+
)
|
|
232
|
+
except ImportError:
|
|
233
|
+
# Metrics not available, continue without metrics
|
|
234
|
+
pass
|
|
235
|
+
|
|
261
236
|
# Capture result metadata with better structure
|
|
262
237
|
if result is not None:
|
|
263
238
|
if isinstance(result, str | int | float | bool):
|
|
@@ -288,9 +263,6 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
288
263
|
# For any result, record its type
|
|
289
264
|
span.set_attribute("mcp.tool.result.class", type(result).__name__)
|
|
290
265
|
|
|
291
|
-
print(
|
|
292
|
-
f"[TELEMETRY DEBUG] Tool execution completed successfully: {tool_name}"
|
|
293
|
-
)
|
|
294
266
|
return result
|
|
295
267
|
except Exception as e:
|
|
296
268
|
span.record_exception(e)
|
|
@@ -305,12 +277,26 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
305
277
|
"error.message": str(e),
|
|
306
278
|
},
|
|
307
279
|
)
|
|
308
|
-
|
|
280
|
+
|
|
281
|
+
# Record metrics for failed execution
|
|
282
|
+
try:
|
|
283
|
+
from golf.metrics import get_metrics_collector
|
|
284
|
+
|
|
285
|
+
metrics_collector = get_metrics_collector()
|
|
286
|
+
metrics_collector.increment_tool_execution(tool_name, "error")
|
|
287
|
+
metrics_collector.increment_error("tool", type(e).__name__)
|
|
288
|
+
except ImportError:
|
|
289
|
+
# Metrics not available, continue without metrics
|
|
290
|
+
pass
|
|
291
|
+
|
|
309
292
|
raise
|
|
310
293
|
|
|
311
294
|
@functools.wraps(func)
|
|
312
295
|
def sync_wrapper(*args, **kwargs):
|
|
313
|
-
|
|
296
|
+
# Record metrics timing
|
|
297
|
+
import time
|
|
298
|
+
|
|
299
|
+
start_time = time.time()
|
|
314
300
|
|
|
315
301
|
# Create a more descriptive span name
|
|
316
302
|
span_name = f"mcp.tool.{tool_name}.execute"
|
|
@@ -354,52 +340,9 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
354
340
|
if session_id_from_baggage:
|
|
355
341
|
span.set_attribute("mcp.session.id", session_id_from_baggage)
|
|
356
342
|
|
|
357
|
-
# Add tool arguments as span attributes (be careful with sensitive data)
|
|
358
|
-
for i, arg in enumerate(args):
|
|
359
|
-
if isinstance(arg, str | int | float | bool) or arg is None:
|
|
360
|
-
span.set_attribute(f"mcp.tool.arg.{i}", str(arg))
|
|
361
|
-
elif hasattr(arg, "__dict__"):
|
|
362
|
-
# For objects, just record the type
|
|
363
|
-
span.set_attribute(f"mcp.tool.arg.{i}.type", type(arg).__name__)
|
|
364
|
-
|
|
365
|
-
# Add named arguments with better naming
|
|
366
|
-
for key, value in kwargs.items():
|
|
367
|
-
if key != "ctx":
|
|
368
|
-
if value is None:
|
|
369
|
-
span.set_attribute(f"mcp.tool.input.{key}", "null")
|
|
370
|
-
elif isinstance(value, str | int | float | bool):
|
|
371
|
-
span.set_attribute(f"mcp.tool.input.{key}", str(value))
|
|
372
|
-
elif isinstance(value, list | tuple):
|
|
373
|
-
span.set_attribute(f"mcp.tool.input.{key}.count", len(value))
|
|
374
|
-
span.set_attribute(f"mcp.tool.input.{key}.type", "array")
|
|
375
|
-
elif isinstance(value, dict):
|
|
376
|
-
span.set_attribute(f"mcp.tool.input.{key}.count", len(value))
|
|
377
|
-
span.set_attribute(f"mcp.tool.input.{key}.type", "object")
|
|
378
|
-
# Only show first few keys to avoid exceeding attribute limits
|
|
379
|
-
if len(value) > 0 and len(value) <= 5:
|
|
380
|
-
keys_list = list(value.keys())[:5]
|
|
381
|
-
# Limit key length and join
|
|
382
|
-
truncated_keys = [
|
|
383
|
-
str(k)[:20] + "..." if len(str(k)) > 20 else str(k)
|
|
384
|
-
for k in keys_list
|
|
385
|
-
]
|
|
386
|
-
span.set_attribute(
|
|
387
|
-
f"mcp.tool.input.{key}.sample_keys",
|
|
388
|
-
",".join(truncated_keys),
|
|
389
|
-
)
|
|
390
|
-
else:
|
|
391
|
-
# For other types, at least record the type
|
|
392
|
-
span.set_attribute(
|
|
393
|
-
f"mcp.tool.input.{key}.type", type(value).__name__
|
|
394
|
-
)
|
|
395
|
-
|
|
396
343
|
# Add event for tool execution start
|
|
397
344
|
span.add_event("tool.execution.started", {"tool.name": tool_name})
|
|
398
345
|
|
|
399
|
-
print(
|
|
400
|
-
f"[TELEMETRY DEBUG] Tool span created: {span_name} (span_id: {span.get_span_context().span_id:016x})"
|
|
401
|
-
)
|
|
402
|
-
|
|
403
346
|
try:
|
|
404
347
|
result = func(*args, **kwargs)
|
|
405
348
|
span.set_status(Status(StatusCode.OK))
|
|
@@ -407,6 +350,19 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
407
350
|
# Add event for successful completion
|
|
408
351
|
span.add_event("tool.execution.completed", {"tool.name": tool_name})
|
|
409
352
|
|
|
353
|
+
# Record metrics for successful execution
|
|
354
|
+
try:
|
|
355
|
+
from golf.metrics import get_metrics_collector
|
|
356
|
+
|
|
357
|
+
metrics_collector = get_metrics_collector()
|
|
358
|
+
metrics_collector.increment_tool_execution(tool_name, "success")
|
|
359
|
+
metrics_collector.record_tool_duration(
|
|
360
|
+
tool_name, time.time() - start_time
|
|
361
|
+
)
|
|
362
|
+
except ImportError:
|
|
363
|
+
# Metrics not available, continue without metrics
|
|
364
|
+
pass
|
|
365
|
+
|
|
410
366
|
# Capture result metadata with better structure
|
|
411
367
|
if result is not None:
|
|
412
368
|
if isinstance(result, str | int | float | bool):
|
|
@@ -437,9 +393,6 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
437
393
|
# For any result, record its type
|
|
438
394
|
span.set_attribute("mcp.tool.result.class", type(result).__name__)
|
|
439
395
|
|
|
440
|
-
print(
|
|
441
|
-
f"[TELEMETRY DEBUG] Tool execution completed successfully: {tool_name}"
|
|
442
|
-
)
|
|
443
396
|
return result
|
|
444
397
|
except Exception as e:
|
|
445
398
|
span.record_exception(e)
|
|
@@ -454,7 +407,18 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
454
407
|
"error.message": str(e),
|
|
455
408
|
},
|
|
456
409
|
)
|
|
457
|
-
|
|
410
|
+
|
|
411
|
+
# Record metrics for failed execution
|
|
412
|
+
try:
|
|
413
|
+
from golf.metrics import get_metrics_collector
|
|
414
|
+
|
|
415
|
+
metrics_collector = get_metrics_collector()
|
|
416
|
+
metrics_collector.increment_tool_execution(tool_name, "error")
|
|
417
|
+
metrics_collector.increment_error("tool", type(e).__name__)
|
|
418
|
+
except ImportError:
|
|
419
|
+
# Metrics not available, continue without metrics
|
|
420
|
+
pass
|
|
421
|
+
|
|
458
422
|
raise
|
|
459
423
|
|
|
460
424
|
# Return appropriate wrapper based on function type
|
|
@@ -701,16 +665,6 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
|
|
|
701
665
|
if session_id_from_baggage:
|
|
702
666
|
span.set_attribute("mcp.session.id", session_id_from_baggage)
|
|
703
667
|
|
|
704
|
-
# Add prompt arguments
|
|
705
|
-
for key, value in kwargs.items():
|
|
706
|
-
if key != "ctx":
|
|
707
|
-
if isinstance(value, str | int | float | bool) or value is None:
|
|
708
|
-
span.set_attribute(f"mcp.prompt.arg.{key}", str(value))
|
|
709
|
-
else:
|
|
710
|
-
span.set_attribute(
|
|
711
|
-
f"mcp.prompt.arg.{key}.type", type(value).__name__
|
|
712
|
-
)
|
|
713
|
-
|
|
714
668
|
# Add event for prompt generation start
|
|
715
669
|
span.add_event("prompt.generation.started", {"prompt.name": prompt_name})
|
|
716
670
|
|
|
@@ -805,16 +759,6 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
|
|
|
805
759
|
if session_id_from_baggage:
|
|
806
760
|
span.set_attribute("mcp.session.id", session_id_from_baggage)
|
|
807
761
|
|
|
808
|
-
# Add prompt arguments
|
|
809
|
-
for key, value in kwargs.items():
|
|
810
|
-
if key != "ctx":
|
|
811
|
-
if isinstance(value, str | int | float | bool) or value is None:
|
|
812
|
-
span.set_attribute(f"mcp.prompt.arg.{key}", str(value))
|
|
813
|
-
else:
|
|
814
|
-
span.set_attribute(
|
|
815
|
-
f"mcp.prompt.arg.{key}.type", type(value).__name__
|
|
816
|
-
)
|
|
817
|
-
|
|
818
762
|
# Add event for prompt generation start
|
|
819
763
|
span.add_event("prompt.generation.started", {"prompt.name": prompt_name})
|
|
820
764
|
|
|
@@ -897,13 +841,64 @@ async def telemetry_lifespan(mcp_instance):
|
|
|
897
841
|
from starlette.requests import Request
|
|
898
842
|
|
|
899
843
|
class SessionTracingMiddleware(BaseHTTPMiddleware):
|
|
844
|
+
def __init__(self, app):
|
|
845
|
+
super().__init__(app)
|
|
846
|
+
# Track seen sessions to count unique sessions
|
|
847
|
+
self.seen_sessions = set()
|
|
848
|
+
# Track session start times for duration calculation
|
|
849
|
+
self.session_start_times = {}
|
|
850
|
+
|
|
900
851
|
async def dispatch(self, request: Request, call_next):
|
|
852
|
+
# Record HTTP request timing
|
|
853
|
+
import time
|
|
854
|
+
|
|
855
|
+
start_time = time.time()
|
|
856
|
+
|
|
901
857
|
# Extract session ID from query params or headers
|
|
902
858
|
session_id = request.query_params.get("session_id")
|
|
903
859
|
if not session_id:
|
|
904
860
|
# Check headers as fallback
|
|
905
861
|
session_id = request.headers.get("x-session-id")
|
|
906
862
|
|
|
863
|
+
# Track session metrics
|
|
864
|
+
if session_id:
|
|
865
|
+
current_time = time.time()
|
|
866
|
+
|
|
867
|
+
# Record new session if we haven't seen this session ID before
|
|
868
|
+
if session_id not in self.seen_sessions:
|
|
869
|
+
self.seen_sessions.add(session_id)
|
|
870
|
+
self.session_start_times[session_id] = current_time
|
|
871
|
+
try:
|
|
872
|
+
from golf.metrics import get_metrics_collector
|
|
873
|
+
|
|
874
|
+
metrics_collector = get_metrics_collector()
|
|
875
|
+
metrics_collector.increment_session()
|
|
876
|
+
except ImportError:
|
|
877
|
+
pass
|
|
878
|
+
else:
|
|
879
|
+
# Update session duration (time since first request)
|
|
880
|
+
if session_id in self.session_start_times:
|
|
881
|
+
duration = (
|
|
882
|
+
current_time - self.session_start_times[session_id]
|
|
883
|
+
)
|
|
884
|
+
try:
|
|
885
|
+
from golf.metrics import get_metrics_collector
|
|
886
|
+
|
|
887
|
+
metrics_collector = get_metrics_collector()
|
|
888
|
+
metrics_collector.record_session_duration(duration)
|
|
889
|
+
except ImportError:
|
|
890
|
+
pass
|
|
891
|
+
|
|
892
|
+
# Clean up old session data periodically
|
|
893
|
+
if len(self.seen_sessions) > 10000:
|
|
894
|
+
# Keep only the most recent 5000 sessions
|
|
895
|
+
recent_sessions = list(self.seen_sessions)[-5000:]
|
|
896
|
+
self.seen_sessions = set(recent_sessions)
|
|
897
|
+
# Clean up start times for removed sessions
|
|
898
|
+
for old_session in list(self.session_start_times.keys()):
|
|
899
|
+
if old_session not in self.seen_sessions:
|
|
900
|
+
self.session_start_times.pop(old_session, None)
|
|
901
|
+
|
|
907
902
|
# Create a descriptive span name based on the request
|
|
908
903
|
method = request.method
|
|
909
904
|
path = request.url.path
|
|
@@ -979,6 +974,29 @@ async def telemetry_lifespan(mcp_instance):
|
|
|
979
974
|
},
|
|
980
975
|
)
|
|
981
976
|
|
|
977
|
+
# Record HTTP request metrics
|
|
978
|
+
try:
|
|
979
|
+
from golf.metrics import get_metrics_collector
|
|
980
|
+
|
|
981
|
+
metrics_collector = get_metrics_collector()
|
|
982
|
+
|
|
983
|
+
# Clean up path for metrics (remove query params, normalize)
|
|
984
|
+
clean_path = path.split("?")[0] # Remove query parameters
|
|
985
|
+
if clean_path.startswith("/"):
|
|
986
|
+
clean_path = (
|
|
987
|
+
clean_path[1:] or "root"
|
|
988
|
+
) # Remove leading slash, handle root
|
|
989
|
+
|
|
990
|
+
metrics_collector.increment_http_request(
|
|
991
|
+
method, response.status_code, clean_path
|
|
992
|
+
)
|
|
993
|
+
metrics_collector.record_http_duration(
|
|
994
|
+
method, clean_path, time.time() - start_time
|
|
995
|
+
)
|
|
996
|
+
except ImportError:
|
|
997
|
+
# Metrics not available, continue without metrics
|
|
998
|
+
pass
|
|
999
|
+
|
|
982
1000
|
return response
|
|
983
1001
|
except Exception as e:
|
|
984
1002
|
span.record_exception(e)
|
|
@@ -994,6 +1012,25 @@ async def telemetry_lifespan(mcp_instance):
|
|
|
994
1012
|
"error.message": str(e),
|
|
995
1013
|
},
|
|
996
1014
|
)
|
|
1015
|
+
|
|
1016
|
+
# Record HTTP error metrics
|
|
1017
|
+
try:
|
|
1018
|
+
from golf.metrics import get_metrics_collector
|
|
1019
|
+
|
|
1020
|
+
metrics_collector = get_metrics_collector()
|
|
1021
|
+
|
|
1022
|
+
# Clean up path for metrics
|
|
1023
|
+
clean_path = path.split("?")[0]
|
|
1024
|
+
if clean_path.startswith("/"):
|
|
1025
|
+
clean_path = clean_path[1:] or "root"
|
|
1026
|
+
|
|
1027
|
+
metrics_collector.increment_http_request(
|
|
1028
|
+
method, 500, clean_path
|
|
1029
|
+
) # Assume 500 for exceptions
|
|
1030
|
+
metrics_collector.increment_error("http", type(e).__name__)
|
|
1031
|
+
except ImportError:
|
|
1032
|
+
pass
|
|
1033
|
+
|
|
997
1034
|
raise
|
|
998
1035
|
finally:
|
|
999
1036
|
if token:
|
|
@@ -1004,16 +1041,13 @@ async def telemetry_lifespan(mcp_instance):
|
|
|
1004
1041
|
app = getattr(mcp_instance, "app", getattr(mcp_instance, "_app", None))
|
|
1005
1042
|
if app and hasattr(app, "add_middleware"):
|
|
1006
1043
|
app.add_middleware(SessionTracingMiddleware)
|
|
1007
|
-
print("[TELEMETRY DEBUG] Added SessionTracingMiddleware to FastMCP app")
|
|
1008
1044
|
|
|
1009
1045
|
# Also try to instrument FastMCP's internal handlers
|
|
1010
1046
|
if hasattr(mcp_instance, "_tool_manager") and hasattr(
|
|
1011
1047
|
mcp_instance._tool_manager, "tools"
|
|
1012
1048
|
):
|
|
1013
|
-
print(
|
|
1014
|
-
f"[TELEMETRY DEBUG] Found {len(mcp_instance._tool_manager.tools)} tools in FastMCP"
|
|
1015
|
-
)
|
|
1016
1049
|
# The tools should already be instrumented when they were registered
|
|
1050
|
+
pass
|
|
1017
1051
|
|
|
1018
1052
|
# Try to patch FastMCP's request handling to ensure context propagation
|
|
1019
1053
|
if hasattr(mcp_instance, "handle_request"):
|
|
@@ -1026,10 +1060,9 @@ async def telemetry_lifespan(mcp_instance):
|
|
|
1026
1060
|
return await original_handle_request(*args, **kwargs)
|
|
1027
1061
|
|
|
1028
1062
|
mcp_instance.handle_request = traced_handle_request
|
|
1029
|
-
print("[TELEMETRY DEBUG] Patched FastMCP handle_request method")
|
|
1030
1063
|
|
|
1031
|
-
except Exception
|
|
1032
|
-
|
|
1064
|
+
except Exception:
|
|
1065
|
+
# Silently continue if middleware setup fails
|
|
1033
1066
|
import traceback
|
|
1034
1067
|
|
|
1035
1068
|
traceback.print_exc()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: golf-mcp
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.18
|
|
4
4
|
Summary: Framework for building MCP servers
|
|
5
5
|
Author-email: Antoni Gmitruk <antoni@golf.dev>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -21,7 +21,7 @@ Description-Content-Type: text/markdown
|
|
|
21
21
|
License-File: LICENSE
|
|
22
22
|
Requires-Dist: typer>=0.15.4
|
|
23
23
|
Requires-Dist: rich>=14.0.0
|
|
24
|
-
Requires-Dist: fastmcp
|
|
24
|
+
Requires-Dist: fastmcp<2.6.0,>=2.0.0
|
|
25
25
|
Requires-Dist: pydantic>=2.11.0
|
|
26
26
|
Requires-Dist: python-dotenv>=1.1.0
|
|
27
27
|
Requires-Dist: black>=24.10.0
|
|
@@ -34,6 +34,8 @@ Requires-Dist: opentelemetry-sdk>=1.33.1; extra == "telemetry"
|
|
|
34
34
|
Requires-Dist: opentelemetry-instrumentation-asgi>=0.40b0; extra == "telemetry"
|
|
35
35
|
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=0.40b0; extra == "telemetry"
|
|
36
36
|
Requires-Dist: wrapt>=1.17.0; extra == "telemetry"
|
|
37
|
+
Provides-Extra: metrics
|
|
38
|
+
Requires-Dist: prometheus-client>=0.22.1; extra == "metrics"
|
|
37
39
|
Dynamic: license-file
|
|
38
40
|
|
|
39
41
|
<div align="center">
|
|
@@ -128,7 +130,7 @@ A Golf project initialized with `golf init` will have a structure similar to thi
|
|
|
128
130
|
|
|
129
131
|
- **`golf.json`**: Configures server name, port, transport, telemetry, and other build settings.
|
|
130
132
|
- **`tools/`**, **`resources/`**, **`prompts/`**: Contain your Python files, each defining a single component. These directories can also contain nested subdirectories to further organize your components (e.g., `tools/payments/charge.py`). The module docstring of each file serves as the component's description.
|
|
131
|
-
- Component IDs are automatically derived from their file path. For example, `tools/hello.py` becomes `hello`, and a nested file like `tools/payments/submit.py` would become `
|
|
133
|
+
- Component IDs are automatically derived from their file path. For example, `tools/hello.py` becomes `hello`, and a nested file like `tools/payments/submit.py` would become `submit_payments` (filename, followed by reversed parent directories under the main category, joined by underscores).
|
|
132
134
|
- **`common.py`** (not shown, but can be placed in subdirectories like `tools/payments/common.py`): Used to share code (clients, models, etc.) among components in the same subdirectory.
|
|
133
135
|
|
|
134
136
|
## Example: Defining a Tool
|
|
@@ -178,6 +180,10 @@ The `golf.json` file is the heart of your Golf project configuration. Here's wha
|
|
|
178
180
|
// - "streamable-http": HTTP with streaming support
|
|
179
181
|
// - "stdio": Standard I/O (for CLI integration)
|
|
180
182
|
|
|
183
|
+
// HTTP Transport Configuration (optional)
|
|
184
|
+
"stateless_http": false, // Make streamable-http transport stateless (new session per request)
|
|
185
|
+
// When true, server restarts won't break existing client connections
|
|
186
|
+
|
|
181
187
|
// Health Check Configuration (optional)
|
|
182
188
|
"health_check_enabled": false, // Enable health check endpoint for Kubernetes/load balancers
|
|
183
189
|
"health_check_path": "/health", // HTTP path for health check endpoint
|
|
@@ -198,6 +204,7 @@ The `golf.json` file is the heart of your Golf project configuration. Here's wha
|
|
|
198
204
|
- `"streamable-http"` provides HTTP streaming for traditional API clients
|
|
199
205
|
- `"stdio"` enables integration with command-line tools and scripts
|
|
200
206
|
- **`host` & `port`**: Control where your server listens. Use `"127.0.0.1"` for local development or `"0.0.0.0"` to accept external connections.
|
|
207
|
+
- **`stateless_http`**: When true, makes the streamable-http transport stateless by creating a new session for each request. This ensures that server restarts don't break existing client connections, making the server truly stateless.
|
|
201
208
|
- **`health_check_enabled`**: When true, enables a health check endpoint for Kubernetes readiness/liveness probes and load balancers
|
|
202
209
|
- **`health_check_path`**: Customizable path for the health check endpoint (defaults to "/health")
|
|
203
210
|
- **`health_check_response`**: Customizable response text for successful health checks (defaults to "OK")
|
|
@@ -1,28 +1,30 @@
|
|
|
1
|
-
golf/__init__.py,sha256=
|
|
1
|
+
golf/__init__.py,sha256=6BiuMUkhwQp6bzUZSF8np8F1NwCltEtK0sPBF__tepU,23
|
|
2
2
|
golf/auth/__init__.py,sha256=Rj4yUngJklk6xrDCrxqLTtoDAMzF1HcTvy_l8wREeao,4103
|
|
3
3
|
golf/auth/api_key.py,sha256=LiIraLiH2v7s3yavidaI6BDlAEfK8XnWF15QmaJn9G4,2378
|
|
4
4
|
golf/auth/helpers.py,sha256=ZogdcHM7J2PN6cL6F6OLZ3gyoUR3dwAFDxOJQ2DW_bc,6526
|
|
5
5
|
golf/auth/oauth.py,sha256=-TYcMA4ULWNQacmUvzek2uQVMJpRT3hXC_d5D2k9c44,31156
|
|
6
6
|
golf/auth/provider.py,sha256=3loeYrkNwIRDvyUkf8gbcCRJSiKiVXgE_rMGCSCr5mk,3802
|
|
7
7
|
golf/cli/__init__.py,sha256=R8Y8KdD2C8gDo24fXGq-fdWWNeaq3MYjrbaSB8Hb-Hg,45
|
|
8
|
-
golf/cli/main.py,sha256=
|
|
8
|
+
golf/cli/main.py,sha256=3qexjKNL8vYg-48ATYcwW4-Wv45l3VxntW-mSqDAbEc,13958
|
|
9
9
|
golf/commands/__init__.py,sha256=GKtIEm7EPQWRgot73RPZPWegwN7Zm0bHtUJbR63FNiw,83
|
|
10
10
|
golf/commands/build.py,sha256=jhdxB5EwwCC_8PgqdXLUKuBpnycjh0gft3_7EuTo6ro,2319
|
|
11
|
-
golf/commands/init.py,sha256=
|
|
11
|
+
golf/commands/init.py,sha256=DUAvGqOUapWdF2cgWPscqHRvyOZDiajR0F0Wkn_jm-k,10355
|
|
12
12
|
golf/commands/run.py,sha256=xsiG5LZw4qVt3cRTTfIoWP4Bf4AxNBBJKx0NNfoua40,2884
|
|
13
13
|
golf/core/__init__.py,sha256=4bKeskJ2fPaZqkz2xQScSa3phRLLrmrczwSL632jv-o,52
|
|
14
|
-
golf/core/builder.py,sha256=
|
|
15
|
-
golf/core/builder_auth.py,sha256=
|
|
14
|
+
golf/core/builder.py,sha256=flsXnlwOTUhXFMpmZwoxWgnK_oLj4zpSyqcKuDnXezw,59526
|
|
15
|
+
golf/core/builder_auth.py,sha256=nGgyMTiRAqaNfh1FSvoFe6oTVq9RgfMf9JoFGAv2_do,14050
|
|
16
|
+
golf/core/builder_metrics.py,sha256=j6Gtgd867o46JbDfSNGNsHt1QtV1XHKUJs1z8r4siQM,8830
|
|
16
17
|
golf/core/builder_telemetry.py,sha256=jobFgRSspLQLuitL4ytk6asSUdTqYcDxGY3sTjkrZd4,2654
|
|
17
|
-
golf/core/config.py,sha256=
|
|
18
|
-
golf/core/parser.py,sha256=
|
|
19
|
-
golf/core/
|
|
18
|
+
golf/core/config.py,sha256=6yPtwzVTJauufEnrfUbxsz69H8jC0Ra427oDaRM0-xE,7397
|
|
19
|
+
golf/core/parser.py,sha256=BQRus1O9zmzSmyavwLVfN8BpYFkbrWUDrgQ7yrYfAKw,42457
|
|
20
|
+
golf/core/platform.py,sha256=Z2yEi6ilmQCLC_uAD_oZdVO0WvkL4tsyw7sx0vHhysI,6440
|
|
21
|
+
golf/core/telemetry.py,sha256=CjZ7urbizaRjyFiVBjfGW8V4PmNCG1_quk3FvbVTcjw,15772
|
|
20
22
|
golf/core/transformer.py,sha256=_0nM42M41oM9zw_XxPVVS6MErdBSw2B5lULC7_UFLfU,5287
|
|
21
23
|
golf/examples/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
24
|
golf/examples/api_key/.env,sha256=15dewTdeJEMAIuzQmh1SFc1zEN6PwryWgAc14IV02lY,90
|
|
23
|
-
golf/examples/api_key/.env.example,sha256=
|
|
24
|
-
golf/examples/api_key/README.md,sha256=
|
|
25
|
-
golf/examples/api_key/golf.json,sha256=
|
|
25
|
+
golf/examples/api_key/.env.example,sha256=fvM_r9xLoiJ_41ZbcDc_EQ48uyxwb7zf0gP_bhQroEY,29
|
|
26
|
+
golf/examples/api_key/README.md,sha256=wRcgwYArwiRcjL6GKEOkPVQal7L37WnREa_rwhrIE10,2892
|
|
27
|
+
golf/examples/api_key/golf.json,sha256=V7atsB5T706bBlkZ5iVBPIhAA6_3Ba3dN6NOyVaV06g,214
|
|
26
28
|
golf/examples/api_key/pre_build.py,sha256=-12HGLV70sQcPhgN51zx25uN2o48JeFvTByzF2ayYp4,471
|
|
27
29
|
golf/examples/api_key/tools/issues/create.py,sha256=51X0uGaisUMfMkmJCskArpefpnxZ4fnT4T_f7HhqQdU,2721
|
|
28
30
|
golf/examples/api_key/tools/issues/list.py,sha256=Egl2o1YVSU8Iigu8XO_iK1-Pn1OnFL3j6LOzaG10rTI,2720
|
|
@@ -30,9 +32,9 @@ golf/examples/api_key/tools/repos/list.py,sha256=9JTRLFzA7GwMYAaLjpOD85uXxv0JI4H
|
|
|
30
32
|
golf/examples/api_key/tools/search/code.py,sha256=3quEIzrl0i_DM1fjszTcrzaAqMC7p0TZY99NL5GceKY,2993
|
|
31
33
|
golf/examples/api_key/tools/users/get.py,sha256=VF-hdUkba2_DqKSC_F7vqtbx0EXAK06wUFBEluKqLIA,2470
|
|
32
34
|
golf/examples/basic/.env,sha256=CqdcvPXopWppurJ3bBjT2dODlKUrLv629BHnOy8zBkM,247
|
|
33
|
-
golf/examples/basic/.env.example,sha256=
|
|
35
|
+
golf/examples/basic/.env.example,sha256=fqMyaQ7pc9KHFcT4vChBO4AEGrbW7PVXOfqbrCO6j9Q,157
|
|
34
36
|
golf/examples/basic/README.md,sha256=-mY3R6AAnkXT9FPkALDrJtdf9IyKDvuqjsrLAMTLRYI,2663
|
|
35
|
-
golf/examples/basic/golf.json,sha256=
|
|
37
|
+
golf/examples/basic/golf.json,sha256=8DiRIXmWolrILmbHzEG2tK-ZVwu1W2qBXRLz75XEjAs,166
|
|
36
38
|
golf/examples/basic/pre_build.py,sha256=AG1N_sKd1UUtHPL7sw1v3YGOcZPQvoa9xcL79S9qjGI,1037
|
|
37
39
|
golf/examples/basic/prompts/welcome.py,sha256=Qs_OsXdyPNw_cDZU7cnG4a0ZMzka6LF7vmPfax4cmKM,790
|
|
38
40
|
golf/examples/basic/resources/current_time.py,sha256=hxhV7vGoiOv-DMXVNSVax_jkPoYR3CR9q5PpWYkdliI,1157
|
|
@@ -45,11 +47,14 @@ golf/examples/basic/tools/hello.py,sha256=s7Soiq9Wn7oKIvA6Hid8UKB14iyR7HZppIbIT4
|
|
|
45
47
|
golf/examples/basic/tools/payments/charge.py,sha256=PIYdFV90hu35H1veLI8ueuYwebzrr5SCTX-x6lxRmU4,1800
|
|
46
48
|
golf/examples/basic/tools/payments/common.py,sha256=hfyuQRIjrQfSqGjyY55W6pZSD5jL6O0geCE0DGx0v10,1302
|
|
47
49
|
golf/examples/basic/tools/payments/refund.py,sha256=Qpl4GWvUw-L06oGQMbBzG8pikfCWfBCFcpkRiDOzmyQ,1607
|
|
50
|
+
golf/metrics/__init__.py,sha256=O91y-hj_E9R06gqV8pDZrzHxOIl-1T415Hj9RvFAp3o,283
|
|
51
|
+
golf/metrics/collector.py,sha256=iyRszP8TAAigyOsUFTGdKN8Xeob36LhUvXW9tntJrbA,7617
|
|
52
|
+
golf/metrics/registry.py,sha256=mXQE4Pwf3PopGYjcUu4eGgPDAe085YWcsvcvWk0ny8Q,310
|
|
48
53
|
golf/telemetry/__init__.py,sha256=ESGCg5HKwTCIfID1e_K7EE0bOWkSzMidlLtdqQgBd0w,396
|
|
49
|
-
golf/telemetry/instrumentation.py,sha256=
|
|
50
|
-
golf_mcp-0.1.
|
|
51
|
-
golf_mcp-0.1.
|
|
52
|
-
golf_mcp-0.1.
|
|
53
|
-
golf_mcp-0.1.
|
|
54
|
-
golf_mcp-0.1.
|
|
55
|
-
golf_mcp-0.1.
|
|
54
|
+
golf/telemetry/instrumentation.py,sha256=Sn7KBJSCf0Kih9ILEdminR0HggpbOlBKToJhnC4PmZE,44934
|
|
55
|
+
golf_mcp-0.1.18.dist-info/licenses/LICENSE,sha256=5_j2f6fTJmvfmUewzElhkpAaXg2grVoxKouOA8ihV6E,11348
|
|
56
|
+
golf_mcp-0.1.18.dist-info/METADATA,sha256=w8GR9q2Ln0UzWTXRFG6XdA1EzbAVUsF7D0tq1nEMqd4,12956
|
|
57
|
+
golf_mcp-0.1.18.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
58
|
+
golf_mcp-0.1.18.dist-info/entry_points.txt,sha256=5y7rHYM8jGpU-nfwdknCm5XsApLulqsnA37MO6BUTYg,43
|
|
59
|
+
golf_mcp-0.1.18.dist-info/top_level.txt,sha256=BQToHcBUufdyhp9ONGMIvPE40jMEtmI20lYaKb4hxOg,5
|
|
60
|
+
golf_mcp-0.1.18.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|