obsidian-mcp-server 3.1.1 → 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 (66) hide show
  1. package/CLAUDE.md +10 -1
  2. package/README.md +28 -1
  3. package/changelog/3.1.x/3.1.2.md +23 -0
  4. package/dist/config/server-config.d.ts +5 -2
  5. package/dist/config/server-config.d.ts.map +1 -1
  6. package/dist/config/server-config.js +73 -0
  7. package/dist/config/server-config.js.map +1 -1
  8. package/dist/index.js +39 -5
  9. package/dist/index.js.map +1 -1
  10. package/dist/mcp-server/tools/definitions/index.d.ts +731 -10
  11. package/dist/mcp-server/tools/definitions/index.d.ts.map +1 -1
  12. package/dist/mcp-server/tools/definitions/index.js +15 -9
  13. package/dist/mcp-server/tools/definitions/index.js.map +1 -1
  14. package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.d.ts +7 -1
  15. package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.d.ts.map +1 -1
  16. package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.js +9 -0
  17. package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.js.map +1 -1
  18. package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.d.ts +5 -0
  19. package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.d.ts.map +1 -1
  20. package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.js +6 -0
  21. package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.js.map +1 -1
  22. package/dist/mcp-server/tools/definitions/obsidian-get-note.tool.d.ts +5 -0
  23. package/dist/mcp-server/tools/definitions/obsidian-get-note.tool.d.ts.map +1 -1
  24. package/dist/mcp-server/tools/definitions/obsidian-get-note.tool.js +6 -0
  25. package/dist/mcp-server/tools/definitions/obsidian-get-note.tool.js.map +1 -1
  26. package/dist/mcp-server/tools/definitions/obsidian-list-notes.tool.d.ts +5 -0
  27. package/dist/mcp-server/tools/definitions/obsidian-list-notes.tool.d.ts.map +1 -1
  28. package/dist/mcp-server/tools/definitions/obsidian-list-notes.tool.js +15 -2
  29. package/dist/mcp-server/tools/definitions/obsidian-list-notes.tool.js.map +1 -1
  30. package/dist/mcp-server/tools/definitions/obsidian-manage-frontmatter.tool.d.ts +5 -0
  31. package/dist/mcp-server/tools/definitions/obsidian-manage-frontmatter.tool.d.ts.map +1 -1
  32. package/dist/mcp-server/tools/definitions/obsidian-manage-frontmatter.tool.js +6 -0
  33. package/dist/mcp-server/tools/definitions/obsidian-manage-frontmatter.tool.js.map +1 -1
  34. package/dist/mcp-server/tools/definitions/obsidian-manage-tags.tool.d.ts +5 -0
  35. package/dist/mcp-server/tools/definitions/obsidian-manage-tags.tool.d.ts.map +1 -1
  36. package/dist/mcp-server/tools/definitions/obsidian-manage-tags.tool.js +6 -0
  37. package/dist/mcp-server/tools/definitions/obsidian-manage-tags.tool.js.map +1 -1
  38. package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.d.ts +5 -0
  39. package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.d.ts.map +1 -1
  40. package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.js +6 -0
  41. package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.js.map +1 -1
  42. package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.d.ts +7 -2
  43. package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.d.ts.map +1 -1
  44. package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.js +6 -0
  45. package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.js.map +1 -1
  46. package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.d.ts +5 -0
  47. package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.d.ts.map +1 -1
  48. package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.js +6 -0
  49. package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.js.map +1 -1
  50. package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.d.ts.map +1 -1
  51. package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.js +14 -4
  52. package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.js.map +1 -1
  53. package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.d.ts +5 -0
  54. package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.d.ts.map +1 -1
  55. package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.js +6 -0
  56. package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.js.map +1 -1
  57. package/dist/services/obsidian/obsidian-service.d.ts +7 -0
  58. package/dist/services/obsidian/obsidian-service.d.ts.map +1 -1
  59. package/dist/services/obsidian/obsidian-service.js +114 -17
  60. package/dist/services/obsidian/obsidian-service.js.map +1 -1
  61. package/dist/services/obsidian/path-policy.d.ts +56 -0
  62. package/dist/services/obsidian/path-policy.d.ts.map +1 -0
  63. package/dist/services/obsidian/path-policy.js +154 -0
  64. package/dist/services/obsidian/path-policy.js.map +1 -0
  65. package/package.json +7 -6
  66. 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
  }
@@ -287,6 +295,7 @@ Available skills:
287
295
  | `report-issue-framework` | File a bug or feature request against `@cyanheads/mcp-ts-core` via `gh` CLI |
288
296
  | `report-issue-local` | File a bug or feature request against this server's own repo via `gh` CLI |
289
297
  | `api-auth` | Auth modes, scopes, JWT/OAuth |
298
+ | `api-canvas` | DataCanvas SQL workspace (Tier 3, DuckDB) — not used by this server |
290
299
  | `api-config` | AppConfig, parseConfig, env vars |
291
300
  | `api-context` | Context interface, logger, state, progress |
292
301
  | `api-errors` | McpError, JsonRpcErrorCode, error patterns |
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.1-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,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"}