e04ed9c211
CF: Deploy Dev Docs / deploy (push) Waiting to run
Sync Labels / build (push) Waiting to run
tests / unit tests (macos-latest) (push) Waiting to run
tests / unit tests (ubuntu-latest) (push) Waiting to run
tests / unit tests (windows-latest) (push) Waiting to run
395 lines
12 KiB
Go
395 lines
12 KiB
Go
// Copyright 2025 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 util
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"github.com/go-playground/validator/v10"
|
|
yaml "github.com/goccy/go-yaml"
|
|
"github.com/googleapis/mcp-toolbox/internal/log"
|
|
"github.com/googleapis/mcp-toolbox/internal/telemetry"
|
|
)
|
|
|
|
// GDAClientID is the client ID for Gemini Data Analytics
|
|
const GDAClientID = "GENAI_TOOLBOX"
|
|
|
|
// DecodeJSON decodes a given reader into an interface using the json decoder.
|
|
func DecodeJSON(r io.Reader, v interface{}) error {
|
|
defer io.Copy(io.Discard, r) //nolint:errcheck
|
|
d := json.NewDecoder(r)
|
|
// specify JSON numbers should get parsed to json.Number instead of float64 by default.
|
|
// This prevents loss between floats/ints.
|
|
d.UseNumber()
|
|
return d.Decode(v)
|
|
}
|
|
|
|
// ConvertNumbers traverses an interface and converts all json.Number
|
|
// instances to int64 or float64.
|
|
func ConvertNumbers(data any) (any, error) {
|
|
switch v := data.(type) {
|
|
// If it's a map, recursively convert the values.
|
|
case map[string]any:
|
|
for key, val := range v {
|
|
convertedVal, err := ConvertNumbers(val)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
v[key] = convertedVal
|
|
}
|
|
return v, nil
|
|
|
|
// If it's a slice, recursively convert the elements.
|
|
case []any:
|
|
for i, val := range v {
|
|
convertedVal, err := ConvertNumbers(val)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
v[i] = convertedVal
|
|
}
|
|
return v, nil
|
|
|
|
// If it's a json.Number, convert it to float or int
|
|
case json.Number:
|
|
// Check for a decimal point to decide the type.
|
|
if strings.Contains(v.String(), ".") {
|
|
return v.Float64()
|
|
}
|
|
return v.Int64()
|
|
|
|
// For all other types, return them as is.
|
|
default:
|
|
return data, nil
|
|
}
|
|
}
|
|
|
|
var _ yaml.InterfaceUnmarshalerContext = &DelayedUnmarshaler{}
|
|
|
|
// DelayedUnmarshaler is struct that saves the provided unmarshal function
|
|
// passed to UnmarshalYAML so it can be re-used later once the target interface
|
|
// is known.
|
|
type DelayedUnmarshaler struct {
|
|
unmarshal func(interface{}) error
|
|
}
|
|
|
|
func (d *DelayedUnmarshaler) UnmarshalYAML(ctx context.Context, unmarshal func(interface{}) error) error {
|
|
d.unmarshal = unmarshal
|
|
return nil
|
|
}
|
|
|
|
func (d *DelayedUnmarshaler) Unmarshal(v interface{}) error {
|
|
if d.unmarshal == nil {
|
|
return fmt.Errorf("nothing to unmarshal")
|
|
}
|
|
return d.unmarshal(v)
|
|
}
|
|
|
|
type contextKey string
|
|
|
|
// userAgentKey is the key used to store userAgent within context
|
|
const userAgentKey contextKey = "userAgent"
|
|
|
|
// WithUserAgent adds a user agent into the context as a value
|
|
func WithUserAgent(ctx context.Context, versionString string) context.Context {
|
|
userAgent := "genai-toolbox/" + versionString
|
|
return context.WithValue(ctx, userAgentKey, userAgent)
|
|
}
|
|
|
|
// UserAgentFromContext retrieves the user agent or return an error
|
|
func UserAgentFromContext(ctx context.Context) (string, error) {
|
|
if ua := ctx.Value(userAgentKey); ua != nil {
|
|
return ua.(string), nil
|
|
} else {
|
|
return "", fmt.Errorf("unable to retrieve user agent")
|
|
}
|
|
}
|
|
|
|
type UserAgentRoundTripper struct {
|
|
userAgent string
|
|
next http.RoundTripper
|
|
}
|
|
|
|
func NewUserAgentRoundTripper(ua string, next http.RoundTripper) *UserAgentRoundTripper {
|
|
return &UserAgentRoundTripper{
|
|
userAgent: ua,
|
|
next: next,
|
|
}
|
|
}
|
|
|
|
func (rt *UserAgentRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
// create a deep copy of the request
|
|
newReq := req.Clone(req.Context())
|
|
ua := newReq.Header.Get("User-Agent")
|
|
if ua == "" {
|
|
newReq.Header.Set("User-Agent", rt.userAgent)
|
|
} else {
|
|
newReq.Header.Set("User-Agent", ua+" "+rt.userAgent)
|
|
}
|
|
return rt.next.RoundTrip(newReq)
|
|
}
|
|
|
|
func NewStrictDecoder(v interface{}) (*yaml.Decoder, error) {
|
|
b, err := yaml.Marshal(v)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fail to marshal %q: %w", v, err)
|
|
}
|
|
|
|
dec := yaml.NewDecoder(
|
|
bytes.NewReader(b),
|
|
yaml.Strict(),
|
|
yaml.Validator(validator.New()),
|
|
)
|
|
return dec, nil
|
|
}
|
|
|
|
// loggerKey is the key used to store logger within context
|
|
const loggerKey contextKey = "logger"
|
|
|
|
// WithLogger adds a logger into the context as a value
|
|
func WithLogger(ctx context.Context, logger log.Logger) context.Context {
|
|
return context.WithValue(ctx, loggerKey, logger)
|
|
}
|
|
|
|
// LoggerFromContext retrieves the logger or return an error
|
|
func LoggerFromContext(ctx context.Context) (log.Logger, error) {
|
|
if logger, ok := ctx.Value(loggerKey).(log.Logger); ok {
|
|
return logger, nil
|
|
}
|
|
return nil, fmt.Errorf("unable to retrieve logger")
|
|
}
|
|
|
|
// LogPrimitiveDeprecation is a helper function to mark a primitive as
|
|
// deprecated.
|
|
//
|
|
// kind: kind of primitive. E.g. source / tool / authService / etc.
|
|
// primitiveType: the type of primitive being deprecated. E.g. postgres-sql
|
|
// migrationStep: the migration step for this deprecation. E.g. "Please use
|
|
// [Alternative] instead." / "Please remove this from your configuration file."
|
|
func LogPrimitiveDeprecation(ctx context.Context, kind string, primitiveType string, migrationStep string) error {
|
|
l, err := LoggerFromContext(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
msg := fmt.Sprintf("%s '%s' is deprecated and will be removed in the next major release. %s", kind, primitiveType, migrationStep)
|
|
l.WarnContext(ctx, msg)
|
|
return nil
|
|
}
|
|
|
|
const instrumentationKey contextKey = "instrumentation"
|
|
|
|
// WithInstrumentation adds an instrumentation into the context as a value
|
|
func WithInstrumentation(ctx context.Context, instrumentation *telemetry.Instrumentation) context.Context {
|
|
return context.WithValue(ctx, instrumentationKey, instrumentation)
|
|
}
|
|
|
|
// InstrumentationFromContext retrieves the instrumentation or return an error
|
|
func InstrumentationFromContext(ctx context.Context) (*telemetry.Instrumentation, error) {
|
|
if instrumentation, ok := ctx.Value(instrumentationKey).(*telemetry.Instrumentation); ok {
|
|
return instrumentation, nil
|
|
}
|
|
return nil, fmt.Errorf("unable to retrieve instrumentation")
|
|
}
|
|
|
|
// GenAIMetricAttrs holds gen_ai and network attributes for metrics
|
|
type GenAIMetricAttrs struct {
|
|
OperationName string
|
|
ToolName string
|
|
PromptName string
|
|
NetworkProtocolName string
|
|
NetworkProtocolVersion string
|
|
}
|
|
|
|
const genAIMetricAttrsKey contextKey = "genAIMetricAttrs"
|
|
|
|
// WithGenAIMetricAttrs adds GenAIMetricAttrs to the context
|
|
func WithGenAIMetricAttrs(ctx context.Context, attrs *GenAIMetricAttrs) context.Context {
|
|
return context.WithValue(ctx, genAIMetricAttrsKey, attrs)
|
|
}
|
|
|
|
// GenAIMetricAttrsFromContext retrieves GenAIMetricAttrs from context
|
|
func GenAIMetricAttrsFromContext(ctx context.Context) *GenAIMetricAttrs {
|
|
if attrs, ok := ctx.Value(genAIMetricAttrsKey).(*GenAIMetricAttrs); ok {
|
|
return attrs
|
|
}
|
|
return nil
|
|
}
|
|
|
|
const authTokenClaimsKey contextKey = "authTokenClaims"
|
|
|
|
// WithAuthTokenClaims adds auth token claims into the context as a value
|
|
func WithAuthTokenClaims(ctx context.Context, claims map[string]any) context.Context {
|
|
return context.WithValue(ctx, authTokenClaimsKey, claims)
|
|
}
|
|
|
|
// AuthTokenClaimsFromContext retrieves the auth token claims from context
|
|
func AuthTokenClaimsFromContext(ctx context.Context) map[string]any {
|
|
if claims, ok := ctx.Value(authTokenClaimsKey).(map[string]any); ok {
|
|
return claims
|
|
}
|
|
return nil
|
|
}
|
|
|
|
const clientIPKey contextKey = "clientIP"
|
|
|
|
// WithClientIP adds a client IP address into the context as a value
|
|
func WithClientIP(ctx context.Context, clientIP string) context.Context {
|
|
return context.WithValue(ctx, clientIPKey, clientIP)
|
|
}
|
|
|
|
// ClientIPFromContext retrieves the client IP address or returns false if not present
|
|
func ClientIPFromContext(ctx context.Context) (string, bool) {
|
|
if ip, ok := ctx.Value(clientIPKey).(string); ok {
|
|
return ip, true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// ExtractClientIP retrieves the leftmost client IP from X-Forwarded-For or X-Real-IP header
|
|
func ExtractClientIP(header http.Header) string {
|
|
if xff := header.Get("X-Forwarded-For"); xff != "" {
|
|
for _, ip := range strings.Split(xff, ",") {
|
|
if trimmed := strings.TrimSpace(ip); trimmed != "" {
|
|
return trimmed
|
|
}
|
|
}
|
|
}
|
|
if xri := header.Get("X-Real-IP"); xri != "" {
|
|
return strings.TrimSpace(xri)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// TelemetryAttributes holds client-provided telemetry metadata from _meta["dev.mcp-toolbox/telemetry"].
|
|
type TelemetryAttributes struct {
|
|
ClientName string
|
|
ClientVersion string
|
|
ClientModel string
|
|
ClientUserID string
|
|
ClientAgentID string
|
|
}
|
|
|
|
const telemetryAttrsKey contextKey = "telemetryAttrs"
|
|
|
|
// WithTelemetryAttributes adds TelemetryAttributes to the context
|
|
func WithTelemetryAttributes(ctx context.Context, attrs *TelemetryAttributes) context.Context {
|
|
return context.WithValue(ctx, telemetryAttrsKey, attrs)
|
|
}
|
|
|
|
// TelemetryAttributesFromContext retrieves TelemetryAttributes from context
|
|
func TelemetryAttributesFromContext(ctx context.Context) *TelemetryAttributes {
|
|
if attrs, ok := ctx.Value(telemetryAttrsKey).(*TelemetryAttributes); ok {
|
|
return attrs
|
|
}
|
|
return nil
|
|
}
|
|
|
|
const sqlCommenterEnabledKey contextKey = "sqlCommenterEnabled"
|
|
|
|
// WithSQLCommenterEnabled adds the sql-commenter-enabled flag to the context
|
|
func WithSQLCommenterEnabled(ctx context.Context, enabled bool) context.Context {
|
|
return context.WithValue(ctx, sqlCommenterEnabledKey, enabled)
|
|
}
|
|
|
|
// SQLCommenterEnabledFromContext retrieves the sql-commenter-enabled flag from context
|
|
func SQLCommenterEnabledFromContext(ctx context.Context) bool {
|
|
if enabled, ok := ctx.Value(sqlCommenterEnabledKey).(bool); ok {
|
|
return enabled
|
|
}
|
|
return false
|
|
}
|
|
|
|
// toolboxVersionKey is the key used to store toolbox version within context
|
|
const toolboxVersionKey contextKey = "toolboxVersion"
|
|
|
|
// WithToolboxVersionKey adds a toolbox version into the context as a value
|
|
func WithToolboxVersionKey(ctx context.Context, versionString string) context.Context {
|
|
return context.WithValue(ctx, toolboxVersionKey, versionString)
|
|
}
|
|
|
|
// ToolboxVersionFromContext retrieves the toolbox version or return an error
|
|
func ToolboxVersionFromContext(ctx context.Context) (string, error) {
|
|
if v, ok := ctx.Value(toolboxVersionKey).(string); ok && v != "" {
|
|
return v, nil
|
|
} else {
|
|
return "", fmt.Errorf("unable to retrieve toolbox version")
|
|
}
|
|
}
|
|
|
|
const ignoreUnknownToolsKey contextKey = "ignoreUnknownTools"
|
|
|
|
// WithIgnoreUnknownTools adds the ignore-unknown-tools flag to the context
|
|
func WithIgnoreUnknownTools(ctx context.Context, ignore bool) context.Context {
|
|
return context.WithValue(ctx, ignoreUnknownToolsKey, ignore)
|
|
}
|
|
|
|
// IgnoreUnknownToolsFromContext retrieves the ignore-unknown-tools flag from context
|
|
func IgnoreUnknownToolsFromContext(ctx context.Context) bool {
|
|
if ignore, ok := ctx.Value(ignoreUnknownToolsKey).(bool); ok {
|
|
return ignore
|
|
}
|
|
return false
|
|
}
|
|
|
|
// urlParamsKey is the key used to store URL parameters within context
|
|
const urlParamsKey contextKey = "urlParams"
|
|
|
|
// WithUrlParams adds URL parameters into the context as a value
|
|
func WithUrlParams(ctx context.Context, params map[string]string) context.Context {
|
|
return context.WithValue(ctx, urlParamsKey, params)
|
|
}
|
|
|
|
// UrlParamsFromContext retrieves URL parameters from context
|
|
func UrlParamsFromContext(ctx context.Context) (map[string]string, bool) {
|
|
if params, ok := ctx.Value(urlParamsKey).(map[string]string); ok {
|
|
return params, true
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
// SnakeFromCamelCase converts a camelCase string to snake_case.
|
|
func SnakeFromCamelCase(s string) string {
|
|
var result strings.Builder
|
|
for i, r := range s {
|
|
if i > 0 && unicode.IsUpper(r) {
|
|
result.WriteRune('_')
|
|
}
|
|
result.WriteRune(unicode.ToLower(r))
|
|
}
|
|
return result.String()
|
|
}
|
|
|
|
// enableDraftSpecs is the key to check if the server enabled mcp draft specs
|
|
const enableDraftSpecs contextKey = "enableDraftSpecs"
|
|
|
|
// WithEnableDraftSpecs adds enable draft specs bool into the context as a value
|
|
func WithEnableDraftSpecs(ctx context.Context, enableDraft bool) context.Context {
|
|
return context.WithValue(ctx, enableDraftSpecs, enableDraft)
|
|
}
|
|
|
|
// EnableDraftSpecsFromContext retrieves enable draft specs bool from context
|
|
func EnableDraftSpecsFromContext(ctx context.Context) (bool, bool) {
|
|
if enableDraft, ok := ctx.Value(enableDraftSpecs).(bool); ok {
|
|
return enableDraft, true
|
|
}
|
|
return false, false
|
|
}
|