ideavault-mcp
A personal MCP server that manages an Obsidian vault of project notes, provides scoped external research tools, and offers code-graph analysis via a proxied tree-sitter engine.
README
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 and a structural code-graph alongside it. One Express endpoint, no upstream LLM calls from the server itself — Claude (via Claude Code, Claude.ai, or the Claude mobile app) reads and writes the vault and calls these external services directly through the tools.
Vault lives at ~/ideavault/Projects/*.md — open that folder directly in Obsidian.
Architecture note: the code-graph tools aren't implemented here — they're
codebase-memory (a Python/SQLite/tree-sitter engine, its
own repo history preserved via git subtree when it was folded in here)
spawned as a persistent child process at startup and proxied in over
MCP's stdio client transport (src/codebaseMemory.ts). One external MCP
surface, one systemd service, one auth boundary — but two independent engines
behind it, so neither codebase had to be rewritten in the other's language to
merge them. If the child process connection drops, the whole server exits
(process.exit(1)) rather than silently running vault-only, so systemd's
Restart=on-failure brings both back up together instead of masking a real
crash. Don't also run codebase-memory as its own separate stdio MCP server
(e.g. via claude mcp add) alongside this — both would open the same SQLite
DB file concurrently, which risks lock contention.
Tools
Vault (repos + notes)
| Tool | What it does |
|---|---|
list_repos |
Scans REPOS_DIR (default: home dir), detects stack (node/python/rust/go/dotnet), flags which repos already have a note |
list_repo_files |
Lists files/folders inside a repo so Claude can decide what to read before writing a note. Skips node_modules/.git/etc and secret-looking files |
read_repo_file |
Reads a text file's contents (README, source, config) from inside a repo. Refuses binaries and anything matching a secret/key-file pattern. Capped at 200KB |
write_repo_file |
Creates/overwrites a file inside a repo. If the repo has no .git yet, runs git init + a snapshot commit first. Doesn't commit its own change — review with git diff |
edit_repo_file |
Precise search-replace on an existing file — old_string must match exactly once. Same guardrails and git safety net as write_repo_file |
get_idea |
Reads one repo's note (frontmatter + body) |
upsert_idea |
Creates/updates a note — status, tags, blockers, next_steps, body. Omitted fields keep their old value |
append_log |
Appends a dated line to a note's ## Log section |
list_ideas |
Lists all notes, filterable by status or tag |
search_notes |
Full-text search across the vault |
Note frontmatter: repo, status (idea/in-progress/blocked/done/abandoned), tags[], blockers[], next_steps[], created, updated.
External research (scoped, not a general web-fetch tool)
| Tool | What it does | Needs |
|---|---|---|
solana_get_mint_info |
On-chain mint info (supply, decimals, authorities) via public RPC | nothing |
solana_get_token_price |
Price/liquidity via Jupiter's price API (v3) | nothing |
package_registry_search |
crates.io fuzzy search, or PyPI exact-name lookup (no public PyPI search API exists) | nothing |
github_search_code |
Search public GitHub code for reference implementations | GITHUB_TOKEN in .env |
telegram_bot_info |
A bot's public info via Telegram's getMe — token never leaves the server |
TELEGRAM_BOT_TOKEN_<NAME> in .env per bot |
telegram_send_message |
Send a plain-text message via a bot you control — e.g. notify yourself | TELEGRAM_BOT_TOKEN_<NAME> in .env; chat_id param or TELEGRAM_CHAT_ID_<NAME> in .env |
fetch_docs |
Fetches page text, restricted to an allowlist: docs.rs, pypi.org, solana.com, jup.ag, github.com, raw.githubusercontent.com, telegram.org | nothing |
Code graph (proxied from codebase-memory, read the architecture note above)
| Tool | What it does |
|---|---|
index_repository |
Index (or re-index) a local repo into the graph via tree-sitter — call once per project before the others |
list_projects |
List every indexed project with file/symbol counts |
search_symbols |
Find functions/methods/classes by name substring |
get_symbol_overview |
One call for "show me this function and what touches it" — source + callers + callees |
get_code_snippet |
Just one function's source by qualified name, without reading the whole file |
trace_calls |
Walk the call graph around a symbol (callers/callees/both, 1-10 hops) |
get_architecture |
Language breakdown, symbol counts, top folders, hotspot files — orient in an unfamiliar repo |
detect_changes |
Map uncommitted git changes to the symbols they touched, plus a rough blast-radius (caller count) |
6 languages: C#, Python, JS, TS/TSX, Rust, Go. project is optional on every
query tool except index_repository — omit it and the last project named
anywhere on this server is reused. Note this "last project" state now lives
in the one shared codebase-memory child process behind this HTTP server
rather than a stdio process per Claude Code session (its original design) —
fine for one person using one project at a time, but a second concurrent
caller working on a different repo will get the first caller's active
project if it omits project too. Pass project explicitly to avoid
relying on this. See codebase-memory's own docstring (server.py) for the
full scope/limitations (name-based call resolution, no type inference, no
cross-repo graph).
Graph UI
A small visual dashboard for the code graph, served by this same process —
open https://ideavault.app-me.online/ (or http://127.0.0.1:3007/ locally)
in a browser instead of going through chat. Source in web/ (Vite + React +
TypeScript + Tailwind + Cytoscape.js), built to static files and served via
express.static.
- Dashboard —
get_architectureas language/symbol-kind charts, top folders, hotspot files. - Search —
search_symbols, click a result to open it in Graph view. - Graph —
trace_callsrendered as an actual node/edge graph (callers, callees, or both; depth 1-4), with the selected symbol's source (get_code_snippet) alongside it. - Changes —
detect_changes: uncommitted edits mapped to the symbols they touched, with the existing caller-count risk heuristic.
It talks to a handful of new read-only JSON routes under /api/graph/*
(src/graphApi.ts) that wrap the same proxied codebase-memory tool calls the
MCP surface uses — no second engine, no direct SQLite access from the UI.
Those routes sit behind the same rateLimit → validateOrigin →
requireToken chain as /mcp. The static page itself is unauthenticated
(it's just HTML/JS/CSS); on first load it prompts for AUTH_TOKEN and stores
it in localStorage, sending it as Authorization: Bearer on every API
call — same token as everywhere else, no second credential.
Local dev
npm install
cp .env.example .env # edit AUTH_TOKEN at minimum
npm run build && npm start
# or: npm run dev (tsx watch, no build step — but the UI needs its own build below)
npm run build builds both the server and the web UI (build:web runs
npm --prefix web run build, needs cd web && npm install once first). For
UI-only iteration with hot reload: cd web && npm install && npm run dev —
its dev server proxies /api to 127.0.0.1:3007, so run the main server
(npm run dev at the repo root) alongside it.
Smoke test:
curl http://127.0.0.1:3007/health
curl -X POST http://127.0.0.1:3007/mcp \
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
-H "Authorization: Bearer $AUTH_TOKEN" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Deploy (this machine, following the existing app-me.online pattern)
Port 3007 is used here since 3000-3003/3005/3006 are already taken by
app1-4/sift/muse (see ../NGINX.md).
npm run build- Fill in
.env(realAUTH_TOKEN— generate withopenssl rand -hex 32) - Install the systemd unit:
sudo cp deploy/ideavault-mcp.service /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl enable --now ideavault-mcp - Add the nginx server block (
deploy/nginx-ideavault.conf) next to your otherapp-me.onlineblocks, thencertbot --nginx -d ideavault.app-me.online.
Status: already deployed. Live at https://ideavault.app-me.online, systemd
service running, DNS auto-synced by the existing namecheap-ddns timer (it
discovers hosts straight from nginx server_name directives — no config
changes needed there), cert auto-renews via certbot's timer.
Connect Claude to it
Claude Code (this machine or any other):
claude mcp add --transport http ideavault https://ideavault.app-me.online/mcp \
--header "Authorization: Bearer <your AUTH_TOKEN>"
Claude Desktop / Claude.ai / Claude mobile app: custom connector URL entry (Settings → Connectors → Add custom connector) only showed up on claude.ai in a browser, not in the native mobile app UI — add it there and it syncs to mobile since connectors are account-level. The connector UI has no field for a static bearer header, so the token goes in the URL instead:
https://ideavault.app-me.online/mcp?token=<your AUTH_TOKEN>
The server checks Authorization: Bearer, X-Vault-Token, and ?token= —
whichever the client can send. Once added, it still needs to be toggled on
per-conversation from the tools/connectors icon in the chat composer.
Security notes (personal-use tradeoffs, read before exposing to the internet)
- Auth is a single static shared secret, not OAuth. Fine for a personal tool used by one person, but anyone who gets the token gets full read/write on your vault and every repo's code (write_repo_file/edit_repo_file are not read-only). Treat the token like a password — this is the highest-stakes tool on the connector.
write_repo_file/edit_repo_filenever rungit commit/git pushfor the actual change — only a one-time snapshot commit if a repo had no.gitat all, to guarantee an undo path. Everything after that is a plain working-tree edit: review withgit diff/git statusand commit yourself. Repos that already had uncommitted changes before an edit get flagged (hadPriorChanges) rather than silently folded in.POST /mcpis rate-limited to 60 req/min per client IP (src/rateLimit.ts, in-memory fixed window). Express is set totrust proxy: loopbackso this keys on the real client IP from nginx'sX-Forwarded-For, not nginx's own loopback address for every request.- The nginx site (
deploy/nginx-ideavault.conf) hasaccess_log off— the token travels as?token=...for clients with no header field, and nginx's default log format would otherwise write that in plaintext to/var/log/nginx/access.logon every request. - The systemd unit (
deploy/ideavault-mcp.service) drops all Linux capabilities and blocks kernel/namespace/cgroup access (NoNewPrivileges,ProtectKernelTunables,RestrictNamespaces, etc.) — standard hardening for a plain Node HTTP server, shrinks what a hypothetical RCE (e.g. a compromised npm dependency) could reach. Filesystem sandboxing (ProtectHome/ProtectSystem) is deliberately not used since the app's whole job is read/write across the home directory. - If you want more isolation than "public subdomain + secret token," put this behind Tailscale instead of the public nginx route and skip the token entirely — probably the better long-term answer for a homelab tool like this.
Originheader is checked againstALLOWED_ORIGINS(defaulthttps://claude.ai) only when the header is present — non-browser clients (curl, Claude Code) don't send one, so they're unaffected.- Rotate
AUTH_TOKEN(openssl rand -hex 32, update.env,sudo systemctl restart ideavault-mcp) any time it's been displayed somewhere it shouldn't live long-term — e.g. pasted into a chat transcript. read_repo_file/list_repo_filesrefuse dotfiles,.env*,*.pem/*.key,id_rsa/id_ed25519, and filenames that look like secrets/credentials/wallet keypairs (several of these repos hold Solana keypairs as plain JSON). It's a filename-pattern blocklist, not content scanning — good enough for a personal tool, not a substitute for actually keeping keys out of these directories.- The Graph UI stores
AUTH_TOKENin the browser'slocalStorageafter you enter it once — same token as everywhere else, so anyone with it still has full read/write, not just graph-read access./api/graph/*is read-only, but the token itself isn't scoped down for the browser. Fine on a device you trust; don't paste the token into that prompt on a shared machine.
Roadmap ideas (not built yet)
- Bulk-seed notes for every repo in
list_reposthat hashasNote: false - A
build-mcp-appwidget for browsing/filtering ideas visually in chat - Swap the static token for OAuth (CIMD) if this ever needs to support more than one person
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。