obsidian-mcp-server 3.8.0 → 3.9.0
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 +12 -10
- package/CLAUDE.md +12 -10
- package/README.md +12 -8
- package/changelog/3.9.x/3.9.0.md +43 -0
- package/changelog/template.md +10 -3
- package/dist/config/server-config.d.ts.map +1 -1
- package/dist/config/server-config.js +6 -4
- package/dist/config/server-config.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +22 -15
- package/dist/index.js.map +1 -1
- package/dist/instructions.d.ts +4 -3
- package/dist/instructions.d.ts.map +1 -1
- package/dist/instructions.js +6 -9
- package/dist/instructions.js.map +1 -1
- package/dist/mcp-server/tools/definitions/_shared/suggest-paths.d.ts +7 -5
- package/dist/mcp-server/tools/definitions/_shared/suggest-paths.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/_shared/suggest-paths.js +11 -8
- package/dist/mcp-server/tools/definitions/_shared/suggest-paths.js.map +1 -1
- package/dist/mcp-server/tools/definitions/index.d.ts +237 -217
- package/dist/mcp-server/tools/definitions/index.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/index.js +6 -4
- package/dist/mcp-server/tools/definitions/index.js.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 +8 -1
- 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.js +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.js +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.js +1 -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 +6 -0
- 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 +8 -1
- package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.d.ts +143 -27
- 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 +87 -79
- 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 +6 -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 +10 -3
- package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.js.map +1 -1
- package/dist/services/obsidian/obsidian-service.d.ts +53 -15
- package/dist/services/obsidian/obsidian-service.d.ts.map +1 -1
- package/dist/services/obsidian/obsidian-service.js +461 -155
- package/dist/services/obsidian/obsidian-service.js.map +1 -1
- package/dist/services/obsidian/section-extractor.d.ts +62 -6
- package/dist/services/obsidian/section-extractor.d.ts.map +1 -1
- package/dist/services/obsidian/section-extractor.js +303 -62
- package/dist/services/obsidian/section-extractor.js.map +1 -1
- package/manifest.json +3 -3
- package/package.json +8 -8
- package/server.json +7 -7
- package/skills/obsidian-guide/SKILL.md +61 -0
package/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Agent Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** obsidian-mcp-server
|
|
4
|
-
**Version:** 3.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.
|
|
6
|
-
**Engines:** Bun ≥1.4.
|
|
4
|
+
**Version:** 3.9.0
|
|
5
|
+
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.22`
|
|
6
|
+
**Engines:** Bun ≥1.4.4, Node ≥24.0.0
|
|
7
7
|
**MCP SDK:** `@modelcontextprotocol/server` ^2.4.0
|
|
8
8
|
**Zod:** ^4.6.7
|
|
9
9
|
|
|
@@ -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` filters hits via `svc.policy.filterReadable`. `OBSIDIAN_DENY_PATHS` wins over every allow rule, and setting it turns on `restrictsReads`, so a deny list alone routes listings, tag counts, and JSONLogic through the scoped paths. Under `policy.restrictsReads`, `searchJsonLogic` never sends the caller's tree upstream: one `/search/` guarded by `readScopePattern` fetches the readable notes' contexts (plus, when the tree names `content`, one `GET /vault/{path}` per readable note through the service-wide `#bodyReads` pacer, at most `BODY_READ_CONCURRENCY` (32) in flight, evaluated in batches of at most 32 MiB), `filterReadable` gates them, and the note contexts carry no `links` / `backlinks` / `unresolvedLinks` (a tree whose fixed `var`, `missing`, or `missing_some` key names one fails `link_fields_unavailable` before any request; a computed key reads `null`). `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, a 10 s evaluation deadline, and `SCOPED_SEARCH_DEADLINE_MS` (45 s) over the whole search (`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. Text mode under `restrictsReads` never calls `/search/simple/`: one guarded `/search/` names the readable candidates, one `GET` per candidate body, and `SimpleSearch` (`simple-search.ts`, a port of Obsidian's `prepareSimpleSearch`) builds the hits and windows under the same 45 s deadline (`text_search_too_costly`; `context_length_too_large` counts the response the plugin would send for the readable matches alone, every match's offsets and framing with its window, against V8's longest string). `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_DENY_PATHS` / `OBSIDIAN_READ_ONLY` are the single chokepoint, and `path_forbidden` is declared on every path-taking tool's `errors[]` contract. A `denied_path` denial carries no `activeScope`, and no agent-facing surface (error data, `instructions`) names a deny prefix; only the startup log lists 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` filters hits via `svc.policy.filterReadable`. `OBSIDIAN_DENY_PATHS` wins over every allow rule, and setting it turns on `restrictsReads`, so a deny list alone routes listings, tag counts, and JSONLogic through the scoped paths. Under `policy.restrictsReads`, `searchJsonLogic` never sends the caller's tree upstream: one `/search/` guarded by `readScopePattern` fetches the readable notes' contexts (plus, when the tree names `content`, one `GET /vault/{path}` per readable note through the service-wide `#bodyReads` pacer, at most `BODY_READ_CONCURRENCY` (32) in flight, evaluated in batches of at most 32 MiB), `filterReadable` gates them, and the note contexts carry no `links` / `backlinks` / `unresolvedLinks` (a tree whose fixed `var`, `missing`, or `missing_some` key names one fails `link_fields_unavailable` before any request; a computed key reads `null`). `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, a 10 s evaluation deadline, and `SCOPED_SEARCH_DEADLINE_MS` (45 s) over the whole search (`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. Text mode under `restrictsReads` never calls `/search/simple/`: one guarded `/search/` names the readable candidates, one `GET` per candidate body, and `SimpleSearch` (`simple-search.ts`, a port of Obsidian's `prepareSimpleSearch`) builds the hits and windows under the same 45 s deadline (`text_search_too_costly`; `context_length_too_large` counts the response the plugin would send for the readable matches alone, every match's offsets and framing with its window, against V8's longest string). A scoped walk evaluates once: `pageText` and `pageJsonLogic` store the hit list a first page computed in one store (5 min past the last read, `WALK_STORE_CHARS` (32 Mi characters) across walks, least recently read evicted first), and the walk id rides `nextCursor`; a text entry is keyed by `query`, `contextLength`, and `maxMatchesPerHit` (never the scope, fixed per process, nor `pathPrefix`, applied per page) and holds copied windows, since a window sliced from a note body keeps the whole body alive. Under `restrictsReads` the `omnisearch` mode is withheld (`omnisearchAvailability`): absent from the `mode` enum, the description, and `instructions`, and nothing reaches the Omnisearch URL, the startup probe included, since Omnisearch ranks and selects its results over the whole vault. `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_DENY_PATHS` / `OBSIDIAN_READ_ONLY` are the single chokepoint, and `path_forbidden` is declared on every path-taking tool's `errors[]` contract. A `denied_path` denial carries no `activeScope`, and no agent-facing surface (error data, `instructions`) names a deny prefix; only the startup log lists them.
|
|
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
|
---
|
|
@@ -210,7 +210,7 @@ Handlers receive a unified `ctx` object. Properties this server actually uses:
|
|
|
210
210
|
| `ctx.requestInput` / `ctx.inputs` | Multi-round-trip human-in-the-loop confirmation. Always present, both protocol eras — with `OBSIDIAN_DELETE_ELICITATION=true`, `obsidian_delete_note` requests a confirmation round before the DELETE; off, it reads neither. `requestInput` returns `never`, so write it in return position. |
|
|
211
211
|
| `ctx.state` | Tenant-scoped KV. Used only for `obsidian_delete_note`'s consent records (`consent/<uuid>`, 600 s TTL), written only with `OBSIDIAN_DELETE_ELICITATION=true` — see Session posture above for provider requirements. |
|
|
212
212
|
| `ctx.auth` | Caller identity (`clientId`, `sub`) bound into each consent record; absent on stdio and `MCP_AUTH_MODE=none`, where the record carries empty strings. |
|
|
213
|
-
| `ctx.signal` | `AbortSignal` propagated to the Local REST API client so
|
|
213
|
+
| `ctx.signal` | `AbortSignal` propagated to the Local REST API client so client cancellations cut off in-flight HTTP; the request timer and the call budget join it per attempt. |
|
|
214
214
|
| `ctx.requestId` | Unique request ID — surfaces in log lines for correlation. |
|
|
215
215
|
| `ctx.tenantId` | Tenant ID from JWT or `'default'` for stdio. |
|
|
216
216
|
|
|
@@ -255,7 +255,7 @@ throw notFound(`Not found: ${display}`, data('note_missing'), { cause });
|
|
|
255
255
|
// is non-enumerable and so reaches the log without reaching the client.
|
|
256
256
|
```
|
|
257
257
|
|
|
258
|
-
|
|
258
|
+
An attempt (`#send`) is the fetch and the body read together, run under a timer of `OBSIDIAN_REQUEST_TIMEOUT_MS` joined to the request's signal, the one clock both runtimes honor (Bun ignores the undici `Agent`'s timeouts, which are off). GET/PUT/DELETE retry through `withRetry`, and every request one call makes — each attempt, each backoff, each request the handler chains — shares one budget, `CALL_DEADLINE_MS` (45 s) or the timeout when that is longer, anchored per `Context` on the call's first request, so a stalled or slow plugin settles inside the 60 s an MCP client waits. A rejection is classified inside the attempt, so the retry decision sees the typed error: a refused certificate throws `ConfigurationError` `certificate_rejected` on the first attempt; an unreachable plugin, or a connection dropped mid-body, throws `ServiceUnavailable` `obsidian_unreachable` and keeps the GET/PUT/DELETE retries; the timer or the budget running out throws `Timeout` `obsidian_timeout` (a `retry_deadline_exceeded` from `withRetry` is rethrown as `obsidian_timeout`, attempt count kept, unless the last attempt failed some other typed way, which then surfaces as it is). The timer is matched by its own abort reason, never by the `TimeoutError` name, so a caller's cancellation and a scoped search's deadline pass through untouched and are never retried; a scoped search's requests take its 45 s deadline as their budget instead of the call's, so the search still fails `*_too_costly`. All three carry an inline `recovery.hint` rather than relying on the contract fill, because they reach every tool and resource — including resources with no `errors[]` — and the operator, not the agent, fixes them.
|
|
259
259
|
|
|
260
260
|
**Fallback for ad-hoc throws** (no contract entry fits, prototype tools, service-layer code without a contract): use error factories.
|
|
261
261
|
|
|
@@ -265,7 +265,7 @@ throw notFound('Note not found', { path });
|
|
|
265
265
|
throw serviceUnavailable('Local REST API unavailable', { url }, { cause: err });
|
|
266
266
|
```
|
|
267
267
|
|
|
268
|
-
|
|
268
|
+
Local REST API responses never go through `httpErrorFromResponse`. `#throwForStatus` reads the body (already in memory from the attempt) only to pick a reason per status branch, keeps the upstream text on `cause`, and maps every other 4xx/5xx with `httpStatusToErrorCode`, re-attaching only `retryAfter` and a 501's `retryable: false`. The helper would put the upstream's reason phrase in the message and `data.statusText`, which the containment invariant keeps off the wire.
|
|
269
269
|
|
|
270
270
|
Available factories: `notFound`, `validationError`, `forbidden`, `unauthorized`, `invalidParams`, `invalidRequest`, `conflict`, `rateLimited`, `timeout`, `serviceUnavailable`, `configurationError`, `internalError`, `serializationError`, `databaseError`. Plain `Error` is also auto-classified from message patterns (`'not found'` → `NotFound`, etc.). See framework CLAUDE.md and the `api-errors` skill for the full pattern table.
|
|
271
271
|
|
|
@@ -290,7 +290,7 @@ src/
|
|
|
290
290
|
frontmatter-ops.ts # YAML frontmatter parse/serialize/edit helpers + inline tag reader + the wikilink span rule (`wikilinkCloser`) `obsidian_get_note` imports
|
|
291
291
|
markdown-blocks.ts # Block structure (code, HTML, math, tables) for inline tag detection
|
|
292
292
|
patch-instruction.ts # markdown-patch 1.x headers / 2.0 instructions, format negotiation, 2.0 map flattening
|
|
293
|
-
section-extractor.ts # Heading/block/frontmatter section extraction
|
|
293
|
+
section-extractor.ts # Heading/block/frontmatter section extraction + the repeated-link-definition guard for heading writes (`droppedDefinitionInReach`)
|
|
294
294
|
types.ts # Domain types (NoteJson, NoteTarget, PathTarget, etc.)
|
|
295
295
|
mcp-server/
|
|
296
296
|
tools/definitions/
|
|
@@ -305,6 +305,8 @@ src/
|
|
|
305
305
|
obsidian-status.resource.ts # obsidian://status
|
|
306
306
|
prompts/definitions/
|
|
307
307
|
index.ts # allPromptDefinitions = [] (intentionally empty)
|
|
308
|
+
skills/
|
|
309
|
+
obsidian-guide/SKILL.md # Published skill: gates and multi-call workflows, served as skill://obsidian-guide/SKILL.md
|
|
308
310
|
```
|
|
309
311
|
|
|
310
312
|
---
|
|
@@ -327,7 +329,7 @@ Two skill trees can live at the project root, written for different readers:
|
|
|
327
329
|
| Tree | For | Written by |
|
|
328
330
|
|:-----|:----|:-----------|
|
|
329
331
|
| `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) |
|
|
330
|
-
| `skills/` | The agents that install the server and call its tools — tool chains, identifier sources, gates | You, deliberately, with the `add-skill` skill.
|
|
332
|
+
| `skills/` | The agents that install the server and call its tools — tool chains, identifier sources, gates | You, deliberately, with the `add-skill` skill. Here: `obsidian-guide`, served through `createApp({ skills: 'skills' })` |
|
|
331
333
|
|
|
332
334
|
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.
|
|
333
335
|
|
|
@@ -412,7 +414,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
412
414
|
|
|
413
415
|
## Bundling
|
|
414
416
|
|
|
415
|
-
`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
|
|
417
|
+
`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/`, `AGENTS.md`, and `CLAUDE.md` at a package's root, and `SKILL.md` anywhere (a `skills/` deeper in a package, such as the MCP SDK's `dist/ext/skills/`, is runtime code and stays); 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/`, 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.
|
|
416
418
|
|
|
417
419
|
**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`.
|
|
418
420
|
|
package/CLAUDE.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Agent Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** obsidian-mcp-server
|
|
4
|
-
**Version:** 3.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.
|
|
6
|
-
**Engines:** Bun ≥1.4.
|
|
4
|
+
**Version:** 3.9.0
|
|
5
|
+
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.22`
|
|
6
|
+
**Engines:** Bun ≥1.4.4, Node ≥24.0.0
|
|
7
7
|
**MCP SDK:** `@modelcontextprotocol/server` ^2.4.0
|
|
8
8
|
**Zod:** ^4.6.7
|
|
9
9
|
|
|
@@ -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` filters hits via `svc.policy.filterReadable`. `OBSIDIAN_DENY_PATHS` wins over every allow rule, and setting it turns on `restrictsReads`, so a deny list alone routes listings, tag counts, and JSONLogic through the scoped paths. Under `policy.restrictsReads`, `searchJsonLogic` never sends the caller's tree upstream: one `/search/` guarded by `readScopePattern` fetches the readable notes' contexts (plus, when the tree names `content`, one `GET /vault/{path}` per readable note through the service-wide `#bodyReads` pacer, at most `BODY_READ_CONCURRENCY` (32) in flight, evaluated in batches of at most 32 MiB), `filterReadable` gates them, and the note contexts carry no `links` / `backlinks` / `unresolvedLinks` (a tree whose fixed `var`, `missing`, or `missing_some` key names one fails `link_fields_unavailable` before any request; a computed key reads `null`). `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, a 10 s evaluation deadline, and `SCOPED_SEARCH_DEADLINE_MS` (45 s) over the whole search (`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. Text mode under `restrictsReads` never calls `/search/simple/`: one guarded `/search/` names the readable candidates, one `GET` per candidate body, and `SimpleSearch` (`simple-search.ts`, a port of Obsidian's `prepareSimpleSearch`) builds the hits and windows under the same 45 s deadline (`text_search_too_costly`; `context_length_too_large` counts the response the plugin would send for the readable matches alone, every match's offsets and framing with its window, against V8's longest string). `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_DENY_PATHS` / `OBSIDIAN_READ_ONLY` are the single chokepoint, and `path_forbidden` is declared on every path-taking tool's `errors[]` contract. A `denied_path` denial carries no `activeScope`, and no agent-facing surface (error data, `instructions`) names a deny prefix; only the startup log lists 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` filters hits via `svc.policy.filterReadable`. `OBSIDIAN_DENY_PATHS` wins over every allow rule, and setting it turns on `restrictsReads`, so a deny list alone routes listings, tag counts, and JSONLogic through the scoped paths. Under `policy.restrictsReads`, `searchJsonLogic` never sends the caller's tree upstream: one `/search/` guarded by `readScopePattern` fetches the readable notes' contexts (plus, when the tree names `content`, one `GET /vault/{path}` per readable note through the service-wide `#bodyReads` pacer, at most `BODY_READ_CONCURRENCY` (32) in flight, evaluated in batches of at most 32 MiB), `filterReadable` gates them, and the note contexts carry no `links` / `backlinks` / `unresolvedLinks` (a tree whose fixed `var`, `missing`, or `missing_some` key names one fails `link_fields_unavailable` before any request; a computed key reads `null`). `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, a 10 s evaluation deadline, and `SCOPED_SEARCH_DEADLINE_MS` (45 s) over the whole search (`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. Text mode under `restrictsReads` never calls `/search/simple/`: one guarded `/search/` names the readable candidates, one `GET` per candidate body, and `SimpleSearch` (`simple-search.ts`, a port of Obsidian's `prepareSimpleSearch`) builds the hits and windows under the same 45 s deadline (`text_search_too_costly`; `context_length_too_large` counts the response the plugin would send for the readable matches alone, every match's offsets and framing with its window, against V8's longest string). A scoped walk evaluates once: `pageText` and `pageJsonLogic` store the hit list a first page computed in one store (5 min past the last read, `WALK_STORE_CHARS` (32 Mi characters) across walks, least recently read evicted first), and the walk id rides `nextCursor`; a text entry is keyed by `query`, `contextLength`, and `maxMatchesPerHit` (never the scope, fixed per process, nor `pathPrefix`, applied per page) and holds copied windows, since a window sliced from a note body keeps the whole body alive. Under `restrictsReads` the `omnisearch` mode is withheld (`omnisearchAvailability`): absent from the `mode` enum, the description, and `instructions`, and nothing reaches the Omnisearch URL, the startup probe included, since Omnisearch ranks and selects its results over the whole vault. `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_DENY_PATHS` / `OBSIDIAN_READ_ONLY` are the single chokepoint, and `path_forbidden` is declared on every path-taking tool's `errors[]` contract. A `denied_path` denial carries no `activeScope`, and no agent-facing surface (error data, `instructions`) names a deny prefix; only the startup log lists them.
|
|
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
|
---
|
|
@@ -210,7 +210,7 @@ Handlers receive a unified `ctx` object. Properties this server actually uses:
|
|
|
210
210
|
| `ctx.requestInput` / `ctx.inputs` | Multi-round-trip human-in-the-loop confirmation. Always present, both protocol eras — with `OBSIDIAN_DELETE_ELICITATION=true`, `obsidian_delete_note` requests a confirmation round before the DELETE; off, it reads neither. `requestInput` returns `never`, so write it in return position. |
|
|
211
211
|
| `ctx.state` | Tenant-scoped KV. Used only for `obsidian_delete_note`'s consent records (`consent/<uuid>`, 600 s TTL), written only with `OBSIDIAN_DELETE_ELICITATION=true` — see Session posture above for provider requirements. |
|
|
212
212
|
| `ctx.auth` | Caller identity (`clientId`, `sub`) bound into each consent record; absent on stdio and `MCP_AUTH_MODE=none`, where the record carries empty strings. |
|
|
213
|
-
| `ctx.signal` | `AbortSignal` propagated to the Local REST API client so
|
|
213
|
+
| `ctx.signal` | `AbortSignal` propagated to the Local REST API client so client cancellations cut off in-flight HTTP; the request timer and the call budget join it per attempt. |
|
|
214
214
|
| `ctx.requestId` | Unique request ID — surfaces in log lines for correlation. |
|
|
215
215
|
| `ctx.tenantId` | Tenant ID from JWT or `'default'` for stdio. |
|
|
216
216
|
|
|
@@ -255,7 +255,7 @@ throw notFound(`Not found: ${display}`, data('note_missing'), { cause });
|
|
|
255
255
|
// is non-enumerable and so reaches the log without reaching the client.
|
|
256
256
|
```
|
|
257
257
|
|
|
258
|
-
|
|
258
|
+
An attempt (`#send`) is the fetch and the body read together, run under a timer of `OBSIDIAN_REQUEST_TIMEOUT_MS` joined to the request's signal, the one clock both runtimes honor (Bun ignores the undici `Agent`'s timeouts, which are off). GET/PUT/DELETE retry through `withRetry`, and every request one call makes — each attempt, each backoff, each request the handler chains — shares one budget, `CALL_DEADLINE_MS` (45 s) or the timeout when that is longer, anchored per `Context` on the call's first request, so a stalled or slow plugin settles inside the 60 s an MCP client waits. A rejection is classified inside the attempt, so the retry decision sees the typed error: a refused certificate throws `ConfigurationError` `certificate_rejected` on the first attempt; an unreachable plugin, or a connection dropped mid-body, throws `ServiceUnavailable` `obsidian_unreachable` and keeps the GET/PUT/DELETE retries; the timer or the budget running out throws `Timeout` `obsidian_timeout` (a `retry_deadline_exceeded` from `withRetry` is rethrown as `obsidian_timeout`, attempt count kept, unless the last attempt failed some other typed way, which then surfaces as it is). The timer is matched by its own abort reason, never by the `TimeoutError` name, so a caller's cancellation and a scoped search's deadline pass through untouched and are never retried; a scoped search's requests take its 45 s deadline as their budget instead of the call's, so the search still fails `*_too_costly`. All three carry an inline `recovery.hint` rather than relying on the contract fill, because they reach every tool and resource — including resources with no `errors[]` — and the operator, not the agent, fixes them.
|
|
259
259
|
|
|
260
260
|
**Fallback for ad-hoc throws** (no contract entry fits, prototype tools, service-layer code without a contract): use error factories.
|
|
261
261
|
|
|
@@ -265,7 +265,7 @@ throw notFound('Note not found', { path });
|
|
|
265
265
|
throw serviceUnavailable('Local REST API unavailable', { url }, { cause: err });
|
|
266
266
|
```
|
|
267
267
|
|
|
268
|
-
|
|
268
|
+
Local REST API responses never go through `httpErrorFromResponse`. `#throwForStatus` reads the body (already in memory from the attempt) only to pick a reason per status branch, keeps the upstream text on `cause`, and maps every other 4xx/5xx with `httpStatusToErrorCode`, re-attaching only `retryAfter` and a 501's `retryable: false`. The helper would put the upstream's reason phrase in the message and `data.statusText`, which the containment invariant keeps off the wire.
|
|
269
269
|
|
|
270
270
|
Available factories: `notFound`, `validationError`, `forbidden`, `unauthorized`, `invalidParams`, `invalidRequest`, `conflict`, `rateLimited`, `timeout`, `serviceUnavailable`, `configurationError`, `internalError`, `serializationError`, `databaseError`. Plain `Error` is also auto-classified from message patterns (`'not found'` → `NotFound`, etc.). See framework CLAUDE.md and the `api-errors` skill for the full pattern table.
|
|
271
271
|
|
|
@@ -290,7 +290,7 @@ src/
|
|
|
290
290
|
frontmatter-ops.ts # YAML frontmatter parse/serialize/edit helpers + inline tag reader + the wikilink span rule (`wikilinkCloser`) `obsidian_get_note` imports
|
|
291
291
|
markdown-blocks.ts # Block structure (code, HTML, math, tables) for inline tag detection
|
|
292
292
|
patch-instruction.ts # markdown-patch 1.x headers / 2.0 instructions, format negotiation, 2.0 map flattening
|
|
293
|
-
section-extractor.ts # Heading/block/frontmatter section extraction
|
|
293
|
+
section-extractor.ts # Heading/block/frontmatter section extraction + the repeated-link-definition guard for heading writes (`droppedDefinitionInReach`)
|
|
294
294
|
types.ts # Domain types (NoteJson, NoteTarget, PathTarget, etc.)
|
|
295
295
|
mcp-server/
|
|
296
296
|
tools/definitions/
|
|
@@ -305,6 +305,8 @@ src/
|
|
|
305
305
|
obsidian-status.resource.ts # obsidian://status
|
|
306
306
|
prompts/definitions/
|
|
307
307
|
index.ts # allPromptDefinitions = [] (intentionally empty)
|
|
308
|
+
skills/
|
|
309
|
+
obsidian-guide/SKILL.md # Published skill: gates and multi-call workflows, served as skill://obsidian-guide/SKILL.md
|
|
308
310
|
```
|
|
309
311
|
|
|
310
312
|
---
|
|
@@ -327,7 +329,7 @@ Two skill trees can live at the project root, written for different readers:
|
|
|
327
329
|
| Tree | For | Written by |
|
|
328
330
|
|:-----|:----|:-----------|
|
|
329
331
|
| `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) |
|
|
330
|
-
| `skills/` | The agents that install the server and call its tools — tool chains, identifier sources, gates | You, deliberately, with the `add-skill` skill.
|
|
332
|
+
| `skills/` | The agents that install the server and call its tools — tool chains, identifier sources, gates | You, deliberately, with the `add-skill` skill. Here: `obsidian-guide`, served through `createApp({ skills: 'skills' })` |
|
|
331
333
|
|
|
332
334
|
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.
|
|
333
335
|
|
|
@@ -412,7 +414,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
412
414
|
|
|
413
415
|
## Bundling
|
|
414
416
|
|
|
415
|
-
`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
|
|
417
|
+
`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/`, `AGENTS.md`, and `CLAUDE.md` at a package's root, and `SKILL.md` anywhere (a `skills/` deeper in a package, such as the MCP SDK's `dist/ext/skills/`, is runtime code and stays); 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/`, 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.
|
|
416
418
|
|
|
417
419
|
**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`.
|
|
418
420
|
|
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
|
|
|
@@ -32,7 +32,7 @@ Your Obsidian vault, through the Local REST API plugin. Read, search, and write
|
|
|
32
32
|
| `obsidian_get_note` | Read a note as raw content, full structured form, document map, or a single section |
|
|
33
33
|
| `obsidian_list_notes` | List notes and folders under a vault path, recursively, with extension and name filters |
|
|
34
34
|
| `obsidian_list_tags` | List vault tags with usage counts, most-used first |
|
|
35
|
-
| `obsidian_search_notes` | Search by text, JSONLogic, or BM25-ranked Omnisearch when that plugin is reachable |
|
|
35
|
+
| `obsidian_search_notes` | Search by text, JSONLogic, or BM25-ranked Omnisearch when that plugin is reachable and reads are unscoped |
|
|
36
36
|
| `obsidian_write_note` | Create a note, replace one section, or overwrite a whole file with `overwrite: true` |
|
|
37
37
|
| `obsidian_append_to_note` | Append to a note (creating it if missing) or to one heading, block, or frontmatter field |
|
|
38
38
|
| `obsidian_patch_note` | Append, prepend, or replace against one heading, block reference, or frontmatter field |
|
|
@@ -54,6 +54,8 @@ Your Obsidian vault, through the Local REST API plugin. Read, search, and write
|
|
|
54
54
|
|
|
55
55
|
Note and tag data are also reachable through tools (`obsidian_get_note`, `obsidian_list_tags`); `obsidian://status` has no tool equivalent.
|
|
56
56
|
|
|
57
|
+
The [`obsidian-guide`](./skills/obsidian-guide/SKILL.md) skill, loaded by plugin installs and served over MCP as `skill://obsidian-guide/SKILL.md`, carries the gates and multi-call workflows no single tool describes: pinning `active` and `periodic` targets, round-tripping heading sections, paging a search before editing its hits, exact tag matching, and repeating a write after a timeout or a dropped connection.
|
|
58
|
+
|
|
57
59
|
## Capability reference
|
|
58
60
|
|
|
59
61
|
### `obsidian_get_note` <sub>tool</sub>
|
|
@@ -84,7 +86,7 @@ Note and tag data are also reachable through tools (`obsidian_get_note`, `obsidi
|
|
|
84
86
|
|
|
85
87
|
- `mode: "text"` requires every space-separated 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, and not under `OBSIDIAN_READ_PATHS` or `OBSIDIAN_DENY_PATHS`), with `glob` / `regexp` taking `[PATTERN, VALUE]`
|
|
86
88
|
- `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
|
-
- `mode: "omnisearch"` (BM25 ranking, quoted phrases, `-exclusion`, `path:` / `ext:` filters) is offered only when the Omnisearch plugin answered at startup. It keeps its top 50 hits; `path:`, `ext:`, and plain terms apply before that cap, quoted phrases and `-exclusion` after it. `truncated: true` marks a result the cap may have cut, with `truncatedReason` `cap_reached
|
|
89
|
+
- `mode: "omnisearch"` (BM25 ranking, typo tolerance, PDF/OCR via Text Extractor, quoted phrases, `-exclusion`, `path:` / `ext:` filters) is offered only when the Omnisearch plugin answered at startup and neither `OBSIDIAN_READ_PATHS` nor `OBSIDIAN_DENY_PATHS` is set. It keeps its top 50 hits; `path:`, `ext:`, and plain terms apply before that cap, quoted phrases and `-exclusion` after it. `truncated: true` marks a result the cap may have cut, with `truncatedReason` `cap_reached` or `filtered_after_cap`. Omnisearch locates `matches` for its first 10 hits only, and a note it adds because it embeds a hit has `score` 0
|
|
88
90
|
|
|
89
91
|
---
|
|
90
92
|
|
|
@@ -195,7 +197,7 @@ Built on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core): s
|
|
|
195
197
|
Obsidian-specific:
|
|
196
198
|
|
|
197
199
|
- Typed client for the [Obsidian Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api) plugin; section writes speak markdown-patch 2.0 to plugin v5.0 and later and 1.x to v4.x, chosen from the reported plugin version
|
|
198
|
-
- Heading targets take a full `Parent::Child` path or a bare leaf name; writes reject an ambiguous one with `ambiguous_section` and its `candidates`, unless exactly one match is a top-level heading. On plugin v5.0 and later, content added to a heading is set off by a blank line (a list item continues an adjacent list), and a heading inside it must sit below the section's level (`heading_outside_section`)
|
|
200
|
+
- Heading targets take a full `Parent::Child` path or a bare leaf name; writes reject an ambiguous one with `ambiguous_section` and its `candidates`, unless exactly one match is a top-level heading. On plugin v5.0 and later, content added to a heading is set off by a blank line (a list item continues an adjacent list), and a heading inside it must sit below the section's level (`heading_outside_section`). A heading write is refused with `repeated_link_definition` when a link reference definition that repeats an earlier label sits above where its section ends (for the last section, above its heading), since the plugin's parser drops it and misplaces every heading below it
|
|
199
201
|
- Folder-scoped read/write permissions, a deny list that withholds paths from both, a read-only switch, and an opt-in command-palette pair (see [Path policy](#path-policy)); server-level `instructions` on `initialize` report the active policy
|
|
200
202
|
- `obsidian_get_note` and `obsidian_open_in_ui` retry a case-mismatched path against the real filename, disclosed as `requestedPath` plus a `notice`, and add `Did you mean` suggestions to a miss; writes and deletes match the exact path
|
|
201
203
|
- Regex inputs are capped (`nameRegex` at 256 chars, `useRegex` at 1024) and rejected with `regex_unsafe` when they nest quantifiers
|
|
@@ -248,14 +250,14 @@ Generate the key in Obsidian under **Settings → Community Plugins → Local RE
|
|
|
248
250
|
| `OBSIDIAN_API_KEY` | **Required.** Bearer token for the Local REST API plugin. | — |
|
|
249
251
|
| `OBSIDIAN_BASE_URL` | Local REST API base URL; `https://127.0.0.1:27124` is the always-on HTTPS port. A trailing slash is stripped. When nothing answers, calls fail with `obsidian_unreachable`. | `http://127.0.0.1:27123` |
|
|
250
252
|
| `OBSIDIAN_VERIFY_SSL` | Verify the plugin's TLS certificate. Off by default for its self-signed cert; the relaxation applies only to an `https:` `OBSIDIAN_BASE_URL`. With `true`, an untrusted cert fails calls with `certificate_rejected`. | `false` |
|
|
251
|
-
| `OBSIDIAN_REQUEST_TIMEOUT_MS` |
|
|
253
|
+
| `OBSIDIAN_REQUEST_TIMEOUT_MS` | Milliseconds (at most 2147483647) one request attempt, headers and body, may take. `GET`, `PUT`, and `DELETE` requests retry a timed-out attempt; all the requests of one tool call share a 45 s budget (or this value, when longer), so at the default a stalled Obsidian fails the call with `obsidian_timeout` before a typical MCP client's 60 s request timeout. | `30000` |
|
|
252
254
|
| `OBSIDIAN_ENABLE_COMMANDS` | Enable `obsidian_list_commands` and `obsidian_execute_command`. Commands are opaque and can be destructive. | `false` |
|
|
253
255
|
| `OBSIDIAN_READ_PATHS` | Comma-separated folder allowlist for reads. See [Path policy](#path-policy). | unset (full vault) |
|
|
254
256
|
| `OBSIDIAN_WRITE_PATHS` | Comma-separated folder allowlist for writes. See [Path policy](#path-policy). | unset (the read scope, or the full vault without one) |
|
|
255
257
|
| `OBSIDIAN_DENY_PATHS` | Comma-separated paths withheld from reads and writes alike, overriding both allowlists. See [Path policy](#path-policy). | unset (nothing denied) |
|
|
256
258
|
| `OBSIDIAN_READ_ONLY` | Deny every write and disable the command-palette pair. | `false` |
|
|
257
259
|
| `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` |
|
|
258
|
-
| `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 |
|
|
260
|
+
| `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. Under `OBSIDIAN_READ_PATHS` or `OBSIDIAN_DENY_PATHS` it is never probed or called and the mode is withheld (see [Path policy](#path-policy)). | derived |
|
|
259
261
|
| `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http`. | `stdio` |
|
|
260
262
|
| `MCP_HTTP_PORT` | HTTP server port. | `3010` |
|
|
261
263
|
| `MCP_SESSION_MODE` | HTTP session mode: `stateful`, `stateless`, or `auto`. With `OBSIDIAN_DELETE_ELICITATION=true` and `OBSIDIAN_READ_ONLY` off, the server requires a stateful session, because the confirmation needs one on 2025-era clients, so `stateless` fails startup over HTTP. No effect on stdio. | `stateful` |
|
|
@@ -286,7 +288,8 @@ Four optional env vars limit which vault paths the tools can touch. Unset, reads
|
|
|
286
288
|
- JSONLogic: the server evaluates the tree over the readable notes, so it never reads a note outside the scope. Notes carry no `links`, `backlinks`, or `unresolvedLinks`, since Obsidian resolves a link across the whole vault: a tree naming one by a fixed `var`, `missing`, or `missing_some` key fails with `link_fields_unavailable`, and a computed key reads `null`. Match link text in `content` instead. A tree that names `content` reads each readable note with one request per note, at most 32 in flight across the server.
|
|
287
289
|
- Evaluation is bounded (1,000 levels of nesting, a work budget per note and per search, 10 s of evaluation, 45 s for the whole search); a tree past a bound fails with `logic_too_costly`. A readable note nested too deep for the evaluator (about 3,500 levels under Node) fails the search with `note_too_deep`.
|
|
288
290
|
- Text: the server never calls the plugin's text search. It asks the plugin which readable notes may hold every token, reads each candidate note, and applies Obsidian's search rule itself, so hits and context windows match the unscoped search. Past 45 s it fails with `text_search_too_costly`, and `context_length_too_large` fails where the plugin's response for the readable matches alone would pass its size ceiling.
|
|
289
|
-
-
|
|
291
|
+
- Paging: a scoped text or JSONLogic search reads the notes on its first page only. Its later pages come from the hits that page kept, for 5 minutes past the last page read and within 32 Mi characters across searches, so they show the vault as the first page found it; a cursor whose hits are gone, or sent with a different `query`, `contextLength`, `maxMatchesPerHit`, or tree, searches again.
|
|
292
|
+
- Omnisearch: the `omnisearch` mode is withheld and the plugin is never called. Omnisearch ranks and selects its results over the whole vault, so they depend on notes outside the scope. Ranking, typo tolerance, and PDF/OCR coverage are not available under a scope.
|
|
290
293
|
- Tag listings (`obsidian_list_tags`, `obsidian://tags`) under `OBSIDIAN_READ_PATHS` or `OBSIDIAN_DENY_PATHS` cover readable notes only, counting notes rather than occurrences. Write paths and read-only alone leave them vault-wide.
|
|
291
294
|
- 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. With a deny list set, each scope reads "…, except paths the operator withholds", naming none of them, and an empty scope reads `none`. The startup log prints the configured lists, the deny list included.
|
|
292
295
|
|
|
@@ -294,7 +297,7 @@ Four optional env vars limit which vault paths the tools can touch. Unset, reads
|
|
|
294
297
|
|
|
295
298
|
### Local development
|
|
296
299
|
|
|
297
|
-
Requires [Bun v1.4.
|
|
300
|
+
Requires [Bun v1.4.4](https://bun.sh/) or higher (or Node.js v24+).
|
|
298
301
|
|
|
299
302
|
- **Clone and install:**
|
|
300
303
|
|
|
@@ -344,6 +347,7 @@ If the port is reachable from other machines, set `MCP_AUTH_MODE=jwt` (with `MCP
|
|
|
344
347
|
| `src/mcp-server/tools` | Tool definitions (`*.tool.ts`) and shared schemas and helpers. |
|
|
345
348
|
| `src/mcp-server/resources` | Resource definitions (`*.resource.ts`). |
|
|
346
349
|
| `src/mcp-server/prompts` | Prompt definitions (none registered). |
|
|
350
|
+
| `skills/` | The `obsidian-guide` skill published to agents that install the server. |
|
|
347
351
|
| `tests/` | Vitest tests for tools, resources, services, and config. |
|
|
348
352
|
| `docs/` | Local REST API OpenAPI spec and the generated `tree.md`. |
|
|
349
353
|
| `changelog/` | Per-version release notes; `CHANGELOG.md` is the regenerated rollup. |
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "Request timeouts are enforced on Bun and Node, Omnisearch is withheld under a read scope, and heading writes below a repeated link definition are refused before any PATCH. Scoped text searches evaluate once per walk, and the server publishes its first skill, obsidian-guide."
|
|
3
|
+
breaking: true
|
|
4
|
+
security: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 3.9.0 — 2026-11-11
|
|
8
|
+
|
|
9
|
+
## Added
|
|
10
|
+
|
|
11
|
+
- **The `obsidian-guide` skill**, the server's first published skill, served as `skill://obsidian-guide/SKILL.md` through `createApp({ skills: 'skills' })`. The server `instructions` point at it.
|
|
12
|
+
|
|
13
|
+
## Changed
|
|
14
|
+
|
|
15
|
+
- **`obsidian_search_notes` with `mode: "omnisearch"` under `OBSIDIAN_READ_PATHS` or `OBSIDIAN_DENY_PATHS` is rejected as invalid params (-32602)** before any Omnisearch request, the startup probe included. The mode is withheld from the `mode` enum and the tool description, and the output enum `truncatedReason` no longer has `read_scope`. Scoped callers search with `text` or `jsonlogic`. ([#173](https://github.com/cyanheads/obsidian-mcp-server/issues/173))
|
|
16
|
+
- **Heading writes below a repeated link definition fail `repeated_link_definition` before any PATCH**, from `obsidian_patch_note` and from `obsidian_append_to_note` and `obsidian_write_note` with `section`. For a section that runs to the end of the note, the repeat must sit above the target heading itself; a repeat inside that last section is not refused. ([#175](https://github.com/cyanheads/obsidian-mcp-server/issues/175))
|
|
17
|
+
- **The refusal also covers writes no plugin engine would have misplaced**: a prepend whose repeat sits inside the target section; an append to a section that runs to the end of the note, with the repeat above its heading; a heading created under an ancestor heading path that occurs more than once in the note, which the check places at the farther of the two engines' candidate positions, so it can refuse a create only the other wire format would misplace; a write that would have left the note unchanged; and labels the check folds more coarsely than the parser does, a lazy blockquote continuation included. On plugin 5.x a heading write now reads the note before the PATCH, so a read of it that fails persistently or with a 4xx fails a write 3.8.0 sent. ([#175](https://github.com/cyanheads/obsidian-mcp-server/issues/175))
|
|
18
|
+
- **A request attempt that runs past `OBSIDIAN_REQUEST_TIMEOUT_MS`, headers or body, fails `obsidian_timeout` on Bun and Node.** On Bun an Omnisearch query past it now fails `omnisearch_unreachable`, where 3.8.0 had no bound and let it finish. ([#176](https://github.com/cyanheads/obsidian-mcp-server/issues/176))
|
|
19
|
+
- **A call whose requests together pass max(45 s, `OBSIDIAN_REQUEST_TIMEOUT_MS`) fails `obsidian_timeout`.** ([#176](https://github.com/cyanheads/obsidian-mcp-server/issues/176))
|
|
20
|
+
- **`OBSIDIAN_REQUEST_TIMEOUT_MS` above 2147483647 fails at startup.**
|
|
21
|
+
- **A connection dropped mid-body fails `obsidian_unreachable` and is retried on GET, PUT, and DELETE.** ([#176](https://github.com/cyanheads/obsidian-mcp-server/issues/176))
|
|
22
|
+
- **A scoped `text` search is searched once.** Later pages come from the stored hit list, kept up to 5 minutes past the last page read and 32 Mi characters across walks, and show the first page's snapshot. A walk is stored only when the hits run past the page being served. ([#177](https://github.com/cyanheads/obsidian-mcp-server/issues/177))
|
|
23
|
+
- **The Omnisearch-mode `matches`, `excerpt`, and `score` descriptions are corrected**, and `tools/list` declares `omnisearch_unreachable` only where the mode is offered. ([#173](https://github.com/cyanheads/obsidian-mcp-server/issues/173))
|
|
24
|
+
- **The `obsidian_list_tags` description gains JSONLogic tag-matching examples**, and the server `instructions` point at the `obsidian-guide` skill.
|
|
25
|
+
|
|
26
|
+
## Fixed
|
|
27
|
+
|
|
28
|
+
- **A call waiting on the plugin-version read another call started no longer fails when that other call is cancelled**; it reads the version itself.
|
|
29
|
+
- **The did-you-mean, ambiguous-path, and case-variant path suggestions in `content[]` render as code spans.**
|
|
30
|
+
- **Creating a heading under an existing ancestor finds that ancestor in time linear in the note.**
|
|
31
|
+
|
|
32
|
+
## Security
|
|
33
|
+
|
|
34
|
+
- **Under `OBSIDIAN_READ_PATHS` or `OBSIDIAN_DENY_PATHS`, Omnisearch ranked and selected its results over the whole vault**, so notes outside the scope changed what a scoped caller saw. The mode is now withheld under either. ([#173](https://github.com/cyanheads/obsidian-mcp-server/issues/173))
|
|
35
|
+
|
|
36
|
+
## Dependencies
|
|
37
|
+
|
|
38
|
+
- `@cyanheads/mcp-ts-core` ^0.13.18 → ^0.13.22
|
|
39
|
+
- `engines.bun` >=1.4.0 → >=1.4.4
|
|
40
|
+
- `undici` ^8.12.3 → ^8.12.4
|
|
41
|
+
- `@biomejs/biome` ^2.6.4 → ^2.6.6 (dev)
|
|
42
|
+
- `vitest`, `@vitest/coverage-istanbul` ^5.0.5 → ^5.1.0 (dev)
|
|
43
|
+
- `tsc-alias` ^1.9.9 → ^1.9.10 (dev)
|
package/changelog/template.md
CHANGED
|
@@ -116,14 +116,21 @@ security: false
|
|
|
116
116
|
resolves to whatever real item already owns 42, and timeline previews pull
|
|
117
117
|
in that unrelated item's metadata.
|
|
118
118
|
|
|
119
|
+
Outside reporters: when the issue or PR came from outside the project
|
|
120
|
+
(`git-wrapup` step 5 says how to tell), thank its author in the same
|
|
121
|
+
parenthetical, profile linked by full URL for the same reason:
|
|
122
|
+
|
|
123
|
+
([#38](https://github.com/cyanheads/mcp-ts-core/issues/38), thanks [@octocat](https://github.com/octocat)!)
|
|
124
|
+
|
|
119
125
|
TAG ANNOTATIONS — the annotated tag body renders as the GitHub Release body
|
|
120
126
|
via `gh release create --notes-from-tag`. It is a condensed digest of this
|
|
121
127
|
entry, never a copy, and its format is owned by the `release-and-publish`
|
|
122
128
|
skill (step 4, "Create the annotated tag"): a short subject line without the
|
|
123
129
|
version, flat headline bullets — no Keep-a-Changelog section headers, no
|
|
124
|
-
gates line — at most one deps line, issue backlinks
|
|
125
|
-
last. In release-PR mode the `git-wrapup`
|
|
126
|
-
PR body's `## Changes` and the tag copies
|
|
130
|
+
gates line — at most one deps line, issue backlinks with any outside-reporter
|
|
131
|
+
thanks, and the changelog link last. In release-PR mode the `git-wrapup`
|
|
132
|
+
skill authors those bullets as the PR body's `## Changes` and the tag copies
|
|
133
|
+
them.
|
|
127
134
|
-->
|
|
128
135
|
|
|
129
136
|
## Added
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-config.d.ts","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAwF3C,QAAA,MAAM,kBAAkB;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"server-config.d.ts","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAwF3C,QAAA,MAAM,kBAAkB;;;;;;;;;;;;iBAmEtB,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAI9D,wBAAgB,eAAe,IAAI,YAAY,CAe9C;AAED,iEAAiE;AACjE,wBAAgB,iBAAiB,IAAI,IAAI,CAExC"}
|
|
@@ -102,8 +102,8 @@ const ServerConfigSchema = z.object({
|
|
|
102
102
|
/**
|
|
103
103
|
* Normalized here rather than at each `${baseUrl}${path}` join: the schema
|
|
104
104
|
* is the one place that runs before anything reads the value, so a single
|
|
105
|
-
* transform covers `#request
|
|
106
|
-
* and `deriveOmnisearchUrl` alike (issue #121).
|
|
105
|
+
* transform covers `#request` (the HEAD size probe included), the
|
|
106
|
+
* capability probe, and `deriveOmnisearchUrl` alike (issue #121).
|
|
107
107
|
*/
|
|
108
108
|
.transform((url) => url.replace(/\/+$/, '')),
|
|
109
109
|
verifySsl: envBoolean
|
|
@@ -113,8 +113,10 @@ const ServerConfigSchema = z.object({
|
|
|
113
113
|
.number()
|
|
114
114
|
.int()
|
|
115
115
|
.positive()
|
|
116
|
+
// The longest delay a timer takes; past it both runtimes fire after 1 ms.
|
|
117
|
+
.max(2_147_483_647)
|
|
116
118
|
.default(30_000)
|
|
117
|
-
.describe('
|
|
119
|
+
.describe('Milliseconds (at most 2147483647) one attempt at a Local REST API request — headers and body — may take before it fails with `obsidian_timeout`; an Omnisearch query past it fails with `omnisearch_unreachable`. GET, PUT, and DELETE retry a timed-out attempt, and every Local REST API request one tool or resource call makes shares a budget of 45 s, or of this value when it is longer.'),
|
|
118
120
|
enableCommands: envBoolean
|
|
119
121
|
.default(false)
|
|
120
122
|
.describe('Opt-in flag for the command-palette pair (`obsidian_list_commands` + `obsidian_execute_command`). Off by default — Obsidian commands are opaque and can be destructive.'),
|
|
@@ -131,7 +133,7 @@ const ServerConfigSchema = z.object({
|
|
|
131
133
|
.string()
|
|
132
134
|
.url()
|
|
133
135
|
.optional()
|
|
134
|
-
.describe('Override URL for the Omnisearch plugin HTTP server. When unset, derives from OBSIDIAN_BASE_URL host with port 51361 (falling back to http://localhost:51361). Used to enable the optional `omnisearch` mode on `obsidian_search_notes`; if the URL is unreachable at startup, the mode is omitted from the tool schema.'),
|
|
136
|
+
.describe('Override URL for the Omnisearch plugin HTTP server. When unset, derives from OBSIDIAN_BASE_URL host with port 51361 (falling back to http://localhost:51361). Used to enable the optional `omnisearch` mode on `obsidian_search_notes`; if the URL is unreachable at startup, the mode is omitted from the tool schema. Under OBSIDIAN_READ_PATHS or OBSIDIAN_DENY_PATHS the mode is withheld and the URL is never called: Omnisearch ranks and selects its results over the whole vault, so they depend on notes outside the scope.'),
|
|
135
137
|
});
|
|
136
138
|
let _config;
|
|
137
139
|
export function getServerConfig() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* @fileoverview obsidian-mcp-server entry point. Initializes the Obsidian
|
|
4
|
-
* Local REST API service at module load so
|
|
4
|
+
* Local REST API service at module load so Omnisearch availability is decided
|
|
5
5
|
* before tools are constructed — `obsidian_search_notes` is built via a
|
|
6
|
-
* factory that takes
|
|
7
|
-
*
|
|
6
|
+
* factory that takes it as input, so the `omnisearch` mode appears in the tool
|
|
7
|
+
* schema only with reads unscoped and the plugin actually reachable.
|
|
8
8
|
* @module index
|
|
9
9
|
*/
|
|
10
10
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* @fileoverview obsidian-mcp-server entry point. Initializes the Obsidian
|
|
4
|
-
* Local REST API service at module load so
|
|
4
|
+
* Local REST API service at module load so Omnisearch availability is decided
|
|
5
5
|
* before tools are constructed — `obsidian_search_notes` is built via a
|
|
6
|
-
* factory that takes
|
|
7
|
-
*
|
|
6
|
+
* factory that takes it as input, so the `omnisearch` mode appears in the tool
|
|
7
|
+
* schema only with reads unscoped and the plugin actually reachable.
|
|
8
8
|
* @module index
|
|
9
9
|
*/
|
|
10
10
|
import { createApp, disabledTool } from '@cyanheads/mcp-ts-core';
|
|
@@ -13,21 +13,22 @@ import { getServerConfig } from './config/server-config.js';
|
|
|
13
13
|
import { buildInstructions } from './instructions.js';
|
|
14
14
|
import { allPromptDefinitions } from './mcp-server/prompts/definitions/index.js';
|
|
15
15
|
import { allResourceDefinitions } from './mcp-server/resources/definitions/index.js';
|
|
16
|
-
import { buildDeleteNoteTool, buildSearchNotesTool, commandToolDefinitions, readToolDefinitions, writeToolDefinitions, } from './mcp-server/tools/definitions/index.js';
|
|
16
|
+
import { buildDeleteNoteTool, buildSearchNotesTool, commandToolDefinitions, omnisearchAvailability, readToolDefinitions, writeToolDefinitions, } from './mcp-server/tools/definitions/index.js';
|
|
17
17
|
import { getObsidianService, initObsidianService } from './services/obsidian/obsidian-service.js';
|
|
18
18
|
import { PathPolicy } from './services/obsidian/path-policy.js';
|
|
19
19
|
const config = getServerConfig();
|
|
20
20
|
const policy = new PathPolicy(config);
|
|
21
21
|
/**
|
|
22
|
-
* Init the service at module load (rather than inside `setup()`) so
|
|
23
|
-
* Omnisearch
|
|
24
|
-
*
|
|
22
|
+
* Init the service at module load (rather than inside `setup()`) so
|
|
23
|
+
* Omnisearch availability — the probe, or `withheld` under a read scope with
|
|
24
|
+
* no probe — is known before tool construction. `setup()` runs after tools
|
|
25
|
+
* are passed into `createApp()`, which is too late to influence the
|
|
25
26
|
* search-notes schema.
|
|
26
27
|
*/
|
|
27
28
|
initObsidianService(config);
|
|
28
29
|
const obsidian = getObsidianService();
|
|
29
|
-
const
|
|
30
|
-
const searchNotesTool = buildSearchNotesTool({
|
|
30
|
+
const omnisearch = await omnisearchAvailability(obsidian);
|
|
31
|
+
const searchNotesTool = buildSearchNotesTool({ omnisearch });
|
|
31
32
|
const writeToolSet = [
|
|
32
33
|
...writeToolDefinitions,
|
|
33
34
|
buildDeleteNoteTool({ elicitation: config.deleteElicitation }),
|
|
@@ -63,10 +64,12 @@ const { services } = await createApp({
|
|
|
63
64
|
prompts: allPromptDefinitions,
|
|
64
65
|
instructions: buildInstructions({
|
|
65
66
|
enableCommands: config.enableCommands,
|
|
66
|
-
|
|
67
|
+
omnisearch,
|
|
67
68
|
policy,
|
|
68
69
|
readOnly: config.readOnly,
|
|
69
70
|
}),
|
|
71
|
+
/** Root `skills/`, served as `skill://<name>/SKILL.md` to clients without the plugin. */
|
|
72
|
+
skills: 'skills',
|
|
70
73
|
/**
|
|
71
74
|
* With `OBSIDIAN_DELETE_ELICITATION=true` (and writes allowed),
|
|
72
75
|
* `obsidian_delete_note` confirms through `ctx.requestInput`, which a
|
|
@@ -94,7 +97,9 @@ const { services } = await createApp({
|
|
|
94
97
|
* are readable; a write needs read access), so the LLM (or operator) can
|
|
95
98
|
* self-correct without scrolling the log. The deny list (`denyPaths`, present
|
|
96
99
|
* when set) appears only here: the agent-facing surfaces say paths are
|
|
97
|
-
* withheld without naming them.
|
|
100
|
+
* withheld without naming them. `omnisearch` is the startup
|
|
101
|
+
* `omnisearchAvailability`; under a read scope it is `withheld` and the
|
|
102
|
+
* plugin was never probed.
|
|
98
103
|
*/
|
|
99
104
|
const bannerCtx = requestContextService.createRequestContext({
|
|
100
105
|
operation: 'startup',
|
|
@@ -103,13 +108,15 @@ const bannerCtx = requestContextService.createRequestContext({
|
|
|
103
108
|
enableCommands: config.enableCommands && !config.readOnly,
|
|
104
109
|
deleteElicitation: deleteGateActive,
|
|
105
110
|
omnisearchUrl: obsidian.omnisearchUrl,
|
|
106
|
-
|
|
111
|
+
omnisearch,
|
|
107
112
|
},
|
|
108
113
|
});
|
|
109
114
|
services.logger.info('Path policy', bannerCtx);
|
|
110
|
-
services.logger.info(
|
|
111
|
-
|
|
112
|
-
: `Omnisearch not reachable at ${obsidian.omnisearchUrl} — \`omnisearch\` mode omitted from \`obsidian_search_notes\`. Set OBSIDIAN_OMNISEARCH_URL or enable the Omnisearch plugin's HTTP server to use it.`,
|
|
115
|
+
services.logger.info({
|
|
116
|
+
offered: `Omnisearch reachable at ${obsidian.omnisearchUrl} — \`omnisearch\` mode enabled on \`obsidian_search_notes\`.`,
|
|
117
|
+
unreachable: `Omnisearch not reachable at ${obsidian.omnisearchUrl} — \`omnisearch\` mode omitted from \`obsidian_search_notes\`. Set OBSIDIAN_OMNISEARCH_URL or enable the Omnisearch plugin's HTTP server to use it.`,
|
|
118
|
+
withheld: 'Omnisearch not probed — `omnisearch` mode withheld from `obsidian_search_notes` because OBSIDIAN_READ_PATHS or OBSIDIAN_DENY_PATHS scopes reads. Omnisearch ranks and selects its results over the whole vault, so they depend on notes outside the scope; `text` and `jsonlogic` modes search the readable notes alone.',
|
|
119
|
+
}[omnisearch], bannerCtx);
|
|
113
120
|
if (policy.readOnlyShadowsWritePaths) {
|
|
114
121
|
services.logger.warning('OBSIDIAN_WRITE_PATHS is set but ignored because OBSIDIAN_READ_ONLY=true. Unset one of the two to remove the conflict.', bannerCtx);
|
|
115
122
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;GAOG;AAEH,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,sBAAsB,EAAE,MAAM,6CAA6C,CAAC;AACrF,OAAO,EACL,mBAAmB,EACnB,oBAAoB,EACpB,sBAAsB,EACtB,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,yCAAyC,CAAC;AAClG,OAAO,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AAEhE,MAAM,MAAM,GAAG,eAAe,EAAE,CAAC;AACjC,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC;AAEtC
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|