← app-me.online
Project Deep Dive

ideavault-mcp

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.

Meta note: this write-up was researched using this exact server's own tools — 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}.

Architecture

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.

CLIENTS IDEAVAULT-MCP SERVER · EXPRESS :3007 MCP Client Claude Code · HTTP + bearer Graph UI browser dashboard Auth + Rate Limit bearer token · 60 req/min Vault & Repo Tools read/write/edit files · idea notes External Research Tools Solana · packages · GitHub · docs Code-Graph Proxy MCP stdio client ↳ also backs Graph UI's /api/graph/* Vault notes ~/ideavault/*.md Repo filesystem ~/ (guarded) SQLite code graph External APIs Solana · GitHub · Telegram · docs codebase-memory Python · tree-sitter (child process)
this project's own process
persistent storage
third-party vendor API

What's actually built

24 tools total, split across four concerns that don't pretend to be one thing.

Vault as plain markdown

Idea notes live at ~/ideavault/Projects/*.md, openable directly in Obsidian — no proprietary format. Frontmatter tracks status, tags, blockers, and next steps per project.

Guarded repo read/write

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.

A merged code-graph engine

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.

A real graph UI, not just chat tools

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.

Scoped external research

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.

Ops automation over Telegram

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.

One shared credential, rate-limited

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).

Fails loud, not quiet

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.

Design decisions that aren't obvious

The kind of judgment calls that don't show up just reading the tool list.

DecisionWhy
Proxy a separate Python engine over stdio instead of rewriting it in TypeScriptcodebase-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 boundaryThe 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 serverSilently 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 processcodebase-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 transitionsThe 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 IDThis 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.

Stack

TypeScript / Node.jsExpressMCP SDKPython tree-sitterSQLiteReact + Cytoscape.jsTelegram Bot API systemd