Skip to content

generate-cli writes generated files in the locale encoding, so non-UTF-8 platforms crash or emit an unimportable CLI #5354

Description

@ch-z-hc

Description and Issues

generate_cli_command writes both generated files with the platform's locale encoding instead of UTF-8:

# fastmcp/cli/generate.py:770 and :784
output_path.write_text(script)
skill_path.write_text(skill_content)

Path.write_text() without encoding= uses locale.getpreferredencoding(False), which on a Chinese Windows install is cp936/GBK. Two distinct failure modes follow from that, both hit by ordinary tool descriptions:

1. Generation aborts. Any character GBK cannot encode (emoji, most typographic symbols) raises out of the CLI:

File ".../fastmcp/cli/generate.py", line 770, in generate_cli_command
    output_path.write_text(script)
  File ".../Python311/Lib/pathlib.py", line 1079, in write_text
    return f.write(data)
UnicodeEncodeError: 'gbk' codec can't encode character '\U0001f642' in position 681

2. The generated CLI is not importable. When the text is GBK-encodable — i.e. any Chinese, Japanese or Cyrillic description — the write succeeds but the bytes on disk are GBK, while CPython decodes source files as UTF-8. So the file the tool just produced cannot be run:

bytes on disk : b'DESC = "\xb2\xe9\xd1\xaf Boston \xb5\xc4\xcc\xec\xc6\xf8"\r\n'
valid UTF-8   : NO -> 'utf-8' codec can't decode byte 0xb2 in position 8: invalid start byte
importable    : NO -> (unicode error) 'utf-8' codec can't decode byte 0xb2 in position 0: invalid start character

SKILL.md has the same problem, and it is worse there: skill files are read back by agents and the convention is UTF-8, so a GBK SKILL.md is silently unreadable to whatever consumes it.

This is inconsistent with the rest of the codebase, which already pins the encoding on the files it writes — fastmcp/mcp_config.py:348 and fastmcp/utilities/skills.py:208 both pass encoding="utf-8". generate.py looks like the two call sites that were missed.

Steps to Reproduce

Click to expand

Needs a non-UTF-8 locale to see the failure (Windows with a zh-CN system locale here). On a UTF-8 machine both writes already succeed, which is why CI does not catch it.

import asyncio, tempfile
from pathlib import Path
from unittest.mock import patch

from fastmcp import FastMCP
from fastmcp.cli import generate as generate_module
from fastmcp.cli.generate import generate_cli_command
from fastmcp.client import Client

server = FastMCP("WeatherServer")

@server.tool
def get_weather(city: str) -> str:
    """查询 Boston 的天气 — returns a forecast 🙂"""
    return f"sunny in {city}"

async def main():
    out = Path(tempfile.mkdtemp()) / "cli.py"
    with (
        patch.object(generate_module, "resolve_server_spec", lambda spec, **kw: "fake://server"),
        patch.object(generate_module, "_build_client", lambda resolved, **kw: Client(server)),
    ):
        await generate_cli_command("weather-server", str(out))
    print(out.read_bytes()[:80])

asyncio.run(main())

Drop the emoji from the docstring to get failure mode 2 instead of mode 1.

Expected Behavior

Both generated files are written as UTF-8 regardless of the platform locale, so that a CLI produced on any machine is importable on any other, and SKILL.md is valid UTF-8.

Proposed fix

Pass encoding="utf-8" at the two call sites, matching mcp_config.py and utilities/skills.py, plus a regression test that asserts the produced bytes decode as UTF-8 (which holds on every platform, and only actually fails on a non-UTF-8 locale).

Two related notes, for whatever they are worth:

  • fastmcp/cli/install/claude_desktop.py:118 reads the user's existing claude_desktop_config.json the same way (.read_text() with no encoding). That file is JSON, hence UTF-8 by RFC 8259, so a config containing a non-ASCII path or server name fails to load on a cp936 machine. Same class, different command, and it needs the read side fixed as well as the write side.
  • The repo's own guard test is affected by the same rule: tests/server/auth/test_redirect_validation.py:232 does path.read_text() over the auth modules, and on this machine it raises UnicodeDecodeError: 'gbk' codec can't decode byte 0x94 — so that security guard silently does not run outside UTF-8 locales. Happy to fold that one in if you want it in the same PR.

I can open a PR with the fix and the regression test if this is wanted.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working. Reports of errors, unexpected behavior, or broken functionality.cliRelated to FastMCP CLI commands (run, dev, install) or CLI functionality.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions