A personal MCP server that turns this machine's project folders into an Obsidian vault of ideas, with a few scoped external-research tools, a structural code-graph engine, and Telegram-driven ops automation alongside it. One Express endpoint — Claude reads and writes the vault and calls these tools directly, no upstream LLM call inside the server itself.
get_idea, get_architecture, list_projects — called live against https://ideavault.app-me.online while writing this page. curl https://ideavault.app-me.online/health → {"ok":true}.One Express process behind auth + rate limiting, fanning out to three tool groups. The code-graph group doesn't implement graph logic itself — it proxies a separate Python engine spawned as a child process, so neither codebase had to be rewritten in the other's language to merge them.
24 tools total, split across four concerns that don't pretend to be one thing.
Idea notes live at ~/ideavault/Projects/*.md, openable directly in Obsidian — no proprietary format. Frontmatter tracks status, tags, blockers, and next steps per project.
Reads are capped at 200KB and refuse binaries and anything matching a secret/key-file pattern. First write to an untracked repo runs git init + a snapshot commit automatically, so there's always a rollback point.
codebase-memory (Python, tree-sitter, SQLite) is spawned as a persistent child process and proxied in over MCP's own stdio client transport — 8 tools, 7 languages, one systemd service instead of two.
Vite + React + Cytoscape.js dashboard served by the same process: dependency graphs, symbol search, and uncommitted-change blast-radius, reusing the exact same proxied engine calls as the chat tools.
Solana RPC/Jupiter pricing, crates.io/PyPI, GitHub code search, an allowlisted docs fetch (six domains, nothing else). Deliberately not a general web-fetch tool.
A 5-minute watchdog checks real port bindings (not just systemd "active") and only alerts on state transitions. A two-way loop lets a Telegram message edit a live Minecraft server's allowlist.
A single bearer token checked via header or query param, 60 req/min per IP, behind a hardened systemd unit (no new privileges, cleared capability set).
If the codebase-memory child process disconnects, the whole server exits rather than silently degrading to vault-only — systemd brings both back up together instead of masking a real crash.
The kind of judgment calls that don't show up just reading the tool list.
| Decision | Why |
|---|---|
| Proxy a separate Python engine over stdio instead of rewriting it in TypeScript | codebase-memory already worked and was verified against real repos. A thin proxy layer (JSON-Schema → zod) merges the surface without risking a rewrite bug in either codebase. |
| Real bug at the merge boundary | The proxy layer was forwarding an explicit null for omitted optional params (from Python's str | None = None defaults), which failed pydantic validation on the far side. Fixed by not forwarding schema defaults at all — omitted keys already fall through to the callee's own defaults correctly. |
| Child-process death kills the whole server | Silently falling back to vault-only tools would hide a real crash. process.exit(1) lets systemd's Restart=on-failure bring both halves back up together. |
| "Last active project" now lives in one shared process | codebase-memory used to spawn fresh per Claude Code session; merged in, that convenience default is now shared across every caller. Fine for one person, so query tools accept an explicit project param to avoid relying on it. |
| Ops alerts fire only on state transitions | The health watchdog runs every 5 minutes but only messages Telegram on newly-broken or newly-recovered — a service stuck down doesn't re-alert every cycle. |
| Bedrock allowlist control is hardcoded to one chat ID | This loop injects real commands into a live game server's console. The Telegram chat-ID check is hardcoded, not configurable, because this specific path can affect real players. |