obsidian-mcp-server 3.7.0 → 3.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +31 -11
- package/CLAUDE.md +31 -11
- package/Dockerfile +10 -3
- package/README.md +30 -91
- package/changelog/3.7.x/3.7.1.md +40 -0
- package/changelog/3.7.x/3.7.2.md +34 -0
- package/dist/config/server-config.js +1 -1
- package/dist/config/server-config.js.map +1 -1
- package/dist/index.js +13 -32
- package/dist/index.js.map +1 -1
- package/dist/instructions.d.ts +20 -0
- package/dist/instructions.d.ts.map +1 -0
- package/dist/instructions.js +40 -0
- package/dist/instructions.js.map +1 -0
- package/dist/mcp-server/tools/definitions/_shared/markdown.d.ts +14 -0
- package/dist/mcp-server/tools/definitions/_shared/markdown.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/_shared/markdown.js +17 -0
- package/dist/mcp-server/tools/definitions/_shared/markdown.js.map +1 -0
- package/dist/mcp-server/tools/definitions/_shared/suggest-paths.d.ts +39 -0
- package/dist/mcp-server/tools/definitions/_shared/suggest-paths.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/_shared/suggest-paths.js +63 -2
- package/dist/mcp-server/tools/definitions/_shared/suggest-paths.js.map +1 -1
- package/dist/mcp-server/tools/definitions/index.d.ts +127 -116
- package/dist/mcp-server/tools/definitions/index.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.d.ts +6 -0
- package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.js +15 -6
- package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.js +5 -4
- package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-execute-command.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-execute-command.tool.js +2 -1
- package/dist/mcp-server/tools/definitions/obsidian-execute-command.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-get-note.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-get-note.tool.js +51 -40
- package/dist/mcp-server/tools/definitions/obsidian-get-note.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-list-commands.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-list-commands.tool.js +7 -4
- package/dist/mcp-server/tools/definitions/obsidian-list-commands.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-list-notes.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-list-notes.tool.js +7 -6
- package/dist/mcp-server/tools/definitions/obsidian-list-notes.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.js +3 -2
- package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-manage-frontmatter.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-manage-frontmatter.tool.js +13 -9
- package/dist/mcp-server/tools/definitions/obsidian-manage-frontmatter.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-manage-tags.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-manage-tags.tool.js +9 -5
- package/dist/mcp-server/tools/definitions/obsidian-manage-tags.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.js +2 -1
- package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.js +5 -4
- package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.js +12 -4
- package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.d.ts +26 -2
- package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.js +61 -34
- package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.d.ts +5 -0
- package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.js +24 -6
- package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.js.map +1 -1
- package/dist/services/obsidian/frontmatter-ops.d.ts +15 -1
- package/dist/services/obsidian/frontmatter-ops.d.ts.map +1 -1
- package/dist/services/obsidian/frontmatter-ops.js +55 -10
- package/dist/services/obsidian/frontmatter-ops.js.map +1 -1
- package/dist/services/obsidian/jsonlogic-evaluator.d.ts +83 -0
- package/dist/services/obsidian/jsonlogic-evaluator.d.ts.map +1 -0
- package/dist/services/obsidian/jsonlogic-evaluator.js +192 -0
- package/dist/services/obsidian/jsonlogic-evaluator.js.map +1 -0
- package/dist/services/obsidian/jsonlogic-worker.d.ts +53 -0
- package/dist/services/obsidian/jsonlogic-worker.d.ts.map +1 -0
- package/dist/services/obsidian/jsonlogic-worker.js +237 -0
- package/dist/services/obsidian/jsonlogic-worker.js.map +1 -0
- package/dist/services/obsidian/obsidian-service.d.ts +56 -8
- package/dist/services/obsidian/obsidian-service.d.ts.map +1 -1
- package/dist/services/obsidian/obsidian-service.js +254 -48
- package/dist/services/obsidian/obsidian-service.js.map +1 -1
- package/dist/services/obsidian/path-policy.d.ts +33 -5
- package/dist/services/obsidian/path-policy.d.ts.map +1 -1
- package/dist/services/obsidian/path-policy.js +99 -17
- package/dist/services/obsidian/path-policy.js.map +1 -1
- package/dist/services/obsidian/types.d.ts +12 -3
- package/dist/services/obsidian/types.d.ts.map +1 -1
- package/manifest.json +2 -2
- package/package.json +20 -15
- package/server.json +5 -5
package/AGENTS.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Agent Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** obsidian-mcp-server
|
|
4
|
-
**Version:** 3.7.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.
|
|
4
|
+
**Version:** 3.7.2
|
|
5
|
+
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.18`
|
|
6
6
|
**Engines:** Bun ≥1.4.0, Node ≥24.0.0
|
|
7
|
-
**MCP SDK:** `@modelcontextprotocol/server` ^2.
|
|
8
|
-
**Zod:** ^4.6.
|
|
7
|
+
**MCP SDK:** `@modelcontextprotocol/server` ^2.4.0
|
|
8
|
+
**Zod:** ^4.6.7
|
|
9
9
|
|
|
10
10
|
> **Read the framework docs first:** `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` contains the full API reference — builders, Context, error codes, exports, patterns. This file covers server-specific conventions only.
|
|
11
11
|
|
|
@@ -39,7 +39,7 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
|
|
|
39
39
|
- **Secrets in env vars only.** `OBSIDIAN_API_KEY` is required; never hardcoded.
|
|
40
40
|
- **Cut noise.** Add only what earns its place: no speculative generality, no guards for states the framework already prevents (Zod-validated params, classified errors), no abstraction until a third caller proves it, no option nothing sets.
|
|
41
41
|
- **Command-palette tools are opt-in.** `obsidian_list_commands` and `obsidian_execute_command` are callable only when `OBSIDIAN_ENABLE_COMMANDS=true` — Obsidian commands are opaque and can be destructive. When the flag is unset, the entry point wraps both with `disabledTool()` so they're absent from `tools/list` (LLM can't invoke) but visible in the operator-facing manifest with a hint to enable them.
|
|
42
|
-
- **Path-policy gating goes through `PathPolicy`.** Every path-taking method on `ObsidianService` calls `policy.assertReadable` / `assertWritable` before the upstream HTTP call; `listFiles` gates with `assertListable` and filters entries with `filterListing` (readable entries plus folders on the way to the scope), so every listing consumer inherits the scope; `obsidian_search_notes`
|
|
42
|
+
- **Path-policy gating goes through `PathPolicy`.** Every path-taking method on `ObsidianService` calls `policy.assertReadable` / `assertWritable` before the upstream HTTP call; `listFiles` gates with `assertListable` and filters entries with `filterListing` (readable entries plus folders on the way to the scope), so every listing consumer inherits the scope; `obsidian_search_notes` filters hits via `svc.policy.filterReadable`. Under `policy.restrictsReads`, `searchJsonLogic` never sends the caller's tree upstream: one `/search/` guarded by `readScopePattern` fetches the readable notes' contexts (plus one body fetch per batch of readable notes when the tree names `content`), `filterReadable` gates them, `keepReadable` reduces `links` / `backlinks` / `unresolvedLinks`, and `createJsonLogicEvaluation` runs the tree on a worker thread with the plugin's operators, bounded by depth, a per-note build budget, a per-search kept-result budget, and a deadline (`logic_too_costly`; a note too deep to hand to the worker is `note_too_deep`), so no hit, result, or error reads a note outside the scope. Link targets are still Obsidian's vault-wide resolution, so the reduced link fields can shift when an out-of-scope note appears or disappears (#165); docs state that bound, never a stronger one. `listTags` reads per-note tags from the same readable contexts, with no evaluation. An `active` / `periodic` target is gated on the path it resolves to, under the target's label: `resolveTarget` / `pinTarget` return a `PathTarget` whose `resolvedFrom` the asserts take, so a denial names `(active file)` or `daily note for 2026-10-01`, never a path the caller did not send, and a tool that reads a target then writes it writes to that pinned path rather than resolving the target again. Don't bypass this — `OBSIDIAN_READ_PATHS` / `OBSIDIAN_WRITE_PATHS` / `OBSIDIAN_READ_ONLY` are the single chokepoint, and `path_forbidden` is declared on every path-taking tool's `errors[]` contract.
|
|
43
43
|
- **Close the loop on issues.** When implementing work tracked by a GitHub issue, comment on the issue with what landed and close it. Do both — a comment without a close leaves stale issues open; a close without a comment leaves no record of what shipped. The comment is for future readers — state the concrete changes, not the conversation that produced them.
|
|
44
44
|
|
|
45
45
|
---
|
|
@@ -274,19 +274,25 @@ Available factories: `notFound`, `validationError`, `forbidden`, `unauthorized`,
|
|
|
274
274
|
```text
|
|
275
275
|
src/
|
|
276
276
|
index.ts # createApp() entry point — registers tools/resources, inits Obsidian service
|
|
277
|
+
instructions.ts # buildInstructions() — server `instructions`, effective path scope included
|
|
277
278
|
config/
|
|
278
279
|
server-config.ts # OBSIDIAN_* env vars (Zod schema)
|
|
279
280
|
services/
|
|
280
281
|
obsidian/
|
|
281
282
|
obsidian-service.ts # Local REST API client (init/accessor pattern)
|
|
282
|
-
|
|
283
|
+
path-policy.ts # PathPolicy — OBSIDIAN_READ_PATHS / WRITE_PATHS / READ_ONLY gate
|
|
284
|
+
jsonlogic-deps.d.ts # Type declarations for json-logic-js and glob-to-regexp
|
|
285
|
+
jsonlogic-evaluator.ts # Scoped JSONLogic evaluation: bounds, worker hand-off, logic_* errors
|
|
286
|
+
jsonlogic-worker.ts # Worker entry: the plugin's JSONLogic operators, hardened and budgeted
|
|
287
|
+
frontmatter-ops.ts # YAML frontmatter parse/serialize/edit helpers + inline tag reader + the wikilink span rule (`wikilinkCloser`) `obsidian_get_note` imports
|
|
283
288
|
markdown-blocks.ts # Block structure (code, HTML, math, tables) for inline tag detection
|
|
284
289
|
patch-instruction.ts # markdown-patch 1.x headers / 2.0 instructions, format negotiation, 2.0 map flattening
|
|
285
290
|
section-extractor.ts # Heading/block/frontmatter section extraction
|
|
286
|
-
types.ts # Domain types (NoteJson, NoteTarget, etc.)
|
|
291
|
+
types.ts # Domain types (NoteJson, NoteTarget, PathTarget, etc.)
|
|
287
292
|
mcp-server/
|
|
288
293
|
tools/definitions/
|
|
289
294
|
_shared/schemas.ts # Shared TargetSchema + SectionSchema reused across tools
|
|
295
|
+
_shared/suggest-paths.ts # Case-mismatch fallback and suggestions for reads; names the existing note on a `case_variant_exists` write
|
|
290
296
|
index.ts # read/write/command tool sets + buildSearchNotesTool (Omnisearch-aware) and buildDeleteNoteTool (elicitation mode) factories
|
|
291
297
|
obsidian-*.tool.ts # 14 tool definitions (12 base + 2 opt-in command-palette pair)
|
|
292
298
|
resources/definitions/
|
|
@@ -313,9 +319,18 @@ src/
|
|
|
313
319
|
|
|
314
320
|
## Skills
|
|
315
321
|
|
|
316
|
-
|
|
322
|
+
Two skill trees can live at the project root, written for different readers:
|
|
317
323
|
|
|
318
|
-
|
|
324
|
+
| Tree | For | Written by |
|
|
325
|
+
|:-----|:----|:-----------|
|
|
326
|
+
| `framework-skills/` | The agent developing this server — framework skills (`metadata.audience: external`) plus any development-time skills of the server's own | `init` and the `maintenance` sync (framework skills); you (your own) |
|
|
327
|
+
| `skills/` | The agents that install the server and call its tools — tool chains, identifier sources, gates | You, deliberately, with the `add-skill` skill. Absent here: this server publishes none, so `createApp()` gets no `skills` option |
|
|
328
|
+
|
|
329
|
+
Development skills are modular instructions. Read them directly when a task matches — e.g., `framework-skills/add-tool/SKILL.md` when adding a tool. `bun run list-skills` prints the full registry. Their directory is deliberately not `skills/`: Claude Code and Codex auto-load a plugin's root `skills/`, so a server that ships `.claude-plugin/` or `.codex-plugin/` would hand them to every agent that installs it.
|
|
330
|
+
|
|
331
|
+
A published skill in root `skills/` reaches plugin installs and the `.mcpb` bundle as-is, ships in the npm package and the Docker image (`package.json` `files` and the Dockerfile already carry `skills/`), and reaches MCP clients when the server opts in with `createApp({ skills: 'skills' })`. `lint:packaging` validates the tree.
|
|
332
|
+
|
|
333
|
+
**Agent skill directory:** Copy `framework-skills/` into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). Skills then load as context without referencing `framework-skills/` paths. After framework updates, run the `maintenance` skill — Phase B re-syncs the agent directory.
|
|
319
334
|
|
|
320
335
|
Available skills:
|
|
321
336
|
|
|
@@ -329,6 +344,7 @@ Available skills:
|
|
|
329
344
|
| `add-prompt` | Scaffold a new prompt definition |
|
|
330
345
|
| `add-service` | Scaffold a new service integration |
|
|
331
346
|
| `add-test` | Scaffold test file for a tool, resource, or service |
|
|
347
|
+
| `add-skill` | Author a skill the server publishes in root `skills/` for the agents that use it — when one earns its place, structure, validation, delivery |
|
|
332
348
|
| `field-test` | Exercise tools/resources/prompts with real inputs, verify behavior, report issues |
|
|
333
349
|
| `security-pass` | Audit server for MCP-flavored security gaps: output injection, scope blast radius, input sinks, tenant isolation |
|
|
334
350
|
| `tool-defs-analysis` | Read-only audit of MCP definition language across the surface — voice, leaks, defaults, recovery hints, output descriptions |
|
|
@@ -378,9 +394,10 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
378
394
|
| `bun run format` | Auto-fix formatting (safe fixes only) |
|
|
379
395
|
| `bun run format:unsafe` | Also apply Biome's unsafe autofixes — review the diff; they can change behavior |
|
|
380
396
|
| `bun run lint:mcp` | Validate MCP definitions against the linter rules |
|
|
381
|
-
| `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity, `user_config` wiring, plugin manifest identity and env contract (run by devcheck) |
|
|
397
|
+
| `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity, `user_config` wiring, `.mcpbignore` coverage, plugin manifest identity and env contract, the published `skills/` tree (run by devcheck) |
|
|
382
398
|
| `bun run bundle` | Build, pack, and clean a `.mcpb` for one-click Claude Desktop install |
|
|
383
399
|
| `bun run test` | Run tests (Vitest — use `bun run test`, not `bun test`) |
|
|
400
|
+
| `bun run test:coverage` | Run tests with Istanbul coverage |
|
|
384
401
|
| `bun run start:stdio` | Production mode (stdio) — requires `bun run build` first |
|
|
385
402
|
| `bun run start:http` | Production mode (HTTP) — requires `bun run build` first |
|
|
386
403
|
| `bun run changelog:build` | Regenerate `CHANGELOG.md` rollup from `changelog/<minor>.x/*.md` |
|
|
@@ -392,7 +409,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
392
409
|
|
|
393
410
|
## Bundling
|
|
394
411
|
|
|
395
|
-
`bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips
|
|
412
|
+
`bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. `mcpb pack` reads `.mcpbignore`, not `.gitignore`, so every root path the bundle must not ship — source, tests, docs, caches, logs, agent docs, plugin manifests, gitignored local files — needs a root-anchored line there (`/docs/`, never `docs/`, which also strips `node_modules/*/docs/`); `lint:packaging` names each missing or unanchored line. Root `skills/` ships on purpose. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips four classes of `node_modules/**` content that root-anchored `.mcpbignore` patterns cannot reach: dependency-shipped agent docs (`framework-skills/`, `skills/`, `.claude/`, `.agents/`, `SKILL.md`); platform-specific native bindings, which would otherwise lock the bundle to the platform it was packed on; node-gyp build output beside a `binding.gyp`, which records the packing machine's paths (compiled `*.node` addons stay); and build-only package files — the framework's `changelog/`, `scripts/`, `templates/`, `CLAUDE.md`, `AGENTS.md`, and config bases. MCPB is stdio-only — HTTP deployments are unaffected. Delete `manifest.json` and `.mcpbignore` if not shipping MCPB bundles; `lint:packaging` skips cleanly.
|
|
396
413
|
|
|
397
414
|
**Adding an env var requires both files:** `server.json` (registry discovery, `environmentVariables[]`) and `manifest.json` (bundle install UX, `mcp_config.env` + `user_config`). `lint:packaging` (run by `devcheck`) verifies the env var names match, that every `user_config` option is wired into `mcp_config.env` as `"X": "${user_config.X}"` (the host substitutes nothing else — `"${X}"` reaches the server as that literal string), and that an optional string option carries `"default": ""`. A user-supplied variable also goes into the plugin manifests: `.claude-plugin/plugin.json` `userConfig` + `${user_config.<option>}` in `env`, and `.codex-plugin/mcp.json` `env_vars`.
|
|
398
415
|
|
|
@@ -435,6 +452,7 @@ security: false # optional — true ONLY for a source
|
|
|
435
452
|
bun publish --access public
|
|
436
453
|
|
|
437
454
|
docker buildx build --platform linux/amd64,linux/arm64 \
|
|
455
|
+
--build-arg APP_VERSION=<version> \
|
|
438
456
|
-t ghcr.io/cyanheads/obsidian-mcp-server:<version> \
|
|
439
457
|
-t ghcr.io/cyanheads/obsidian-mcp-server:latest \
|
|
440
458
|
--push .
|
|
@@ -470,7 +488,9 @@ import { getMyService } from '@/services/my-domain/my-service.js';
|
|
|
470
488
|
- [ ] If wrapping external API: tests include at least one sparse payload case with omitted upstream fields
|
|
471
489
|
- [ ] Registered in `createApp()` arrays (directly or via barrel exports). Conditional registration (e.g. `commandToolDefinitions` behind `OBSIDIAN_ENABLE_COMMANDS`) happens in `src/index.ts`, not in the barrel
|
|
472
490
|
- [ ] Tests use `createMockContext()` from `@cyanheads/mcp-ts-core/testing`
|
|
491
|
+
- [ ] `plugin.json` + `mcp.json` (Agent Plugins 1.0.0 — read by Codex, Cursor, GitHub Copilot, VS Code, Kiro) populated — `plugin.json` `name` = the unscoped repo name; `version`, `description`, `author`, `homepage`, `repository`, `license`, `keywords` from `package.json`; no other top-level field, `extensions` included. `mcp.json` holds one `"type": "stdio"` entry keyed by the unscoped name, `npx -y obsidian-mcp-server`, and the same fixed `env` as `.codex-plugin/mcp.json`. Neither file carries a user-supplied variable (no `""`, no `${…}` besides `${PLUGIN_ROOT}`/`${PLUGIN_DATA}`): keys reach Codex through `.codex-plugin/mcp.json` `env_vars`, which works only while `.codex-plugin/plugin.json` keeps `"mcpServers": "./.codex-plugin/mcp.json"`
|
|
473
492
|
- [ ] `.codex-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; `interface.displayName` = the unscoped repo name (never the npm scope — `lint:packaging` enforces this); `interface.shortDescription` from `package.json` description
|
|
474
493
|
- [ ] `.codex-plugin/mcp.json` updated — server name key is the unscoped repo name; every user-supplied variable (API key, instance URL) is listed in `env_vars` so Codex forwards it from the user's environment. Never write `"KEY": ""` into `env` — an empty value replaces the user's exported key and is read as unset
|
|
475
494
|
- [ ] `.claude-plugin/plugin.json` populated — `name`, `version`, `description`, `author`, `repository`, `license`, `keywords` from `package.json`; inline `mcpServers` entry keyed by the unscoped repo name. Every user-supplied variable is declared under `userConfig` (`type`, `title`, `description`; `sensitive: true` for keys and tokens; `required: true` or `default: ""`) and referenced from `env` as `"KEY": "${user_config.<option>}"` — mirror the `user_config` block in `manifest.json`. Never write `"KEY": ""` into `env`
|
|
495
|
+
- [ ] Any new root dev path has a root-anchored line in `.mcpbignore`; `bun run bundle` and `lint:packaging` pass
|
|
476
496
|
- [ ] `bun run devcheck` passes
|
package/CLAUDE.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Agent Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** obsidian-mcp-server
|
|
4
|
-
**Version:** 3.7.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.
|
|
4
|
+
**Version:** 3.7.2
|
|
5
|
+
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.18`
|
|
6
6
|
**Engines:** Bun ≥1.4.0, Node ≥24.0.0
|
|
7
|
-
**MCP SDK:** `@modelcontextprotocol/server` ^2.
|
|
8
|
-
**Zod:** ^4.6.
|
|
7
|
+
**MCP SDK:** `@modelcontextprotocol/server` ^2.4.0
|
|
8
|
+
**Zod:** ^4.6.7
|
|
9
9
|
|
|
10
10
|
> **Read the framework docs first:** `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` contains the full API reference — builders, Context, error codes, exports, patterns. This file covers server-specific conventions only.
|
|
11
11
|
|
|
@@ -39,7 +39,7 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
|
|
|
39
39
|
- **Secrets in env vars only.** `OBSIDIAN_API_KEY` is required; never hardcoded.
|
|
40
40
|
- **Cut noise.** Add only what earns its place: no speculative generality, no guards for states the framework already prevents (Zod-validated params, classified errors), no abstraction until a third caller proves it, no option nothing sets.
|
|
41
41
|
- **Command-palette tools are opt-in.** `obsidian_list_commands` and `obsidian_execute_command` are callable only when `OBSIDIAN_ENABLE_COMMANDS=true` — Obsidian commands are opaque and can be destructive. When the flag is unset, the entry point wraps both with `disabledTool()` so they're absent from `tools/list` (LLM can't invoke) but visible in the operator-facing manifest with a hint to enable them.
|
|
42
|
-
- **Path-policy gating goes through `PathPolicy`.** Every path-taking method on `ObsidianService` calls `policy.assertReadable` / `assertWritable` before the upstream HTTP call; `listFiles` gates with `assertListable` and filters entries with `filterListing` (readable entries plus folders on the way to the scope), so every listing consumer inherits the scope; `obsidian_search_notes`
|
|
42
|
+
- **Path-policy gating goes through `PathPolicy`.** Every path-taking method on `ObsidianService` calls `policy.assertReadable` / `assertWritable` before the upstream HTTP call; `listFiles` gates with `assertListable` and filters entries with `filterListing` (readable entries plus folders on the way to the scope), so every listing consumer inherits the scope; `obsidian_search_notes` filters hits via `svc.policy.filterReadable`. Under `policy.restrictsReads`, `searchJsonLogic` never sends the caller's tree upstream: one `/search/` guarded by `readScopePattern` fetches the readable notes' contexts (plus one body fetch per batch of readable notes when the tree names `content`), `filterReadable` gates them, `keepReadable` reduces `links` / `backlinks` / `unresolvedLinks`, and `createJsonLogicEvaluation` runs the tree on a worker thread with the plugin's operators, bounded by depth, a per-note build budget, a per-search kept-result budget, and a deadline (`logic_too_costly`; a note too deep to hand to the worker is `note_too_deep`), so no hit, result, or error reads a note outside the scope. Link targets are still Obsidian's vault-wide resolution, so the reduced link fields can shift when an out-of-scope note appears or disappears (#165); docs state that bound, never a stronger one. `listTags` reads per-note tags from the same readable contexts, with no evaluation. An `active` / `periodic` target is gated on the path it resolves to, under the target's label: `resolveTarget` / `pinTarget` return a `PathTarget` whose `resolvedFrom` the asserts take, so a denial names `(active file)` or `daily note for 2026-10-01`, never a path the caller did not send, and a tool that reads a target then writes it writes to that pinned path rather than resolving the target again. Don't bypass this — `OBSIDIAN_READ_PATHS` / `OBSIDIAN_WRITE_PATHS` / `OBSIDIAN_READ_ONLY` are the single chokepoint, and `path_forbidden` is declared on every path-taking tool's `errors[]` contract.
|
|
43
43
|
- **Close the loop on issues.** When implementing work tracked by a GitHub issue, comment on the issue with what landed and close it. Do both — a comment without a close leaves stale issues open; a close without a comment leaves no record of what shipped. The comment is for future readers — state the concrete changes, not the conversation that produced them.
|
|
44
44
|
|
|
45
45
|
---
|
|
@@ -274,19 +274,25 @@ Available factories: `notFound`, `validationError`, `forbidden`, `unauthorized`,
|
|
|
274
274
|
```text
|
|
275
275
|
src/
|
|
276
276
|
index.ts # createApp() entry point — registers tools/resources, inits Obsidian service
|
|
277
|
+
instructions.ts # buildInstructions() — server `instructions`, effective path scope included
|
|
277
278
|
config/
|
|
278
279
|
server-config.ts # OBSIDIAN_* env vars (Zod schema)
|
|
279
280
|
services/
|
|
280
281
|
obsidian/
|
|
281
282
|
obsidian-service.ts # Local REST API client (init/accessor pattern)
|
|
282
|
-
|
|
283
|
+
path-policy.ts # PathPolicy — OBSIDIAN_READ_PATHS / WRITE_PATHS / READ_ONLY gate
|
|
284
|
+
jsonlogic-deps.d.ts # Type declarations for json-logic-js and glob-to-regexp
|
|
285
|
+
jsonlogic-evaluator.ts # Scoped JSONLogic evaluation: bounds, worker hand-off, logic_* errors
|
|
286
|
+
jsonlogic-worker.ts # Worker entry: the plugin's JSONLogic operators, hardened and budgeted
|
|
287
|
+
frontmatter-ops.ts # YAML frontmatter parse/serialize/edit helpers + inline tag reader + the wikilink span rule (`wikilinkCloser`) `obsidian_get_note` imports
|
|
283
288
|
markdown-blocks.ts # Block structure (code, HTML, math, tables) for inline tag detection
|
|
284
289
|
patch-instruction.ts # markdown-patch 1.x headers / 2.0 instructions, format negotiation, 2.0 map flattening
|
|
285
290
|
section-extractor.ts # Heading/block/frontmatter section extraction
|
|
286
|
-
types.ts # Domain types (NoteJson, NoteTarget, etc.)
|
|
291
|
+
types.ts # Domain types (NoteJson, NoteTarget, PathTarget, etc.)
|
|
287
292
|
mcp-server/
|
|
288
293
|
tools/definitions/
|
|
289
294
|
_shared/schemas.ts # Shared TargetSchema + SectionSchema reused across tools
|
|
295
|
+
_shared/suggest-paths.ts # Case-mismatch fallback and suggestions for reads; names the existing note on a `case_variant_exists` write
|
|
290
296
|
index.ts # read/write/command tool sets + buildSearchNotesTool (Omnisearch-aware) and buildDeleteNoteTool (elicitation mode) factories
|
|
291
297
|
obsidian-*.tool.ts # 14 tool definitions (12 base + 2 opt-in command-palette pair)
|
|
292
298
|
resources/definitions/
|
|
@@ -313,9 +319,18 @@ src/
|
|
|
313
319
|
|
|
314
320
|
## Skills
|
|
315
321
|
|
|
316
|
-
|
|
322
|
+
Two skill trees can live at the project root, written for different readers:
|
|
317
323
|
|
|
318
|
-
|
|
324
|
+
| Tree | For | Written by |
|
|
325
|
+
|:-----|:----|:-----------|
|
|
326
|
+
| `framework-skills/` | The agent developing this server — framework skills (`metadata.audience: external`) plus any development-time skills of the server's own | `init` and the `maintenance` sync (framework skills); you (your own) |
|
|
327
|
+
| `skills/` | The agents that install the server and call its tools — tool chains, identifier sources, gates | You, deliberately, with the `add-skill` skill. Absent here: this server publishes none, so `createApp()` gets no `skills` option |
|
|
328
|
+
|
|
329
|
+
Development skills are modular instructions. Read them directly when a task matches — e.g., `framework-skills/add-tool/SKILL.md` when adding a tool. `bun run list-skills` prints the full registry. Their directory is deliberately not `skills/`: Claude Code and Codex auto-load a plugin's root `skills/`, so a server that ships `.claude-plugin/` or `.codex-plugin/` would hand them to every agent that installs it.
|
|
330
|
+
|
|
331
|
+
A published skill in root `skills/` reaches plugin installs and the `.mcpb` bundle as-is, ships in the npm package and the Docker image (`package.json` `files` and the Dockerfile already carry `skills/`), and reaches MCP clients when the server opts in with `createApp({ skills: 'skills' })`. `lint:packaging` validates the tree.
|
|
332
|
+
|
|
333
|
+
**Agent skill directory:** Copy `framework-skills/` into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). Skills then load as context without referencing `framework-skills/` paths. After framework updates, run the `maintenance` skill — Phase B re-syncs the agent directory.
|
|
319
334
|
|
|
320
335
|
Available skills:
|
|
321
336
|
|
|
@@ -329,6 +344,7 @@ Available skills:
|
|
|
329
344
|
| `add-prompt` | Scaffold a new prompt definition |
|
|
330
345
|
| `add-service` | Scaffold a new service integration |
|
|
331
346
|
| `add-test` | Scaffold test file for a tool, resource, or service |
|
|
347
|
+
| `add-skill` | Author a skill the server publishes in root `skills/` for the agents that use it — when one earns its place, structure, validation, delivery |
|
|
332
348
|
| `field-test` | Exercise tools/resources/prompts with real inputs, verify behavior, report issues |
|
|
333
349
|
| `security-pass` | Audit server for MCP-flavored security gaps: output injection, scope blast radius, input sinks, tenant isolation |
|
|
334
350
|
| `tool-defs-analysis` | Read-only audit of MCP definition language across the surface — voice, leaks, defaults, recovery hints, output descriptions |
|
|
@@ -378,9 +394,10 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
378
394
|
| `bun run format` | Auto-fix formatting (safe fixes only) |
|
|
379
395
|
| `bun run format:unsafe` | Also apply Biome's unsafe autofixes — review the diff; they can change behavior |
|
|
380
396
|
| `bun run lint:mcp` | Validate MCP definitions against the linter rules |
|
|
381
|
-
| `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity, `user_config` wiring, plugin manifest identity and env contract (run by devcheck) |
|
|
397
|
+
| `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity, `user_config` wiring, `.mcpbignore` coverage, plugin manifest identity and env contract, the published `skills/` tree (run by devcheck) |
|
|
382
398
|
| `bun run bundle` | Build, pack, and clean a `.mcpb` for one-click Claude Desktop install |
|
|
383
399
|
| `bun run test` | Run tests (Vitest — use `bun run test`, not `bun test`) |
|
|
400
|
+
| `bun run test:coverage` | Run tests with Istanbul coverage |
|
|
384
401
|
| `bun run start:stdio` | Production mode (stdio) — requires `bun run build` first |
|
|
385
402
|
| `bun run start:http` | Production mode (HTTP) — requires `bun run build` first |
|
|
386
403
|
| `bun run changelog:build` | Regenerate `CHANGELOG.md` rollup from `changelog/<minor>.x/*.md` |
|
|
@@ -392,7 +409,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
392
409
|
|
|
393
410
|
## Bundling
|
|
394
411
|
|
|
395
|
-
`bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips
|
|
412
|
+
`bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. `mcpb pack` reads `.mcpbignore`, not `.gitignore`, so every root path the bundle must not ship — source, tests, docs, caches, logs, agent docs, plugin manifests, gitignored local files — needs a root-anchored line there (`/docs/`, never `docs/`, which also strips `node_modules/*/docs/`); `lint:packaging` names each missing or unanchored line. Root `skills/` ships on purpose. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips four classes of `node_modules/**` content that root-anchored `.mcpbignore` patterns cannot reach: dependency-shipped agent docs (`framework-skills/`, `skills/`, `.claude/`, `.agents/`, `SKILL.md`); platform-specific native bindings, which would otherwise lock the bundle to the platform it was packed on; node-gyp build output beside a `binding.gyp`, which records the packing machine's paths (compiled `*.node` addons stay); and build-only package files — the framework's `changelog/`, `scripts/`, `templates/`, `CLAUDE.md`, `AGENTS.md`, and config bases. MCPB is stdio-only — HTTP deployments are unaffected. Delete `manifest.json` and `.mcpbignore` if not shipping MCPB bundles; `lint:packaging` skips cleanly.
|
|
396
413
|
|
|
397
414
|
**Adding an env var requires both files:** `server.json` (registry discovery, `environmentVariables[]`) and `manifest.json` (bundle install UX, `mcp_config.env` + `user_config`). `lint:packaging` (run by `devcheck`) verifies the env var names match, that every `user_config` option is wired into `mcp_config.env` as `"X": "${user_config.X}"` (the host substitutes nothing else — `"${X}"` reaches the server as that literal string), and that an optional string option carries `"default": ""`. A user-supplied variable also goes into the plugin manifests: `.claude-plugin/plugin.json` `userConfig` + `${user_config.<option>}` in `env`, and `.codex-plugin/mcp.json` `env_vars`.
|
|
398
415
|
|
|
@@ -435,6 +452,7 @@ security: false # optional — true ONLY for a source
|
|
|
435
452
|
bun publish --access public
|
|
436
453
|
|
|
437
454
|
docker buildx build --platform linux/amd64,linux/arm64 \
|
|
455
|
+
--build-arg APP_VERSION=<version> \
|
|
438
456
|
-t ghcr.io/cyanheads/obsidian-mcp-server:<version> \
|
|
439
457
|
-t ghcr.io/cyanheads/obsidian-mcp-server:latest \
|
|
440
458
|
--push .
|
|
@@ -470,7 +488,9 @@ import { getMyService } from '@/services/my-domain/my-service.js';
|
|
|
470
488
|
- [ ] If wrapping external API: tests include at least one sparse payload case with omitted upstream fields
|
|
471
489
|
- [ ] Registered in `createApp()` arrays (directly or via barrel exports). Conditional registration (e.g. `commandToolDefinitions` behind `OBSIDIAN_ENABLE_COMMANDS`) happens in `src/index.ts`, not in the barrel
|
|
472
490
|
- [ ] Tests use `createMockContext()` from `@cyanheads/mcp-ts-core/testing`
|
|
491
|
+
- [ ] `plugin.json` + `mcp.json` (Agent Plugins 1.0.0 — read by Codex, Cursor, GitHub Copilot, VS Code, Kiro) populated — `plugin.json` `name` = the unscoped repo name; `version`, `description`, `author`, `homepage`, `repository`, `license`, `keywords` from `package.json`; no other top-level field, `extensions` included. `mcp.json` holds one `"type": "stdio"` entry keyed by the unscoped name, `npx -y obsidian-mcp-server`, and the same fixed `env` as `.codex-plugin/mcp.json`. Neither file carries a user-supplied variable (no `""`, no `${…}` besides `${PLUGIN_ROOT}`/`${PLUGIN_DATA}`): keys reach Codex through `.codex-plugin/mcp.json` `env_vars`, which works only while `.codex-plugin/plugin.json` keeps `"mcpServers": "./.codex-plugin/mcp.json"`
|
|
473
492
|
- [ ] `.codex-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; `interface.displayName` = the unscoped repo name (never the npm scope — `lint:packaging` enforces this); `interface.shortDescription` from `package.json` description
|
|
474
493
|
- [ ] `.codex-plugin/mcp.json` updated — server name key is the unscoped repo name; every user-supplied variable (API key, instance URL) is listed in `env_vars` so Codex forwards it from the user's environment. Never write `"KEY": ""` into `env` — an empty value replaces the user's exported key and is read as unset
|
|
475
494
|
- [ ] `.claude-plugin/plugin.json` populated — `name`, `version`, `description`, `author`, `repository`, `license`, `keywords` from `package.json`; inline `mcpServers` entry keyed by the unscoped repo name. Every user-supplied variable is declared under `userConfig` (`type`, `title`, `description`; `sensitive: true` for keys and tokens; `required: true` or `default: ""`) and referenced from `env` as `"KEY": "${user_config.<option>}"` — mirror the `user_config` block in `manifest.json`. Never write `"KEY": ""` into `env`
|
|
495
|
+
- [ ] Any new root dev path has a root-anchored line in `.mcpbignore`; `bun run bundle` and `lint:packaging` pass
|
|
476
496
|
- [ ] `bun run devcheck` passes
|
package/Dockerfile
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
# output. A stage that compiles a native addon needs the target-arch toolchain
|
|
15
15
|
# and cannot cross-compile this way — drop the flag there.
|
|
16
16
|
# ==============================================================================
|
|
17
|
-
FROM --platform=$BUILDPLATFORM oven/bun:1.4.
|
|
17
|
+
FROM --platform=$BUILDPLATFORM oven/bun:1.4.4 AS build
|
|
18
18
|
|
|
19
19
|
WORKDIR /usr/src/app
|
|
20
20
|
|
|
@@ -29,6 +29,11 @@ RUN --mount=type=cache,target=/root/.bun/install/cache \
|
|
|
29
29
|
# Copy the rest of the source code
|
|
30
30
|
COPY . .
|
|
31
31
|
|
|
32
|
+
# The skills a server publishes (`createApp({ skills: 'skills' })`) are read at
|
|
33
|
+
# runtime from the application root. Created when the project has none, so the
|
|
34
|
+
# production stage's copy below holds with or without one.
|
|
35
|
+
RUN mkdir -p skills
|
|
36
|
+
|
|
32
37
|
# Build the application
|
|
33
38
|
RUN bun run build
|
|
34
39
|
|
|
@@ -46,7 +51,7 @@ RUN bun run build
|
|
|
46
51
|
# A clean image rather than `FROM build`: the build stage's node_modules holds
|
|
47
52
|
# devDependencies.
|
|
48
53
|
# ==============================================================================
|
|
49
|
-
FROM --platform=$BUILDPLATFORM oven/bun:1.4.
|
|
54
|
+
FROM --platform=$BUILDPLATFORM oven/bun:1.4.4 AS deps
|
|
50
55
|
|
|
51
56
|
WORKDIR /usr/src/app
|
|
52
57
|
|
|
@@ -113,7 +118,7 @@ RUN rm -rf node_modules/@socketsecurity/bun-security-scanner
|
|
|
113
118
|
# dependencies and build artifacts. Its only Bun invocations are HEALTHCHECK
|
|
114
119
|
# and CMD, which run on the real target at container start.
|
|
115
120
|
# ==============================================================================
|
|
116
|
-
FROM oven/bun:1.4.
|
|
121
|
+
FROM oven/bun:1.4.4-slim AS production
|
|
117
122
|
|
|
118
123
|
WORKDIR /usr/src/app
|
|
119
124
|
|
|
@@ -135,6 +140,7 @@ COPY --from=deps /usr/src/app/node_modules ./node_modules
|
|
|
135
140
|
|
|
136
141
|
# Copy the compiled application code from the build stage
|
|
137
142
|
COPY --from=build /usr/src/app/dist ./dist
|
|
143
|
+
COPY --from=build /usr/src/app/skills ./skills
|
|
138
144
|
|
|
139
145
|
# The 'oven/bun' image already provides a non-root user named 'bun'.
|
|
140
146
|
# We will use this existing user for enhanced security.
|
|
@@ -158,6 +164,7 @@ ENV MCP_HTTP_PORT=${PORT:-3010}
|
|
|
158
164
|
# bind exposes the upstream OBSIDIAN_API_KEY to any caller that can reach the port.
|
|
159
165
|
ENV MCP_HTTP_HOST="0.0.0.0"
|
|
160
166
|
ENV MCP_TRANSPORT_TYPE="http"
|
|
167
|
+
ENV MCP_SESSION_MODE="stateful"
|
|
161
168
|
ENV MCP_LOG_LEVEL="info"
|
|
162
169
|
ENV LOGS_DIR="/var/log/obsidian-mcp-server"
|
|
163
170
|
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
<div align="center">
|
|
9
9
|
|
|
10
|
-
[](./CHANGELOG.md) [](./LICENSE) [](https://github.com/users/cyanheads/packages/container/package/obsidian-mcp-server) [](https://modelcontextprotocol.io/) [](https://www.npmjs.com/package/obsidian-mcp-server) [](https://www.typescriptlang.org/) [](https://bun.sh/)
|
|
11
11
|
|
|
12
12
|
</div>
|
|
13
13
|
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
## Overview
|
|
25
25
|
|
|
26
|
-
Obsidian vault
|
|
26
|
+
Your Obsidian vault, through the Local REST API plugin. Read, search, and write notes, edit a single heading, block, or frontmatter field in place, and manage tags, with folder-scoped read/write permissions built in.
|
|
27
27
|
|
|
28
28
|
### Tools
|
|
29
29
|
|
|
@@ -82,7 +82,7 @@ Note and tag data are also reachable through tools (`obsidian_get_note`, `obsidi
|
|
|
82
82
|
|
|
83
83
|
### `obsidian_search_notes` <sub>tool</sub>
|
|
84
84
|
|
|
85
|
-
- `mode: "text"` requires every whitespace-split token of `query` as a case-insensitive substring of the filename or body (quotes are literal), shaped by `contextLength` (default 100), `pathPrefix`, and `maxMatchesPerHit` (default 10); `mode: "jsonlogic"` evaluates a `logic` tree over `path`, `content`, `frontmatter.<key>`, `tags`, and `stat`, with `glob` / `regexp` taking `[PATTERN, VALUE]`
|
|
85
|
+
- `mode: "text"` requires every whitespace-split token of `query` as a case-insensitive substring of the filename or body (quotes are literal), shaped by `contextLength` (default 100), `pathPrefix`, and `maxMatchesPerHit` (default 10); `mode: "jsonlogic"` evaluates a `logic` tree over `path`, `content`, `frontmatter.<key>`, `tags`, and `stat` (plus `links`, `backlinks`, and `unresolvedLinks` on plugin versions that provide them), with `glob` / `regexp` taking `[PATTERN, VALUE]`
|
|
86
86
|
- `result.mode` discriminates the payload; every mode reports `totalCount` and pages via `nextCursor`, and a text hit clipped to `maxMatchesPerHit` carries `truncated` and `totalMatches`
|
|
87
87
|
- `mode: "omnisearch"` (BM25 ranking, quoted phrases, `-exclusion`, `path:` / `ext:` filters) is offered only when the Omnisearch plugin answered at startup; its 50-hit upstream cap sets `truncated: true`
|
|
88
88
|
|
|
@@ -91,6 +91,7 @@ Note and tag data are also reachable through tools (`obsidian_get_note`, `obsidi
|
|
|
91
91
|
### `obsidian_write_note` <sub>tool</sub>
|
|
92
92
|
|
|
93
93
|
- `target` and `content`, with optional `section` and `contentType` (`markdown` / `json`); a whole-file write to an existing note fails with `file_exists` unless `overwrite: true`
|
|
94
|
+
- On a case-insensitive filesystem (macOS, Windows), a whole-file write to a path that differs from an existing note only in letter case fails with `case_variant_exists` and writes nothing; `existingPath` names that note when the parent folder's listing shows exactly one case match, and is omitted when several match or when a file Obsidian does not index (a dot-file) sits at the path
|
|
94
95
|
- With `section`, replaces only that heading, block, or frontmatter field and keeps the heading line; output reports `created`, `sectionTargeted`, and the resolved `sectionTarget`
|
|
95
96
|
|
|
96
97
|
---
|
|
@@ -99,6 +100,7 @@ Note and tag data are also reachable through tools (`obsidian_get_note`, `obsidi
|
|
|
99
100
|
|
|
100
101
|
- Without `section`, appends to the file or creates it (`created: true`); with `section`, appends to that heading, block, or frontmatter field of an existing note, and `createTargetIfMissing: true` creates the section
|
|
101
102
|
- A section append whose content is already at the target fails with `content_preexists`; block targets add no separator, so start `content` with a newline if you want one
|
|
103
|
+
- A whole-file append to a path that differs from an existing note only in letter case fails with `case_variant_exists` on a case-insensitive filesystem, with `existingPath` naming that note when the parent folder's listing shows exactly one case match
|
|
102
104
|
|
|
103
105
|
---
|
|
104
106
|
|
|
@@ -208,110 +210,37 @@ Agent-friendly output:
|
|
|
208
210
|
|
|
209
211
|
## Getting started
|
|
210
212
|
|
|
211
|
-
Add
|
|
213
|
+
Add it to Claude Code:
|
|
212
214
|
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
"mcpServers": {
|
|
216
|
-
"obsidian-mcp-server": {
|
|
217
|
-
"type": "stdio",
|
|
218
|
-
"command": "bunx",
|
|
219
|
-
"args": ["obsidian-mcp-server@latest"],
|
|
220
|
-
"env": {
|
|
221
|
-
"MCP_TRANSPORT_TYPE": "stdio",
|
|
222
|
-
"MCP_LOG_LEVEL": "info",
|
|
223
|
-
"OBSIDIAN_API_KEY": "your-local-rest-api-key"
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
215
|
+
```sh
|
|
216
|
+
claude mcp add obsidian-mcp-server -e OBSIDIAN_API_KEY=your-api-key -- npx -y obsidian-mcp-server
|
|
228
217
|
```
|
|
229
218
|
|
|
230
|
-
|
|
219
|
+
For other clients, add this to the MCP config file:
|
|
231
220
|
|
|
232
221
|
```json
|
|
233
222
|
{
|
|
234
223
|
"mcpServers": {
|
|
235
224
|
"obsidian-mcp-server": {
|
|
236
|
-
"type": "stdio",
|
|
237
225
|
"command": "npx",
|
|
238
|
-
"args": ["-y", "obsidian-mcp-server
|
|
239
|
-
"env": {
|
|
240
|
-
"MCP_TRANSPORT_TYPE": "stdio",
|
|
241
|
-
"MCP_LOG_LEVEL": "info",
|
|
242
|
-
"OBSIDIAN_API_KEY": "your-local-rest-api-key"
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
Or with Docker:
|
|
250
|
-
|
|
251
|
-
```json
|
|
252
|
-
{
|
|
253
|
-
"mcpServers": {
|
|
254
|
-
"obsidian-mcp-server": {
|
|
255
|
-
"type": "stdio",
|
|
256
|
-
"command": "docker",
|
|
257
|
-
"args": [
|
|
258
|
-
"run", "-i", "--rm",
|
|
259
|
-
"-e", "MCP_TRANSPORT_TYPE=stdio",
|
|
260
|
-
"-e", "MCP_LOG_LEVEL=info",
|
|
261
|
-
"-e", "OBSIDIAN_API_KEY=your-local-rest-api-key",
|
|
262
|
-
"ghcr.io/cyanheads/obsidian-mcp-server:latest"
|
|
263
|
-
]
|
|
226
|
+
"args": ["-y", "obsidian-mcp-server"],
|
|
227
|
+
"env": { "OBSIDIAN_API_KEY": "your-api-key" }
|
|
264
228
|
}
|
|
265
229
|
}
|
|
266
230
|
}
|
|
267
231
|
```
|
|
268
232
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
For Streamable HTTP, set the transport and start the server:
|
|
272
|
-
|
|
273
|
-
```sh
|
|
274
|
-
MCP_TRANSPORT_TYPE=http OBSIDIAN_API_KEY=... bun run start:http
|
|
275
|
-
# Server listens at http://127.0.0.1:3010/mcp by default
|
|
276
|
-
```
|
|
233
|
+
Generate the key in Obsidian under **Settings → Community Plugins → Local REST API**, with the plugin set up as [Prerequisites](#prerequisites) describes. For Docker or a source build, see [Running the server](#running-the-server).
|
|
277
234
|
|
|
278
235
|
### Prerequisites
|
|
279
236
|
|
|
280
|
-
- [
|
|
281
|
-
- The [Obsidian Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api) plugin, v4.0.0 or later, enabled in your vault. Generate an API key under **Settings → Community Plugins → Local REST API** and set it as `OBSIDIAN_API_KEY`.
|
|
237
|
+
- The [Obsidian Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api) plugin, v4.0.0 or later, enabled in your vault. Its API key is `OBSIDIAN_API_KEY`.
|
|
282
238
|
- The server defaults to `http://127.0.0.1:27123`, so enable **"Non-encrypted (HTTP) Server"** in the plugin settings, or set `OBSIDIAN_BASE_URL=https://127.0.0.1:27124` for the always-on HTTPS port (its self-signed cert is accepted while `OBSIDIAN_VERIFY_SSL=false`, the default).
|
|
283
239
|
- Periodic-note targets work natively on plugin v5.0.1 and earlier. From v5.0.2 they need the [periodic-notes API extension](https://github.com/coddingtonbear/obsidian-local-rest-api-periodic-notes); without it they fail with `periodic_unsupported`.
|
|
284
240
|
- On plugin v4.x, heading section writes to a note with CRLF (Windows) line endings land mid-line: the plugin's markdown-patch 1.x engine misplaces them, and the server can't correct it. Plugin v5.0 and later handle CRLF notes.
|
|
285
241
|
- Plugin v6.0 drops markdown-patch 1.x, which two table-row writes (`contentType: "json"`) still use: rows under a heading, and rows through a block ID on its own line below the table. On v6.0, target the table by an ID on its last row.
|
|
286
242
|
- With `OBSIDIAN_DELETE_ELICITATION=true`, an MCP client that supports elicitation and renders its form, to use `obsidian_delete_note`. Every other tool, and the default delete, works without it.
|
|
287
243
|
|
|
288
|
-
### Installation
|
|
289
|
-
|
|
290
|
-
1. **Clone the repository:**
|
|
291
|
-
|
|
292
|
-
```sh
|
|
293
|
-
git clone https://github.com/cyanheads/obsidian-mcp-server.git
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
2. **Navigate into the directory:**
|
|
297
|
-
|
|
298
|
-
```sh
|
|
299
|
-
cd obsidian-mcp-server
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
3. **Install dependencies:**
|
|
303
|
-
|
|
304
|
-
```sh
|
|
305
|
-
bun install
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
4. **Configure environment:**
|
|
309
|
-
|
|
310
|
-
```sh
|
|
311
|
-
cp .env.example .env
|
|
312
|
-
# edit .env and set OBSIDIAN_API_KEY
|
|
313
|
-
```
|
|
314
|
-
|
|
315
244
|
## Configuration
|
|
316
245
|
|
|
317
246
|
| Variable | Description | Default |
|
|
@@ -322,7 +251,7 @@ MCP_TRANSPORT_TYPE=http OBSIDIAN_API_KEY=... bun run start:http
|
|
|
322
251
|
| `OBSIDIAN_REQUEST_TIMEOUT_MS` | Per-request timeout in milliseconds. | `30000` |
|
|
323
252
|
| `OBSIDIAN_ENABLE_COMMANDS` | Enable `obsidian_list_commands` and `obsidian_execute_command`. Commands are opaque and can be destructive. | `false` |
|
|
324
253
|
| `OBSIDIAN_READ_PATHS` | Comma-separated folder allowlist for reads. See [Path policy](#path-policy). | unset (full vault) |
|
|
325
|
-
| `OBSIDIAN_WRITE_PATHS` | Comma-separated folder allowlist for writes. See [Path policy](#path-policy). | unset (full vault) |
|
|
254
|
+
| `OBSIDIAN_WRITE_PATHS` | Comma-separated folder allowlist for writes. See [Path policy](#path-policy). | unset (the read scope, or the full vault without one) |
|
|
326
255
|
| `OBSIDIAN_READ_ONLY` | Deny every write and disable the command-palette pair. | `false` |
|
|
327
256
|
| `OBSIDIAN_DELETE_ELICITATION` | Ask the user to confirm each `obsidian_delete_note` call through an elicitation form before deleting. Needs a client that renders elicitation, and a stateful session over HTTP (not required when `OBSIDIAN_READ_ONLY=true` disables the tool). Off, the delete runs on the first call. | `false` |
|
|
328
257
|
| `OBSIDIAN_OMNISEARCH_URL` | [Omnisearch](https://github.com/scambier/obsidian-omnisearch) HTTP server URL. Unset derives from the `OBSIDIAN_BASE_URL` host on port `51361`. Probed once at startup; the `omnisearch` search mode appears only if it answers. | derived |
|
|
@@ -348,14 +277,25 @@ Three optional env vars limit which vault paths the tools can touch. Unset, read
|
|
|
348
277
|
|
|
349
278
|
- Matching is by prefix, recursive, and case-insensitive; trailing slashes are normalized. Write paths are also readable.
|
|
350
279
|
- `OBSIDIAN_READ_ONLY=true` removes every write tool and the command-palette pair from `tools/list`, including `obsidian_manage_frontmatter` and `obsidian_manage_tags` (so their `get` / `list` go too). `obsidian_open_in_ui` still opens existing files but won't create one.
|
|
351
|
-
- A denial fails with `path_forbidden`, echoing the active scope in `data.activeScope` and the recovery hint.
|
|
280
|
+
- A denial fails with `path_forbidden`, echoing the active scope in `data.activeScope` and the recovery hint. A denial on an `active` or `periodic` target names the target (`(active file)`, `daily note for 2026-10-01`), not the note it resolved to. `obsidian_list_notes` lists only readable entries plus the folders leading to the scope, so a nested scope can be browsed to from the root.
|
|
281
|
+
- Under `OBSIDIAN_READ_PATHS`, searches return readable notes only, and a JSONLogic tree never reads a note outside the scope: the server evaluates it over the readable notes, with `links`, `backlinks`, and `unresolvedLinks` holding readable entries only. Which note a link points to is still Obsidian's resolution across the whole vault, so those three fields can change when a note outside the scope is created or removed; match link text in `content` when that matters. That evaluation is bounded — 1,000 levels of nesting, a work budget per note for the values a tree builds and one per search for the results it keeps, 10 s — and a tree past a bound fails with `logic_too_costly`. A readable note whose data is nested too deep to hand to the evaluator (about 3,500 levels under Node) fails the search with `note_too_deep`. Without a read scope the tree goes to the plugin unchanged.
|
|
352
282
|
- Tag listings (`obsidian_list_tags`, `obsidian://tags`) under `OBSIDIAN_READ_PATHS` cover readable notes only, counting notes rather than occurrences. Write paths and read-only alone leave them vault-wide.
|
|
353
|
-
- The startup log prints the
|
|
283
|
+
- The server `instructions` state the effective scope: readable is the read paths plus the write paths, and writable is the write paths, or the read scope when only reads are restricted. The startup log prints the configured lists.
|
|
354
284
|
|
|
355
285
|
## Running the server
|
|
356
286
|
|
|
357
287
|
### Local development
|
|
358
288
|
|
|
289
|
+
Requires [Bun v1.4.0](https://bun.sh/) or higher (or Node.js v24+).
|
|
290
|
+
|
|
291
|
+
- **Clone and install:**
|
|
292
|
+
|
|
293
|
+
```sh
|
|
294
|
+
git clone https://github.com/cyanheads/obsidian-mcp-server.git
|
|
295
|
+
cd obsidian-mcp-server && bun install
|
|
296
|
+
cp .env.example .env # set OBSIDIAN_API_KEY
|
|
297
|
+
```
|
|
298
|
+
|
|
359
299
|
- **Build and run the production version:**
|
|
360
300
|
|
|
361
301
|
```sh
|
|
@@ -364,7 +304,7 @@ Three optional env vars limit which vault paths the tools can touch. Unset, read
|
|
|
364
304
|
|
|
365
305
|
# Run the built server
|
|
366
306
|
bun run start:stdio
|
|
367
|
-
# or
|
|
307
|
+
# or, listening at http://127.0.0.1:3010/mcp
|
|
368
308
|
bun run start:http
|
|
369
309
|
```
|
|
370
310
|
|
|
@@ -379,11 +319,10 @@ Three optional env vars limit which vault paths the tools can touch. Unset, read
|
|
|
379
319
|
### Docker
|
|
380
320
|
|
|
381
321
|
```sh
|
|
382
|
-
docker
|
|
383
|
-
docker run --rm -e OBSIDIAN_API_KEY=your-key -p 3010:3010 obsidian-mcp-server
|
|
322
|
+
docker run --rm -e OBSIDIAN_API_KEY=your-key -p 3010:3010 ghcr.io/cyanheads/obsidian-mcp-server:latest
|
|
384
323
|
```
|
|
385
324
|
|
|
386
|
-
The image defaults to HTTP transport on `0.0.0.0`, stateful sessions, and logs in `/var/log/obsidian-mcp-server`.
|
|
325
|
+
The image defaults to HTTP transport on `0.0.0.0`, stateful sessions, and logs in `/var/log/obsidian-mcp-server`. Inside a container, the default `OBSIDIAN_BASE_URL` (`http://127.0.0.1:27123`) is the container's own loopback: point it at `http://host.docker.internal:27123` (Docker Desktop) or use `--network host` (Linux) to reach the plugin on your host. To run it over stdio from an MCP client, use `"command": "docker"` with `"args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "-e", "OBSIDIAN_API_KEY=your-key", "-e", "OBSIDIAN_BASE_URL=http://host.docker.internal:27123", "ghcr.io/cyanheads/obsidian-mcp-server:latest"]`. To build locally, `docker build -t obsidian-mcp-server .`; OpenTelemetry peer dependencies are installed by default — build with `--build-arg OTEL_ENABLED=false` to omit them.
|
|
387
326
|
|
|
388
327
|
If the port is reachable from other machines, set `MCP_AUTH_MODE=jwt` (with `MCP_AUTH_SECRET_KEY`) or `oauth`. With the default `none`, every caller acts on your vault with your `OBSIDIAN_API_KEY`.
|
|
389
328
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "Read-scope fixes: under OBSIDIAN_READ_PATHS, JSONLogic search runs in the server over readable notes only, active and periodic denials name the target rather than the resolved note, read-then-write tools write to the note they read, and instructions state the effective scope. A case-variant write fails as case_variant_exists."
|
|
3
|
+
breaking: false
|
|
4
|
+
security: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 3.7.1 — 2026-10-24
|
|
8
|
+
|
|
9
|
+
## Changed
|
|
10
|
+
|
|
11
|
+
- **The `.mcpb` bundle carries only runtime files and package metadata** ([#166](https://github.com/cyanheads/obsidian-mcp-server/issues/166)). `src/`, `tests/`, `scripts/`, `docs/`, CI and editor config, and agent files are left out.
|
|
12
|
+
|
|
13
|
+
## Fixed
|
|
14
|
+
|
|
15
|
+
- **A whole-file `obsidian_write_note` or `obsidian_append_to_note` to a path that differs from an existing note only in letter case fails `case_variant_exists`** ([#159](https://github.com/cyanheads/obsidian-mcp-server/issues/159)). Conflict, with `existingPath` when the folder listing shows exactly one case match; it replaces a retried `ServiceUnavailable`. Nothing is written.
|
|
16
|
+
- **`obsidian_manage_tags` reads a wikilink's extent as Obsidian does** ([#160](https://github.com/cyanheads/obsidian-mcp-server/issues/160)). A `#word` inside `[[Note|a #b [1]]]` is link text, not an inline tag.
|
|
17
|
+
- **The server `instructions` state the effective scope** ([#158](https://github.com/cyanheads/obsidian-mcp-server/issues/158)): readable is the read paths plus the write paths, and writable is the write paths, or the read scope when only reads are restricted. Under `OBSIDIAN_READ_ONLY=true` with `OBSIDIAN_READ_PATHS`, the read scope is stated.
|
|
18
|
+
|
|
19
|
+
## Security
|
|
20
|
+
|
|
21
|
+
- **Under `OBSIDIAN_READ_PATHS`, `obsidian_search_notes` in `jsonlogic` mode evaluates the tree in the server over readable notes only** ([#156](https://github.com/cyanheads/obsidian-mcp-server/issues/156)). No hit, result, or error reads a note outside the scope, and `links`, `backlinks`, and `unresolvedLinks` hold readable entries. Which note a link resolves to is still Obsidian's vault-wide resolution ([#165](https://github.com/cyanheads/obsidian-mcp-server/issues/165)). Evaluation is bounded: a tree past a bound fails `logic_too_costly`, and a readable note whose data is nested too deep to hand to the evaluator fails `note_too_deep`. Without a read scope the tree goes to the plugin unchanged.
|
|
22
|
+
- **A `path_forbidden` denial on an `active` or `periodic` target names the target** ([#157](https://github.com/cyanheads/obsidian-mcp-server/issues/157)), such as `(active file)` or `daily note for 2026-10-01`, not the note it resolved to.
|
|
23
|
+
- **`obsidian_replace_in_note`, `obsidian_manage_frontmatter` (`delete`), and `obsidian_manage_tags` (`add`, `remove`) write to the path their read resolved** ([#162](https://github.com/cyanheads/obsidian-mcp-server/issues/162)), so a change of active note or period between the read and the write cannot redirect the write to another note. `obsidian_manage_tags` reads back that same path, and `obsidian_get_note` `format: "document-map"` on an `active` or `periodic` target resolves it once, so the map and the returned `path` describe the same note.
|
|
24
|
+
|
|
25
|
+
## Dependencies
|
|
26
|
+
|
|
27
|
+
- `@cyanheads/mcp-ts-core` ^0.13.12 → ^0.13.15
|
|
28
|
+
- `js-yaml` ^5.4.2 → ^5.5.0
|
|
29
|
+
- `pino-pretty` ^13.1.3 → ^13.2.0
|
|
30
|
+
- `undici` ^8.11.2 → ^8.12.3
|
|
31
|
+
- `zod` ^4.6.5 → ^4.6.7
|
|
32
|
+
- `json-logic-js` 2.0.5 (new; the plugin's own JSONLogic engine, evaluated in-server under a read scope)
|
|
33
|
+
- `glob-to-regexp` 0.4.1 (new; the plugin's `glob` operator)
|
|
34
|
+
- dev: `@biomejs/biome` ^2.5.15 → ^2.6.2
|
|
35
|
+
- dev: `@types/node` ^26.6.4 → ^26.6.5
|
|
36
|
+
- dev: `ignore` ^7.0.12 → ^7.0.13
|
|
37
|
+
- dev: `tsc-alias` ^1.9.7 → ^1.9.8
|
|
38
|
+
- dev: `vitest` ^5.0.3 → ^5.0.5
|
|
39
|
+
- dev: `@vitest/coverage-istanbul` ^5.0.5 (new, `test:coverage` script)
|
|
40
|
+
- toolchain: Bun 1.4.2 → 1.4.4 (`packageManager`, Dockerfile, README badge)
|