Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/internal/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ func PersistentFlags(parentCmd *cobra.Command, opts *ToolboxOptions) {
persistentFlags.BoolVar(&opts.Cfg.TelemetryGCP, "telemetry-gcp", false, "Enable exporting directly to Google Cloud Monitoring.")
persistentFlags.StringVar(&opts.Cfg.TelemetryOTLP, "telemetry-otlp", "", "Enable exporting using OpenTelemetry Protocol (OTLP) to the specified endpoint (e.g. 'http://127.0.0.1:4318')")
persistentFlags.StringVar(&opts.Cfg.TelemetryServiceName, "telemetry-service-name", "toolbox", "Sets the value of the service.name resource attribute for telemetry data.")
persistentFlags.BoolVar(&opts.Cfg.SQLCommenter, "sql-commenter", false, "Enable appending SQLCommenter-format comments to SQL statements.")
persistentFlags.StringSliceVar(&opts.Cfg.UserAgentMetadata, "user-agent-metadata", []string{}, "Appends additional metadata to the User-Agent.")
}

Expand Down
1 change: 1 addition & 0 deletions docs/en/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ description: >
| | `--telemetry-gcp` | Enable exporting directly to Google Cloud Monitoring. | |
| | `--telemetry-otlp` | Enable exporting using OpenTelemetry Protocol (OTLP) to the specified endpoint (e.g. 'http://127.0.0.1:4318') | |
| | `--telemetry-service-name` | Sets the value of the service.name resource attribute for telemetry data. | `toolbox` |
| | `--sql-commenter` | Append SQLCommenter-format comments (traceparent, server, tool.name, db.system.name, client metadata from `_meta["dev.mcp-toolbox/telemetry"]`) to executed SQL. | |
| | `--config` | File path specifying the tool configuration. Cannot be used with --configs or --config-folder. | |
| | `--configs` | Multiple file paths specifying tool configurations. Files will be merged. Cannot be used with --config or --config-folder. | |
| | `--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. | |
Expand Down
2 changes: 2 additions & 0 deletions internal/server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ type ServerConfig struct {
TelemetryOTLP string
// TelemetryServiceName defines the value of service.name resource attribute.
TelemetryServiceName string
// SQLCommenter enables appending SQLCommenter-format comments to SQL statements.
SQLCommenter bool
// Stdio indicates if Toolbox is listening via MCP stdio.
Stdio bool
// DisableReload indicates if the user has disabled dynamic reloading for Toolbox.
Expand Down
59 changes: 50 additions & 9 deletions internal/server/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,14 +146,15 @@ func (c traceContextCarrier) Keys() []string {
return keys
}

// extractTraceContext extracts W3C Trace Context from params._meta
func extractTraceContext(ctx context.Context, body []byte) context.Context {
// Try to parse the request to extract _meta
// extractMeta parses params._meta from the request body in a single pass,
// extracting both W3C Trace Context and client telemetry attributes.
func extractMeta(ctx context.Context, body []byte) context.Context {
var req struct {
Params struct {
Meta struct {
Traceparent string `json:"traceparent,omitempty"`
Tracestate string `json:"tracestate,omitempty"`
Traceparent string `json:"traceparent,omitempty"`
Tracestate string `json:"tracestate,omitempty"`
TelemetryAttrs map[string]string `json:"dev.mcp-toolbox/telemetry,omitempty"`
} `json:"_meta,omitempty"`
} `json:"params,omitempty"`
}
Expand All @@ -162,15 +163,27 @@ func extractTraceContext(ctx context.Context, body []byte) context.Context {
return ctx
}

// If traceparent is present, extract the context
// Extract W3C Trace Context
if req.Params.Meta.Traceparent != "" {
carrier := traceContextCarrier{
"traceparent": req.Params.Meta.Traceparent,
}
if req.Params.Meta.Tracestate != "" {
carrier["tracestate"] = req.Params.Meta.Tracestate
}
return otel.GetTextMapPropagator().Extract(ctx, carrier)
ctx = otel.GetTextMapPropagator().Extract(ctx, carrier)
}

// Extract client telemetry attributes
if attrs := req.Params.Meta.TelemetryAttrs; len(attrs) > 0 {
ta := &util.TelemetryAttributes{
ClientName: attrs["client.name"],
ClientVersion: attrs["client.version"],
ClientModel: attrs["client.model"],
ClientUserID: attrs["client.user.id"],
ClientAgentID: attrs["client.agent.id"],
}
ctx = util.WithTelemetryAttributes(ctx, ta)
}

return ctx
Expand All @@ -192,6 +205,8 @@ func (s *stdioSession) Start(ctx context.Context) error {
// readInputStream reads requests/notifications from MCP clients through stdin
func (s *stdioSession) readInputStream(ctx context.Context) error {
sessionStart := time.Now()
ctx = util.WithUserAgent(ctx, s.server.version)
ctx = util.WithSQLCommenterEnabled(ctx, s.server.sqlCommenterEnabled)

// Define attributes for session metrics
// Note: mcp.protocol.version is added dynamically after protocol negotiation
Expand Down Expand Up @@ -239,7 +254,7 @@ func (s *stdioSession) readInputStream(ctx context.Context) error {

if err := func() error {
// This ensures the transport span becomes a child of the client span
msgCtx := extractTraceContext(ctx, []byte(line))
msgCtx := extractMeta(ctx, []byte(line))

// Create span for STDIO transport
msgCtx, span := s.server.instrumentation.Tracer.Start(msgCtx, "toolbox/server/mcp/stdio",
Expand Down Expand Up @@ -470,6 +485,9 @@ func httpHandler(s *Server, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")

ctx := r.Context()
ctx = util.WithLogger(ctx, s.logger)
ctx = util.WithUserAgent(ctx, s.version)
ctx = util.WithSQLCommenterEnabled(ctx, s.sqlCommenterEnabled)

// Read body first so we can extract trace context
body, err := io.ReadAll(r.Body)
Expand All @@ -482,7 +500,7 @@ func httpHandler(s *Server, w http.ResponseWriter, r *http.Request) {
}

// This ensures the transport span becomes a child of the client span
ctx = extractTraceContext(ctx, body)
ctx = extractMeta(ctx, body)

// Create span for HTTP transport
ctx, span := s.instrumentation.Tracer.Start(ctx, "toolbox/server/mcp/http",
Expand Down Expand Up @@ -706,6 +724,29 @@ func processMcpMessage(ctx context.Context, body []byte, s *Server, protocolVers
attribute.String("network.protocol.name", networkProtocolName),
)

// Set client telemetry attributes from _meta["dev.mcp-toolbox/telemetry"]
if ta := util.TelemetryAttributesFromContext(ctx); ta != nil {
telemetryAttrs := make([]attribute.KeyValue, 0, 5)
if ta.ClientName != "" {
telemetryAttrs = append(telemetryAttrs, attribute.String("client.name", ta.ClientName))
}
if ta.ClientVersion != "" {
telemetryAttrs = append(telemetryAttrs, attribute.String("client.version", ta.ClientVersion))
}
if ta.ClientModel != "" {
telemetryAttrs = append(telemetryAttrs, attribute.String("client.model", ta.ClientModel))
}
if ta.ClientUserID != "" {
telemetryAttrs = append(telemetryAttrs, attribute.String("client.user.id", ta.ClientUserID))
}
if ta.ClientAgentID != "" {
telemetryAttrs = append(telemetryAttrs, attribute.String("client.agent.id", ta.ClientAgentID))
}
if len(telemetryAttrs) > 0 {
span.SetAttributes(telemetryAttrs...)
}
}

// Set network protocol version if available
if networkProtocolVersion != "" {
span.SetAttributes(attribute.String("network.protocol.version", networkProtocolVersion))
Expand Down
87 changes: 87 additions & 0 deletions internal/server/mcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ import (
"github.com/googleapis/mcp-toolbox/internal/server/mcp/jsonrpc"
"github.com/googleapis/mcp-toolbox/internal/server/resources"
"github.com/googleapis/mcp-toolbox/internal/telemetry"
"github.com/googleapis/mcp-toolbox/internal/util"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/trace"

"github.com/googleapis/mcp-toolbox/internal/testutils"
)
Expand Down Expand Up @@ -1198,3 +1202,86 @@ func TestSseManagerGetNilSessionValue(t *testing.T) {
t.Error("expected nil session for nil session value")
}
}

// withTraceContextPropagator registers the W3C trace-context propagator globally
// for the duration of the test. extractMeta delegates to otel.GetTextMapPropagator,
// and the default global propagator is a no-op — so without this helper the
// "extracted" trace context would always be invalid.
func withTraceContextPropagator(t *testing.T) {
t.Helper()
prev := otel.GetTextMapPropagator()
otel.SetTextMapPropagator(propagation.TraceContext{})
t.Cleanup(func() { otel.SetTextMapPropagator(prev) })
}

func TestExtractMeta_EmptyOrInvalidBody(t *testing.T) {
cases := map[string][]byte{
"empty": []byte(""),
"not json": []byte("not json"),
"no _meta": []byte(`{"params":{}}`),
"no params": []byte(`{"method":"tools/call"}`),
"empty meta": []byte(`{"params":{"_meta":{}}}`),
}
for name, body := range cases {
t.Run(name, func(t *testing.T) {
ctx := extractMeta(context.Background(), body)
if util.TelemetryAttributesFromContext(ctx) != nil {
t.Error("expected no telemetry attributes")
}
})
}
}

func TestExtractMeta_TraceparentOnly(t *testing.T) {
withTraceContextPropagator(t)
body := []byte(`{"params":{"_meta":{"traceparent":"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"}}}`)
ctx := extractMeta(context.Background(), body)

sc := trace.SpanContextFromContext(ctx)
if !sc.IsValid() {
t.Fatal("expected valid span context from extracted traceparent")
}
if got := sc.TraceID().String(); got != "0af7651916cd43dd8448eb211c80319c" {
t.Errorf("trace id mismatch: got %s", got)
}
if util.TelemetryAttributesFromContext(ctx) != nil {
t.Error("expected no telemetry attributes when only traceparent is sent")
}
}

func TestExtractMeta_TelemetryAttrsOnly(t *testing.T) {
body := []byte(`{"params":{"_meta":{"dev.mcp-toolbox/telemetry":{` +
`"client.name":"toolbox-langchain-python",` +
`"client.version":"v0.1.0",` +
`"client.model":"gemini-2.5-flash",` +
`"client.user.id":"user-123",` +
`"client.agent.id":"agent-456"}}}}`)

ta := util.TelemetryAttributesFromContext(extractMeta(context.Background(), body))
if ta == nil {
t.Fatal("expected TelemetryAttributes in context")
}
want := util.TelemetryAttributes{
ClientName: "toolbox-langchain-python", ClientVersion: "v0.1.0",
ClientModel: "gemini-2.5-flash", ClientUserID: "user-123", ClientAgentID: "agent-456",
}
if *ta != want {
t.Errorf("got %+v, want %+v", *ta, want)
}
}

func TestExtractMeta_TraceparentAndTelemetryBoth(t *testing.T) {
withTraceContextPropagator(t)
body := []byte(`{"params":{"_meta":{` +
`"traceparent":"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",` +
`"dev.mcp-toolbox/telemetry":{"client.name":"foo","client.version":"v1"}}}}`)
ctx := extractMeta(context.Background(), body)

if !trace.SpanContextFromContext(ctx).IsValid() {
t.Error("expected valid span context")
}
ta := util.TelemetryAttributesFromContext(ctx)
if ta == nil || ta.ClientName != "foo" || ta.ClientVersion != "v1" {
t.Errorf("expected telemetry attrs alongside traceparent, got %+v", ta)
}
}
40 changes: 21 additions & 19 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,17 @@ import (

// Server contains info for running an instance of Toolbox. Should be instantiated with NewServer().
type Server struct {
version string
toolboxUrl string
srv *http.Server
listener net.Listener
root chi.Router
logger log.Logger
instrumentation *telemetry.Instrumentation
sseManager *sseManager
ResourceMgr *resources.ResourceManager
mcpPrmFile string
version string
sqlCommenterEnabled bool
toolboxUrl string
srv *http.Server
listener net.Listener
root chi.Router
logger log.Logger
instrumentation *telemetry.Instrumentation
sseManager *sseManager
ResourceMgr *resources.ResourceManager
mcpPrmFile string
}

func InitializeConfigs(ctx context.Context, cfg ServerConfig) (
Expand Down Expand Up @@ -380,15 +381,16 @@ func NewServer(ctx context.Context, cfg ServerConfig) (*Server, error) {
resourceManager := resources.NewResourceManager(sourcesMap, authServicesMap, embeddingModelsMap, toolsMap, toolsetsMap, promptsMap, promptsetsMap)

s := &Server{
version: cfg.Version,
srv: srv,
root: r,
logger: l,
instrumentation: instrumentation,
sseManager: sseManager,
ResourceMgr: resourceManager,
toolboxUrl: cfg.ToolboxUrl,
mcpPrmFile: cfg.McpPrmFile,
version: cfg.Version,
sqlCommenterEnabled: cfg.SQLCommenter,
srv: srv,
root: r,
logger: l,
instrumentation: instrumentation,
sseManager: sseManager,
ResourceMgr: resourceManager,
toolboxUrl: cfg.ToolboxUrl,
mcpPrmFile: cfg.McpPrmFile,
}

// cors
Expand Down
2 changes: 2 additions & 0 deletions internal/sources/alloydbpg/alloydb_pg.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"cloud.google.com/go/alloydbconn"
"github.com/goccy/go-yaml"
"github.com/googleapis/mcp-toolbox/internal/sources"
"github.com/googleapis/mcp-toolbox/internal/sources/sqlcommenter"
"github.com/googleapis/mcp-toolbox/internal/util"
"github.com/googleapis/mcp-toolbox/internal/util/orderedmap"
"github.com/jackc/pgx/v5/pgxpool"
Expand Down Expand Up @@ -103,6 +104,7 @@ func (s *Source) PostgresPool() *pgxpool.Pool {
}

func (s *Source) RunSQL(ctx context.Context, statement string, params []any) (any, error) {
statement = sqlcommenter.AppendComment(ctx, statement, SourceType)
results, err := s.Pool.Query(ctx, statement, params...)
if err != nil {
return nil, fmt.Errorf("unable to execute query: %w", err)
Expand Down
2 changes: 2 additions & 0 deletions internal/sources/cloudsqlmssql/cloud_sql_mssql.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"cloud.google.com/go/cloudsqlconn/sqlserver/mssql"
"github.com/goccy/go-yaml"
"github.com/googleapis/mcp-toolbox/internal/sources"
"github.com/googleapis/mcp-toolbox/internal/sources/sqlcommenter"
"github.com/googleapis/mcp-toolbox/internal/util"
"github.com/googleapis/mcp-toolbox/internal/util/orderedmap"
"go.opentelemetry.io/otel/trace"
Expand Down Expand Up @@ -108,6 +109,7 @@ func (s *Source) MSSQLDB() *sql.DB {
}

func (s *Source) RunSQL(ctx context.Context, statement string, params []any) (any, error) {
statement = sqlcommenter.AppendComment(ctx, statement, SourceType)
results, err := s.MSSQLDB().QueryContext(ctx, statement, params...)
if err != nil {
return nil, fmt.Errorf("unable to execute query: %w", err)
Expand Down
2 changes: 2 additions & 0 deletions internal/sources/cloudsqlmysql/cloud_sql_mysql.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"cloud.google.com/go/cloudsqlconn/mysql/mysql"
"github.com/goccy/go-yaml"
"github.com/googleapis/mcp-toolbox/internal/sources"
"github.com/googleapis/mcp-toolbox/internal/sources/sqlcommenter"
"github.com/googleapis/mcp-toolbox/internal/tools/mysql/mysqlcommon"
"github.com/googleapis/mcp-toolbox/internal/util"
"github.com/googleapis/mcp-toolbox/internal/util/orderedmap"
Expand Down Expand Up @@ -107,6 +108,7 @@ func (s *Source) MySQLDatabase() string {
}

func (s *Source) RunSQL(ctx context.Context, statement string, params []any) (any, error) {
statement = sqlcommenter.AppendComment(ctx, statement, SourceType)
results, err := s.MySQLPool().QueryContext(ctx, statement, params...)
if err != nil {
return nil, fmt.Errorf("unable to execute query: %w", err)
Expand Down
2 changes: 2 additions & 0 deletions internal/sources/cloudsqlpg/cloud_sql_pg.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"cloud.google.com/go/cloudsqlconn"
"github.com/goccy/go-yaml"
"github.com/googleapis/mcp-toolbox/internal/sources"
"github.com/googleapis/mcp-toolbox/internal/sources/sqlcommenter"
"github.com/googleapis/mcp-toolbox/internal/util"
"github.com/googleapis/mcp-toolbox/internal/util/orderedmap"
"github.com/jackc/pgx/v5/pgxpool"
Expand Down Expand Up @@ -109,6 +110,7 @@ func (s *Source) PostgresPool() *pgxpool.Pool {
}

func (s *Source) RunSQL(ctx context.Context, statement string, params []any) (any, error) {
statement = sqlcommenter.AppendComment(ctx, statement, SourceType)
results, err := s.PostgresPool().Query(ctx, statement, params...)
if err != nil {
return nil, fmt.Errorf("unable to execute query: %w", err)
Expand Down
2 changes: 2 additions & 0 deletions internal/sources/mssql/mssql.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (

"github.com/goccy/go-yaml"
"github.com/googleapis/mcp-toolbox/internal/sources"
"github.com/googleapis/mcp-toolbox/internal/sources/sqlcommenter"
"github.com/googleapis/mcp-toolbox/internal/util"
"github.com/googleapis/mcp-toolbox/internal/util/orderedmap"
_ "github.com/microsoft/go-mssqldb"
Expand Down Expand Up @@ -106,6 +107,7 @@ func (s *Source) MSSQLDB() *sql.DB {
}

func (s *Source) RunSQL(ctx context.Context, statement string, params []any) (any, error) {
statement = sqlcommenter.AppendComment(ctx, statement, SourceType)
results, err := s.MSSQLDB().QueryContext(ctx, statement, params...)
if err != nil {
return nil, fmt.Errorf("unable to execute query: %w", err)
Expand Down
2 changes: 2 additions & 0 deletions internal/sources/mysql/mysql.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
driver "github.com/go-sql-driver/mysql"
"github.com/goccy/go-yaml"
"github.com/googleapis/mcp-toolbox/internal/sources"
"github.com/googleapis/mcp-toolbox/internal/sources/sqlcommenter"
"github.com/googleapis/mcp-toolbox/internal/tools/mysql/mysqlcommon"
"github.com/googleapis/mcp-toolbox/internal/util"
"github.com/googleapis/mcp-toolbox/internal/util/orderedmap"
Expand Down Expand Up @@ -106,6 +107,7 @@ func (s *Source) MySQLDatabase() string {
}

func (s *Source) RunSQL(ctx context.Context, statement string, params []any) (any, error) {
statement = sqlcommenter.AppendComment(ctx, statement, SourceType)
results, err := s.MySQLPool().QueryContext(ctx, statement, params...)
if err != nil {
return nil, fmt.Errorf("unable to execute query: %w", err)
Expand Down
Loading
Loading