googleapis/mcp-toolbox · error

tool %q declared more than once

Error message

tool %q declared more than once

What it means

UnmarshalPrimitiveConfig rejects duplicate tool names: after a "tool" document unmarshals successfully, it checks `toolConfigs[name]` and fails if the name already exists. Tool names are map keys in the parsed config, so a second declaration would silently overwrite the first; the library treats this as a config error instead.

Source

Thrown at internal/server/config.go:277

				return nil, nil, nil, nil, nil, nil, fmt.Errorf("authService %q declared more than once", name)
			}
			authServiceConfigs[name] = c
		case "tool":
			c, err := UnmarshalYAMLToolConfig(ctx, name, resource)
			if err != nil {
				if len(file.Docs) > 1 {
					return nil, nil, nil, nil, nil, nil, fmt.Errorf("document %d: error unmarshaling %s %q: %w", docIndex, kind, name, err)
				}
				return nil, nil, nil, nil, nil, nil, fmt.Errorf("error unmarshaling %s: %w", kind, err)
			}
			if c == nil {
				continue
			}
			if toolConfigs == nil {
				toolConfigs = make(ToolConfigs)
			}
			if _, exists := toolConfigs[name]; exists {
				return nil, nil, nil, nil, nil, nil, fmt.Errorf("tool %q declared more than once", name)
			}
			toolConfigs[name] = c
		case "toolset":
			c, err := UnmarshalYAMLToolsetConfig(ctx, name, resource)
			if err != nil {
				if len(file.Docs) > 1 {
					return nil, nil, nil, nil, nil, nil, fmt.Errorf("document %d: error unmarshaling %s %q: %w", docIndex, kind, name, err)
				}
				return nil, nil, nil, nil, nil, nil, fmt.Errorf("error unmarshaling %s: %w", kind, err)
			}
			if toolsetGroups == nil {
				toolsetGroups = make(map[string]group.GroupConfig)
			}
			if _, exists := toolsetGroups[name]; exists {
				return nil, nil, nil, nil, nil, nil, fmt.Errorf("toolset %q declared more than once", name)
			}
			toolsetGroups[name] = group.GroupConfig{Name: name, ToolNames: c.ToolNames}
		case "embeddingModel":

View on GitHub (pinned to 8cc6e09de2)

Solutions

  1. Rename one of the two tools to a unique name in the error message.
  2. Delete the redundant duplicate tool definition if it is no longer needed.
  3. Search the whole file for the duplicated name (including across `---` document separators).
  4. If both tools must exist with the same logical purpose, consolidate into one tool with parameters.

Example fix

// before
tools:
  get-user:
    kind: postgres-sql
    source: pg
    statement: SELECT 1
  get-user:
    kind: postgres-sql
    source: pg
    statement: SELECT 2
// after
tools:
  get-user:
    kind: postgres-sql
    source: pg
    statement: SELECT 1
  get-user-count:
    kind: postgres-sql
    source: pg
    statement: SELECT 2
Defensive patterns

Strategy: validation

Validate before calling

func checkDuplicateToolNames(yamlBytes []byte) error {
    var probe struct {
        Tools map[string]yaml.Node `yaml:"tools"`
    }
    if err := yaml.Unmarshal(yamlBytes, &probe); err != nil {
        return err
    }
    if len(probe.Tools) == 0 {
        return nil // YAML maps silently collapse duplicates; check raw keys below
    }
    return nil
}
// YAML map keys dedupe in Go, so scan raw lines:
func rawDuplicateKeys(data string, section string) []string {
    var seen, dups []string
    inSection := false
    for _, line := range strings.Split(data, "\n") {
        trimmed := strings.TrimSpace(line)
        if strings.HasPrefix(line, section+":") { inSection = true; continue }
        if inSection && strings.HasSuffix(trimmed, ":") && !strings.HasPrefix(trimmed, "-") {
            name := strings.TrimSuffix(trimmed, ":")
            if slices.Contains(seen, name) { dups = append(dups, name) }
            seen = append(seen, name)
        }
    }
    return dups
}

Type guard

func isDuplicateToolErr(err error) bool {
    return err != nil && strings.Contains(err.Error(), "declared more than once")
}

Try / catch

cfg, err := server.ParseConfig(ctx, yamlBytes)
if err != nil {
    if strings.Contains(err.Error(), "tool \"" ) && strings.Contains(err.Error(), "declared more than once") {
        name := extractQuoted(err.Error())
        return fmt.Errorf("rename or remove the duplicate tool %q", name)
    }
    return err
}

Prevention

When it happens

Trigger: Calling ParseConfig with a YAML file (or multi-document file) that declares two `tool` resources with the same `name` (or the same YAML map key), so the second declaration hits the `exists` check.

Common situations: Copy-pasting a tool block and forgetting to rename it; merging two config files that both define a tool named e.g. `search-items`; multi-document YAML where two documents define the same tool name.

Related errors


AI-assisted analysis of googleapis/mcp-toolbox@8cc6e09de2 (2026-09-05). Data as JSON: /api/errors/bafbfc9b6759942c. Report an issue: GitHub.