golf-mcp 0.1.11__py3-none-any.whl → 0.1.13__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 +38 -26
- golf/auth/api_key.py +16 -23
- golf/auth/helpers.py +68 -54
- golf/auth/oauth.py +340 -277
- golf/auth/provider.py +58 -53
- golf/cli/__init__.py +1 -1
- golf/cli/main.py +209 -87
- golf/commands/__init__.py +1 -1
- golf/commands/build.py +31 -25
- golf/commands/init.py +81 -53
- golf/commands/run.py +30 -15
- golf/core/__init__.py +1 -1
- golf/core/builder.py +493 -362
- golf/core/builder_auth.py +115 -107
- golf/core/builder_telemetry.py +12 -9
- golf/core/config.py +62 -46
- golf/core/parser.py +174 -136
- golf/core/telemetry.py +216 -95
- golf/core/transformer.py +53 -55
- golf/examples/__init__.py +0 -1
- golf/examples/api_key/pre_build.py +2 -2
- golf/examples/api_key/tools/issues/create.py +35 -36
- golf/examples/api_key/tools/issues/list.py +42 -37
- golf/examples/api_key/tools/repos/list.py +50 -29
- golf/examples/api_key/tools/search/code.py +50 -37
- golf/examples/api_key/tools/users/get.py +21 -20
- golf/examples/basic/pre_build.py +4 -4
- golf/examples/basic/prompts/welcome.py +6 -7
- golf/examples/basic/resources/current_time.py +10 -9
- golf/examples/basic/resources/info.py +6 -5
- golf/examples/basic/resources/weather/common.py +16 -10
- golf/examples/basic/resources/weather/current.py +15 -11
- golf/examples/basic/resources/weather/forecast.py +15 -11
- golf/examples/basic/tools/github_user.py +19 -21
- golf/examples/basic/tools/hello.py +10 -6
- golf/examples/basic/tools/payments/charge.py +34 -25
- golf/examples/basic/tools/payments/common.py +8 -6
- golf/examples/basic/tools/payments/refund.py +29 -25
- golf/telemetry/__init__.py +6 -6
- golf/telemetry/instrumentation.py +455 -310
- {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/METADATA +1 -1
- golf_mcp-0.1.13.dist-info/RECORD +55 -0
- golf_mcp-0.1.11.dist-info/RECORD +0 -55
- {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/WHEEL +0 -0
- {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/entry_points.txt +0 -0
- {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/licenses/LICENSE +0 -0
- {golf_mcp-0.1.11.dist-info → golf_mcp-0.1.13.dist-info}/top_level.txt +0 -0
|
@@ -1,43 +1,46 @@
|
|
|
1
1
|
"""Component-level OpenTelemetry instrumentation for Golf-built servers."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
4
|
+
import functools
|
|
3
5
|
import os
|
|
4
6
|
import sys
|
|
5
|
-
import
|
|
6
|
-
from typing import Callable, Optional, TypeVar
|
|
7
|
+
from collections.abc import Callable
|
|
7
8
|
from contextlib import asynccontextmanager
|
|
8
|
-
import
|
|
9
|
+
from typing import TypeVar
|
|
9
10
|
|
|
10
|
-
from opentelemetry import trace
|
|
11
|
+
from opentelemetry import baggage, trace
|
|
12
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
13
|
+
from opentelemetry.sdk.resources import Resource
|
|
11
14
|
from opentelemetry.sdk.trace import TracerProvider
|
|
12
15
|
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
|
|
13
|
-
from opentelemetry.
|
|
14
|
-
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
15
|
-
from opentelemetry.trace import Status, StatusCode, Span
|
|
16
|
-
from opentelemetry import baggage
|
|
16
|
+
from opentelemetry.trace import Status, StatusCode
|
|
17
17
|
|
|
18
|
-
T = TypeVar(
|
|
18
|
+
T = TypeVar("T")
|
|
19
19
|
|
|
20
20
|
# Global tracer instance
|
|
21
|
-
_tracer:
|
|
22
|
-
_provider:
|
|
21
|
+
_tracer: trace.Tracer | None = None
|
|
22
|
+
_provider: TracerProvider | None = None
|
|
23
|
+
|
|
23
24
|
|
|
24
|
-
def init_telemetry(service_name: str = "golf-mcp-server") ->
|
|
25
|
+
def init_telemetry(service_name: str = "golf-mcp-server") -> TracerProvider | None:
|
|
25
26
|
"""Initialize OpenTelemetry with environment-based configuration.
|
|
26
|
-
|
|
27
|
+
|
|
27
28
|
Returns None if required environment variables are not set.
|
|
28
29
|
"""
|
|
29
30
|
global _provider
|
|
30
|
-
|
|
31
|
+
|
|
31
32
|
# Check for required environment variables based on exporter type
|
|
32
33
|
exporter_type = os.environ.get("OTEL_TRACES_EXPORTER", "console").lower()
|
|
33
|
-
|
|
34
|
+
|
|
34
35
|
# For OTLP HTTP exporter, check if endpoint is configured
|
|
35
36
|
if exporter_type == "otlp_http":
|
|
36
37
|
endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT")
|
|
37
38
|
if not endpoint:
|
|
38
|
-
print(
|
|
39
|
+
print(
|
|
40
|
+
"[WARNING] OpenTelemetry tracing is disabled: OTEL_EXPORTER_OTLP_ENDPOINT is not set for OTLP HTTP exporter"
|
|
41
|
+
)
|
|
39
42
|
return None
|
|
40
|
-
|
|
43
|
+
|
|
41
44
|
# Create resource with service information
|
|
42
45
|
resource_attributes = {
|
|
43
46
|
"service.name": os.environ.get("OTEL_SERVICE_NAME", service_name),
|
|
@@ -45,16 +48,18 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> Optional[TracerProv
|
|
|
45
48
|
"service.instance.id": os.environ.get("SERVICE_INSTANCE_ID", "default"),
|
|
46
49
|
}
|
|
47
50
|
resource = Resource.create(resource_attributes)
|
|
48
|
-
|
|
51
|
+
|
|
49
52
|
# Create provider
|
|
50
53
|
provider = TracerProvider(resource=resource)
|
|
51
|
-
|
|
54
|
+
|
|
52
55
|
# Configure exporter based on type
|
|
53
56
|
try:
|
|
54
57
|
if exporter_type == "otlp_http":
|
|
55
|
-
endpoint = os.environ.get(
|
|
58
|
+
endpoint = os.environ.get(
|
|
59
|
+
"OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318/v1/traces"
|
|
60
|
+
)
|
|
56
61
|
headers = os.environ.get("OTEL_EXPORTER_OTLP_HEADERS", "")
|
|
57
|
-
|
|
62
|
+
|
|
58
63
|
# Parse headers if provided
|
|
59
64
|
header_dict = {}
|
|
60
65
|
if headers:
|
|
@@ -62,19 +67,19 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> Optional[TracerProv
|
|
|
62
67
|
if "=" in header:
|
|
63
68
|
key, value = header.split("=", 1)
|
|
64
69
|
header_dict[key.strip()] = value.strip()
|
|
65
|
-
|
|
70
|
+
|
|
66
71
|
exporter = OTLPSpanExporter(
|
|
67
|
-
endpoint=endpoint,
|
|
68
|
-
headers=header_dict if header_dict else None
|
|
72
|
+
endpoint=endpoint, headers=header_dict if header_dict else None
|
|
69
73
|
)
|
|
70
74
|
else:
|
|
71
75
|
# Default to console exporter
|
|
72
76
|
exporter = ConsoleSpanExporter(out=sys.stderr)
|
|
73
|
-
except Exception
|
|
77
|
+
except Exception:
|
|
74
78
|
import traceback
|
|
79
|
+
|
|
75
80
|
traceback.print_exc()
|
|
76
81
|
raise
|
|
77
|
-
|
|
82
|
+
|
|
78
83
|
# Add batch processor for better performance
|
|
79
84
|
try:
|
|
80
85
|
processor = BatchSpanProcessor(
|
|
@@ -82,27 +87,32 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> Optional[TracerProv
|
|
|
82
87
|
max_queue_size=2048,
|
|
83
88
|
schedule_delay_millis=1000, # Export every 1 second instead of default 5 seconds
|
|
84
89
|
max_export_batch_size=512,
|
|
85
|
-
export_timeout_millis=5000
|
|
90
|
+
export_timeout_millis=5000,
|
|
86
91
|
)
|
|
87
92
|
provider.add_span_processor(processor)
|
|
88
|
-
except Exception
|
|
93
|
+
except Exception:
|
|
89
94
|
import traceback
|
|
95
|
+
|
|
90
96
|
traceback.print_exc()
|
|
91
97
|
raise
|
|
92
|
-
|
|
98
|
+
|
|
93
99
|
# Set as global provider
|
|
94
100
|
try:
|
|
95
101
|
# Check if a provider is already set to avoid the warning
|
|
96
102
|
existing_provider = trace.get_tracer_provider()
|
|
97
|
-
if
|
|
103
|
+
if (
|
|
104
|
+
existing_provider is None
|
|
105
|
+
or str(type(existing_provider).__name__) == "ProxyTracerProvider"
|
|
106
|
+
):
|
|
98
107
|
# Only set if no provider exists or it's the default proxy provider
|
|
99
108
|
trace.set_tracer_provider(provider)
|
|
100
109
|
_provider = provider
|
|
101
|
-
except Exception
|
|
110
|
+
except Exception:
|
|
102
111
|
import traceback
|
|
112
|
+
|
|
103
113
|
traceback.print_exc()
|
|
104
114
|
raise
|
|
105
|
-
|
|
115
|
+
|
|
106
116
|
# Create a test span to verify everything is working
|
|
107
117
|
try:
|
|
108
118
|
test_tracer = provider.get_tracer("golf.telemetry.test", "1.0.0")
|
|
@@ -110,45 +120,52 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> Optional[TracerProv
|
|
|
110
120
|
span.set_attribute("test", True)
|
|
111
121
|
span.set_attribute("service.name", service_name)
|
|
112
122
|
span.set_attribute("exporter.type", exporter_type)
|
|
113
|
-
span.set_attribute(
|
|
114
|
-
|
|
123
|
+
span.set_attribute(
|
|
124
|
+
"endpoint", os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", "not set")
|
|
125
|
+
)
|
|
126
|
+
except Exception:
|
|
115
127
|
import traceback
|
|
128
|
+
|
|
116
129
|
traceback.print_exc()
|
|
117
|
-
|
|
130
|
+
|
|
118
131
|
return provider
|
|
119
132
|
|
|
133
|
+
|
|
120
134
|
def get_tracer() -> trace.Tracer:
|
|
121
135
|
"""Get or create the global tracer instance."""
|
|
122
136
|
global _tracer, _provider
|
|
123
|
-
|
|
137
|
+
|
|
124
138
|
# If no provider is set, telemetry is disabled - return no-op tracer
|
|
125
139
|
if _provider is None:
|
|
126
140
|
return trace.get_tracer("golf.mcp.components.noop", "1.0.0")
|
|
127
|
-
|
|
141
|
+
|
|
128
142
|
if _tracer is None:
|
|
129
143
|
_tracer = trace.get_tracer("golf.mcp.components", "1.0.0")
|
|
130
144
|
return _tracer
|
|
131
145
|
|
|
146
|
+
|
|
132
147
|
def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
133
148
|
"""Instrument a tool function with OpenTelemetry tracing."""
|
|
134
149
|
global _provider
|
|
135
|
-
|
|
150
|
+
|
|
136
151
|
# If telemetry is disabled, return the original function
|
|
137
152
|
if _provider is None:
|
|
138
153
|
return func
|
|
139
|
-
|
|
154
|
+
|
|
140
155
|
tracer = get_tracer()
|
|
141
|
-
|
|
156
|
+
|
|
142
157
|
# Add debug logging
|
|
143
|
-
print(
|
|
144
|
-
|
|
158
|
+
print(
|
|
159
|
+
f"[TELEMETRY DEBUG] Instrumenting tool: {tool_name} (function: {func.__name__})"
|
|
160
|
+
)
|
|
161
|
+
|
|
145
162
|
@functools.wraps(func)
|
|
146
163
|
async def async_wrapper(*args, **kwargs):
|
|
147
164
|
print(f"[TELEMETRY DEBUG] Executing async tool: {tool_name}")
|
|
148
|
-
|
|
165
|
+
|
|
149
166
|
# Create a more descriptive span name
|
|
150
167
|
span_name = f"mcp.tool.{tool_name}.execute"
|
|
151
|
-
|
|
168
|
+
|
|
152
169
|
# start_as_current_span automatically uses the current context and manages it
|
|
153
170
|
with tracer.start_as_current_span(span_name) as span:
|
|
154
171
|
# Add comprehensive attributes
|
|
@@ -156,45 +173,54 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
156
173
|
span.set_attribute("mcp.component.name", tool_name)
|
|
157
174
|
span.set_attribute("mcp.tool.name", tool_name)
|
|
158
175
|
span.set_attribute("mcp.tool.function", func.__name__)
|
|
159
|
-
span.set_attribute(
|
|
160
|
-
|
|
176
|
+
span.set_attribute(
|
|
177
|
+
"mcp.tool.module",
|
|
178
|
+
func.__module__ if hasattr(func, "__module__") else "unknown",
|
|
179
|
+
)
|
|
180
|
+
|
|
161
181
|
# Add execution context
|
|
162
182
|
span.set_attribute("mcp.execution.args_count", len(args))
|
|
163
183
|
span.set_attribute("mcp.execution.kwargs_count", len(kwargs))
|
|
164
184
|
span.set_attribute("mcp.execution.async", True)
|
|
165
|
-
|
|
185
|
+
|
|
166
186
|
# Extract Context parameter if present
|
|
167
|
-
ctx = kwargs.get(
|
|
187
|
+
ctx = kwargs.get("ctx")
|
|
168
188
|
if ctx:
|
|
169
189
|
# Only extract known MCP context attributes
|
|
170
|
-
ctx_attrs = [
|
|
190
|
+
ctx_attrs = [
|
|
191
|
+
"request_id",
|
|
192
|
+
"session_id",
|
|
193
|
+
"client_id",
|
|
194
|
+
"user_id",
|
|
195
|
+
"tenant_id",
|
|
196
|
+
]
|
|
171
197
|
for attr in ctx_attrs:
|
|
172
198
|
if hasattr(ctx, attr):
|
|
173
199
|
value = getattr(ctx, attr)
|
|
174
200
|
if value is not None:
|
|
175
201
|
span.set_attribute(f"mcp.context.{attr}", str(value))
|
|
176
|
-
|
|
202
|
+
|
|
177
203
|
# Also check baggage for session ID
|
|
178
204
|
session_id_from_baggage = baggage.get_baggage("mcp.session.id")
|
|
179
205
|
if session_id_from_baggage:
|
|
180
206
|
span.set_attribute("mcp.session.id", session_id_from_baggage)
|
|
181
|
-
|
|
207
|
+
|
|
182
208
|
# Add tool arguments as span attributes (be careful with sensitive data)
|
|
183
209
|
for i, arg in enumerate(args):
|
|
184
|
-
if isinstance(arg,
|
|
210
|
+
if isinstance(arg, str | int | float | bool) or arg is None:
|
|
185
211
|
span.set_attribute(f"mcp.tool.arg.{i}", str(arg))
|
|
186
|
-
elif hasattr(arg,
|
|
212
|
+
elif hasattr(arg, "__dict__"):
|
|
187
213
|
# For objects, just record the type
|
|
188
214
|
span.set_attribute(f"mcp.tool.arg.{i}.type", type(arg).__name__)
|
|
189
|
-
|
|
215
|
+
|
|
190
216
|
# Add named arguments with better naming
|
|
191
217
|
for key, value in kwargs.items():
|
|
192
|
-
if key !=
|
|
218
|
+
if key != "ctx":
|
|
193
219
|
if value is None:
|
|
194
220
|
span.set_attribute(f"mcp.tool.input.{key}", "null")
|
|
195
|
-
elif isinstance(value,
|
|
221
|
+
elif isinstance(value, str | int | float | bool):
|
|
196
222
|
span.set_attribute(f"mcp.tool.input.{key}", str(value))
|
|
197
|
-
elif isinstance(value,
|
|
223
|
+
elif isinstance(value, list | tuple):
|
|
198
224
|
span.set_attribute(f"mcp.tool.input.{key}.count", len(value))
|
|
199
225
|
span.set_attribute(f"mcp.tool.input.{key}.type", "array")
|
|
200
226
|
elif isinstance(value, dict):
|
|
@@ -204,33 +230,41 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
204
230
|
if len(value) > 0 and len(value) <= 5:
|
|
205
231
|
keys_list = list(value.keys())[:5]
|
|
206
232
|
# Limit key length and join
|
|
207
|
-
truncated_keys = [
|
|
208
|
-
|
|
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
|
+
)
|
|
209
241
|
else:
|
|
210
242
|
# For other types, at least record the type
|
|
211
|
-
span.set_attribute(
|
|
212
|
-
|
|
243
|
+
span.set_attribute(
|
|
244
|
+
f"mcp.tool.input.{key}.type", type(value).__name__
|
|
245
|
+
)
|
|
246
|
+
|
|
213
247
|
# Add event for tool execution start
|
|
214
|
-
span.add_event("tool.execution.started", {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
248
|
+
span.add_event("tool.execution.started", {"tool.name": tool_name})
|
|
249
|
+
|
|
250
|
+
print(
|
|
251
|
+
f"[TELEMETRY DEBUG] Tool span created: {span_name} (span_id: {span.get_span_context().span_id:016x})"
|
|
252
|
+
)
|
|
253
|
+
|
|
220
254
|
try:
|
|
221
255
|
result = await func(*args, **kwargs)
|
|
222
256
|
span.set_status(Status(StatusCode.OK))
|
|
223
|
-
|
|
257
|
+
|
|
224
258
|
# Add event for successful completion
|
|
225
|
-
span.add_event("tool.execution.completed", {
|
|
226
|
-
|
|
227
|
-
})
|
|
228
|
-
|
|
259
|
+
span.add_event("tool.execution.completed", {"tool.name": tool_name})
|
|
260
|
+
|
|
229
261
|
# Capture result metadata with better structure
|
|
230
262
|
if result is not None:
|
|
231
|
-
if isinstance(result,
|
|
263
|
+
if isinstance(result, str | int | float | bool):
|
|
232
264
|
span.set_attribute("mcp.tool.result.value", str(result))
|
|
233
|
-
span.set_attribute(
|
|
265
|
+
span.set_attribute(
|
|
266
|
+
"mcp.tool.result.type", type(result).__name__
|
|
267
|
+
)
|
|
234
268
|
elif isinstance(result, list):
|
|
235
269
|
span.set_attribute("mcp.tool.result.count", len(result))
|
|
236
270
|
span.set_attribute("mcp.tool.result.type", "array")
|
|
@@ -241,36 +275,46 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
241
275
|
if len(result) > 0 and len(result) <= 5:
|
|
242
276
|
keys_list = list(result.keys())[:5]
|
|
243
277
|
# Limit key length and join
|
|
244
|
-
truncated_keys = [
|
|
245
|
-
|
|
246
|
-
|
|
278
|
+
truncated_keys = [
|
|
279
|
+
str(k)[:20] + "..." if len(str(k)) > 20 else str(k)
|
|
280
|
+
for k in keys_list
|
|
281
|
+
]
|
|
282
|
+
span.set_attribute(
|
|
283
|
+
"mcp.tool.result.sample_keys", ",".join(truncated_keys)
|
|
284
|
+
)
|
|
285
|
+
elif hasattr(result, "__len__"):
|
|
247
286
|
span.set_attribute("mcp.tool.result.length", len(result))
|
|
248
|
-
|
|
287
|
+
|
|
249
288
|
# For any result, record its type
|
|
250
289
|
span.set_attribute("mcp.tool.result.class", type(result).__name__)
|
|
251
|
-
|
|
252
|
-
print(
|
|
290
|
+
|
|
291
|
+
print(
|
|
292
|
+
f"[TELEMETRY DEBUG] Tool execution completed successfully: {tool_name}"
|
|
293
|
+
)
|
|
253
294
|
return result
|
|
254
295
|
except Exception as e:
|
|
255
296
|
span.record_exception(e)
|
|
256
297
|
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
257
|
-
|
|
298
|
+
|
|
258
299
|
# Add event for error
|
|
259
|
-
span.add_event(
|
|
260
|
-
"tool.
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
300
|
+
span.add_event(
|
|
301
|
+
"tool.execution.error",
|
|
302
|
+
{
|
|
303
|
+
"tool.name": tool_name,
|
|
304
|
+
"error.type": type(e).__name__,
|
|
305
|
+
"error.message": str(e),
|
|
306
|
+
},
|
|
307
|
+
)
|
|
264
308
|
print(f"[TELEMETRY DEBUG] Tool execution failed: {tool_name} - {e}")
|
|
265
309
|
raise
|
|
266
|
-
|
|
310
|
+
|
|
267
311
|
@functools.wraps(func)
|
|
268
312
|
def sync_wrapper(*args, **kwargs):
|
|
269
313
|
print(f"[TELEMETRY DEBUG] Executing sync tool: {tool_name}")
|
|
270
|
-
|
|
314
|
+
|
|
271
315
|
# Create a more descriptive span name
|
|
272
316
|
span_name = f"mcp.tool.{tool_name}.execute"
|
|
273
|
-
|
|
317
|
+
|
|
274
318
|
# start_as_current_span automatically uses the current context and manages it
|
|
275
319
|
with tracer.start_as_current_span(span_name) as span:
|
|
276
320
|
# Add comprehensive attributes
|
|
@@ -278,45 +322,54 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
278
322
|
span.set_attribute("mcp.component.name", tool_name)
|
|
279
323
|
span.set_attribute("mcp.tool.name", tool_name)
|
|
280
324
|
span.set_attribute("mcp.tool.function", func.__name__)
|
|
281
|
-
span.set_attribute(
|
|
282
|
-
|
|
325
|
+
span.set_attribute(
|
|
326
|
+
"mcp.tool.module",
|
|
327
|
+
func.__module__ if hasattr(func, "__module__") else "unknown",
|
|
328
|
+
)
|
|
329
|
+
|
|
283
330
|
# Add execution context
|
|
284
331
|
span.set_attribute("mcp.execution.args_count", len(args))
|
|
285
332
|
span.set_attribute("mcp.execution.kwargs_count", len(kwargs))
|
|
286
333
|
span.set_attribute("mcp.execution.async", False)
|
|
287
|
-
|
|
334
|
+
|
|
288
335
|
# Extract Context parameter if present
|
|
289
|
-
ctx = kwargs.get(
|
|
336
|
+
ctx = kwargs.get("ctx")
|
|
290
337
|
if ctx:
|
|
291
338
|
# Only extract known MCP context attributes
|
|
292
|
-
ctx_attrs = [
|
|
339
|
+
ctx_attrs = [
|
|
340
|
+
"request_id",
|
|
341
|
+
"session_id",
|
|
342
|
+
"client_id",
|
|
343
|
+
"user_id",
|
|
344
|
+
"tenant_id",
|
|
345
|
+
]
|
|
293
346
|
for attr in ctx_attrs:
|
|
294
347
|
if hasattr(ctx, attr):
|
|
295
348
|
value = getattr(ctx, attr)
|
|
296
349
|
if value is not None:
|
|
297
350
|
span.set_attribute(f"mcp.context.{attr}", str(value))
|
|
298
|
-
|
|
351
|
+
|
|
299
352
|
# Also check baggage for session ID
|
|
300
353
|
session_id_from_baggage = baggage.get_baggage("mcp.session.id")
|
|
301
354
|
if session_id_from_baggage:
|
|
302
355
|
span.set_attribute("mcp.session.id", session_id_from_baggage)
|
|
303
|
-
|
|
356
|
+
|
|
304
357
|
# Add tool arguments as span attributes (be careful with sensitive data)
|
|
305
358
|
for i, arg in enumerate(args):
|
|
306
|
-
if isinstance(arg,
|
|
359
|
+
if isinstance(arg, str | int | float | bool) or arg is None:
|
|
307
360
|
span.set_attribute(f"mcp.tool.arg.{i}", str(arg))
|
|
308
|
-
elif hasattr(arg,
|
|
361
|
+
elif hasattr(arg, "__dict__"):
|
|
309
362
|
# For objects, just record the type
|
|
310
363
|
span.set_attribute(f"mcp.tool.arg.{i}.type", type(arg).__name__)
|
|
311
|
-
|
|
364
|
+
|
|
312
365
|
# Add named arguments with better naming
|
|
313
366
|
for key, value in kwargs.items():
|
|
314
|
-
if key !=
|
|
367
|
+
if key != "ctx":
|
|
315
368
|
if value is None:
|
|
316
369
|
span.set_attribute(f"mcp.tool.input.{key}", "null")
|
|
317
|
-
elif isinstance(value,
|
|
370
|
+
elif isinstance(value, str | int | float | bool):
|
|
318
371
|
span.set_attribute(f"mcp.tool.input.{key}", str(value))
|
|
319
|
-
elif isinstance(value,
|
|
372
|
+
elif isinstance(value, list | tuple):
|
|
320
373
|
span.set_attribute(f"mcp.tool.input.{key}.count", len(value))
|
|
321
374
|
span.set_attribute(f"mcp.tool.input.{key}.type", "array")
|
|
322
375
|
elif isinstance(value, dict):
|
|
@@ -326,33 +379,41 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
326
379
|
if len(value) > 0 and len(value) <= 5:
|
|
327
380
|
keys_list = list(value.keys())[:5]
|
|
328
381
|
# Limit key length and join
|
|
329
|
-
truncated_keys = [
|
|
330
|
-
|
|
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
|
+
)
|
|
331
390
|
else:
|
|
332
391
|
# For other types, at least record the type
|
|
333
|
-
span.set_attribute(
|
|
334
|
-
|
|
392
|
+
span.set_attribute(
|
|
393
|
+
f"mcp.tool.input.{key}.type", type(value).__name__
|
|
394
|
+
)
|
|
395
|
+
|
|
335
396
|
# Add event for tool execution start
|
|
336
|
-
span.add_event("tool.execution.started", {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
397
|
+
span.add_event("tool.execution.started", {"tool.name": tool_name})
|
|
398
|
+
|
|
399
|
+
print(
|
|
400
|
+
f"[TELEMETRY DEBUG] Tool span created: {span_name} (span_id: {span.get_span_context().span_id:016x})"
|
|
401
|
+
)
|
|
402
|
+
|
|
342
403
|
try:
|
|
343
404
|
result = func(*args, **kwargs)
|
|
344
405
|
span.set_status(Status(StatusCode.OK))
|
|
345
|
-
|
|
406
|
+
|
|
346
407
|
# Add event for successful completion
|
|
347
|
-
span.add_event("tool.execution.completed", {
|
|
348
|
-
|
|
349
|
-
})
|
|
350
|
-
|
|
408
|
+
span.add_event("tool.execution.completed", {"tool.name": tool_name})
|
|
409
|
+
|
|
351
410
|
# Capture result metadata with better structure
|
|
352
411
|
if result is not None:
|
|
353
|
-
if isinstance(result,
|
|
412
|
+
if isinstance(result, str | int | float | bool):
|
|
354
413
|
span.set_attribute("mcp.tool.result.value", str(result))
|
|
355
|
-
span.set_attribute(
|
|
414
|
+
span.set_attribute(
|
|
415
|
+
"mcp.tool.result.type", type(result).__name__
|
|
416
|
+
)
|
|
356
417
|
elif isinstance(result, list):
|
|
357
418
|
span.set_attribute("mcp.tool.result.count", len(result))
|
|
358
419
|
span.set_attribute("mcp.tool.result.type", "array")
|
|
@@ -363,48 +424,59 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
363
424
|
if len(result) > 0 and len(result) <= 5:
|
|
364
425
|
keys_list = list(result.keys())[:5]
|
|
365
426
|
# Limit key length and join
|
|
366
|
-
truncated_keys = [
|
|
367
|
-
|
|
368
|
-
|
|
427
|
+
truncated_keys = [
|
|
428
|
+
str(k)[:20] + "..." if len(str(k)) > 20 else str(k)
|
|
429
|
+
for k in keys_list
|
|
430
|
+
]
|
|
431
|
+
span.set_attribute(
|
|
432
|
+
"mcp.tool.result.sample_keys", ",".join(truncated_keys)
|
|
433
|
+
)
|
|
434
|
+
elif hasattr(result, "__len__"):
|
|
369
435
|
span.set_attribute("mcp.tool.result.length", len(result))
|
|
370
|
-
|
|
436
|
+
|
|
371
437
|
# For any result, record its type
|
|
372
438
|
span.set_attribute("mcp.tool.result.class", type(result).__name__)
|
|
373
|
-
|
|
374
|
-
print(
|
|
439
|
+
|
|
440
|
+
print(
|
|
441
|
+
f"[TELEMETRY DEBUG] Tool execution completed successfully: {tool_name}"
|
|
442
|
+
)
|
|
375
443
|
return result
|
|
376
444
|
except Exception as e:
|
|
377
445
|
span.record_exception(e)
|
|
378
446
|
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
379
|
-
|
|
447
|
+
|
|
380
448
|
# Add event for error
|
|
381
|
-
span.add_event(
|
|
382
|
-
"tool.
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
449
|
+
span.add_event(
|
|
450
|
+
"tool.execution.error",
|
|
451
|
+
{
|
|
452
|
+
"tool.name": tool_name,
|
|
453
|
+
"error.type": type(e).__name__,
|
|
454
|
+
"error.message": str(e),
|
|
455
|
+
},
|
|
456
|
+
)
|
|
386
457
|
print(f"[TELEMETRY DEBUG] Tool execution failed: {tool_name} - {e}")
|
|
387
458
|
raise
|
|
388
|
-
|
|
459
|
+
|
|
389
460
|
# Return appropriate wrapper based on function type
|
|
390
461
|
if asyncio.iscoroutinefunction(func):
|
|
391
462
|
return async_wrapper
|
|
392
463
|
else:
|
|
393
464
|
return sync_wrapper
|
|
394
465
|
|
|
466
|
+
|
|
395
467
|
def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[..., T]:
|
|
396
468
|
"""Instrument a resource function with OpenTelemetry tracing."""
|
|
397
469
|
global _provider
|
|
398
|
-
|
|
470
|
+
|
|
399
471
|
# If telemetry is disabled, return the original function
|
|
400
472
|
if _provider is None:
|
|
401
473
|
return func
|
|
402
|
-
|
|
474
|
+
|
|
403
475
|
tracer = get_tracer()
|
|
404
|
-
|
|
476
|
+
|
|
405
477
|
# Determine if this is a template based on URI pattern
|
|
406
|
-
is_template =
|
|
407
|
-
|
|
478
|
+
is_template = "{" in resource_uri
|
|
479
|
+
|
|
408
480
|
@functools.wraps(func)
|
|
409
481
|
async def async_wrapper(*args, **kwargs):
|
|
410
482
|
# Create a more descriptive span name
|
|
@@ -416,43 +488,50 @@ def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[.
|
|
|
416
488
|
span.set_attribute("mcp.resource.uri", resource_uri)
|
|
417
489
|
span.set_attribute("mcp.resource.is_template", is_template)
|
|
418
490
|
span.set_attribute("mcp.resource.function", func.__name__)
|
|
419
|
-
span.set_attribute(
|
|
491
|
+
span.set_attribute(
|
|
492
|
+
"mcp.resource.module",
|
|
493
|
+
func.__module__ if hasattr(func, "__module__") else "unknown",
|
|
494
|
+
)
|
|
420
495
|
span.set_attribute("mcp.execution.async", True)
|
|
421
|
-
|
|
496
|
+
|
|
422
497
|
# Extract Context parameter if present
|
|
423
|
-
ctx = kwargs.get(
|
|
498
|
+
ctx = kwargs.get("ctx")
|
|
424
499
|
if ctx:
|
|
425
500
|
# Only extract known MCP context attributes
|
|
426
|
-
ctx_attrs = [
|
|
501
|
+
ctx_attrs = [
|
|
502
|
+
"request_id",
|
|
503
|
+
"session_id",
|
|
504
|
+
"client_id",
|
|
505
|
+
"user_id",
|
|
506
|
+
"tenant_id",
|
|
507
|
+
]
|
|
427
508
|
for attr in ctx_attrs:
|
|
428
509
|
if hasattr(ctx, attr):
|
|
429
510
|
value = getattr(ctx, attr)
|
|
430
511
|
if value is not None:
|
|
431
512
|
span.set_attribute(f"mcp.context.{attr}", str(value))
|
|
432
|
-
|
|
513
|
+
|
|
433
514
|
# Also check baggage for session ID
|
|
434
515
|
session_id_from_baggage = baggage.get_baggage("mcp.session.id")
|
|
435
516
|
if session_id_from_baggage:
|
|
436
517
|
span.set_attribute("mcp.session.id", session_id_from_baggage)
|
|
437
|
-
|
|
518
|
+
|
|
438
519
|
# Add event for resource read start
|
|
439
|
-
span.add_event("resource.read.started", {
|
|
440
|
-
|
|
441
|
-
})
|
|
442
|
-
|
|
520
|
+
span.add_event("resource.read.started", {"resource.uri": resource_uri})
|
|
521
|
+
|
|
443
522
|
try:
|
|
444
523
|
result = await func(*args, **kwargs)
|
|
445
524
|
span.set_status(Status(StatusCode.OK))
|
|
446
|
-
|
|
525
|
+
|
|
447
526
|
# Add event for successful read
|
|
448
|
-
span.add_event(
|
|
449
|
-
"resource.uri": resource_uri
|
|
450
|
-
|
|
451
|
-
|
|
527
|
+
span.add_event(
|
|
528
|
+
"resource.read.completed", {"resource.uri": resource_uri}
|
|
529
|
+
)
|
|
530
|
+
|
|
452
531
|
# Add result metadata
|
|
453
|
-
if hasattr(result,
|
|
532
|
+
if hasattr(result, "__len__"):
|
|
454
533
|
span.set_attribute("mcp.resource.result.size", len(result))
|
|
455
|
-
|
|
534
|
+
|
|
456
535
|
# Determine content type if possible
|
|
457
536
|
if isinstance(result, str):
|
|
458
537
|
span.set_attribute("mcp.resource.result.type", "text")
|
|
@@ -466,20 +545,23 @@ def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[.
|
|
|
466
545
|
elif isinstance(result, list):
|
|
467
546
|
span.set_attribute("mcp.resource.result.type", "array")
|
|
468
547
|
span.set_attribute("mcp.resource.result.items_count", len(result))
|
|
469
|
-
|
|
548
|
+
|
|
470
549
|
return result
|
|
471
550
|
except Exception as e:
|
|
472
551
|
span.record_exception(e)
|
|
473
552
|
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
474
|
-
|
|
553
|
+
|
|
475
554
|
# Add event for error
|
|
476
|
-
span.add_event(
|
|
477
|
-
"resource.
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
555
|
+
span.add_event(
|
|
556
|
+
"resource.read.error",
|
|
557
|
+
{
|
|
558
|
+
"resource.uri": resource_uri,
|
|
559
|
+
"error.type": type(e).__name__,
|
|
560
|
+
"error.message": str(e),
|
|
561
|
+
},
|
|
562
|
+
)
|
|
481
563
|
raise
|
|
482
|
-
|
|
564
|
+
|
|
483
565
|
@functools.wraps(func)
|
|
484
566
|
def sync_wrapper(*args, **kwargs):
|
|
485
567
|
# Create a more descriptive span name
|
|
@@ -491,43 +573,50 @@ def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[.
|
|
|
491
573
|
span.set_attribute("mcp.resource.uri", resource_uri)
|
|
492
574
|
span.set_attribute("mcp.resource.is_template", is_template)
|
|
493
575
|
span.set_attribute("mcp.resource.function", func.__name__)
|
|
494
|
-
span.set_attribute(
|
|
576
|
+
span.set_attribute(
|
|
577
|
+
"mcp.resource.module",
|
|
578
|
+
func.__module__ if hasattr(func, "__module__") else "unknown",
|
|
579
|
+
)
|
|
495
580
|
span.set_attribute("mcp.execution.async", False)
|
|
496
|
-
|
|
581
|
+
|
|
497
582
|
# Extract Context parameter if present
|
|
498
|
-
ctx = kwargs.get(
|
|
583
|
+
ctx = kwargs.get("ctx")
|
|
499
584
|
if ctx:
|
|
500
585
|
# Only extract known MCP context attributes
|
|
501
|
-
ctx_attrs = [
|
|
586
|
+
ctx_attrs = [
|
|
587
|
+
"request_id",
|
|
588
|
+
"session_id",
|
|
589
|
+
"client_id",
|
|
590
|
+
"user_id",
|
|
591
|
+
"tenant_id",
|
|
592
|
+
]
|
|
502
593
|
for attr in ctx_attrs:
|
|
503
594
|
if hasattr(ctx, attr):
|
|
504
595
|
value = getattr(ctx, attr)
|
|
505
596
|
if value is not None:
|
|
506
597
|
span.set_attribute(f"mcp.context.{attr}", str(value))
|
|
507
|
-
|
|
598
|
+
|
|
508
599
|
# Also check baggage for session ID
|
|
509
600
|
session_id_from_baggage = baggage.get_baggage("mcp.session.id")
|
|
510
601
|
if session_id_from_baggage:
|
|
511
602
|
span.set_attribute("mcp.session.id", session_id_from_baggage)
|
|
512
|
-
|
|
603
|
+
|
|
513
604
|
# Add event for resource read start
|
|
514
|
-
span.add_event("resource.read.started", {
|
|
515
|
-
|
|
516
|
-
})
|
|
517
|
-
|
|
605
|
+
span.add_event("resource.read.started", {"resource.uri": resource_uri})
|
|
606
|
+
|
|
518
607
|
try:
|
|
519
608
|
result = func(*args, **kwargs)
|
|
520
609
|
span.set_status(Status(StatusCode.OK))
|
|
521
|
-
|
|
610
|
+
|
|
522
611
|
# Add event for successful read
|
|
523
|
-
span.add_event(
|
|
524
|
-
"resource.uri": resource_uri
|
|
525
|
-
|
|
526
|
-
|
|
612
|
+
span.add_event(
|
|
613
|
+
"resource.read.completed", {"resource.uri": resource_uri}
|
|
614
|
+
)
|
|
615
|
+
|
|
527
616
|
# Add result metadata
|
|
528
|
-
if hasattr(result,
|
|
617
|
+
if hasattr(result, "__len__"):
|
|
529
618
|
span.set_attribute("mcp.resource.result.size", len(result))
|
|
530
|
-
|
|
619
|
+
|
|
531
620
|
# Determine content type if possible
|
|
532
621
|
if isinstance(result, str):
|
|
533
622
|
span.set_attribute("mcp.resource.result.type", "text")
|
|
@@ -541,35 +630,39 @@ def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[.
|
|
|
541
630
|
elif isinstance(result, list):
|
|
542
631
|
span.set_attribute("mcp.resource.result.type", "array")
|
|
543
632
|
span.set_attribute("mcp.resource.result.items_count", len(result))
|
|
544
|
-
|
|
633
|
+
|
|
545
634
|
return result
|
|
546
635
|
except Exception as e:
|
|
547
636
|
span.record_exception(e)
|
|
548
637
|
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
549
|
-
|
|
638
|
+
|
|
550
639
|
# Add event for error
|
|
551
|
-
span.add_event(
|
|
552
|
-
"resource.
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
640
|
+
span.add_event(
|
|
641
|
+
"resource.read.error",
|
|
642
|
+
{
|
|
643
|
+
"resource.uri": resource_uri,
|
|
644
|
+
"error.type": type(e).__name__,
|
|
645
|
+
"error.message": str(e),
|
|
646
|
+
},
|
|
647
|
+
)
|
|
556
648
|
raise
|
|
557
|
-
|
|
649
|
+
|
|
558
650
|
if asyncio.iscoroutinefunction(func):
|
|
559
651
|
return async_wrapper
|
|
560
652
|
else:
|
|
561
653
|
return sync_wrapper
|
|
562
654
|
|
|
655
|
+
|
|
563
656
|
def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[..., T]:
|
|
564
657
|
"""Instrument a prompt function with OpenTelemetry tracing."""
|
|
565
658
|
global _provider
|
|
566
|
-
|
|
659
|
+
|
|
567
660
|
# If telemetry is disabled, return the original function
|
|
568
661
|
if _provider is None:
|
|
569
662
|
return func
|
|
570
|
-
|
|
663
|
+
|
|
571
664
|
tracer = get_tracer()
|
|
572
|
-
|
|
665
|
+
|
|
573
666
|
@functools.wraps(func)
|
|
574
667
|
async def async_wrapper(*args, **kwargs):
|
|
575
668
|
# Create a more descriptive span name
|
|
@@ -580,83 +673,100 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
|
|
|
580
673
|
span.set_attribute("mcp.component.name", prompt_name)
|
|
581
674
|
span.set_attribute("mcp.prompt.name", prompt_name)
|
|
582
675
|
span.set_attribute("mcp.prompt.function", func.__name__)
|
|
583
|
-
span.set_attribute(
|
|
676
|
+
span.set_attribute(
|
|
677
|
+
"mcp.prompt.module",
|
|
678
|
+
func.__module__ if hasattr(func, "__module__") else "unknown",
|
|
679
|
+
)
|
|
584
680
|
span.set_attribute("mcp.execution.async", True)
|
|
585
|
-
|
|
681
|
+
|
|
586
682
|
# Extract Context parameter if present
|
|
587
|
-
ctx = kwargs.get(
|
|
683
|
+
ctx = kwargs.get("ctx")
|
|
588
684
|
if ctx:
|
|
589
685
|
# Only extract known MCP context attributes
|
|
590
|
-
ctx_attrs = [
|
|
686
|
+
ctx_attrs = [
|
|
687
|
+
"request_id",
|
|
688
|
+
"session_id",
|
|
689
|
+
"client_id",
|
|
690
|
+
"user_id",
|
|
691
|
+
"tenant_id",
|
|
692
|
+
]
|
|
591
693
|
for attr in ctx_attrs:
|
|
592
694
|
if hasattr(ctx, attr):
|
|
593
695
|
value = getattr(ctx, attr)
|
|
594
696
|
if value is not None:
|
|
595
697
|
span.set_attribute(f"mcp.context.{attr}", str(value))
|
|
596
|
-
|
|
698
|
+
|
|
597
699
|
# Also check baggage for session ID
|
|
598
700
|
session_id_from_baggage = baggage.get_baggage("mcp.session.id")
|
|
599
701
|
if session_id_from_baggage:
|
|
600
702
|
span.set_attribute("mcp.session.id", session_id_from_baggage)
|
|
601
|
-
|
|
703
|
+
|
|
602
704
|
# Add prompt arguments
|
|
603
705
|
for key, value in kwargs.items():
|
|
604
|
-
if key !=
|
|
605
|
-
if isinstance(value,
|
|
706
|
+
if key != "ctx":
|
|
707
|
+
if isinstance(value, str | int | float | bool) or value is None:
|
|
606
708
|
span.set_attribute(f"mcp.prompt.arg.{key}", str(value))
|
|
607
709
|
else:
|
|
608
|
-
span.set_attribute(
|
|
609
|
-
|
|
710
|
+
span.set_attribute(
|
|
711
|
+
f"mcp.prompt.arg.{key}.type", type(value).__name__
|
|
712
|
+
)
|
|
713
|
+
|
|
610
714
|
# Add event for prompt generation start
|
|
611
|
-
span.add_event("prompt.generation.started", {
|
|
612
|
-
|
|
613
|
-
})
|
|
614
|
-
|
|
715
|
+
span.add_event("prompt.generation.started", {"prompt.name": prompt_name})
|
|
716
|
+
|
|
615
717
|
try:
|
|
616
718
|
result = await func(*args, **kwargs)
|
|
617
719
|
span.set_status(Status(StatusCode.OK))
|
|
618
|
-
|
|
720
|
+
|
|
619
721
|
# Add event for successful generation
|
|
620
|
-
span.add_event(
|
|
621
|
-
"prompt.name": prompt_name
|
|
622
|
-
|
|
623
|
-
|
|
722
|
+
span.add_event(
|
|
723
|
+
"prompt.generation.completed", {"prompt.name": prompt_name}
|
|
724
|
+
)
|
|
725
|
+
|
|
624
726
|
# Add message count and type information
|
|
625
727
|
if isinstance(result, list):
|
|
626
728
|
span.set_attribute("mcp.prompt.result.message_count", len(result))
|
|
627
729
|
span.set_attribute("mcp.prompt.result.type", "message_list")
|
|
628
|
-
|
|
730
|
+
|
|
629
731
|
# Analyze message types if they have role attributes
|
|
630
732
|
roles = []
|
|
631
733
|
for msg in result:
|
|
632
|
-
if hasattr(msg,
|
|
734
|
+
if hasattr(msg, "role"):
|
|
633
735
|
roles.append(msg.role)
|
|
634
|
-
elif isinstance(msg, dict) and
|
|
635
|
-
roles.append(msg[
|
|
636
|
-
|
|
736
|
+
elif isinstance(msg, dict) and "role" in msg:
|
|
737
|
+
roles.append(msg["role"])
|
|
738
|
+
|
|
637
739
|
if roles:
|
|
638
740
|
unique_roles = list(set(roles))
|
|
639
|
-
span.set_attribute(
|
|
640
|
-
|
|
741
|
+
span.set_attribute(
|
|
742
|
+
"mcp.prompt.result.roles", ",".join(unique_roles)
|
|
743
|
+
)
|
|
744
|
+
span.set_attribute(
|
|
745
|
+
"mcp.prompt.result.role_counts",
|
|
746
|
+
str({role: roles.count(role) for role in unique_roles}),
|
|
747
|
+
)
|
|
641
748
|
elif isinstance(result, str):
|
|
642
749
|
span.set_attribute("mcp.prompt.result.type", "string")
|
|
643
750
|
span.set_attribute("mcp.prompt.result.length", len(result))
|
|
644
751
|
else:
|
|
645
752
|
span.set_attribute("mcp.prompt.result.type", type(result).__name__)
|
|
646
|
-
|
|
753
|
+
|
|
647
754
|
return result
|
|
648
755
|
except Exception as e:
|
|
649
756
|
span.record_exception(e)
|
|
650
757
|
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
651
|
-
|
|
758
|
+
|
|
652
759
|
# Add event for error
|
|
653
|
-
span.add_event(
|
|
654
|
-
"prompt.
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
760
|
+
span.add_event(
|
|
761
|
+
"prompt.generation.error",
|
|
762
|
+
{
|
|
763
|
+
"prompt.name": prompt_name,
|
|
764
|
+
"error.type": type(e).__name__,
|
|
765
|
+
"error.message": str(e),
|
|
766
|
+
},
|
|
767
|
+
)
|
|
658
768
|
raise
|
|
659
|
-
|
|
769
|
+
|
|
660
770
|
@functools.wraps(func)
|
|
661
771
|
def sync_wrapper(*args, **kwargs):
|
|
662
772
|
# Create a more descriptive span name
|
|
@@ -667,119 +777,137 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
|
|
|
667
777
|
span.set_attribute("mcp.component.name", prompt_name)
|
|
668
778
|
span.set_attribute("mcp.prompt.name", prompt_name)
|
|
669
779
|
span.set_attribute("mcp.prompt.function", func.__name__)
|
|
670
|
-
span.set_attribute(
|
|
780
|
+
span.set_attribute(
|
|
781
|
+
"mcp.prompt.module",
|
|
782
|
+
func.__module__ if hasattr(func, "__module__") else "unknown",
|
|
783
|
+
)
|
|
671
784
|
span.set_attribute("mcp.execution.async", False)
|
|
672
|
-
|
|
785
|
+
|
|
673
786
|
# Extract Context parameter if present
|
|
674
|
-
ctx = kwargs.get(
|
|
787
|
+
ctx = kwargs.get("ctx")
|
|
675
788
|
if ctx:
|
|
676
789
|
# Only extract known MCP context attributes
|
|
677
|
-
ctx_attrs = [
|
|
790
|
+
ctx_attrs = [
|
|
791
|
+
"request_id",
|
|
792
|
+
"session_id",
|
|
793
|
+
"client_id",
|
|
794
|
+
"user_id",
|
|
795
|
+
"tenant_id",
|
|
796
|
+
]
|
|
678
797
|
for attr in ctx_attrs:
|
|
679
798
|
if hasattr(ctx, attr):
|
|
680
799
|
value = getattr(ctx, attr)
|
|
681
800
|
if value is not None:
|
|
682
801
|
span.set_attribute(f"mcp.context.{attr}", str(value))
|
|
683
|
-
|
|
802
|
+
|
|
684
803
|
# Also check baggage for session ID
|
|
685
804
|
session_id_from_baggage = baggage.get_baggage("mcp.session.id")
|
|
686
805
|
if session_id_from_baggage:
|
|
687
806
|
span.set_attribute("mcp.session.id", session_id_from_baggage)
|
|
688
|
-
|
|
807
|
+
|
|
689
808
|
# Add prompt arguments
|
|
690
809
|
for key, value in kwargs.items():
|
|
691
|
-
if key !=
|
|
692
|
-
if isinstance(value,
|
|
810
|
+
if key != "ctx":
|
|
811
|
+
if isinstance(value, str | int | float | bool) or value is None:
|
|
693
812
|
span.set_attribute(f"mcp.prompt.arg.{key}", str(value))
|
|
694
813
|
else:
|
|
695
|
-
span.set_attribute(
|
|
696
|
-
|
|
814
|
+
span.set_attribute(
|
|
815
|
+
f"mcp.prompt.arg.{key}.type", type(value).__name__
|
|
816
|
+
)
|
|
817
|
+
|
|
697
818
|
# Add event for prompt generation start
|
|
698
|
-
span.add_event("prompt.generation.started", {
|
|
699
|
-
|
|
700
|
-
})
|
|
701
|
-
|
|
819
|
+
span.add_event("prompt.generation.started", {"prompt.name": prompt_name})
|
|
820
|
+
|
|
702
821
|
try:
|
|
703
822
|
result = func(*args, **kwargs)
|
|
704
823
|
span.set_status(Status(StatusCode.OK))
|
|
705
|
-
|
|
824
|
+
|
|
706
825
|
# Add event for successful generation
|
|
707
|
-
span.add_event(
|
|
708
|
-
"prompt.name": prompt_name
|
|
709
|
-
|
|
710
|
-
|
|
826
|
+
span.add_event(
|
|
827
|
+
"prompt.generation.completed", {"prompt.name": prompt_name}
|
|
828
|
+
)
|
|
829
|
+
|
|
711
830
|
# Add message count and type information
|
|
712
831
|
if isinstance(result, list):
|
|
713
832
|
span.set_attribute("mcp.prompt.result.message_count", len(result))
|
|
714
833
|
span.set_attribute("mcp.prompt.result.type", "message_list")
|
|
715
|
-
|
|
834
|
+
|
|
716
835
|
# Analyze message types if they have role attributes
|
|
717
836
|
roles = []
|
|
718
837
|
for msg in result:
|
|
719
|
-
if hasattr(msg,
|
|
838
|
+
if hasattr(msg, "role"):
|
|
720
839
|
roles.append(msg.role)
|
|
721
|
-
elif isinstance(msg, dict) and
|
|
722
|
-
roles.append(msg[
|
|
723
|
-
|
|
840
|
+
elif isinstance(msg, dict) and "role" in msg:
|
|
841
|
+
roles.append(msg["role"])
|
|
842
|
+
|
|
724
843
|
if roles:
|
|
725
844
|
unique_roles = list(set(roles))
|
|
726
|
-
span.set_attribute(
|
|
727
|
-
|
|
845
|
+
span.set_attribute(
|
|
846
|
+
"mcp.prompt.result.roles", ",".join(unique_roles)
|
|
847
|
+
)
|
|
848
|
+
span.set_attribute(
|
|
849
|
+
"mcp.prompt.result.role_counts",
|
|
850
|
+
str({role: roles.count(role) for role in unique_roles}),
|
|
851
|
+
)
|
|
728
852
|
elif isinstance(result, str):
|
|
729
853
|
span.set_attribute("mcp.prompt.result.type", "string")
|
|
730
854
|
span.set_attribute("mcp.prompt.result.length", len(result))
|
|
731
855
|
else:
|
|
732
856
|
span.set_attribute("mcp.prompt.result.type", type(result).__name__)
|
|
733
|
-
|
|
857
|
+
|
|
734
858
|
return result
|
|
735
859
|
except Exception as e:
|
|
736
860
|
span.record_exception(e)
|
|
737
861
|
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
738
|
-
|
|
862
|
+
|
|
739
863
|
# Add event for error
|
|
740
|
-
span.add_event(
|
|
741
|
-
"prompt.
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
864
|
+
span.add_event(
|
|
865
|
+
"prompt.generation.error",
|
|
866
|
+
{
|
|
867
|
+
"prompt.name": prompt_name,
|
|
868
|
+
"error.type": type(e).__name__,
|
|
869
|
+
"error.message": str(e),
|
|
870
|
+
},
|
|
871
|
+
)
|
|
745
872
|
raise
|
|
746
|
-
|
|
873
|
+
|
|
747
874
|
if asyncio.iscoroutinefunction(func):
|
|
748
875
|
return async_wrapper
|
|
749
876
|
else:
|
|
750
877
|
return sync_wrapper
|
|
751
878
|
|
|
879
|
+
|
|
752
880
|
@asynccontextmanager
|
|
753
881
|
async def telemetry_lifespan(mcp_instance):
|
|
754
882
|
"""Simplified lifespan for telemetry initialization and cleanup."""
|
|
755
883
|
global _provider
|
|
756
|
-
|
|
884
|
+
|
|
757
885
|
# Initialize telemetry with the server name
|
|
758
886
|
provider = init_telemetry(service_name=mcp_instance.name)
|
|
759
|
-
|
|
887
|
+
|
|
760
888
|
# If provider is None, telemetry is disabled
|
|
761
889
|
if provider is None:
|
|
762
890
|
# Just yield without any telemetry setup
|
|
763
891
|
yield
|
|
764
892
|
return
|
|
765
|
-
|
|
893
|
+
|
|
766
894
|
# Try to add session tracking middleware if possible
|
|
767
895
|
try:
|
|
768
896
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
769
897
|
from starlette.requests import Request
|
|
770
|
-
|
|
898
|
+
|
|
771
899
|
class SessionTracingMiddleware(BaseHTTPMiddleware):
|
|
772
900
|
async def dispatch(self, request: Request, call_next):
|
|
773
901
|
# Extract session ID from query params or headers
|
|
774
|
-
session_id = request.query_params.get(
|
|
902
|
+
session_id = request.query_params.get("session_id")
|
|
775
903
|
if not session_id:
|
|
776
904
|
# Check headers as fallback
|
|
777
|
-
session_id = request.headers.get(
|
|
778
|
-
|
|
905
|
+
session_id = request.headers.get("x-session-id")
|
|
906
|
+
|
|
779
907
|
# Create a descriptive span name based on the request
|
|
780
908
|
method = request.method
|
|
781
909
|
path = request.url.path
|
|
782
|
-
|
|
910
|
+
|
|
783
911
|
# Determine the operation type from the path
|
|
784
912
|
operation_type = "unknown"
|
|
785
913
|
if "/mcp" in path:
|
|
@@ -788,9 +916,9 @@ async def telemetry_lifespan(mcp_instance):
|
|
|
788
916
|
operation_type = "sse.stream"
|
|
789
917
|
elif "/auth" in path:
|
|
790
918
|
operation_type = "auth"
|
|
791
|
-
|
|
919
|
+
|
|
792
920
|
span_name = f"{operation_type}.{method.lower()}"
|
|
793
|
-
|
|
921
|
+
|
|
794
922
|
tracer = get_tracer()
|
|
795
923
|
with tracer.start_as_current_span(span_name) as span:
|
|
796
924
|
# Add comprehensive HTTP attributes
|
|
@@ -799,102 +927,119 @@ async def telemetry_lifespan(mcp_instance):
|
|
|
799
927
|
span.set_attribute("http.scheme", request.url.scheme)
|
|
800
928
|
span.set_attribute("http.host", request.url.hostname or "unknown")
|
|
801
929
|
span.set_attribute("http.target", path)
|
|
802
|
-
span.set_attribute(
|
|
803
|
-
|
|
930
|
+
span.set_attribute(
|
|
931
|
+
"http.user_agent", request.headers.get("user-agent", "unknown")
|
|
932
|
+
)
|
|
933
|
+
|
|
804
934
|
# Add session tracking
|
|
805
935
|
if session_id:
|
|
806
936
|
span.set_attribute("mcp.session.id", session_id)
|
|
807
937
|
# Add to baggage for propagation
|
|
808
938
|
ctx = baggage.set_baggage("mcp.session.id", session_id)
|
|
809
939
|
from opentelemetry import context
|
|
940
|
+
|
|
810
941
|
token = context.attach(ctx)
|
|
811
942
|
else:
|
|
812
943
|
token = None
|
|
813
|
-
|
|
944
|
+
|
|
814
945
|
# Add request size if available
|
|
815
946
|
content_length = request.headers.get("content-length")
|
|
816
947
|
if content_length:
|
|
817
948
|
span.set_attribute("http.request.size", int(content_length))
|
|
818
|
-
|
|
949
|
+
|
|
819
950
|
# Add event for request start
|
|
820
|
-
span.add_event(
|
|
821
|
-
"method": method,
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
951
|
+
span.add_event(
|
|
952
|
+
"http.request.started", {"method": method, "path": path}
|
|
953
|
+
)
|
|
954
|
+
|
|
825
955
|
try:
|
|
826
956
|
response = await call_next(request)
|
|
827
|
-
|
|
957
|
+
|
|
828
958
|
# Add response attributes
|
|
829
959
|
span.set_attribute("http.status_code", response.status_code)
|
|
830
|
-
span.set_attribute(
|
|
831
|
-
|
|
960
|
+
span.set_attribute(
|
|
961
|
+
"http.status_class", f"{response.status_code // 100}xx"
|
|
962
|
+
)
|
|
963
|
+
|
|
832
964
|
# Set span status based on HTTP status
|
|
833
965
|
if response.status_code >= 400:
|
|
834
|
-
span.set_status(
|
|
966
|
+
span.set_status(
|
|
967
|
+
Status(StatusCode.ERROR, f"HTTP {response.status_code}")
|
|
968
|
+
)
|
|
835
969
|
else:
|
|
836
970
|
span.set_status(Status(StatusCode.OK))
|
|
837
|
-
|
|
971
|
+
|
|
838
972
|
# Add event for request completion
|
|
839
|
-
span.add_event(
|
|
840
|
-
"
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
973
|
+
span.add_event(
|
|
974
|
+
"http.request.completed",
|
|
975
|
+
{
|
|
976
|
+
"method": method,
|
|
977
|
+
"path": path,
|
|
978
|
+
"status_code": response.status_code,
|
|
979
|
+
},
|
|
980
|
+
)
|
|
981
|
+
|
|
845
982
|
return response
|
|
846
983
|
except Exception as e:
|
|
847
984
|
span.record_exception(e)
|
|
848
985
|
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
849
|
-
|
|
986
|
+
|
|
850
987
|
# Add event for error
|
|
851
|
-
span.add_event(
|
|
852
|
-
"
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
988
|
+
span.add_event(
|
|
989
|
+
"http.request.error",
|
|
990
|
+
{
|
|
991
|
+
"method": method,
|
|
992
|
+
"path": path,
|
|
993
|
+
"error.type": type(e).__name__,
|
|
994
|
+
"error.message": str(e),
|
|
995
|
+
},
|
|
996
|
+
)
|
|
857
997
|
raise
|
|
858
998
|
finally:
|
|
859
999
|
if token:
|
|
860
1000
|
context.detach(token)
|
|
861
|
-
|
|
1001
|
+
|
|
862
1002
|
# Try to add middleware to FastMCP app if it has Starlette app
|
|
863
|
-
if hasattr(mcp_instance,
|
|
864
|
-
app = getattr(mcp_instance,
|
|
865
|
-
if app and hasattr(app,
|
|
1003
|
+
if hasattr(mcp_instance, "app") or hasattr(mcp_instance, "_app"):
|
|
1004
|
+
app = getattr(mcp_instance, "app", getattr(mcp_instance, "_app", None))
|
|
1005
|
+
if app and hasattr(app, "add_middleware"):
|
|
866
1006
|
app.add_middleware(SessionTracingMiddleware)
|
|
867
1007
|
print("[TELEMETRY DEBUG] Added SessionTracingMiddleware to FastMCP app")
|
|
868
|
-
|
|
1008
|
+
|
|
869
1009
|
# Also try to instrument FastMCP's internal handlers
|
|
870
|
-
if hasattr(mcp_instance,
|
|
871
|
-
|
|
1010
|
+
if hasattr(mcp_instance, "_tool_manager") and hasattr(
|
|
1011
|
+
mcp_instance._tool_manager, "tools"
|
|
1012
|
+
):
|
|
1013
|
+
print(
|
|
1014
|
+
f"[TELEMETRY DEBUG] Found {len(mcp_instance._tool_manager.tools)} tools in FastMCP"
|
|
1015
|
+
)
|
|
872
1016
|
# The tools should already be instrumented when they were registered
|
|
873
|
-
|
|
1017
|
+
|
|
874
1018
|
# Try to patch FastMCP's request handling to ensure context propagation
|
|
875
|
-
if hasattr(mcp_instance,
|
|
1019
|
+
if hasattr(mcp_instance, "handle_request"):
|
|
876
1020
|
original_handle_request = mcp_instance.handle_request
|
|
877
|
-
|
|
1021
|
+
|
|
878
1022
|
async def traced_handle_request(*args, **kwargs):
|
|
879
1023
|
tracer = get_tracer()
|
|
880
1024
|
with tracer.start_as_current_span("mcp.handle_request") as span:
|
|
881
1025
|
span.set_attribute("mcp.request.handler", "handle_request")
|
|
882
1026
|
return await original_handle_request(*args, **kwargs)
|
|
883
|
-
|
|
1027
|
+
|
|
884
1028
|
mcp_instance.handle_request = traced_handle_request
|
|
885
1029
|
print("[TELEMETRY DEBUG] Patched FastMCP handle_request method")
|
|
886
|
-
|
|
1030
|
+
|
|
887
1031
|
except Exception as e:
|
|
888
1032
|
print(f"[TELEMETRY DEBUG] Error setting up telemetry middleware: {e}")
|
|
889
1033
|
import traceback
|
|
1034
|
+
|
|
890
1035
|
traceback.print_exc()
|
|
891
|
-
|
|
1036
|
+
|
|
892
1037
|
try:
|
|
893
1038
|
# Yield control back to FastMCP
|
|
894
1039
|
yield
|
|
895
1040
|
finally:
|
|
896
1041
|
# Cleanup - shutdown the provider
|
|
897
|
-
if _provider and hasattr(_provider,
|
|
1042
|
+
if _provider and hasattr(_provider, "shutdown"):
|
|
898
1043
|
_provider.force_flush()
|
|
899
1044
|
_provider.shutdown()
|
|
900
|
-
_provider = None
|
|
1045
|
+
_provider = None
|