Files
genai-toolbox/internal/tools/tools.go
Yuan Teoh 293c1d6889 feat!: update configuration file v2 (#2369)
This PR introduces a significant update to the Toolbox configuration
file format, which is one of the primary **breaking changes** required
for the implementation of the Advanced Control Plane.

# Summary of Changes
The configuration schema has been updated to enforce resource isolation
and facilitate atomic, incremental updates.
* Resource Isolation: Resource definitions are now separated into
individual blocks, using a distinct structure for each resource type
(Source, Tool, Toolset, etc.). This improves readability, management,
and auditing of configuration files.
* Field Name Modification: Internal field names have been modified to
align with declarative methodologies. Specifically, the configuration
now separates kind (general resource type, e.g., Source) from type
(specific implementation, e.g., Postgres).

# User Impact
Existing tools.yaml configuration files are now in an outdated format.
Users must eventually update their files to the new YAML format.

# Mitigation & Compatibility
Backward compatibility is maintained during this transition to ensure no
immediate user action is required for existing files.
* Immediate Backward Compatibility: The source code includes a
pre-processing layer that automatically detects outdated configuration
files (v1 format) and converts them to the new v2 format under the hood.
* [COMING SOON] Migration Support: The new toolbox migrate subcommand
will be introduced to allow users to automatically convert their old
configuration files to the latest format.

# Example
Example for config file v2:
```
kind: sources
name: my-pg-instance
type: cloud-sql-postgres
project: my-project
region: my-region
instance: my-instance
database: my_db
user: my_user
password: my_pass
---
kind: authServices
name: my-google-auth
type: google
clientId: testing-id
---
kind: tools
name: example_tool
type: postgres-sql
source: my-pg-instance
description: some description
statement: SELECT * FROM SQL_STATEMENT;
parameters:
- name: country
  type: string
  description: some description
---
kind: tools
name: example_tool_2
type: postgres-sql
source: my-pg-instance
description: returning the number one
statement: SELECT 1;
---
kind: toolsets
name: example_toolset
tools:
- example_tool
```

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Averi Kitsch <akitsch@google.com>
2026-01-27 16:58:43 -08:00

176 lines
6.3 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 tools
import (
"context"
"fmt"
"slices"
"strings"
yaml "github.com/goccy/go-yaml"
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/util"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
)
// ToolConfigFactory defines the signature for a function that creates and
// decodes a specific tool's configuration. It takes the context, the tool's
// name, and a YAML decoder to parse the config.
type ToolConfigFactory func(ctx context.Context, name string, decoder *yaml.Decoder) (ToolConfig, error)
var toolRegistry = make(map[string]ToolConfigFactory)
// Register allows individual tool packages to register their configuration
// factory function. This is typically called from an init() function in the
// tool's package. It associates a 'type' string with a function that can
// produce the specific ToolConfig type. It returns true if the registration was
// successful, and false if a tool with the same type was already registered.
func Register(resourceType string, factory ToolConfigFactory) bool {
if _, exists := toolRegistry[resourceType]; exists {
// Tool with this type already exists, do not overwrite.
return false
}
toolRegistry[resourceType] = factory
return true
}
// DecodeConfig looks up the registered factory for the given type and uses it
// to decode the tool configuration.
func DecodeConfig(ctx context.Context, resourceType string, name string, decoder *yaml.Decoder) (ToolConfig, error) {
factory, found := toolRegistry[resourceType]
if !found {
return nil, fmt.Errorf("unknown tool type: %q", resourceType)
}
toolConfig, err := factory(ctx, name, decoder)
if err != nil {
return nil, fmt.Errorf("unable to parse tool %q as type %q: %w", name, resourceType, err)
}
return toolConfig, nil
}
type ToolConfig interface {
ToolConfigType() string
Initialize(map[string]sources.Source) (Tool, error)
}
// https://modelcontextprotocol.io/specification/2025-06-18/schema#toolannotations
type ToolAnnotations struct {
DestructiveHint *bool `json:"destructiveHint,omitempty" yaml:"destructiveHint,omitempty"`
IdempotentHint *bool `json:"idempotentHint,omitempty" yaml:"idempotentHint,omitempty"`
OpenWorldHint *bool `json:"openWorldHint,omitempty" yaml:"openWorldHint,omitempty"`
ReadOnlyHint *bool `json:"readOnlyHint,omitempty" yaml:"readOnlyHint,omitempty"`
}
type AccessToken string
func (token AccessToken) ParseBearerToken() (string, error) {
headerParts := strings.Split(string(token), " ")
if len(headerParts) != 2 || strings.ToLower(headerParts[0]) != "bearer" {
return "", fmt.Errorf("authorization header must be in the format 'Bearer <token>': %w", util.ErrUnauthorized)
}
return headerParts[1], nil
}
type Tool interface {
Invoke(context.Context, SourceProvider, parameters.ParamValues, AccessToken) (any, error)
ParseParams(map[string]any, map[string]map[string]any) (parameters.ParamValues, error)
EmbedParams(context.Context, parameters.ParamValues, map[string]embeddingmodels.EmbeddingModel) (parameters.ParamValues, error)
Manifest() Manifest
McpManifest() McpManifest
Authorized([]string) bool
RequiresClientAuthorization(SourceProvider) (bool, error)
ToConfig() ToolConfig
GetAuthTokenHeaderName(SourceProvider) (string, error)
GetParameters() parameters.Parameters
}
// SourceProvider defines the minimal view of the server.ResourceManager
// that the Tool package needs.
// This is implemented to prevent import cycles.
type SourceProvider interface {
GetSource(sourceName string) (sources.Source, bool)
}
// Manifest is the representation of tools sent to Client SDKs.
type Manifest struct {
Description string `json:"description"`
Parameters []parameters.ParameterManifest `json:"parameters"`
AuthRequired []string `json:"authRequired"`
}
// Definition for a tool the MCP client can call.
type McpManifest struct {
// The name of the tool.
Name string `json:"name"`
// A human-readable description of the tool.
Description string `json:"description,omitempty"`
Annotations *ToolAnnotations `json:"annotations,omitempty"`
// A JSON Schema object defining the expected parameters for the tool.
InputSchema parameters.McpToolsSchema `json:"inputSchema,omitempty"`
Metadata map[string]any `json:"_meta,omitempty"`
}
func GetMcpManifest(name, desc string, authInvoke []string, params parameters.Parameters, annotations *ToolAnnotations) McpManifest {
inputSchema, authParams := params.McpManifest()
mcpManifest := McpManifest{
Name: name,
Description: desc,
InputSchema: inputSchema,
Annotations: annotations,
}
// construct metadata, if applicable
metadata := make(map[string]any)
if len(authInvoke) > 0 {
metadata["toolbox/authInvoke"] = authInvoke
}
if len(authParams) > 0 {
metadata["toolbox/authParam"] = authParams
}
if len(metadata) > 0 {
mcpManifest.Metadata = metadata
}
return mcpManifest
}
// Helper function that returns if a tool invocation request is authorized
func IsAuthorized(authRequiredSources []string, verifiedAuthServices []string) bool {
if len(authRequiredSources) == 0 {
// no authorization requirement
return true
}
for _, a := range authRequiredSources {
if slices.Contains(verifiedAuthServices, a) {
return true
}
}
return false
}
func GetCompatibleSource[T any](resourceMgr SourceProvider, sourceName, toolName, toolType string) (T, error) {
var zero T
s, ok := resourceMgr.GetSource(sourceName)
if !ok {
return zero, fmt.Errorf("unable to retrieve source %q for tool %q", sourceName, toolName)
}
source, ok := s.(T)
if !ok {
return zero, fmt.Errorf("invalid source for %q tool: source %q is not a compatible type", toolType, sourceName)
}
return source, nil
}