Composite Tools Quick Reference

April 21, 2026 · View on GitHub

Quick reference for Virtual MCP Composite Tool workflows.

Basic Workflow Structure

apiVersion: toolhive.stacklok.dev/v1beta1
kind: VirtualMCPCompositeToolDefinition
metadata:
  name: my-workflow
  namespace: default
spec:
  name: my_workflow_name        # Tool name exposed to clients
  description: What it does      # Required description
  timeout: 30m                   # Optional: workflow timeout (default: 30m)
  failureMode: abort             # Optional: abort|continue (default: abort)

  parameters:                    # Optional: input parameters
    param_name:
      type: string
      description: Description of the parameter
      required: false

  steps:                         # Required: workflow steps
    - id: step1
      type: tool                 # tool|elicitation|forEach
      tool: workload.tool_name
      arguments:
        key: "{{.params.param_name}}"

Parallel Execution

# Independent steps run in parallel automatically
steps:
  - id: fetch_a                  # Level 1: Runs in parallel ─┐
  - id: fetch_b                  # Level 1: Runs in parallel ─┼─> aggregate
  - id: fetch_c                  # Level 1: Runs in parallel ─┘

  - id: aggregate                # Level 2: Waits for Level 1
    dependsOn: [fetch_a, fetch_b, fetch_c]

Step Dependencies

steps:
  - id: step1

  - id: step2
    dependsOn: [step1]          # Runs after step1 completes

  - id: step3
    dependsOn: [step1, step2]   # Waits for both step1 AND step2

Template Syntax

Workflows use Go's text/template syntax with additional context variables and functions.

Basic Access

# Access input parameters
"{{.params.parameter_name}}"

# Access step outputs
"{{.steps.step_id.output}}"
"{{.steps.step_id.output.field_name}}"
"{{.steps.step_id.status}}"     # completed|failed|skipped|running

# Access workflow-scoped variables
"{{.vars.variable_name}}"

# Access step errors
"{{.steps.step_id.error}}"

Functions

Composite Tools supports all the built-in functions from the text/template library in addition to some functions for converting to/from JSON.

# JSON encoding - convert value to JSON string
arguments:
  data: "{{json .steps.step1.output}}"

# JSON decoding - parse JSON string to access fields
# Useful when MCP servers return JSON as text content
arguments:
  name: "{{(fromJson .steps.api.output.text).user.name}}"

# String quoting
arguments:
  quoted: "{{quote .params.value}}"

Conditional Logic

# Comparison operators (eq, ne, lt, le, gt, ge)
condition: "{{eq .steps.step1.status \"completed\"}}"
condition: "{{ne .steps.step1.status \"failed\"}}"
condition: "{{gt .steps.step1.output.count 10}}"

# Boolean operators (and, or, not)
condition: "{{and .params.enabled (eq .steps.step1.status \"completed\")}}"
condition: "{{or .params.force (gt .steps.check.output.count 0)}}"
condition: "{{not .params.disabled}}"

Advanced Features

All Go template built-ins are available: index, len, range, with, printf, etc. See Go text/template documentation for complete reference.

Error Handling

Workflow-Level

spec:
  failureMode: abort             # Stop on first error (default)
  failureMode: continue          # Log errors, continue workflow

Step-Level (Overrides Workflow)

steps:
  # Abort on error (default)
  - id: critical
    tool: payment.charge
    # Uses workflow failureMode

  # Continue despite errors
  - id: optional
    tool: notification.send
    onError:
      action: continue

  # Retry with exponential backoff
  - id: resilient
    tool: external.api
    onError:
      action: retry
      maxRetries: 3             # Max 3 retries (4 total attempts)

Default Results

Provide fallback values when a step may be skipped (condition) or fail (continue-on-error):

steps:
  - id: optional_step
    tool: enrichment.api
    condition: "{{.params.enable_enrichment}}"
    defaultResults:
      text: "fallback value"    # Used when step is skipped

  - id: unreliable_step
    tool: external.api
    onError:
      action: continue
    defaultResults:
      text: "{\"status\": \"unavailable\"}"  # Used when step fails

