https://github.com/psenger/obsidian-markdown-lint-mcp-server
Stdio MCP server for Claude Code that lints Markdown, validates front matter against JSON Schema, and renders Mermaid diagrams to SVG, all inside Docker.
https://github.com/psenger/obsidian-markdown-lint-mcp-server
claude claude-code docker front-matter json-schema markdown markdownlint mcp mermaid model-context-protocol obsidian stdio svg typescript
Last synced: 3 days ago
JSON representation
Stdio MCP server for Claude Code that lints Markdown, validates front matter against JSON Schema, and renders Mermaid diagrams to SVG, all inside Docker.
- Host: GitHub
- URL: https://github.com/psenger/obsidian-markdown-lint-mcp-server
- Owner: psenger
- License: mit
- Created: 2026-06-08T04:23:05.000Z (13 days ago)
- Default Branch: main
- Last Pushed: 2026-06-15T23:55:40.000Z (6 days ago)
- Last Synced: 2026-06-18T03:33:56.577Z (3 days ago)
- Topics: claude, claude-code, docker, front-matter, json-schema, markdown, markdownlint, mcp, mermaid, model-context-protocol, obsidian, stdio, svg, typescript
- Language: TypeScript
- Size: 516 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Security: SECURITY.md
Awesome Lists containing this project
README
# obsidian-markdown-lint-mcp-server
**An MCP server that lints, validates, and renders your Obsidian vault markdown — all inside Docker.**
[](https://nodejs.org) [](https://github.com/modelcontextprotocol/typescript-sdk) [](LICENSE)
[Tools](#tools) • [Quick Start](#quick-start) • [Configuration](#configuration) • [Schemas](#front-matter-schemas) • [Development](#development)
---
Run markdown linting, front matter validation, and Mermaid-to-SVG rendering as MCP tools callable from Claude Code. Docker isolates Chromium and Node.js from your host machine. The server is stateless — it processes content you send and returns results; Claude Code handles all disk I/O.
This is a **stdio MCP server**: Claude Code launches it as a subprocess (`docker run -i --rm`) and talks to it over stdin/stdout. There is no port and no long-running container — the container starts when a session opens and is removed when it ends.
## Why Docker
Mermaid rendering requires a Chromium browser (via Puppeteer). Running that, plus dozens of Node.js dependencies, directly on your laptop is a valid security concern. This server packages everything inside a container. Your vault never mounts into it; content travels as strings.
## Tools
| Tool | What it does |
|-------|----|
| `lint_markdown` | Runs markdownlint on content, returns errors and a corrected version |
| `validate_front_matter` | Validates YAML front matter against a JSON Schema you pass in |
| `render_mermaid_diagrams` | Renders all ` ```mermaid ``` ` blocks to SVG, replaces them with GitHub image links, embeds the original Mermaid source as base64 in each SVG's `` |
| `extract_mermaid_from_svg` | Reads a rendered SVG and returns the original Mermaid source as a code block, ready to edit |
The render/extract pair supports a full edit cycle: render → view SVG in Obsidian → extract → edit source → re-render.
## Quick start
**Prerequisites:** Docker Desktop, Node.js ≥ 20, Claude Code.
```bash
git clone
cd obsidian-markdown-lint-mcp-server
npm install
npm run build # compile TypeScript → dist/
docker build -t obsidian-markdown-lint-mcp . # or: docker compose build
```
Register the server with Claude Code. Claude Code runs `docker run -i --rm` itself, per session — you do **not** start a container yourself.
**Per-project (recommended for a vault)** — run this from your vault root; it writes a committable `.mcp.json`:
```bash
claude mcp add obsidian-markdown-lint -s project -- docker run -i --rm obsidian-markdown-lint-mcp
```
The resulting `.mcp.json`:
```json
{
"mcpServers": {
"obsidian-markdown-lint": {
"command": "docker",
"args": ["run", "-i", "--rm", "obsidian-markdown-lint-mcp"]
}
}
}
```
**Global** — make it available in every project (`-s user` writes to `~/.claude.json`):
```bash
claude mcp add obsidian-markdown-lint -s user -- docker run -i --rm obsidian-markdown-lint-mcp
```
Verify with `claude mcp list` (should show `obsidian-markdown-lint` → `connected`), then start a **new** Claude Code session — tools are discovered at session start. The four tools are now available.
> The `-i` flag is required: it keeps the container's stdin open for the JSON-RPC stream. Without it the container gets EOF and exits immediately. Do **not** use `docker compose up` for this server — it is a stdio subprocess, not a long-running HTTP service.
### Hardening (optional)
The server needs no network, no writable root filesystem, and little memory, so you can lock the container down. `--network none` and `--security-opt no-new-privileges` are always safe (the image skips the Puppeteer download and renders offline):
```bash
claude mcp add obsidian-markdown-lint -s project -- docker run -i --rm --network none --security-opt no-new-privileges --memory 2g obsidian-markdown-lint-mcp
```
For stricter isolation, add a read-only root with a writable temp dir and drop all Linux capabilities. Chromium renders into `/tmp` because the server launches it with `--disable-dev-shm-usage`, so the `--tmpfs /tmp` is required. Verify a Mermaid render still succeeds under these flags before relying on them, since Chromium's writable paths vary by version:
```bash
docker run -i --rm --network none --security-opt no-new-privileges --read-only --tmpfs /tmp --cap-drop ALL --memory 2g obsidian-markdown-lint-mcp
```
## Configuration
### Vault layout
The server reads nothing from disk directly. Claude Code reads your vault files and passes content as strings. Place these config files at your vault root:
```
vault/
.markdownlint.json ← linting rules (optional; defaults to markdownlint defaults)
.schemas/
_shared.json ← shared field definitions (reference; not loaded at runtime)
article.json
how-to.json
technical.json
deep-research.json
strategy.json
meeting.json
brainstorming.json
```
Pre-built schemas for all seven note types are included in this repo's `.schemas/` directory. Copy them to your vault root. The seven type schemas are self-contained, so `validate_front_matter` only needs the one matching the document's `type`; `_shared.json` is the source the type schemas are built from and is kept for maintenance.
### CLAUDE.md for your vault
Add a `CLAUDE.md` to your vault project that tells Claude how to use the server. Minimum viable example:
```markdown
## MCP: obsidian-markdown-lint-mcp-server
Vault attachments directory: attachments
Schemas directory: .schemas/
Linting config: .markdownlint.json
When asked to "process a vault file":
1. Read the file
2. Read .markdownlint.json and call lint_markdown
3. Read the type field from front matter, read .schemas/{type}.json, call validate_front_matter
4. Call render_mermaid_diagrams with attachments_dir="attachments" and the document title
5. Write the modified_content back to the file
6. Write each SVG from the svgs array to its path field (decoded from base64)
SVG files contain the original Mermaid source base64-encoded in .
To edit a diagram: call extract_mermaid_from_svg with the SVG content,
edit the returned mermaid_block, then re-run render_mermaid_diagrams.
```
### Mermaid front matter options
Control rendering per-document:
```yaml
mermaid_theme: dark # default | dark | neutral | forest (default: default)
mermaid_background: white # any CSS color or "transparent" (default: white)
```
After rendering, the server adds `mermaid_svg_source: base64-embedded` to the front matter so Claude knows SVGs in this document contain extractable source.
## Front matter schemas
Seven note types are supported. Each schema lives at `.schemas/{type}.json`.
| Type | Extra required fields |
|---|---|
| `article` | core only |
| `how-to` | core only |
| `technical` | `system`, `component` |
| `deep-research` | `sources` (array, min 1) |
| `strategy` | `related`, `prepared_for`, `quarter` |
| `meeting` | `meeting_date`, `attendees` (array, min 1) |
| `brainstorming` | core only |
Core required fields on every type: `type`, `title`, `author`, `category`, `tags`, `description`, `summary`, `status`, `version`, `date_created`, `date_updated`.
The `type` field is the discriminator. Claude reads it from the front matter to select the right schema file before calling `validate_front_matter`.
## How SVG embedding works
Each rendered SVG contains the original Mermaid source, base64-encoded, inside the SVG's `` element:
```xml
Zmxvd2NoYXJ0IExSCiAgICBBIC0tPiBCCg==
```
Base64 avoids all whitespace normalization issues — Mermaid is whitespace-sensitive and attribute-value normalization in XML would corrupt indentation. The `extract_mermaid_from_svg` tool decodes this and returns a ready-to-use ` ```mermaid ``` ` block.
## SVG output paths
Diagrams render to `{attachments_dir}/{document-title-slug}/{diagram-type}-{n}.svg`.
For a document titled "System Architecture" with `attachments_dir = "attachments"` and two flowchart blocks, you get:
```
attachments/system-architecture/flowchart-1.svg
attachments/system-architecture/flowchart-2.svg
```
The markdown is updated to:
```markdown

```
Standard GitHub-flavored markdown — renders in Obsidian, GitHub, and any standard markdown viewer.
## Testing
Three layers, all runnable from the repo without building the Docker image:
```bash
npm test # 52 Jest unit tests (ESM), enforced coverage thresholds
npm run eval # tool-correctness evals against the compiled functions
npm run snapshot # end-to-end fixture snapshots (requires a real Chromium)
```
**Unit tests** (`tests/unit/`) call the tool functions directly. Coverage is gated (90% lines/functions/statements, 85% branches); `src/server.ts` and `src/create-server.ts` are excluded as pure wiring and are covered separately through an in-memory MCP transport. Mermaid rendering is tested with an injected browser/renderer, so no Chromium is needed here. Use `npm run test:coverage` for the lcov/HTML report.
**Evals** (`tests/evals/`) check tool correctness against the compiled functions (no Docker). Output is JSON with `summary.passed`, `summary.failed`, and a `results` array; exit code is `1` if any eval fails. The suite covers all four tools: lint detection, schema validation (pass and fail cases), Mermaid rendering with a mock browser, and SVG round-trip extraction. In Claude Code, say *"run evals"* or *"evaluate the tools"* and it runs `npm run eval` and reports the results.
**Snapshots** (`tests/snapshot/`) prove that each input under `test-obsidian-vault/original/` reproduces its committed `test-obsidian-vault/snap-shot/` output through the real tool pipeline. Unlike the evals, `test-1` launches Puppeteer for an actual render, so this needs a real Chromium and is kept out of `npm test`. SVG geometry is not byte-compared (it varies by Mermaid, Chromium, and font version); the deterministic projection is, namely the modified markdown, the file layout, and the embedded Mermaid source. Output is JSON; exits non-zero on any failure.
## JSON Schema format
Each file in `.schemas/` is a self-contained [JSON Schema draft-07](https://json-schema.org/) object. `validate_front_matter` parses the document's YAML front matter and validates the resulting object against the schema you pass in. A trimmed view of `.schemas/article.json`:
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Article",
"type": "object",
"required": ["type","title","author","category","tags","description","summary","status","version","date_created","date_updated"],
"properties": {
"type": { "const": "article" },
"title": { "type": "string", "minLength": 1 },
"category": { "type": "string", "enum": ["Software Development", "Security", "DevOps", "..."] },
"tags": { "type": "array", "items": { "type": "string", "pattern": "^[a-z0-9][a-z0-9_\\-/]*$" }, "minItems": 4, "maxItems": 12 },
"status": { "type": "string", "enum": ["published", "draft", "in-progress"] },
"version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+$" },
"date_created": { "type": "string", "format": "date" }
},
"additionalProperties": false
}
```
See `.schemas/article.json` for the complete schema, including the optional fields it also permits (`aliases`, `subtitle`, `source`, `sources`, `reviewers`, `signoff`, `mermaid_theme`, `mermaid_background`, `mermaid_svg_source`). Every schema sets `additionalProperties: false`, so any front matter key not declared in the schema is a validation error; add a key to the schema before using it in notes.
**Important:** Date fields must be quoted strings in YAML (`date_created: "2026-01-01"`) because YAML auto-converts unquoted `YYYY-MM-DD` values to JavaScript `Date` objects, which fail the `type: string` check.
The per-type required fields are listed in [Front matter schemas](#front-matter-schemas) above.
## Development
```bash
npm install
npm run build # compile TypeScript → dist/
```
TypeScript source is in `src/`. After changing code, rebuild the image so the next Claude Code session picks it up:
```bash
npm run build && docker build -t obsidian-markdown-lint-mcp . # or: docker compose build
```
Then start a new Claude Code session (stdio tools are loaded at session start).
To smoke-test the container by hand without Claude Code, drive it over stdio:
```bash
docker compose run --rm obsidian-markdown-lint-mcp
# then paste a JSON-RPC line, e.g. an initialize request, and read the reply
```
## Project structure
```
src/
server.ts stdio entry point (StdioServerTransport bootstrap)
create-server.ts builds the McpServer and registers the 4 tools
tools/
lint.ts lint_markdown implementation
validate.ts validate_front_matter implementation
mermaid.ts render_mermaid_diagrams + extract_mermaid_from_svg
lib/
frontmatter.ts gray-matter parse/update helpers
svg-metadata.ts base64 embed/extract helpers
.schemas/ JSON Schema files for all 7 note types
Dockerfile stdio server image (Chromium for Mermaid)
docker-compose.yml build/tag helper (not `up` — see comments)
```
## License
[MIT](LICENSE) © 2026 Philip Senger
---
**Lint, validate, and render your Obsidian vault markdown without touching your host system.**
[Report a bug](../../issues) • [Request a feature](../../issues)