AgentStack
MCP unreviewed MIT Self-run

Markdown Mcp

mcp-planexhq-markdown-mcp · by planexhq

MCP server giving AI agents structured, addressable access to a local markdown vault (Obsidian, Foam, plain folder).

No reviews yet
0 installs
14 views
0.0% view→install

Install

$ agentstack add mcp-planexhq-markdown-mcp

Open-source listing — not yet scanned by AgentStack. Follow the source repository for install instructions.

Security review

⚠ Flagged

1 finding(s); flagged for manual review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures
  • high Possible prompt-injection directive.

What it can access

  • Network access Used
  • Filesystem access Used
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Markdown Mcp? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

markdown-mcp

[](https://www.npmjs.com/package/markdown-mcp) [](https://nodejs.org) [](LICENSE) [](https://modelcontextprotocol.io)

Agents fed raw markdown end up paraphrasing from memory and fabricating citations. markdown-mcp gives them structured, addressable access to a local vault (Obsidian, Foam, plain folder) — heading-anchored fragments, BM25 search, stable IDs for re-fetching — so retrieved content stays verifiable.

Seven tools — get_vault_tree, get_file_outline, get_fragment, search, get_metadata, get_links, get_server_info — plus the note://{path} resource. Stdio transport, MCP spec 2025-06-18. Single-vault per process. v1 is read-only; write tools are planned for v2.

Features

  • BM25 full-text search with frontmatter filtering (tags, dates, custom fields). Two modes: query and filter-only.
  • Heading-anchored fragments addressed by heading_path or stable_id. Stale IDs recover via fuzzy fallback when files are edited.
  • Wikilink resolution + backlinks. Resolves Obsidian/Foam-style [[note]], [[note#section]], [[note^block]]; surfaces incoming links per file or section.
  • OpenAPI 3.x + AsyncAPI 3.x + opaque YAML. Set VAULT_EXTENSIONS=md,yaml,yml to admit YAML alongside markdown. OpenAPI 3.x specs expose one fragment per operation (GET /pets, POST /pets); AsyncAPI 3.x specs expose one fragment per operation (send userSignedUp, receive lightMeasured); other YAML files index opaquely with the parsed top-level surfaced as frontmatter for filter queries.
  • Prisma schema (PSL). Set VAULT_EXTENSIONS=md,prisma to admit .prisma files. Each top-level block (model, enum, view, type, datasource, generator) exposes one fragment (model User, enum Role, datasource db); /// doc comments surface as prose; @@map("X") surfaces as a Table: X line; datasource + generator blocks surface as frontmatter so nested-path filters work (fields["datasource.db.provider"].eq: "postgresql").
  • Async-reconcile startup. Server is up immediately; the index warms in the background. Bounded reads (outline, fragment, metadata) work during warmup.
  • No Obsidian plugin required. Reads the vault directly from disk; works with any markdown folder.
  • Fast. Sub-second warm restart on 10K-file vaults; search p95 ` (constant-time compare). Compatible with MCP hosts that speak Streamable HTTP per the 2025-06-18+ spec.

get_server_info.server.transport reports "http" (vs. "stdio") and surfaces the resolved bind_address + port so agents can self-verify.

Install (optional)

If you'd rather have a stable markdown-mcp binary on PATH (skips the ~1–2 s npx cold-cache fetch on first run):

npm install -g markdown-mcp
markdown-mcp --vault /path/to/your/vault

From source:

git clone https://github.com/planexhq/markdown-mcp.git
cd markdown-mcp
npm install
npm run build
node dist/index.js --vault /path/to/your/vault

Docker

Run markdown-mcp as an HTTP daemon in a container. The image is multi-stage (Debian-slim base, ~410 MB; the bulk is Node 22's runtime + better-sqlite3's compiled native binary) and runs as the non-root node user (UID 1000).

Quick start — HTTP daemon (Linux)

git clone https://github.com/planexhq/markdown-mcp.git
cd markdown-mcp
docker build -t markdown-mcp .

export MCP_AUTH_TOKEN=$(openssl rand -hex 32)
export VAULT_PATH=/absolute/path/to/your/vault
sudo chown -R 1000:1000 "$VAULT_PATH"   # see "Vault mount + permissions"
docker compose up -d

The server is reachable at http://127.0.0.1:3000/mcp with Authorization: Bearer $MCP_AUTH_TOKEN. Verify the initialize handshake:

curl -s -X POST http://127.0.0.1:3000/mcp \
  -H "Authorization: Bearer $MCP_AUTH_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","clientInfo":{"name":"smoke","version":"0"},"capabilities":{}}}'

Platform notes

  • Linux: network_mode: host makes the container share the host's loopback. --bind 127.0.0.1 inside the container = 127.0.0.1 on the host.
  • macOS / Windows (Docker Desktop): host networking routes through a Linux VM, so 127.0.0.1 inside the container is the VM's loopback — not the host's. The HTTP daemon won't be reachable from host applications until a future network-bind ADR pairs 0.0.0.0 binds with mandatory auth + rate limiting. Today: use stdio mode (below) or run on Linux.

Environment

| Variable | Purpose | Default | |----------|---------|---------| | MCP_AUTH_TOKEN | Bearer auth (HTTP only). Constant-time compare. | unset = no auth | | MCP_HTTP_SESSION_IDLE_MS | Idle-session reclaim threshold | 1 800 000 (30 min) | | MCP_HTTP_SESSION_SWEEP_MS | Idle-session sweep interval | 60 000 (60 s) | | VAULT_EXTENSIONS | Comma-separated indexable extensions | md | | VAULT_TOKENIZER | Token estimator backend | heuristic/content-aware-v1 |

Vault mount + permissions

The container writes a .markdown-mcp/ cache directory (lockfile + SQLite + WAL/SHM) inside the mounted vault. The node user (UID 1000) needs write access. Two options:

# Option A — chown the vault on the host (persistent deployments;
# works for both compose and ad-hoc docker run)
sudo chown -R 1000:1000 /absolute/path/to/your/vault
# Option B — override the container user (development convenience).
# In compose.yaml (HTTP daemon):
services:
  markdown-mcp:
    user: "${UID:-1000}:${GID:-1000}"

Then UID=$(id -u) GID=$(id -g) docker compose up -d. For ad-hoc stdio launches, pass --user to docker run -i (the -i keeps stdin open so JSON-RPC framing survives; without it the server exits immediately on STDIN_EOF):

docker run -i --rm --user "$(id -u):$(id -g)" \
  -v /absolute/path/to/vault:/vault \
  markdown-mcp --vault /vault

Arguments after the image name replace the Dockerfile's CMD (Docker semantics). The stdio example above intentionally drops --transport http; stdio is the CLI default.

Stdio mode (MCP client launches the container)

The same image supports stdio. Point your MCP host at docker run instead of node:

{
  "mcpServers": {
    "vault": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "-v", "/absolute/path/to/vault:/vault",
        "markdown-mcp",
        "--vault", "/vault"
      ]
    }
  }
}

-i keeps stdin open (required for JSON-RPC framing). Do not pass -t — a TTY corrupts the JSON-RPC frame stream.

Connect from an MCP host

Tested with Claude Desktop, Claude Code, Cursor, and Windsurf. Any MCP-compatible host that speaks stdio + protocol 2025-06-18 works.

> Windows users, for any of the host configs below: replace "command": "npx" with "command": "cmd" and prepend "/c", "npx" to args — npm's .cmd shim isn't directly spawnable from JSON config without it.

Claude Desktop / Claude Code

Add to your MCP config:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json
  • Linux: ~/.config/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "my-vault": {
      "command": "npx",
      "args": ["-y", "markdown-mcp", "--vault", "/Users/you/Documents/Vault"]
    }
  }
}

The -y flag skips npx's "Ok to proceed?" prompt so the host can spawn the server non-interactively. If you globally installed markdown-mcp, swap command: "npx" + the -y / markdown-mcp args for command: "markdown-mcp" and drop the first two args.

Cursor

Config file: ~/.cursor/mcp.json (global) or .cursor/mcp.json (project). Same shape as Claude Desktop:

{
  "mcpServers": {
    "my-vault": {
      "command": "npx",
      "args": ["-y", "markdown-mcp", "--vault", "/Users/you/Documents/Vault"]
    }
  }
}

VS Code

Config file: .vscode/mcp.json (workspace) or via MCP: Open User Configuration (global). Note the top-level key is servers, not mcpServers:

{
  "servers": {
    "my-vault": {
      "command": "npx",
      "args": ["-y", "markdown-mcp", "--vault", "${workspaceFolder}/vault"]
    }
  }
}

Other MCP-compatible hosts

Windsurf, Goose, Zed, and other stdio-based MCP hosts accept the same command + args shape — adapt the JSON key (mcpServers vs servers) per the host's docs.

CLI flags

| Flag | Purpose | |---|---| | --vault | Vault directory (required). Absolute or relative. | | --polling | Force fs polling instead of native FS events. Use on network mounts (NFS/SMB) and platforms where chokidar's native events fire unreliably. ~10× slower; only enable when needed. | | --include-hidden | Include dot-prefixed files and directories on every surface. Default excludes them. All-or-nothing per server. | | --prose-only | Suppress structuredContent on every tool response so the markdown prose body is the sole channel. Useful for token-constrained LLM-consumer workflows. get_server_info.server.prose_only reflects the flag for agent self-verification. | | --transport | stdio (default) or http. HTTP speaks Streamable HTTP per MCP spec 2025-06-18+; one process serves multiple concurrent sessions sharing one warm index. See [Transports](#transports). | | --port | HTTP listener port (default 3000; only with --transport http). Use 0 for OS-assigned. | | --bind | HTTP bind address (default 127.0.0.1; only with --transport http). v1 supports loopback only (127.0.0.0/8, ::1, localhost); anything else exits 2 at startup. | | -h, --help | Show usage and exit. |

Environment variables

| Variable | Purpose | |---|---| | VAULT_EXTENSIONS | Comma-separated list of file extensions treated as parseable notes (no leading dot, case-insensitive). Default: md. Examples: md,markdown, md,mdx, md,yaml,yml, md,prisma. Gates note://, get_vault_tree resource links, and every direct-read tool. YAML files route through OpenAPI 3.x synthesis, then AsyncAPI 3.x synthesis, then opaque emission. .prisma files route through Prisma schema synthesis. Changing this value forces a one-time cold rescan on next start. | | MCP_AUTH_TOKEN | Optional bearer token for HTTP transport. When set, every HTTP request must carry Authorization: Bearer (constant-time crypto.timingSafeEqual against the configured value). Unset → no auth (loopback-trust model). Stdio is unaffected. Read once at startup; restart to rotate. |

Tools

| Tool | Returns | |---|---| | get_vault_tree | Paginated DFS over the vault. Files, directories, dfsrank for stable cursors. | | get_file_outline | Full heading tree + block-ID index for one file (not paginated). | | get_fragment | Anchor-resolved fragment: heading, block, preamble, or whole file. Stableid with fuzzy stale-recovery. | | search | BM25 full-text + structured frontmatter filter. Two modes: query and filter-only. Discriminated-union response. | | get_metadata | Parsed YAML frontmatter for one file. | | get_links | Outgoing wikilinks + incoming backlinks. Optional narrowing by headingpath or stableid. | | get_server_info | Identity / health snapshot for agent self-verification: server version, vault root_hash, index state + freshness, algorithm IDs, registered tools. Zero input. |

The note://{path} Resource returns the raw on-disk markdown (frontmatter included) so hosts can stream a literal note when a parsed fragment isn't what they want.

OpenAPI 3.x YAML (when admitted via VAULT_EXTENSIONS): get_file_outline returns one node per operation (GET /pets); OpenAPI 3.1 webhooks join paths as Webhook: headings. get_fragment returns a synthesized prose rendering (summary, description, parameter prose) plus a compact JSON fence of the operation object; get_metadata returns the whole top-level spec so nested-path filters (fields["info.version"].eq) work directly. note://api/petstore.yaml returns the literal on-disk YAML with mimeType: application/yaml. Operations with a stable operationId keep their stable_id across path renames (e.g. /v1/pets/api/v2/pets). Wikilinks into YAML are not yet resolved; other YAML files index opaquely (whole source searchable, top-level exposed as frontmatter).

AsyncAPI 3.x YAML (same admittance gate): get_file_outline returns one node per top-level operation (send userSignedUp, receive lightMeasured); get_fragment returns synthesized prose with the resolved channel address, message list, reply info, tags, plus a compact JSON fence of the full operation object. Intra-document $ref (#/channels/, #/channels//messages/) is resolved; external $ref renders verbatim. ## Channels and ## Components catch-all sections keep large specs navigable. AsyncAPI 2.x (with nested publish/subscribe) is deferred and falls through to opaque YAML emission.

Prisma schema (admit via VAULT_EXTENSIONS=md,prisma): get_file_outline returns one node per top-level block — model User, enum Role, view UserStats, type Address, datasource db, generator client. get_fragment renders block-level /// doc comments as prose, fields as a Fields: bullet list (with field-level /// as suffix), @@map("X") as a Table: X line, block-level @@id / @@index / @@unique as a Block attributes: section, plus a compact JSON fence of the full AST subtree. get_metadata returns { datasource, generator } so nested-path filters work (fields["datasource.db.provider"].eq: "postgresql"). note://schema.prisma returns the literal on-disk PSL with mimeType: text/x-prisma. Wikilinks INTO .prisma are not yet resolved (same as YAML); free-floating /// not attached to any block surfaces in a trailing ## Schema notes section.

Typical agent flow

┌──────────────────┐   ┌──────────────────┐   ┌──────────────────┐
│  get_vault_tree  │──▶│ get_file_outline │──▶│   get_fragment   │
│  browse / scope  │   │  pick a heading  │   │  read the section│
└──────────────────┘   └──────────────────┘   └──────────────────┘
         │                                            ▲
         │             ┌──────────────────┐           │
         └────────────▶│      search      │───────────┘
                       │  query + filter  │
                       └──────────────────┘
        get_metadata = frontmatter only · get_links = follow citations

Examples

Inputs are the tool arguments the host passes; outputs are abbreviated tool results (the _meta envelope is omitted for brevity; nextCursor is shown when relevant).

Every tool returns two parallel channels: structuredContent (the typed JSON shown in each example below) for programmatic consumers, and content[0].text (a compact markdown rendering of the same data) for LLM consumers that read the prose channel directly. Pass [--prose-only](#cli-flags) to drop structuredContent and keep only the prose body. The prose channel uses a few rendering conventions:

  • File-with-heading addresses join on (e.g. notes/auth.md › OAuth2). When a filename contains , the file portion is always wrapped in «…» so the boundary stays unambiguous — both standalone (e.g. [file] «notes/foo › bar.md» in get_vault_tree, «notes/foo › bar.md» · file in search file/preamble rows) and inside addresses («notes/foo › bar.md» › OAuth2). Passing either form back through a file: argument round-trips to the same path.
  • Wikilink aliases are quoted JSON-style ("alias text"), so embedded " and \ are escaped (\", \\).
  • get_metadata renders frontmatter as a YAML block whose entire content[0].text reparses as valid YAML — header and meta footer are #-prefixed comments.

get_vault_tree — browse the vault

// in
{ "path": "projects", "pageSize": 5 }

/

…

## Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [planexhq](https://github.com/planexhq)
- **Source:** [planexhq/markdown-mcp](https://github.com/planexhq/markdown-mcp)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.