Skip to content

Commit da51fa5

Browse files
AjmeraParth132anubhav756
authored andcommitted
feat: tests + export client metadata
1 parent 3e1b274 commit da51fa5

3 files changed

Lines changed: 111 additions & 0 deletions

File tree

docs/en/reference/cli.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ description: >
2424
| | `--telemetry-gcp` | Enable exporting directly to Google Cloud Monitoring. | |
2525
| | `--telemetry-otlp` | Enable exporting using OpenTelemetry Protocol (OTLP) to the specified endpoint (e.g. 'http://127.0.0.1:4318') | |
2626
| | `--telemetry-service-name` | Sets the value of the service.name resource attribute for telemetry data. | `toolbox` |
27+
| | `--sql-commenter` | Append SQLCommenter-format comments (traceparent, server, tool.name, db.system.name, client metadata from `_meta["dev.mcp-toolbox/telemetry"]`) to executed SQL. | |
2728
| | `--config` | File path specifying the tool configuration. Cannot be used with --configs or --config-folder. | |
2829
| | `--configs` | Multiple file paths specifying tool configurations. Files will be merged. Cannot be used with --config or --config-folder. | |
2930
| | `--config-folder` | Directory path containing YAML tool configuration files. All .yaml and .yml files in the directory will be loaded and merged. Cannot be used with --config or --configs. | |

internal/server/mcp.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,29 @@ func processMcpMessage(ctx context.Context, body []byte, s *Server, protocolVers
708708
attribute.String("network.protocol.name", networkProtocolName),
709709
)
710710

