Tool aggregation and conflict resolution
When aggregating multiple MCP servers, tool name conflicts can occur when different backend servers expose tools with the same name. Virtual MCP Server (vMCP) provides strategies to resolve these conflicts automatically.
Overview
vMCP discovers tools from backend MCPServer resources in the referenced group
and presents the advertised tools as a unified set to clients. By default, every
backend contributes tools. You can instead require each backend to be listed
explicitly, then choose which tools it advertises. When two backend MCP servers
have tools with the same name (for example, both GitHub and Jira have a
create_issue tool), a conflict resolution strategy determines how to handle
the collision.
When aggregating many backends, the total number of exposed tools can grow quickly. Consider enabling the optimizer to reduce token usage and improve tool selection accuracy.
Conflict resolution strategies
Prefix strategy (default)
By default, vMCP prefixes all tool names with the workload identifier (the
metadata.name of each MCPServer resource). This guarantees unique names and is
the safest option for most deployments.
spec:
config:
aggregation:
conflictResolution: prefix
conflictResolutionConfig:
prefixFormat: '{workload}_'
Prefix format options:
| Format | Example result |
|---|---|
{workload} | githubcreate_issue |
{workload}_ | github_create_issue |
{workload}. | github.create_issue |
Example:
With backend servers github and jira, both exposing create_issue:
- GitHub's tool becomes
github_create_issue - Jira's tool becomes
jira_create_issue
Priority strategy
When multiple backend MCP servers offer tools with the same name, the priority
strategy keeps the tool from the first backend in the priority order and drops
the duplicate tools from lower-priority backend servers.
spec:
config:
aggregation:
conflictResolution: priority
conflictResolutionConfig:
priorityOrder: ['github', 'jira', 'slack']
In this example, if both GitHub and Jira provide a create_issue tool, only
GitHub's version is exposed. Jira's duplicate is dropped.
When to use: When you have a preferred backend MCP server for specific tools and want to hide duplicates.
The priority strategy drops tools from lower-priority backend servers. Ensure this is the intended behavior before using in production.
Manual strategy
The manual strategy gives you explicit control over tool naming when conflicts
occur. You must provide overrides for all conflicting tools, or the vMCP will
fail to start.
spec:
config:
aggregation:
conflictResolution: manual
tools:
- workload: github
overrides:
create_issue:
name: gh_create_issue
- workload: jira
overrides:
create_issue:
name: jira_ticket
When to use: Production deployments where you want explicit control over tool names.
Tool filtering
Use filters to expose only specific tools from a backend MCP server, excluding all others. This reduces the number of tools presented to LLM clients and removes unnecessary tools:
spec:
config:
aggregation:
tools:
- workload: github
filter: ['create_issue', 'list_issues', 'get_issue']
Only the listed tools are advertised to clients; all others are hidden from
tools/list responses and cannot be called directly. Hidden tools remain
available in the internal routing table so composite tool workflow steps can
call them.
Require workloads to opt in
By default, a workload with no entry in aggregation.tools contributes all of
its tools. Set defaultToolVisibility: deny when group membership changes over
time and you want each workload to be reviewed before its tools become
available:
spec:
config:
aggregation:
defaultToolVisibility: deny
tools:
- workload: github
filter: ['get_issue', 'list_issues']
- workload: jira
filter: ['get_issue']
Only the github and jira workloads contribute tools. A listed workload is
opted in by its entry, and its filter or excludeAll setting determines which
tools are advertised. An unlisted workload contributes no tools, even if it is
later added to the referenced group.
The accepted values are allow and deny. An omitted value behaves as allow.
This setting affects tools only; resources, resource templates, and prompts from
unlisted workloads remain advertised.
Before using defaultToolVisibility, upgrade the ToolHive CRDs to v0.42.1 or
later. Earlier CRDs prune this field when you apply the resource, so vMCP
receives no value and uses allow.
Verify that Kubernetes stored the field:
kubectl get virtualmcpserver <NAME> \
-o jsonpath='{.spec.config.aggregation.defaultToolVisibility}'
When you combine defaultToolVisibility: deny with the priority conflict
resolution strategy, every workload in priorityOrder must also have an entry
in aggregation.tools. vMCP rejects the configuration otherwise.
Excluding all tools
To hide every tool from tools/list, either globally or per workload, use
excludeAllTools or excludeAll. Use these settings when you want clients to
interact only through composite tools instead of raw
backend tools.
Hidden tools are removed from tools/list responses, and direct tools/call
requests return an unknown-tool error. Clients must use the exact
conflict-resolved names returned by tools/list. The
<WORKLOAD_ID>.<TOOL_NAME> form is reserved for tool references inside
composite workflow steps. Hidden tools remain in the internal routing table so
those workflow steps can still call them.
Hide all backend tools globally
Set aggregation.excludeAllTools: true to hide every tool from every backend:
spec:
config:
aggregation:
excludeAllTools: true # hide every backend tool from tools/list
Hide all tools for a specific workload
Set excludeAll: true inside a workload entry to hide all tools from one
backend while leaving other backends unaffected:
spec:
config:
aggregation:
tools:
- workload: github
excludeAll: true # hide all github tools from tools/list
- workload: jira
filter: ['create_issue', 'search_issues']
When to use: When composite tools are the only surface you intend to expose
to clients. Set excludeAllTools: true (or excludeAll: true per workload) to
prevent clients from calling raw backend tools directly, then define
composite tools that orchestrate the hidden tools
internally.
Tool overrides
Use overrides to customize tool names and descriptions without modifying backend MCP server configurations. This is useful for disambiguating similarly-named tools or providing more context to LLM clients:
spec:
config:
aggregation:
tools:
- workload: github
overrides:
create_issue:
name: gh_new_issue
description: 'Create a new GitHub issue in the repository'
Annotation overrides
Override MCP tool annotations to provide hints to LLM clients about tool behavior. Annotations are optional - only set the fields you want to override:
spec:
config:
aggregation:
tools:
- workload: github
overrides:
delete_repository:
annotations:
destructiveHint: true
readOnlyHint: false
list_issues:
annotations:
title: 'List GitHub Issues'
readOnlyHint: true
idempotentHint: true
Available annotation fields:
| Field | Type | Description |
|---|---|---|
title | string | Display title for the tool in MCP clients |
readOnlyHint | boolean | Indicates the tool does not modify data |
destructiveHint | boolean | Indicates the tool may delete or overwrite data |
idempotentHint | boolean | Indicates repeated calls produce the same result |
openWorldHint | boolean | Indicates the tool interacts with external systems |
Annotation overrides can be combined with name and description overrides on the same tool.
You can also reference an MCPToolConfig resource using toolConfigRef instead
of inline filter and overrides. This feature is currently in development.
Combine filters and overrides
You can combine filtering and overrides for fine-grained control:
spec:
config:
aggregation:
conflictResolution: prefix
conflictResolutionConfig:
prefixFormat: '{workload}_'
tools:
- workload: github
filter: ['create_issue', 'list_issues']
overrides:
create_issue:
description: 'Create a GitHub issue (engineering team)'
- workload: jira
filter: ['create_issue', 'search_issues']
Example: Aggregating multiple MCP servers
This example shows two MCP servers (fetch and osv) aggregated with prefix-based conflict resolution:
# MCPGroup to organize backend servers
apiVersion: toolhive.stacklok.dev/v1beta1
kind: MCPGroup
metadata:
name: demo-tools
namespace: toolhive-system
spec:
description: Demo group for tool aggregation
---
# First backend: fetch server
apiVersion: toolhive.stacklok.dev/v1beta1
kind: MCPServer
metadata:
name: fetch
namespace: toolhive-system
spec:
image: ghcr.io/stackloklabs/gofetch/server
transport: streamable-http
proxyPort: 8080
mcpPort: 8080
groupRef:
name: demo-tools
---
# Second backend: osv server
apiVersion: toolhive.stacklok.dev/v1beta1
kind: MCPServer
metadata:
name: osv
namespace: toolhive-system
spec:
image: ghcr.io/stackloklabs/osv-mcp/server
transport: streamable-http
proxyPort: 8080
mcpPort: 8080
groupRef:
name: demo-tools
---
# VirtualMCPServer aggregating both backends
apiVersion: toolhive.stacklok.dev/v1beta1
kind: VirtualMCPServer
metadata:
name: demo-vmcp
namespace: toolhive-system
spec:
incomingAuth:
type: anonymous
groupRef:
name: demo-tools
config:
aggregation:
conflictResolution: prefix
conflictResolutionConfig:
prefixFormat: '{workload}_'
With this configuration, tools from each backend are prefixed:
fetch_*tools from the fetch serverosv_*tools from the osv server
Next steps
- Create composite tools to build multi-step workflows that span multiple backends
- Optimize tool discovery to reduce token usage when aggregating many tools