Notes:

  • Keys in defaultResults must match output fields referenced by downstream templates
  • Backend tools return text under text key, so use defaultResults.text for text output
  • Required when skippable steps are referenced by downstream templates

Timeouts

spec:
  timeout: 30m                   # Workflow timeout (default: 30m)

  steps:
    - id: step1
      timeout: 5m                # Step timeout (default: 5m)

Precedence: Step timeout ≤ Workflow timeout

Common Patterns

Fan-Out / Fan-In

steps:
  # Fan-out: Parallel collection
  - id: fetch_1
  - id: fetch_2
  - id: fetch_3

  # Fan-in: Aggregate
  - id: combine
    dependsOn: [fetch_1, fetch_2, fetch_3]

Sequential Pipeline

steps:
  - id: fetch
  - id: transform
    dependsOn: [fetch]
  - id: store
    dependsOn: [transform]

Diamond Pattern

steps:
  - id: fetch

  - id: process_a
    dependsOn: [fetch]
  - id: process_b
    dependsOn: [fetch]

  - id: merge
    dependsOn: [process_a, process_b]

ForEach Iteration

steps:
  - id: get_packages
    type: tool
    tool: oci.get_image_config
    arguments:
      image: "{{.params.image}}"

  - id: check_vulns
    type: forEach
    collection: "{{json .steps.get_packages.output.packages}}"
    itemVar: pkg                   # defaults to "item"
    maxParallel: 5                 # defaults to DAG maxParallel (10)
    step:                          # single inner step (tool only)
      type: tool
      tool: osv.query_vulnerability
      arguments:
        package_name: "{{.forEach.pkg.name}}"
    dependsOn: [get_packages]
    onError:
      action: continue             # skip failed items, don't abort

Output: {{.steps.check_vulns.output.iterations}}, .count, .completed, .failed

Retry with Fallback

steps:
  - id: try_primary
    tool: primary.api
    onError:
      action: retry
      maxRetries: 2

  - id: use_fallback
    tool: secondary.api
    dependsOn: [try_primary]
    condition: "{{ne .steps.try_primary.status \"completed\"}}"

Validation Rules

  • ✅ Workflow name: ^[a-z0-9]([a-z0-9_-]*[a-z0-9])?$ (1-64 chars)
  • ✅ Step IDs must be unique
  • ✅ All dependsOn step IDs must exist
  • ✅ No circular dependencies
  • ✅ Tool format: workload_id.tool_name
  • ✅ Max retry count: 10 (runtime capped - values > 10 are silently reduced with warning)
  • ✅ Max workflow steps: 100 (runtime enforced - workflows > 100 steps fail validation)
  • ✅ forEach maxIterations: 1000 (hard cap), defaults to 100
  • ✅ forEach maxParallel: 50 (hard cap), defaults to DAG maxParallel (10)
  • ✅ forEach inner step must be type tool (no nested forEach or elicitation)
  • ✅ forEach itemVar cannot be index (reserved)

Note: Max retry and max steps limits are currently enforced at runtime. Future work may add CRD-level validation (+kubebuilder:validation:MaxItems=100) and webhook validation to fail at submission time rather than execution time.

Debugging

Check Workflow Status

# In VirtualMCPCompositeToolDefinition
status:
  validationStatus: Valid|Invalid
  validationErrors:
    - "error message here"
  referencedBy:
    - namespace: default
      name: vmcp-server-1

Common Issues

ErrorCauseFix
"output not found"Missing dependsOnAdd dependency
"circular dependency"Cycle in dependsOnRemove cycle
"tool not found"Invalid tool referenceCheck workload.tool format
"template error"Invalid Go templateFix template syntax

Performance Tips

  1. ✅ Remove unnecessary dependsOn constraints
  2. ✅ Group related steps in same execution level
  3. ✅ Set realistic timeouts based on SLAs
  4. ✅ Use retry for transient failures only
  5. ✅ Keep steps focused (one responsibility)