Repository navigation
Description
Observed Behavior
When MAF turns a JSON-like tool result into model-visible text, it calls json.dumps(..., default=str) without ensure_ascii=False. Every non-ASCII character is therefore sent to the model as a \uXXXX escape: Año becomes A\u00f1o, Género becomes G\u00e9nero.
This affects:
- MCP tools,
structuredContent: the block built forstructured_first(the default),structured_onlyandboth. See_mcp.py#L1219-L1221. - Function tools returning a dict, list or model:
FunctionTool.parse_result. See_tools.py#L1406and_tools.py#L1412. Content.from_function_result: the same pattern. See_types.py#L1010.
The impact:
- More tokens on non-English data. For a short list of Spanish column names, the escaped JSON is 45 tokens against 27 for the UTF-8 form (
o200k_base), about 67% more. - The model echoes the escapes. It can copy
\u00f1sequences into its answers or into the arguments of the next tool call. - The default MCP mode cannot be used for this data. The only way to avoid the escaping is
tool_result_content="content_first"or"content_only", and that works only when the server's owncontentblock is UTF-8. There is no workaround for function tools.
The MCP server's own content text is passed through unescaped, so the escaping comes from MAF's serialization, not from the server.
Expected Behavior
Tool results reach the model as UTF-8 JSON ("Año", "Género"), the same way MAF already serializes text elsewhere. _compaction.py, _sessions.py, observability.py and the tool-arguments logging in _tools.py all pass ensure_ascii=False. Earlier fixes for the same problem: #6627 / #6628 (Bedrock JSON content blocks), #7124 (compaction token counts) and #3894 (span attributes).
Suggested fix: add ensure_ascii=False to the three json.dumps calls above.
Steps to Reproduce
- Save
server.pyandrepro.pyfrom Minimal Reproduction in one folder. - Install
agent-framework-coreandmcp. - Run
python -X utf8 repro.py. The-X utf8only makes the Windows console print UTF-8; it does not change MAF's output. - Observe that the
structuredContentblocks and the function-tool result contain\u00f1/\u00e9/\u00ed, while the MCP server's owncontentblock containsñ/é/í.
Minimal Reproduction
server.py, a FastMCP server whose typed return value produces both content and structuredContent:
from typing import TypedDict
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("demo", log_level="WARNING")
class Columns(TypedDict):
columns: list[str]
@mcp.tool()
def get_columns() -> Columns:
"""Return dataset column names."""
return {"columns": ["Año", "Género", "Categoría"]}
if __name__ == "__main__":
mcp.run()repro.py:
import asyncio
import sys
from agent_framework import MCPStdioTool, tool
@tool
def get_columns_local() -> dict:
"""Return dataset column names."""
return {"columns": ["Año", "Género", "Categoría"]}
async def main() -> None:
for mode in ("structured_first", "content_first", "both"):
async with MCPStdioTool(
name="demo",
command=sys.executable,
args=["server.py"],
tool_result_content=mode,
) as mcp_tool:
func = next(f for f in mcp_tool.functions if f.name == "get_columns")
for content in await func.invoke():
print(f"MCP {mode}: {content.text}")
for content in await get_columns_local.invoke():
print(f"FunctionTool: {content.text}")
asyncio.run(main())Output (server logs on stderr omitted):
MCP structured_first: {"columns": ["A\u00f1o", "G\u00e9nero", "Categor\u00eda"]}
MCP content_first: {
"columns": [
"Año",
"Género",
"Categoría"
]
}
MCP both: {
"columns": [
"Año",
"Género",
"Categoría"
]
}
MCP both: {"columns": ["A\u00f1o", "G\u00e9nero", "Categor\u00eda"]}
FunctionTool: {"columns": ["A\u00f1o", "G\u00e9nero", "Categor\u00eda"]}
Error Messages and Stack Traces
None. Nothing is raised; the model just receives escaped text.
Package Versions
agent-framework-core: 1.21.0, mcp: 1.30.0
Python Version
Python 3.13.1
Operating System
Windows
Regression
No
Additional Context
Token counts, using tiktoken with o200k_base, for {"columns": ["Año", "Género", "Categoría", "Líquido medio", "Cotización"]}:
| Serialization | Tokens |
|---|---|
json.dumps(data) |
45 |
json.dumps(data, ensure_ascii=False) |
27 |
We hit this on an MCP server that returns Spanish dataset metadata. #7866 fixed the duplicated output by adding tool_result_content, but the default structured_first still sends the escaped form, so on non-English data we have to move off the default to keep token use down.
Acknowledgements
- I searched existing issues and did not find a duplicate.
- I personally verified this behavior and the reproduction details are authentic.
- I will wait for explicit maintainer agreement before starting implementation of a non-trivial change.
Metadata
Metadata
Labels
Type
Projects
- StatusShow more project fieldsNo status
Activity
eavanvalkenburg commentedon Oct 14, 2026
/plan
github-actions commentedon Oct 14, 2026
Summary
The issue reports that Model Context Protocol (MCP) tool results and function-tool results are turned into text with Python's
json.dumps(..., default=str)withoutensure_ascii=False. By default,json.dumpsescapes every non-ASCII character to a\uXXXXsequence, so"Año"becomes"A\u00f1o"before it reaches the model. This happens in three places:_mcp.py(around line 1290), wherestructuredContentreturned by an MCP server is serialized for the model-visible text (the defaulttool_result_content="structured_first"mode, plusstructured_only/both)._tools.py(around lines 1412/1416), whereFunctionTool.parse_resultturns a dict/list/model return value from a@tool-decorated function into text._types.py(around line 1040 inContent.from_function_result), the same pattern for a raw (non-Content) result.I checked these locations directly in the current
mainbranch and confirmed none of the three calls passensure_ascii=False, while other serialization call sites in the same package (_compaction.py,_sessions.py,observability.py, and the tool-argument logging line in_tools.pyitself) already do. So the inconsistency is real and verified, not just as described in the issue.Recommendation
Code changes are warranted. This is a real inconsistency, not a documented limitation or something an existing feature already solves:
tool_result_content="content_first"or"content_only"onMCPTool, and that only helps if the MCP server's owncontentblock is UTF‐8 text — it does nothing forstructuredContent-only servers, and there is no workaround at all for function tools returning dicts/lists directly.ensure_ascii=False, to three existingjson.dumpscalls) and matches a serialization convention the codebase has already adopted elsewhere, so the risk of introducing new bugs or compatibility problems is low.\uXXXXescapes into later tool-call arguments instead of the original characters.I did not find any indication that non-ASCII JSON escaping is an intentional design choice anywhere in these three call sites (no comments or tests asserting escaped output), so this looks like an oversight rather than a deliberate boundary.
Proposed plan
_mcp.py: addensure_ascii=Falseto thejson.dumps(mcp_type.structuredContent, default=str)call used to build thestructuredContenttext block._tools.py: addensure_ascii=Falseto bothjson.dumps(dumpable, default=str)calls inFunctionTool.parse_result(the list-item branch and the single-result branch)._types.py: addensure_ascii=Falseto thejson.dumps(result, default=str)call inContent.from_function_result.test_mcp.pyandtest_tools.py, where MCP and function-tool result parsing are already covered) that return non-ASCII data (e.g. a dict with accented characters) and assert the produced text contains the literal characters ("Año") rather than\uXXXXescapes, for each of the three code paths. Also confirm nearby behavior is unchanged — e.g. that ASCII-only results, non-dict/list results, and the existingcontent_first/bothmodes still serialize the same as before except for the escaping change.repro.py) prints{"columns": ["Año", "Género", "Categoría"]}instead of the escaped form for all three reported cases (structured_first,both, and the plain function tool).This is a proposal only — no code has been changed. The main open question is just confirming the three call sites above are the complete set MAF currently has for this pattern (a quick repo-wide search for
json.dumpswithoutensure_ascii=Falseduring implementation would catch any sibling occurrence missed here).ZedingZhang commentedon Oct 14, 2026
I'd like to work on this based on the proposed plan above. Add
ensure_ascii=Falseto the result serialization in_mcp.py, both JSON serialization branches ofFunctionTool.parse_resultin_tools.py, andContent.from_function_resultin_types.py. Add regression tests asserting that non-ASCII characters are preserved in the resulting text, covering MCP structured results and function-tool results, while checking that ASCII output and existing content-selection behavior remain unchangedeavanvalkenburg commentedon Oct 27, 2026
Thanks for the detailed report. We will consider this in core MAF as a consistent policy for framework-generated, model-visible JSON in function/MCP results and prompts. Literal Unicode and escaped Unicode preserve the same decoded JSON values; the benefit here is readable multilingual model context rather than fixing demonstrated data corruption.
The scope should stay model-facing: unrelated transport serialization, Host-payload byte budgeting and checkpoint persistence should not change. The existing planning result and #9400 are the coordination points, so another /plan is not needed. Scope still needs to account for the ResourceLink prompt branch and the current MCP v2 structured-result conversion, with focused coverage of multilingual output and unchanged decoded values.
This records core consideration, not an implementation/timing commitment or implementation-ready approval. The existing volunteer and PR author should be coordinated before any new assignment.