Skip to main content
Glama
nadimhoss

mcp-notes-server

by nadimhoss

mcp-notes-server

An MCP server that gives an AI agent a searchable markdown notes vault: six tools over a flat directory of .md files with YAML-style frontmatter.

Notes stay plain markdown on disk. Nothing is locked in a database, so the same files work with your editor, grep, and git.

~/notes/
├── pad-thai.md
├── sourdough-starter.md
└── weekly-review.md

Tools

Tool

Description

create_note

Create a note. Returns the generated slug.

read_note

Read one note in full, by slug.

list_notes

Summaries (no bodies), newest first, optionally filtered by tag.

search_notes

Ranked full-text search with snippets.

update_note

Patch title / body / tags. The slug never changes.

delete_note

Delete a note by slug.

Install

npm install
npm run build

Use it with an MCP client

Add it to your client's server config — for Claude Desktop, claude_desktop_config.json:

{
  "mcpServers": {
    "notes": {
      "command": "node",
      "args": ["/absolute/path/to/mcp-notes-server/dist/src/index.js", "--vault", "/absolute/path/to/notes"]
    }
  }
}

The vault directory is resolved in this order: --vault <dir>, then $NOTES_VAULT, then ~/notes. It is created on startup if it does not exist.

Design notes

Slugs are the identity, and they are validated. A note lives at <vault>/<slug>.md, and every slug is checked against /^[a-z0-9]+(?:-[a-z0-9]+)*$/ before it reaches the filesystem. That is what makes read_note({slug: "../../.ssh/id_rsa"}) impossible rather than merely unlikely — an allowlist, not an escaping pass. Slugs are also stable across updates: renaming the file when the title changes would invalidate any slug the model is still holding from an earlier tool call.

Tool errors are data, not exceptions. "No note with slug X" comes back as a normal tool result with isError: true, so the model reads it and corrects itself. If it were thrown, the client would see a protocol error, which the model cannot recover from. Genuine bugs (anything that is not a VaultError) are still thrown, so they stay loud.

Search is weighted term frequency. A term in the title counts triple and a term in a tag counts double, so searching sourdough ranks a note about sourdough above one that mentions it in passing. It is deliberately simple — no index to keep in sync, and a vault of a few thousand notes scans in milliseconds.

The vault knows nothing about MCP. src/vault.ts is plain filesystem code, src/server.ts is the MCP binding, and src/index.ts is the stdio entry point. That split is why the vault can be tested directly and the server can be tested through a real MCP client over an in-memory transport, with no subprocess and no mocks.

Development

npm test          # 46 tests, vitest
npm run typecheck # tsc --noEmit
npm run build     # emit to dist/

Tests cover the vault directly (test/vault.test.ts) and the server end to end through a real MCP Client over InMemoryTransport (test/server.test.ts), so tool schemas, argument validation, and result shapes are all exercised — not just the logic behind them.

License

MIT