golf-mcp 0.1.9__py3-none-any.whl → 0.1.11__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/auth/__init__.py +1 -1
- golf/auth/helpers.py +112 -2
- golf/cli/main.py +27 -14
- golf/commands/init.py +73 -62
- golf/core/builder.py +35 -12
- golf/core/builder_auth.py +31 -91
- golf/core/telemetry.py +57 -4
- golf/telemetry/instrumentation.py +511 -151
- {golf_mcp-0.1.9.dist-info → golf_mcp-0.1.11.dist-info}/METADATA +1 -1
- {golf_mcp-0.1.9.dist-info → golf_mcp-0.1.11.dist-info}/RECORD +15 -15
- {golf_mcp-0.1.9.dist-info → golf_mcp-0.1.11.dist-info}/WHEEL +1 -1
- {golf_mcp-0.1.9.dist-info → golf_mcp-0.1.11.dist-info}/entry_points.txt +0 -0
- {golf_mcp-0.1.9.dist-info → golf_mcp-0.1.11.dist-info}/licenses/LICENSE +0 -0
- {golf_mcp-0.1.9.dist-info → golf_mcp-0.1.11.dist-info}/top_level.txt +0 -0
|
@@ -20,7 +20,6 @@ T = TypeVar('T')
|
|
|
20
20
|
# Global tracer instance
|
|
21
21
|
_tracer: Optional[trace.Tracer] = None
|
|
22
22
|
_provider: Optional[TracerProvider] = None
|
|
23
|
-
_instrumented_tools = []
|
|
24
23
|
|
|
25
24
|
def init_telemetry(service_name: str = "golf-mcp-server") -> Optional[TracerProvider]:
|
|
26
25
|
"""Initialize OpenTelemetry with environment-based configuration.
|
|
@@ -130,16 +129,6 @@ def get_tracer() -> trace.Tracer:
|
|
|
130
129
|
_tracer = trace.get_tracer("golf.mcp.components", "1.0.0")
|
|
131
130
|
return _tracer
|
|
132
131
|
|
|
133
|
-
def _add_component_attributes(span: Span, component_type: str, component_name: str, **kwargs):
|
|
134
|
-
"""Add standard component attributes to a span."""
|
|
135
|
-
span.set_attribute("mcp.component.type", component_type)
|
|
136
|
-
span.set_attribute("mcp.component.name", component_name)
|
|
137
|
-
|
|
138
|
-
# Add any additional attributes
|
|
139
|
-
for key, value in kwargs.items():
|
|
140
|
-
if value is not None:
|
|
141
|
-
span.set_attribute(f"mcp.component.{key}", str(value))
|
|
142
|
-
|
|
143
132
|
def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
144
133
|
"""Instrument a tool function with OpenTelemetry tracing."""
|
|
145
134
|
global _provider
|
|
@@ -150,31 +139,39 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
150
139
|
|
|
151
140
|
tracer = get_tracer()
|
|
152
141
|
|
|
142
|
+
# Add debug logging
|
|
143
|
+
print(f"[TELEMETRY DEBUG] Instrumenting tool: {tool_name} (function: {func.__name__})")
|
|
144
|
+
|
|
153
145
|
@functools.wraps(func)
|
|
154
146
|
async def async_wrapper(*args, **kwargs):
|
|
155
|
-
|
|
147
|
+
print(f"[TELEMETRY DEBUG] Executing async tool: {tool_name}")
|
|
156
148
|
|
|
157
|
-
#
|
|
158
|
-
|
|
159
|
-
token = context.attach(trace.set_span_in_context(span))
|
|
149
|
+
# Create a more descriptive span name
|
|
150
|
+
span_name = f"mcp.tool.{tool_name}.execute"
|
|
160
151
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
152
|
+
# start_as_current_span automatically uses the current context and manages it
|
|
153
|
+
with tracer.start_as_current_span(span_name) as span:
|
|
154
|
+
# Add comprehensive attributes
|
|
155
|
+
span.set_attribute("mcp.component.type", "tool")
|
|
156
|
+
span.set_attribute("mcp.component.name", tool_name)
|
|
157
|
+
span.set_attribute("mcp.tool.name", tool_name)
|
|
158
|
+
span.set_attribute("mcp.tool.function", func.__name__)
|
|
159
|
+
span.set_attribute("mcp.tool.module", func.__module__ if hasattr(func, '__module__') else "unknown")
|
|
165
160
|
|
|
166
|
-
#
|
|
161
|
+
# Add execution context
|
|
162
|
+
span.set_attribute("mcp.execution.args_count", len(args))
|
|
163
|
+
span.set_attribute("mcp.execution.kwargs_count", len(kwargs))
|
|
164
|
+
span.set_attribute("mcp.execution.async", True)
|
|
165
|
+
|
|
166
|
+
# Extract Context parameter if present
|
|
167
167
|
ctx = kwargs.get('ctx')
|
|
168
168
|
if ctx:
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if 'session' in attr.lower() and not attr.startswith('_'):
|
|
176
|
-
value = getattr(ctx, attr, None)
|
|
177
|
-
if value:
|
|
169
|
+
# Only extract known MCP context attributes
|
|
170
|
+
ctx_attrs = ['request_id', 'session_id', 'client_id', 'user_id', 'tenant_id']
|
|
171
|
+
for attr in ctx_attrs:
|
|
172
|
+
if hasattr(ctx, attr):
|
|
173
|
+
value = getattr(ctx, attr)
|
|
174
|
+
if value is not None:
|
|
178
175
|
span.set_attribute(f"mcp.context.{attr}", str(value))
|
|
179
176
|
|
|
180
177
|
# Also check baggage for session ID
|
|
@@ -185,89 +182,118 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
185
182
|
# Add tool arguments as span attributes (be careful with sensitive data)
|
|
186
183
|
for i, arg in enumerate(args):
|
|
187
184
|
if isinstance(arg, (str, int, float, bool)) or arg is None:
|
|
188
|
-
span.set_attribute(f"tool.arg.{i}", str(arg))
|
|
185
|
+
span.set_attribute(f"mcp.tool.arg.{i}", str(arg))
|
|
189
186
|
elif hasattr(arg, '__dict__'):
|
|
190
187
|
# For objects, just record the type
|
|
191
|
-
span.set_attribute(f"tool.arg.{i}.type", type(arg).__name__)
|
|
188
|
+
span.set_attribute(f"mcp.tool.arg.{i}.type", type(arg).__name__)
|
|
192
189
|
|
|
193
|
-
# Add named arguments
|
|
190
|
+
# Add named arguments with better naming
|
|
194
191
|
for key, value in kwargs.items():
|
|
195
192
|
if key != 'ctx':
|
|
196
193
|
if value is None:
|
|
197
|
-
span.set_attribute(f"tool.
|
|
194
|
+
span.set_attribute(f"mcp.tool.input.{key}", "null")
|
|
198
195
|
elif isinstance(value, (str, int, float, bool)):
|
|
199
|
-
span.set_attribute(f"tool.
|
|
196
|
+
span.set_attribute(f"mcp.tool.input.{key}", str(value))
|
|
200
197
|
elif isinstance(value, (list, tuple)):
|
|
201
|
-
span.set_attribute(f"tool.
|
|
198
|
+
span.set_attribute(f"mcp.tool.input.{key}.count", len(value))
|
|
199
|
+
span.set_attribute(f"mcp.tool.input.{key}.type", "array")
|
|
202
200
|
elif isinstance(value, dict):
|
|
203
|
-
span.set_attribute(f"tool.
|
|
201
|
+
span.set_attribute(f"mcp.tool.input.{key}.count", len(value))
|
|
202
|
+
span.set_attribute(f"mcp.tool.input.{key}.type", "object")
|
|
203
|
+
# Only show first few keys to avoid exceeding attribute limits
|
|
204
|
+
if len(value) > 0 and len(value) <= 5:
|
|
205
|
+
keys_list = list(value.keys())[:5]
|
|
206
|
+
# Limit key length and join
|
|
207
|
+
truncated_keys = [str(k)[:20] + "..." if len(str(k)) > 20 else str(k) for k in keys_list]
|
|
208
|
+
span.set_attribute(f"mcp.tool.input.{key}.sample_keys", ",".join(truncated_keys))
|
|
204
209
|
else:
|
|
205
210
|
# For other types, at least record the type
|
|
206
|
-
span.set_attribute(f"tool.
|
|
211
|
+
span.set_attribute(f"mcp.tool.input.{key}.type", type(value).__name__)
|
|
212
|
+
|
|
213
|
+
# Add event for tool execution start
|
|
214
|
+
span.add_event("tool.execution.started", {
|
|
215
|
+
"tool.name": tool_name
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
print(f"[TELEMETRY DEBUG] Tool span created: {span_name} (span_id: {span.get_span_context().span_id:016x})")
|
|
207
219
|
|
|
208
220
|
try:
|
|
209
221
|
result = await func(*args, **kwargs)
|
|
210
222
|
span.set_status(Status(StatusCode.OK))
|
|
211
223
|
|
|
212
|
-
#
|
|
224
|
+
# Add event for successful completion
|
|
225
|
+
span.add_event("tool.execution.completed", {
|
|
226
|
+
"tool.name": tool_name
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
# Capture result metadata with better structure
|
|
213
230
|
if result is not None:
|
|
214
231
|
if isinstance(result, (str, int, float, bool)):
|
|
215
|
-
span.set_attribute("tool.result", str(result))
|
|
232
|
+
span.set_attribute("mcp.tool.result.value", str(result))
|
|
233
|
+
span.set_attribute("mcp.tool.result.type", type(result).__name__)
|
|
216
234
|
elif isinstance(result, list):
|
|
217
|
-
span.set_attribute("tool.result.count", len(result))
|
|
218
|
-
span.set_attribute("tool.result.type", "
|
|
235
|
+
span.set_attribute("mcp.tool.result.count", len(result))
|
|
236
|
+
span.set_attribute("mcp.tool.result.type", "array")
|
|
219
237
|
elif isinstance(result, dict):
|
|
220
|
-
span.set_attribute("tool.result.
|
|
221
|
-
span.set_attribute("tool.result.type", "
|
|
238
|
+
span.set_attribute("mcp.tool.result.count", len(result))
|
|
239
|
+
span.set_attribute("mcp.tool.result.type", "object")
|
|
240
|
+
# Only show first few keys to avoid exceeding attribute limits
|
|
241
|
+
if len(result) > 0 and len(result) <= 5:
|
|
242
|
+
keys_list = list(result.keys())[:5]
|
|
243
|
+
# Limit key length and join
|
|
244
|
+
truncated_keys = [str(k)[:20] + "..." if len(str(k)) > 20 else str(k) for k in keys_list]
|
|
245
|
+
span.set_attribute("mcp.tool.result.sample_keys", ",".join(truncated_keys))
|
|
222
246
|
elif hasattr(result, '__len__'):
|
|
223
|
-
span.set_attribute("tool.result.length", len(result))
|
|
247
|
+
span.set_attribute("mcp.tool.result.length", len(result))
|
|
224
248
|
|
|
225
249
|
# For any result, record its type
|
|
226
|
-
span.set_attribute("tool.result.class", type(result).__name__)
|
|
250
|
+
span.set_attribute("mcp.tool.result.class", type(result).__name__)
|
|
227
251
|
|
|
252
|
+
print(f"[TELEMETRY DEBUG] Tool execution completed successfully: {tool_name}")
|
|
228
253
|
return result
|
|
229
254
|
except Exception as e:
|
|
230
255
|
span.record_exception(e)
|
|
231
256
|
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
257
|
+
|
|
258
|
+
# Add event for error
|
|
259
|
+
span.add_event("tool.execution.error", {
|
|
260
|
+
"tool.name": tool_name,
|
|
261
|
+
"error.type": type(e).__name__,
|
|
262
|
+
"error.message": str(e)
|
|
263
|
+
})
|
|
264
|
+
print(f"[TELEMETRY DEBUG] Tool execution failed: {tool_name} - {e}")
|
|
232
265
|
raise
|
|
233
|
-
finally:
|
|
234
|
-
# End the span and detach context
|
|
235
|
-
span.end()
|
|
236
|
-
context.detach(token)
|
|
237
|
-
|
|
238
|
-
# Force flush the provider to ensure spans are exported
|
|
239
|
-
global _provider
|
|
240
|
-
if _provider:
|
|
241
|
-
try:
|
|
242
|
-
_provider.force_flush(timeout_millis=1000)
|
|
243
|
-
except Exception as e:
|
|
244
|
-
pass
|
|
245
266
|
|
|
246
267
|
@functools.wraps(func)
|
|
247
268
|
def sync_wrapper(*args, **kwargs):
|
|
248
|
-
|
|
269
|
+
print(f"[TELEMETRY DEBUG] Executing sync tool: {tool_name}")
|
|
249
270
|
|
|
250
|
-
#
|
|
251
|
-
|
|
252
|
-
token = context.attach(trace.set_span_in_context(span))
|
|
271
|
+
# Create a more descriptive span name
|
|
272
|
+
span_name = f"mcp.tool.{tool_name}.execute"
|
|
253
273
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
274
|
+
# start_as_current_span automatically uses the current context and manages it
|
|
275
|
+
with tracer.start_as_current_span(span_name) as span:
|
|
276
|
+
# Add comprehensive attributes
|
|
277
|
+
span.set_attribute("mcp.component.type", "tool")
|
|
278
|
+
span.set_attribute("mcp.component.name", tool_name)
|
|
279
|
+
span.set_attribute("mcp.tool.name", tool_name)
|
|
280
|
+
span.set_attribute("mcp.tool.function", func.__name__)
|
|
281
|
+
span.set_attribute("mcp.tool.module", func.__module__ if hasattr(func, '__module__') else "unknown")
|
|
282
|
+
|
|
283
|
+
# Add execution context
|
|
284
|
+
span.set_attribute("mcp.execution.args_count", len(args))
|
|
285
|
+
span.set_attribute("mcp.execution.kwargs_count", len(kwargs))
|
|
286
|
+
span.set_attribute("mcp.execution.async", False)
|
|
258
287
|
|
|
259
|
-
# Extract Context parameter if present
|
|
288
|
+
# Extract Context parameter if present
|
|
260
289
|
ctx = kwargs.get('ctx')
|
|
261
290
|
if ctx:
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if 'session' in attr.lower() and not attr.startswith('_'):
|
|
269
|
-
value = getattr(ctx, attr, None)
|
|
270
|
-
if value:
|
|
291
|
+
# Only extract known MCP context attributes
|
|
292
|
+
ctx_attrs = ['request_id', 'session_id', 'client_id', 'user_id', 'tenant_id']
|
|
293
|
+
for attr in ctx_attrs:
|
|
294
|
+
if hasattr(ctx, attr):
|
|
295
|
+
value = getattr(ctx, attr)
|
|
296
|
+
if value is not None:
|
|
271
297
|
span.set_attribute(f"mcp.context.{attr}", str(value))
|
|
272
298
|
|
|
273
299
|
# Also check baggage for session ID
|
|
@@ -278,63 +304,87 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
278
304
|
# Add tool arguments as span attributes (be careful with sensitive data)
|
|
279
305
|
for i, arg in enumerate(args):
|
|
280
306
|
if isinstance(arg, (str, int, float, bool)) or arg is None:
|
|
281
|
-
span.set_attribute(f"tool.arg.{i}", str(arg))
|
|
307
|
+
span.set_attribute(f"mcp.tool.arg.{i}", str(arg))
|
|
282
308
|
elif hasattr(arg, '__dict__'):
|
|
283
309
|
# For objects, just record the type
|
|
284
|
-
span.set_attribute(f"tool.arg.{i}.type", type(arg).__name__)
|
|
310
|
+
span.set_attribute(f"mcp.tool.arg.{i}.type", type(arg).__name__)
|
|
285
311
|
|
|
286
|
-
# Add named arguments
|
|
312
|
+
# Add named arguments with better naming
|
|
287
313
|
for key, value in kwargs.items():
|
|
288
314
|
if key != 'ctx':
|
|
289
315
|
if value is None:
|
|
290
|
-
span.set_attribute(f"tool.
|
|
316
|
+
span.set_attribute(f"mcp.tool.input.{key}", "null")
|
|
291
317
|
elif isinstance(value, (str, int, float, bool)):
|
|
292
|
-
span.set_attribute(f"tool.
|
|
318
|
+
span.set_attribute(f"mcp.tool.input.{key}", str(value))
|
|
293
319
|
elif isinstance(value, (list, tuple)):
|
|
294
|
-
span.set_attribute(f"tool.
|
|
320
|
+
span.set_attribute(f"mcp.tool.input.{key}.count", len(value))
|
|
321
|
+
span.set_attribute(f"mcp.tool.input.{key}.type", "array")
|
|
295
322
|
elif isinstance(value, dict):
|
|
296
|
-
span.set_attribute(f"tool.
|
|
323
|
+
span.set_attribute(f"mcp.tool.input.{key}.count", len(value))
|
|
324
|
+
span.set_attribute(f"mcp.tool.input.{key}.type", "object")
|
|
325
|
+
# Only show first few keys to avoid exceeding attribute limits
|
|
326
|
+
if len(value) > 0 and len(value) <= 5:
|
|
327
|
+
keys_list = list(value.keys())[:5]
|
|
328
|
+
# Limit key length and join
|
|
329
|
+
truncated_keys = [str(k)[:20] + "..." if len(str(k)) > 20 else str(k) for k in keys_list]
|
|
330
|
+
span.set_attribute(f"mcp.tool.input.{key}.sample_keys", ",".join(truncated_keys))
|
|
297
331
|
else:
|
|
298
332
|
# For other types, at least record the type
|
|
299
|
-
span.set_attribute(f"tool.
|
|
333
|
+
span.set_attribute(f"mcp.tool.input.{key}.type", type(value).__name__)
|
|
334
|
+
|
|
335
|
+
# Add event for tool execution start
|
|
336
|
+
span.add_event("tool.execution.started", {
|
|
337
|
+
"tool.name": tool_name
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
print(f"[TELEMETRY DEBUG] Tool span created: {span_name} (span_id: {span.get_span_context().span_id:016x})")
|
|
300
341
|
|
|
301
342
|
try:
|
|
302
343
|
result = func(*args, **kwargs)
|
|
303
344
|
span.set_status(Status(StatusCode.OK))
|
|
304
345
|
|
|
305
|
-
#
|
|
346
|
+
# Add event for successful completion
|
|
347
|
+
span.add_event("tool.execution.completed", {
|
|
348
|
+
"tool.name": tool_name
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
# Capture result metadata with better structure
|
|
306
352
|
if result is not None:
|
|
307
353
|
if isinstance(result, (str, int, float, bool)):
|
|
308
|
-
span.set_attribute("tool.result", str(result))
|
|
354
|
+
span.set_attribute("mcp.tool.result.value", str(result))
|
|
355
|
+
span.set_attribute("mcp.tool.result.type", type(result).__name__)
|
|
309
356
|
elif isinstance(result, list):
|
|
310
|
-
span.set_attribute("tool.result.count", len(result))
|
|
311
|
-
span.set_attribute("tool.result.type", "
|
|
357
|
+
span.set_attribute("mcp.tool.result.count", len(result))
|
|
358
|
+
span.set_attribute("mcp.tool.result.type", "array")
|
|
312
359
|
elif isinstance(result, dict):
|
|
313
|
-
span.set_attribute("tool.result.
|
|
314
|
-
span.set_attribute("tool.result.type", "
|
|
360
|
+
span.set_attribute("mcp.tool.result.count", len(result))
|
|
361
|
+
span.set_attribute("mcp.tool.result.type", "object")
|
|
362
|
+
# Only show first few keys to avoid exceeding attribute limits
|
|
363
|
+
if len(result) > 0 and len(result) <= 5:
|
|
364
|
+
keys_list = list(result.keys())[:5]
|
|
365
|
+
# Limit key length and join
|
|
366
|
+
truncated_keys = [str(k)[:20] + "..." if len(str(k)) > 20 else str(k) for k in keys_list]
|
|
367
|
+
span.set_attribute("mcp.tool.result.sample_keys", ",".join(truncated_keys))
|
|
315
368
|
elif hasattr(result, '__len__'):
|
|
316
|
-
span.set_attribute("tool.result.length", len(result))
|
|
369
|
+
span.set_attribute("mcp.tool.result.length", len(result))
|
|
317
370
|
|
|
318
371
|
# For any result, record its type
|
|
319
|
-
span.set_attribute("tool.result.class", type(result).__name__)
|
|
372
|
+
span.set_attribute("mcp.tool.result.class", type(result).__name__)
|
|
320
373
|
|
|
374
|
+
print(f"[TELEMETRY DEBUG] Tool execution completed successfully: {tool_name}")
|
|
321
375
|
return result
|
|
322
376
|
except Exception as e:
|
|
323
377
|
span.record_exception(e)
|
|
324
378
|
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
379
|
+
|
|
380
|
+
# Add event for error
|
|
381
|
+
span.add_event("tool.execution.error", {
|
|
382
|
+
"tool.name": tool_name,
|
|
383
|
+
"error.type": type(e).__name__,
|
|
384
|
+
"error.message": str(e)
|
|
385
|
+
})
|
|
386
|
+
print(f"[TELEMETRY DEBUG] Tool execution failed: {tool_name} - {e}")
|
|
325
387
|
raise
|
|
326
|
-
finally:
|
|
327
|
-
# End the span and detach context
|
|
328
|
-
span.end()
|
|
329
|
-
context.detach(token)
|
|
330
|
-
|
|
331
|
-
# Force flush the provider to ensure spans are exported
|
|
332
|
-
global _provider
|
|
333
|
-
if _provider:
|
|
334
|
-
try:
|
|
335
|
-
_provider.force_flush(timeout_millis=1000)
|
|
336
|
-
except Exception as e:
|
|
337
|
-
pass
|
|
338
388
|
|
|
339
389
|
# Return appropriate wrapper based on function type
|
|
340
390
|
if asyncio.iscoroutinefunction(func):
|
|
@@ -357,54 +407,152 @@ def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[.
|
|
|
357
407
|
|
|
358
408
|
@functools.wraps(func)
|
|
359
409
|
async def async_wrapper(*args, **kwargs):
|
|
360
|
-
|
|
410
|
+
# Create a more descriptive span name
|
|
411
|
+
span_name = f"mcp.resource.{'template' if is_template else 'static'}.read"
|
|
361
412
|
with tracer.start_as_current_span(span_name) as span:
|
|
362
|
-
|
|
363
|
-
|
|
413
|
+
# Add comprehensive attributes
|
|
414
|
+
span.set_attribute("mcp.component.type", "resource")
|
|
415
|
+
span.set_attribute("mcp.component.name", resource_uri)
|
|
416
|
+
span.set_attribute("mcp.resource.uri", resource_uri)
|
|
417
|
+
span.set_attribute("mcp.resource.is_template", is_template)
|
|
418
|
+
span.set_attribute("mcp.resource.function", func.__name__)
|
|
419
|
+
span.set_attribute("mcp.resource.module", func.__module__ if hasattr(func, '__module__') else "unknown")
|
|
420
|
+
span.set_attribute("mcp.execution.async", True)
|
|
364
421
|
|
|
365
422
|
# Extract Context parameter if present
|
|
366
423
|
ctx = kwargs.get('ctx')
|
|
367
|
-
if ctx
|
|
368
|
-
|
|
424
|
+
if ctx:
|
|
425
|
+
# Only extract known MCP context attributes
|
|
426
|
+
ctx_attrs = ['request_id', 'session_id', 'client_id', 'user_id', 'tenant_id']
|
|
427
|
+
for attr in ctx_attrs:
|
|
428
|
+
if hasattr(ctx, attr):
|
|
429
|
+
value = getattr(ctx, attr)
|
|
430
|
+
if value is not None:
|
|
431
|
+
span.set_attribute(f"mcp.context.{attr}", str(value))
|
|
432
|
+
|
|
433
|
+
# Also check baggage for session ID
|
|
434
|
+
session_id_from_baggage = baggage.get_baggage("mcp.session.id")
|
|
435
|
+
if session_id_from_baggage:
|
|
436
|
+
span.set_attribute("mcp.session.id", session_id_from_baggage)
|
|
437
|
+
|
|
438
|
+
# Add event for resource read start
|
|
439
|
+
span.add_event("resource.read.started", {
|
|
440
|
+
"resource.uri": resource_uri
|
|
441
|
+
})
|
|
369
442
|
|
|
370
443
|
try:
|
|
371
444
|
result = await func(*args, **kwargs)
|
|
372
445
|
span.set_status(Status(StatusCode.OK))
|
|
373
446
|
|
|
374
|
-
# Add
|
|
447
|
+
# Add event for successful read
|
|
448
|
+
span.add_event("resource.read.completed", {
|
|
449
|
+
"resource.uri": resource_uri
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
# Add result metadata
|
|
375
453
|
if hasattr(result, '__len__'):
|
|
376
|
-
span.set_attribute("mcp.resource.size", len(result))
|
|
454
|
+
span.set_attribute("mcp.resource.result.size", len(result))
|
|
455
|
+
|
|
456
|
+
# Determine content type if possible
|
|
457
|
+
if isinstance(result, str):
|
|
458
|
+
span.set_attribute("mcp.resource.result.type", "text")
|
|
459
|
+
span.set_attribute("mcp.resource.result.length", len(result))
|
|
460
|
+
elif isinstance(result, bytes):
|
|
461
|
+
span.set_attribute("mcp.resource.result.type", "binary")
|
|
462
|
+
span.set_attribute("mcp.resource.result.size_bytes", len(result))
|
|
463
|
+
elif isinstance(result, dict):
|
|
464
|
+
span.set_attribute("mcp.resource.result.type", "object")
|
|
465
|
+
span.set_attribute("mcp.resource.result.keys_count", len(result))
|
|
466
|
+
elif isinstance(result, list):
|
|
467
|
+
span.set_attribute("mcp.resource.result.type", "array")
|
|
468
|
+
span.set_attribute("mcp.resource.result.items_count", len(result))
|
|
377
469
|
|
|
378
470
|
return result
|
|
379
471
|
except Exception as e:
|
|
380
472
|
span.record_exception(e)
|
|
381
473
|
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
474
|
+
|
|
475
|
+
# Add event for error
|
|
476
|
+
span.add_event("resource.read.error", {
|
|
477
|
+
"resource.uri": resource_uri,
|
|
478
|
+
"error.type": type(e).__name__,
|
|
479
|
+
"error.message": str(e)
|
|
480
|
+
})
|
|
382
481
|
raise
|
|
383
482
|
|
|
384
483
|
@functools.wraps(func)
|
|
385
484
|
def sync_wrapper(*args, **kwargs):
|
|
386
|
-
|
|
485
|
+
# Create a more descriptive span name
|
|
486
|
+
span_name = f"mcp.resource.{'template' if is_template else 'static'}.read"
|
|
387
487
|
with tracer.start_as_current_span(span_name) as span:
|
|
388
|
-
|
|
389
|
-
|
|
488
|
+
# Add comprehensive attributes
|
|
489
|
+
span.set_attribute("mcp.component.type", "resource")
|
|
490
|
+
span.set_attribute("mcp.component.name", resource_uri)
|
|
491
|
+
span.set_attribute("mcp.resource.uri", resource_uri)
|
|
492
|
+
span.set_attribute("mcp.resource.is_template", is_template)
|
|
493
|
+
span.set_attribute("mcp.resource.function", func.__name__)
|
|
494
|
+
span.set_attribute("mcp.resource.module", func.__module__ if hasattr(func, '__module__') else "unknown")
|
|
495
|
+
span.set_attribute("mcp.execution.async", False)
|
|
390
496
|
|
|
391
497
|
# Extract Context parameter if present
|
|
392
498
|
ctx = kwargs.get('ctx')
|
|
393
|
-
if ctx
|
|
394
|
-
|
|
499
|
+
if ctx:
|
|
500
|
+
# Only extract known MCP context attributes
|
|
501
|
+
ctx_attrs = ['request_id', 'session_id', 'client_id', 'user_id', 'tenant_id']
|
|
502
|
+
for attr in ctx_attrs:
|
|
503
|
+
if hasattr(ctx, attr):
|
|
504
|
+
value = getattr(ctx, attr)
|
|
505
|
+
if value is not None:
|
|
506
|
+
span.set_attribute(f"mcp.context.{attr}", str(value))
|
|
507
|
+
|
|
508
|
+
# Also check baggage for session ID
|
|
509
|
+
session_id_from_baggage = baggage.get_baggage("mcp.session.id")
|
|
510
|
+
if session_id_from_baggage:
|
|
511
|
+
span.set_attribute("mcp.session.id", session_id_from_baggage)
|
|
512
|
+
|
|
513
|
+
# Add event for resource read start
|
|
514
|
+
span.add_event("resource.read.started", {
|
|
515
|
+
"resource.uri": resource_uri
|
|
516
|
+
})
|
|
395
517
|
|
|
396
518
|
try:
|
|
397
519
|
result = func(*args, **kwargs)
|
|
398
520
|
span.set_status(Status(StatusCode.OK))
|
|
399
521
|
|
|
400
|
-
# Add
|
|
522
|
+
# Add event for successful read
|
|
523
|
+
span.add_event("resource.read.completed", {
|
|
524
|
+
"resource.uri": resource_uri
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
# Add result metadata
|
|
401
528
|
if hasattr(result, '__len__'):
|
|
402
|
-
span.set_attribute("mcp.resource.size", len(result))
|
|
529
|
+
span.set_attribute("mcp.resource.result.size", len(result))
|
|
530
|
+
|
|
531
|
+
# Determine content type if possible
|
|
532
|
+
if isinstance(result, str):
|
|
533
|
+
span.set_attribute("mcp.resource.result.type", "text")
|
|
534
|
+
span.set_attribute("mcp.resource.result.length", len(result))
|
|
535
|
+
elif isinstance(result, bytes):
|
|
536
|
+
span.set_attribute("mcp.resource.result.type", "binary")
|
|
537
|
+
span.set_attribute("mcp.resource.result.size_bytes", len(result))
|
|
538
|
+
elif isinstance(result, dict):
|
|
539
|
+
span.set_attribute("mcp.resource.result.type", "object")
|
|
540
|
+
span.set_attribute("mcp.resource.result.keys_count", len(result))
|
|
541
|
+
elif isinstance(result, list):
|
|
542
|
+
span.set_attribute("mcp.resource.result.type", "array")
|
|
543
|
+
span.set_attribute("mcp.resource.result.items_count", len(result))
|
|
403
544
|
|
|
404
545
|
return result
|
|
405
546
|
except Exception as e:
|
|
406
547
|
span.record_exception(e)
|
|
407
548
|
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
549
|
+
|
|
550
|
+
# Add event for error
|
|
551
|
+
span.add_event("resource.read.error", {
|
|
552
|
+
"resource.uri": resource_uri,
|
|
553
|
+
"error.type": type(e).__name__,
|
|
554
|
+
"error.message": str(e)
|
|
555
|
+
})
|
|
408
556
|
raise
|
|
409
557
|
|
|
410
558
|
if asyncio.iscoroutinefunction(func):
|
|
@@ -424,50 +572,176 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
|
|
|
424
572
|
|
|
425
573
|
@functools.wraps(func)
|
|
426
574
|
async def async_wrapper(*args, **kwargs):
|
|
427
|
-
|
|
428
|
-
|
|
575
|
+
# Create a more descriptive span name
|
|
576
|
+
span_name = f"mcp.prompt.{prompt_name}.generate"
|
|
577
|
+
with tracer.start_as_current_span(span_name) as span:
|
|
578
|
+
# Add comprehensive attributes
|
|
579
|
+
span.set_attribute("mcp.component.type", "prompt")
|
|
580
|
+
span.set_attribute("mcp.component.name", prompt_name)
|
|
581
|
+
span.set_attribute("mcp.prompt.name", prompt_name)
|
|
582
|
+
span.set_attribute("mcp.prompt.function", func.__name__)
|
|
583
|
+
span.set_attribute("mcp.prompt.module", func.__module__ if hasattr(func, '__module__') else "unknown")
|
|
584
|
+
span.set_attribute("mcp.execution.async", True)
|
|
429
585
|
|
|
430
586
|
# Extract Context parameter if present
|
|
431
587
|
ctx = kwargs.get('ctx')
|
|
432
|
-
if ctx
|
|
433
|
-
|
|
588
|
+
if ctx:
|
|
589
|
+
# Only extract known MCP context attributes
|
|
590
|
+
ctx_attrs = ['request_id', 'session_id', 'client_id', 'user_id', 'tenant_id']
|
|
591
|
+
for attr in ctx_attrs:
|
|
592
|
+
if hasattr(ctx, attr):
|
|
593
|
+
value = getattr(ctx, attr)
|
|
594
|
+
if value is not None:
|
|
595
|
+
span.set_attribute(f"mcp.context.{attr}", str(value))
|
|
596
|
+
|
|
597
|
+
# Also check baggage for session ID
|
|
598
|
+
session_id_from_baggage = baggage.get_baggage("mcp.session.id")
|
|
599
|
+
if session_id_from_baggage:
|
|
600
|
+
span.set_attribute("mcp.session.id", session_id_from_baggage)
|
|
601
|
+
|
|
602
|
+
# Add prompt arguments
|
|
603
|
+
for key, value in kwargs.items():
|
|
604
|
+
if key != 'ctx':
|
|
605
|
+
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
606
|
+
span.set_attribute(f"mcp.prompt.arg.{key}", str(value))
|
|
607
|
+
else:
|
|
608
|
+
span.set_attribute(f"mcp.prompt.arg.{key}.type", type(value).__name__)
|
|
609
|
+
|
|
610
|
+
# Add event for prompt generation start
|
|
611
|
+
span.add_event("prompt.generation.started", {
|
|
612
|
+
"prompt.name": prompt_name
|
|
613
|
+
})
|
|
434
614
|
|
|
435
615
|
try:
|
|
436
616
|
result = await func(*args, **kwargs)
|
|
437
617
|
span.set_status(Status(StatusCode.OK))
|
|
438
618
|
|
|
439
|
-
# Add
|
|
619
|
+
# Add event for successful generation
|
|
620
|
+
span.add_event("prompt.generation.completed", {
|
|
621
|
+
"prompt.name": prompt_name
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
# Add message count and type information
|
|
440
625
|
if isinstance(result, list):
|
|
441
|
-
span.set_attribute("mcp.prompt.message_count", len(result))
|
|
626
|
+
span.set_attribute("mcp.prompt.result.message_count", len(result))
|
|
627
|
+
span.set_attribute("mcp.prompt.result.type", "message_list")
|
|
628
|
+
|
|
629
|
+
# Analyze message types if they have role attributes
|
|
630
|
+
roles = []
|
|
631
|
+
for msg in result:
|
|
632
|
+
if hasattr(msg, 'role'):
|
|
633
|
+
roles.append(msg.role)
|
|
634
|
+
elif isinstance(msg, dict) and 'role' in msg:
|
|
635
|
+
roles.append(msg['role'])
|
|
636
|
+
|
|
637
|
+
if roles:
|
|
638
|
+
unique_roles = list(set(roles))
|
|
639
|
+
span.set_attribute("mcp.prompt.result.roles", ",".join(unique_roles))
|
|
640
|
+
span.set_attribute("mcp.prompt.result.role_counts", str({role: roles.count(role) for role in unique_roles}))
|
|
641
|
+
elif isinstance(result, str):
|
|
642
|
+
span.set_attribute("mcp.prompt.result.type", "string")
|
|
643
|
+
span.set_attribute("mcp.prompt.result.length", len(result))
|
|
644
|
+
else:
|
|
645
|
+
span.set_attribute("mcp.prompt.result.type", type(result).__name__)
|
|
442
646
|
|
|
443
647
|
return result
|
|
444
648
|
except Exception as e:
|
|
445
649
|
span.record_exception(e)
|
|
446
650
|
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
651
|
+
|
|
652
|
+
# Add event for error
|
|
653
|
+
span.add_event("prompt.generation.error", {
|
|
654
|
+
"prompt.name": prompt_name,
|
|
655
|
+
"error.type": type(e).__name__,
|
|
656
|
+
"error.message": str(e)
|
|
657
|
+
})
|
|
447
658
|
raise
|
|
448
659
|
|
|
449
660
|
@functools.wraps(func)
|
|
450
661
|
def sync_wrapper(*args, **kwargs):
|
|
451
|
-
|
|
452
|
-
|
|
662
|
+
# Create a more descriptive span name
|
|
663
|
+
span_name = f"mcp.prompt.{prompt_name}.generate"
|
|
664
|
+
with tracer.start_as_current_span(span_name) as span:
|
|
665
|
+
# Add comprehensive attributes
|
|
666
|
+
span.set_attribute("mcp.component.type", "prompt")
|
|
667
|
+
span.set_attribute("mcp.component.name", prompt_name)
|
|
668
|
+
span.set_attribute("mcp.prompt.name", prompt_name)
|
|
669
|
+
span.set_attribute("mcp.prompt.function", func.__name__)
|
|
670
|
+
span.set_attribute("mcp.prompt.module", func.__module__ if hasattr(func, '__module__') else "unknown")
|
|
671
|
+
span.set_attribute("mcp.execution.async", False)
|
|
453
672
|
|
|
454
673
|
# Extract Context parameter if present
|
|
455
674
|
ctx = kwargs.get('ctx')
|
|
456
|
-
if ctx
|
|
457
|
-
|
|
675
|
+
if ctx:
|
|
676
|
+
# Only extract known MCP context attributes
|
|
677
|
+
ctx_attrs = ['request_id', 'session_id', 'client_id', 'user_id', 'tenant_id']
|
|
678
|
+
for attr in ctx_attrs:
|
|
679
|
+
if hasattr(ctx, attr):
|
|
680
|
+
value = getattr(ctx, attr)
|
|
681
|
+
if value is not None:
|
|
682
|
+
span.set_attribute(f"mcp.context.{attr}", str(value))
|
|
683
|
+
|
|
684
|
+
# Also check baggage for session ID
|
|
685
|
+
session_id_from_baggage = baggage.get_baggage("mcp.session.id")
|
|
686
|
+
if session_id_from_baggage:
|
|
687
|
+
span.set_attribute("mcp.session.id", session_id_from_baggage)
|
|
688
|
+
|
|
689
|
+
# Add prompt arguments
|
|
690
|
+
for key, value in kwargs.items():
|
|
691
|
+
if key != 'ctx':
|
|
692
|
+
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
693
|
+
span.set_attribute(f"mcp.prompt.arg.{key}", str(value))
|
|
694
|
+
else:
|
|
695
|
+
span.set_attribute(f"mcp.prompt.arg.{key}.type", type(value).__name__)
|
|
696
|
+
|
|
697
|
+
# Add event for prompt generation start
|
|
698
|
+
span.add_event("prompt.generation.started", {
|
|
699
|
+
"prompt.name": prompt_name
|
|
700
|
+
})
|
|
458
701
|
|
|
459
702
|
try:
|
|
460
703
|
result = func(*args, **kwargs)
|
|
461
704
|
span.set_status(Status(StatusCode.OK))
|
|
462
705
|
|
|
463
|
-
# Add
|
|
706
|
+
# Add event for successful generation
|
|
707
|
+
span.add_event("prompt.generation.completed", {
|
|
708
|
+
"prompt.name": prompt_name
|
|
709
|
+
})
|
|
710
|
+
|
|
711
|
+
# Add message count and type information
|
|
464
712
|
if isinstance(result, list):
|
|
465
|
-
span.set_attribute("mcp.prompt.message_count", len(result))
|
|
713
|
+
span.set_attribute("mcp.prompt.result.message_count", len(result))
|
|
714
|
+
span.set_attribute("mcp.prompt.result.type", "message_list")
|
|
715
|
+
|
|
716
|
+
# Analyze message types if they have role attributes
|
|
717
|
+
roles = []
|
|
718
|
+
for msg in result:
|
|
719
|
+
if hasattr(msg, 'role'):
|
|
720
|
+
roles.append(msg.role)
|
|
721
|
+
elif isinstance(msg, dict) and 'role' in msg:
|
|
722
|
+
roles.append(msg['role'])
|
|
723
|
+
|
|
724
|
+
if roles:
|
|
725
|
+
unique_roles = list(set(roles))
|
|
726
|
+
span.set_attribute("mcp.prompt.result.roles", ",".join(unique_roles))
|
|
727
|
+
span.set_attribute("mcp.prompt.result.role_counts", str({role: roles.count(role) for role in unique_roles}))
|
|
728
|
+
elif isinstance(result, str):
|
|
729
|
+
span.set_attribute("mcp.prompt.result.type", "string")
|
|
730
|
+
span.set_attribute("mcp.prompt.result.length", len(result))
|
|
731
|
+
else:
|
|
732
|
+
span.set_attribute("mcp.prompt.result.type", type(result).__name__)
|
|
466
733
|
|
|
467
734
|
return result
|
|
468
735
|
except Exception as e:
|
|
469
736
|
span.record_exception(e)
|
|
470
737
|
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
738
|
+
|
|
739
|
+
# Add event for error
|
|
740
|
+
span.add_event("prompt.generation.error", {
|
|
741
|
+
"prompt.name": prompt_name,
|
|
742
|
+
"error.type": type(e).__name__,
|
|
743
|
+
"error.message": str(e)
|
|
744
|
+
})
|
|
471
745
|
raise
|
|
472
746
|
|
|
473
747
|
if asyncio.iscoroutinefunction(func):
|
|
@@ -478,7 +752,7 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
|
|
|
478
752
|
@asynccontextmanager
|
|
479
753
|
async def telemetry_lifespan(mcp_instance):
|
|
480
754
|
"""Simplified lifespan for telemetry initialization and cleanup."""
|
|
481
|
-
global _provider
|
|
755
|
+
global _provider
|
|
482
756
|
|
|
483
757
|
# Initialize telemetry with the server name
|
|
484
758
|
provider = init_telemetry(service_name=mcp_instance.name)
|
|
@@ -496,38 +770,124 @@ async def telemetry_lifespan(mcp_instance):
|
|
|
496
770
|
|
|
497
771
|
class SessionTracingMiddleware(BaseHTTPMiddleware):
|
|
498
772
|
async def dispatch(self, request: Request, call_next):
|
|
499
|
-
# Extract session ID from query params
|
|
773
|
+
# Extract session ID from query params or headers
|
|
500
774
|
session_id = request.query_params.get('session_id')
|
|
501
|
-
if session_id:
|
|
502
|
-
#
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
775
|
+
if not session_id:
|
|
776
|
+
# Check headers as fallback
|
|
777
|
+
session_id = request.headers.get('x-session-id')
|
|
778
|
+
|
|
779
|
+
# Create a descriptive span name based on the request
|
|
780
|
+
method = request.method
|
|
781
|
+
path = request.url.path
|
|
782
|
+
|
|
783
|
+
# Determine the operation type from the path
|
|
784
|
+
operation_type = "unknown"
|
|
785
|
+
if "/mcp" in path:
|
|
786
|
+
operation_type = "mcp.request"
|
|
787
|
+
elif "/sse" in path:
|
|
788
|
+
operation_type = "sse.stream"
|
|
789
|
+
elif "/auth" in path:
|
|
790
|
+
operation_type = "auth"
|
|
791
|
+
|
|
792
|
+
span_name = f"{operation_type}.{method.lower()}"
|
|
793
|
+
|
|
794
|
+
tracer = get_tracer()
|
|
795
|
+
with tracer.start_as_current_span(span_name) as span:
|
|
796
|
+
# Add comprehensive HTTP attributes
|
|
797
|
+
span.set_attribute("http.method", method)
|
|
798
|
+
span.set_attribute("http.url", str(request.url))
|
|
799
|
+
span.set_attribute("http.scheme", request.url.scheme)
|
|
800
|
+
span.set_attribute("http.host", request.url.hostname or "unknown")
|
|
801
|
+
span.set_attribute("http.target", path)
|
|
802
|
+
span.set_attribute("http.user_agent", request.headers.get("user-agent", "unknown"))
|
|
506
803
|
|
|
507
|
-
#
|
|
508
|
-
|
|
509
|
-
with tracer.start_as_current_span(f"http.{request.method} {request.url.path}") as span:
|
|
510
|
-
span.set_attribute("http.method", request.method)
|
|
511
|
-
span.set_attribute("http.url", str(request.url))
|
|
512
|
-
span.set_attribute("http.session_id", session_id)
|
|
804
|
+
# Add session tracking
|
|
805
|
+
if session_id:
|
|
513
806
|
span.set_attribute("mcp.session.id", session_id)
|
|
807
|
+
# Add to baggage for propagation
|
|
808
|
+
ctx = baggage.set_baggage("mcp.session.id", session_id)
|
|
809
|
+
from opentelemetry import context
|
|
810
|
+
token = context.attach(ctx)
|
|
811
|
+
else:
|
|
812
|
+
token = None
|
|
813
|
+
|
|
814
|
+
# Add request size if available
|
|
815
|
+
content_length = request.headers.get("content-length")
|
|
816
|
+
if content_length:
|
|
817
|
+
span.set_attribute("http.request.size", int(content_length))
|
|
818
|
+
|
|
819
|
+
# Add event for request start
|
|
820
|
+
span.add_event("http.request.started", {
|
|
821
|
+
"method": method,
|
|
822
|
+
"path": path
|
|
823
|
+
})
|
|
824
|
+
|
|
825
|
+
try:
|
|
826
|
+
response = await call_next(request)
|
|
827
|
+
|
|
828
|
+
# Add response attributes
|
|
829
|
+
span.set_attribute("http.status_code", response.status_code)
|
|
830
|
+
span.set_attribute("http.status_class", f"{response.status_code // 100}xx")
|
|
514
831
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
span.
|
|
518
|
-
|
|
519
|
-
|
|
832
|
+
# Set span status based on HTTP status
|
|
833
|
+
if response.status_code >= 400:
|
|
834
|
+
span.set_status(Status(StatusCode.ERROR, f"HTTP {response.status_code}"))
|
|
835
|
+
else:
|
|
836
|
+
span.set_status(Status(StatusCode.OK))
|
|
837
|
+
|
|
838
|
+
# Add event for request completion
|
|
839
|
+
span.add_event("http.request.completed", {
|
|
840
|
+
"method": method,
|
|
841
|
+
"path": path,
|
|
842
|
+
"status_code": response.status_code
|
|
843
|
+
})
|
|
844
|
+
|
|
845
|
+
return response
|
|
846
|
+
except Exception as e:
|
|
847
|
+
span.record_exception(e)
|
|
848
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
849
|
+
|
|
850
|
+
# Add event for error
|
|
851
|
+
span.add_event("http.request.error", {
|
|
852
|
+
"method": method,
|
|
853
|
+
"path": path,
|
|
854
|
+
"error.type": type(e).__name__,
|
|
855
|
+
"error.message": str(e)
|
|
856
|
+
})
|
|
857
|
+
raise
|
|
858
|
+
finally:
|
|
859
|
+
if token:
|
|
520
860
|
context.detach(token)
|
|
521
|
-
else:
|
|
522
|
-
return await call_next(request)
|
|
523
861
|
|
|
524
862
|
# Try to add middleware to FastMCP app if it has Starlette app
|
|
525
863
|
if hasattr(mcp_instance, 'app') or hasattr(mcp_instance, '_app'):
|
|
526
864
|
app = getattr(mcp_instance, 'app', getattr(mcp_instance, '_app', None))
|
|
527
865
|
if app and hasattr(app, 'add_middleware'):
|
|
528
866
|
app.add_middleware(SessionTracingMiddleware)
|
|
529
|
-
|
|
530
|
-
|
|
867
|
+
print("[TELEMETRY DEBUG] Added SessionTracingMiddleware to FastMCP app")
|
|
868
|
+
|
|
869
|
+
# Also try to instrument FastMCP's internal handlers
|
|
870
|
+
if hasattr(mcp_instance, '_tool_manager') and hasattr(mcp_instance._tool_manager, 'tools'):
|
|
871
|
+
print(f"[TELEMETRY DEBUG] Found {len(mcp_instance._tool_manager.tools)} tools in FastMCP")
|
|
872
|
+
# The tools should already be instrumented when they were registered
|
|
873
|
+
|
|
874
|
+
# Try to patch FastMCP's request handling to ensure context propagation
|
|
875
|
+
if hasattr(mcp_instance, 'handle_request'):
|
|
876
|
+
original_handle_request = mcp_instance.handle_request
|
|
877
|
+
|
|
878
|
+
async def traced_handle_request(*args, **kwargs):
|
|
879
|
+
tracer = get_tracer()
|
|
880
|
+
with tracer.start_as_current_span("mcp.handle_request") as span:
|
|
881
|
+
span.set_attribute("mcp.request.handler", "handle_request")
|
|
882
|
+
return await original_handle_request(*args, **kwargs)
|
|
883
|
+
|
|
884
|
+
mcp_instance.handle_request = traced_handle_request
|
|
885
|
+
print("[TELEMETRY DEBUG] Patched FastMCP handle_request method")
|
|
886
|
+
|
|
887
|
+
except Exception as e:
|
|
888
|
+
print(f"[TELEMETRY DEBUG] Error setting up telemetry middleware: {e}")
|
|
889
|
+
import traceback
|
|
890
|
+
traceback.print_exc()
|
|
531
891
|
|
|
532
892
|
try:
|
|
533
893
|
# Yield control back to FastMCP
|