mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-01-09 07:28:05 -05:00
## Description Add support for healthcare source, tool and prebuilt config. This branch consist of all previously approved PRs. 🛠️ Fixes #1648 --------- Co-authored-by: Marwan Tammam <15021613+Quarz0@users.noreply.github.com>
962 lines
40 KiB
Go
962 lines
40 KiB
Go
// Copyright 2024 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 cmd
|
|
|
|
import (
|
|
"context"
|
|
_ "embed"
|
|
"fmt"
|
|
"io"
|
|
"maps"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"slices"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/fsnotify/fsnotify"
|
|
yaml "github.com/goccy/go-yaml"
|
|
"github.com/googleapis/genai-toolbox/internal/auth"
|
|
"github.com/googleapis/genai-toolbox/internal/log"
|
|
"github.com/googleapis/genai-toolbox/internal/prebuiltconfigs"
|
|
"github.com/googleapis/genai-toolbox/internal/server"
|
|
"github.com/googleapis/genai-toolbox/internal/sources"
|
|
"github.com/googleapis/genai-toolbox/internal/telemetry"
|
|
"github.com/googleapis/genai-toolbox/internal/tools"
|
|
"github.com/googleapis/genai-toolbox/internal/util"
|
|
|
|
// Import tool packages for side effect of registration
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/alloydb/alloydbcreatecluster"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/alloydb/alloydbcreateinstance"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/alloydb/alloydbcreateuser"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/alloydb/alloydbgetcluster"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/alloydb/alloydbgetinstance"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/alloydb/alloydbgetuser"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/alloydb/alloydblistclusters"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/alloydb/alloydblistinstances"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/alloydb/alloydblistusers"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/alloydb/alloydbwaitforoperation"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/alloydbainl"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/bigquery/bigqueryanalyzecontribution"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/bigquery/bigqueryconversationalanalytics"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/bigquery/bigqueryexecutesql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/bigquery/bigqueryforecast"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/bigquery/bigquerygetdatasetinfo"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/bigquery/bigquerygettableinfo"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/bigquery/bigquerylistdatasetids"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/bigquery/bigquerylisttableids"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/bigquery/bigquerysearchcatalog"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/bigquery/bigquerysql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/bigtable"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/cassandra/cassandracql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/clickhouse/clickhouseexecutesql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/clickhouse/clickhouselistdatabases"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/clickhouse/clickhouselisttables"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/clickhouse/clickhousesql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/cloudhealthcare/cloudhealthcarefhirfetchpage"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/cloudhealthcare/cloudhealthcarefhirpatienteverything"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/cloudhealthcare/cloudhealthcarefhirpatientsearch"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/cloudhealthcare/cloudhealthcaregetdataset"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/cloudhealthcare/cloudhealthcaregetdicomstore"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/cloudhealthcare/cloudhealthcaregetdicomstoremetrics"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/cloudhealthcare/cloudhealthcaregetfhirresource"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/cloudhealthcare/cloudhealthcaregetfhirstore"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/cloudhealthcare/cloudhealthcaregetfhirstoremetrics"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/cloudhealthcare/cloudhealthcarelistdicomstores"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/cloudhealthcare/cloudhealthcarelistfhirstores"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/cloudhealthcare/cloudhealthcareretrieverendereddicominstance"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/cloudhealthcare/cloudhealthcaresearchdicominstances"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/cloudhealthcare/cloudhealthcaresearchdicomseries"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/cloudhealthcare/cloudhealthcaresearchdicomstudies"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/cloudmonitoring"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/cloudsql/cloudsqlcreatedatabase"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/cloudsql/cloudsqlcreateusers"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/cloudsql/cloudsqlgetinstances"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/cloudsql/cloudsqllistdatabases"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/cloudsql/cloudsqllistinstances"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/cloudsql/cloudsqlwaitforoperation"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/cloudsqlmssql/cloudsqlmssqlcreateinstance"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/cloudsqlmysql/cloudsqlmysqlcreateinstance"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/cloudsqlpg/cloudsqlpgcreateinstances"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/couchbase"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/dataform/dataformcompilelocal"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/dataplex/dataplexlookupentry"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/dataplex/dataplexsearchaspecttypes"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/dataplex/dataplexsearchentries"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/dgraph"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/elasticsearch/elasticsearchesql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/firebird/firebirdexecutesql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/firebird/firebirdsql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/firestore/firestoreadddocuments"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/firestore/firestoredeletedocuments"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/firestore/firestoregetdocuments"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/firestore/firestoregetrules"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/firestore/firestorelistcollections"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/firestore/firestorequery"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/firestore/firestorequerycollection"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/firestore/firestoreupdatedocument"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/firestore/firestorevalidaterules"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/http"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookeradddashboardelement"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerconversationalanalytics"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookercreateprojectfile"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerdeleteprojectfile"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerdevmode"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetconnectiondatabases"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetconnections"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetconnectionschemas"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetconnectiontablecolumns"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetconnectiontables"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetdashboards"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetdimensions"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetexplores"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetfilters"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetlooks"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetmeasures"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetmodels"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetparameters"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetprojectfile"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetprojectfiles"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetprojects"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerhealthanalyze"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerhealthpulse"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerhealthvacuum"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookermakedashboard"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookermakelook"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerquery"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerquerysql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerqueryurl"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerrundashboard"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerrunlook"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerupdateprojectfile"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/mindsdb/mindsdbexecutesql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/mindsdb/mindsdbsql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/mongodb/mongodbaggregate"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/mongodb/mongodbdeletemany"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/mongodb/mongodbdeleteone"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/mongodb/mongodbfind"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/mongodb/mongodbfindone"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/mongodb/mongodbinsertmany"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/mongodb/mongodbinsertone"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/mongodb/mongodbupdatemany"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/mongodb/mongodbupdateone"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/mssql/mssqlexecutesql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/mssql/mssqllisttables"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/mssql/mssqlsql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/mysql/mysqlexecutesql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/mysql/mysqllistactivequeries"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/mysql/mysqllisttablefragmentation"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/mysql/mysqllisttables"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/mysql/mysqllisttablesmissinguniqueindexes"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/mysql/mysqlsql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/neo4j/neo4jcypher"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/neo4j/neo4jexecutecypher"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/neo4j/neo4jschema"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/oceanbase/oceanbaseexecutesql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/oceanbase/oceanbasesql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/oracle/oracleexecutesql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/oracle/oraclesql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgresexecutesql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslistactivequeries"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslistavailableextensions"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslistinstalledextensions"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslistschemas"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslisttables"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslistviews"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgressql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/redis"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/serverlessspark/serverlesssparkcancelbatch"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/serverlessspark/serverlesssparkgetbatch"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/serverlessspark/serverlesssparklistbatches"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/singlestore/singlestoreexecutesql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/singlestore/singlestoresql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/spanner/spannerexecutesql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/spanner/spannerlisttables"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/spanner/spannersql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/sqlite/sqliteexecutesql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/sqlite/sqlitesql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/tidb/tidbexecutesql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/tidb/tidbsql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/trino/trinoexecutesql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/trino/trinosql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/utility/wait"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/valkey"
|
|
_ "github.com/googleapis/genai-toolbox/internal/tools/yugabytedbsql"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/alloydbadmin"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/alloydbpg"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/bigquery"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/bigtable"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/cassandra"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/clickhouse"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/cloudhealthcare"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/cloudmonitoring"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/cloudsqladmin"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/cloudsqlmssql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/cloudsqlmysql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/cloudsqlpg"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/couchbase"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/dataplex"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/dgraph"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/elasticsearch"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/firebird"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/firestore"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/http"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/looker"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/mindsdb"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/mongodb"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/mssql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/mysql"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/neo4j"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/oceanbase"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/oracle"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/postgres"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/redis"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/serverlessspark"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/singlestore"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/spanner"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/sqlite"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/tidb"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/trino"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/valkey"
|
|
_ "github.com/googleapis/genai-toolbox/internal/sources/yugabytedb"
|
|
)
|
|
|
|
var (
|
|
// versionString stores the full semantic version, including build metadata.
|
|
versionString string
|
|
// versionNum indicates the numerical part fo the version
|
|
//go:embed version.txt
|
|
versionNum string
|
|
// metadataString indicates additional build or distribution metadata.
|
|
buildType string = "dev" // should be one of "dev", "binary", or "container"
|
|
// commitSha is the git commit it was built from
|
|
commitSha string
|
|
)
|
|
|
|
func init() {
|
|
versionString = semanticVersion()
|
|
}
|
|
|
|
// semanticVersion returns the version of the CLI including a compile-time metadata.
|
|
func semanticVersion() string {
|
|
metadataStrings := []string{buildType, runtime.GOOS, runtime.GOARCH}
|
|
if commitSha != "" {
|
|
metadataStrings = append(metadataStrings, commitSha)
|
|
}
|
|
v := strings.TrimSpace(versionNum) + "+" + strings.Join(metadataStrings, ".")
|
|
return v
|
|
}
|
|
|
|
// Execute adds all child commands to the root command and sets flags appropriately.
|
|
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
|
func Execute() {
|
|
if err := NewCommand().Execute(); err != nil {
|
|
exit := 1
|
|
os.Exit(exit)
|
|
}
|
|
}
|
|
|
|
// Command represents an invocation of the CLI.
|
|
type Command struct {
|
|
*cobra.Command
|
|
|
|
cfg server.ServerConfig
|
|
logger log.Logger
|
|
tools_file string
|
|
tools_files []string
|
|
tools_folder string
|
|
prebuiltConfig string
|
|
inStream io.Reader
|
|
outStream io.Writer
|
|
errStream io.Writer
|
|
}
|
|
|
|
// NewCommand returns a Command object representing an invocation of the CLI.
|
|
func NewCommand(opts ...Option) *Command {
|
|
in := os.Stdin
|
|
out := os.Stdout
|
|
err := os.Stderr
|
|
|
|
baseCmd := &cobra.Command{
|
|
Use: "toolbox",
|
|
Version: versionString,
|
|
SilenceErrors: true,
|
|
}
|
|
cmd := &Command{
|
|
Command: baseCmd,
|
|
inStream: in,
|
|
outStream: out,
|
|
errStream: err,
|
|
}
|
|
|
|
for _, o := range opts {
|
|
o(cmd)
|
|
}
|
|
|
|
// Do not print Usage on runtime error
|
|
cmd.SilenceUsage = true
|
|
|
|
// Set server version
|
|
cmd.cfg.Version = versionString
|
|
|
|
// set baseCmd in, out and err the same as cmd.
|
|
baseCmd.SetIn(cmd.inStream)
|
|
baseCmd.SetOut(cmd.outStream)
|
|
baseCmd.SetErr(cmd.errStream)
|
|
|
|
flags := cmd.Flags()
|
|
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.")
|
|
// 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.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.")
|
|
flags.StringVar(&cmd.cfg.TelemetryOTLP, "telemetry-otlp", "", "Enable exporting using OpenTelemetry Protocol (OTLP) to the specified endpoint (e.g. 'http://127.0.0.1:4318')")
|
|
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'.",
|
|
strings.Join(prebuiltconfigs.GetPrebuiltSources(), "', '"),
|
|
)
|
|
flags.StringVar(&cmd.prebuiltConfig, "prebuilt", "", prebuiltHelp)
|
|
flags.BoolVar(&cmd.cfg.Stdio, "stdio", false, "Listens via MCP STDIO instead of acting as a remote HTTP server.")
|
|
flags.BoolVar(&cmd.cfg.DisableReload, "disable-reload", false, "Disables dynamic reloading of tools file.")
|
|
flags.BoolVar(&cmd.cfg.UI, "ui", false, "Launches the Toolbox UI web server.")
|
|
|
|
// wrap RunE command so that we have access to original Command object
|
|
cmd.RunE = func(*cobra.Command, []string) error { return run(cmd) }
|
|
|
|
return cmd
|
|
}
|
|
|
|
type ToolsFile struct {
|
|
Sources server.SourceConfigs `yaml:"sources"`
|
|
AuthSources server.AuthServiceConfigs `yaml:"authSources"` // Deprecated: Kept for compatibility.
|
|
AuthServices server.AuthServiceConfigs `yaml:"authServices"`
|
|
Tools server.ToolConfigs `yaml:"tools"`
|
|
Toolsets server.ToolsetConfigs `yaml:"toolsets"`
|
|
}
|
|
|
|
// parseEnv replaces environment variables ${ENV_NAME} with their values.
|
|
// also support ${ENV_NAME:default_value}.
|
|
func parseEnv(input string) (string, error) {
|
|
re := regexp.MustCompile(`\$\{(\w+)(:(\w*))?\}`)
|
|
|
|
var err error
|
|
output := re.ReplaceAllStringFunc(input, func(match string) string {
|
|
parts := re.FindStringSubmatch(match)
|
|
|
|
// extract the variable name
|
|
variableName := parts[1]
|
|
if value, found := os.LookupEnv(variableName); found {
|
|
return value
|
|
}
|
|
if parts[2] != "" {
|
|
return parts[3]
|
|
}
|
|
err = fmt.Errorf("environment variable not found: %q", variableName)
|
|
return ""
|
|
})
|
|
return output, err
|
|
}
|
|
|
|
// parseToolsFile parses the provided yaml into appropriate configs.
|
|
func parseToolsFile(ctx context.Context, raw []byte) (ToolsFile, error) {
|
|
var toolsFile ToolsFile
|
|
// Replace environment variables if found
|
|
output, err := parseEnv(string(raw))
|
|
if err != nil {
|
|
return toolsFile, fmt.Errorf("error parsing environment variables: %s", err)
|
|
}
|
|
raw = []byte(output)
|
|
|
|
// Parse contents
|
|
err = yaml.UnmarshalContext(ctx, raw, &toolsFile, yaml.Strict())
|
|
if err != nil {
|
|
return toolsFile, err
|
|
}
|
|
return toolsFile, nil
|
|
}
|
|
|
|
// mergeToolsFiles merges multiple ToolsFile structs into one.
|
|
// Detects and raises errors for resource conflicts in sources, authServices, tools, and toolsets.
|
|
// All resource names (sources, authServices, tools, toolsets) must be unique across all files.
|
|
func mergeToolsFiles(files ...ToolsFile) (ToolsFile, error) {
|
|
merged := ToolsFile{
|
|
Sources: make(server.SourceConfigs),
|
|
AuthServices: make(server.AuthServiceConfigs),
|
|
Tools: make(server.ToolConfigs),
|
|
Toolsets: make(server.ToolsetConfigs),
|
|
}
|
|
|
|
var conflicts []string
|
|
|
|
for fileIndex, file := range files {
|
|
// Check for conflicts and merge sources
|
|
for name, source := range file.Sources {
|
|
if _, exists := merged.Sources[name]; exists {
|
|
conflicts = append(conflicts, fmt.Sprintf("source '%s' (file #%d)", name, fileIndex+1))
|
|
} else {
|
|
merged.Sources[name] = source
|
|
}
|
|
}
|
|
|
|
// Check for conflicts and merge authSources (deprecated, but still support)
|
|
for name, authSource := range file.AuthSources {
|
|
if _, exists := merged.AuthSources[name]; exists {
|
|
conflicts = append(conflicts, fmt.Sprintf("authSource '%s' (file #%d)", name, fileIndex+1))
|
|
} else {
|
|
merged.AuthSources[name] = authSource
|
|
}
|
|
}
|
|
|
|
// Check for conflicts and merge authServices
|
|
for name, authService := range file.AuthServices {
|
|
if _, exists := merged.AuthServices[name]; exists {
|
|
conflicts = append(conflicts, fmt.Sprintf("authService '%s' (file #%d)", name, fileIndex+1))
|
|
} else {
|
|
merged.AuthServices[name] = authService
|
|
}
|
|
}
|
|
|
|
// Check for conflicts and merge tools
|
|
for name, tool := range file.Tools {
|
|
if _, exists := merged.Tools[name]; exists {
|
|
conflicts = append(conflicts, fmt.Sprintf("tool '%s' (file #%d)", name, fileIndex+1))
|
|
} else {
|
|
merged.Tools[name] = tool
|
|
}
|
|
}
|
|
|
|
// Check for conflicts and merge toolsets
|
|
for name, toolset := range file.Toolsets {
|
|
if _, exists := merged.Toolsets[name]; exists {
|
|
conflicts = append(conflicts, fmt.Sprintf("toolset '%s' (file #%d)", name, fileIndex+1))
|
|
} else {
|
|
merged.Toolsets[name] = toolset
|
|
}
|
|
}
|
|
}
|
|
|
|
// If conflicts were detected, return an error
|
|
if len(conflicts) > 0 {
|
|
return ToolsFile{}, fmt.Errorf("resource conflicts detected:\n - %s\n\nPlease ensure each source, authService, tool, and toolset has a unique name across all files", strings.Join(conflicts, "\n - "))
|
|
}
|
|
|
|
return merged, nil
|
|
}
|
|
|
|
// loadAndMergeToolsFiles loads multiple YAML files and merges them
|
|
func loadAndMergeToolsFiles(ctx context.Context, filePaths []string) (ToolsFile, error) {
|
|
var toolsFiles []ToolsFile
|
|
|
|
for _, filePath := range filePaths {
|
|
buf, err := os.ReadFile(filePath)
|
|
if err != nil {
|
|
return ToolsFile{}, fmt.Errorf("unable to read tool file at %q: %w", filePath, err)
|
|
}
|
|
|
|
toolsFile, err := parseToolsFile(ctx, buf)
|
|
if err != nil {
|
|
return ToolsFile{}, fmt.Errorf("unable to parse tool file at %q: %w", filePath, err)
|
|
}
|
|
|
|
toolsFiles = append(toolsFiles, toolsFile)
|
|
}
|
|
|
|
mergedFile, err := mergeToolsFiles(toolsFiles...)
|
|
if err != nil {
|
|
return ToolsFile{}, fmt.Errorf("unable to merge tools files: %w", err)
|
|
}
|
|
|
|
return mergedFile, nil
|
|
}
|
|
|
|
// loadAndMergeToolsFolder loads all YAML files from a directory and merges them
|
|
func loadAndMergeToolsFolder(ctx context.Context, folderPath string) (ToolsFile, error) {
|
|
// Check if directory exists
|
|
info, err := os.Stat(folderPath)
|
|
if err != nil {
|
|
return ToolsFile{}, fmt.Errorf("unable to access tools folder at %q: %w", folderPath, err)
|
|
}
|
|
if !info.IsDir() {
|
|
return ToolsFile{}, fmt.Errorf("path %q is not a directory", folderPath)
|
|
}
|
|
|
|
// Find all YAML files in the directory
|
|
pattern := filepath.Join(folderPath, "*.yaml")
|
|
yamlFiles, err := filepath.Glob(pattern)
|
|
if err != nil {
|
|
return ToolsFile{}, fmt.Errorf("error finding YAML files in %q: %w", folderPath, err)
|
|
}
|
|
|
|
// Also find .yml files
|
|
ymlPattern := filepath.Join(folderPath, "*.yml")
|
|
ymlFiles, err := filepath.Glob(ymlPattern)
|
|
if err != nil {
|
|
return ToolsFile{}, fmt.Errorf("error finding YML files in %q: %w", folderPath, err)
|
|
}
|
|
|
|
// Combine both file lists
|
|
allFiles := append(yamlFiles, ymlFiles...)
|
|
|
|
if len(allFiles) == 0 {
|
|
return ToolsFile{}, fmt.Errorf("no YAML files found in directory %q", folderPath)
|
|
}
|
|
|
|
// Use existing loadAndMergeToolsFiles function
|
|
return loadAndMergeToolsFiles(ctx, allFiles)
|
|
}
|
|
|
|
func handleDynamicReload(ctx context.Context, toolsFile ToolsFile, s *server.Server) error {
|
|
logger, err := util.LoggerFromContext(ctx)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
sourcesMap, authServicesMap, toolsMap, toolsetsMap, err := validateReloadEdits(ctx, toolsFile)
|
|
if err != nil {
|
|
errMsg := fmt.Errorf("unable to validate reloaded edits: %w", err)
|
|
logger.WarnContext(ctx, errMsg.Error())
|
|
return err
|
|
}
|
|
|
|
s.ResourceMgr.SetResources(sourcesMap, authServicesMap, toolsMap, toolsetsMap)
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateReloadEdits checks that the reloaded tools file configs can initialized without failing
|
|
func validateReloadEdits(
|
|
ctx context.Context, toolsFile ToolsFile,
|
|
) (map[string]sources.Source, map[string]auth.AuthService, map[string]tools.Tool, map[string]tools.Toolset, error,
|
|
) {
|
|
logger, err := util.LoggerFromContext(ctx)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
instrumentation, err := util.InstrumentationFromContext(ctx)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
logger.DebugContext(ctx, "Attempting to parse and validate reloaded tools file.")
|
|
|
|
ctx, span := instrumentation.Tracer.Start(ctx, "toolbox/server/reload")
|
|
defer span.End()
|
|
|
|
reloadedConfig := server.ServerConfig{
|
|
Version: versionString,
|
|
SourceConfigs: toolsFile.Sources,
|
|
AuthServiceConfigs: toolsFile.AuthServices,
|
|
ToolConfigs: toolsFile.Tools,
|
|
ToolsetConfigs: toolsFile.Toolsets,
|
|
}
|
|
|
|
sourcesMap, authServicesMap, toolsMap, toolsetsMap, err := server.InitializeConfigs(ctx, reloadedConfig)
|
|
if err != nil {
|
|
errMsg := fmt.Errorf("unable to initialize reloaded configs: %w", err)
|
|
logger.WarnContext(ctx, errMsg.Error())
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
|
|
return sourcesMap, authServicesMap, toolsMap, toolsetsMap, nil
|
|
}
|
|
|
|
// watchChanges checks for changes in the provided yaml tools file(s) or folder.
|
|
func watchChanges(ctx context.Context, watchDirs map[string]bool, watchedFiles map[string]bool, s *server.Server) {
|
|
logger, err := util.LoggerFromContext(ctx)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
w, err := fsnotify.NewWatcher()
|
|
if err != nil {
|
|
logger.WarnContext(ctx, "error setting up new watcher %s", err)
|
|
return
|
|
}
|
|
|
|
defer w.Close()
|
|
|
|
watchingFolder := false
|
|
var folderToWatch string
|
|
|
|
// if watchedFiles is empty, indicates that user passed entire folder instead
|
|
if len(watchedFiles) == 0 {
|
|
watchingFolder = true
|
|
|
|
// validate that watchDirs only has single element
|
|
if len(watchDirs) > 1 {
|
|
logger.WarnContext(ctx, "error setting watcher, expected single tools folder if no file(s) are defined.")
|
|
return
|
|
}
|
|
|
|
for onlyKey := range watchDirs {
|
|
folderToWatch = onlyKey
|
|
break
|
|
}
|
|
}
|
|
|
|
for dir := range watchDirs {
|
|
err := w.Add(dir)
|
|
if err != nil {
|
|
logger.WarnContext(ctx, fmt.Sprintf("Error adding path %s to watcher: %s", dir, err))
|
|
break
|
|
}
|
|
logger.DebugContext(ctx, fmt.Sprintf("Added directory %s to watcher.", dir))
|
|
}
|
|
|
|
// debounce timer is used to prevent multiple writes triggering multiple reloads
|
|
debounceDelay := 100 * time.Millisecond
|
|
debounce := time.NewTimer(1 * time.Minute)
|
|
debounce.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
logger.DebugContext(ctx, "file watcher context cancelled")
|
|
return
|
|
case err, ok := <-w.Errors:
|
|
if !ok {
|
|
logger.WarnContext(ctx, "file watcher was closed unexpectedly")
|
|
return
|
|
}
|
|
if err != nil {
|
|
logger.WarnContext(ctx, "file watcher error %s", err)
|
|
return
|
|
}
|
|
|
|
case e, ok := <-w.Events:
|
|
if !ok {
|
|
logger.WarnContext(ctx, "file watcher already closed")
|
|
return
|
|
}
|
|
|
|
// only check for events which indicate user saved a new tools file
|
|
// multiple operations checked due to various file update methods across editors
|
|
if !e.Has(fsnotify.Write | fsnotify.Create | fsnotify.Rename) {
|
|
continue
|
|
}
|
|
|
|
cleanedFilename := filepath.Clean(e.Name)
|
|
logger.DebugContext(ctx, fmt.Sprintf("%s event detected in %s", e.Op, cleanedFilename))
|
|
|
|
folderChanged := watchingFolder &&
|
|
(strings.HasSuffix(cleanedFilename, ".yaml") || strings.HasSuffix(cleanedFilename, ".yml"))
|
|
|
|
if folderChanged || watchedFiles[cleanedFilename] {
|
|
// indicates the write event is on a relevant file
|
|
debounce.Reset(debounceDelay)
|
|
}
|
|
|
|
case <-debounce.C:
|
|
debounce.Stop()
|
|
var reloadedToolsFile ToolsFile
|
|
|
|
if watchingFolder {
|
|
logger.DebugContext(ctx, "Reloading tools folder.")
|
|
reloadedToolsFile, err = loadAndMergeToolsFolder(ctx, folderToWatch)
|
|
if err != nil {
|
|
logger.WarnContext(ctx, "error loading tools folder %s", err)
|
|
continue
|
|
}
|
|
} else {
|
|
logger.DebugContext(ctx, "Reloading tools file(s).")
|
|
reloadedToolsFile, err = loadAndMergeToolsFiles(ctx, slices.Collect(maps.Keys(watchedFiles)))
|
|
if err != nil {
|
|
logger.WarnContext(ctx, "error loading tools files %s", err)
|
|
continue
|
|
}
|
|
}
|
|
|
|
err = handleDynamicReload(ctx, reloadedToolsFile, s)
|
|
if err != nil {
|
|
errMsg := fmt.Errorf("unable to parse reloaded tools file at %q: %w", reloadedToolsFile, err)
|
|
logger.WarnContext(ctx, errMsg.Error())
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func resolveWatcherInputs(toolsFile string, toolsFiles []string, toolsFolder string) (map[string]bool, map[string]bool) {
|
|
var relevantFiles []string
|
|
|
|
// map for efficiently checking if a file is relevant
|
|
watchedFiles := make(map[string]bool)
|
|
|
|
// dirs that will be added to watcher (fsnotify prefers watching directory then filtering for file)
|
|
watchDirs := make(map[string]bool)
|
|
|
|
if len(toolsFiles) > 0 {
|
|
relevantFiles = toolsFiles
|
|
} else if toolsFolder != "" {
|
|
watchDirs[filepath.Clean(toolsFolder)] = true
|
|
} else {
|
|
relevantFiles = []string{toolsFile}
|
|
}
|
|
|
|
// extract parent dir for relevant files and dedup
|
|
for _, f := range relevantFiles {
|
|
cleanFile := filepath.Clean(f)
|
|
watchedFiles[cleanFile] = true
|
|
watchDirs[filepath.Dir(cleanFile)] = true
|
|
}
|
|
|
|
return watchDirs, watchedFiles
|
|
}
|
|
|
|
func run(cmd *Command) error {
|
|
ctx, cancel := context.WithCancel(cmd.Context())
|
|
defer cancel()
|
|
|
|
// watch for sigterm / sigint signals
|
|
signals := make(chan os.Signal, 1)
|
|
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
|
|
go func(sCtx context.Context) {
|
|
var s os.Signal
|
|
select {
|
|
case <-sCtx.Done():
|
|
// this should only happen when the context supplied when testing is canceled
|
|
return
|
|
case s = <-signals:
|
|
}
|
|
switch s {
|
|
case syscall.SIGINT:
|
|
cmd.logger.DebugContext(sCtx, "Received SIGINT signal to shutdown.")
|
|
case syscall.SIGTERM:
|
|
cmd.logger.DebugContext(sCtx, "Sending SIGTERM signal to shutdown.")
|
|
}
|
|
cancel()
|
|
}(ctx)
|
|
|
|
// If stdio, set logger's out stream (usually DEBUG and INFO logs) to errStream
|
|
loggerOut := cmd.outStream
|
|
if cmd.cfg.Stdio {
|
|
loggerOut = cmd.errStream
|
|
}
|
|
|
|
// Handle logger separately from config
|
|
switch strings.ToLower(cmd.cfg.LoggingFormat.String()) {
|
|
case "json":
|
|
logger, err := log.NewStructuredLogger(loggerOut, cmd.errStream, cmd.cfg.LogLevel.String())
|
|
if err != nil {
|
|
return fmt.Errorf("unable to initialize logger: %w", err)
|
|
}
|
|
cmd.logger = logger
|
|
case "standard":
|
|
logger, err := log.NewStdLogger(loggerOut, cmd.errStream, cmd.cfg.LogLevel.String())
|
|
if err != nil {
|
|
return fmt.Errorf("unable to initialize logger: %w", err)
|
|
}
|
|
cmd.logger = logger
|
|
default:
|
|
return fmt.Errorf("logging format invalid")
|
|
}
|
|
|
|
ctx = util.WithLogger(ctx, cmd.logger)
|
|
|
|
// Set up OpenTelemetry
|
|
otelShutdown, err := telemetry.SetupOTel(ctx, cmd.cfg.Version, cmd.cfg.TelemetryOTLP, cmd.cfg.TelemetryGCP, cmd.cfg.TelemetryServiceName)
|
|
if err != nil {
|
|
errMsg := fmt.Errorf("error setting up OpenTelemetry: %w", err)
|
|
cmd.logger.ErrorContext(ctx, errMsg.Error())
|
|
return errMsg
|
|
}
|
|
defer func() {
|
|
err := otelShutdown(ctx)
|
|
if err != nil {
|
|
errMsg := fmt.Errorf("error shutting down OpenTelemetry: %w", err)
|
|
cmd.logger.ErrorContext(ctx, errMsg.Error())
|
|
}
|
|
}()
|
|
|
|
var toolsFile ToolsFile
|
|
|
|
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())
|
|
return err
|
|
}
|
|
logMsg := fmt.Sprint("Using prebuilt tool configuration for ", cmd.prebuiltConfig)
|
|
cmd.logger.InfoContext(ctx, logMsg)
|
|
// Append prebuilt.source to Version string for the User Agent
|
|
cmd.cfg.Version += "+prebuilt." + cmd.prebuiltConfig
|
|
|
|
toolsFile, 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
|
|
}
|
|
}
|
|
|
|
cmd.cfg.SourceConfigs, cmd.cfg.AuthServiceConfigs, cmd.cfg.ToolConfigs, cmd.cfg.ToolsetConfigs = toolsFile.Sources, toolsFile.AuthServices, toolsFile.Tools, toolsFile.Toolsets
|
|
authSourceConfigs := toolsFile.AuthSources
|
|
if authSourceConfigs != nil {
|
|
cmd.logger.WarnContext(ctx, "`authSources` is deprecated, use `authServices` instead")
|
|
cmd.cfg.AuthServiceConfigs = authSourceConfigs
|
|
}
|
|
|
|
instrumentation, err := telemetry.CreateTelemetryInstrumentation(versionString)
|
|
if err != nil {
|
|
errMsg := fmt.Errorf("unable to create telemetry instrumentation: %w", err)
|
|
cmd.logger.ErrorContext(ctx, errMsg.Error())
|
|
return errMsg
|
|
}
|
|
|
|
ctx = util.WithInstrumentation(ctx, instrumentation)
|
|
|
|
// start server
|
|
s, err := server.NewServer(ctx, cmd.cfg)
|
|
if err != nil {
|
|
errMsg := fmt.Errorf("toolbox failed to initialize: %w", err)
|
|
cmd.logger.ErrorContext(ctx, errMsg.Error())
|
|
return errMsg
|
|
}
|
|
|
|
// run server in background
|
|
srvErr := make(chan error)
|
|
if cmd.cfg.Stdio {
|
|
go func() {
|
|
defer close(srvErr)
|
|
err = s.ServeStdio(ctx, cmd.inStream, cmd.outStream)
|
|
if err != nil {
|
|
srvErr <- err
|
|
}
|
|
}()
|
|
} else {
|
|
err = s.Listen(ctx)
|
|
if err != nil {
|
|
errMsg := fmt.Errorf("toolbox failed to start listener: %w", err)
|
|
cmd.logger.ErrorContext(ctx, errMsg.Error())
|
|
return errMsg
|
|
}
|
|
cmd.logger.InfoContext(ctx, "Server ready to serve!")
|
|
if cmd.cfg.UI {
|
|
cmd.logger.InfoContext(ctx, fmt.Sprintf("Toolbox UI is up and running at: http://%s:%d/ui", cmd.cfg.Address, cmd.cfg.Port))
|
|
}
|
|
|
|
go func() {
|
|
defer close(srvErr)
|
|
err = s.Serve(ctx)
|
|
if err != nil {
|
|
srvErr <- err
|
|
}
|
|
}()
|
|
}
|
|
|
|
watchDirs, watchedFiles := resolveWatcherInputs(cmd.tools_file, cmd.tools_files, cmd.tools_folder)
|
|
|
|
if !cmd.cfg.DisableReload {
|
|
// start watching the file(s) or folder for changes to trigger dynamic reloading
|
|
go watchChanges(ctx, watchDirs, watchedFiles, s)
|
|
}
|
|
|
|
// wait for either the server to error out or the command's context to be canceled
|
|
select {
|
|
case err := <-srvErr:
|
|
if err != nil {
|
|
errMsg := fmt.Errorf("toolbox crashed with the following error: %w", err)
|
|
cmd.logger.ErrorContext(ctx, errMsg.Error())
|
|
return errMsg
|
|
}
|
|
case <-ctx.Done():
|
|
shutdownContext, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
cmd.logger.WarnContext(shutdownContext, "Shutting down gracefully...")
|
|
err := s.Shutdown(shutdownContext)
|
|
if err == context.DeadlineExceeded {
|
|
return fmt.Errorf("graceful shutdown timed out... forcing exit")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|