711+
// Set client telemetry attributes from _meta["dev.mcp-toolbox/telemetry"]
712+
if ta := util.TelemetryAttributesFromContext(ctx); ta != nil {
713+
telemetryAttrs := make([]attribute.KeyValue, 0, 5)
714+
if ta.ClientName != "" {
715+
telemetryAttrs = append(telemetryAttrs, attribute.String("client.name", ta.ClientName))
716+
}
717+
if ta.ClientVersion != "" {
718+
telemetryAttrs = append(telemetryAttrs, attribute.String("client.version", ta.ClientVersion))
719+
}
720+
if ta.ClientModel != "" {
721+
telemetryAttrs = append(telemetryAttrs, attribute.String("client.model", ta.ClientModel))
722+
}
723+
if ta.ClientUserID != "" {
724+
telemetryAttrs = append(telemetryAttrs, attribute.String("client.user.id", ta.ClientUserID))
725+
}
726+
if ta.ClientAgentID != "" {
727+
telemetryAttrs = append(telemetryAttrs, attribute.String("client.agent.id", ta.ClientAgentID))
728+
}
729+
if len(telemetryAttrs) > 0 {
730+
span.SetAttributes(telemetryAttrs...)
731+
}
732+
}
733+
711734
// Set network protocol version if available
712735
if networkProtocolVersion != "" {
713736
span.SetAttributes(attribute.String("network.protocol.version", networkProtocolVersion))

internal/server/mcp_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ import (
3131
"github.com/googleapis/mcp-toolbox/internal/server/mcp/jsonrpc"
3232
"github.com/googleapis/mcp-toolbox/internal/server/resources"
3333
"github.com/googleapis/mcp-toolbox/internal/telemetry"
34+
"github.com/googleapis/mcp-toolbox/internal/util"
35+
"go.opentelemetry.io/otel"
36+
"go.opentelemetry.io/otel/propagation"
37+
"go.opentelemetry.io/otel/trace"
3438

3539
"github.com/googleapis/mcp-toolbox/internal/testutils"
3640
)
@@ -1198,3 +1202,86 @@ func TestSseManagerGetNilSessionValue(t *testing.T) {
11981202
t.Error("expected nil session for nil session value")
11991203
}
12001204
}
1205+
1206+
// withTraceContextPropagator registers the W3C trace-context propagator globally
1207+
// for the duration of the test. extractMeta delegates to otel.GetTextMapPropagator,
1208+
// and the default global propagator is a no-op — so without this helper the
1209+
// "extracted" trace context would always be invalid.
1210+
func withTraceContextPropagator(t *testing.T) {
1211+
t.Helper()
1212+
prev := otel.GetTextMapPropagator()
1213+
otel.SetTextMapPropagator(propagation.TraceContext{})
1214+
t.Cleanup(func() { otel.SetTextMapPropagator(prev) })
1215+
}
1216+
1217+
func TestExtractMeta_EmptyOrInvalidBody(t *testing.T) {
1218+
cases := map[string][]byte{
1219+
"empty": []byte(""),
1220+
"not json": []byte("not json"),
1221+
"no _meta": []byte(`{"params":{}}`),
1222+
"no params": []byte(`{"method":"tools/call"}`),
1223+
"empty meta": []byte(`{"params":{"_meta":{}}}`),
1224+
}
1225+
for name, body := range cases {
1226+
t.Run(name, func(t *testing.T) {
1227+
ctx := extractMeta(context.Background(), body)
1228+
if util.TelemetryAttributesFromContext(ctx) != nil {
1229+
t.Error("expected no telemetry attributes")
1230+
}
1231+
})
1232+
}
1233+
}
1234+
1235+
func TestExtractMeta_TraceparentOnly(t *testing.T) {
1236+
withTraceContextPropagator(t)
1237+
body := []byte(`{"params":{"_meta":{"traceparent":"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"}}}`)
1238+
ctx := extractMeta(context.Background(), body)
1239+
1240+
sc := trace.SpanContextFromContext(ctx)
1241+
if !sc.IsValid() {
1242+
t.Fatal("expected valid span context from extracted traceparent")
1243+
}
1244+
if got := sc.TraceID().String(); got != "0af7651916cd43dd8448eb211c80319c" {
1245+
t.Errorf("trace id mismatch: got %s", got)
1246+
}
1247+
if util.TelemetryAttributesFromContext(ctx) != nil {
1248+
t.Error("expected no telemetry attributes when only traceparent is sent")
1249+
}
1250+
}
1251+
1252+
func TestExtractMeta_TelemetryAttrsOnly(t *testing.T) {
1253+
body := []byte(`{"params":{"_meta":{"dev.mcp-toolbox/telemetry":{` +
1254+
`"client.name":"toolbox-langchain-python",` +
1255+
`"client.version":"v0.1.0",` +
1256+
`"client.model":"gemini-2.5-flash",` +
1257+
`"client.user.id":"user-123",` +
1258+
`"client.agent.id":"agent-456"}}}}`)
1259+
1260+
ta := util.TelemetryAttributesFromContext(extractMeta(context.Background(), body))
1261+
if ta == nil {
1262+
t.Fatal("expected TelemetryAttributes in context")
1263+
}
1264+
want := util.TelemetryAttributes{
1265+
ClientName: "toolbox-langchain-python", ClientVersion: "v0.1.0",
1266+
ClientModel: "gemini-2.5-flash", ClientUserID: "user-123", ClientAgentID: "agent-456",
1267+
}
1268+
if *ta != want {
1269+
t.Errorf("got %+v, want %+v", *ta, want)
1270+
}
1271+
}
1272+
1273+
func TestExtractMeta_TraceparentAndTelemetryBoth(t *testing.T) {
1274+
withTraceContextPropagator(t)
1275+
body := []byte(`{"params":{"_meta":{` +
1276+
`"traceparent":"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",` +
1277+
`"dev.mcp-toolbox/telemetry":{"client.name":"foo","client.version":"v1"}}}}`)
1278+
ctx := extractMeta(context.Background(), body)
1279+
1280+
if !trace.SpanContextFromContext(ctx).IsValid() {
1281+
t.Error("expected valid span context")
1282+
}
1283+
ta := util.TelemetryAttributesFromContext(ctx)
1284+
if ta == nil || ta.ClientName != "foo" || ta.ClientVersion != "v1" {
1285+
t.Errorf("expected telemetry attrs alongside traceparent, got %+v", ta)
1286+
}
1287+
}

0 commit comments

Comments
 (0)