Skip to main content

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
info

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:

VirtualMCPServer resource
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:

  1. Parameters: Define the workflow inputs (just url in this case)
  2. Step 1 (fetch): Calls the fetch.fetch tool with the URL from parameters using template syntax {{.params.url}}
  3. Step 2 (summarize): Waits for the fetch step (dependsOn: [fetch]), then calls llm.summarize with 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:

VirtualMCPServer resource
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:

VirtualMCPServer resource
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:

VirtualMCPServer resource
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:

VirtualMCPServer resource
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:

VirtualMCPServer resource
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:

VirtualMCPServer resource
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:

ActionDescription
abortStop workflow immediately
continueLog error, proceed to next step
retryRetry with exponential backoff
VirtualMCPServer resource
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:

TemplateDescription
{{.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.