@@ -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"
6768spec :
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
319348Access 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
333411apiVersion: toolhive.stacklok.dev/v1alpha1
334412kind: VirtualMCPServer
335413metadata:
336- name: workflow -vmcp
414+ name: research -vmcp
337415 namespace: toolhive-system
338416spec:
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"
375469spec:
0 commit comments