UAsset Reference MCP ships three command-line binaries and a browser UI from one npm package, plus a Unity Editor package that must not reach npm at all. In v0.4.0 a global install dropped from roughly 134 MB to about 40 MB, and the published tarball from 1.45 MB to 0.72 MB.

Nothing was removed from the tool. What changed is the answer to a question that had been answered by habit: which of these packages does the installed artifact actually need?

A Dependency Is Not a Runtime Dependency Because Your Code Imports It

The viewer is a React application. It imports three, @react-three/fiber, @tanstack/react-query, zustand, lucide-react, @fontsource/*, and more. All of them were declared in dependencies, which is what you do when your code imports something.

But the viewer is bundled by Vite at publish time. What lands in the published package is dist/web/public/, a set of already-built JavaScript chunks. A user installing the prebuilt tarball never resolves those imports, because the imports no longer exist in the shipped form — they were inlined at build time.

Declaring them as runtime dependencies meant every global install downloaded roughly 93 MB of packages that nothing on that machine could ever execute.

The distinction is worth being precise about, because moving a package from dependencies to devDependencies does not delete it. A contributor cloning the repository and running a plain npm install still downloads the whole tree — that number did not move. What changed is npm install --omit=dev, which is what a consumer of the published package actually gets: roughly 134 MB before, about 37-40 MB after. Those are two different installs, and only one of them is the user-facing story.

The correct test is not “does my source import it” but “does the installed artifact need it present.” For a bundled frontend, most of the frontend’s dependencies fail that test.

Finding the Real Answer by Reading the Built Output

The safe way to move a dependency to devDependencies is not to reason about it. It is to scan every non-relative import in the built output, outside the viewer bundle, and see what is left.

The built server imports exactly three packages:

"dependencies": {
  "@modelcontextprotocol/sdk": "^1.29.0",
  "better-sqlite3": "^12.11.1",
  "zod": "^4.4.3"
}

An MCP transport, a SQLite driver, and a schema validator. Everything else the project builds with is a build-time concern.

The verification that matters is that the viewer still works with those packages absent. It does: the 915 KB three.js chunk loads from the static bundle with three nowhere in node_modules, because that chunk is three.js, already bundled.

A production install now reports 0 vulnerabilities from npm audit as a side effect, since the advisories lived in packages that were never needed at runtime in the first place.

The Artifact That Did Not Belong Inside the Artifact

The server-less WASM viewer is a build of the same UI against sql.js, meant to be opened directly from the filesystem with no server running. It shipped inside the npm package.

That package installs three binaries, one of which starts a server. Anyone who installs it has the server. The WASM flavor was charging every install about 706 KB of WebAssembly that a server-backed install would never load, to serve a use case that install had already solved.

It is now a release-page download, asset-graph-viewer-static-<version>.zip. Nothing about the flavor itself changed, but this is a real removal for anyone who was using it from inside node_modules, which is why it is called out in the changelog rather than quietly dropped.

Fonts, Twice, in Six Alphabets

Fontsource’s weight-level CSS declares one @font-face per subset — cyrillic, cyrillic-ext, greek, vietnamese, latin, latin-ext — and each rule carries a WOFF2 source with a WOFF fallback.

The viewer shipped all of it: 54 font files, 704 KB. Half of those were .woff files that no browser capable of running this UI would ever request. Most of the rest were alphabets that Unity asset paths do not use.

Trimming to latin and latin-ext WOFF2 leaves 10 files and 180 KB.

The unicode-range Bug That a Green Build Hid

The obvious way to trim is to import Fontsource’s per-subset entrypoints — latin-400.css instead of 400.css. It looks correct. It produces exactly the right file count. It would have broken every glyph in the UI.

Those per-subset files omit unicode-range. Two @font-face rules with the same family and weight and no unicode-range do not merge into a complementary set — the later one simply wins. latin-ext contains only accented characters, so it would have shadowed latin, and every piece of basic ASCII in the interface would have silently fallen back to a system font.

The build would have been green. The file count would have been right. Only reading the emitted CSS catches it.

So the pruning happens in a Vite transform that filters the original weight-level rules, keeping their unicode-range intact:

const KEEP_SUBSET = /-(latin|latin-ext)-\d+-normal\./;
const WOFF_FALLBACK = /,\s*url\([^)]+\.woff\)\s*format\(\s*(['"])woff\1\s*\)/g;

const kept = faces.filter((face) => KEEP_SUBSET.test(face));
if (kept.length === 0) {
  // A fontsource layout change would otherwise silently ship no fonts.
  throw new Error(`trim-font-subsets: no latin @font-face left in ${id}`);
}

The throw is the important line. A future Fontsource restructure that leaves no matching face behind now fails the build instead of shipping a UI with no fonts, because the failure mode of this optimization is invisible in every automated check that is not looking at glyphs.

Two Package Managers, One Repository

The Unity Editor package is not an npm concern, and files in package.json is an allowlist:

"files": ["dist", "CHANGELOG.md", "scripts/install.ps1", "scripts/install.sh", "README.md", "LICENSE"]

unity/ and rust/ are excluded by omission, which is the property worth relying on. A denylist would have to be updated every time a new top-level directory appears; an allowlist fails closed.

The Unity package versions independently and sits at 0.2.0. It changes only when the Editor exporter changes, so the npm version moving to 0.4.0 does not drag it along.

It installs three ways, and they are not equivalent:

.tgz            Package Manager -> Add package from tarball   UPM-managed
.zip            unzip, then Add package from disk             UPM-managed
.unitypackage   Assets -> Import Package -> Custom Package    not UPM-managed

A .tgz requires a package/ root, because that is the npm tarball layout Unity’s Package Manager expects. A .unitypackage is a different thing entirely: it imports files into Assets/ and is not upgradable in place. Listing it as an equal third option would be misleading, so it is listed with what it does.

Building a .unitypackage Without Unity

All three formats are built in CI with no Unity Editor and no licence. That works because of one property: every asset in the package carries a committed .meta with a stable GUID, and the GUID is the only thing a .unitypackage needs that Unity would otherwise generate.

The format is a gzipped tar of one directory per asset:

<guid>/asset        the file itself
<guid>/asset.meta   its committed .meta
<guid>/pathname     where it lands, e.g. Assets/AssetReferenceMemory/Editor/Exporter.cs

The builder walks the package, reads each sibling .meta for its GUID, and writes those three files per entry. An entry with no GUID is a hard failure rather than a skip:

if (!guid) {
  // Without a GUID the entry cannot be represented and Unity would simply
  // not import it. Fail rather than ship a package missing files.
  throw new Error(`no .meta guid for ${relative(repoRoot, path)} — .unitypackage would silently drop it`);
}

Unity does not report a missing entry on import. It imports what it was given and says nothing about what it was not, so a silently incomplete package would be discovered by a user rather than by CI.

Ports That Pick Themselves

The last piece of packaging is what happens after installation. The viewer used to require a port, and a second instance died with an unhandled EADDRINUSE. It now binds the first free port starting at 7777, so several Unity projects can be served at once with no coordination between them.

Configuration that can be derived is configuration a user should not have to supply. The same reasoning gives the viewer project discovery from the working directory, and the whole install-to-running path is now three commands:

npm install -g unity-asset-reference-mcp
cd /path/to/UnityProject
unity-asset-reference-mcp-web

Smaller, Not Faster

None of this made the tool faster — that was a separate investigation with its own numbers. It made the tool smaller, and it is worth being precise about which is which.

The summary, measured:

Consumer install       ~134 MB  ->  ~37-40 MB   (npm install --omit=dev)
Published package      1.45 MB  ->  0.72 MB packed
                        2.9 MB  ->  2.1 MB unpacked
Fonts                 704 KB / 54 files  ->  180 KB / 10 files
npm audit                        0 vulnerabilities

Every one of those came from asking what the installed artifact needs, rather than what the repository builds with. Those are different questions, and a dependencies block is where the difference gets lost.