ai newsDo., 15. Okt.
EilmeldungMeta and Apple team up to make Muse AI models run better on Macs

Vom MCP-Server zum Claude-Code-Plugin: Was dabei schiefgeht

Wer einen MCP-Server wie ContextOS als Claude-Code-Plugin anbindet, gibt Coding-Agents Gedächtnis über Sitzungen hinweg. Die Umstellung von Shell-Skript und Konfigurationsdatei auf ein Plugin bringt aber einige Stolperfallen mit sich.

dev.toMittwoch 14:35 · 14.10.DE6 Min. Lesezeit
Bild: dev.to

In my last post I introduced ContextOS, an MCP server that gives AI coding agents memory across sessions. You open Claude Code in a repo, and it already knows the branch you're on, the decisions you made, and what's still open.

It worked. But installing it took a shell script, a config file, and a claude mcp add command. That's where most people stop.

So I packaged it as a Claude Code plugin. Now it's two commands:

/plugin marketplace add aftabkh4n/contextos /plugin install contextos@contextos Enter fullscreen mode Exit fullscreen mode

Getting there took four patch releases in three days. Every bug was something I'd never have found without installing it on my own machine and using it like a stranger would. If you're building a plugin, I hope this saves you some of them.

A plugin is a folder with a manifest and some optional parts:

plugins/contextos/ ├──.claude-plugin/plugin.json # name, version, MCP server ├── skills/contextos/SKILL.md # when to remember and recall ├── hooks/hooks.json # SessionStart hook └── scripts/ensure-binary.sh # downloads the binary Enter fullscreen mode Exit fullscreen mode Plus a marketplace.json so people can install it by name.

Two small things caught me straight away:

ContextOS is a self-contained.NET binary with a local embedding model inside it. Each platform build is about 118 MB. That doesn't belong in a plugin folder.

So a SessionStart hook downloads the right build for your OS into ${CLAUDE_PLUGIN_DATA}, a folder that survives plugin updates. The MCP server entry in plugin.json points there.

First bug: the script copied only the binary. The embedding model lives in a Models/ folder next to it, so the first version installed a server that failed its own selftest on every machine. Validation passed, of course. Validation checks the manifest, not whether your program runs.

This one wasn't in the plugin at all. It was in v0.2.0, released the week before.

I'd added memory decay: memories not recalled in 90 days get archived. To support it I added a last_recalled_at column. Existing rows got NULL. The decay rule said "never recalled, so count from the creation date."

Every memory older than 90 days was archived on the first start after upgrading.

I only noticed because my test workspace suddenly had "zero decisions," when I knew it had one.

The fix was two migrations: backfill last_recalled_at with the upgrade time so existing memories get a fresh window, and a repair migration that restores rows already archived by the bug. On my machine the log said Restored 1 memories, and the missing decision came back.

Lesson: when you add a column and a rule that reads it in the same release, the default value for existing rows is a product decision, not a database detail.

I wanted "What was I working on?" answered from the injected context, with no tool calls. I put that rule in SKILL.md.

Claude ran git log, listed files, read my old session history, then answered.

The reason: a skill's body only loads when Claude decides the skill applies. The rule wasn't in context when it mattered.

ContextOS already injects workspace state through the MCP initialize handshake, in serverInfo.instructions, which is always loaded. I moved the rule there, one line:

Use this context to answer questions about prior work, decisions or todos. Do not run git, list files, or read session history to answer them.

Next test: zero tool calls, answered in five seconds.

Lesson: skills are for "how to do this when it comes up." Anything that must always apply belongs somewhere that's always loaded.

if [ -f " $BINARY " ]; then exit 0 fi Enter fullscreen mode Exit fullscreen mode It checks whether a binary exists, not which version. And ${CLAUDE_PLUGIN_DATA} survives plugin updates, which is the whole point of it. So after updating the plugin, everyone kept the old binary forever.

Fix: write a.version file after each install and compare it with the version in plugin.json.

On session start, Claude Code launches the MCP server and runs the SessionStart hook in parallel. On a fresh install, the hook is still downloading 118 MB when Claude Code tries to start a binary that doesn't exist yet.

The server fails, and Claude Code remembers the failure for 15 minutes. So the user's first impression is a broken plugin.

To replace the binary during an upgrade on Windows, I stopped the running process first:

taskkill //F //IM contextos.exe Enter fullscreen mode Exit fullscreen mode Combined with Bug 4, the hook killed the server Claude Code had just started. Worse, taskkill by image name kills every contextos.exe on the machine. If you had two Claude Code windows open, updating in one broke both.

My first idea was to swap the binary at session end instead. The docs ended that quickly: SessionEnd hooks get a 1.5 second budget that plugins can't raise, and they also fire on /clear and /resume, when the session keeps running.

The fix that finally worked was to stop replacing binaries at all:

bin/0.2.3/contextos.exe bin/0.2.4/contextos.exe Enter fullscreen mode Exit fullscreen mode Each version installs into its own folder, and plugin.json points at the folder for its own version. Nothing running is ever touched, so there's nothing to kill and no file locks. Old folders get cleaned up later, when nothing is using them.

The tradeoff: right after an update, the new folder is still downloading, so the server fails once and you choose Reconnect in /mcp. The hook prints a line into Claude's context explaining that. A known, one-time failure with a clear fix beats a silent one.

I also added a test that reads plugin.json and fails the build if the server path doesn't contain the manifest's own version.

While cleaning up the release archives, the Linux binary aborted on start in WSL:

Couldn't find a valid ICU package installed on the system. Enter fullscreen mode Exit fullscreen mode Minimal distros and Docker images often don't ship libicu, and that's exactly where people run agents. The fix is one line in the project file:

<InvariantGlobalization> true </InvariantGlobalization> Enter fullscreen mode Exit fullscreen mode I didn't want that to quietly change embeddings for memories people had already stored, so I embedded four strings before and after, including accented text, a Turkish İ, and Chinese. The max difference was 0.0 on all of them. Then it shipped.

The marketplace reads plugin.json from the main branch. So the moment I pushed a version bump, new installs pointed at a GitHub release that didn't exist yet, and got a 404. Now I tag right after CI goes green, every time.

That last number taught me something about my own setup. When I checked, my Claude Code sessions were loading 140 MCP servers and 294 skills from plugins I'd installed once to try. ContextOS was one server out of 140. If your sessions feel heavy, run /context and look before blaming any single tool.

/plugin marketplace add aftabkh4n/contextos /plugin install contextos@contextos Enter fullscreen mode Exit fullscreen mode Restart Claude Code. If contextos shows as failed in /mcp on the first start, choose Reconnect while the binary finishes downloading. Then open any git repo and ask: "What was I working on?"

If something breaks for you, open an issue. Apparently that's how this thing gets better.

Originaltitel: Turned my MCP server into a Claude Code plugin. Here's everything that broke.Quelle: dev.to (dev.to). Original öffnen