Skip to content

Commit 49d01f9

Browse files
jerm-droCopilot
andauthored
Replace hypothetical example with real arxiv-mcp-server workflow (#474)
* 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 * Update docs/toolhive/guides-vmcp/composite-tools.mdx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix formatting --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 0bf97c8 commit 49d01f9

1 file changed

Lines changed: 137 additions & 43 deletions

File tree

docs/toolhive/guides-vmcp/composite-tools.mdx

Lines changed: 137 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -61,44 +61,73 @@ For complex, reusable workflows, you can also reference external
6161

6262
## Simple example
6363

64-
Here's a basic composite tool that fetches a URL and then summarizes it:
64+
Here's a composite tool that searches arXiv for papers on a topic and reads the
65+
top result:
6566

6667
```yaml title="VirtualMCPServer resource"
6768
spec:
6869
config:
6970
compositeTools:
70-
- name: fetch_and_summarize
71-
description: Fetch a URL and create a summary
71+
- name: research_topic
72+
description: Search arXiv for papers and read the top result
7273
parameters:
7374
type: object
7475
properties:
75-
url:
76+
query:
7677
type: string
78+
description: Research topic to search for
7779
required:
78-
- url
80+
- query
7981
steps:
80-
- id: fetch
81-
tool: fetch.fetch
82+
# Step 1: Search arXiv for papers matching the query
83+
- id: search
84+
tool: arxiv.search_papers
8285
arguments:
83-
url: '{{.params.url}}'
84-
- id: summarize
85-
tool: llm.summarize
86+
query: '{{.params.query}}'
87+
max_results: 1
88+
# Step 2: Download the paper (required before reading)
89+
# Note: fromJson is needed when the MCP server returns JSON as text
90+
# rather than structured content. This is common for servers that
91+
# don't fully support MCP's structuredContent field.
92+
- id: download
93+
tool: arxiv.download_paper
8694
arguments:
87-
text: '{{.steps.fetch.output.content}}'
88-
dependsOn: [fetch]
95+
paper_id:
96+
'{{(index (fromJson .steps.search.output.text).papers 0).id}}'
97+
dependsOn: [search]
98+
# Step 3: Read the downloaded paper content
99+
- id: read
100+
tool: arxiv.read_paper
101+
arguments:
102+
paper_id:
103+
'{{(index (fromJson .steps.search.output.text).papers 0).id}}'
104+
dependsOn: [download]
89105
```
90106
91107
**What's happening:**
92108
93-
1. **Parameters**: Define the workflow inputs (just `url` in this case)
94-
2. **Step 1 (fetch)**: Calls the `fetch.fetch` tool with the URL from parameters
95-
using template syntax `{{.params.url}}`
96-
3. **Step 2 (summarize)**: Waits for the fetch step (`dependsOn: [fetch]`), then
97-
calls `llm.summarize` with the fetched content using
98-
`{{.steps.fetch.output.content}}`
109+
1. **Parameters**: Define the workflow inputs (`query` for the research topic)
110+
2. **Step 1 (search)**: Calls `arxiv.search_papers` with the query from
111+
parameters using template syntax `{{.params.query}}`
112+
3. **Step 2 (download)**: Waits for search (`dependsOn: [search]`), then
113+
downloads the paper. The `fromJson` function parses the JSON text returned by
114+
the server, and `index` accesses the first paper's ID.
115+
4. **Step 3 (read)**: Waits for download, then reads the paper content.
116+
117+
When a client calls this composite tool, vMCP executes all three steps in
118+
sequence and returns the paper content.
119+
120+
**Structured content vs JSON text**
99121

100-
When a client calls this composite tool, vMCP executes both steps in sequence
101-
and returns the final summary.
122+
MCP servers can return data in two ways:
123+
124+
- **Structured content**: Data is in `structuredContent` and can be accessed
125+
directly: `{{.steps.stepid.output.field}}`
126+
- **JSON text**: Data is returned as a JSON string in the `text` field and
127+
requires parsing: `{{(fromJson .steps.stepid.output.text).field}}`
128+
129+
The arxiv-mcp-server in this example uses JSON text, so we use `fromJson`. Check
130+
your backend's response format to determine which approach to use.
102131

103132
## Use cases
104133

@@ -318,58 +347,123 @@ spec:
318347

319348
Access workflow context in arguments:
320349

321-
| Template | Description |
322-
| ----------------------- | ------------------------------------------ |
323-
| `{{.params.name}}` | Input parameter |
324-
| `{{.steps.id.output}}` | Step output |
325-
| `{{.steps.id.content}}` | Elicitation response content |
326-
| `{{.steps.id.action}}` | Elicitation action (accept/decline/cancel) |
350+
| Template | Description |
351+
| --------------------------- | ------------------------------------------ |
352+
| `{{.params.name}}` | Input parameter |
353+
| `{{.steps.id.output}}` | Step output (map) |
354+
| `{{.steps.id.output.text}}` | Text content from step output |
355+
| `{{.steps.id.content}}` | Elicitation response content |
356+
| `{{.steps.id.action}}` | Elicitation action (accept/decline/cancel) |
357+
358+
### Template functions
359+
360+
The following functions are available for use in templates:
361+
362+
| Function | Description | Example |
363+
| ---------- | -------------------------------- | -------------------------------------------- |
364+
| `fromJson` | Parse a JSON string into a value | `{{(fromJson .steps.s1.output.text).field}}` |
365+
| `json` | Encode a value as a JSON string | `{{json .steps.s1.output}}` |
366+
| `quote` | Quote a string value | `{{quote .params.name}}` |
367+
| `index` | Access array elements by index | `{{index .steps.s1.output.items 0}}` |
368+
369+
### Accessing step outputs
370+
371+
When an MCP server returns structured content, you can access output fields
372+
directly:
373+
374+
```yaml
375+
# Direct access when server supports structuredContent
376+
result: '{{.steps.fetch.output.data}}'
377+
items: '{{index .steps.search.output.results 0}}'
378+
```
379+
380+
This is the simplest approach and works when the backend MCP server populates
381+
the `structuredContent` field in its response.
382+
383+
### Working with JSON text responses
384+
385+
Some MCP servers return structured data as JSON text rather than using MCP's
386+
`structuredContent` field. When this happens, use `fromJson` to parse it:
387+
388+
```yaml
389+
# Parse JSON text and access a nested field
390+
paper_id: '{{(index (fromJson .steps.search.output.text).papers 0).id}}'
391+
```
392+
393+
This pattern:
394+
395+
1. Gets the text output: `.steps.search.output.text`
396+
2. Parses it as JSON: `fromJson ...`
397+
3. Accesses the `papers` array and gets the first element: `index ... 0`
398+
4. Gets the `id` field: `.id`
399+
400+
**How to tell which approach to use:** Call the backend tool directly and
401+
inspect the response. If `structuredContent` contains your data fields, use
402+
direct access. If `structuredContent` only has a `text` field containing JSON,
403+
use `fromJson`.
327404

328405
## Complete example
329406

330-
A VirtualMCPServer with an inline composite tool:
407+
A VirtualMCPServer with an inline composite tool using the
408+
[arxiv-mcp-server](https://github.com/blazickjp/arxiv-mcp-server):
331409

332410
```yaml
333411
apiVersion: toolhive.stacklok.dev/v1alpha1
334412
kind: VirtualMCPServer
335413
metadata:
336-
name: workflow-vmcp
414+
name: research-vmcp
337415
namespace: toolhive-system
338416
spec:
339417
incomingAuth:
340418
type: anonymous
341419
config:
342-
groupRef: my-tools
420+
groupRef: research-tools
343421
aggregation:
344422
conflictResolution: prefix
345423
conflictResolutionConfig:
346424
prefixFormat: '{workload}_'
347425
compositeTools:
348-
- name: fetch_and_summarize
349-
description: Fetch a URL and create a summary
426+
- name: research_topic
427+
description: Search arXiv for papers and read the top result
350428
parameters:
351429
type: object
352430
properties:
353-
url:
431+
query:
354432
type: string
355-
description: URL to fetch
433+
description: Research topic to search for
356434
required:
357-
- url
435+
- query
358436
steps:
359-
- id: fetch_content
360-
tool: fetch.fetch
437+
- id: search
438+
tool: arxiv.search_papers
439+
arguments:
440+
query: '{{.params.query}}'
441+
max_results: 1
442+
- id: download
443+
tool: arxiv.download_paper
361444
arguments:
362-
url: '{{.params.url}}'
363-
- id: summarize
364-
tool: llm.summarize # Hypothetical backend - replace with your actual LLM server
445+
paper_id:
446+
'{{(index (fromJson .steps.search.output.text).papers 0).id}}'
447+
dependsOn: [search]
448+
- id: read
449+
tool: arxiv.read_paper
365450
arguments:
366-
text: '{{.steps.fetch_content.output.content}}'
367-
dependsOn: [fetch_content]
451+
paper_id:
452+
'{{(index (fromJson .steps.search.output.text).papers 0).id}}'
453+
dependsOn: [download]
368454
timeout: '5m'
369455
```
370456

371-
For complex, reusable workflows, create `VirtualMCPCompositeToolDefinition`
372-
resources and reference them with `spec.config.compositeToolRefs`:
457+
> Note: The example above assumes you have:
458+
>
459+
> - An `MCPGroup` named `research-tools`.
460+
> - An `arxiv-mcp-server` deployed as an `MCPServer` or `MCPRemoteProxy`
461+
> resource that references the `research-tools` group.
462+
>
463+
> For a complete example of configuring MCP groups and backend servers, see the
464+
> quickstart and tool aggregation guides. For complex, reusable workflows,
465+
> create `VirtualMCPCompositeToolDefinition` resources and reference them with
466+
> `spec.config.compositeToolRefs`:
373467

374468
```yaml title="VirtualMCPServer resource"
375469
spec:

0 commit comments

Comments
 (0)