Skip to content

feat: add fromjson template filter for consuming MCP tool results - #580

Merged
Jason Robert (jrob5756) merged 4 commits into
microsoft:mainfrom
hertznsk:feat/fromjson-template-filter
Oct 1, 2026
Merged

Jason Robert (jrob5756) merged 4 commits into
microsoft:mainfrom
hertznsk:feat/fromjson-template-filter

Conversation

@hertznsk

Copy link
Copy Markdown
Contributor

Closes #579.

Summary

MCP steps (type: mcp) return the raw MCP envelope, and many widely used MCP servers return data only as a JSON string in content[0].text and never populate structuredContent. Jinja2 ships tojson but no inverse, and TemplateRenderer registered only the json (dumps) and default filters — so downstream steps could not parse a previous MCP tool's JSON text inside arguments, routes, or stdin templates, blocking deterministic read-modify-write workflows (the issue's GitLab update_merge_request label-merge chain is the concrete case).

This PR registers a built-in fromjson filter — the inverse of the existing json filter:

labels: "{{ (((steps.get_mr.output.content[0].text | fromjson).labels | default([])) + ['Conductor::Need human']) | unique | list | tojson }}"

On non-string input or invalid JSON it raises TemplateError with a specific suggestion, consistent with the renderer's error contract.

One extra fix found during review

TemplateRenderer.render() re-wrapped any TemplateError raised by a filter under its generic except Exception handler, replacing the filter's specific suggestion with "Check template and context for errors" and burying the real cause. render() now re-raises filter-raised TemplateErrors unchanged (attaching the rendered template for context when missing), so the filter's own error contract reaches the caller. Regression tests pin the structured contract (suggestion, template_string, and the JSONDecodeError cause).

Changes

  • src/conductor/executor/template.py — fromjson filter registration + implementation; TemplateError-preserving branch in render()
  • tests/test_executor/test_template.py — 6 unit tests: object/array/scalar parsing, composition with default + concat, both error paths with the structured contract asserted
  • tests/test_executor/test_mcp_step.py — integration regression: a prior MCP step's content[0].text parsed with fromjson, merged, serialized with tojson, arriving at the tool as a native list via argument auto-coercion (the issue's exact pattern)
  • docs/workflow-syntax.md — MCP Steps section documents parsing content[0].text with fromjson when a server returns no structuredContent
  • plugins/conductor/skills/conductor/references/{yaml-schema,authoring}.md — fromjson added to both ### Filters blocks
  • changelog.d/579.added.md — towncrier fragment

Verification

  • tests/test_executor/ + tests/test_config/: 2172 passed
  • Full suite (-n 4): 11024 passed; the single failure (tests/test_cli/test_status.py::TestTerminalCostCellBilling::test_status_table_renders_the_label) is a pre-existing parallel-isolation flake — it fails identically on clean main under the same command and passes in isolation with and without this change
  • make check (ruff + ty): clean

MCP steps return the raw MCP envelope, and many widely used MCP servers
return data only as a JSON string in content[0].text without populating
structuredContent. With no inverse of the existing json filter, downstream
steps could not parse that text inside arguments, routes, or stdin
templates, blocking deterministic read-modify-write workflows.

Register a built-in fromjson filter on TemplateRenderer: it parses a JSON
string into native data and raises a TemplateError with a specific
suggestion on non-string input or invalid JSON.

render() previously re-wrapped any TemplateError raised by a filter under
its generic handler, replacing the specific suggestion with "Check template
and context for errors"; it now re-raises filter-raised TemplateErrors
unchanged (attaching the template when missing) so the filter's own error
contract reaches the caller.

Closes microsoft#579
The integration test added for microsoft#579 hand-built a context dict using a
steps.<name>.output prefix, which WorkflowContext never creates: prior
steps are exposed directly by name (ctx[agent] = {"output": ...}). Build
the context through WorkflowContext.store()/build_for_agent() and use the
canonical get_mr.output.content[0].text path so the test pins the shape
the engine actually produces.
@codecov-commenter

Codecov Comments Bot (codecov-commenter) commented Oct 1, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (main@11dcc41). Learn more about missing BASE report.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #580   +/-   ##
=======================================
  Coverage        ?   92.94%           
=======================================
  Files           ?      198           
  Lines           ?    32033           
  Branches        ?        0           
=======================================
  Hits            ?    29772           
  Misses          ?     2261           
  Partials        ?        0           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jrob5756 Jason Robert (jrob5756) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding the fromjson filter for MCP tool results. I have one recommended clarification about passing parsed collections to MCP arguments and route conditions.

1 approved findings: 1 inline, 0 in this review body.

Comment thread docs/workflow-syntax.md Outdated
A bare {{ ... }} interpolation of a parsed collection stringifies it
with Python repr before the MCP argument YAML-coercion pass, turning
null into "None" and escaping embedded newlines; an empty list
renders as the truthy string "[]" in route conditions. Apply the
review feedback: keep parsed values JSON via tojson when they feed
arguments, and test the collection (length > 0) in route conditions.
Regression coverage for the MCP arguments guidance: bare interpolation
of a parsed collection corrupts it through YAML coercion while tojson
round-trips exactly, and an empty list interpolated bare is truthy in
route conditions while length > 0 is not.

@jrob5756 Jason Robert (jrob5756) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks for adding the fromjson template filter to consume MCP tool results!

@jrob5756
Jason Robert (jrob5756) merged commit 3480c4f into microsoft:main Oct 1, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a fromjson template filter for consuming MCP tool results in step arguments

3 participants