Your AI assistant can write a state machine in eight seconds but has no idea what assets you own.
AssetHoard is a local-first desktop asset manager for indie game devs, built in Rust and Tauri. It already knows every model, texture, sprite and sound in your library, what bundle it lives in, how it is tagged and what it depends on. Version 0.2.3 adds a built-in MCP server so your assistant can ask.
This is the engineering side: where the server lives, why it talks HTTP, and what it takes to make a local server that can write to your library safe to leave running.
Inside the app, not beside it
The obvious shortcut is a standalone MCP binary that opens the app's SQLite file and reads it directly. We ruled that out for two reasons:
- Two processes writing to one SQLite file is how you corrupt a library.
- The app would never learn about changes. Ask the assistant to tag forty rocks and the open window keeps showing them untagged until restart.
So the server runs inside AssetHoard and only answers while the app is open. That turned out to be a feature: every change the assistant makes shows up in the UI immediately.
One rule: MCP never touches the database
The MCP module is not allowed to call app commands or SQL. Everything goes through a small internal library API that sits underneath both the UI and the MCP server.
When an assistant moves an asset into a bundle, it runs the same function as a drag and drop. Same validation, same audit log, same undo entry. There is no second code path drifting out of sync with the real one.
It also makes new tools cheap. A tool is a typed argument struct, whose doc comments become the descriptions the model reads, plus a thin method that calls the library API. The interesting logic lives in one place and gets tested once.
Stack: rmcp, streamable HTTP, axum
- rmcp, the official Rust MCP SDK, with its streamable HTTP transport mounted on axum
- Database work on blocking worker threads, so a large search never stalls the async runtime
- Each call recorded in a recent-activity list visible in Settings
We chose HTTP over stdio because the server lives inside an app that is already running. Claude Code, Cursor, VS Code and Windsurf connect to a local endpoint with a header, which is a one-line setup. Claude Desktop only launches stdio servers, so for now it goes through the mcp-remote bridge.
The port is fixed at 47823 by default. It is configurable, but the app never picks another one on its own if it is busy. Auto-switching sounds helpful until every client config you have written silently stops matching. A taken port is a readable error instead.
Making it safe to leave on
A local server that reads your files and writes to your library deserves some paranoia.
- Off by default. One toggle in Settings, and a toolbar icon whenever it is running.
-
Loopback only. Binds to
127.0.0.1. - Bearer token. 48 random hex characters, compared in constant time. Regenerating it disconnects every client.
-
Browsers refused. Any request with an
Originheader is rejected, andHostmust be a loopback address. Browsers always sendOrigin; CLI and IDE clients do not. This blocks DNS rebinding, and it is what the MCP spec asks local servers to do. - Write tools are a second opt-in. Checked on every call, so switching it off takes effect for already-connected clients.
- No delete tools for assets or bundles. Exports skip existing files rather than overwrite.
- Imports ask first. A native prompt shows the path, file count, size and target bundle. No answer within 60 seconds is a no.
-
Everything is undoable. Writes land on the normal undo stack with an
MCP:label, e.g.MCP: Moved 1 asset into 'Rocks'.
Small decisions that paid off
Text results, separate thumbnails. Search returns ids, names, types, paths and tags. If the model wants to look, it calls get_thumbnail and gets an image of at most 256px. It can check that the "mossy rock" really is mossy without flooding its context with every result's picture.
Dependencies travel with exports. A model brings its textures, a Godot .tres brings what it references, Unreal assets bring theirs. Assets inside zips or .unitypackage files are extracted first.
Deep links. Every asset, bundle and project returned includes an assethoard:// link, so the assistant can hand you something clickable that jumps straight to the item in the app.
Errors the model can read. Unknown ids or disabled write tools come back as plain error messages rather than protocol failures, so the assistant can explain or try something else.
What it looks like in use
"Find three different barrel models and a wood texture that matches, and copy them into
Assets/Props."
The assistant searches, checks thumbnails, picks a set and exports it, textures included. You keep building the level.
"Make a bundle called 'Rocks' under 'Environment', move every rock model into it and tag them 'nature'."
Each step appears live in the app as its own undo entry. If it moves a boulder that was actually a potato, Ctrl+Z.
What is next
The main gap is Claude Desktop needing mcp-remote. A small native shim is on the list.
The full write-up, including more workflows, is on the AssetHoard blog. Setup per client is in the MCP docs and every tool is in the tool reference. AssetHoard is free to download during the open beta.
If you have built a local MCP server into a desktop app, I would like to hear how you handled auth and write access.
Top comments (1)
The shared library API and normal undo stack make the write boundary easier to reason about. I'd test revocation while an import prompt is open: start the import, switch write tools off (or regenerate the token), then approve the old prompt. Does the app recheck permission at commit and reject that queued write, with no new files or undo entry? Checking each incoming call may not cover a call that already passed the gate and is waiting for the user's answer.