Fixed
-
The savings headline no longer credits mcptoon for a saving it is not
delivering. Delivery mode had one "gateway is wired" state,serve, and
every surface quoted it as "saves 99%". But a gateway can be registered in one
agent while other host configs still list the upstream servers mcptoon
manages — so those hosts keep loading every full schema natively and the
machine is actually loading catalog + gateway (measured here: 640,787 + 3,303
tokens/turn, i.e. ~0.5% MORE than before install) while the footer, statusline,
status,credit,report,savingsandbenchall reported "省 99%" /
"≈省 2B · 3,119 次". That is the credit pointing the wrong way: the user is told
they are saving when they are paying a little extra.mode.detect()now returns a fifth state,serve-alongside, when the gateway
is registered andsync.takeover_plan()finds direct server entries it
would remove (alongside/direct_entriestravel with the mode). Every
surface branches on it and says the honest thing instead — "wired but not taken
over, saving nothing yet" — with the one fix,mcptoon sync --takeover, and
the statusline tagserve→takeover.mode recommendand the JSONone_action
carry the same command.takeover_plan()is read-only and ~1.7 ms, and
detect()swallows any failure back to plainserve, so a broken config can
never turn this into a crash. -
The first-run card (
welcome) never checked the delivery mode. It showed
"上下文本钱 640,787 → 3,303(省 99%)" on adirectmachine (installed, not
wired) that saves nothing, and would have shown the same on the new
serve-alongsidestate.welcome.render()now takes the mode from
footer.current_mode()and, on either not-yet-saving state, replaces the saving
row with the truth ("full N tokens load · not wired/not taken over, no saving
yet") plus a "First" row namingmcptoon sync --self/--takeover. A
genuinely savingserve/climachine keeps its saving row unchanged, and a
barerender()with no mode argument (tests, older callers) still renders the
plain row. -
serve --httpwas single-threaded despite promising concurrency.
_build_http_serverbuilt a plainhttp.server.HTTPServer, which handles one
request at a time, so a single slow upstream call blocked every other agent —
a self-inflicted denial of service — even though_run_http's own docstring
says "Supports multiple concurrent agents". It now builds a
ThreadingHTTPServerwithdaemon_threads = True. -
servenever forced UTF-8 on its stdio pipes. On Windows a piped stdout
defaults to the ANSI code page (cp1252/cp936), so a response carrying a Chinese
tool result raisedUnicodeEncodeErrorinside_send_response's try/except,
was swallowed, and left the client waiting for a reply that never came.run()
now reconfiguressys.stdin/sys.stdoutto UTF-8 up front (the same guard the
bundled demo bridge already had). -
serve's startup timeout was defeated byshutdown(wait=True). The
parallel manifest fetch wrappedThreadPoolExecutorin awithblock and
cappedas_completed(timeout=60); on timeout thewithexit still waited for
every hung worker (up toMCPTOON_CALL_TIMEOUT, 900s) while holding
_ensure_initialized's lock. It now cancels pending futures and shuts down
withwait=False, so startup is actually bounded at 60s. -
A reconnect could inject a spurious "server process exited" into a new
request. The stdio response pump signals every registered queue with aNone
sentinel on EOF. After_reconnect_if_deadspawned a new process, the old
pump thread (still blocked on the dead pipe) could fire that sentinel into the
shared queue dict and fail a brand-new in-flight request. Each spawn now owns a
fresh per-generation queue dict, so a dead pump can only fail its own waiters. -
An unreadable agent config was overwritten with no backup. In
sync._write_json_safe, ifpath.read_text()raisedOSError,old_text
becameNone, which skipped both the "refuse to overwrite an unparseable file"
guard and the.bakwrite — then wrote over a config mcptoon never read,
irreversibly. An unreadable existing file is now treated like an unparseable
one: refuse and report. -
mcptoon sync --watch --drysilently wrote configs. The--watchbranch
in_cmd_syncreturned intowatch.main()beforesyncparsed--dry/
--self/--takeover, andwatch.maindropped them, so every watch cycle
calledsync_to_all()with no arguments (a real write). The flags are now
threaded throughwatch.main→watch()→sync_to_all(...). -
call --autoignored the per-tool output policy and lost the receipt's
server/tool. The explicitcall <server> <tool>path rendered through
_effective_fmt()and passedserver/tool, but--autodid neither — so a
policy set <tool> rawwas honoured for one path and ignored for the other,
and smart compression's retrieve handle lost itsserver:toolidentity.
call_tool_autonow returns the chosen server and--autorenders through the
same policy path. -
mcptoon_healthcounted an alive-but-empty server as failed. The native
health tool inferred failure from "absent from the tool index", which is also
true of a healthy server that exposes zero tools — reintroducing a bug
serve._handle_healthhad already fixed. It now uses the bridge's authoritative
fetch_errorsmap. -
Bad numeric/enum arguments exited 0.
manifest --sort bogus,
manifest --top xyzandhealth --timeout abcprinted an error but returned
from the handler, somcptoon ... && nextrannext. All three nowexit 1. -
--raw --fullsilently truncated at 50,000 chars.--fullsets the
effective limit to 0, so the normal truncation notice never fired and the user
got a cut raw body with no warning. The raw hard cap is kept (an unbounded dump
can wedge a terminal) but now announces the cut. -
plugin install --forcedeleted the old version before copying the new
one. A copy failure then left no plugin at all while the registry still listed
it installed. The upgrade now copies to a temp dir first and swaps, so a failed
upgrade leaves the working install intact. -
Two writers raced on a fixed temp filename.
skills._save_version_ledger
andbake._save_bakesboth wrote to a fixed.tmpsibling, so two concurrent
runs could clobber each other's temp andos.replacecould swap in a partial
file (corrupting the shared skill version ledger). Both now use a unique
pid+uuid temp name. -
installer._save_installedcould silently lose the whole installed-server
registry. It wrotepro_installed.jsonwith a plainopen("w")(truncate
first), so a crash or disk-full mid-write left a truncated file that
_load_installedreads back as{}— total, silent loss. It now writes to a
unique temp file andos.replaces. Generated HTTP handler files are likewise
written atomically. -
mcptoon_usage(the MCP tool) measured tokens withlen(json)//4while the
CLI/footer use tiktoken. On CJK or code-heavy schemas the two diverge, so an
agent reading the tool'ssavingsPctsaw a different number thanmcptoon statson the same machine — directly contradicting the tool's own docstring.
It now uses the sharedusage.count_tokenscaliber, as does
schema_simplifier.compute_token_stats. -
Two more fixed-temp writers raced under concurrency.
footer._save_token_totals
(runs after nearly every command) andoauth.save_token(a credential file)
both used a fixed.tmpname; concurrent processes shared one scratch file, so
oneos.replacewon and the loser raisedFileNotFoundError, or an interleaved
write left a torn cache/token. Both now usetempfile.mkstemp. -
discoverleaked an HTTP socket on every probe._probe_endpointand
_probe_http_mcpread the response but never closed it (every other urllib
call in the repo useswith ... as resp:). They now close it too. -
_human_tokensprinted1000Minstead of1B. Values from 999,500,000 to
999,999,999 rounded to a whole number of millions before the billion check, so
they rendered as the unreadable "1000M". It now promotes to1B. -
democould label the winning format wrong on a token tie. The best format
was looked up through a value→name dict, which kept the last write on a tie. It
is now a singlemin()over(tokens, name)pairs.