mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-01-07 22:54:06 -05:00
feat: Support combining prebuilt and custom tool configurations (#2188)
## Description This PR updates the CLI to allow the --prebuilt flag to be used simultaneously with custom tool flags (--tools-file, --tools-files, or --tools-folder). This enables users to extend a standard prebuilt environment with their own custom tools and configurations. ### Key changes - Sequential Loading: Load prebuilt configurations first, then accumulate any specified custom configurations before merging. - Smart Defaults: Updated logic to only default to tools.yaml if no configuration flags are provided. - Legacy Auth Compatibility: Implemented an additive merge strategy for authentication. Legacy authSources from custom files are merged into the modern authServices map used by prebuilt tools. - Strict Validation: To prevent ambiguity, the server will throw an explicit error if a legacy authSource name conflicts with an existing authService name (e.g., from a prebuilt config). ## PR Checklist > Thank you for opening a Pull Request! Before submitting your PR, there are a > few things you can do to make sure it goes smoothly: - [x] Make sure you reviewed [CONTRIBUTING.md](https://github.com/googleapis/genai-toolbox/blob/main/CONTRIBUTING.md) - [x] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/genai-toolbox/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [x] Ensure the tests and linter pass - [x] Code coverage does not decrease (if any source code was changed) - [x] Appropriate docs were updated (if necessary) - [x] Make sure to add `!` if this involve a breaking change 🛠️ Fixes https://github.com/googleapis/genai-toolbox/issues/1220 --------- Co-authored-by: Yuan Teoh <45984206+Yuan325@users.noreply.github.com>
This commit is contained in:
164
cmd/root.go
164
cmd/root.go
@@ -355,12 +355,12 @@ func NewCommand(opts ...Option) *Command {
|
||||
flags.StringVarP(&cmd.cfg.Address, "address", "a", "127.0.0.1", "Address of the interface the server will listen on.")
|
||||
flags.IntVarP(&cmd.cfg.Port, "port", "p", 5000, "Port the server will listen on.")
|
||||
|
||||
flags.StringVar(&cmd.tools_file, "tools_file", "", "File path specifying the tool configuration. Cannot be used with --prebuilt.")
|
||||
flags.StringVar(&cmd.tools_file, "tools_file", "", "File path specifying the tool configuration. Cannot be used with --tools-files, or --tools-folder.")
|
||||
// deprecate tools_file
|
||||
_ = flags.MarkDeprecated("tools_file", "please use --tools-file instead")
|
||||
flags.StringVar(&cmd.tools_file, "tools-file", "", "File path specifying the tool configuration. Cannot be used with --prebuilt, --tools-files, or --tools-folder.")
|
||||
flags.StringSliceVar(&cmd.tools_files, "tools-files", []string{}, "Multiple file paths specifying tool configurations. Files will be merged. Cannot be used with --prebuilt, --tools-file, or --tools-folder.")
|
||||
flags.StringVar(&cmd.tools_folder, "tools-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 --prebuilt, --tools-file, or --tools-files.")
|
||||
flags.StringVar(&cmd.tools_file, "tools-file", "", "File path specifying the tool configuration. Cannot be used with --tools-files, or --tools-folder.")
|
||||
flags.StringSliceVar(&cmd.tools_files, "tools-files", []string{}, "Multiple file paths specifying tool configurations. Files will be merged. Cannot be used with --tools-file, or --tools-folder.")
|
||||
flags.StringVar(&cmd.tools_folder, "tools-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 --tools-file, or --tools-files.")
|
||||
flags.Var(&cmd.cfg.LogLevel, "log-level", "Specify the minimum level logged. Allowed: 'DEBUG', 'INFO', 'WARN', 'ERROR'.")
|
||||
flags.Var(&cmd.cfg.LoggingFormat, "logging-format", "Specify logging format to use. Allowed: 'standard' or 'JSON'.")
|
||||
flags.BoolVar(&cmd.cfg.TelemetryGCP, "telemetry-gcp", false, "Enable exporting directly to Google Cloud Monitoring.")
|
||||
@@ -368,7 +368,7 @@ func NewCommand(opts ...Option) *Command {
|
||||
flags.StringVar(&cmd.cfg.TelemetryServiceName, "telemetry-service-name", "toolbox", "Sets the value of the service.name resource attribute for telemetry data.")
|
||||
// Fetch prebuilt tools sources to customize the help description
|
||||
prebuiltHelp := fmt.Sprintf(
|
||||
"Use a prebuilt tool configuration by source type. Cannot be used with --tools-file. Allowed: '%s'.",
|
||||
"Use a prebuilt tool configuration by source type. Allowed: '%s'.",
|
||||
strings.Join(prebuiltconfigs.GetPrebuiltSources(), "', '"),
|
||||
)
|
||||
flags.StringVar(&cmd.prebuiltConfig, "prebuilt", "", prebuiltHelp)
|
||||
@@ -462,6 +462,9 @@ func mergeToolsFiles(files ...ToolsFile) (ToolsFile, error) {
|
||||
if _, exists := merged.AuthSources[name]; exists {
|
||||
conflicts = append(conflicts, fmt.Sprintf("authSource '%s' (file #%d)", name, fileIndex+1))
|
||||
} else {
|
||||
if merged.AuthSources == nil {
|
||||
merged.AuthSources = make(server.AuthServiceConfigs)
|
||||
}
|
||||
merged.AuthSources[name] = authSource
|
||||
}
|
||||
}
|
||||
@@ -838,16 +841,10 @@ func run(cmd *Command) error {
|
||||
}
|
||||
}()
|
||||
|
||||
var toolsFile ToolsFile
|
||||
var allToolsFiles []ToolsFile
|
||||
|
||||
// Load Prebuilt Configuration
|
||||
if cmd.prebuiltConfig != "" {
|
||||
// Make sure --prebuilt and --tools-file/--tools-files/--tools-folder flags are mutually exclusive
|
||||
if cmd.tools_file != "" || len(cmd.tools_files) > 0 || cmd.tools_folder != "" {
|
||||
errMsg := fmt.Errorf("--prebuilt and --tools-file/--tools-files/--tools-folder flags cannot be used simultaneously")
|
||||
cmd.logger.ErrorContext(ctx, errMsg.Error())
|
||||
return errMsg
|
||||
}
|
||||
// Use prebuilt tools
|
||||
buf, err := prebuiltconfigs.Get(cmd.prebuiltConfig)
|
||||
if err != nil {
|
||||
cmd.logger.ErrorContext(ctx, err.Error())
|
||||
@@ -858,72 +855,96 @@ func run(cmd *Command) error {
|
||||
// Append prebuilt.source to Version string for the User Agent
|
||||
cmd.cfg.Version += "+prebuilt." + cmd.prebuiltConfig
|
||||
|
||||
toolsFile, err = parseToolsFile(ctx, buf)
|
||||
parsed, err := parseToolsFile(ctx, buf)
|
||||
if err != nil {
|
||||
errMsg := fmt.Errorf("unable to parse prebuilt tool configuration: %w", err)
|
||||
cmd.logger.ErrorContext(ctx, errMsg.Error())
|
||||
return errMsg
|
||||
}
|
||||
} else if len(cmd.tools_files) > 0 {
|
||||
// Make sure --tools-file, --tools-files, and --tools-folder flags are mutually exclusive
|
||||
if cmd.tools_file != "" || cmd.tools_folder != "" {
|
||||
errMsg := fmt.Errorf("--tools-file, --tools-files, and --tools-folder flags cannot be used simultaneously")
|
||||
cmd.logger.ErrorContext(ctx, errMsg.Error())
|
||||
return errMsg
|
||||
}
|
||||
|
||||
// Use multiple tools files
|
||||
cmd.logger.InfoContext(ctx, fmt.Sprintf("Loading and merging %d tool configuration files", len(cmd.tools_files)))
|
||||
var err error
|
||||
toolsFile, err = loadAndMergeToolsFiles(ctx, cmd.tools_files)
|
||||
if err != nil {
|
||||
cmd.logger.ErrorContext(ctx, err.Error())
|
||||
return err
|
||||
}
|
||||
} else if cmd.tools_folder != "" {
|
||||
// Make sure --tools-folder and other flags are mutually exclusive
|
||||
if cmd.tools_file != "" || len(cmd.tools_files) > 0 {
|
||||
errMsg := fmt.Errorf("--tools-file, --tools-files, and --tools-folder flags cannot be used simultaneously")
|
||||
cmd.logger.ErrorContext(ctx, errMsg.Error())
|
||||
return errMsg
|
||||
}
|
||||
|
||||
// Use tools folder
|
||||
cmd.logger.InfoContext(ctx, fmt.Sprintf("Loading and merging all YAML files from directory: %s", cmd.tools_folder))
|
||||
var err error
|
||||
toolsFile, err = loadAndMergeToolsFolder(ctx, cmd.tools_folder)
|
||||
if err != nil {
|
||||
cmd.logger.ErrorContext(ctx, err.Error())
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Set default value of tools-file flag to tools.yaml
|
||||
if cmd.tools_file == "" {
|
||||
cmd.tools_file = "tools.yaml"
|
||||
}
|
||||
|
||||
// Read single tool file contents
|
||||
buf, err := os.ReadFile(cmd.tools_file)
|
||||
if err != nil {
|
||||
errMsg := fmt.Errorf("unable to read tool file at %q: %w", cmd.tools_file, err)
|
||||
cmd.logger.ErrorContext(ctx, errMsg.Error())
|
||||
return errMsg
|
||||
}
|
||||
|
||||
toolsFile, err = parseToolsFile(ctx, buf)
|
||||
if err != nil {
|
||||
errMsg := fmt.Errorf("unable to parse tool file at %q: %w", cmd.tools_file, err)
|
||||
cmd.logger.ErrorContext(ctx, errMsg.Error())
|
||||
return errMsg
|
||||
}
|
||||
allToolsFiles = append(allToolsFiles, parsed)
|
||||
}
|
||||
|
||||
cmd.cfg.SourceConfigs, cmd.cfg.AuthServiceConfigs, cmd.cfg.ToolConfigs, cmd.cfg.ToolsetConfigs, cmd.cfg.PromptConfigs = toolsFile.Sources, toolsFile.AuthServices, toolsFile.Tools, toolsFile.Toolsets, toolsFile.Prompts
|
||||
// Determine if Custom Files should be loaded
|
||||
// Check for explicit custom flags
|
||||
isCustomConfigured := cmd.tools_file != "" || len(cmd.tools_files) > 0 || cmd.tools_folder != ""
|
||||
|
||||
authSourceConfigs := toolsFile.AuthSources
|
||||
// Determine if default 'tools.yaml' should be used (No prebuilt AND No custom flags)
|
||||
useDefaultToolsFile := cmd.prebuiltConfig == "" && !isCustomConfigured
|
||||
|
||||
if useDefaultToolsFile {
|
||||
cmd.tools_file = "tools.yaml"
|
||||
isCustomConfigured = true
|
||||
}
|
||||
|
||||
// Load Custom Configurations
|
||||
if isCustomConfigured {
|
||||
// Enforce exclusivity among custom flags (tools-file vs tools-files vs tools-folder)
|
||||
if (cmd.tools_file != "" && len(cmd.tools_files) > 0) ||
|
||||
(cmd.tools_file != "" && cmd.tools_folder != "") ||
|
||||
(len(cmd.tools_files) > 0 && cmd.tools_folder != "") {
|
||||
errMsg := fmt.Errorf("--tools-file, --tools-files, and --tools-folder flags cannot be used simultaneously")
|
||||
cmd.logger.ErrorContext(ctx, errMsg.Error())
|
||||
return errMsg
|
||||
}
|
||||
|
||||
var customTools ToolsFile
|
||||
var err error
|
||||
|
||||
if len(cmd.tools_files) > 0 {
|
||||
// Use tools-files
|
||||
cmd.logger.InfoContext(ctx, fmt.Sprintf("Loading and merging %d tool configuration files", len(cmd.tools_files)))
|
||||
customTools, err = loadAndMergeToolsFiles(ctx, cmd.tools_files)
|
||||
} else if cmd.tools_folder != "" {
|
||||
// Use tools-folder
|
||||
cmd.logger.InfoContext(ctx, fmt.Sprintf("Loading and merging all YAML files from directory: %s", cmd.tools_folder))
|
||||
customTools, err = loadAndMergeToolsFolder(ctx, cmd.tools_folder)
|
||||
} else {
|
||||
// Use single file (tools-file or default `tools.yaml`)
|
||||
buf, readFileErr := os.ReadFile(cmd.tools_file)
|
||||
if readFileErr != nil {
|
||||
errMsg := fmt.Errorf("unable to read tool file at %q: %w", cmd.tools_file, readFileErr)
|
||||
cmd.logger.ErrorContext(ctx, errMsg.Error())
|
||||
return errMsg
|
||||
}
|
||||
customTools, err = parseToolsFile(ctx, buf)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("unable to parse tool file at %q: %w", cmd.tools_file, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
cmd.logger.ErrorContext(ctx, err.Error())
|
||||
return err
|
||||
}
|
||||
allToolsFiles = append(allToolsFiles, customTools)
|
||||
}
|
||||
|
||||
// Merge Everything
|
||||
// This will error if custom tools collide with prebuilt tools
|
||||
finalToolsFile, err := mergeToolsFiles(allToolsFiles...)
|
||||
if err != nil {
|
||||
cmd.logger.ErrorContext(ctx, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.cfg.SourceConfigs = finalToolsFile.Sources
|
||||
cmd.cfg.AuthServiceConfigs = finalToolsFile.AuthServices
|
||||
cmd.cfg.ToolConfigs = finalToolsFile.Tools
|
||||
cmd.cfg.ToolsetConfigs = finalToolsFile.Toolsets
|
||||
cmd.cfg.PromptConfigs = finalToolsFile.Prompts
|
||||
|
||||
authSourceConfigs := finalToolsFile.AuthSources
|
||||
if authSourceConfigs != nil {
|
||||
cmd.logger.WarnContext(ctx, "`authSources` is deprecated, use `authServices` instead")
|
||||
cmd.cfg.AuthServiceConfigs = authSourceConfigs
|
||||
|
||||
for k, v := range authSourceConfigs {
|
||||
if _, exists := cmd.cfg.AuthServiceConfigs[k]; exists {
|
||||
errMsg := fmt.Errorf("resource conflict detected: authSource '%s' has the same name as an existing authService. Please rename your authSource", k)
|
||||
cmd.logger.ErrorContext(ctx, errMsg.Error())
|
||||
return errMsg
|
||||
}
|
||||
cmd.cfg.AuthServiceConfigs[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
instrumentation, err := telemetry.CreateTelemetryInstrumentation(versionString)
|
||||
@@ -974,9 +995,8 @@ func run(cmd *Command) error {
|
||||
}()
|
||||
}
|
||||
|
||||
watchDirs, watchedFiles := resolveWatcherInputs(cmd.tools_file, cmd.tools_files, cmd.tools_folder)
|
||||
|
||||
if !cmd.cfg.DisableReload {
|
||||
if isCustomConfigured && !cmd.cfg.DisableReload {
|
||||
watchDirs, watchedFiles := resolveWatcherInputs(cmd.tools_file, cmd.tools_files, cmd.tools_folder)
|
||||
// start watching the file(s) or folder for changes to trigger dynamic reloading
|
||||
go watchChanges(ctx, watchDirs, watchedFiles, s)
|
||||
}
|
||||
|
||||
245
cmd/root_test.go
245
cmd/root_test.go
@@ -92,6 +92,21 @@ func invokeCommand(args []string) (*Command, string, error) {
|
||||
return c, buf.String(), err
|
||||
}
|
||||
|
||||
// invokeCommandWithContext executes the command with a context and returns the captured output.
|
||||
func invokeCommandWithContext(ctx context.Context, args []string) (*Command, string, error) {
|
||||
// Capture output using a buffer
|
||||
buf := new(bytes.Buffer)
|
||||
c := NewCommand(WithStreams(buf, buf))
|
||||
|
||||
c.SetArgs(args)
|
||||
c.SilenceUsage = true
|
||||
c.SilenceErrors = true
|
||||
c.SetContext(ctx)
|
||||
|
||||
err := c.Execute()
|
||||
return c, buf.String(), err
|
||||
}
|
||||
|
||||
func TestVersion(t *testing.T) {
|
||||
data, err := os.ReadFile("version.txt")
|
||||
if err != nil {
|
||||
@@ -1755,11 +1770,6 @@ func TestMutuallyExclusiveFlags(t *testing.T) {
|
||||
args []string
|
||||
errString string
|
||||
}{
|
||||
{
|
||||
desc: "--prebuilt and --tools-file",
|
||||
args: []string{"--prebuilt", "alloydb", "--tools-file", "my.yaml"},
|
||||
errString: "--prebuilt and --tools-file/--tools-files/--tools-folder flags cannot be used simultaneously",
|
||||
},
|
||||
{
|
||||
desc: "--tools-file and --tools-files",
|
||||
args: []string{"--tools-file", "my.yaml", "--tools-files", "a.yaml,b.yaml"},
|
||||
@@ -1902,3 +1912,228 @@ func TestMergeToolsFiles(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestPrebuiltAndCustomTools(t *testing.T) {
|
||||
t.Setenv("SQLITE_DATABASE", "test.db")
|
||||
// Setup custom tools file
|
||||
customContent := `
|
||||
tools:
|
||||
custom_tool:
|
||||
kind: http
|
||||
source: my-http
|
||||
method: GET
|
||||
path: /
|
||||
description: "A custom tool for testing"
|
||||
sources:
|
||||
my-http:
|
||||
kind: http
|
||||
baseUrl: http://example.com
|
||||
`
|
||||
customFile := filepath.Join(t.TempDir(), "custom.yaml")
|
||||
if err := os.WriteFile(customFile, []byte(customContent), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Tool Conflict File
|
||||
// SQLite prebuilt has a tool named 'list_tables'
|
||||
toolConflictContent := `
|
||||
tools:
|
||||
list_tables:
|
||||
kind: http
|
||||
source: my-http
|
||||
method: GET
|
||||
path: /
|
||||
description: "Conflicting tool"
|
||||
sources:
|
||||
my-http:
|
||||
kind: http
|
||||
baseUrl: http://example.com
|
||||
`
|
||||
toolConflictFile := filepath.Join(t.TempDir(), "tool_conflict.yaml")
|
||||
if err := os.WriteFile(toolConflictFile, []byte(toolConflictContent), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Source Conflict File
|
||||
// SQLite prebuilt has a source named 'sqlite-source'
|
||||
sourceConflictContent := `
|
||||
sources:
|
||||
sqlite-source:
|
||||
kind: http
|
||||
baseUrl: http://example.com
|
||||
tools:
|
||||
dummy_tool:
|
||||
kind: http
|
||||
source: sqlite-source
|
||||
method: GET
|
||||
path: /
|
||||
description: "Dummy"
|
||||
`
|
||||
sourceConflictFile := filepath.Join(t.TempDir(), "source_conflict.yaml")
|
||||
if err := os.WriteFile(sourceConflictFile, []byte(sourceConflictContent), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Toolset Conflict File
|
||||
// SQLite prebuilt has a toolset named 'sqlite_database_tools'
|
||||
toolsetConflictContent := `
|
||||
sources:
|
||||
dummy-src:
|
||||
kind: http
|
||||
baseUrl: http://example.com
|
||||
tools:
|
||||
dummy_tool:
|
||||
kind: http
|
||||
source: dummy-src
|
||||
method: GET
|
||||
path: /
|
||||
description: "Dummy"
|
||||
toolsets:
|
||||
sqlite_database_tools:
|
||||
- dummy_tool
|
||||
`
|
||||
toolsetConflictFile := filepath.Join(t.TempDir(), "toolset_conflict.yaml")
|
||||
if err := os.WriteFile(toolsetConflictFile, []byte(toolsetConflictContent), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
//Legacy Auth File
|
||||
authContent := `
|
||||
authSources:
|
||||
legacy-auth:
|
||||
kind: google
|
||||
clientId: "test-client-id"
|
||||
`
|
||||
authFile := filepath.Join(t.TempDir(), "auth.yaml")
|
||||
if err := os.WriteFile(authFile, []byte(authContent), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
args []string
|
||||
wantErr bool
|
||||
errString string
|
||||
cfgCheck func(server.ServerConfig) error
|
||||
}{
|
||||
{
|
||||
desc: "success mixed",
|
||||
args: []string{"--prebuilt", "sqlite", "--tools-file", customFile},
|
||||
wantErr: false,
|
||||
cfgCheck: func(cfg server.ServerConfig) error {
|
||||
if _, ok := cfg.ToolConfigs["custom_tool"]; !ok {
|
||||
return fmt.Errorf("custom tool not found")
|
||||
}
|
||||
if _, ok := cfg.ToolConfigs["list_tables"]; !ok {
|
||||
return fmt.Errorf("prebuilt tool 'list_tables' not found")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "tool conflict error",
|
||||
args: []string{"--prebuilt", "sqlite", "--tools-file", toolConflictFile},
|
||||
wantErr: true,
|
||||
errString: "resource conflicts detected",
|
||||
},
|
||||
{
|
||||
desc: "source conflict error",
|
||||
args: []string{"--prebuilt", "sqlite", "--tools-file", sourceConflictFile},
|
||||
wantErr: true,
|
||||
errString: "resource conflicts detected",
|
||||
},
|
||||
{
|
||||
desc: "toolset conflict error",
|
||||
args: []string{"--prebuilt", "sqlite", "--tools-file", toolsetConflictFile},
|
||||
wantErr: true,
|
||||
errString: "resource conflicts detected",
|
||||
},
|
||||
{
|
||||
desc: "legacy auth additive",
|
||||
args: []string{"--prebuilt", "sqlite", "--tools-file", authFile},
|
||||
wantErr: false,
|
||||
cfgCheck: func(cfg server.ServerConfig) error {
|
||||
if _, ok := cfg.AuthServiceConfigs["legacy-auth"]; !ok {
|
||||
return fmt.Errorf("legacy auth source not merged into auth services")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
cmd, output, err := invokeCommandWithContext(ctx, tc.args)
|
||||
|
||||
if tc.wantErr {
|
||||
if err == nil {
|
||||
t.Fatalf("expected an error but got none")
|
||||
}
|
||||
if !strings.Contains(err.Error(), tc.errString) {
|
||||
t.Errorf("expected error message to contain %q, but got %q", tc.errString, err.Error())
|
||||
}
|
||||
} else {
|
||||
if err != nil && err != context.DeadlineExceeded && err != context.Canceled {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !strings.Contains(output, "Server ready to serve!") {
|
||||
t.Errorf("server did not start successfully (no ready message found). Output:\n%s", output)
|
||||
}
|
||||
if tc.cfgCheck != nil {
|
||||
if err := tc.cfgCheck(cmd.cfg); err != nil {
|
||||
t.Errorf("config check failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultToolsFileBehavior(t *testing.T) {
|
||||
t.Setenv("SQLITE_DATABASE", "test.db")
|
||||
testCases := []struct {
|
||||
desc string
|
||||
args []string
|
||||
expectRun bool
|
||||
errString string
|
||||
}{
|
||||
{
|
||||
desc: "no flags (defaults to tools.yaml)",
|
||||
args: []string{},
|
||||
expectRun: false,
|
||||
errString: "tools.yaml", // Expect error because tools.yaml doesn't exist in test env
|
||||
},
|
||||
{
|
||||
desc: "prebuilt only (skips tools.yaml)",
|
||||
args: []string{"--prebuilt", "sqlite"},
|
||||
expectRun: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
|
||||
defer cancel()
|
||||
_, output, err := invokeCommandWithContext(ctx, tc.args)
|
||||
|
||||
if tc.expectRun {
|
||||
if err != nil && err != context.DeadlineExceeded && err != context.Canceled {
|
||||
t.Fatalf("expected server start, got error: %v", err)
|
||||
}
|
||||
// Verify it actually started
|
||||
if !strings.Contains(output, "Server ready to serve!") {
|
||||
t.Errorf("server did not start successfully (no ready message found). Output:\n%s", output)
|
||||
}
|
||||
} else {
|
||||
if err == nil {
|
||||
t.Fatalf("expected error reading default file, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), tc.errString) {
|
||||
t.Errorf("expected error message to contain %q, but got %q", tc.errString, err.Error())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,14 +16,14 @@ description: >
|
||||
| | `--log-level` | Specify the minimum level logged. Allowed: 'DEBUG', 'INFO', 'WARN', 'ERROR'. | `info` |
|
||||
| | `--logging-format` | Specify logging format to use. Allowed: 'standard' or 'JSON'. | `standard` |
|
||||
| `-p` | `--port` | Port the server will listen on. | `5000` |
|
||||
| | `--prebuilt` | Use a prebuilt tool configuration by source type. Cannot be used with --tools-file. See [Prebuilt Tools Reference](prebuilt-tools.md) for allowed values. | |
|
||||
| | `--prebuilt` | Use a prebuilt tool configuration by source type. See [Prebuilt Tools Reference](prebuilt-tools.md) for allowed values. | |
|
||||
| | `--stdio` | Listens via MCP STDIO instead of acting as a remote HTTP server. | |
|
||||
| | `--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` |
|
||||
| | `--tools-file` | File path specifying the tool configuration. Cannot be used with --prebuilt, --tools-files, or --tools-folder. | |
|
||||
| | `--tools-files` | Multiple file paths specifying tool configurations. Files will be merged. Cannot be used with --prebuilt, --tools-file, or --tools-folder. | |
|
||||
| | `--tools-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 --prebuilt, --tools-file, or --tools-files. | |
|
||||
| | `--tools-file` | File path specifying the tool configuration. Cannot be used with --tools-files or --tools-folder. | |
|
||||
| | `--tools-files` | Multiple file paths specifying tool configurations. Files will be merged. Cannot be used with --tools-file or --tools-folder. | |
|
||||
| | `--tools-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 --tools-file or --tools-files. | |
|
||||
| | `--ui` | Launches the Toolbox UI web server. | |
|
||||
| | `--allowed-origins` | Specifies a list of origins permitted to access this server. | `*` |
|
||||
| `-v` | `--version` | version for toolbox | |
|
||||
@@ -46,6 +46,9 @@ description: >
|
||||
```bash
|
||||
# Basic server with custom port configuration
|
||||
./toolbox --tools-file "tools.yaml" --port 8080
|
||||
|
||||
# Server with prebuilt + custom tools configurations
|
||||
./toolbox --tools-file tools.yaml --prebuilt alloydb-postgres
|
||||
```
|
||||
|
||||
### Tool Configuration Sources
|
||||
@@ -72,8 +75,8 @@ The CLI supports multiple mutually exclusive ways to specify tool configurations
|
||||
|
||||
{{< notice tip >}}
|
||||
The CLI enforces mutual exclusivity between configuration source flags,
|
||||
preventing simultaneous use of `--prebuilt` with file-based options, and
|
||||
ensuring only one of `--tools-file`, `--tools-files`, or `--tools-folder` is
|
||||
preventing simultaneous use of the file-based options ensuring only one of
|
||||
`--tools-file`, `--tools-files`, or `--tools-folder` is
|
||||
used at a time.
|
||||
{{< /notice >}}
|
||||
|
||||
|
||||
@@ -13,6 +13,12 @@ allowing developers to interact with and take action on databases.
|
||||
See guides, [Connect from your IDE](../how-to/connect-ide/_index.md), for
|
||||
details on how to connect your AI tools (IDEs) to databases via Toolbox and MCP.
|
||||
|
||||
{{< notice tip >}}
|
||||
You can now use `--prebuilt` along `--tools-file`, `--tools-files`, or
|
||||
`--tools-folder` to combine prebuilt configs with custom tools.
|
||||
See [Usage Examples](../reference/cli.md#examples).
|
||||
{{< /notice >}}
|
||||
|
||||
## AlloyDB Postgres
|
||||
|
||||
* `--prebuilt` value: `alloydb-postgres`
|
||||
|
||||
Reference in New Issue
Block a user