obsidian-mcp-server 3.1.0 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CLAUDE.md +24 -9
  2. package/README.md +28 -1
  3. package/changelog/3.1.x/3.1.1.md +21 -0
  4. package/changelog/3.1.x/3.1.2.md +23 -0
  5. package/dist/config/server-config.d.ts +5 -2
  6. package/dist/config/server-config.d.ts.map +1 -1
  7. package/dist/config/server-config.js +73 -0
  8. package/dist/config/server-config.js.map +1 -1
  9. package/dist/index.js +39 -5
  10. package/dist/index.js.map +1 -1
  11. package/dist/mcp-server/tools/definitions/index.d.ts +793 -10
  12. package/dist/mcp-server/tools/definitions/index.d.ts.map +1 -1
  13. package/dist/mcp-server/tools/definitions/index.js +15 -9
  14. package/dist/mcp-server/tools/definitions/index.js.map +1 -1
  15. package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.d.ts +7 -1
  16. package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.d.ts.map +1 -1
  17. package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.js +9 -0
  18. package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.js.map +1 -1
  19. package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.d.ts +14 -0
  20. package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.d.ts.map +1 -1
  21. package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.js +20 -1
  22. package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.js.map +1 -1
  23. package/dist/mcp-server/tools/definitions/obsidian-execute-command.tool.d.ts +1 -0
  24. package/dist/mcp-server/tools/definitions/obsidian-execute-command.tool.d.ts.map +1 -1
  25. package/dist/mcp-server/tools/definitions/obsidian-execute-command.tool.js +1 -0
  26. package/dist/mcp-server/tools/definitions/obsidian-execute-command.tool.js.map +1 -1
  27. package/dist/mcp-server/tools/definitions/obsidian-get-note.tool.d.ts +14 -0
  28. package/dist/mcp-server/tools/definitions/obsidian-get-note.tool.d.ts.map +1 -1
  29. package/dist/mcp-server/tools/definitions/obsidian-get-note.tool.js +17 -0
  30. package/dist/mcp-server/tools/definitions/obsidian-get-note.tool.js.map +1 -1
  31. package/dist/mcp-server/tools/definitions/obsidian-list-notes.tool.d.ts +7 -0
  32. package/dist/mcp-server/tools/definitions/obsidian-list-notes.tool.d.ts.map +1 -1
  33. package/dist/mcp-server/tools/definitions/obsidian-list-notes.tool.js +18 -3
  34. package/dist/mcp-server/tools/definitions/obsidian-list-notes.tool.js.map +1 -1
  35. package/dist/mcp-server/tools/definitions/obsidian-manage-frontmatter.tool.d.ts +14 -0
  36. package/dist/mcp-server/tools/definitions/obsidian-manage-frontmatter.tool.d.ts.map +1 -1
  37. package/dist/mcp-server/tools/definitions/obsidian-manage-frontmatter.tool.js +17 -0
  38. package/dist/mcp-server/tools/definitions/obsidian-manage-frontmatter.tool.js.map +1 -1
  39. package/dist/mcp-server/tools/definitions/obsidian-manage-tags.tool.d.ts +14 -0
  40. package/dist/mcp-server/tools/definitions/obsidian-manage-tags.tool.d.ts.map +1 -1
  41. package/dist/mcp-server/tools/definitions/obsidian-manage-tags.tool.js +17 -1
  42. package/dist/mcp-server/tools/definitions/obsidian-manage-tags.tool.js.map +1 -1
  43. package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.d.ts +6 -0
  44. package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.d.ts.map +1 -1
  45. package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.js +14 -4
  46. package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.js.map +1 -1
  47. package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.d.ts +16 -2
  48. package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.d.ts.map +1 -1
  49. package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.js +16 -0
  50. package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.js.map +1 -1
  51. package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.d.ts +14 -0
  52. package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.d.ts.map +1 -1
  53. package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.js +17 -1
  54. package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.js.map +1 -1
  55. package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.d.ts +3 -0
  56. package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.d.ts.map +1 -1
  57. package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.js +21 -4
  58. package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.js.map +1 -1
  59. package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.d.ts +6 -0
  60. package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.d.ts.map +1 -1
  61. package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.js +13 -1
  62. package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.js.map +1 -1
  63. package/dist/services/obsidian/obsidian-service.d.ts +7 -0
  64. package/dist/services/obsidian/obsidian-service.d.ts.map +1 -1
  65. package/dist/services/obsidian/obsidian-service.js +132 -28
  66. package/dist/services/obsidian/obsidian-service.js.map +1 -1
  67. package/dist/services/obsidian/path-policy.d.ts +56 -0
  68. package/dist/services/obsidian/path-policy.d.ts.map +1 -0
  69. package/dist/services/obsidian/path-policy.js +154 -0
  70. package/dist/services/obsidian/path-policy.js.map +1 -0
  71. package/package.json +7 -8
  72. package/server.json +41 -3
package/CLAUDE.md CHANGED
@@ -34,7 +34,8 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
34
34
  - **Check `ctx.elicit`** for presence before calling — used by `obsidian_delete_note` to confirm destructive ops.
35
35
  - **All Obsidian access goes through `getObsidianService()`.** No direct `fetch()` calls to the Local REST API in tools/resources — the service centralizes auth, TLS, timeouts, and `ctx.signal` propagation.
36
36
  - **Secrets in env vars only.** `OBSIDIAN_API_KEY` is required; never hardcoded.
37
- - **Command-palette tools are opt-in.** `obsidian_list_commands` and `obsidian_execute_command` are registered together only when `OBSIDIAN_ENABLE_COMMANDS=true` — Obsidian commands are opaque and can be destructive.
37
+ - **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.
38
+ - **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.
38
39
 
39
40
  ---
40
41
 
@@ -138,6 +139,10 @@ const ServerConfigSchema = z.object({
138
139
  verifySsl: envBoolean.default(false),
139
140
  requestTimeoutMs: z.coerce.number().int().positive().default(30_000),
140
141
  enableCommands: envBoolean.default(false),
142
+ /** Path-policy allowlists — comma-separated, prefix-based, case-insensitive. Unset = full vault. */
143
+ readPaths: envPathList,
144
+ writePaths: envPathList,
145
+ readOnly: envBoolean.default(false),
141
146
  });
142
147
 
143
148
  let _config: z.infer<typeof ServerConfigSchema> | undefined;
@@ -148,6 +153,9 @@ export function getServerConfig() {
148
153
  verifySsl: 'OBSIDIAN_VERIFY_SSL',
149
154
  requestTimeoutMs: 'OBSIDIAN_REQUEST_TIMEOUT_MS',
150
155
  enableCommands: 'OBSIDIAN_ENABLE_COMMANDS',
156
+ readPaths: 'OBSIDIAN_READ_PATHS',
157
+ writePaths: 'OBSIDIAN_WRITE_PATHS',
158
+ readOnly: 'OBSIDIAN_READ_ONLY',
151
159
  });
152
160
  return _config;
153
161
  }
@@ -177,25 +185,33 @@ The framework also provides `ctx.state`, `ctx.sample`, and `ctx.progress`. They
177
185
 
178
186
  Handlers throw — the framework catches, classifies, and formats.
179
187
 
180
- **Recommended: typed error contract.** Declare `errors: [{ reason, code, when, retryable? }]` on `tool()` / `resource()` to advertise the failure surface in `tools/list` (under `_meta['mcp-ts-core/errors']`) and 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. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`) bubble freely and don't need declaring.
188
+ **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 recovery hint that flows to the wire. Spread `ctx.recoveryFor('reason')` into `data` to opt the contract recovery onto the wire (the framework mirrors `data.recovery.hint` into `content[]` text). Override with explicit `{ recovery: { hint: '...' } }` when runtime context matters. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`) bubble freely and don't need declaring.
181
189
 
182
190
  ```ts
183
191
  errors: [
184
- { reason: 'note_missing', code: JsonRpcErrorCode.NotFound, when: 'No note matched the path' },
185
- { reason: 'plugin_unreachable', code: JsonRpcErrorCode.ServiceUnavailable, when: 'Local REST API plugin is offline', retryable: true },
192
+ { reason: 'note_missing', code: JsonRpcErrorCode.NotFound,
193
+ when: 'No note matched the path',
194
+ recovery: 'Verify the path with obsidian_list_notes or use obsidian_search_notes to locate the note.' },
195
+ { reason: 'plugin_unreachable', code: JsonRpcErrorCode.ServiceUnavailable,
196
+ when: 'Local REST API plugin is offline', retryable: true,
197
+ recovery: 'Confirm Obsidian is running with the Local REST API plugin enabled.' },
186
198
  ],
187
199
  async handler(input, ctx) {
188
200
  const note = await svc.getNote(input.path, ctx);
189
- if (!note) throw ctx.fail('note_missing', `Note ${input.path} not found`);
201
+ // Static recovery pulled from the contract via ctx.recoveryFor.
202
+ if (!note) throw ctx.fail('note_missing', `Note ${input.path} not found`, {
203
+ ...ctx.recoveryFor('note_missing'),
204
+ });
190
205
  return note;
191
206
  }
192
207
  ```
193
208
 
194
- Services don't have `ctx.fail` they pass `data: { reason: 'note_missing' }` to a factory so the wire shape matches what `ctx.fail` produces:
209
+ 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:
195
210
 
196
211
  ```ts
197
212
  // inside obsidian-service.ts
198
- throw notFound('Note not found', { reason: 'note_missing', path });
213
+ throw notFound(`Not found: ${display}`, data('note_missing'));
214
+ // where data(reason) does: { path, reason, ...ctx.recoveryFor(reason), upstream? }
199
215
  ```
200
216
 
201
217
  **Fallback for ad-hoc throws** (no contract entry fits, prototype tools, service-layer code without a contract): use error factories.
@@ -279,6 +295,7 @@ Available skills:
279
295
  | `report-issue-framework` | File a bug or feature request against `@cyanheads/mcp-ts-core` via `gh` CLI |
280
296
  | `report-issue-local` | File a bug or feature request against this server's own repo via `gh` CLI |
281
297
  | `api-auth` | Auth modes, scopes, JWT/OAuth |
