From Go-bundled to JSON-pluggable — and into Claude Code itself.
A week ago, adding a new MCP server to loomcycle's curated catalog meant a Go PR, a recompile, a binary release, and a Homebrew bump. Today it means dropping a JSON file in $LOOMCYCLE_MCP_RECIPES_ROOT and running loomcycle mcp-registry list. The catalog moved from code to data. Once it became data, three things became obvious that hadn't been before — and the third one ships today as a Claude Code plugin.
The reconsideration
The first version of loomcycle's MCP catalog was a Go slice. Thirteen curated recipes — Tavily, Brave Search, GitHub, GitLab, Notion, Slack, Discord, Telegram, email, arXiv, fetch, filesystem, an internal jobs API — each a struct with a command, args, env-var allowlist, and a metadata block. Operators could see what was on offer through loomcycle mcp list, and the typed struct meant every recipe was schema-correct by construction. It was the right first move; it shipped the catalog as a feature without inventing infrastructure.
It also meant the catalog grew at the speed of releases. An operator who needed a recipe loomcycle didn't yet ship had two options: open a PR, or write the MCP server entry by hand in their yaml. Neither matches how operators think about an MCP catalog. The catalog is their domain — which servers are trusted in this deployment, which env vars are gated, which credential keys are required. Hard-coding it in Go is a category error: that's data, not code.
PR #274 moved the catalog. The recipes now live as JSON files under internal/recipes/bundled/, embedded into the binary via //go:embed so operators get the curated thirteen for free. A filesystem overlay at $LOOMCYCLE_MCP_RECIPES_ROOT shadows the bundled set with complete-replace semantics — your overlay file beats the bundled file wholesale. A sibling <name>.disabled file hides a recipe operators don't want surfaced. Seven CLI verbs (list, show, append-to-config, add, remove, enable, disable) cover discover-and-install for the bundled set and full CRUD over the overlay. No recompile, ever.
The shape choice that mattered most: each recipe is Claude Code's .mcp.json per-server JSON shape, with a sibling _loomcycle metadata block. Top-level fields are byte-compatible with Claude Code's format — a bundled loomcycle recipe drops cleanly into a Claude Code .mcp.json if you strip the _loomcycle block, and a Claude Code .mcp.json entry lifts into the loomcycle library if you add one. One JSON schema, two consumers, no translation layer.
// internal/recipes/bundled/slack.json
{
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-slack"],
"env": {
"SLACK_BOT_TOKEN": "${LOOMCYCLE_SLACK_BOT_TOKEN}",
"SLACK_TEAM_ID": "${LOOMCYCLE_SLACK_TEAM_ID}"
},
"_loomcycle": {
"description": "Slack via the official MCP server.",
"transport": "stdio",
"pool_size": 4,
"env_vars_required": ["LOOMCYCLE_SLACK_BOT_TOKEN", "LOOMCYCLE_SLACK_TEAM_ID"],
"credentials": ["slack"],
"schedule_compatible": true,
"agent_prompt_hint": "Slack publish + DM."
}
}
What the format-share unlocked
As soon as the loomcycle catalog spoke Claude Code's JSON, an obvious primitive surfaced: walk a Claude Code repo's .claude/ directory and ingest it into loomcycle yaml. Most teams using Claude Code already have a real .claude/ by the time they hear about loomcycle — agents under .claude/agents/, skills under .claude/skills/, MCP servers in .claude/mcp.json or a project-root .mcp.json. Asking them to re-author all of that in loomcycle's shape was a non-starter. Asking the loomcycle binary to read it directly is a one-command move.
PR #275 shipped loomcycle import claude-code. The verb walks the .claude/ tree, maps each artifact to a loomcycle yaml fragment, and emits a typed ImportReport with what was mapped, what was skipped, what was unmapped, and what needs operator review. Six output modes (--dry-run, --write, --json, --emit-recipes, --force, --no-recipe-match) cover the spectrum from "show me what you'd do" to "rebuild loomcycle yaml from this Claude repo in CI."
Two design choices in the importer are worth naming:
- MCP server matching is by package, not by name. If your
.claude/mcp.jsoncalls a server "my-slack" but the package is@modelcontextprotocol/server-slack, the importer matches the package to loomcycle's bundled Slack recipe and substitutes the recipe's command/args/env-allowlist — while preserving your custom namemy-slackso your agents that reference it keep working. Operator names are sacred; the recipe wins on transport-level details. Operators wanting literal port pass--no-recipe-match. - Substrate-field stubs follow a default-deny floor with name-pattern hints. An agent named
*-scheduleror*-orchestratorgets aschedule_def_scopes: ["any"]stub with a yaml comment pointing at RFC E and a "tighten manually" note. An agent named*-evolverormeta-*gets aagent_def_scopes: ["self"]stub. Anyone else gets nothing. Heuristics fire only when they're conservative; operators see the rationale in the yaml and decide.
Lossy import is loud, not silent. Unmapped frontmatter (Claude Code's hooks, output_style, temperature, top_p, color, custom keys) surfaces in the report's Unmapped slice with a per-field hint. Malformed individual files become warnings, not aborts. Operators see what was ingested cleanly, what needs review, and what loomcycle deliberately doesn't model — and they can act on each separately.
Closing the loop — claude-code-plugin-loomcycle
Both the catalog change and the import command were data movement. The third consequence is runtime: once loomcycle can be authored from Claude Code (via import) and exposed as MCP (via loomcycle mcp install, shipped in v0.8.15), the missing piece is the UX layer that makes loomcycle feel like a Claude Code citizen rather than a separately-managed sidecar. That's claude-code-plugin-loomcycle, released today.
It's a separate-repo plugin (denn-gubsky/claude-code-plugin-loomcycle), distributed via Claude Code's plugin marketplace — markdown + JSON in a git repo, no npm, no build step. Install with /plugin marketplace add denn-gubsky/claude-code-plugin-loomcycle then /plugin install loomcycle. Claude Code prompts you for four bits of config at install — bin_path, config_path, auth_token (keychain-stored as sensitive), base_url — and wires the MCP server entry automatically.
What you get once it's installed:
- Six slash commands wrapping the most common loomcycle workflows —
/loomcycle:connect,/loomcycle:run,/loomcycle:runs,/loomcycle:cancel,/loomcycle:snapshot,/loomcycle:eval. Each takes operator-friendly named arguments rather than JSON pasting; results render in Claude Code's markdown surface as tables and code blocks. - Four bundled skills —
loomcycle-spawn-evaluator,loomcycle-replay-failed-run,loomcycle-diff-agentdefs,loomcycle-import-claude-code. The last one wraps theloomcycle import claude-codeCLI as a guided Claude Code conversation, so operators can do the import without dropping to a shell. - Two opt-in PostToolUse hooks, both no-op by default.
capture-run-telemetry(enabled byLOOMCYCLE_PLUGIN_TELEMETRY=1) appends a{ts, run_id, agent_id}JSONL record to${CLAUDE_PLUGIN_DATA}/run-telemetry.jsonlafter eachspawn_run.auto-snapshot-on-error(enabled byLOOMCYCLE_PLUGIN_AUTO_SNAPSHOT=1) runs theloomcycle snapshot --description pre-error-<ts>CLI when a state-mutating tool errors out — the matcher is scoped tospawn_run,restore_snapshot,register_agent,unregister_agent,pause_runtime,resume_runtimerather than every MCP call. The trust posture matches loomcycle's own default-deny floor on Memory / Channel / Bash.
Notably the plugin ships zero loomcycle-side code changes. It consumes the existing loomcycle mcp stdio server plus the 20+ meta-tools (spawn_run, cancel_run, memory, channel, agentdef, evaluation, context, pause/snapshot ops) verbatim. The plugin's job is wiring + UX, not new capabilities. The only loomcycle-side change was a one-page docs/CLAUDE-CODE.md at denn-gubsky/loomcycle/docs/CLAUDE-CODE.md covering both paths (plugin-recommended, manual loomcycle mcp install still supported).
What the three together unlock
Until this week, "use loomcycle from Claude Code" required three uncomfortable choices about where authoring lived, where runtime lived, and how the two synced. From today the answer is the same loop in three steps:
- Author in Claude Code. Your
.claude/agents/,.claude/skills/,.claude/mcp.jsonstay where they are — the IDE-as-truth posture is untouched. Claude Code remains the editor, the version-control surface, the place where the human refines prompts. - Import into loomcycle.
loomcycle import claude-code --writelifts the repo into loomcycle yaml. Recipe-match collapses your custom-named MCP server entries onto the curated catalog's transport-correct shape. Substrate-field stubs hint at where RFC E (ScheduleDef) and RFC F (per-run credentials) apply. Run it in CI on every push if you want the loomcycle yaml regenerated from.claude/on every commit. - Drive runtime from inside Claude Code. The plugin gives you slash commands against the running loomcycle instance — local or remote, single-replica or cluster. Same auth token, same OTEL trace context propagated end-to-end, same per-tenant fairness as any other loomcycle caller.
The three steps share one JSON schema (the .mcp.json shape), one CLI tool (loomcycle), one auth surface (the bearer token), one observability stack (loomcycle's OTEL profiles). Nothing is duplicated, nothing is translated.
The lesson is the architectural choice that comes earlier than the artifact. Moving the MCP catalog from Go-bundled to JSON-pluggable wasn't a feature; it was a decision about who owns the catalog (operator) and what shape that ownership takes (a file format both ends speak). Once that was settled, the import command and the plugin both became obvious next moves rather than ambitious ones. "Catalog as data, in a shape your neighbor already uses" is the small idea the whole week hinges on.
What you can do with it today
If you're on a loomcycle build past PR #274:
$ loomcycle mcp-registry list
13 bundled recipes (loomcycle internal).
Overlay root: $LOOMCYCLE_MCP_RECIPES_ROOT (none set).
arxiv ArXiv search via mcp-arxiv.
brave-search Brave Search API.
discord Discord via the official MCP server.
email Email via mcp-email.
fetch HTTP fetch via the official MCP server.
filesystem Local filesystem (operator-restricted roots).
github GitHub via the official MCP server.
gitlab GitLab via the official MCP server.
jobs Internal jobs API.
notion Notion via the official MCP server.
slack Slack via the official MCP server.
tavily Tavily search.
telegram Telegram bot via mcp-telegram.
$ loomcycle import claude-code ~/work/my-claude-repo --dry-run
Found 6 agents, 3 skills, 4 mcp servers under .claude/.
Recipe-matched 3 of 4 mcp entries (github, slack, notion).
Unmapped frontmatter: 2 fields across 1 agent (output_style, color).
Run with --write to apply.
Then in Claude Code:
/plugin marketplace add denn-gubsky/claude-code-plugin-loomcycle
/plugin install loomcycle
/loomcycle:connect [email protected] --persist
/loomcycle:run job-search-batch "find FE roles in Berlin"
Companion reading: Three MCP tokens in one run (the per-run credentials story the recipes' credentials field plumbs into), Scheduled runs at 30,000 fires (where the recipes' schedule_compatible flag becomes load-bearing), and the n8n integration writeup (the same UX-layer pattern in a different IDE).