From f8c86de9fadc653ce9952473a4e81ecd61b7aa5e Mon Sep 17 00:00:00 2001 From: Jeremy Drouillard Date: Wed, 28 Jan 2026 10:33:32 -0800 Subject: [PATCH 1/3] Replace hypothetical example with real arxiv-mcp-server workflow Replace the hypothetical llm.summarize example in composite tools documentation with a working arxiv-mcp-server example that users can actually deploy and test. Add documentation for template functions (fromJson, json, quote, index) and explain how to handle both structured content and JSON text responses from MCP servers. Fixes #367 --- docs/toolhive/guides-vmcp/composite-tools.mdx | 168 +++++++++++++----- 1 file changed, 127 insertions(+), 41 deletions(-) diff --git a/docs/toolhive/guides-vmcp/composite-tools.mdx b/docs/toolhive/guides-vmcp/composite-tools.mdx index 24805143..eb3cbe65 100644 --- a/docs/toolhive/guides-vmcp/composite-tools.mdx +++ b/docs/toolhive/guides-vmcp/composite-tools.mdx @@ -61,44 +61,73 @@ For complex, reusable workflows, you can also reference external ## Simple example -Here's a basic composite tool that fetches a URL and then summarizes it: +Here's a composite tool that searches arXiv for papers on a topic and reads the +top result: ```yaml title="VirtualMCPServer resource" spec: config: compositeTools: - - name: fetch_and_summarize - description: Fetch a URL and create a summary + - name: research_topic + description: Search arXiv for papers and read the top result parameters: type: object properties: - url: + query: type: string + description: Research topic to search for required: - - url + - query steps: - - id: fetch - tool: fetch.fetch + # Step 1: Search arXiv for papers matching the query + - id: search + tool: arxiv.search_papers arguments: - url: '{{.params.url}}' - - id: summarize - tool: llm.summarize + query: '{{.params.query}}' + max_results: 1 + # Step 2: Download the paper (required before reading) + # Note: fromJson is needed when the MCP server returns JSON as text + # rather than structured content. This is common for servers that + # don't fully support MCP's structuredContent field. + - id: download + tool: arxiv.download_paper arguments: - text: '{{.steps.fetch.output.content}}' - dependsOn: [fetch] + paper_id: + '{{(index (fromJson .steps.search.output.text).papers 0).id}}' + dependsOn: [search] + # Step 3: Read the downloaded paper content + - id: read + tool: arxiv.read_paper + arguments: + paper_id: + '{{(index (fromJson .steps.search.output.text).papers 0).id}}' + dependsOn: [download] ``` **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}}` +1. **Parameters**: Define the workflow inputs (`query` for the research topic) +2. **Step 1 (search)**: Calls `arxiv.search_papers` with the query from + parameters using template syntax `{{.params.query}}` +3. **Step 2 (download)**: Waits for search (`dependsOn: [search]`), then + downloads the paper. The `fromJson` function parses the JSON text returned by + the server, and `index` accesses the first paper's ID. +4. **Step 3 (read)**: Waits for download, then reads the paper content. + +When a client calls this composite tool, vMCP executes all three steps in +sequence and returns the paper content. + +**Structured content vs JSON text** -When a client calls this composite tool, vMCP executes both steps in sequence -and returns the final summary. +MCP servers can return data in two ways: + +- **Structured content**: Data is in `structuredContent` and can be accessed + directly: `{{.steps.stepid.output.field}}` +- **JSON text**: Data is returned as a JSON string in the `text` field and + requires parsing: `{{(fromJson .steps.stepid.output.text).field}}` + +The arxiv-mcp-server in this example uses JSON text, so we use `fromJson`. Check +your backend's response format to determine which approach to use. ## Use cases @@ -318,53 +347,110 @@ spec: 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) | +| Template | Description | +| --------------------------- | ------------------------------------------ | +| `{{.params.name}}` | Input parameter | +| `{{.steps.id.output}}` | Step output (map) | +| `{{.steps.id.output.text}}` | Text content from step output | +| `{{.steps.id.content}}` | Elicitation response content | +| `{{.steps.id.action}}` | Elicitation action (accept/decline/cancel) | + +### Template functions + +The following functions are available for use in templates: + +| Function | Description | Example | +| ---------- | -------------------------------- | -------------------------------------------- | +| `fromJson` | Parse a JSON string into a value | `{{(fromJson .steps.s1.output.text).field}}` | +| `json` | Encode a value as a JSON string | `{{json .steps.s1.output}}` | +| `quote` | Quote a string value | `{{quote .params.name}}` | +| `index` | Access array elements by index | `{{index .steps.s1.output.items 0}}` | + +### Accessing step outputs + +When an MCP server returns structured content, you can access output fields +directly: + +```yaml +# Direct access when server supports structuredContent +result: '{{.steps.fetch.output.data}}' +items: '{{index .steps.search.output.results 0}}' +``` + +This is the simplest approach and works when the backend MCP server populates +the `structuredContent` field in its response. + +### Working with JSON text responses + +Some MCP servers return structured data as JSON text rather than using MCP's +`structuredContent` field. When this happens, use `fromJson` to parse it: + +```yaml +# Parse JSON text and access a nested field +paper_id: '{{(index (fromJson .steps.search.output.text).papers 0).id}}' +``` + +This pattern: + +1. Gets the text output: `.steps.search.output.text` +2. Parses it as JSON: `fromJson ...` +3. Accesses the `papers` array and gets the first element: `index ... 0` +4. Gets the `id` field: `.id` + +**How to tell which approach to use:** Call the backend tool directly and +inspect the response. If `structuredContent` contains your data fields, use +direct access. If `structuredContent` only has a `text` field containing JSON, +use `fromJson`. ## Complete example -A VirtualMCPServer with an inline composite tool: +A VirtualMCPServer with an inline composite tool using the +[arxiv-mcp-server](https://github.com/blazickjp/arxiv-mcp-server): ```yaml apiVersion: toolhive.stacklok.dev/v1alpha1 kind: VirtualMCPServer metadata: - name: workflow-vmcp + name: research-vmcp namespace: toolhive-system spec: incomingAuth: type: anonymous config: - groupRef: my-tools + groupRef: research-tools aggregation: conflictResolution: prefix conflictResolutionConfig: prefixFormat: '{workload}_' compositeTools: - - name: fetch_and_summarize - description: Fetch a URL and create a summary + - name: research_topic + description: Search arXiv for papers and read the top result parameters: type: object properties: - url: + query: type: string - description: URL to fetch + description: Research topic to search for required: - - url + - query steps: - - id: fetch_content - tool: fetch.fetch + - id: search + tool: arxiv.search_papers + arguments: + query: '{{.params.query}}' + max_results: 1 + - id: download + tool: arxiv.download_paper arguments: - url: '{{.params.url}}' - - id: summarize - tool: llm.summarize # Hypothetical backend - replace with your actual LLM server + paper_id: + '{{(index (fromJson .steps.search.output.text).papers 0).id}}' + dependsOn: [search] + - id: read + tool: arxiv.read_paper arguments: - text: '{{.steps.fetch_content.output.content}}' - dependsOn: [fetch_content] + paper_id: + '{{(index (fromJson .steps.search.output.text).papers 0).id}}' + dependsOn: [download] timeout: '5m' ``` From 2e74ab2810bbdce2172336b5a52f160731a8b06d Mon Sep 17 00:00:00 2001 From: Jeremy Drouillard Date: Wed, 28 Jan 2026 10:43:36 -0800 Subject: [PATCH 2/3] Update docs/toolhive/guides-vmcp/composite-tools.mdx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/toolhive/guides-vmcp/composite-tools.mdx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/toolhive/guides-vmcp/composite-tools.mdx b/docs/toolhive/guides-vmcp/composite-tools.mdx index eb3cbe65..18da291f 100644 --- a/docs/toolhive/guides-vmcp/composite-tools.mdx +++ b/docs/toolhive/guides-vmcp/composite-tools.mdx @@ -454,6 +454,12 @@ spec: timeout: '5m' ``` +> Note: The example above assumes you have: +> +> - An `MCPGroup` named `research-tools`. +> - An `arxiv-mcp-server` deployed as an `MCPServer` or `MCPRemoteProxy` resource that references the `research-tools` group. +> +> For a complete example of configuring MCP groups and backend servers, see the quickstart and tool aggregation guides. For complex, reusable workflows, create `VirtualMCPCompositeToolDefinition` resources and reference them with `spec.config.compositeToolRefs`: From bc85a519c62f17609b9bc13893e6986933f47e03 Mon Sep 17 00:00:00 2001 From: Jeremy Drouillard Date: Wed, 28 Jan 2026 10:51:44 -0800 Subject: [PATCH 3/3] Fix formatting --- docs/toolhive/guides-vmcp/composite-tools.mdx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/toolhive/guides-vmcp/composite-tools.mdx b/docs/toolhive/guides-vmcp/composite-tools.mdx index 18da291f..1b2b07ee 100644 --- a/docs/toolhive/guides-vmcp/composite-tools.mdx +++ b/docs/toolhive/guides-vmcp/composite-tools.mdx @@ -457,11 +457,13 @@ spec: > Note: The example above assumes you have: > > - An `MCPGroup` named `research-tools`. -> - An `arxiv-mcp-server` deployed as an `MCPServer` or `MCPRemoteProxy` resource that references the `research-tools` group. +> - An `arxiv-mcp-server` deployed as an `MCPServer` or `MCPRemoteProxy` +> resource that references the `research-tools` group. > -> For a complete example of configuring MCP groups and backend servers, see the quickstart and tool aggregation guides. -For complex, reusable workflows, create `VirtualMCPCompositeToolDefinition` -resources and reference them with `spec.config.compositeToolRefs`: +> For a complete example of configuring MCP groups and backend servers, see the +> quickstart and tool aggregation guides. For complex, reusable workflows, +> create `VirtualMCPCompositeToolDefinition` resources and reference them with +> `spec.config.compositeToolRefs`: ```yaml title="VirtualMCPServer resource" spec: