Skip to content

obsidian mcp server changelog

io.github.cyanheads/obsidian-mcp-server

9 changes recorded. MCP Harbor re-reads this listing from the official registry every six hours and asks the live server what it exposes, and keeps what changed between one look and the next.

  • AGENTS.md +12 −10

    https://raw.githubusercontent.com/cyanheads/obsidian-mcp-server/HEAD/AGENTS.md

    − **Version:** 3.8.0− **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.18`− **Engines:** Bun ≥1.4.0, Node ≥24.0.0− - **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 tur− | `ctx.signal` | `AbortSignal` propagated to the Local REST API client so per-request timeouts and client cancellations cut off in-flight HTTP. |− A `fetch` that rejects before any response is classified inside the attempt (`#send`), so the retry decision sees the typed error: a refused certificate throws `ConfigurationError` `certificate_rejected` on the first attempt, an unreachable plugin throws `ServiceUnavailable` `obsidian_unreachable` and keeps the GET/PUT/DELETE retries. Both carry an inline `recovery.hint` rather than relying on the contract fill, because they reach every tool and resource — including resources with no `errors[]` − For HTTP responses from the Local REST API, use `httpErrorFromResponse(response, { service: 'obsidian-rest' })` from `/utils` — maps the full status table (401/403/408/422/429/5xx) and captures body + `Retry-After`.−       section-extractor.ts              # Heading/block/frontmatter section extraction− | `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 |− `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+ **Version:** 3.9.0+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.22`+ **Engines:** Bun ≥1.4.4, Node ≥24.0.0+ - **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 tur+ | `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. |+ 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 firs+ 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.+       section-extractor.ts              # Heading/block/frontmatter section extraction + the repeated-link-definition guard for heading writes (`droppedDefinitionInReach`)+ skills/+   obsidian-guide/SKILL.md               # Published skill: gates and multi-call workflows, served as skill://obsidian-guide/SKILL.md+ | `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' })` |+ `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

  • server.json 1 field changed
    version
    3.8.0 3.9.0

  • AGENTS.md +35 −15

    https://raw.githubusercontent.com/cyanheads/obsidian-mcp-server/HEAD/AGENTS.md

    − **Version:** 3.7.0− **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.12`− **MCP SDK:** `@modelcontextprotocol/server` ^2.2.0− **Zod:** ^4.6.5− - **The delete confirmation is opt-in; when on, it is gated on a consent record.** `OBSIDIAN_DELETE_ELICITATION` (default `false`) decides whether `obsidian_delete_note` confirms through `ctx.requestInput`. Off is the default for two reasons: a client that declares elicitation but cannot render the form declines every round automatically, which the server cannot tell from a real decline (#151), and the round forces a stateful HTTP session, which locks out `MCP_SESSION_MODE=stateless`. Off, `OBSI− - **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` post-filters hits via `svc.policy.filterReadable`, and `listTags` collects per-note tags through it when `policy.r−               'Times the tag, or a tag nested under it, occurs across the vault. When OBSIDIAN_READ_PATHS is set: the number of readable notes carrying the tag or a tag nested under it.',−   /** Path-policy allowlists — comma-separated, prefix-based, case-insensitive. Unset = full vault. */− `teardown` closes the undici `Agent` dispatcher `ObsidianService` holds for the Local REST API and Omnisearch, releasing its keep-alive sockets. It runs after the transport stops accepting requests, on every shutdown path.−       frontmatter-ops.ts                # YAML frontmatter parse/serialize/edit helpers + inline tag reader−       types.ts                          # Domain types (NoteJson, NoteTarget, etc.)− Skills are modular instructions in `framework-skills/` at the project root. 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. The 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 these development skills to every agent that installs it. Keep `skills/` free for skills meant f− **Agent skill directory:** Copy 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.− | `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) |− `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 two classes of `node_modules/**` content that root-anchored `.mcpbignore` patterns cannot reach: dependency-shipped agent docs (`framework-skills/`, `skills/`, `.claude/`, `.agents/`, `SKILL.md`) and platform-specific native bindings, which would otherwise lock the bundle to the platform it was + **Version:** 3.8.0+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.18`+ **MCP SDK:** `@modelcontextprotocol/server` ^2.4.0+ **Zod:** ^4.6.7+ - **The delete confirmation is opt-in; when on, it is gated on a consent record.** `OBSIDIAN_DELETE_ELICITATION` (default `false`) decides whether `obsidian_delete_note` confirms through `ctx.requestInput`. Off is the default for two reasons: a client that declares elicitation but cannot render the form declines every round automatically, which the server cannot tell from a real decline (#151), and the round forces a stateful HTTP session, which locks out `MCP_SESSION_MODE=stateless`. Off, `OBSI+ - **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 tur+               'Times the tag, or a tag nested under it, occurs across the vault. When OBSIDIAN_READ_PATHS or OBSIDIAN_DENY_PATHS is set: the number of readable notes carrying the tag or a tag nested under it.',+   /** Path-policy lists — comma-separated, prefix-based, case-insensitive. Allowlists unset = full vault; the deny list wins over both. */+   denyPaths: envPathList,+     denyPaths: 'OBSIDIAN_DENY_PATHS',+ `teardown` closes the undici `Agent` dispatcher `ObsidianService` holds for the Local REST API and Omnisearch, releasing its keep-alive sockets, and disposes the note-body read queue, rejecting reads still waiting. It runs after the transport stops accepting requests, on every shutdown path.+   instructions.ts                       # buildInstructions() — server `instructions`, effective path scope included+       path-policy.ts                    # PathPolicy — OBSIDIAN_READ_PATHS / WRITE_PATHS / DENY_PATHS / READ_ONLY gate+       jsonlogic-deps.d.ts               # Type declarations for json-logic-js and glob-to-regexp+       jsonlogic-evaluator.ts            # Scoped JSONLogic evaluation: bounds, worker hand-off, logic_* errors+       jsonlogic-worker.ts               # Worker entry: the plugin's JSONLogic operators, hardened and budgeted+       simple-search.ts                  # In-server port of the plugin's /search/simple/ matching, for text search under a read scope+       frontmatter-ops.ts                # YAML frontmatter parse/serialize/edit helpers + inline tag reader + the wikilink span rule (`wikilinkCloser`) `obsidian_get_note` imports+       types.ts                          # Domain types (NoteJson, NoteTarget, PathTarget, etc.)+       _shared/suggest-paths.ts          # Case-mismatch fallback and suggestions for reads; names the existing note on a `case_variant_exists` write+ Two skill trees can live at the project root, written for different readers:+ | Tree | For | Written by |+ |:-----|:----|:-----------|+ | `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) |+ | `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 |+ 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.+ 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.+ **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.+ | `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 |+ | `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) |+ | `bun run test:coverage` | Run tests with Istanbul coverage |+ `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+   --build-arg APP_VERSION=<version> \+ - [ ] `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+ - [ ] Any new root dev path has a root-anchored line in `.mcpbignore`; `bun run bundle` and `lint:packaging` pass

  • server.json 2 fields changed
    env_vars
    OBSIDIAN_API_KEY, OBSIDIAN_BASE_URL, OBSIDIAN_VERIFY_SSL, OBSIDIAN_REQUEST_TIMEOUT_MS, OBSIDIAN_ENABLE_COMMANDS, OBSIDIAN_READ_PATHS, OBSIDIAN_WRITE_PATHS, OBSIDIAN_READ_ONLY, OBSIDIAN_DELETE_ELICITATION, OBSIDIAN_OMNISEARCH_URL, MCP_LOG_LEVEL OBSIDIAN_API_KEY, OBSIDIAN_BASE_URL, OBSIDIAN_VERIFY_SSL, OBSIDIAN_REQUEST_TIMEOUT_MS, OBSIDIAN_ENABLE_COMMANDS, OBSIDIAN_READ_PATHS, OBSIDIAN_WRITE_PATHS, OBSIDIAN_DENY_PATHS, OBSIDIAN_READ_ONLY, OBSIDIAN_DELETE_ELICITATION, OBSIDIAN_OMNISEARCH_URL, MCP_LOG_LEVEL
    version
    3.7.2 3.8.0

  • server.json 1 field changed
    version
    3.7.1 3.7.2

  • server.json 1 field changed
    version
    3.7.0 3.7.1

  • AGENTS.md +38 −27

    https://raw.githubusercontent.com/cyanheads/obsidian-mcp-server/HEAD/AGENTS.md

    − **Version:** 3.6.0− **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.6`− **MCP SDK:** `@modelcontextprotocol/server` ^2.0.0− - **Confirm destructive ops with `ctx.requestInput`.** `obsidian_delete_note` reads `ctx.inputs` first and `return ctx.requestInput(...)` when the answer is missing; the handler is re-entered with it. Never `await` user input mid-handler.− - **Path-policy gating goes through `PathPolicy`.** Every path-taking method on `ObsidianService` calls `policy.assertReadable` / `assertWritable` before the upstream HTTP call; `obsidian_search_notes` post-filters hits via `svc.policy.filterReadable`. 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.− A small read-only tool that wraps a single upstream endpoint, normalizes the response into the output schema, and renders a markdown twin in `format()`. Reduced for illustration — the live definition also carries `nameRegex` / `minCount` / `limit` inputs, the count-descending sort and cap they feed, an `errors[]` contract, and an `enrichment` block.−           count: z.number().describe('Usage count across the vault.'),− For a destructive tool with human-in-the-loop confirmation, see `obsidian-delete-note.tool.ts` — it suspends with `ctx.requestInput` for an embedded `elicitation/create` round, branches on `ctx.inputs.view()` so a declined prompt fails instead of re-asking, and carries the `destructiveHint` annotation.− const envBoolean = z.preprocess((val) => {−   if (val === undefined || val === null || val === '') return;−   if (typeof val === 'boolean') return val;−   return String(val).toLowerCase().trim() === 'true' || val === '1';− }, z.boolean());−   sessionMode: 'stateful',− `sessionMode: 'stateful'` is the default, not a requirement: `obsidian_delete_note` confirms through `ctx.requestInput`, which a 2025-era HTTP client can only answer on a stateful session. An explicit `MCP_SESSION_MODE` still wins, so an operator who deliberately runs `stateless` gets a working server whose delete confirmation is refused with `client_capability_missing` on those clients. Don't switch it to `require: 'stateful'`.− | `ctx.requestInput` / `ctx.inputs` | Multi-round-trip human-in-the-loop confirmation. Always present, both protocol eras — `obsidian_delete_note` requests a confirmation round before the DELETE. `requestInput` returns `never`, so write it in return position. |− The framework also provides `ctx.state`. It isn't used by this server — Obsidian is single-vault and stateless from the server's perspective, so per-tenant KV isn't needed. See the framework `CLAUDE.md` for the full surface.− **Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable? }]` on `tool()` / `resource()` to receive a typed `ctx.fail(reason, …)` keyed by the declared reason union. TypeScript catches `ctx.fail('typo')` at compile time, `data.reason` is auto-populated for observability, and the linter enforces conformance against the handler body. The `recovery` field is required descriptive metadata (≥ 5 words, lint-validated) — it's the single source of truth for the −   // Static recovery — pulled from the contract via ctx.recoveryFor.−   if (!note) throw ctx.fail('note_missing', `Note ${input.path} not found`, {−     ...ctx.recoveryFor('note_missing'),−   });− Services that accept `ctx` use the same resolver for parity. The Obsidian service threads `ctx` into `#throwForStatus` and spreads `ctx.recoveryFor(reason)` per status branch, so service-side throws carry the calling tool's contract recovery onto the wire:− // where data(reason) does: { ...callerIdentifier(path), reason, ...ctx.recoveryFor(reason) }− A `fetch` that rejects before any response is classified inside the attempt (`#send`), so the retry decision sees the typed error: a refused certificate throws `ConfigurationError` `certificate_rejected` on the first attempt, an unreachable plugin throws `ServiceUnavailable` `obsidian_unreachable` and keeps the GET/PUT/DELETE retries. Both carry an inline `recovery.hint` rather than `ctx.recoveryFor`, because they reach every tool and resource — including resources with no `errors[]` — and the o−       index.ts                          # read/write/command tool sets + buildSearchNotesTool factory (Omnisearch-aware)− **Every release goes through a gated release PR** — `git-wrapup`'s "Release PR mode", mode `gated`. Three separate runs, never one: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the changelog entry plus a gates section); `release-pr-review` reviews and fixes on that branch (each fix an ordinary commit on top of the stack, pushed plainly — nothing already pushed is ever rewritten, so `main` keeps the record of w+ **Version:** 3.7.0+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.12`+ **MCP SDK:** `@modelcontextprotocol/server` ^2.2.0+ - **The delete confirmation is opt-in; when on, it is gated on a consent record.** `OBSIDIAN_DELETE_ELICITATION` (default `false`) decides whether `obsidian_delete_note` confirms through `ctx.requestInput`. Off is the default for two reasons: a client that declares elicitation but cannot render the form declines every round automatically, which the server cannot tell from a real decline (#151), and the round forces a stateful HTTP session, which locks out `MCP_SESSION_MODE=stateless`. Off, `OBSI+ - **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.+ - **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` post-filters hits via `svc.policy.filterReadable`, and `listTags` collects per-note tags through it when `policy.r+ A small read-only tool that wraps a single service call, normalizes the response into the output schema, and renders a markdown twin in `format()`. Reduced for illustration — the live definition also carries `nameRegex` / `minCount` / `limit` inputs, the count-descending sort and cap they feed, an `errors[]` contract, and an `enrichment` block.+           count: z+             .number()+             .describe(+               'Times the tag, or a tag nested under it, occurs across the vault. When OBSIDIAN_READ_PATHS is set: the number of readable notes carrying the tag or a tag nested under it.',+             ),+ For a destructive tool with optional human-in-the-loop confirmation, see `obsidian-delete-note.tool.ts`. `src/index.ts` builds it with `buildDeleteNoteTool({ elicitation })` from `OBSIDIAN_DELETE_ELICITATION`, so the description states the active mode; the exported `obsidianDeleteNote` is the off build, kept as the specimen the definition linter and tests import. Both modes check write scope on the resolved path, then read the note before the DELETE. That read is the folder guard (`path_is_direc+ /** true/false/1/0/yes/no/on/off, case-insensitive; anything else fails startup. */+ const envBoolean = z.union([z.boolean(), z.stringbool()]);+   /** Opt-in delete confirmation round; when true and writes are allowed, HTTP requires a stateful session. */+   deleteElicitation: envBoolean.default(false),+   omnisearchUrl: z.string().url().optional(),+     deleteElicitation: 'OBSIDIAN_DELETE_ELICITATION',+     omnisearchUrl: 'OBSIDIAN_OMNISEARCH_URL',+ const deleteGateActive = config.deleteElicitation && !config.readOnly;+   sessionMode: deleteGateActive+     ? { default: 'stateful', require: 'stateful' }+     : { default: 'stateful' },+ Unset, `MCP_SESSION_MODE` runs `stateful`. With `OBSIDIAN_DELETE_ELICITATION=true`, `sessionMode` declares a requirement, not just a default: `obsidian_delete_note` then confirms through `ctx.requestInput`, which a 2025-era HTTP client can only answer on a stateful session, so under stateless HTTP the tool would be unusable for those clients. In that mode, when the resolved HTTP mode is `stateless` (an explicit `MCP_SESSION_MODE=stateless`), startup fails with a `ConfigurationError` naming the c+ The delete confirmation's consent records (on mode only) live in `ctx.state`, backed by the framework storage provider (`STORAGE_PROVIDER_TYPE`, default `in-memory`, process-local). That holds for stdio and for a single stateful HTTP instance, where every round of a confirmation reaches the process that asked. A multi-instance deployment, where a retry can land on another instance, needs a shared provider — `filesystem`, `supabase`, or `cloudflare-d1`, never `cloudflare-kv` (eventually consisten+ | `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. |+ | `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. |+ | `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. |+ See the framework `CLAUDE.md` for the full surface.+ **Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()` / `resource()` to receive `ctx.fail(reason, …)` typed against the reason union. TypeScript catches typos at compile time, `data.reason` is auto-populated for observability, linter enforces conformance against the handler body. `recovery` is required (≥ 5 words, lint-validated) — the single source of truth for the agent's next move. The framework puts it on the+   // Static recovery — the framework fills the contract's hint onto the wire.+   if (!note) throw ctx.fail('note_missing', `Note ${input.path} not found`);+ Service-side throws carry only `data.reason`; the tool and resource handler factories fill the calling definition's contract recovery from it, so `#throwForStatus` sets the reason per status branch and nothing else:+ // where data(reason) does: { ...callerIdentifier(path), reason }+ A `fetch` that rejects before any response is classified inside the attempt (`#send`), so the retry decision sees the typed error: a refused certificate throws `ConfigurationError` `certificate_rejected` on the first attempt, an unreachable plugin throws `ServiceUnavailable` `obsidian_unreachable` and keeps the GET/PUT/DELETE retries. Both carry an inline `recovery.hint` rather than relying on the contract fill, because they reach every tool and resource — including resources with no `errors[]` +       index.ts                          # read/write/command tool sets + buildSearchNotesTool (Omnisearch-aware) and buildDeleteNoteTool (elicitation mode) factories+ **Every release goes through a gated release PR** — `git-wrapup`'s "Release PR mode", mode `gated`. Three separate runs, never one: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the release digest: theme line, `## Changes`, `## Gates`, changelog link last); `release-pr-review` reviews and fixes on that branch (each fix an ordinary commit on top of the stack, pushed plainly — nothing already pushed is ever rewri

  • server.json 2 fields changed
    env_vars
    OBSIDIAN_API_KEY, OBSIDIAN_BASE_URL, OBSIDIAN_VERIFY_SSL, OBSIDIAN_REQUEST_TIMEOUT_MS, OBSIDIAN_ENABLE_COMMANDS, OBSIDIAN_READ_PATHS, OBSIDIAN_WRITE_PATHS, OBSIDIAN_READ_ONLY, OBSIDIAN_OMNISEARCH_URL, MCP_LOG_LEVEL OBSIDIAN_API_KEY, OBSIDIAN_BASE_URL, OBSIDIAN_VERIFY_SSL, OBSIDIAN_REQUEST_TIMEOUT_MS, OBSIDIAN_ENABLE_COMMANDS, OBSIDIAN_READ_PATHS, OBSIDIAN_WRITE_PATHS, OBSIDIAN_READ_ONLY, OBSIDIAN_DELETE_ELICITATION, OBSIDIAN_OMNISEARCH_URL, MCP_LOG_LEVEL
    version
    3.6.1 3.7.0

  • server.json 1 field changed
    version
    3.6.0 3.6.1