298
+ | `api-canvas` | DataCanvas SQL workspace (Tier 3, DuckDB) — not used by this server |
282
299
  | `api-config` | AppConfig, parseConfig, env vars |
283
300
  | `api-context` | Context interface, logger, state, progress |
284
301
  | `api-errors` | McpError, JsonRpcErrorCode, error patterns |
@@ -306,8 +323,6 @@ When you complete a skill's checklist, check the boxes and add a completion time
306
323
  | `bun run format` | Auto-fix formatting (Biome) |
307
324
  | `bun run lint:mcp` | Validate MCP definitions against the linter rules |
308
325
  | `bun run test` | Run Vitest tests |
309
- | `bun run dev:stdio` | Dev mode (stdio, watch) |
310
- | `bun run dev:http` | Dev mode (HTTP, watch) |
311
326
  | `bun run start:stdio` | Production mode (stdio) — requires `bun run build` first |
312
327
  | `bun run start:http` | Production mode (HTTP) — requires `bun run build` first |
313
328
  | `bun run changelog:build` | Regenerate `CHANGELOG.md` rollup from `changelog/<minor>.x/*.md` |
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  <div align="center">
9
9
 
10
- [![npm](https://img.shields.io/npm/v/obsidian-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/obsidian-mcp-server) [![Version](https://img.shields.io/badge/Version-3.1.0-blue.svg?style=flat-square)](./CHANGELOG.md) [![Framework](https://img.shields.io/badge/Built%20on-@cyanheads/mcp--ts--core-259?style=flat-square)](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^1.29.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/)
10
+ [![npm](https://img.shields.io/npm/v/obsidian-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/obsidian-mcp-server) [![Version](https://img.shields.io/badge/Version-3.1.2-blue.svg?style=flat-square)](./CHANGELOG.md) [![Framework](https://img.shields.io/badge/Built%20on-@cyanheads/mcp--ts--core-259?style=flat-square)](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^1.29.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/)
11
11
 
12
12
  [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![TypeScript](https://img.shields.io/badge/TypeScript-^6.0.3-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.3.11-blueviolet.svg?style=flat-square)](https://bun.sh/)
13
13
 
@@ -126,6 +126,29 @@ Dispatch an Obsidian command-palette command by ID (discoverable via `obsidian_l
126
126
 
127
127
  ---
128
128
 
129
+ ## Path policy (folder-scoped permissions)
130
+
131
+ Three optional env vars gate which vault paths each tool can target. **Default unset = full vault** for both reads and writes — backwards compatible.
132
+
133
+ | Goal | Config |
134
+ |:---|:---|
135
+ | Default (current behavior) | all unset |
136
+ | Read everywhere, write only in `projects/` and `scratch/` | `OBSIDIAN_WRITE_PATHS=projects/,scratch/` |
137
+ | Read only `public/`, write only `public/inbox/` | `OBSIDIAN_READ_PATHS=public/`, `OBSIDIAN_WRITE_PATHS=public/inbox/` |
138
+ | Read-only deployment — no writes anywhere | `OBSIDIAN_READ_ONLY=true` |
139
+
140
+ **Matching is prefix-based with implicit recursion**, case-insensitive, with trailing slashes normalized. `projects/` matches `projects/a.md`, `projects/sub/b.md`, etc.
141
+
142
+ **Write paths are implicitly readable** — you can't sanely edit what you can't see. So a read passes when the target matches `READ_PATHS` *or* `WRITE_PATHS`.
143
+
144
+ **`OBSIDIAN_READ_ONLY=true` short-circuits before the path checks** — every write is denied regardless of `WRITE_PATHS`, and the command-palette pair is unregistered regardless of `OBSIDIAN_ENABLE_COMMANDS` (commands can mutate).
145
+
146
+ Denies are typed `path_forbidden` (JSON-RPC code `Forbidden`) with the active scope echoed back in `data.recovery.hint` and `data.activeScope`, so the LLM can self-correct without inspecting server logs. Search results from `obsidian_search_notes` are filtered against `READ_PATHS` silently — surfacing a "we hid N hits" indicator would defeat the gate.
147
+
148
+ The startup banner logs the active scope so operators can verify their config at boot.
149
+
150
+ ---
151
+
129
152
  ## Resources
130
153
 
131
154
  | Type | URI | Description |
@@ -155,6 +178,7 @@ Obsidian-specific:
155
178
  - Tag reconciliation across both representations: frontmatter `tags:` array and inline `#tag` syntax (skipping fenced code blocks)
156
179
  - Search across three modes: text, Dataview DQL, JSONLogic — with overflow indicator when results exceed the 100-hit cap
157
180
  - Optional human-in-the-loop confirmation for destructive deletes via `ctx.elicit`
181
+ - Folder-scoped read/write permissions via `OBSIDIAN_READ_PATHS` / `OBSIDIAN_WRITE_PATHS` and a global `OBSIDIAN_READ_ONLY` kill switch — denies are typed `path_forbidden` with the active scope echoed back in the error data
158
182
  - Opt-in command-palette pair (`obsidian_list_commands` + `obsidian_execute_command`) — registered only when `OBSIDIAN_ENABLE_COMMANDS=true`
159
183
  - Forgiving path resolution on `obsidian_get_note` and `obsidian_open_in_ui` — silently retries case-mismatched paths against the canonical filename, throws `Conflict` on ambiguous case matches, and enriches `NotFound` with `Did you mean: …?` suggestions when only near-matches exist. `obsidian_delete_note` is deliberately excluded — a destructive op shouldn't silently rewrite the target path.
160
184
 
@@ -247,6 +271,9 @@ MCP_TRANSPORT_TYPE=http OBSIDIAN_API_KEY=... bun run start:http
247
271
  | `OBSIDIAN_VERIFY_SSL` | Verify the TLS certificate. Default `false` because the plugin uses a self-signed cert. On Node, the dispatcher's `rejectUnauthorized` option handles this without any process-wide change. On Bun, the runtime ignores that option, so the service additionally sets `NODE_TLS_REJECT_UNAUTHORIZED=0` — that fallback is scoped to Bun only. | `false` |
248
272
  | `OBSIDIAN_REQUEST_TIMEOUT_MS` | Per-request timeout in milliseconds. | `30000` |
249
273
  | `OBSIDIAN_ENABLE_COMMANDS` | 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. | `false` |
274
+ | `OBSIDIAN_READ_PATHS` | Comma-separated vault-relative folder allowlist for read operations. Prefix-based with implicit recursion; case-insensitive; trailing slashes normalized. Unset = full vault. Write paths are implicitly readable. | unset |
275
+ | `OBSIDIAN_WRITE_PATHS` | Comma-separated vault-relative folder allowlist for write operations. Same syntax as `OBSIDIAN_READ_PATHS`. Unset = full vault. | unset |
276
+ | `OBSIDIAN_READ_ONLY` | Global kill switch. When `true`, denies every write regardless of `OBSIDIAN_WRITE_PATHS`, and suppresses the `OBSIDIAN_ENABLE_COMMANDS` pair (commands can mutate). | `false` |
250
277
  | `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http`. | `stdio` |
251
278
  | `MCP_HTTP_HOST` | Host for the HTTP server. | `127.0.0.1` |
252
279
  | `MCP_HTTP_PORT` | Port for the HTTP server. | `3010` |
@@ -0,0 +1,21 @@
1
+ ---
2
+ summary: Adopt the mcp-ts-core 0.8.6 recovery-hint contract — every error declares a recovery, ObsidianService threads it onto the wire, and a new periodic_disabled reason distinguishes a disabled period from a missing periodic note.
3
+ breaking: false
4
+ ---
5
+
6
+ # 3.1.1 — 2026-04-29
7
+
8
+ A propagation pass for the typed-error contract upgrade in `@cyanheads/mcp-ts-core` 0.8.6. Every tool's `errors[]` now declares a `recovery` hint, the Obsidian service threads `ctx` through its status classifier so service-side throws inherit the calling tool's recovery, and `obsidian_open_in_ui` / `obsidian_write_note` lift long remediation guidance out of the error message into structured `data.recovery.hint`.
9
+
10
+ ## Added
11
+
12
+ - **`periodic_disabled` error reason** on every tool that resolves a `periodic` target (`obsidian_get_note`, `obsidian_patch_note`, `obsidian_replace_in_note`, `obsidian_manage_frontmatter`, `obsidian_manage_tags`, `obsidian_delete_note`). Fired when the upstream Local REST API returns a 400 with `"Specified period is not enabled"` — the requested period (`daily` / `weekly` / `monthly` / `quarterly` / `yearly`) is turned off in Obsidian's Periodic Notes plugin settings. Distinct from `periodic_not_found` (404, the period is enabled but no note exists for the requested date), so clients can suggest enabling the period rather than creating a note.
13
+
14
+ ## Changed
15
+
16
+ - **Every error contract entry now carries a `recovery` hint.** Required descriptive metadata under `mcp-ts-core` 0.8.6 — lint enforces it. Recovery flows to the wire via `ctx.recoveryFor(reason)` (spread into the `ctx.fail(...)` data bag) and the framework mirrors `data.recovery.hint` into the rendered `content[]` text, so clients that render structured error data and clients that parse text both see the same actionable next step.
17
+ - **`obsidian_open_in_ui` and `obsidian_write_note` lift long remediation text into `data.recovery.hint`.** The `note_missing` and `file_exists` error messages drop the `Did you mean:` suggestions, the `failIfMissing: false` tip, and the `obsidian_patch_note` / `obsidian_append_to_note` / `obsidian_replace_in_note` referrals — those move into a structured `recovery.hint` field. Wire-visible: clients that read `error.data.recovery.hint` see a cleaner separation of "what happened" from "how to fix it."
18
+ - **`ObsidianService.#throwForStatus` threads `ctx`.** The status classifier now spreads `ctx.recoveryFor(reason)` per branch, so service-side throws (`note_missing`, `no_active_file`, `periodic_not_found`, `periodic_disabled`, `command_unknown`, `path_is_directory`, `section_target_missing`) inherit the calling tool's contract recovery instead of needing per-tool plumbing.
19
+ - **Drop `dev:stdio` / `dev:http` scripts.** `bun --watch src/index.ts` works directly without the wrapper. The CLAUDE.md commands table loses the two duplicates.
20
+ - **Dependency bump:** `@cyanheads/mcp-ts-core` 0.8.2 → 0.8.6.
21
+ - **Skills synced** with framework patterns: `add-tool`, `add-service`, `api-context`, `api-errors`, `design-mcp-server`, `field-test`.
@@ -0,0 +1,23 @@
1
+ ---
2
+ summary: Folder-scoped read/write permissions and a global read-only kill switch — three opt-in env vars (OBSIDIAN_READ_PATHS, OBSIDIAN_WRITE_PATHS, OBSIDIAN_READ_ONLY) gate every path-taking tool and resource, with a new path_forbidden error reason.
3
+ breaking: false
4
+ ---
5
+
6
+ # 3.1.2 — 2026-05-03
7
+
8
+ Three opt-in env vars now scope which vault paths read and write tools may target. Closes [#40](https://github.com/cyanheads/obsidian-mcp-server/issues/40); resolves the original ask in [discussion #15](https://github.com/cyanheads/obsidian-mcp-server/discussions/15#discussioncomment-13918526). Backwards compatible — all three default unset / false, preserving current full-vault behavior.
9
+
10
+ ## Added
11
+
12
+ - **`OBSIDIAN_READ_PATHS` / `OBSIDIAN_WRITE_PATHS`** — comma-separated vault-relative folder allowlists. Prefix-based with implicit recursion (`projects/` matches `projects/a.md`, `projects/sub/b.md`, …); case-insensitive; trailing slashes normalized; deduplicated. Empty / whitespace-only input falls back to unset; separator-only input (`,`, `,,,`) and absolute paths or `..` traversal throw a `ConfigurationError` at startup naming the offending env var.
13
+ - **`OBSIDIAN_READ_ONLY`** — global kill switch. When `true`, denies every write regardless of `OBSIDIAN_WRITE_PATHS`, and suppresses the `OBSIDIAN_ENABLE_COMMANDS` pair (commands can mutate). Useful for shared or public-facing deployments.
14
+ - **`path_forbidden` error reason** declared on every path-taking tool's `errors[]` contract — mapped to `JsonRpcErrorCode.Forbidden`. The wire payload carries `{ path, op, subreason, activeScope, recovery }` where `subreason` is one of `outside_read_paths`, `outside_write_paths`, or `read_only_mode`, and the recovery hint echoes the active scope so the LLM can self-correct without inspecting server logs.
15
+ - **Startup banner** — `core.logger.info('Path policy', …)` echoes the active scope and command-palette enable state at boot, so operators can verify their config. Emits a warning when `OBSIDIAN_READ_ONLY=true` AND `OBSIDIAN_WRITE_PATHS` is non-empty (the boolean wins; the paths are ignored).
16
+ - **`PathPolicy`** module (`src/services/obsidian/path-policy.ts`) — single chokepoint that decides per path: `isReadable`, `isWritable`, `assertReadable`, `assertWritable`, plus a silent `filterReadable` for search post-filtering.
17
+
18
+ ## Changed
19
+
20
+ - **`ObsidianService` gates every path-taking method** before the upstream HTTP call: `getNoteContent`, `getNoteJson`, `getDocumentMap`, `listFiles`, `openInUi` (read), and `writeNote`, `appendToNote`, `patchNote`, `deleteNote` (write). For non-`path` targets (`active`, `periodic`) under restrictions, the service routes through a JSON resolution fetch first to learn the path, then gates on the resolved path — costs one extra fetch only for users who configured a path scope.
21
+ - **`obsidian_search_notes` post-filters hits against `OBSIDIAN_READ_PATHS`** silently — surfacing a "we hid N hits" indicator would defeat the gate. The `excluded` overflow indicator continues to count hits trimmed by the 100-hit cap, not policy-dropped hits.
22
+ - **Tool registration in `src/index.ts`** splits read-only tools from write tools at the barrel and wraps the write set with `disabledTool()` when `OBSIDIAN_READ_ONLY=true`. The command-palette pair stays wrapped with `disabledTool()` whenever it's not callable (either `OBSIDIAN_ENABLE_COMMANDS=false` or `OBSIDIAN_READ_ONLY=true`), with a `hint` explaining how to enable it. The LLM never sees disabled tools in `tools/list`; they remain visible in the operator-facing manifest.
23
+ - **`obsidian_list_notes` listing semantics** — the vault root always lists, even with `OBSIDIAN_READ_PATHS` set. Specific subdirectories outside the read scope throw `path_forbidden`. Children aren't filtered at the service level; the per-file read gate on subsequent reads (`obsidian_get_note`, etc.) still catches access to out-of-scope notes.
@@ -7,9 +7,12 @@ import { z } from '@cyanheads/mcp-ts-core';
7
7
  declare const ServerConfigSchema: z.ZodObject<{
8
8
  apiKey: z.ZodString;
9
9
  baseUrl: z.ZodDefault<z.ZodString>;
10
- verifySsl: z.ZodDefault<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodBoolean>>;
10
+ verifySsl: z.ZodDefault<z.ZodPreprocess<z.ZodBoolean>>;
11
11
  requestTimeoutMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
12
- enableCommands: z.ZodDefault<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodBoolean>>;
12
+ enableCommands: z.ZodDefault<z.ZodPreprocess<z.ZodBoolean>>;
13
+ readPaths: z.ZodPreprocess<z.ZodPipe<z.ZodOptional<z.ZodArray<z.ZodString>>, z.ZodTransform<string[] | undefined, string[] | undefined>>>;
14
+ writePaths: z.ZodPreprocess<z.ZodPipe<z.ZodOptional<z.ZodArray<z.ZodString>>, z.ZodTransform<string[] | undefined, string[] | undefined>>>;
15
+ readOnly: z.ZodDefault<z.ZodPreprocess<z.ZodBoolean>>;
13
16
  }, z.core.$strip>;
14
17
  export type ServerConfig = z.infer<typeof ServerConfigSchema>;
15
18
  export declare function getServerConfig(): ServerConfig;
@@ -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;AAY3C,QAAA,MAAM,kBAAkB;;;;;;iBA8BtB,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAI9D,wBAAgB,eAAe,IAAI,YAAY,CAS9C;AAED,iEAAiE;AACjE,wBAAgB,iBAAiB,IAAI,IAAI,CAExC"}
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;AA4E3C,QAAA,MAAM,kBAAkB;;;;;;;;;iBAyCtB,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAI9D,wBAAgB,eAAe,IAAI,YAAY,CAY9C;AAED,iEAAiE;AACjE,wBAAgB,iBAAiB,IAAI,IAAI,CAExC"}
@@ -14,6 +14,71 @@ const envBoolean = z.preprocess((val) => {
14
14
  }
15
15
  return val;
16
16
  }, z.boolean());
17
+ /**
18
+ * Comma-separated path-list preprocessor. Semantics (see issue #40):
19
+ * - undefined / `''` / whitespace-only → undefined (treat as unset; default = full vault)
20
+ * - `,` / `,,,` (separators only, no path content) → throw ZodError → ConfigurationError
21
+ * - mixed empties (`a,,b`) → drop empties → `['a', 'b']`
22
+ * - absolute path / `..` traversal → throw
23
+ * - valid: lower-case + trim trailing slash + dedupe (preserves first occurrence order)
24
+ */
25
+ const envPathList = z
26
+ .preprocess((val) => {
27
+ if (val === undefined || val === null)
28
+ return;
29
+ if (Array.isArray(val))
30
+ return val;
31
+ if (typeof val !== 'string')
32
+ return val;
33
+ if (val.trim() === '')
34
+ return;
35
+ const parts = val
36
+ .split(',')
37
+ .map((s) => s.trim())
38
+ .filter((s) => s.length > 0);
39
+ return parts;
40
+ }, z
41
+ .array(z.string())
42
+ .optional()
43
+ .transform((parts, ctx) => {
44
+ if (parts === undefined)
45
+ return;
46
+ if (parts.length === 0) {
47
+ ctx.addIssue({
48
+ code: 'custom',
49
+ message: 'contained separators but no valid paths after trimming',
50
+ });
51
+ return z.NEVER;
52
+ }
53
+ const seen = new Set();
54
+ const out = [];
55
+ for (const raw of parts) {
56
+ if (raw.startsWith('/') || raw.startsWith('\\')) {
57
+ ctx.addIssue({
58
+ code: 'custom',
59
+ message: `must be vault-relative; got absolute path '${raw}'`,
60
+ });
61
+ return z.NEVER;
62
+ }
63
+ const segments = raw.split(/[\\/]/);
64
+ if (segments.includes('..')) {
65
+ ctx.addIssue({
66
+ code: 'custom',
67
+ message: `must not contain '..' traversal; got '${raw}'`,
68
+ });
69
+ return z.NEVER;
70
+ }
71
+ const normalized = raw.toLowerCase().replace(/[\\/]+$/, '');
72
+ if (normalized.length === 0)
73
+ continue;
74
+ if (seen.has(normalized))
75
+ continue;
76
+ seen.add(normalized);
77
+ out.push(normalized);
78
+ }
79
+ return out.length > 0 ? out : undefined;
80
+ }))
81
+ .describe('Comma-separated list of vault-relative folder prefixes. Empty/whitespace falls back to unset.');
17
82
  const ServerConfigSchema = z.object({
18
83
  apiKey: z
19
84
  .string()
@@ -36,6 +101,11 @@ const ServerConfigSchema = z.object({
36
101
  enableCommands: envBoolean
37
102
  .default(false)
38
103
  .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.'),
104
+ readPaths: envPathList.describe('Optional vault-relative folder allowlist for read operations. Comma-separated; prefix-based with implicit recursion; case-insensitive; trailing slashes normalized. Unset = full vault.'),
105
+ writePaths: envPathList.describe('Optional vault-relative folder allowlist for write operations. Same syntax as OBSIDIAN_READ_PATHS. Write paths are implicitly readable. Unset = full vault.'),
106
+ readOnly: envBoolean
107
+ .default(false)
108
+ .describe('Global kill switch. When true, denies every write regardless of OBSIDIAN_WRITE_PATHS, and suppresses the OBSIDIAN_ENABLE_COMMANDS pair (commands can mutate). Defaults to false.'),
39
109
  });
40
110
  let _config;
41
111
  export function getServerConfig() {
@@ -45,6 +115,9 @@ export function getServerConfig() {
45
115
  verifySsl: 'OBSIDIAN_VERIFY_SSL',
46
116
  requestTimeoutMs: 'OBSIDIAN_REQUEST_TIMEOUT_MS',
47
117
  enableCommands: 'OBSIDIAN_ENABLE_COMMANDS',
118
+ readPaths: 'OBSIDIAN_READ_PATHS',
119
+ writePaths: 'OBSIDIAN_WRITE_PATHS',
120
+ readOnly: 'OBSIDIAN_READ_ONLY',
48
121
  });
49
122
  return _config;
50
123
  }
@@ -1 +1 @@
1
- {"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAE/D,MAAM,UAAU,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,GAAG,EAAE,EAAE;IACtC,IAAI,OAAO,GAAG,KAAK,SAAS;QAAE,OAAO,GAAG,CAAC;IACzC,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QAC5B,MAAM,GAAG,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;QACrC,OAAO,GAAG,KAAK,MAAM,IAAI,GAAG,KAAK,GAAG,CAAC;IACvC,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;AAEhB,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,MAAM,EAAE,CAAC;SACN,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,CAAC;SACN,QAAQ,CACP,sGAAsG,CACvG;IACH,OAAO,EAAE,CAAC;SACP,MAAM,EAAE;SACR,GAAG,EAAE;SACL,OAAO,CAAC,wBAAwB,CAAC;SACjC,QAAQ,CACP,0QAA0Q,CAC3Q;IACH,SAAS,EAAE,UAAU;SAClB,OAAO,CAAC,KAAK,CAAC;SACd,QAAQ,CACP,qXAAqX,CACtX;IACH,gBAAgB,EAAE,CAAC,CAAC,MAAM;SACvB,MAAM,EAAE;SACR,GAAG,EAAE;SACL,QAAQ,EAAE;SACV,OAAO,CAAC,MAAM,CAAC;SACf,QAAQ,CAAC,sCAAsC,CAAC;IACnD,cAAc,EAAE,UAAU;SACvB,OAAO,CAAC,KAAK,CAAC;SACd,QAAQ,CACP,yKAAyK,CAC1K;CACJ,CAAC,CAAC;AAIH,IAAI,OAAiC,CAAC;AAEtC,MAAM,UAAU,eAAe;IAC7B,OAAO,KAAK,cAAc,CAAC,kBAAkB,EAAE;QAC7C,MAAM,EAAE,kBAAkB;QAC1B,OAAO,EAAE,mBAAmB;QAC5B,SAAS,EAAE,qBAAqB;QAChC,gBAAgB,EAAE,6BAA6B;QAC/C,cAAc,EAAE,0BAA0B;KAC3C,CAAC,CAAC;IACH,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,iBAAiB;IAC/B,OAAO,GAAG,SAAS,CAAC;AACtB,CAAC"}
1
+ {"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -6,16 +6,34 @@
6
6
  * @module index
7
7
  */
8
8
  import { createApp } from '@cyanheads/mcp-ts-core';
9
+ import { disabledTool } from '@cyanheads/mcp-ts-core/tools';
10
+ import { requestContextService } from '@cyanheads/mcp-ts-core/utils';
9
11
  import { getServerConfig } from './config/server-config.js';
10
12
  import { allPromptDefinitions } from './mcp-server/prompts/definitions/index.js';
11
13
  import { allResourceDefinitions } from './mcp-server/resources/definitions/index.js';
12
- import { baseToolDefinitions, commandToolDefinitions, } from './mcp-server/tools/definitions/index.js';
14
+ import { commandToolDefinitions, readToolDefinitions, writeToolDefinitions, } from './mcp-server/tools/definitions/index.js';
13
15
  import { initObsidianService } from './services/obsidian/obsidian-service.js';
16
+ import { PathPolicy } from './services/obsidian/path-policy.js';
14
17
  const config = getServerConfig();
15
- const tools = config.enableCommands
16
- ? [...baseToolDefinitions, ...commandToolDefinitions]
17
- : baseToolDefinitions;
18
- await createApp({
18
+ const policy = new PathPolicy(config);
19
+ const writeTools = config.readOnly
20
+ ? writeToolDefinitions.map((def) => disabledTool(def, {
21
+ reason: 'Disabled by OBSIDIAN_READ_ONLY=true.',
22
+ hint: 'Unset OBSIDIAN_READ_ONLY (or set it to false) to enable write tools.',
23
+ }))
24
+ : writeToolDefinitions;
25
+ const commandTools = config.enableCommands && !config.readOnly
26
+ ? commandToolDefinitions
27
+ : commandToolDefinitions.map((def) => disabledTool(def, {
28
+ reason: config.readOnly
29
+ ? 'Disabled by OBSIDIAN_READ_ONLY=true (commands can mutate).'
30
+ : 'Disabled by default — Obsidian commands are opaque and can be destructive.',
31
+ hint: config.readOnly
32
+ ? 'Unset OBSIDIAN_READ_ONLY to allow commands; OBSIDIAN_ENABLE_COMMANDS=true is also required.'
33
+ : 'Set OBSIDIAN_ENABLE_COMMANDS=true to enable obsidian_list_commands and obsidian_execute_command.',
34
+ }));
35
+ const tools = [...readToolDefinitions, ...writeTools, ...commandTools];
36
+ const { services } = await createApp({
19
37
  tools,
20
38
  resources: allResourceDefinitions,
21
39
  prompts: allPromptDefinitions,
@@ -23,4 +41,20 @@ await createApp({
23
41
  initObsidianService(config);
24
42
  },
25
43
  });
44
+ /**
45
+ * Startup banner — emitted after createApp() returns so the framework's
46
+ * `logger.initialize()` has run; calls inside `setup()` happen pre-init and
47
+ * are dropped. Operators check this against their config to verify the active
48
+ * path policy. The active scope on `path_forbidden` errors echoes the same
49
+ * data so the LLM (or operator) can self-correct without scrolling the log.
50
+ */
51
+ const bannerCtx = requestContextService.createRequestContext({
52
+ operation: 'startup',
53
+ ...policy.describe(),
54
+ enableCommands: config.enableCommands && !config.readOnly,
55
+ });
56
+ services.logger.info('Path policy', bannerCtx);
57
+ if (policy.readOnlyShadowsWritePaths) {
58
+ 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);
59
+ }
26
60
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;GAKG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,sBAAsB,EAAE,MAAM,6CAA6C,CAAC;AACrF,OAAO,EACL,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAAE,mBAAmB,EAAE,MAAM,yCAAyC,CAAC;AAE9E,MAAM,MAAM,GAAG,eAAe,EAAE,CAAC;AACjC,MAAM,KAAK,GAAG,MAAM,CAAC,cAAc;IACjC,CAAC,CAAC,CAAC,GAAG,mBAAmB,EAAE,GAAG,sBAAsB,CAAC;IACrD,CAAC,CAAC,mBAAmB,CAAC;AAExB,MAAM,SAAS,CAAC;IACd,KAAK;IACL,SAAS,EAAE,sBAAsB;IACjC,OAAO,EAAE,oBAAoB;IAC7B,KAAK;QACH,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC9B,CAAC;CACF,CAAC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;GAKG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAC5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,sBAAsB,EAAE,MAAM,6CAA6C,CAAC;AACrF,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAAE,mBAAmB,EAAE,MAAM,yCAAyC,CAAC;AAC9E,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,MAAM,UAAU,GAAG,MAAM,CAAC,QAAQ;IAChC,CAAC,CAAC,oBAAoB,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAC/B,YAAY,CAAC,GAAG,EAAE;QAChB,MAAM,EAAE,sCAAsC;QAC9C,IAAI,EAAE,sEAAsE;KAC7E,CAAC,CACH;IACH,CAAC,CAAC,oBAAoB,CAAC;AAEzB,MAAM,YAAY,GAChB,MAAM,CAAC,cAAc,IAAI,CAAC,MAAM,CAAC,QAAQ;IACvC,CAAC,CAAC,sBAAsB;IACxB,CAAC,CAAC,sBAAsB,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CACjC,YAAY,CAAC,GAAG,EAAE;QAChB,MAAM,EAAE,MAAM,CAAC,QAAQ;YACrB,CAAC,CAAC,4DAA4D;YAC9D,CAAC,CAAC,4EAA4E;QAChF,IAAI,EAAE,MAAM,CAAC,QAAQ;YACnB,CAAC,CAAC,6FAA6F;YAC/F,CAAC,CAAC,kGAAkG;KACvG,CAAC,CACH,CAAC;AAER,MAAM,KAAK,GAAG,CAAC,GAAG,mBAAmB,EAAE,GAAG,UAAU,EAAE,GAAG,YAAY,CAAC,CAAC;AAEvE,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,SAAS,CAAC;IACnC,KAAK;IACL,SAAS,EAAE,sBAAsB;IACjC,OAAO,EAAE,oBAAoB;IAC7B,KAAK;QACH,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC9B,CAAC;CACF,CAAC,CAAC;AAEH;;;;;;GAMG;AACH,MAAM,SAAS,GAAG,qBAAqB,CAAC,oBAAoB,CAAC;IAC3D,SAAS,EAAE,SAAS;IACpB,GAAG,MAAM,CAAC,QAAQ,EAAE;IACpB,cAAc,EAAE,MAAM,CAAC,cAAc,IAAI,CAAC,MAAM,CAAC,QAAQ;CAC1D,CAAC,CAAC;AACH,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;AAC/C,IAAI,MAAM,CAAC,yBAAyB,EAAE,CAAC;IACrC,QAAQ,CAAC,MAAM,CAAC,OAAO,CACrB,uHAAuH,EACvH,SAAS,CACV,CAAC;AACJ,CAAC"}