Skip to content
Open
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 @@ -22,6 +22,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` | Enable appending SQLCommenter-format comments to SQL statements. | `false` |
| | `--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 @@ -64,6 +64,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
35 changes: 26 additions & 9 deletions internal/server/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,14 +145,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 @@ -161,15 +162,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 @@ -191,6 +204,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 @@ -238,7 +253,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 @@ -463,6 +478,8 @@ func httpHandler(s *Server, w http.ResponseWriter, r *http.Request) {

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 @@ -475,7 +492,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
40 changes: 21 additions & 19 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,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 @@ -378,15 +379,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
2 changes: 2 additions & 0 deletions internal/sources/postgres/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,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/jackc/pgx/v5"
Expand Down Expand Up @@ -101,6 +102,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
117 changes: 117 additions & 0 deletions internal/sources/sqlcommenter/sqlcommenter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package sqlcommenter

import (
"context"
"fmt"
"net/url"
"sort"
"strings"

"github.com/googleapis/mcp-toolbox/internal/util"
"go.opentelemetry.io/otel/trace"
)

// AppendComment appends a SQLCommenter-format comment to the given SQL statement.
// It gathers attributes from the context (trace, server, client, tool metadata)
// and the provided dbSystemName, then appends them as key='value' pairs sorted
// alphabetically.
func AppendComment(ctx context.Context, statement string, dbSystemName string) string {
// Only append SQL comments when sql-commenter is enabled
if !util.SQLCommenterEnabledFromContext(ctx) {
return statement
}

pairs := collectAttributes(ctx, dbSystemName)
if len(pairs) == 0 {
return statement
}

// Sort keys alphabetically
keys := make([]string, 0, len(pairs))
for k := range pairs {
keys = append(keys, k)
}
sort.Strings(keys)

// Build comment in SQLCommenter format: key='url_encoded_value'
parts := make([]string, 0, len(keys))
for _, k := range keys {
encodedKey := url.QueryEscape(k)
encodedVal := url.QueryEscape(pairs[k])
parts = append(parts, fmt.Sprintf("%s='%s'", encodedKey, encodedVal))
}

comment := strings.Join(parts, ",")
return "/*" + comment + "*/ " + statement
}

// collectAttributes gathers all available SQLCommenter attributes from context.
func collectAttributes(ctx context.Context, dbSystemName string) map[string]string {
attrs := make(map[string]string)

// traceparent from OTel span context
spanCtx := trace.SpanFromContext(ctx).SpanContext()
if spanCtx.IsValid() {
traceparent := fmt.Sprintf("00-%s-%s-%s",
spanCtx.TraceID().String(),
spanCtx.SpanID().String(),
spanCtx.TraceFlags().String(),
)
attrs["traceparent"] = traceparent
}

// server from UserAgent context
if ua, err := util.UserAgentFromContext(ctx); err == nil && ua != "" {
attrs["server"] = ua
}

// db.system.name from parameter
if dbSystemName != "" {
attrs["db.system.name"] = dbSystemName
}

// tool.name from GenAIMetricAttrs
if genAI := util.GenAIMetricAttrsFromContext(ctx); genAI != nil {
if genAI.ToolName != "" {
attrs["tool.name"] = genAI.ToolName
}
}

// Client attributes from TelemetryAttributes
if ta := util.TelemetryAttributesFromContext(ctx); ta != nil {
// Combined client = name/version
if ta.ClientName != "" && ta.ClientVersion != "" {
attrs["client"] = ta.ClientName + "/" + ta.ClientVersion
} else if ta.ClientName != "" {
attrs["client"] = ta.ClientName
} else if ta.ClientVersion != "" {
attrs["client"] = ta.ClientVersion
}

if ta.ClientModel != "" {
attrs["client.model"] = ta.ClientModel
}
if ta.ClientUserID != "" {
attrs["client.user.id"] = ta.ClientUserID
}
if ta.ClientAgentID != "" {
attrs["client.agent.id"] = ta.ClientAgentID
}
}

return attrs
}
Loading
Loading