Skip to content

Latest commit

 

History

History
344 lines (269 loc) · 9.76 KB

File metadata and controls

344 lines (269 loc) · 9.76 KB

Composite Tools Quick Reference

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

  annotations:                   # Optional: explicit MCP tool annotations
    title: My Workflow           # Derivation runs at advertise time, not CRD
                                 # admission. The floor is fail-closed: with
                                 # one or more tool steps it is always non-nil,
                                 # and any step with nil/unknown annotations
                                 # taints it to readOnly=false,
                                 # destructive=true, openWorld=true.
    readOnlyHint: false          # ⚠ An explicit readOnlyHint: true here would
                                 # CONTRADICT the floor whenever a step tool
                                 # does not declare readOnlyHint: true (the
                                 # common case), dropping the composite tool
                                 # from tools/list at runtime with a warning.
                                 # thv vmcp validate does NOT catch this — it
                                 # surfaces only in the vMCP logs. Prefer
                                 # leaving readOnlyHint unset (derive it) or
                                 # declaring a value no safer than the floor.

  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)
  • ✅ Annotations must not contradict the step-derived safety floor (e.g. readOnlyHint: true when a step is not read-only) — a contradicting composite tool is dropped from tools/list at runtime with a warning in the vMCP logs (more-conservative hints, e.g. readOnlyHint: false, are allowed)

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

Error Cause Fix
"output not found" Missing dependsOn Add dependency
"circular dependency" Cycle in dependsOn Remove cycle
"tool not found" Invalid tool reference Check workload.tool format
"template error" Invalid Go template Fix 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)

Links