Skip to content

fix(mcp): insert bare-filename rewrites literally - #5522

Merged
WillemJiang merged 1 commit into
bytedance:mainfrom
BlueX888:fix/prep-mcp-bare-filename-rewrite-raw-replacement-template
Sep 18, 2026
Merged

WillemJiang merged 1 commit into
bytedance:mainfrom
BlueX888:fix/prep-mcp-bare-filename-rewrite-raw-replacement-template

Conversation

@BlueX888

Copy link
Copy Markdown
Contributor

Why

_rewrite_unique_bare_filenames correlates a bare filename in a stdio server's free text with the files this tool call created, then substitutes the correlated /mnt/user-data/... virtual path. It handed that path to Pattern.subn as a replacement template (backend/packages/harness/deerflow/mcp/tools.py:280).

The replacement is built from the real file's relative path (_local_uri_to_virtual_path, tools.py:169), and a backslash is an ordinary character in a POSIX filename. A model that passes a Windows-style path to a stdio MCP server on a POSIX host gets a file literally named screenshots\q3.png, so the template contained \q — an unknown escape. Pattern.subn compiles the template before it looks for a match, so this raised no matter what the text said:

  • re.error: bad escape \q at position 36 escaped _convert_call_tool_result and failed the whole MCP tool call, even though the server had already written the file. The agent saw an error instead of the path.
  • Where the backslash starts an escape re does accept, the pass substituted that byte into the returned text: screenshots\raw.png came back with a raw CR in the middle of the path, which then rides into the transcript and the checkpoint.

changed_files is the workspace snapshot diff (_changed_workspace_files, tools.py:194), so the offending file does not have to be one this MCP call wrote — any file in the thread workspace with such a name trips the next tool call that returns text content.

This is the only .sub/.subn call in the harness that passes a built string as a template; _rewrite_local_paths_in_text (tools.py:326) and input_sanitization_middleware.py:160 both insert through a callable.

What changed

  • _rewrite_unique_bare_filenames now substitutes through a callable replacement, so the correlated virtual path is inserted verbatim and a backslash in it is never parsed as a regex escape.
  • Regression tests in backend/tests/test_mcp_file_migration.py cover both halves: a backslash that starts a live escape (\r) must survive as two characters, and one re rejects (\q) must not raise.

No change to which filenames are correlated, to the pattern, or to the \r-free cases. They are skipped on Windows, where a literal backslash cannot be part of a filename.

Surface area

None of the listed boxes apply — this is the MCP harness tool-result layer (backend/packages/harness/deerflow/mcp/tools.py), not an endpoint under backend/app, an agent node/graph/prompt change, or a sandbox/skills/dependency change.

  • Frontend UI
  • Backend API
  • Agents / LangGraph
  • Sandbox
  • Skills
  • Dependencies
  • Default behavior change
  • Docs / tests / CI only

Bug fix verification

  • Test path that reproduces the bug: backend/tests/test_mcp_file_migration.py::TestRewriteUniqueBareFilenames (2 tests)
  • Did it go red on main and green on this branch? yes

Red on main (tests applied, source fix reverted):

FAILED tests/test_mcp_file_migration.py::TestRewriteUniqueBareFilenames::test_backslash_in_replacement_is_inserted_literally
FAILED tests/test_mcp_file_migration.py::TestRewriteUniqueBareFilenames::test_unknown_regex_escape_in_replacement_does_not_raise
2 failed in 0.95s

with AssertionError: assert 'Saved as /mn...shots\raw.png' == 'Saved as /mn...hots\\raw.png' (the \r became a CR) and re.error: bad escape \q at position 36.

Also confirmed end-to-end through _convert_call_tool_result: on main it raises error bad escape \q at position 36; on this branch it returns Saved as /mnt/user-data/workspace/screenshots\q3.png.

Validation

cd backend && make lint
uv run ruff check .
All checks passed!
uv run ruff format --check .
1504 files already formatted

cd backend && PYTHONPATH=. PYTHONIOENCODING=utf-8 PYTHONUTF8=1 uv run pytest -m "not live" --ignore=tests/blocking_io tests/ -q
17003 passed, 186 skipped, 3 deselected, 32 warnings in 443.05s (0:07:23)

cd backend && PYTHONPATH=. PYTHONIOENCODING=utf-8 PYTHONUTF8=1 .venv/bin/python -m pytest tests/test_mcp_file_migration.py -q
65 passed, 6 skipped in 1.87s

AI assistance

Tool(s) used: Claude Code

How you used it: Claude Code drafted the change, the regression tests and the commit message against a spec I wrote; I ran the red/green cycle myself (including reverting the source fix to confirm the tests fail on main), ran the full backend suite and make lint, and reviewed every line of the final diff.

  • I've read and understand every line of this change and take responsibility for it — it's not unreviewed AI output.

_rewrite_unique_bare_filenames handed the correlated /mnt/user-data virtual
path to Pattern.subn as a replacement template. That path is built from the
file's relative path, and a backslash is an ordinary character in a POSIX
filename, so a file written literally as "screenshots\q3.png" -- the shape a
model produces by passing a Windows-style path to a stdio server on a POSIX
host -- turned \q into an unknown template escape. Pattern.subn compiles the
template eagerly, so re.error escaped _convert_call_tool_result and failed the
whole tool call even though the server had already written the file, and the
agent never saw the path.

When the backslash does start a known escape (\r, \t, \b ...), the bare-filename
pass substituted that byte into the returned text instead, so
"screenshots\raw.png" came back as a path with a raw CR in the middle of it.

Insert the correlated path through a callable replacement, matching what
_rewrite_local_paths_in_text already does, so it is never parsed as a template.
@github-actions github-actions Bot added area:mcp Model Context Protocol integration risk:medium Medium risk: regular code changes size/S PR changes 20-100 lines labels Sep 17, 2026
@WillemJiang WillemJiang added this to the 2.1.0 milestone Sep 18, 2026
@WillemJiang
WillemJiang merged commit 796ca28 into bytedance:main Sep 18, 2026
16 checks passed
@WillemJiang WillemJiang modified the milestones: 2.1.0, 2.2.0 Sep 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:mcp Model Context Protocol integration risk:medium Medium risk: regular code changes size/S PR changes 20-100 lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants