Composite tools and workflows
Composite tools let you define multi-step workflows that execute across multiple backend MCP servers with parallel execution, conditional logic, approval gates, and error handling.
Overview
A composite tool combines multiple backend tool calls into a single workflow. When a client calls a composite tool, vMCP orchestrates the execution across backend MCP servers, handling dependencies and collecting results.
Key capabilities
- Parallel execution: Independent steps run concurrently; dependent steps wait for their prerequisites
- Template expansion: Dynamic arguments using step outputs
- Elicitation: Request user input mid-workflow (approval gates, choices)
- Error handling: Configurable abort, continue, or retry behavior
- Timeouts: Workflow and per-step timeout configuration
Elicitation (user prompts during workflow execution) is defined in the CRD but has not been extensively tested. Test thoroughly in non-production environments first.
Configuration location
Composite tools are defined in the VirtualMCPServer resource under the
spec.compositeTools array:
apiVersion: toolhive.stacklok.dev/v1alpha1
kind: VirtualMCPServer
metadata:
name: my-vmcp
spec:
groupRef:
name: my-tools
# ... other configuration ...
compositeTools:
- name: my_workflow
description: A multi-step workflow
parameters:
# Input parameters (JSON Schema)
steps:
# Workflow steps
See the CompositeToolSpec definition in the CRD for all available fields.
Simple example
Here's a basic composite tool that fetches a URL and then summarizes it:
spec:
compositeTools:
- name: fetch_and_summarize
description: Fetch a URL and create a summary
parameters:
type: object
properties:
url:
type: string
required:
- url
steps:
- id: fetch
tool: fetch.fetch
arguments:
url: '{{.params.url}}'
- id: summarize
tool: llm.summarize
arguments:
text: '{{.steps.fetch.output.content}}'
dependsOn: [fetch]
What's happening:
- Parameters: Define the workflow inputs (just
urlin this case) - Step 1 (fetch): Calls the
fetch.fetchtool with the URL from parameters using template syntax{{.params.url}} - Step 2 (summarize): Waits for the fetch step (
dependsOn: [fetch]), then callsllm.summarizewith the fetched content using{{.steps.fetch.output.content}}
When a client calls this composite tool, vMCP executes both steps in sequence and returns the final summary.
Use cases
Multi-source documentation aggregation
Fetch and aggregate documentation from multiple sources in parallel:
spec:
compositeTools:
- name: aggregate_docs
description: Fetch and aggregate documentation from multiple sources
parameters:
type: object
properties:
sources:
type: array
default:
- https://raw.githubusercontent.com/modelcontextprotocol/specification/main/README.md
- https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/main/README.md
- https://raw.githubusercontent.com/modelcontextprotocol/servers/main/README.md
timeout: '2m'
steps:
# These three steps run in parallel (no dependencies)
- id: fetch_source_1
tool: fetch_fetch
arguments:
url: '{{index .params.sources 0}}'
max_length: 5000
- id: fetch_source_2
tool: fetch_fetch
arguments:
url: '{{index .params.sources 1}}'
max_length: 5000
- id: fetch_source_3
tool: fetch_fetch
arguments:
url: '{{index .params.sources 2}}'
max_length: 5000
GitHub repository analysis
Analyze a GitHub repository's health and activity:
spec:
compositeTools:
- name: analyze_repository
description: Analyze a GitHub repository's health and activity
parameters:
type: object
properties:
owner:
type: string
default: modelcontextprotocol
repo:
type: string
default: specification
timeout: '2m'
steps:
# These steps run in parallel
- id: list_issues
tool: github_list_issues
arguments:
owner: '{{.params.owner}}'
repo: '{{.params.repo}}'
perPage: 5
- id: list_commits
tool: github_list_commits
arguments:
owner: '{{.params.owner}}'
repo: '{{.params.repo}}'
perPage: 3
Container image investigation
Investigate a container image from an OCI registry:
spec:
compositeTools:
- name: investigate_image
description: Investigate a container image from an OCI registry
parameters:
type: object
properties:
image:
type: string
default: docker.io/library/alpine:latest
timeout: '2m'
steps:
# Step 1: Get basic image information
- id: get_image_info
tool: oci-registry_get_image_info
arguments:
image_ref: '{{.params.image}}'
# Steps 2 and 3 run in parallel (both depend on step 1)
- id: get_manifest
tool: oci-registry_get_image_manifest
arguments:
image_ref: '{{.params.image}}'
dependsOn: [get_image_info]
- id: get_config
tool: oci-registry_get_image_config
arguments:
image_ref: '{{.params.image}}'
dependsOn: [get_image_info]
Workflow definition
Parameters
Define input parameters using JSON Schema format:
spec:
compositeTools:
- name: <TOOL_NAME>
parameters:
type: object
properties:
required_param:
type: string
optional_param:
type: integer
default: 10
required:
- required_param
Steps
Each step can be a tool call or an elicitation:
spec:
compositeTools:
- name: <TOOL_NAME>
steps:
- id: step_name # Unique identifier
tool: backend.tool # Tool to call
arguments: # Arguments with template expansion
arg1: '{{.params.input}}'
dependsOn: [other_step] # Dependencies (this step waits for other_step)
condition: '{{.steps.check.output.approved}}' # Optional condition
timeout: '30s' # Step timeout
onError:
action: abort # abort | continue | retry
Elicitation (user prompts)
Request input from users during workflow execution:
spec:
compositeTools:
- name: <TOOL_NAME>
steps:
- id: approval
type: elicitation
message: 'Proceed with deployment?'
schema:
type: object
properties:
confirm: { type: boolean }
timeout: '5m'
Error handling
Configure behavior when steps fail:
| Action | Description |
|---|---|
abort | Stop workflow immediately |
continue | Log error, proceed to next step |
retry | Retry with exponential backoff |
spec:
compositeTools:
- name: <TOOL_NAME>
steps:
- id: <STEP_ID>
# ... other step config (tool, arguments, etc.)
onError:
action: retry
maxRetries: 3
Template syntax
Access workflow context in arguments:
| Template | Description |
|---|---|
{{.params.name}} | Input parameter |
{{.steps.id.output}} | Step output |
{{.steps.id.content}} | Elicitation response content |
{{.steps.id.action}} | Elicitation action (accept/decline/cancel) |
Complete example
A VirtualMCPServer with an inline composite tool:
apiVersion: toolhive.stacklok.dev/v1alpha1
kind: VirtualMCPServer
metadata:
name: workflow-vmcp
namespace: toolhive-system
spec:
groupRef:
name: my-tools
incomingAuth:
type: anonymous
aggregation:
conflictResolution: prefix
conflictResolutionConfig:
prefixFormat: '{workload}_'
compositeTools:
- name: fetch_and_summarize
description: Fetch a URL and create a summary
parameters:
type: object
properties:
url:
type: string
description: URL to fetch
required:
- url
steps:
- id: fetch_content
tool: fetch.fetch
arguments:
url: '{{.params.url}}'
- id: summarize
tool: llm.summarize # Hypothetical backend - replace with your actual LLM server
arguments:
text: '{{.steps.fetch_content.output.content}}'
dependsOn: [fetch_content]
timeout: '5m'
For complex, reusable workflows, use VirtualMCPCompositeToolDefinition
resources and reference them with compositeToolRefs.