Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:32:45 +08:00

302 lines
9.4 KiB
Go

// 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 internal
import (
"context"
"fmt"
"io"
"os"
"slices"
"strings"
"github.com/googleapis/mcp-toolbox/internal/log"
"github.com/googleapis/mcp-toolbox/internal/prebuiltconfigs"
"github.com/googleapis/mcp-toolbox/internal/server"
"github.com/googleapis/mcp-toolbox/internal/telemetry"
"github.com/googleapis/mcp-toolbox/internal/util"
)
type IOStreams struct {
In io.Reader
Out io.Writer
ErrOut io.Writer
}
// ToolboxOptions holds dependencies shared by all commands.
type ToolboxOptions struct {
IOStreams IOStreams
Logger log.Logger
Cfg server.ServerConfig
Config string
Configs []string
ConfigFolder string
PrebuiltConfigs []string
VersionNum string
}
// Option defines a function that modifies the ToolboxOptions struct.
type Option func(*ToolboxOptions)
// NewToolboxOptions creates a new instance with defaults, then applies any
// provided options.
func NewToolboxOptions(opts ...Option) *ToolboxOptions {
o := &ToolboxOptions{
IOStreams: IOStreams{
In: os.Stdin,
Out: os.Stdout,
ErrOut: os.Stderr,
},
}
for _, opt := range opts {
opt(o)
}
return o
}
// Apply allows you to update an EXISTING ToolboxOptions instance.
// This is useful for "late binding".
func (o *ToolboxOptions) Apply(opts ...Option) {
for _, opt := range opts {
opt(o)
}
}
// WithIOStreams updates the IO streams.
func WithIOStreams(out, err io.Writer) Option {
return func(o *ToolboxOptions) {
o.IOStreams.Out = out
o.IOStreams.ErrOut = err
}
}
// Setup create logger and telemetry instrumentations.
func (opts *ToolboxOptions) Setup(ctx context.Context) (context.Context, func(context.Context) error, error) {
// If stdio, set logger's out stream (usually DEBUG and INFO logs) to
// errStream
loggerOut := opts.IOStreams.Out
if opts.Cfg.Stdio {
loggerOut = opts.IOStreams.ErrOut
}
// Handle logger separately from config
logger, err := log.NewLogger(opts.Cfg.LoggingFormat.String(), opts.Cfg.LogLevel.String(), loggerOut, opts.IOStreams.ErrOut)
if err != nil {
return ctx, nil, fmt.Errorf("unable to initialize logger: %w", err)
}
ctx = util.WithLogger(ctx, logger)
opts.Logger = logger
ctx = util.WithIgnoreUnknownTools(ctx, opts.Cfg.IgnoreUnknownTools)
logger.InfoContext(ctx, fmt.Sprintf("Starting MCP Toolbox for Databases version %s", opts.Cfg.Version))
// Set up OpenTelemetry
otelShutdown, err := telemetry.SetupOTel(ctx, opts.Cfg.Version, opts.Cfg.TelemetryOTLP, opts.Cfg.TelemetryGCP, opts.Cfg.TelemetryGCPProject, opts.Cfg.TelemetryServiceName)
if err != nil {
errMsg := fmt.Errorf("error setting up OpenTelemetry: %w", err)
logger.ErrorContext(ctx, errMsg.Error())
return ctx, nil, errMsg
}
shutdownFunc := func(ctx context.Context) error {
err := otelShutdown(ctx)
if err != nil {
errMsg := fmt.Errorf("error shutting down OpenTelemetry: %w", err)
logger.ErrorContext(ctx, errMsg.Error())
return err
}
return nil
}
instrumentation, err := telemetry.CreateTelemetryInstrumentation(opts.Cfg.Version)
if err != nil {
errMsg := fmt.Errorf("unable to create telemetry instrumentation: %w", err)
logger.ErrorContext(ctx, errMsg.Error())
return ctx, shutdownFunc, errMsg
}
ctx = util.WithInstrumentation(ctx, instrumentation)
return ctx, shutdownFunc, nil
}
// GetCustomConfigFiles retrieves the list of custom config file paths
func (opts *ToolboxOptions) GetCustomConfigFiles(ctx context.Context) ([]string, bool, error) {
// Determine if Custom Files should be loaded
// Check for explicit custom flags
isCustomConfigured := opts.Config != "" || len(opts.Configs) > 0 || opts.ConfigFolder != ""
logger, err := util.LoggerFromContext(ctx)
if err != nil {
return nil, isCustomConfigured, err
}
// Load Custom Configurations
if isCustomConfigured {
if len(opts.Configs) > 0 {
// Use tools-files
logger.InfoContext(ctx, fmt.Sprintf("retrieving %d tool configuration files", len(opts.Configs)))
return opts.Configs, isCustomConfigured, nil
} else if opts.ConfigFolder != "" {
// Use tools-folder
allFiles, err := GetPathsFromConfigFolder(ctx, opts.ConfigFolder)
return allFiles, isCustomConfigured, err
} else {
// use tools-file
return []string{opts.Config}, isCustomConfigured, nil
}
}
// Determine if default 'tools.yaml' should be used (No prebuilt AND No custom flags)
useDefaultConfig := len(opts.PrebuiltConfigs) == 0
if useDefaultConfig {
// else we will add the default path regardless
return []string{"tools.yaml"}, true, nil
}
// no custom config files are found
// server are likely using prebuilt configs
return []string{}, false, nil
}
// LoadConfig checks and merge files that should be loaded into the server
func (opts *ToolboxOptions) LoadConfig(ctx context.Context, parser *ConfigParser) (bool, error) {
// get all the file paths for custom config file
filesPaths, isCustomConfigured, err := opts.GetCustomConfigFiles(ctx)
if err != nil {
return isCustomConfigured, err
}
logger, err := util.LoggerFromContext(ctx)
if err != nil {
return isCustomConfigured, err
}
var allConfigs []Config
// Load Prebuilt Configuration
if len(opts.PrebuiltConfigs) > 0 {
slices.Sort(opts.PrebuiltConfigs)
sourcesList := strings.Join(opts.PrebuiltConfigs, ", ")
logMsg := fmt.Sprintf("Using prebuilt tool configurations for: %s", sourcesList)
logger.InfoContext(ctx, logMsg)
logger.WarnContext(ctx, "These prebuilt configs are intended for 'build-time' use cases, where agents are helping trusted developers build things. They are not secure enough for 'run time' use cases, where the agent will be talking to potentially untrusted developers.")
for _, configName := range opts.PrebuiltConfigs {
if !strings.Contains(configName, "/") {
for _, sep := range []string{".", ":", "@"} {
if strings.Contains(configName, sep) {
parts := strings.SplitN(configName, sep, 2)
if slices.Contains(prebuiltconfigs.GetPrebuiltSources(), parts[0]) {
errMsg := fmt.Errorf("invalid prebuilt config format '%s'. Did you mean '%s/%s'? Use '/' to specify a toolset", configName, parts[0], parts[1])
logger.ErrorContext(ctx, errMsg.Error())
return isCustomConfigured, errMsg
}
}
}
}
sourceName, toolsetName, _ := strings.Cut(configName, "/")
buf, err := prebuiltconfigs.Get(sourceName)
if err != nil {
logger.ErrorContext(ctx, err.Error())
return isCustomConfigured, err
}
// Parse into Config struct
parsed, err := parser.ParseConfig(ctx, buf)
if err != nil {
errMsg := fmt.Errorf("unable to parse prebuilt tool configuration for '%s': %w", configName, err)
logger.ErrorContext(ctx, errMsg.Error())
return isCustomConfigured, errMsg
}
if toolsetName != "" {
targetToolset, exists := parsed.Toolsets[toolsetName]
if !exists {
var available []string
for k := range parsed.Toolsets {
available = append(available, k)
}
slices.Sort(available)
errMsg := fmt.Errorf("toolset '%s' not found in prebuilt configuration '%s'. Available toolsets: %s", toolsetName, sourceName, strings.Join(available, ", "))
logger.ErrorContext(ctx, errMsg.Error())
return isCustomConfigured, errMsg
}
// Filter tools to only include those in the target toolset
filteredTools := make(server.ToolConfigs)
for _, tName := range targetToolset.ToolNames {
if tCfg, tExists := parsed.Tools[tName]; tExists {
filteredTools[tName] = tCfg
}
}
parsed.Tools = filteredTools
// Filter toolsets to only include the target toolset
filteredToolsets := make(server.ToolsetConfigs)
filteredToolsets[toolsetName] = targetToolset
parsed.Toolsets = filteredToolsets
}
allConfigs = append(allConfigs, parsed)
}
}
// Load Custom Configurations
if isCustomConfigured {
customTools, err := parser.LoadAndMergeConfigs(ctx, filesPaths)
if err != nil {
logger.ErrorContext(ctx, err.Error())
return isCustomConfigured, err
}
allConfigs = append(allConfigs, customTools)
}
// Modify version string based on loaded configurations
if len(opts.PrebuiltConfigs) > 0 {
tag := "prebuilt"
if isCustomConfigured {
tag = "custom"
}
// prebuiltConfigs is already sorted above
for _, configName := range opts.PrebuiltConfigs {
sanitizedConfigName := strings.ReplaceAll(configName, "/", ".")
opts.Cfg.Version += fmt.Sprintf("+%s.%s", tag, sanitizedConfigName)
}
}
// Merge Everything
// This will error if custom tools collide with prebuilt tools
finalConfig, err := mergeConfigs(allConfigs...)
if err != nil {
logger.ErrorContext(ctx, err.Error())
return isCustomConfigured, err
}
opts.Cfg.SourceConfigs = finalConfig.Sources
opts.Cfg.AuthServiceConfigs = finalConfig.AuthServices
opts.Cfg.EmbeddingModelConfigs = finalConfig.EmbeddingModels
opts.Cfg.ToolConfigs = finalConfig.Tools
opts.Cfg.ToolsetConfigs = finalConfig.Toolsets
opts.Cfg.PromptConfigs = finalConfig.Prompts
return isCustomConfigured, nil
}