The VirtualMCPServer CRD enables aggregation of multiple backend MCPServers into a unified virtual endpoint. This allows clients to interact with multiple MCP servers through a single interface, with features like:
- Unified authentication: Single authentication point for clients
- Backend discovery: Automatic discovery of backend authentication configurations
- Tool aggregation: Intelligent conflict resolution when multiple backends expose tools with the same name
- Composite tools: Define workflows that orchestrate calls across multiple backends
- Token caching: Efficient token exchange and caching for improved performance
- Group:
toolhive.stacklok.dev - Version: `v1beta1`
- Kind:
VirtualMCPServer
- Singular:
virtualmcpserver - Plural:
virtualmcpservers - Short Names:
vmcp,virtualmcp
References an existing MCPGroup that defines the backend workloads to aggregate.
The referenced MCPGroup must exist in the same namespace.
Type: MCPGroupRef (object with name field)
Example:
spec:
groupRef:
name: engineering-teamA VirtualMCPServer aggregates three types of backends from the referenced MCPGroup:
| Type | CRD | Infrastructure | Use Case |
|---|---|---|---|
| Container | MCPServer |
Pod + Service | MCP servers running as containers in the cluster |
| Proxy | MCPRemoteProxy |
Proxy Pod + Service | Remote servers requiring a proxy with its own auth/audit layer |
| Entry | MCPServerEntry |
None (config only) | Remote servers where VirtualMCPServer connects directly |
When to use MCPServerEntry vs MCPRemoteProxy:
- Use
MCPServerEntrywhen VirtualMCPServer can connect directly to the remote server. This is simpler (zero infrastructure) and eliminates the dual auth boundary problem where both the proxy and vMCP need separate auth configs. - Use
MCPRemoteProxywhen you need the proxy's own authentication middleware, audit logging, or observability for standalone (non-vMCP) access to the remote server.
Example: MCPServerEntry backend
apiVersion: toolhive.stacklok.dev/v1beta1
kind: MCPServerEntry
metadata:
name: context7
spec:
remoteUrl: https://mcp.context7.com/mcp
transport: streamable-http
groupRef:
name: engineering-team
# No externalAuthConfigRef — public endpoint, no auth neededConfigures authentication for clients connecting to the Virtual MCP server. Reuses MCPServer OIDC and authorization patterns.
Type: IncomingAuthConfig
Fields:
type(string, required): Authentication type. Must be explicitly specified.anonymous: No authentication required (use this when no auth is needed)oidc: OIDC/OAuth2 authentication
oidcConfigRef(MCPOIDCConfigReference, optional): Reference to a shared MCPOIDCConfig resource (required when type=oidc).name(string, required): Name of the MCPOIDCConfig resource (same namespace)audience(string, required): Must be unique per server to prevent token replayscopes([]string, optional): Defaults to["openid"]
authzConfig(AuthzConfigRef, optional): Authorization policy configurationtype(string, required):inlineorconfigMapinline(InlineAuthzConfig, required when type=inline): Inline Cedar policiespolicies([]string, required): Cedar policy stringsentitiesJson(string, optional): Cedar entities (JSON). Required when transitive policies (e.g.ClaimGroup→PlatformRole) need a static entity store. Defaults to"[]".primaryUpstreamProvider(string, optional): Deprecated. Use.spec.authServerConfig.primaryUpstreamProviderinstead. Setting this field still resolves a primary upstream for backward compatibility and emits a Warning event with reasonAuthzPrimaryUpstreamProviderDeprecated. Planned removal one release after the deprecation cycle.
configMap(ConfigMapAuthzRef, required when type=configMap): Reference to a ConfigMap holding Cedar policies. The operator resolves the ConfigMap at reconcile time and bakes the policies into the rendered vmcpconfig.yaml. Failures surface asAuthConfigured=Falsewith reasonAuthzConfigMapNotFound(missing reference) orAuthzConfigMapInvalid(parse, validation, or non-Cedar payload).groupClaimName(string, optional): JWT claim key Cedar should treat as the group list (overrides the well-known defaultsgroups,roles,cognito:groups). WhentypeisconfigMap, the spec value overrides anygroup_claim_nameset in the ConfigMap payload.roleClaimName(string, optional): JWT claim key Cedar should treat as the role list. Same spec-over-ConfigMap precedence asgroupClaimName.groupEntityType(string, optional): Cedar entity type used for principal parent UIDs synthesised from JWT group/role claims. Defaults toTHVGroup. Must match the entity type used in the static entity store for transitiveinchecks to resolve. Same spec-over-ConfigMap precedence asgroupClaimName.
Important: The type field must always be explicitly specified. When no authentication is required, use type: anonymous.
Example (anonymous auth):
spec:
incomingAuth:
type: anonymousExample (OIDC auth with shared MCPOIDCConfig — preferred):
spec:
incomingAuth:
type: oidc
oidcConfigRef:
name: corporate-idp # references an MCPOIDCConfig resource
audience: vmcp-api # unique per server
scopes: ["openid"]
authzConfig:
type: inline
inline:
policies:
- |
permit(
principal,
action == Action::"tools/call",
resource
);Configures authentication from Virtual MCP to backend MCPServers.
Type: OutgoingAuthConfig
Fields:
source(string, optional): How backend authentication configurations are determineddiscovered(default): Automatically discover from backend'sMCPServer.spec.externalAuthConfigRefinline: Explicit per-backend configuration in VirtualMCPServer
default(BackendAuthConfig, optional): Default behavior for backends without explicit auth configbackends(map[string]BackendAuthConfig, optional): Per-backend authentication overrides
In both modes the operator reads spec.externalAuthConfigRef from the backend
workload (MCPServer, MCPRemoteProxy or MCPServerEntry) and mounts the secrets
that strategy needs onto the vMCP Deployment. An entry under backends overrides
the workload's configuration for that backend, and only the override's secrets are
mounted. source selects how the running vMCP resolves backend auth, not whether
the operator reads these workload references.
Example (discovered mode):
spec:
outgoingAuth:
source: discovered
default:
type: discoveredExample (inline mode):
spec:
outgoingAuth:
source: inline
backends:
github:
type: externalAuthConfigRef
externalAuthConfigRef:
name: github-token-exchange
slack:
type: service_account
serviceAccount:
credentialsRef:
name: slack-bot-token
key: token
headerName: Authorization
headerFormat: "Bearer {token}"Fields:
type(string, required): Authentication typediscovered: Automatically discover from backendexternalAuthConfigRef: Reference an MCPExternalAuthConfig resource
externalAuthConfigRef(ExternalAuthConfigRef, optional): Auth config reference (when type=externalAuthConfigRef)
Allowlist of incoming client request headers forwarded verbatim to every
backend. The value is captured at the auth boundary and injected into the
session's outgoing backend requests. Type: []string. Names are matched
case-insensitively; restricted headers (Host, Authorization,
X-Forwarded-*, hop-by-hop) are rejected at startup. Takes precedence over
spec.config.passthroughHeaders.
spec:
passthroughHeaders:
- x-litellm-api-key # forwarded to all backends; absent on a request → not forwardedSecurity: forwarded headers are caller-controlled. Use only behind a trusted upstream (gateway/mesh) that sets or strips them; pair with
incomingAuth: anonymousonly in that case.Horizontal scaling: forwarded headers are not persisted across replicas (only the
(issuer, subject)identity tuple is). A session restored on another replica re-captures them from the next request — keep clients pinned withsessionAffinity: ClientIP.
Defines tool aggregation and conflict resolution strategies.
Type: AggregationConfig
Fields:
conflictResolution(string, optional, default: "prefix"): Strategy for resolving tool name conflictsprefix: Automatically prefix tool names with workload identifierpriority: First workload in priority order winsmanual: Explicitly define overrides for all conflicts
conflictResolutionConfig(ConflictResolutionConfig, optional): Configuration for the chosen strategytools([]WorkloadToolConfig, optional): Per-workload tool filtering and overridesexcludeAllTools(bool, optional): Excludes all tools from aggregation when truedefaultToolVisibility(string, optional, default: "allow"): Whether a backend with notoolsentry is advertisedallow: every tool from an unlisted backend is advertiseddeny: an unlisted backend contributes no tools, so only backends named intoolsare advertised
Example (prefix strategy):
spec:
groupRef:
name: my-services
config:
aggregation:
conflictResolution: prefix
conflictResolutionConfig:
prefixFormat: "{workload}_"
tools:
- workload: github
filter: ["create_pr", "merge_pr"]
- workload: jira
toolConfigRef:
name: jira-tool-configExample (deny-by-default):
By default, a workload in the group with no tools entry has all of its tools
advertised, so adding a workload to the group exposes it. Set
defaultToolVisibility: deny to invert that, so only the workloads you list
contribute tools:
spec:
groupRef:
name: my-services
config:
aggregation:
conflictResolution: prefix
defaultToolVisibility: deny
tools:
# Only these two workloads are advertised. Any other workload in
# my-services contributes no tools, including ones added later.
- workload: github
filter: ["get_issue", "list_prs"]
- workload: jira
filter: ["get_issue"]A workload that is listed is opted in by its entry: excludeAll and filter
on that entry then decide which of its tools are advertised. An entry with
neither advertises all of that workload's tools.
defaultToolVisibility: deny cannot be combined with the priority conflict
strategy unless every priorityOrder entry also has a tools entry. An unlisted
backend can win a name conflict and then be withheld, which would hide the tool
from every workload offering it; the configuration is rejected rather than
silently dropping the tool.
Note:
defaultToolVisibilitycontrols advertising only, as doexcludeAllToolsandexcludeAll. Tools hidden this way remain in the routing table so composite tools can still call them, anddefaultToolVisibilitydoes not affect resources or prompts, which are advertised regardless. For per-identity authorization, use Cedar policies (see Authorization).
Example (priority strategy):
spec:
groupRef:
name: my-services
config:
aggregation:
conflictResolution: priority
conflictResolutionConfig:
priorityOrder: ["github", "jira", "slack"]Example (manual strategy):
spec:
groupRef:
name: my-services
config:
aggregation:
conflictResolution: manual
tools:
- workload: github
filter: ["create_pr", "merge_pr", "list_repos"]
overrides:
create_pr:
name: github_create_pr
description: "Create a pull request in GitHub"
- workload: jira
filter: ["create_issue", "update_issue"]
overrides:
create_issue:
name: jira_create_issue
description: "Create an issue in Jira"
# All tool name conflicts must be explicitly resolved via overrides
# Runtime validation ensures no unresolved conflicts existFields:
workload(string, required): Name of the backend MCPServer workloadtoolConfigRef(ToolConfigRef, optional): Reference to MCPToolConfig resource for Kubernetes deploymentsfilter([]string, optional): Inline list of tool names to allow (only used if toolConfigRef not specified)overrides(map[string]ToolOverride, optional): Inline tool overrides (only used if toolConfigRef not specified)excludeAll(bool, optional): Excludes all tools from this workload when true
Defines inline composite tool workflows. For complex workflows, reference VirtualMCPCompositeToolDefinition resources instead.
Type: []CompositeToolSpec
Fields:
name(string, required): Name of the composite tooldescription(string, required): Description of the composite toolparameters(map[string]ParameterSpec, optional): Input parameterssteps([]WorkflowStep, required): Workflow stepstimeout(string, optional, default: "30m"): Maximum execution time
Example:
spec:
compositeTools:
- name: deploy_and_notify
description: Deploy PR with user confirmation and notification
parameters:
pr_number:
type: integer
required: true
steps:
- id: merge
tool: github.merge_pr
arguments:
pr: "{{.params.pr_number}}"
- id: confirm_deploy
type: elicitation
message: "PR {{.params.pr_number}} merged. Proceed with deployment?"
dependsOn: ["merge"]
- id: deploy
tool: kubernetes.deploy
arguments:
pr: "{{.params.pr_number}}"
dependsOn: ["confirm_deploy"]Defines operational settings like timeouts and health checks.
Type: OperationalConfig
Fields:
logLevel(string, optional): Log level for the Virtual MCP server. Set to "debug" to enable debug logging.timeouts(TimeoutConfig, optional): Timeout configurationfailureHandling(FailureHandlingConfig, optional): Failure handling configuration
Example:
spec:
config:
operational:
logLevel: debug
timeouts:
default: 30s
perWorkload:
github: 45s
failureHandling:
healthCheckInterval: 30s
unhealthyThreshold: 3
partialFailureMode: fail
circuitBreaker:
enabled: true
failureThreshold: 5
timeout: 60sDefines the pod template for customizing the Virtual MCP server pod configuration. Use the vmcp container name to modify the Virtual MCP server container.
Type: runtime.RawExtension
Example:
spec:
podTemplateSpec:
spec:
containers:
- name: vmcp
resources:
requests:
memory: "256Mi"
cpu: "500m"
limits:
memory: "512Mi"
cpu: "1000m"Configures OpenTelemetry-based observability for the Virtual MCP server, including distributed tracing, OTLP metrics export, and Prometheus metrics endpoint.
Type: telemetry.Config
Fields:
endpoint(string): OTLP endpoint URL for tracing and metricsserviceName(string): Service name for telemetryserviceVersion(string): Service version for telemetrytracingEnabled(boolean): Controls whether distributed tracing is enabledmetricsEnabled(boolean): Controls whether OTLP metrics are enabledsamplingRate(string): Trace sampling rate (0.0-1.0), only used when tracingEnabled is true. Example: "0.05" for 5% sampling.headers(map[string]string): Authentication headers for the OTLP endpointinsecure(boolean): Use HTTP instead of HTTPS for the OTLP endpointenablePrometheusMetricsPath(boolean): Controls whether to expose Prometheus-style /metrics endpointenvironmentVariables([]string): Environment variable names to include in telemetry spans as attributescustomAttributes(map[string]string): Custom resource attributes to be added to all telemetry signals
Example:
spec:
groupRef:
name: my-group
config:
telemetry:
endpoint: "otel-collector:4317"
serviceName: "my-vmcp"
insecure: true
tracingEnabled: true
samplingRate: "0.1"
metricsEnabled: true
enablePrometheusMetricsPath: trueFor details on what metrics and traces are emitted, see the Virtual MCP Server Observability documentation.
Standard Kubernetes conditions representing the latest observations of the VirtualMCPServer's state.
Type: []metav1.Condition
Standard Condition Types:
Ready: Indicates whether the VirtualMCPServer is readyAuthConfigured: Indicates whether authentication is configuredBackendsDiscovered: Indicates whether backends have been discoveredGroupRefValidated: Indicates whether the GroupRef is valid
Lists discovered backend configurations when source=discovered.
Type: []DiscoveredBackend
Fields:
name(string): Name of the backend MCPServerauthConfigRef(string): Name of the discovered MCPExternalAuthConfigauthType(string): Type of authentication configuredstatus(string): Current status (ready,degraded,unavailable)lastHealthCheck(metav1.Time): Timestamp of the last health checkurl(string): URL of the backend MCPServer
Summarizes aggregated capabilities from all backends.
Type: CapabilitiesSummary
Fields:
toolCount(int): Total number of tools exposedresourceCount(int): Total number of resources exposedpromptCount(int): Total number of prompts exposedcompositeToolCount(int): Number of composite tools defined
Current phase of the VirtualMCPServer.
Type: VirtualMCPServerPhase
Values:
Pending: VirtualMCPServer is being initializedReady: VirtualMCPServer is ready and serving requestsDegraded: VirtualMCPServer is running but some backends are unavailableFailed: VirtualMCPServer has failed
Provides additional information about the current phase.
Type: string
URL where the Virtual MCP server can be accessed.
Type: string
Hash of the referenced MCPOIDCConfig spec, used for change detection. Only present when oidcConfigRef is set.
Type: string
The most recent generation observed for this VirtualMCPServer.
Type: int64
apiVersion: toolhive.stacklok.dev/v1beta1
kind: VirtualMCPServer
metadata:
name: engineering-vmcp
namespace: default
spec:
# Reference to MCPGroup defining backend workloads
groupRef:
name: engineering-team
# Tool aggregation
config:
aggregation:
conflictResolution: prefix
conflictResolutionConfig:
prefixFormat: "{workload}_"
tools:
- workload: github
filter: ["create_pr", "merge_pr"]
- workload: jira
toolConfigRef:
name: jira-tool-config
# Client authentication (preferred: reference a shared MCPOIDCConfig)
incomingAuth:
type: oidc
oidcConfigRef:
name: engineering-idp # references an MCPOIDCConfig in the same namespace
audience: engineering-vmcp
authzConfig:
type: inline
inline:
policies:
- |
permit(
principal,
action == Action::"tools/call",
resource
);
# Backend authentication (discovered mode)
outgoingAuth:
source: discovered
default:
type: discovered
backends:
slack: # Override for specific backend
type: service_account
serviceAccount:
credentialsRef:
name: slack-bot-token
key: token
# Composite tools
compositeTools:
- name: investigate_incident
description: Gather logs and metrics for incident analysis
parameters:
incident_id:
type: string
required: true
steps:
- id: fetch_logs
tool: fetch.fetch
arguments:
url: "https://logs.company.com/api/query?incident={{.params.incident_id}}"
- id: create_report
tool: jira.create_issue
arguments:
title: "Incident {{.params.incident_id}} Analysis"
description: "{{.steps.fetch_logs.output}}"
dependsOn: ["fetch_logs"]
# Operational settings
operational:
timeouts:
default: 30s
perWorkload:
github: 45s
failureHandling:
healthCheckInterval: 30s
unhealthyThreshold: 3
partialFailureMode: fail
circuitBreaker:
enabled: true
failureThreshold: 5
timeout: 60s
# Observability is configured in spec.config.telemetry (see .spec.config.telemetry section above)
status:
phase: Ready
message: "Virtual MCP serving 3 backends with 15 tools"
url: "http://engineering-vmcp.default.svc.cluster.local:8080"
observedGeneration: 1
conditions:
- type: Ready
status: "True"
lastTransitionTime: "2025-10-20T10:00:00Z"
reason: AllBackendsReady
message: "Virtual MCP is ready and serving requests"
- type: AuthConfigured
status: "True"
reason: IncomingAuthValid
message: "Incoming authentication configured"
- type: BackendsDiscovered
status: "True"
reason: DiscoveryComplete
message: "Discovered 3 backends with authentication"
discoveredBackends:
- name: github
authConfigRef: github-token-exchange
authType: token_exchange
status: ready
lastHealthCheck: "2025-10-20T10:05:00Z"
url: "http://github-mcp.default.svc.cluster.local:8080"
- name: jira
authConfigRef: jira-token-exchange
authType: token_exchange
status: ready
lastHealthCheck: "2025-10-20T10:05:00Z"
url: "http://jira-mcp.default.svc.cluster.local:8080"
- name: slack
authConfigRef: ""
authType: service_account
status: ready
lastHealthCheck: "2025-10-20T10:05:00Z"
url: "http://slack-mcp.default.svc.cluster.local:8080"
capabilities:
toolCount: 15
resourceCount: 3
promptCount: 2
compositeToolCount: 1The VirtualMCPServer CRD includes comprehensive validation:
- Required Fields:
spec.groupRef.namemust be specifiedspec.incomingAuth.typemust be explicitly specified (useanonymouswhen no auth is needed)
- Reference Validation: All references (groupRef, authConfigRef, toolConfigRef) must be valid
- Conflict Resolution: Priority strategy requires
priorityOrderconfiguration - Composite Tools: Must have unique names, valid steps with IDs, and proper dependencies
- Token Cache: Redis provider requires valid address configuration
- Same-Namespace References: All references must be in the same namespace for security
- MCPGroup: Defines groups of MCPServers
- MCPServer: Individual MCP server instances
- MCPOIDCConfig: Shared OIDC provider configuration (referenced via
oidcConfigRef) - MCPExternalAuthConfig: External authentication configuration
- MCPToolConfig: Tool filtering and renaming configuration
- Virtual MCP Server Observability: Telemetry and metrics documentation
- Virtual MCP Proposal: Complete design proposal