<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title></title>
    <description>Day dreamer, night coder, and a life long learner.</description>
    <link>https://vincewang.dev/</link>
    <atom:link href="https://vincewang.dev/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Mon, 24 Aug 2026 12:53:34 +0000</pubDate>
    <lastBuildDate>Mon, 24 Aug 2026 12:53:34 +0000</lastBuildDate>
    <generator>Jekyll v4.4.1</generator>
    
      <item>
        <title>UAsset Reference MCP: Shipping the Tool, Not the Toolchain</title>
        <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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?&lt;/p&gt;

&lt;h2 id=&quot;a-dependency-is-not-a-runtime-dependency-because-your-code-imports-it&quot;&gt;A Dependency Is Not a Runtime Dependency Because Your Code Imports It&lt;/h2&gt;

&lt;p&gt;The viewer is a React application. It imports &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;three&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@react-three/fiber&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@tanstack/react-query&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;zustand&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lucide-react&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@fontsource/*&lt;/code&gt;, and more. All of them were declared in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dependencies&lt;/code&gt;, which is what you do when your code imports something.&lt;/p&gt;

&lt;p&gt;But the viewer is bundled by Vite at publish time. What lands in the published package is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dist/web/public/&lt;/code&gt;, 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.&lt;/p&gt;

&lt;p&gt;Declaring them as runtime dependencies meant every global install downloaded roughly 93 MB of packages that nothing on that machine could ever execute.&lt;/p&gt;

&lt;p&gt;The distinction is worth being precise about, because moving a package from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dependencies&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;devDependencies&lt;/code&gt; does not delete it. A contributor cloning the repository and running a plain &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm install&lt;/code&gt; still downloads the whole tree — that number did not move. What changed is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm install --omit=dev&lt;/code&gt;, 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2 id=&quot;finding-the-real-answer-by-reading-the-built-output&quot;&gt;Finding the Real Answer by Reading the Built Output&lt;/h2&gt;

&lt;p&gt;The safe way to move a dependency to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;devDependencies&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;The built server imports exactly three packages:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nl&quot;&gt;&quot;dependencies&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;@modelcontextprotocol/sdk&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;^1.29.0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;better-sqlite3&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;^12.11.1&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;zod&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;^4.4.3&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;An MCP transport, a SQLite driver, and a schema validator. Everything else the project builds with is a build-time concern.&lt;/p&gt;

&lt;p&gt;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 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;three&lt;/code&gt; nowhere in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node_modules&lt;/code&gt;, because that chunk &lt;em&gt;is&lt;/em&gt; three.js, already bundled.&lt;/p&gt;

&lt;p&gt;A production install now reports 0 vulnerabilities from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm audit&lt;/code&gt; as a side effect, since the advisories lived in packages that were never needed at runtime in the first place.&lt;/p&gt;

&lt;h2 id=&quot;the-artifact-that-did-not-belong-inside-the-artifact&quot;&gt;The Artifact That Did Not Belong Inside the Artifact&lt;/h2&gt;

&lt;p&gt;The server-less WASM viewer is a build of the same UI against &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sql.js&lt;/code&gt;, meant to be opened directly from the filesystem with no server running. It shipped inside the npm package.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;It is now a release-page download, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;asset-graph-viewer-static-&amp;lt;version&amp;gt;.zip&lt;/code&gt;. Nothing about the flavor itself changed, but this is a real removal for anyone who was using it from inside &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node_modules&lt;/code&gt;, which is why it is called out in the changelog rather than quietly dropped.&lt;/p&gt;

&lt;h2 id=&quot;fonts-twice-in-six-alphabets&quot;&gt;Fonts, Twice, in Six Alphabets&lt;/h2&gt;

&lt;p&gt;Fontsource’s weight-level CSS declares one &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@font-face&lt;/code&gt; per subset — cyrillic, cyrillic-ext, greek, vietnamese, latin, latin-ext — and each rule carries a WOFF2 source with a WOFF fallback.&lt;/p&gt;

&lt;p&gt;The viewer shipped all of it: 54 font files, 704 KB. Half of those were &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.woff&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;Trimming to latin and latin-ext WOFF2 leaves 10 files and 180 KB.&lt;/p&gt;

&lt;h2 id=&quot;the-unicode-range-bug-that-a-green-build-hid&quot;&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unicode-range&lt;/code&gt; Bug That a Green Build Hid&lt;/h2&gt;

&lt;p&gt;The obvious way to trim is to import Fontsource’s per-subset entrypoints — &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;latin-400.css&lt;/code&gt; instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;400.css&lt;/code&gt;. It looks correct. It produces exactly the right file count. It would have broken every glyph in the UI.&lt;/p&gt;

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

&lt;p&gt;The build would have been green. The file count would have been right. Only reading the emitted CSS catches it.&lt;/p&gt;

&lt;p&gt;So the pruning happens in a Vite transform that filters the original weight-level rules, keeping their &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unicode-range&lt;/code&gt; intact:&lt;/p&gt;

&lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;KEEP_SUBSET&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sr&quot;&gt;/-&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;latin|latin-ext&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\d&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;+-normal&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\.&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;WOFF_FALLBACK&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sr&quot;&gt;/,&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\s&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;*url&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\([^&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\.&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;woff&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\)\s&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;*format&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\(\s&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;&apos;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;])&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;woff&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\1\s&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\)&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;kept&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;faces&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;face&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;KEEP_SUBSET&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;face&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;kept&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// A fontsource layout change would otherwise silently ship no fonts.&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`trim-font-subsets: no latin @font-face left in &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2 id=&quot;two-package-managers-one-repository&quot;&gt;Two Package Managers, One Repository&lt;/h2&gt;

&lt;p&gt;The Unity Editor package is not an npm concern, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;files&lt;/code&gt; in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package.json&lt;/code&gt; is an allowlist:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nl&quot;&gt;&quot;files&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;dist&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;CHANGELOG.md&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;scripts/install.ps1&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;scripts/install.sh&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;README.md&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;LICENSE&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unity/&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rust/&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;It installs three ways, and they are not equivalent:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;.tgz            Package Manager -&amp;gt; Add package from tarball   UPM-managed
.zip            unzip, then Add package from disk             UPM-managed
.unitypackage   Assets -&amp;gt; Import Package -&amp;gt; Custom Package    not UPM-managed
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;A &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.tgz&lt;/code&gt; requires a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package/&lt;/code&gt; root, because that is the npm tarball layout Unity’s Package Manager expects. A &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.unitypackage&lt;/code&gt; is a different thing entirely: it imports files into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Assets/&lt;/code&gt; and is not upgradable in place. Listing it as an equal third option would be misleading, so it is listed with what it does.&lt;/p&gt;

&lt;h2 id=&quot;building-a-unitypackage-without-unity&quot;&gt;Building a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.unitypackage&lt;/code&gt; Without Unity&lt;/h2&gt;

&lt;p&gt;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 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.meta&lt;/code&gt; with a stable GUID, and the GUID is the only thing a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.unitypackage&lt;/code&gt; needs that Unity would otherwise generate.&lt;/p&gt;

&lt;p&gt;The format is a gzipped tar of one directory per asset:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;lt;guid&amp;gt;/asset        the file itself
&amp;lt;guid&amp;gt;/asset.meta   its committed .meta
&amp;lt;guid&amp;gt;/pathname     where it lands, e.g. Assets/AssetReferenceMemory/Editor/Exporter.cs
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The builder walks the package, reads each sibling &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.meta&lt;/code&gt; for its GUID, and writes those three files per entry. An entry with no GUID is a hard failure rather than a skip:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;if &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;guid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// Without a GUID the entry cannot be represented and Unity would simply&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// not import it. Fail rather than ship a package missing files.&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`no .meta guid for &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;relative&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;repoRoot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; — .unitypackage would silently drop it`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2 id=&quot;ports-that-pick-themselves&quot;&gt;Ports That Pick Themselves&lt;/h2&gt;

&lt;p&gt;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 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EADDRINUSE&lt;/code&gt;. It now binds the first free port starting at 7777, so several Unity projects can be served at once with no coordination between them.&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;npm &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-g&lt;/span&gt; unity-asset-reference-mcp
&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; /path/to/UnityProject
unity-asset-reference-mcp-web
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;smaller-not-faster&quot;&gt;Smaller, Not Faster&lt;/h2&gt;

&lt;p&gt;None of this made the tool faster — that was &lt;a href=&quot;/2026/uasset-reference-mcp-measuring-before-rewriting/&quot;&gt;a separate investigation&lt;/a&gt; with its own numbers. It made the tool smaller, and it is worth being precise about which is which.&lt;/p&gt;

&lt;p&gt;The summary, measured:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Consumer install       ~134 MB  -&amp;gt;  ~37-40 MB   (npm install --omit=dev)
Published package      1.45 MB  -&amp;gt;  0.72 MB packed
                        2.9 MB  -&amp;gt;  2.1 MB unpacked
Fonts                 704 KB / 54 files  -&amp;gt;  180 KB / 10 files
npm audit                        0 vulnerabilities
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;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 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dependencies&lt;/code&gt; block is where the difference gets lost.&lt;/p&gt;
</description>
        <pubDate>Mon, 24 Aug 2026 04:00:00 +0000</pubDate>
        <link>https://vincewang.dev/2026/uasset-reference-mcp-shipping-the-tool-not-the-toolchain/</link>
        <guid isPermaLink="true">https://vincewang.dev/2026/uasset-reference-mcp-shipping-the-tool-not-the-toolchain/</guid>
        
        
        <category>tools</category>
        
      </item>
    
      <item>
        <title>UAsset Reference MCP: The Bottleneck Was Never the Language</title>
        <description>&lt;p&gt;Indexing a Unity project means parsing YAML looking for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{fileID, guid, type}&lt;/code&gt; records. It is the most obviously compute-heavy step in the pipeline, which made it the obvious candidate for a native rewrite. So a Rust extractor was built, it produced byte-identical output, and it was benchmarked properly.&lt;/p&gt;

&lt;p&gt;It parsed about 2.9x faster and delivered results about 38x slower. Then measuring the real project moved the bottleneck somewhere neither implementation touched.&lt;/p&gt;

&lt;h2 id=&quot;the-obvious-optimization-was-obvious-for-the-wrong-reason&quot;&gt;The Obvious Optimization Was Obvious for the Wrong Reason&lt;/h2&gt;

&lt;p&gt;The proof of concept lives in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rust/uasset-ref-extractor&lt;/code&gt;, parses with Rayon, and is invoked as a subprocess over NDJSON by a benchmark script. It was never wired into product code.&lt;/p&gt;

&lt;p&gt;Against the same 2,000 prefab records, with every mode producing 2,000 outputs, 1,998 edges, zero unresolved references, and exact output parity:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Mode&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;End-to-end median&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Extractor-only median&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Node, 8 workers&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;3.146 ms&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;3.146 ms&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Rust, 8 threads&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;119.852 ms&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;1.074 ms&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The parse itself went from 3.146 ms to 1.074 ms. Delivering that parse cost roughly 118 ms of process startup plus NDJSON serialization, which the in-process Node path never pays.&lt;/p&gt;

&lt;p&gt;That is not a result about Rust. Rust parsed faster, exactly as expected. It is a result about a subprocess being the wrong delivery shape for work measured in single-digit milliseconds. An in-process binding would erase the overhead entirely — which turns the interesting question into a different one. If the parse were free, how much would indexing actually save?&lt;/p&gt;

&lt;h2 id=&quot;the-fixture-that-said-3-and-the-fixture-that-said-76&quot;&gt;The Fixture That Said 3%, and the Fixture That Said 76%&lt;/h2&gt;

&lt;p&gt;This is the part worth telling straight, because both attempts to answer that question were wrong.&lt;/p&gt;

&lt;p&gt;The repository’s own benchmark fixture generated 2,000 Unity-shaped prefab assets of about 100 bytes each, carrying one reference apiece. Measured against it at concurrency 8, total indexing was roughly 110 ms and the extraction phase roughly 40 ms, most of it file reads — the parse itself was around 3 ms. Removing parsing entirely would have saved under 3% of runtime.&lt;/p&gt;

&lt;p&gt;That number was used to shelve the Rust port. It was also measuring the wrong thing: 100-byte single-reference files measure per-file overhead, not parse throughput. The fixture answered a question about &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;open&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;read&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;close&lt;/code&gt;, and the question was about parsing.&lt;/p&gt;

&lt;p&gt;So a denser fixture was built for the write-path benchmark, generating about 139 references per asset. Against that corpus, SQLite writes looked like roughly 76% of the work — a completely different conclusion, pointing at a completely different phase.&lt;/p&gt;

&lt;p&gt;The real project has &lt;strong&gt;0.89 references per asset&lt;/strong&gt;. The second fixture was around 150x too dense, wrong in the opposite direction from the first.&lt;/p&gt;

&lt;p&gt;Both synthetic corpora encoded an assumption about the shape of a Unity project, and both assumptions were wrong. Neither was a rhetorical setup; they were two real mistakes made in sequence, and the only thing that settled the question was measuring a project that actually exists.&lt;/p&gt;

&lt;h2 id=&quot;what-the-real-project-showed&quot;&gt;What the Real Project Showed&lt;/h2&gt;

&lt;p&gt;The profile was captured against a real Unity project of 32,579 indexed assets and 28,842 edges, on macOS with 8 logical CPUs. The project’s own &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.asset-memory/index.db&lt;/code&gt; was never written to; every run used a scratch database.&lt;/p&gt;

&lt;p&gt;At concurrency 8, medians over four runs with about 4% total spread:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Phase&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Median&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Share&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;scan&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;2963-3082 ms&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;~60%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;extract (read + parse)&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;1631-1733 ms&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;~33%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;write&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;267-338 ms&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;~6%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;total&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;4964-5164 ms&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt; &lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Reference parsing alone, measured across the project’s 1,827 YAML assets totalling 128.8 MB, takes 485 ms serial — about 266 MB/s. Against a ~5,000 ms total, that is roughly 2% once parallelized.&lt;/p&gt;

&lt;p&gt;The Node parser is not fast in absolute terms. It is simply not the bottleneck, and a 2.9x faster parser applied to 2% of runtime is a rounding error with a cross-compilation matrix attached.&lt;/p&gt;

&lt;h2 id=&quot;most-of-the-dominant-phase-was-not-filesystem-work&quot;&gt;Most of the Dominant Phase Was Not Filesystem Work&lt;/h2&gt;

&lt;p&gt;Scan was 60%, so scan is where the time was. Decomposing it is where the actual finding is.&lt;/p&gt;

&lt;p&gt;Pure directory traversal across the project measured around 160 ms. Reading every &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.meta&lt;/code&gt; file measured around 400 ms. That is roughly 560 ms of real filesystem work inside a phase that reports 1,600-3,000 ms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Around 65-80% of the dominant phase was not filesystem I/O at all.&lt;/strong&gt; It was promise-scheduling overhead — the cost of concurrency pools nested per directory, one pool spawned per level of recursion, each one coordinating work that was mostly waiting anyway.&lt;/p&gt;

&lt;p&gt;The bottleneck was coordination structure, in a language whose scheduler you do not rewrite by switching languages.&lt;/p&gt;

&lt;h2 id=&quot;what-actually-got-faster&quot;&gt;What Actually Got Faster&lt;/h2&gt;

&lt;p&gt;v0.4.0 ships bounded concurrency for scanning and reference extraction, with SQLite writes still transactional and serialized. On the repository fixture at concurrency 8 that measured about 2.7x total speedup with identical output.&lt;/p&gt;

&lt;p&gt;Correctness was the gate, not the speedup. A full index at concurrency 1 and at concurrency 8 produces byte-identical &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assets&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;edges&lt;/code&gt; tables, verified by hashing the fully sorted tables rather than by comparing row counts.&lt;/p&gt;

&lt;p&gt;The concurrency cap itself turned out to be the largest remaining measured win, and the reason is the same finding restated:&lt;/p&gt;

&lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;DEFAULT_INDEX_CONCURRENCY&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Math&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Math&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;availableParallelism&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That is a CPU-count heuristic applied to a workload that is roughly 93% filesystem I/O. Workers spend their time waiting on syscalls, not computing, so throughput keeps improving well past the core count. On the real project, with edge counts identical across every run:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Concurrency&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Median total&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;vs 8&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;CV&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;8 (current default cap)&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;4203 ms&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;1.00x&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;4.3%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;16&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;3841 ms&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;1.09x&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;2.7%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;32&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;3558 ms&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;1.18x&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;8.5%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;48&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;3359 ms&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;1.25x&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;9.3%&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The default cap did not change in this release. The gains above 16 carry 8-9% coefficient of variation, close to their own effect size, and every number here comes from one macOS machine. A spinning disk, a network share, or a virus scanner walking the same directories could reorder the phases entirely. So &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--concurrency &amp;lt;n&amp;gt;&lt;/code&gt; ships as a flag to tune per machine, and the default stays where it can be defended.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UV_THREADPOOL_SIZE&lt;/code&gt; was checked and is not the lever: raising it from 4 to 32 moved scan about 8% and total about 5%, near the noise floor.&lt;/p&gt;

&lt;h2 id=&quot;the-write-path-was-measured-too-and-mostly-left-alone&quot;&gt;The Write Path Was Measured Too, and Mostly Left Alone&lt;/h2&gt;

&lt;p&gt;The same benchmark ran nine variants of the write configuration against a dense corpus, interleaved across rounds with a discarded warm-up so drift hit every variant equally, and asserting row-count parity so a variant that lost rows could not read as a win.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WITHOUT ROWID&lt;/code&gt; on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;edges&lt;/code&gt; measured consistently slower at 0.92x — the key is four wide TEXT columns, so the b-tree rows are large. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;synchronous = NORMAL&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;foreign_keys = OFF&lt;/code&gt;, and a larger &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cache_size&lt;/code&gt; all landed within noise, and the first two buy nothing measurable in exchange for durability and integrity.&lt;/p&gt;

&lt;p&gt;One finding holds independently of any timing: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;idx_edges_from&lt;/code&gt; is redundant. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;edges&lt;/code&gt; is keyed &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PRIMARY KEY (from_guid, to_guid, ref_kind, context)&lt;/code&gt;, so &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;from_guid&lt;/code&gt; is already the leftmost column of the primary key index, and the extra index costs write time and disk for no read benefit.&lt;/p&gt;

&lt;p&gt;None of those write-path changes shipped in v0.4.0. They are measured candidates with their own validation still ahead, and the write phase is ~6% of runtime, so the ceiling on all of them together is small. Saying that plainly is cheaper than shipping a change whose benefit cannot be distinguished from noise.&lt;/p&gt;

&lt;h2 id=&quot;the-decision-is-written-down-so-the-argument-happens-once&quot;&gt;The Decision Is Written Down So the Argument Happens Once&lt;/h2&gt;

&lt;p&gt;Keeping extraction in TypeScript is recorded as &lt;a href=&quot;https://github.com/JVinceW/uasset-reference-memory-mcp/blob/main/docs/decisions/0020-node-reference-extraction-over-rust.md&quot;&gt;ADR 0020&lt;/a&gt;, with two conditions for revisiting, in order:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;A phase breakdown from a real Unity project — not a synthetic fixture — showing that parsing is a materially larger share than measured here.&lt;/li&gt;
  &lt;li&gt;An in-process binding rather than a subprocess. The ~118 ms overhead is structural and cannot be tuned away.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If both ever hold, native extraction ships as prebuilt per-platform binaries selected at install time, never as a source build that requires a toolchain on a user’s machine.&lt;/p&gt;

&lt;p&gt;The Rust crate was kept rather than deleted, on an unmerged branch. It is not built, not published, and not on any code path a user reaches — but the measurements in this post are only reproducible because it still exists, and a decision defended by numbers should stay re-runnable.&lt;/p&gt;

&lt;p&gt;The lesson is not that Rust is slow, because it was not. It is that “the parser is the bottleneck” was a hypothesis that felt too obvious to test, and the two fixtures built to test it were each shaped by an assumption about Unity projects that the actual Unity project did not share.&lt;/p&gt;
</description>
        <pubDate>Mon, 24 Aug 2026 03:00:00 +0000</pubDate>
        <link>https://vincewang.dev/2026/uasset-reference-mcp-measuring-before-rewriting/</link>
        <guid isPermaLink="true">https://vincewang.dev/2026/uasset-reference-mcp-measuring-before-rewriting/</guid>
        
        
        <category>tools</category>
        
      </item>
    
      <item>
        <title>UAsset Reference MCP: Looking at the Graph Instead of Querying It</title>
        <description>&lt;p&gt;UAsset Reference MCP v0.4.0 adds an interactive viewer for the asset graph. It renders the dependency graph in three modes, points at what already looks wrong, and starts with no arguments from anywhere inside a Unity project.&lt;/p&gt;

&lt;p&gt;The first three releases made the graph queryable. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;find_references&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;get_dependencies&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;trace_path&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;find_unused_assets&lt;/code&gt; all answer a question precisely, and all of them require the same thing first: a name. On a project with 32,579 indexed assets, naming the right asset is the hard part. Answering “what references &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BattleFloor.mat&lt;/code&gt;” is cheap once you already suspect &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BattleFloor.mat&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;a-query-answers-a-question-you-already-have&quot;&gt;A Query Answers a Question You Already Have&lt;/h2&gt;

&lt;p&gt;The graph in that project holds 32,579 assets and 28,842 edges. There is no useful way to read that as a list. Sorting by reference count gives you the top of the list and nothing about shape. Sorting by path gives you the Project window, which is the view you already had when you got stuck.&lt;/p&gt;

&lt;p&gt;What a developer usually wants before a refactor is not one answer but a direction: which corner of the project is dense, which assets sit at the center of everything, which references are already broken, which folders nothing points at. Those are shape questions, and the reason they are hard to ask through a query API is that you cannot name the answer in advance.&lt;/p&gt;

&lt;p&gt;So v0.4.0 stops requiring the name. The viewer draws the graph, and the first thing it draws is a short list of things that already deserve attention.&lt;/p&gt;

&lt;h2 id=&quot;the-attention-panel-answers-the-unasked-question&quot;&gt;The Attention Panel Answers the Unasked Question&lt;/h2&gt;

&lt;p&gt;The panel is deliberately narrow. It reports three categories, each one already computable from the index:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Broken references      edges whose target GUID resolves to nothing
Unused candidates      assets with no incoming serialized edge
Most referenced        dependency hubs, ranked by incoming edge count
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Broken references are the least ambiguous. If an asset serialized a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{fileID, guid, type}&lt;/code&gt; record and no asset in the graph owns that GUID, something was deleted or moved outside Unity, and the index can say so without judgment.&lt;/p&gt;

&lt;p&gt;Unused candidates are a review signal, not a delete list, for the same reason described in the &lt;a href=&quot;/2026/uasset-reference-mcp-addressables-discovery/&quot;&gt;Addressables post&lt;/a&gt;: an asset with no incoming edge can still be loaded by an Addressables address or a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Resources.Load&lt;/code&gt; string. The panel sorts those candidates by size, because the ones worth reviewing first are the ones costing the most.&lt;/p&gt;

&lt;p&gt;Most-referenced assets are the opposite signal. They are the assets where a change is expensive, which makes them the assets worth looking at before planning one.&lt;/p&gt;

&lt;p&gt;Clicking any row selects that asset in the graph, so the panel is an entry point into the picture rather than a separate report.&lt;/p&gt;

&lt;h2 id=&quot;three-renderings-one-graph&quot;&gt;Three Renderings, One Graph&lt;/h2&gt;

&lt;p&gt;The viewer draws the same graph three ways:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;2D    force-directed, inline SVG        cluster and density
DAG   layered hierarchy, inline SVG     direction of dependency
3D    three.js, loaded on demand        structure of a large neighborhood
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;2D and DAG are plain inline SVG, so they cost nothing beyond the page. 3D pulls in three.js, and it does so only when the mode is selected — the module is not on the initial load path.&lt;/p&gt;

&lt;p&gt;The important part is what the modes do not mean. They are three ways to draw one graph, not three levels of detail. Switching to 3D does not reveal edges that 2D was hiding. DAG is useful when the question is “what flows into what” and 2D is useful when the question is “what is clumped together,” and both are reading the same rows from the same SQLite index.&lt;/p&gt;

&lt;h2 id=&quot;the-node-budget-bounds-the-drawing-not-the-index&quot;&gt;The Node Budget Bounds the Drawing, Not the Index&lt;/h2&gt;

&lt;p&gt;A 32,000-node force layout is a hairball, and a hairball is not a visualization. The viewer keeps large projects readable with two controls.&lt;/p&gt;

&lt;p&gt;Filters restrict the graph by asset type and by origin, where origin is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;project&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package&lt;/code&gt;, or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;builtin&lt;/code&gt; — the same distinction the indexer records when it walks project assets, embedded and external UPM packages, and Unity’s built-in resources.&lt;/p&gt;

&lt;p&gt;The node budget caps how many nodes are requested at all. It defaults to 320 and is clamped between 50 and 5,000:&lt;/p&gt;

&lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;setNodeBudget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;nodeBudget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nf&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;nodeBudget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Number&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;isFinite&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;nodeBudget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Math&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;50&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Math&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Math&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;trunc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;nodeBudget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)))&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;320&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The budget is a rendering bound, not an indexing bound. Nothing is dropped from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.asset-memory/index.db&lt;/code&gt;, and no query result is silently truncated on the server’s behalf — the viewer asks for a bounded slice because a bounded slice is the only kind a person can read.&lt;/p&gt;

&lt;h2 id=&quot;starting-it-takes-no-path-no-port-and-no-config&quot;&gt;Starting It Takes No Path, No Port, and No Config&lt;/h2&gt;

&lt;p&gt;The viewer used to want a database path and a port. Both are now inferred.&lt;/p&gt;

&lt;p&gt;Project discovery walks up from the working directory the way &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git&lt;/code&gt; finds a repository. A directory counts as a project scope if it already holds an index, or if it looks like a Unity project root:&lt;/p&gt;

&lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;isProjectRoot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dir&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;exists&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;existsSync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;boolean&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;exists&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dir&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.asset-memory&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)))&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;exists&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dir&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Assets&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;exists&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dir&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;ProjectSettings&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.asset-memory&lt;/code&gt; is checked first, so a project that has already been indexed is recognized even when its layout is unusual.&lt;/p&gt;

&lt;p&gt;Port selection starts at 7777 and advances to the first free port. Previously a second viewer died on an unhandled &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EADDRINUSE&lt;/code&gt;; now it binds the next port and prints where it landed. If &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--port&lt;/code&gt; was passed explicitly and that port was taken, it says so rather than pretending.&lt;/p&gt;

&lt;p&gt;The result is that the whole workflow is two commands from inside the project:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;unity-asset-reference-mcp-index index &lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt;
unity-asset-reference-mcp-web
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;asset graph viewer → http://localhost:7777  (db: /path/to/UnityProject/.asset-memory/index.db)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--project &amp;lt;root&amp;gt;&lt;/code&gt; still works, and now matches the argument the MCP server and indexer already accept, so none of the three binaries asks the user to spell out &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.asset-memory/index.db&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;five-routes-and-no-new-mcp-tools&quot;&gt;Five Routes, and No New MCP Tools&lt;/h2&gt;

&lt;p&gt;The viewer is a client of the same HTTP API the tool already served. v0.4.0 adds five routes to support it:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;/api/asset-detail
/api/broken-references
/api/graph
/api/index-status
/api/root-trace
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The MCP tool count is unchanged at 15. Nothing about the agent-facing surface moved to make room for a UI, because the UI is reading the same index through the same query layer.&lt;/p&gt;

&lt;h2 id=&quot;what-it-does-not-do&quot;&gt;What It Does Not Do&lt;/h2&gt;

&lt;p&gt;The viewer is read-only. It does not edit, move, refactor, or delete anything in the Unity project, and it never writes to the index. It reports what the graph contains and leaves every production decision in the Editor where it belongs.&lt;/p&gt;

&lt;p&gt;Two changes are worth knowing before upgrading. The new viewer is served at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/&lt;/code&gt;, and the previous Cytoscape viewer is still bundled at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/legacy.html&lt;/code&gt;. The server-less WASM viewer left the npm package: it exists to be opened directly from the filesystem, so it was charging every install roughly 706 KB of WASM that a server-backed install would never load. It is now a release-page download, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;asset-graph-viewer-static-&amp;lt;version&amp;gt;.zip&lt;/code&gt;, and nothing about that flavor itself changed.&lt;/p&gt;

&lt;p&gt;Upgrading from 0.3.x needs nothing else. The index schema is still version 3, so an existing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.asset-memory/index.db&lt;/code&gt; is read as-is.&lt;/p&gt;

&lt;p&gt;Three releases of query tools were the right order to build this in. The graph had to be trustworthy before it was worth looking at. But a trustworthy graph you can only address by name is still a graph you have to already understand, and drawing it is the cheapest way to stop needing to.&lt;/p&gt;
</description>
        <pubDate>Mon, 24 Aug 2026 02:00:00 +0000</pubDate>
        <link>https://vincewang.dev/2026/uasset-reference-mcp-graph-viewer/</link>
        <guid isPermaLink="true">https://vincewang.dev/2026/uasset-reference-mcp-graph-viewer/</guid>
        
        
        <category>tools</category>
        
      </item>
    
      <item>
        <title>UAsset Reference MCP: Indexing External Unity Packages</title>
        <description>&lt;p&gt;UAsset Reference MCP v0.3.2 fixes a gap that appears as soon as a Unity project becomes modular. A game can depend on a local Unity Package Manager package through &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Packages/manifest.json&lt;/code&gt;, while that package’s source folder lives outside the Unity project directory. Unity imports it, resolves its GUIDs, and lets project assets reference it. A project-root-only asset scan can still miss it.&lt;/p&gt;

&lt;p&gt;The change in v0.3.2 is automatic external local UPM package indexing. The indexer now discovers active local directory packages from Unity package metadata, reads their files from their physical source folders, and stores graph nodes under canonical Unity paths such as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Packages/com.company.gameplay/Runtime/EnemyConfig.asset&lt;/code&gt;. The SQLite graph stays portable, and traversal still works through GUID edges rather than through filesystem assumptions.&lt;/p&gt;

&lt;h2 id=&quot;the-missing-root-is-not-optional&quot;&gt;The Missing Root Is Not Optional&lt;/h2&gt;

&lt;p&gt;Unity projects often split shared gameplay, UI, networking, or tools code into UPM packages. During development, those packages are commonly referenced as local directories so the package can be edited alongside the game.&lt;/p&gt;

&lt;p&gt;The Unity manifest might look like this:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;dependencies&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;com.company.gameplay&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;file:../../modules/com.company.gameplay&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;From Unity’s point of view, assets inside that module are part of the active project package set. A prefab in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Assets/Characters/Enemy.prefab&lt;/code&gt; can serialize a reference to a ScriptableObject inside &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;com.company.gameplay&lt;/code&gt;, and Unity resolves the reference through the target asset’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.meta&lt;/code&gt; GUID.&lt;/p&gt;

&lt;p&gt;A scanner that only walks the project root has a different view. It can see &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Assets/&lt;/code&gt;, embedded packages under &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Packages/&lt;/code&gt;, and cached registry packages under &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Library/PackageCache/&lt;/code&gt;. It cannot see &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;../../modules/com.company.gameplay&lt;/code&gt; unless it understands Unity’s package metadata. The result is an unresolved GUID even though the Unity Editor can resolve the same asset.&lt;/p&gt;

&lt;p&gt;v0.3.2 makes the scan scope match the active Unity package graph more closely:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Unity project root
  Assets/                         scanned as project assets
  Packages/com.embedded.tool/      scanned as embedded package assets
  Library/PackageCache/...         scanned as cached package assets

External local package
  ../../modules/com.company.gameplay/
                                  scanned as package assets
                                  stored as Packages/com.company.gameplay/...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The indexer is still static and offline. It does not launch Unity or ask the Editor for a package list. It reads the metadata Unity already writes, then builds the graph from files on disk.&lt;/p&gt;

&lt;h2 id=&quot;physical-sources-become-unity-paths&quot;&gt;Physical Sources Become Unity Paths&lt;/h2&gt;

&lt;p&gt;The central model is physical root versus virtual path. The physical root is where the indexer reads bytes. The virtual path is the Unity path stored in the graph.&lt;/p&gt;

&lt;p&gt;For an external package named &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;com.company.gameplay&lt;/code&gt;, the physical source might be:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;/Users/vincewang/modules/com.company.gameplay/Runtime/EnemyConfig.asset
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The graph stores it as:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Packages/com.company.gameplay/Runtime/EnemyConfig.asset
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That stored path is the important part. Absolute machine paths are local development details. If the index persisted &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/Users/...&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;../../modules/...&lt;/code&gt;, a shared snapshot would describe one developer’s folder layout instead of the Unity project model. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Packages/&amp;lt;package-name&amp;gt;/...&lt;/code&gt; is the path Unity users expect when talking about package assets, so it is the path the database records.&lt;/p&gt;

&lt;p&gt;The node still carries package identity:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;path:       Packages/com.company.gameplay/Runtime/EnemyConfig.asset
origin:     package
package_id: com.company.gameplay
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Reference extraction does not need a separate package traversal mode. Unity serialized references point to GUIDs, and the index resolves GUIDs across all active roots. A project asset can reference a package asset. A package asset can reference a project asset. Two packages can reference each other. Once the GUID map includes every active root, the edge model remains the same.&lt;/p&gt;

&lt;h2 id=&quot;discovery-follows-unity-metadata&quot;&gt;Discovery Follows Unity Metadata&lt;/h2&gt;

&lt;p&gt;The scanner builds a package-source plan before walking files. It reads &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Packages/manifest.json&lt;/code&gt; for direct dependencies and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Packages/packages-lock.json&lt;/code&gt; when Unity has written resolved package metadata.&lt;/p&gt;

&lt;p&gt;The manifest is the authority for direct dependency declarations. A direct local dependency using a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;file:&lt;/code&gt; directory can be discovered from the manifest alone. Relative &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;file:&lt;/code&gt; paths resolve from the project’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Packages/&lt;/code&gt; directory, matching how Unity interprets those paths, not from whichever directory happened to launch the CLI.&lt;/p&gt;

&lt;p&gt;The lockfile can add resolved local-package information, including local transitive packages. That matters when a project pulls in a package that itself depends on another local package. The indexer can include the package Unity resolved without asking the user to configure extra scan roots.&lt;/p&gt;

&lt;p&gt;Discovery is intentionally narrow. The indexer follows package paths explicitly declared or resolved by Unity project metadata. It does not crawl parent folders, scan sibling repositories, expand arbitrary workspace directories, read environment variables, download missing packages, or treat local &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.tgz&lt;/code&gt; files and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;file://&lt;/code&gt; Git URLs as external directories. Those sources remain Unity package-resolution concerns, and cached package content continues to be handled through &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Library/PackageCache/&lt;/code&gt; when present.&lt;/p&gt;

&lt;p&gt;Each external package candidate has to be an accessible directory with a parseable &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package.json&lt;/code&gt;. The package manifest’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name&lt;/code&gt; must match the dependency name declared by the project. A mismatch is skipped with a package-discovery warning instead of being indexed under the wrong Unity path.&lt;/p&gt;

&lt;h2 id=&quot;precedence-prevents-duplicate-package-views&quot;&gt;Precedence Prevents Duplicate Package Views&lt;/h2&gt;

&lt;p&gt;One package name must map to one active source. Otherwise the graph could index both a live local package and a stale cached copy of the same package, creating duplicate nodes and confusing dependency answers.&lt;/p&gt;

&lt;p&gt;v0.3.2 uses deterministic source precedence:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;1. Embedded package under Packages/&amp;lt;name&amp;gt;
2. Active external local package from manifest or lock metadata
3. Matching package cache under Library/PackageCache
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;An embedded package wins because it is physically inside the Unity project and overrides package-manager resolution for that package name. An active external local package wins over a matching cache entry because the local source is the package Unity is using for development. The cache remains useful for registry and Git packages, but it should not compete with the current local source.&lt;/p&gt;

&lt;p&gt;The existing duplicate-GUID validation remains the final safety boundary. If two genuinely active assets claim the same Unity GUID, indexing fails before publishing a new database. External package discovery does not silently choose a GUID winner, because Unity asset identity has to stay unambiguous for graph answers to be trustworthy.&lt;/p&gt;

&lt;h2 id=&quot;incremental-refresh-tracks-package-selection&quot;&gt;Incremental Refresh Tracks Package Selection&lt;/h2&gt;

&lt;p&gt;Normal indexing is still incremental. Each logical asset uses the newer modification time of the asset file and its sibling &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.meta&lt;/code&gt;, including assets inside external packages. Editing a package asset or its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.meta&lt;/code&gt; therefore participates in the same incremental path as project assets.&lt;/p&gt;

&lt;p&gt;Package selection introduces another freshness problem. The manifest or lockfile can change while the selected package assets appear unchanged. A dependency can be removed, pointed at another folder, or replaced with a different source that preserves the same virtual path, GUID, and timestamps. A file-level timestamp check is not enough to detect that package-source decision.&lt;/p&gt;

&lt;p&gt;v0.3.2 records a package-discovery fingerprint for the indexed package plan. The fingerprint is derived from the project manifest, lockfile, and selected package-source descriptors. When that fingerprint changes, the next index run discards the copied incremental staging database and performs a fresh candidate-graph reconciliation before the atomic publish step.&lt;/p&gt;

&lt;p&gt;That gives the indexer two layers of freshness. Asset and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.meta&lt;/code&gt; mtimes handle ordinary edits. The package-discovery fingerprint handles changes to which package sources are active. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;force: true&lt;/code&gt; remains the explicit guaranteed-freshness path and scans all active roots unconditionally.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;unity-asset-reference-mcp-index index /path/to/UnityProject &lt;span class=&quot;nt&quot;&gt;--force&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;MCP callers use the same public tool as before:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;index_project(path, force?)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;There is no new scan-root argument. The point of this release is that active local UPM packages are project metadata, so indexing them should not require a second configuration system.&lt;/p&gt;

&lt;h2 id=&quot;warnings-are-bounded-and-localized&quot;&gt;Warnings Are Bounded and Localized&lt;/h2&gt;

&lt;p&gt;Package discovery can fail for reasons that should not make the whole project impossible to index. A package directory may be missing on one machine. A local path may be inaccessible. A &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package.json&lt;/code&gt; file may be malformed. A package may declare a different name than the dependency key in the Unity manifest.&lt;/p&gt;

&lt;p&gt;Those cases produce &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package-discovery&lt;/code&gt; warnings and skip the invalid source. Other valid roots continue to index. The previous good index is still replaced only after the complete candidate graph passes the existing validation and publication checks.&lt;/p&gt;

&lt;p&gt;A missing or malformed project manifest disables external-local discovery for that run and emits one bounded manifest warning. Embedded package and package-cache scanning still continue, so the indexer preserves as much useful graph data as it can without inventing package state.&lt;/p&gt;

&lt;p&gt;Ignore rules use canonical paths, not physical source paths. A configured ignore such as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Packages/com.company.gameplay/Samples~/**&lt;/code&gt; applies to the external package as it appears in Unity. Root-level ignores and descendant ignores are evaluated before recursion, so an ignored package root produces neither nodes nor per-file warning noise.&lt;/p&gt;

&lt;h2 id=&quot;the-release-changes-scope-not-the-public-shape&quot;&gt;The Release Changes Scope, Not the Public Shape&lt;/h2&gt;

&lt;p&gt;v0.3.2 expands what the existing indexer can see. It does not change the public query surface.&lt;/p&gt;

&lt;p&gt;The npm package was published as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unity-asset-reference-mcp@0.3.2&lt;/code&gt;, and the annotated &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;v0.3.2&lt;/code&gt; tag points at the published release commit. The Unity verification package stays at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.2.0&lt;/code&gt;. SQLite remains schema 3. MCP and CLI inputs and successful response shapes are unchanged.&lt;/p&gt;

&lt;p&gt;The release validation covered the package-indexing behavior and the publication boundary. The release evidence records root Vitest passing across 39 test files and 298 tests, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm run typecheck&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm run build&lt;/code&gt;, package dry-runs for both the root package and Unity package, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git diff --check&lt;/code&gt;, a successful GitHub Actions release workflow, and npm registry verification returning exactly &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.3.2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The exclusions are as important as the feature. v0.3.2 does not add manual root configuration, filesystem watchers, Unity Editor callbacks, package downloads, local package mutation, schema migration, or new MCP tools. It reads the active local packages Unity already knows about, represents them with Unity’s virtual package paths, and lets the existing GUID graph answer cross-boundary dependency questions.&lt;/p&gt;

&lt;p&gt;External local package indexing makes the graph match modular Unity projects more closely. Package assets no longer disappear just because their source folder sits outside the game repository. The index still publishes portable SQLite data, still resolves by GUID, and still keeps package discovery explicit enough to warn when local machine state does not match the project’s package declarations.&lt;/p&gt;
</description>
        <pubDate>Fri, 24 Jul 2026 00:00:00 +0000</pubDate>
        <link>https://vincewang.dev/2026/uasset-reference-mcp-external-upm-package-indexing/</link>
        <guid isPermaLink="true">https://vincewang.dev/2026/uasset-reference-mcp-external-upm-package-indexing/</guid>
        
        
        <category>tools</category>
        
      </item>
    
      <item>
        <title>UAsset Reference MCP: Modeling Unity Addressables Separately</title>
        <description>&lt;p&gt;Addressables changed the unused-asset question in UAsset Reference MCP. A texture, prefab, or scene can be unreferenced by serialized assets and still be intentionally loaded through an Addressables entry.&lt;/p&gt;

&lt;p&gt;That means a reference graph needs two different kinds of evidence. One answers whether a serialized asset points to another asset. The other answers whether an asset is reachable because the project declares it as Addressable content.&lt;/p&gt;

&lt;h2 id=&quot;serialized-references-and-addressables-answer-different-questions&quot;&gt;Serialized References and Addressables Answer Different Questions&lt;/h2&gt;

&lt;p&gt;The first release of UAsset Reference MCP focused on Unity’s serialized reference records. In text mode, Unity writes links as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{fileID, guid, type}&lt;/code&gt; values. Those records are direct asset-to-asset edges.&lt;/p&gt;

&lt;p&gt;Addressables are not the same shape. An Addressables group file describes entries, labels, read-only state, group membership, and an address that can be loaded at runtime. That metadata does not mean another asset serialized a field reference to the entry. It means the project author declared the entry as loadable content.&lt;/p&gt;

&lt;p&gt;Flattening both concepts into one edge type would make the graph easier to query but harder to trust. A developer reviewing unused assets needs to know why an asset is reachable.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Serialized edge:
Assets/Scenes/Battle.unity -&amp;gt; Assets/Prefabs/Enemy.prefab

Addressables reachability:
Addressables group &quot;RemoteCharacters&quot; contains Assets/Prefabs/Enemy.prefab
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Those statements support different decisions. The scene edge explains a concrete dependency. The Addressables entry explains runtime availability.&lt;/p&gt;

&lt;h2 id=&quot;normalized-tables-keep-the-model-honest&quot;&gt;Normalized Tables Keep the Model Honest&lt;/h2&gt;

&lt;p&gt;UAsset Reference MCP stores Addressables metadata in separate SQLite tables instead of hiding it inside the regular &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;edges&lt;/code&gt; table.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;addressable_groups
addressable_entries
addressable_entry_labels
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The group table records the Addressables group identity and source file. The entries table records the asset GUID, address, owning group, read-only state, and indexed source bytes. Labels live in their own table because one entry can have multiple labels, and queries need deterministic filtering.&lt;/p&gt;

&lt;p&gt;This structure keeps regular graph traversal simple while still letting the query layer answer Addressables-specific questions. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;find_unused_assets&lt;/code&gt; can treat Addressables entries as roots when configured to do so, while &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;get_addressable_info&lt;/code&gt; can explain the exact group and labels that make one asset reachable.&lt;/p&gt;

&lt;h2 id=&quot;reachability-is-a-review-signal&quot;&gt;Reachability Is a Review Signal&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reachableOnlyBecauseAddressable&lt;/code&gt; is deliberately a review signal. It tells the user that an asset is not reachable through scenes or serialized asset references, but it is reachable through Addressables metadata.&lt;/p&gt;

&lt;p&gt;That distinction matters because Unity projects often load content through code:&lt;/p&gt;

&lt;div class=&quot;language-csharp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;Addressables&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;LoadAssetAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GameObject&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;EnemyBoss&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Static graph indexing does not prove whether string-based runtime loading is used correctly. It can show that the asset is declared as an Addressables entry and that no serialized edge points at it. A developer still needs to review the loading code before deleting or moving the asset.&lt;/p&gt;

&lt;p&gt;The tool avoids presenting Addressables reachability as deletion safety. It reports the graph evidence and leaves the production decision with the engineer.&lt;/p&gt;

&lt;h2 id=&quot;mcp-tools-make-addressables-queryable&quot;&gt;MCP Tools Make Addressables Queryable&lt;/h2&gt;

&lt;p&gt;The Addressables release added read-only MCP tools for common inspection paths.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;get_addressable_info(asset)
search_addressables(query?, group?, label?, pathPrefix?, type?, reachableOnlyBecauseAddressable?, limit?)
list_addressable_groups()
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;get_addressable_info&lt;/code&gt; resolves one asset or Addressables address and returns membership, owning group, labels, reference counts, and reachability signals. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;search_addressables&lt;/code&gt; gives agents bounded discovery across entries. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;list_addressable_groups&lt;/code&gt; gives a compact inventory of groups, labels, entry counts, and indexed source bytes.&lt;/p&gt;

&lt;p&gt;The tools are intentionally read-only. They help an agent explain the project; they do not rewrite Addressables configuration, move entries between groups, or predict bundle output.&lt;/p&gt;

&lt;h2 id=&quot;verification-uses-real-project-exports&quot;&gt;Verification Uses Real Project Exports&lt;/h2&gt;

&lt;p&gt;Addressables parsing has more edge cases than plain &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.meta&lt;/code&gt; GUID scanning. Group files can have different layouts, entries can be read-only, labels can be absent, and generated metadata can vary between package versions.&lt;/p&gt;

&lt;p&gt;The implementation was verified against Unity-side exports from a real project. The verification flow compares what Unity reports with what the Node indexer stored. That gives the parser a concrete contract: group identity, entry identity, labels, and reachability data must match the project Unity understands.&lt;/p&gt;

&lt;p&gt;The result is still scoped. Stage 1 models Addressables discovery and query behavior. Group schemas, profiles, providers, packing, compression, build/load paths, content-update settings, and bundle analysis remain outside this release.&lt;/p&gt;

&lt;p&gt;Addressables support is useful because it keeps the asset graph from pretending every reachable asset is reachable for the same reason. Serialized references, Addressables entries, and runtime string loads need separate evidence. UAsset Reference MCP models the first two directly and makes the third explicit as a limitation.&lt;/p&gt;

</description>
        <pubDate>Tue, 21 Jul 2026 00:00:00 +0000</pubDate>
        <link>https://vincewang.dev/2026/uasset-reference-mcp-addressables-discovery/</link>
        <guid isPermaLink="true">https://vincewang.dev/2026/uasset-reference-mcp-addressables-discovery/</guid>
        
        
        <category>tools</category>
        
      </item>
    
      <item>
        <title>UAsset Reference MCP: Building a Unity Asset Reference Graph</title>
        <description>&lt;p&gt;UAsset Reference MCP turns a Unity project’s serialized assets into a queryable reference graph. The indexer reads Unity’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.meta&lt;/code&gt; GUIDs and YAML reference records, stores the result in SQLite, then exposes that database through a CLI, an MCP server, and a local web viewer.&lt;/p&gt;

&lt;p&gt;The goal is to answer asset questions before they become production problems. If a developer wants to delete a material, rename a prefab folder, split an Addressables group, or understand why a scene pulls in a dependency chain, the graph gives them a concrete answer instead of a manual search across the Project window.&lt;/p&gt;

&lt;h2 id=&quot;unity-already-stores-the-asset-graph&quot;&gt;Unity Already Stores the Asset Graph&lt;/h2&gt;

&lt;p&gt;Unity assets have stable identity through &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.meta&lt;/code&gt; files. A prefab, scene, material, texture, script, or Addressables asset has a GUID that other serialized files can reference.&lt;/p&gt;

&lt;p&gt;When a project uses text serialization, Unity writes asset references into YAML-like files as records such as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{fileID, guid, type}&lt;/code&gt;. That format is not a guess. It is the same reference information Unity uses to reconnect serialized fields to project assets.&lt;/p&gt;

&lt;p&gt;UAsset Reference MCP builds on that property. The indexer scans project assets, parses each asset’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.meta&lt;/code&gt; GUID, extracts serialized reference records, resolves those GUIDs back to asset paths, and stores the result as directed edges.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Assets/Scenes/Battle.unity
  -&amp;gt; Assets/Prefabs/Enemy.prefab
  -&amp;gt; Assets/Materials/BattleFloor.mat

Assets/Prefabs/Enemy.prefab
  -&amp;gt; Assets/Textures/Enemy_Diffuse.png
  -&amp;gt; Assets/Animations/Enemy.controller
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That graph becomes useful once it is durable. A one-off scan can answer one question, but a SQLite index lets command-line tools, MCP clients, and the web viewer share the same model.&lt;/p&gt;

&lt;h2 id=&quot;sqlite-is-the-boundary-between-indexing-and-querying&quot;&gt;SQLite Is the Boundary Between Indexing and Querying&lt;/h2&gt;

&lt;p&gt;The indexer writes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.asset-memory/index.db&lt;/code&gt; inside the Unity project. That database stores assets as nodes, references as edges, unresolved references as broken links, and Addressables metadata in normalized tables.&lt;/p&gt;

&lt;p&gt;SQLite keeps the tool simple. There is no service to deploy, no graph database to run, and no MCP client lock-in. The MCP server can expose curated tools for agents, while external scripts can still inspect the artifact directly when needed.&lt;/p&gt;

&lt;p&gt;The core workflow looks like this:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;unity-asset-reference-mcp-index index /path/to/UnityProject &lt;span class=&quot;nt&quot;&gt;--force&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;After indexing, the project has a local graph database:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;/path/to/UnityProject
└── .asset-memory
    └── index.db
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The same index can support impact analysis, dependency tracing, unused-asset review, broken-reference reporting, and JSON export without rescanning the project for every query.&lt;/p&gt;

&lt;h2 id=&quot;mcp-gives-agents-a-curated-query-surface&quot;&gt;MCP Gives Agents a Curated Query Surface&lt;/h2&gt;

&lt;p&gt;The MCP server runs over stdio and works with MCP-compatible hosts. Instead of giving an agent raw SQL access, it exposes specific tools with bounded behavior.&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;mcpServers&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;unity-asset-graph&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;command&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;npx&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;args&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;-y&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;unity-asset-reference-mcp&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;--project&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;/path/to/UnityProject&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;An agent can then ask practical Unity questions through tools such as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;find_references&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;get_dependencies&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;trace_path&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;find_unused_assets&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;search_assets&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;get_overview&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That boundary matters. The agent receives a project-specific graph interface, not a general file-system search habit. It can answer, “what references this prefab?” or “why is this texture reachable?” with graph data instead of scanning every YAML file during the conversation.&lt;/p&gt;

&lt;h2 id=&quot;verification-keeps-the-parser-honest&quot;&gt;Verification Keeps the Parser Honest&lt;/h2&gt;

&lt;p&gt;The Node indexer parses Unity serialization outside the Unity Editor. That keeps indexing scriptable and agent-friendly, but it also means the parser needs a way to prove its results against Unity’s own view of the project.&lt;/p&gt;

&lt;p&gt;The companion Unity package exports a verification file from the Editor:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Tools &amp;gt; Asset Reference Memory &amp;gt; Export Verification
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The CLI compares that export against the SQLite graph:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;unity-asset-reference-mcp-index verify-index /path/to/UnityProject &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--verify&lt;/span&gt; /path/to/UnityProject/.asset-memory/verify.json
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This makes parser accuracy measurable. Missed edges, extra edges, and unresolved references can be reported as data instead of discovered later through a broken production workflow.&lt;/p&gt;

&lt;h2 id=&quot;what-this-series-will-cover&quot;&gt;What This Series Will Cover&lt;/h2&gt;

&lt;p&gt;This first post sets the boundary of the tool: Unity text serialization in, SQLite graph out, MCP and viewer queries on top. The rest of the series can go deeper into the implementation details that made the asset useful in real projects.&lt;/p&gt;

&lt;p&gt;Future posts can cover the GUID and YAML parser, the SQLite schema, incremental indexing, Addressables modeling, MCP tool design, the web viewer, and the Unity verification harness.&lt;/p&gt;

&lt;p&gt;The useful part is not that Unity assets can be scanned. The useful part is turning Unity’s existing serialized reference data into a durable graph that both developers and agents can query before changing a project.&lt;/p&gt;
</description>
        <pubDate>Wed, 08 Jul 2026 00:00:00 +0000</pubDate>
        <link>https://vincewang.dev/2026/uasset-reference-mcp-unity-asset-graph/</link>
        <guid isPermaLink="true">https://vincewang.dev/2026/uasset-reference-mcp-unity-asset-graph/</guid>
        
        
        <category>tools</category>
        
      </item>
    
  </channel>
</rss>
