Right-click menus can hand the conversation a plain-text reference to what the user points at. The clip menu ends with Send to chat (the selection plus the right-clicked moment); an empty spot on a track and the ruler offer Send this moment to chat; the media menu offers Send to chat after Open in Source. A right-click on a marker now opens a two-item menu, Edit marker… and Send marker to chat; double-click still opens the editor directly. The new miniapp/client/chat-context.js (global FableCutChat, loaded before app.js) builds the text from pure functions: ids, track ids and timeline seconds as the MCP tools address them, timecode in the editor's own MM:SS:FF, frame numbers, the open sequence, the composition path and the project revision. User text is reduced to one bounded line, lists are capped and the whole reference stays under 8,000 characters. Labels go through T(); ids, seconds and user text do not. The page calls window.mcode.miniapp.message.append, which needs no manifest declaration and shows the Host's own confirmation; the items stay visible and disabled with a tooltip when the bridge is missing, and each rejection code gets its own toast. Thirty dictionary keys were added to both languages. tests/chat-context.test.mjs covers the text, frame snapping at seven frame rates, hostile names and the size budget; tests/send-to-chat.test.mjs drives every menu in Chromium with the bridge stubbed, a cancelled confirmation, Chinese labels and a client without the bridge.
The monitor header keeps one row of tabs and icon controls. Canvas aspect, frame rate, export framing and resolution move into a keyboard-accessible popover; the original controls and stored values remain unchanged. Snap targets and sequence actions follow the selected language while user sequence names stay untouched and escaped. Playback controls wrap when the monitor is narrow, avoiding overlap with timecodes. Six browser tests cover menu localization, raw values, escaped names, popover operation and layout at five viewport widths.
Thirteen media and timeline actions use inline SVG icons with localized accessible names, shortcut tooltips and visually hidden text. Their IDs, handlers and enum values remain unchanged. Import keeps a keyboard-focusable file input and visible focus ring. Empty states use one short prompt; Export, navigation and field labels remain visible. The two range-delete labels remain distinct. Eleven tests cover names, keyboard access, action behavior and 32-pixel targets at three viewport widths.
The client toolbar now has primary and secondary rows with wrapping controls and a non-wrapping zoom group. Media and inspector panels have accessible collapse/restore rails with browser-local preferences and responsive defaults. The monitor meter occupies a separate gutter outside the picture; monitor fitting uses the remaining picture area without changing canvas resolution. Chinese monitor and range-edit terminology was revised. Six browser tests cover English/Chinese layouts at 1280x900, 923x899 and 720x800, panel restoration, meter separation and monitor zoom. Host checks covered the empty workspace and a synthetic timeline; the test project is retained separately.
Everything under miniapp/node/vendor/fablecut/ and miniapp/client/ is
vendored from FableCut 1.16.0,
commit fd2af081d73ec94464b88df8c21f640adcc78985 (MIT, © 2026 ronak-create).
The upstream revision is recorded in upstream.json.
The changes below are the whole difference. Each one says what it is for, so the upstream files can be re-vendored later and the patches re-applied.
Files this package adds that are not upstream:
| File | What it is |
|---|---|
miniapp/node/server.mjs |
The Node entry: start(context), the HTTP server, the routing, the lifecycle. |
miniapp/node/adapters/local-api.mjs |
The in-process transport that lets the MCP tools call the editor without a socket. |
miniapp/node/adapters/mcp-http.mjs |
The Streamable HTTP MCP endpoint; rejects browser Origin/Fetch Metadata requests so page scripts cannot acquire Agent file capabilities. |
miniapp/node/adapters/export-tool.mjs |
The wrapper that adds cutline_export to the upstream tool list without touching the upstream dispatcher. |
miniapp/node/export/job-state.mjs |
The state of one export job: ids, the managed root, the atomic state file. |
miniapp/node/export/prepare.mjs |
Copies the saved project, its media, the library assets, the editor and the runner into a job folder under the data dir. |
miniapp/node/export/export-mcp.mjs |
The cutline_export tool definition and the text it answers with. |
miniapp/node/export/runner.mjs |
The standalone runner: its own loopback server, its own secret, its own headless browser. Copied into every job folder. |
miniapp/node/vendor/puppeteer/ |
A bundled build of Puppeteer Core and the 55 runtime packages it pulls in, the browser automation library the runner drives the render with. See its PROVENANCE.md and licenses/INVENTORY.md. |
miniapp/node/vendor/fablecut/processes.js |
Tracks media child processes and waits for their termination when the Host stops the runtime. |
miniapp/node/vendor/fablecut/media-types.js |
The shared managed-media allowlist and browser-executable document classification used by uploads, imports, URL imports and safe static serving. |
Whole-project MCP writes require an explicit baseRevision; a revision embedded in the submitted document is not accepted as evidence of a prior read, and the server allocates the next revision. Forced REST writes also advance the current on-disk revision instead of rewinding it. The Agent manual no longer tells callers to increment a document revision themselves or claims that an unbased targeted patch preserves same-field concurrent edits.
Managed media copies share one video/audio/image/SVG allowlist, with font support confined to the library. Upload, project-folder import and standalone-export preparation reject non-media documents before copying. Every Node and standalone-runner response carries X-Content-Type-Options: nosniff; media, library, proxy and export documents with browser-executable extensions also carry a sandbox CSP, while each editor's own static payload remains executable.
Standalone export preparation rejects unknown built-in or saved delivery preset IDs instead of allowing a later Custom fallback, and applies the same managed-media boundary as an interactive import. Runner heartbeat failures enter the same bounded teardown as render failures, concurrent cleanup is idempotent, and browser shutdown has a bounded close attempt before the process is killed. An independent teardown deadline also bounds a cleanup started by the overall render timeout.
Locale repainting covers the export batch list and timeline transition/fade tooltips. User-facing toast keys and the batch counter are translated without passing user content or stored enum values through the translator.
-
The static root is configurable.
ROOTstays the module's own folder (the encode profiles are watched there); a newSTATIC_DIRtakes the editor's static files fromFABLECUT_STATIC_DIR, which the entry points at the client payload. The last two lines of the request handler serve fromSTATIC_DIRinstead ofROOT. -
The request handler is a plain function.
const server = http.createServer(async (req, res) => {…})becameasync function handleRequest(req, res) {…}, and the module exports it. A Mini App must bind only the port the Host assigns, so the upstream server cannot create and listen on a server of its own. -
The standalone listener is gone. The
server.listen(PORT, HOST, …)block, with its banner and itsconsole.loglines, was removed together with thePORT/HOSTconstants: the Host owns stdout, and a fixed port is not allowed.startExportSweep()now runs at load, where the listen callback used to call it. -
shutdown()is a library call. It no longer closes a server or callsprocess.exit, and it no longer registersSIGINT/SIGTERMhandlers. It closes the export sweep, the export sessions, the auto-version timer, the two debounce timers, the directory watchers and the live-reload clients; the entry calls it fromdispose(). Thefs.watchcalls now keep their handles in awatchersarray so they can be closed. -
The logger is injectable.
console.warnon a damagedproject.jsongoes throughlog.warn, which readsglobalThis.__FABLECUT_LOG__— the entry sets that tocontext.logger. The fallback is stderr. -
The host gate has a same-origin rule.
FABLECUT_TRUST_HOST=1accepts a request whoseOriginnames the same host as the request itself, whatever host name the Host serves the page under. A cross-origin request still fails the origin check. The Host already guards the assigned port with a per-run credential, so a request that reaches the handler is authenticated. -
path=needs the local transport, not a header. TheX-FableCut-Client: mcpheader is gone as the gate: the page is same-origin and can set any header it likes. The absolute-path capability is granted to a request that carries the symbolSymbol.for("fablecut.localRequest"), which only the in-process transport sets and which cannot be forged from a page.The split is the upstream one, with the page on the outside of it: the page cannot name a file outside
media/andlibrary/at all, while the local MCP client — which the user asked for, and which can read any file the user can — still can. The editor page itself never usespath=; it reads servedsrc=paths only.
-
A transport can replace the HTTP client.
apiJSON,apiBytesandhttpOkuse a transport when one is installed (setTransport), andensureUIServer()then reports that the editor is there. Upstream reaches the editor over HTTP on 127.0.0.1:7777; here the editor is this same process, so the tools call its request handler in-process. Without a transport the upstream HTTP path is unchanged. -
The stdio plumbing is gone. The newline-delimited JSON-RPC loop over stdin and its
process.stdout.writewere replaced byhandleMessage(msg), which answers with the response object, and by the module exports. The Host owns stdin and stdout. -
fablecut_set_projectrequires an explicit, trusted base revision. Upstream compared the file's revision withlastReadRevision, a process-global that records the last read. With several MCP sessions in one process, another session's read answered for this write and a stale document slipped through. The tool now requiresbaseRevisionfrom the caller's lastfablecut_get_project; the submitted document's own revision is not treated as proof of that read. A different current revision refuses the write unlessforce:truewas explicitly supplied, and a successful write stores current revision + 1. The write helper also refuses writes after runtime disposal has begun. -
relinkMediaandreplaceMediasee the file's details. The ops are applied synchronously, but the details come from ffprobe, which is asynchronous: upstream handed the op a Promise, so a replaced media entry kept no duration and no width and its clips were never trimmed to fit. The files the ops name are probed first, and the op reads a plain value. -
The editor's location is settable. Upstream printed its own loopback URL, so the user could open the editor in a browser. A host that serves the page itself has no such URL to print:
setEditorLocation()replaces the three strings, and the entry points them at the Mini App. -
fablecut_import_projectwas added. The upstream editor can import a project document, but a document on its own is not portable: its media entries name/media/…paths that belong to the data folder it was made in. The new tool imports a project folder: the document is read, the media files it uses are copied into this installation's library (never overwriting anything), the entries point at the copies, the originals are left untouched, and the project is saved as the open one at the actual current revision. It prevalidates document/media real paths, refuses missing, escaping or unsupported non-media sources, supports nested source folders, and copies into safe unique destination names. Library references are resolved by content rather than silently reusing a different asset. It snapshots the existing project before replacement and refuses concurrent changes during probing. It writes the same sharedproject.jsonthatfablecut_set_projectwrites. -
The
whereparameters say what is possible. The fourwheredescriptions and thefablecut_exportandfablecut_framedescriptions no longer offer a headless browser: in this package there is none, because a background browser cannot load the editor's authenticated origin.
createExportJobs takes allowHeadless. When it is false — as it is here —
where: "headless" is refused with an explanation, and where: "auto" with
no editor tab open is refused as well. The compositor is the editor tab.
This package adds a second way to render, beside the editor-tab export. It is built entirely from this package's own files; the vendored FableCut sources are unchanged for it.
cutline_export {action:"prepare"} copies into a job folder under the
data dir:
- the saved project document,
- every media and library file the document names (byte-for-byte copies, never hard links), plus the user's saved presets, templates, delivery presets and fonts,
- the editor's client payload and the whole
vendor/fablecut/runtime, - a copy of
runner.mjsandjob-state.mjs, - the bundled Puppeteer Core build under
vendor/puppeteer/.
The copy refuses a source that is missing or that resolves outside the project's own media or library folder, before the job becomes runnable.
The copied runtime folder gets a package.json of its own that says
"type": "commonjs". Without it the nearest package.json above the job
folder decides how the runtime's .js files are read, and an ancestor that
says "type": "module" turns them into ES modules - the runner's
require of server.js then fails with "require is not defined".
The runner then loads its copy of the runtime with FABLECUT_DATA_DIR and
FABLECUT_STATIC_DIR pointing at the job folder, so it reads the snapshot and
never the live project, and it does not depend on the installed package
directory after the handoff. It serves the editor on an ephemeral loopback port
of its own:
- every request has to carry a secret the runner generated for that run; the secret never appears in a URL, in an argument or in a log line,
- only the packaged render assets, the snapshot, the media and library copies and the render's own begin/frame/audio/end endpoints are served; every other route, method and query parameter is refused before the upstream handler sees it,
- the page may not reach any other origin: the browser's requests are intercepted and anything that is not this origin is aborted.
The render itself is upstream's: the editor page's fastExport, its
renderAudioMix and the upstream server's ffmpeg encoding are used unchanged.
The upstream allowHeadless: false is left alone - the runner does not use
upstream's headless path, it drives its own browser.
analyze.js, denoise.js, encode-profiles.js, export-jobs.js,
import-url.js, mcp-server.js, media-probe.js, proxy.js, and server.js
import the local processes.js adapter instead of child_process. Their
argument arrays and media operations remain unchanged. The adapter tracks
asynchronous children, stops them on disposal and rejects new work after
shutdown. The entry also cleans up children when binding the Host port fails.
The node analyze.js <video> command line entry was removed: it writes a
frame or a beat sheet to stdout, and the Host owns stdout. The functions it
used are still exported and still reachable through the editor and the tools.
The keys of the SUPERSEDED map follow the font file names, which this
package had to rename (see below), so the five entries with a space in the
name were changed to the hyphenated name. The checksums are unchanged.
The font family a library font is registered under is now read through
shippedFontFamily(). The file names of the shipped fonts carry no space in
this package (a package path may not contain one), and the map keeps the
upstream family names — "Abril Fatface", "Archivo Black", "Bebas Neue",
"DM Sans", "Playfair Display" — that the text presets ask for. A font the
user drops in is still registered under its own name.
This package adds a second interface language to the editor. The upstream editor has no translation layer, so the whole feature is local to the client payload; the Node side, the MCP tools and the project format are untouched.
Three files are new:
miniapp/client/i18n.js— the runtime. It owns the locale, the browser-local preference (localStorage, keyfablecut-locale), the lookup (t()), the staticdata-i18npass, thehtml langattribute and the document title. A key that is not in a dictionary is returned untouched; a key that is in one is translated, which is why the editor never hands user text to the translator.miniapp/client/i18n-en.js— the English dictionary. Its values are the keys, so it is the fallback locale and the source of truth for the key set.miniapp/client/i18n-zh-CN.js— the Simplified Chinese dictionary.
The key is the English source phrase. Both dictionaries hold exactly the same
key set — 1799 keys — which tests/i18n.test.mjs checks. A plain key's English
value is that same phrase, so English is the fallback locale and the source of
truth for the key set. Labels that would collide across contexts are grouped
with a prefix (kf., fx., anim., delivery., fit., blend., preset.,
align., vAlign., direction., channel., denoise., matte., kind.,
shape., transition., textAnim.); a grouped key's English value is a
readable label (fit.contain → Contain) rather than the id, and a grouped
label is translated at the display site only, so the stored value never changes.
How the translation reaches the screen:
- The static markup carries
data-i18n,data-i18n-title,data-i18n-aria-labelanddata-i18n-placeholder.data-i18nwritestextContent, neverinnerHTML. - The parts the editor builds at runtime call
T()at the point where the text is produced: a toast, atextContent, atitle, anaria-label, a placeholder, a label in an HTML template, a menu item, a select option. - A built-in constant keeps its English source (
TITLE_STYLES,ASPECT_PRESETS, the audio-FX and colour and delivery tables, the template and animation-preset names). Only the text on screen is translated, at the display site. - Nothing rewrites the page: there is no
MutationObserverand no bulk text replacement, so user text is never touched by accident.
An option whose label is translated always carries an explicit value
attribute holding the raw id (<option value="${x}" …>${enumLabel("transition", x)}</option>). Without it the browser would use the translated text as the
value and the saved project would store the label instead of the id.
User text never reaches the translator. The editor writes it straight into the
DOM, escaped (escapeHtml(p.name), escapeHtml(c.name || ""),
escapeHtml(m.name), escapeHtml(comp.name)). The only places a .name is
translated are templateLabel() and animPresetLabel(), which first check
that the name belongs to one of the built-in templates or animation presets;
deliveryPresetLabel() and encodeProfileLabel() do the same for delivery
presets and encoding profiles. A template group the user typed is kept as it
is — only the built-in group names are translated.
What was deliberately left alone:
- Text the user typed — project, media and clip names, captions, project notes, template group names, template field values. It is written straight into the DOM.
- Technical values: track and clip ids, stored enum values, file names, codec, container and colour-space names, pixel formats, ffmpeg arguments.
- Developer and server error details (
[FableCut] …) stay in English. - The text that ends up in the picture: a caption or a title is drawn from the project, so an exported frame does not depend on the interface language.
The deliberately untranslated remainder is limited to technical, developer and
sample-content strings: codec, container and platform names ("Apple ProRes",
"YouTube Shorts"), CSS class selectors and pixel-format labels ("4:2:0 10-bit"),
developer and server error details, sample text inside the built-in templates
("Big news is here"), CSS selectors and style values, [FableCut] … log lines,
ffmpeg argument strings, the keyboard key name "Backspace" and the default names
"Untitled Project" and "Main".
The choice is made in Settings → Appearance → Language and applies
without a reload. repaintForLocale() redraws the surfaces the editor
builds at runtime (the bin, the inspector, the mixer, the colour page, the
project dashboard, the export queue and batch list, the track headers and the
timeline's transition/fade tooltips) and keeps the caret where it was. A redraw never schedules a save: scheduleSave() returns
early while localeRepainting is set, so a language switch never changes
the project.
The i18n scripts are loaded before every other client script, so T() is
defined while the other scripts evaluate their own labels. The export runner
copies the whole client directory, so the dictionaries travel with the job
and an exported frame is identical in both languages.
tests/i18n.test.mjs (37 tests, no dependency, no browser) checks the
dictionary parity, the English value rule, the runtime behaviour and the way
the client payload uses it. tests/i18n-browser.test.mjs (9 tests) drives a
real headless Chrome against a real Mini App instance and skips itself when
the machine has no Chromium-based browser.
The five font files whose names contain a space were renamed to their
hyphenated form (Abril Fatface.woff2 → Abril-Fatface.woff2, and so on):
a path inside a Mini App package may not contain a space. The files are
byte-identical; only the names changed.
Each bundled font has its full family-specific OFL text and embedded font
copyright recorded beside it. library/fonts/LICENSES.md maps the files,
sources and known version/header differences. Font binaries are unchanged.
The active-clip branch in miniapp/client/app.js seeks before starting
playback and keeps the clip silent while seeking. Audio clips now receive the
same lookahead pre-seek as video clips. This prevents a nonzero audio In point
from briefly playing the beginning of its source file. After seeking/loading,
chained elements restore unity element volume before playback; Web Audio owns
the clip gain. Chainless elements restore the requested clip gain. Regression
tests execute the actual sync function for both paths and all waiting states.
HTTPS imports validate resolved addresses and pin that validated result into the connection lookup rather than allowing a second DNS resolution. The URL hostname remains the HTTP Host and TLS identity. Each redirect is independently validated before any request is sent to its target. Deterministic transport tests exercise rebinding and private-address refusals without contacting real hosts.
Reference analysis resolves /media/ URLs through decoded-path and realpath
containment checks; explicit absolute source paths remain supported. Before an
external reference is copied into media/, its extension must identify a
supported audio or video format. All local media copies use an exclusive-create
loop, so concurrent imports cannot overwrite a same-named file. Served
/media/ and /library/ paths and MCP media lookups resolve symlinks and refuse
files whose real paths escape their managed roots. MCP retains at most 256
sessions. Once full, initialization returns 503 until a client deletes an unused
session or the runtime restarts. Existing sessions and active jobs are not timed
out or evicted. Provenance lists mp4-demux.js only in the client payload.
Test fixtures live under repository .fablecut-acceptance/ (or the explicit
FABLECUT_TEST_ROOT override), outside the installable package. Media-dependent
tests skip explicitly when FFmpeg is not on PATH, so a Node-and-Git-only CI
runner still exercises every non-media test. The private-redirect counter
listens on 127.0.0.1, while the URL still names 127.0.0.2: a correct
implementation never connects to that target, so macOS does not have to
configure the second loopback address. Verbatim font license whitespace and the
upstream analyze.js final blank line remain intact; these produce existing
git diff --cached --check diagnostics.
The editor-tab timeout message suggests checking/reopening the Mini App and
retrying with where: "tab"; it no longer recommends the unavailable headless
mode.
fablecut_analyze_reference accepts only video and audio extensions before it copies an explicit local path into media/. This prevents arbitrary files from being exposed through the editor's media route. Supported extensions are .mp4, .webm, .mov, .mkv, .m4v, .avi, .mp3, .wav, .ogg, .m4a, .aac and .flac. The regression test verifies that a .pem file is rejected and never copied.
Managed /media/ and /library/ URLs are resolved with realpath containment in both the HTTP server and MCP media reader; a symlink inside either managed root that targets outside it is refused. Local media copies use an exclusive-create loop so concurrent imports cannot overwrite a same-named file. Single-file imports and reference analysis also resolve symbolic links before checking the extension, so a media-looking alias cannot copy a non-media target into media/. The upstream paths.js requires FABLECUT_DATA_DIR to be set before loading, preventing its standalone fallbacks from writing into the package or a home-directory folder if the entry initialization order changes.
The package was renamed from fablecut to cutline and now presents the
product name Cutline. The rename is deliberately shallow: the public
tool surface keeps its upstream names, so a later upstream revision can still
be applied without renaming anything.
Changed in the vendored files:
-
miniapp/node/vendor/fablecut/mcp-server.js— the MCP server identity.serverInfo.nameanswerscutlineinstead offablecut. The version stays1.16.0: that is the upstream revision this server implements. -
miniapp/node/vendor/fablecut/mcp-server.js— the product name in the tool descriptions and in two error messages. "the FableCut project" reads as "the Cutline project", "FableCut's media library" as "Cutline's media library", "another FableCut data folder" as "another Cutline data folder", and the two messages that name the data folder and the project document follow. These strings are the only product references in the file that changed.
Deliberately unchanged:
- Every existing tool name. The 19 upstream tools and the project-folder
import tool keep their existing
fablecut_*names, in the definitions, the dispatch switch, the descriptions, the served manual and the tests. Renaming them would break every saved prompt, recipe andmcode-toolscall that uses the upstream names, and would make a later upstream update a rewrite rather than a re-vendor. The package adds one tool of its own,cutline_export, under its own name; it is appended to the list by a wrapper and the upstream dispatcher is untouched. - The module globals (
self.FableCutEdit,self.FableCutMotion, …), theFABLECUT_*environment variables, theX-FableCut-ClientandX-FableCut-Projectheaders, theSymbol.for("fablecut.localRequest")marker and thevendor/fablecut/directory itself. These are the upstream internals; they are not user-visible names. - The upstream agent manual (
vendor/fablecut/CLAUDE.md), apart from the scope note in item 5 below. It is upstream's own documentation, including its "FableCut" wording; the note only says which of its instructions do not apply in this package. - The upstream file banners and log prefixes (
/* FableCut … */,console.warn("[FableCut] …")) in the vendored sources.
In the package's own files:
-
miniapp/client/index.html— the visible brand. The document title isCutline - Video Editorand the top-bar logo readsCutline. The logo mark and the rest of the layout are untouched. -
miniapp/client/app.js— the user-visible status text. The two context-menu entries and the proxy entry that say "Needs the FableCut server" now say "Needs the Cutline server". -
miniapp/node/vendor/fablecut/CLAUDE.md— a scope note at the top. The manual is upstream's, and it describes upstream's standalone install:npm install fablecut,npx -y fablecut mcp,claude mcp add … fablecut, the Claude DesktopmcpServerssnippet,/plugin install fablecut@fablecut,node server.jsonhttp://localhost:7777and the data in~/FableCut. Thefablecut_docstool serves this file verbatim, so an Agent that reads it in this package would otherwise try to install and start a server that the Host already runs. A new first section, "Scope note — the Cutline Mini App (read this first)", states that nothing is to be installed or started, that there is nolocalhost:7777, that the data lives in the Host'scontext.dataDirand thatFABLECUT_DATA_DIRmust not be set by hand; it also states the tool count (21), that headless rendering is unavailable, and that a tool with apathreads any file the user can read. Short notes mark the places the upstream text still describes the old install: the "MCP connection" section, its Claude Desktop snippet, its "Where the files are" subsection and the "Run" section. Nothing else in the file changed.
Not vendored changes, but part of the same rename: the plugin directory, the
plugin id, the package name, the Skill directory and id, the MCP endpoint
(/mcp/cutline), the entry's log event names and the READMEs. The
references/media-generation.md guidance was removed from the package because
media generation is out of scope; it is not shipped with the plugin, and no
file in the package refers to it.
This follow-up keeps the tool surface at 21 entries while shortening the
standing MCP descriptions: nineteen upstream fablecut_* tools, the local
fablecut_import_project tool and the package's cutline_export tool. Tool
names, schema shape and dispatch behavior stay unchanged. Safety-critical
conditions remain in the descriptions, including explicit baseRevision
conflict handling,
editor-tab rendering, conditional IN/OUT export range, absolute-path frame
reads, remote-SVG rejection and profile combinations.
The package and root READMEs now show synthetic English and Simplified Chinese
editor/export previews, describe the standalone export path, and disclose the
absolute-path frame-read boundary and shared-project behavior. The new
tests/mcp-tool-payload.test.mjs checks the 16,000-byte serialized tool-list budget,
tool provenance/count, retained safety guidance and schema shape.