obsidian-mcp-pro 2.1.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -42
- package/build/config.d.ts.map +1 -1
- package/build/config.js +44 -9
- package/build/config.js.map +1 -1
- package/build/http-server.d.ts +4 -2
- package/build/http-server.d.ts.map +1 -1
- package/build/http-server.js +52 -20
- package/build/http-server.js.map +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +36 -38
- package/build/index.js.map +1 -1
- package/build/install.d.ts.map +1 -1
- package/build/install.js +1 -1
- package/build/install.js.map +1 -1
- package/build/lib/bases.d.ts +11 -4
- package/build/lib/bases.d.ts.map +1 -1
- package/build/lib/bases.js +21 -19
- package/build/lib/bases.js.map +1 -1
- package/build/lib/chunker.js +81 -2
- package/build/lib/chunker.js.map +1 -1
- package/build/lib/confirmation.d.ts +21 -0
- package/build/lib/confirmation.d.ts.map +1 -0
- package/build/lib/confirmation.js +37 -0
- package/build/lib/confirmation.js.map +1 -0
- package/build/lib/embedding-providers.d.ts.map +1 -1
- package/build/lib/embedding-providers.js +5 -15
- package/build/lib/embedding-providers.js.map +1 -1
- package/build/lib/embedding-store.d.ts +4 -0
- package/build/lib/embedding-store.d.ts.map +1 -1
- package/build/lib/embedding-store.js +100 -22
- package/build/lib/embedding-store.js.map +1 -1
- package/build/lib/errors.d.ts +13 -5
- package/build/lib/errors.d.ts.map +1 -1
- package/build/lib/errors.js +53 -19
- package/build/lib/errors.js.map +1 -1
- package/build/lib/fs-ops.d.ts +3 -0
- package/build/lib/fs-ops.d.ts.map +1 -0
- package/build/lib/fs-ops.js +32 -0
- package/build/lib/fs-ops.js.map +1 -0
- package/build/lib/index-cache.d.ts +13 -7
- package/build/lib/index-cache.d.ts.map +1 -1
- package/build/lib/index-cache.js +49 -205
- package/build/lib/index-cache.js.map +1 -1
- package/build/lib/link-rewriter.d.ts.map +1 -1
- package/build/lib/link-rewriter.js +23 -6
- package/build/lib/link-rewriter.js.map +1 -1
- package/build/lib/logger.d.ts.map +1 -1
- package/build/lib/logger.js +143 -23
- package/build/lib/logger.js.map +1 -1
- package/build/lib/markdown.d.ts +16 -5
- package/build/lib/markdown.d.ts.map +1 -1
- package/build/lib/markdown.js +143 -24
- package/build/lib/markdown.js.map +1 -1
- package/build/lib/permissions.d.ts.map +1 -1
- package/build/lib/permissions.js +13 -7
- package/build/lib/permissions.js.map +1 -1
- package/build/lib/tag-rewriter.d.ts.map +1 -1
- package/build/lib/tag-rewriter.js +12 -43
- package/build/lib/tag-rewriter.js.map +1 -1
- package/build/lib/tool-output.d.ts +11 -0
- package/build/lib/tool-output.d.ts.map +1 -0
- package/build/lib/tool-output.js +40 -0
- package/build/lib/tool-output.js.map +1 -0
- package/build/lib/vault.d.ts +61 -3
- package/build/lib/vault.d.ts.map +1 -1
- package/build/lib/vault.js +566 -160
- package/build/lib/vault.js.map +1 -1
- package/build/lib/yaml.d.ts +2 -0
- package/build/lib/yaml.d.ts.map +1 -0
- package/build/lib/yaml.js +66 -0
- package/build/lib/yaml.js.map +1 -0
- package/build/tools/attachments.d.ts.map +1 -1
- package/build/tools/attachments.js +213 -60
- package/build/tools/attachments.js.map +1 -1
- package/build/tools/bases.d.ts.map +1 -1
- package/build/tools/bases.js +50 -57
- package/build/tools/bases.js.map +1 -1
- package/build/tools/canvas.d.ts.map +1 -1
- package/build/tools/canvas.js +190 -66
- package/build/tools/canvas.js.map +1 -1
- package/build/tools/links.d.ts.map +1 -1
- package/build/tools/links.js +253 -163
- package/build/tools/links.js.map +1 -1
- package/build/tools/prompts.d.ts.map +1 -1
- package/build/tools/prompts.js +11 -9
- package/build/tools/prompts.js.map +1 -1
- package/build/tools/read.d.ts.map +1 -1
- package/build/tools/read.js +200 -123
- package/build/tools/read.js.map +1 -1
- package/build/tools/sections.d.ts.map +1 -1
- package/build/tools/sections.js +427 -33
- package/build/tools/sections.js.map +1 -1
- package/build/tools/semantic.d.ts.map +1 -1
- package/build/tools/semantic.js +173 -53
- package/build/tools/semantic.js.map +1 -1
- package/build/tools/tags.d.ts.map +1 -1
- package/build/tools/tags.js +160 -59
- package/build/tools/tags.js.map +1 -1
- package/build/tools/write.d.ts.map +1 -1
- package/build/tools/write.js +65 -25
- package/build/tools/write.js.map +1 -1
- package/package.json +20 -11
package/README.md
CHANGED
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
[](https://www.npmjs.com/package/obsidian-mcp-pro)
|
|
19
19
|
[](https://github.com/rps321321/obsidian-mcp-pro)
|
|
20
20
|
[](https://opensource.org/licenses/MIT)
|
|
21
|
-
[](https://nodejs.org/)
|
|
22
|
+
[](https://github.com/rps321321/obsidian-mcp-pro)
|
|
23
|
+
[](https://glama.ai/mcp/servers/rps321321/obsidian-mcp-pro)
|
|
24
24
|
|
|
25
25
|
Give AI assistants deep, structured access to your Obsidian knowledge base. Read, write, search, tag, analyze links, traverse graphs, manipulate canvases, query Bases, edit by heading or block reference, run semantic search, and pull binary attachments. All through the [Model Context Protocol](https://modelcontextprotocol.io/).
|
|
26
26
|
|
|
@@ -50,7 +50,7 @@ Give AI assistants deep, structured access to your Obsidian knowledge base. Read
|
|
|
50
50
|
## Features
|
|
51
51
|
|
|
52
52
|
### Read & Search
|
|
53
|
-
-
|
|
53
|
+
- Focus-ranked full-text search across all vault notes, with query-centered snippets (cached: re-runs only re-read changed files)
|
|
54
54
|
- Read individual notes whole, or as a fragment by heading path, block id, or line range
|
|
55
55
|
- List and filter notes by folder, date, or pattern
|
|
56
56
|
- Search by frontmatter fields and values
|
|
@@ -86,7 +86,7 @@ Give AI assistants deep, structured access to your Obsidian knowledge base. Read
|
|
|
86
86
|
### Bases (Obsidian 1.10+)
|
|
87
87
|
- `list_bases` enumerates `.base` files
|
|
88
88
|
- `read_base` returns the parsed YAML (filters, properties, views)
|
|
89
|
-
- `query_base` runs the filter DSL against the vault and returns matching notes; supports `taggedWith()`, `file.hasTag()`, `file.inFolder()`, comparison operators, and `and`/`or`/`not` combinators
|
|
89
|
+
- `query_base` runs the filter DSL against the vault and returns matching notes; supports `taggedWith()`, `file.hasTag()`, `file.inFolder()`, comparison operators, and `and`/`or`/`not` combinators. Unsupported filters warn and fail closed instead of broadening results.
|
|
90
90
|
|
|
91
91
|
### Attachments
|
|
92
92
|
- `list_attachments` enumerates every non-md/canvas/base file with a per-extension count summary
|
|
@@ -95,8 +95,10 @@ Give AI assistants deep, structured access to your Obsidian knowledge base. Read
|
|
|
95
95
|
|
|
96
96
|
### Semantic Search (optional, Ollama or OpenAI)
|
|
97
97
|
- `index_vault` chunks each note (heading-aware), embeds via the configured provider, persists vectors to `<vault>/.obsidian/cache/`, and incrementally re-embeds only changed notes
|
|
98
|
+
- Because indexing sends readable note chunks to the configured embedding provider, `index_vault` requires `confirm: "send-vault-text-to-embedding-provider"` on each call
|
|
98
99
|
- `search_semantic` ranks notes by cosine similarity against an embedded query
|
|
99
|
-
- `find_similar_notes` reuses an existing note's embeddings to surface neighbors without a live API call
|
|
100
|
+
- `find_similar_notes` reuses an existing note's embeddings, with source-topic anchoring, to surface neighbors without a live API call
|
|
101
|
+
- Stored snippets are returned only while the current note content hash still matches the indexed text; stale chunks are pruned on search
|
|
100
102
|
|
|
101
103
|
### MCP Resources
|
|
102
104
|
- `obsidian://note/{path}` reads any note by its vault-relative path
|
|
@@ -114,9 +116,9 @@ The server exposes five starter prompts that clients (Claude Desktop, Cursor) su
|
|
|
114
116
|
|
|
115
117
|
### Operational features
|
|
116
118
|
- **Folder-scoped permissions**: `OBSIDIAN_READ_PATHS` / `OBSIDIAN_WRITE_PATHS` allowlists gate every tool at the path-resolution choke point
|
|
117
|
-
- **
|
|
119
|
+
- **In-memory mtime cache** speeds repeated vault scans within a running process without persisting note bodies to disk
|
|
118
120
|
- **Progress notifications** (`notifications/progress`) on `rename_tag`, `find_unused_attachments`, and `index_vault` when the client subscribes via `_meta.progressToken`
|
|
119
|
-
- **
|
|
121
|
+
- **Hard bulk-write latches** require `confirmPath` for default `move_note` reference rewrites, `confirmTag` for non-dry-run `rename_tag`, and `confirm=true` for permanent deletion; clients with elicitation support also get typed prompts
|
|
120
122
|
|
|
121
123
|
---
|
|
122
124
|
|
|
@@ -178,22 +180,22 @@ claude mcp add obsidian-mcp-pro -- npx -y obsidian-mcp-pro
|
|
|
178
180
|
### HTTP Transport (Remote Clients, Cursor, ChatGPT, Web)
|
|
179
181
|
|
|
180
182
|
```bash
|
|
181
|
-
npx -y obsidian-mcp-pro --transport=http --port=3333
|
|
183
|
+
MCP_HTTP_TOKEN=your-secret npx -y obsidian-mcp-pro --transport=http --port=3333
|
|
182
184
|
```
|
|
183
185
|
|
|
184
|
-
Endpoint: `http://127.0.0.1:3333/mcp` (Streamable HTTP).
|
|
186
|
+
Endpoint: `http://127.0.0.1:3333/mcp` (Streamable HTTP). HTTP transport requires a bearer token:
|
|
185
187
|
|
|
186
188
|
```bash
|
|
187
|
-
npx -y obsidian-mcp-pro --transport=http
|
|
188
|
-
# or: MCP_HTTP_TOKEN=your-secret npx -y obsidian-mcp-pro --transport=http
|
|
189
|
+
MCP_HTTP_TOKEN=your-secret npx -y obsidian-mcp-pro --transport=http
|
|
189
190
|
```
|
|
190
191
|
|
|
191
192
|
The HTTP server binds to `127.0.0.1` by default with DNS rebinding protection enabled.
|
|
193
|
+
Startup always requires `MCP_HTTP_TOKEN`, including loopback-only local servers. The old `--token` flag was removed because command-line secrets can be exposed through OS process listings.
|
|
192
194
|
|
|
193
195
|
> [!WARNING]
|
|
194
|
-
> **Never bind `--host=0.0.0.0` directly to the public internet.** Doing so exposes your entire Obsidian vault to anyone who can reach the port.
|
|
196
|
+
> **Never bind `--host=0.0.0.0` directly to the public internet.** Doing so exposes your entire Obsidian vault to anyone who can reach the port. The server refuses HTTP startup without a bearer token, but if you need remote access:
|
|
195
197
|
> - Put the server behind a reverse proxy (nginx, Caddy, Cloudflare Tunnel) that terminates TLS, **and**
|
|
196
|
-
> -
|
|
198
|
+
> - Set `MCP_HTTP_TOKEN`, **and**
|
|
197
199
|
> - Restrict `--allow-origin` to the specific origins you trust, **and**
|
|
198
200
|
> - Set `--rate-limit` to cap request volume per IP.
|
|
199
201
|
>
|
|
@@ -203,17 +205,17 @@ Additional hardening flags:
|
|
|
203
205
|
|
|
204
206
|
| Flag | Purpose |
|
|
205
207
|
|------|---------|
|
|
206
|
-
| `--allow-origin=<csv>` | Restrict CORS to an allowlist (e.g. `https://claude.ai,https://chat.openai.com`). Default is
|
|
208
|
+
| `--allow-origin=<csv>` | Restrict CORS to an allowlist (e.g. `https://claude.ai,https://chat.openai.com`). Default is localhost-only; `*` requires bearer auth. |
|
|
207
209
|
| `--rate-limit=<n>` | Cap requests per minute per client IP. `/health` and `/version` are exempt. Default is unlimited. |
|
|
208
210
|
|
|
209
211
|
Operational endpoints (no auth required):
|
|
210
212
|
|
|
211
213
|
| Endpoint | Returns |
|
|
212
214
|
|----------|---------|
|
|
213
|
-
| `GET /health` | `{ status: "ok",
|
|
215
|
+
| `GET /health` | `{ status: "ok", version: <string> }` — liveness for monitoring. |
|
|
214
216
|
| `GET /version` | `{ version: <string> }` — package version, for rollout auditing. |
|
|
215
217
|
|
|
216
|
-
Structured logging is controlled by `LOG_LEVEL` (`debug`/`info`/`warn`/`error`/`silent`, default `info`) and `LOG_FORMAT` (`text`/`json`, default `text`). All logs go to stderr so the stdio transport on stdout is never polluted.
|
|
218
|
+
Structured logging is controlled by `LOG_LEVEL` (`debug`/`info`/`warn`/`error`/`silent`, default `info`) and `LOG_FORMAT` (`text`/`json`, default `text`). All logs go to stderr so the stdio transport on stdout is never polluted, with absolute paths, vault-relative path fields, secret-bearing URLs, and control characters redacted before write.
|
|
217
219
|
|
|
218
220
|
---
|
|
219
221
|
|
|
@@ -303,18 +305,22 @@ Restrict the tools' read/write surface to specific folders without exposing the
|
|
|
303
305
|
| `OBSIDIAN_READ_PATHS` | Comma- or colon-separated list of folders that read tools may access. Unset means unrestricted. Use `.` to mean the vault root. |
|
|
304
306
|
| `OBSIDIAN_WRITE_PATHS` | Same shape, but for mutations (create / append / update / delete / move / surgical edits). |
|
|
305
307
|
|
|
306
|
-
Read and write are independent, so an audit account can be read-only on most of the vault but write-only to a `Drafts/` folder. The startup log line and `--help` advertise the active scope. The allowlist is enforced at a single path-resolution choke point so every tool inherits it.
|
|
308
|
+
Read and write are independent, so an audit account can be read-only on most of the vault but write-only to a `Drafts/` folder. Moving a note requires read access to the source and write access to the destination, because the move carries the source content into its new folder. Surgical edits (`replace_in_note`, `update_section`, `insert_at_section`, and `edit_block`) require read access to the target plus write access because they inspect existing content before writing. In-vault symlinks are checked against their real target before the allowlist decision is finalized. The startup log line and `--help` advertise the active scope. The allowlist is enforced at a single path-resolution choke point so every tool inherits it.
|
|
307
309
|
|
|
308
|
-
###
|
|
310
|
+
### Caches
|
|
309
311
|
|
|
310
|
-
|
|
312
|
+
The note-content mtime cache is in-memory only. It speeds repeated vault-wide scans
|
|
313
|
+
inside the running process, but it does not persist note bodies or absolute paths to
|
|
314
|
+
disk. Any legacy `<vault>/.obsidian/cache/mcp-pro-index-cache.json` snapshot is
|
|
315
|
+
ignored and removed on first cache use.
|
|
316
|
+
|
|
317
|
+
Semantic embeddings live under `<vault>/.obsidian/cache/`:
|
|
311
318
|
|
|
312
319
|
| File | Purpose |
|
|
313
320
|
|------|---------|
|
|
314
|
-
| `mcp-pro-
|
|
315
|
-
| `mcp-pro-embeddings.json` | Persisted embeddings for semantic search (only present once `index_vault` has run). Vault-relocation safe via an embedded `vaultRoot` check; switching providers / models invalidates entries automatically. |
|
|
321
|
+
| `mcp-pro-embeddings.json` | Persisted embeddings for semantic search (only present once `index_vault` has run). Vault-relocation safe via an embedded `vaultRoot` check; switching providers/models or stale note content invalidates entries automatically. |
|
|
316
322
|
|
|
317
|
-
|
|
323
|
+
The embedding cache is vault-local, excluded from vault scans (`.obsidian/` is pruned), and can be deleted at any time. Persistence can be turned off with `OBSIDIAN_CACHE_DISABLED=1`.
|
|
318
324
|
|
|
319
325
|
### Semantic Search Provider
|
|
320
326
|
|
|
@@ -329,9 +335,17 @@ The semantic-search tools (`index_vault`, `search_semantic`, `find_similar_notes
|
|
|
329
335
|
|
|
330
336
|
For local Ollama: install [Ollama](https://ollama.com/), then `ollama pull nomic-embed-text`. The semantic tools register even when no provider is configured, so they're discoverable; calls return a configuration hint until set up.
|
|
331
337
|
|
|
338
|
+
`index_vault` sends readable note chunks to that provider. Calls must include:
|
|
339
|
+
|
|
340
|
+
```json
|
|
341
|
+
{ "confirm": "send-vault-text-to-embedding-provider" }
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
This latch is per call so scripted index refreshes have to keep the privacy decision visible.
|
|
345
|
+
|
|
332
346
|
### Observability
|
|
333
347
|
|
|
334
|
-
Logs stream to stderr as either plain text (default) or single-line JSON, controlled by `LOG_LEVEL` (`debug`/`info`/`warn`/`error`/`silent`) and `LOG_FORMAT` (`text`/`json`).
|
|
348
|
+
Logs stream to stderr as either plain text (default) or single-line JSON, controlled by `LOG_LEVEL` (`debug`/`info`/`warn`/`error`/`silent`) and `LOG_FORMAT` (`text`/`json`). Local stderr and MCP-forwarded log payloads both redact absolute paths, vault-relative path fields, secret-bearing URLs, and control characters.
|
|
335
349
|
|
|
336
350
|
The server also declares the MCP [`logging` capability](https://modelcontextprotocol.io/specification), so every log line is forwarded to the connected client as a `notifications/message` frame alongside tool responses. Clients that honor `logging/setLevel` can filter server-side logs at runtime without restarting. Claude Desktop surfaces these in its MCP DevTools pane; most other clients currently ignore them, so this is useful primarily for self-hosters and tooling authors.
|
|
337
351
|
|
|
@@ -339,12 +353,22 @@ The server also declares the MCP [`logging` capability](https://modelcontextprot
|
|
|
339
353
|
|
|
340
354
|
## Security
|
|
341
355
|
|
|
342
|
-
- **Vault boundary** — every tool and resource routes through a single path resolver that rejects `..` traversal, null-byte injection, and symlinks pointing outside the vault (ancestor-realpath check).
|
|
356
|
+
- **Vault boundary** — every tool and resource routes through a single path resolver that rejects `..` traversal, null-byte injection, and symlinks pointing outside the vault (ancestor-realpath check). On Windows it also rejects reserved device names, alternate data stream syntax, and trailing-dot/space filename aliases.
|
|
357
|
+
- **Note/file boundary** — note read and edit surfaces only target `.md` files; attachments, Canvas files, and Bases stay on their dedicated tools with their own caps and parsers.
|
|
358
|
+
- **Full-note cap** — full `.md` note reads and read-modify-write helpers refuse notes over 5 MiB before materializing them; `get_note` line fragments still stream from large notes for targeted inspection.
|
|
343
359
|
- **Excluded directories** — `.obsidian`, `.git`, and `.trash` are pruned at traversal time and at resolution time, so nested occurrences never leak back to clients.
|
|
344
|
-
- **HTTP transport** — binds to `127.0.0.1` by default with DNS rebinding protection (host-header allowlist).
|
|
345
|
-
- **
|
|
360
|
+
- **HTTP transport** — requires a bearer token at startup and binds to `127.0.0.1` by default with DNS rebinding protection (host-header allowlist). `/mcp` requests must send `Authorization: Bearer <secret>`, compared in constant time. Malformed request URL or Host data is rejected before routing.
|
|
361
|
+
- **Attachment safety** — executable extensions are blocked, SVGs are returned as `text/plain`, and active text formats such as HTML/XML/CSS are served with `text/plain` resource metadata.
|
|
362
|
+
- **Canvas link safety** — Canvas link nodes added through the server accept only absolute `http://` and `https://` URLs, preventing local file and application-protocol links from being persisted by tool calls.
|
|
363
|
+
- **Regex edit safety** — `replace_in_note` caps regex pattern/input size and rejects backtracking-prone repeated groups before matching.
|
|
364
|
+
- **Error and log sanitization** — filesystem error messages are stripped of absolute host paths before being returned to MCP clients, local stderr and MCP-forwarded logs redact paths and secret-bearing URLs, ASCII control bytes and Unicode bidi controls are escaped in displayed values, and note-derived duplicate-alias text is not copied into graph warnings. Uncaught HTTP errors respond with a generic `Internal server error` body while server logs keep sanitized diagnostics.
|
|
365
|
+
- **Untrusted vault text boundaries** — tool outputs that include note bodies, read, search result, semantic result, semantic index failure, write rewrite-warning, link-graph, attachment, Base, and Canvas path summaries, attachment extension summaries, search snippets, semantic snippets and heading paths, tag lists, tag search result paths and previews, tag rename skipped-note rows, frontmatter values, Base data, wikilink targets in link-analysis output, SVG attachment text, section headings, Canvas node identities, colors, previews, edge endpoints, and edge labels, or backlink context wrap those vault-authored portions in `[BEGIN UNTRUSTED VAULT CONTENT: ...]` / `[END UNTRUSTED VAULT CONTENT: ...]` markers before they enter an MCP client's model context. Note and daily resources use the same visible boundaries for markdown bodies and carry `_meta["obsidian-mcp-pro/contentTrust"] = "untrusted-vault-content"`.
|
|
366
|
+
- **YAML parser boundaries** — Obsidian properties are parsed only from `---` YAML delimiter lines; non-YAML gray-matter language blocks stay as body text, oversized frontmatter is skipped on reads and refused for metadata updates, and note/Base YAML containing anchors or aliases is not parsed.
|
|
367
|
+
- **Semantic index freshness** — `search_semantic` and `find_similar_notes` re-check current note content hashes before returning stored snippets or source-note embeddings, pruning stale cache entries instead of surfacing old note text.
|
|
368
|
+
- **Semantic indexing confirmation** — `index_vault` refuses to read and embed vault notes unless the call includes `confirm: "send-vault-text-to-embedding-provider"`, making provider-bound note transfer explicit.
|
|
369
|
+
- **Bulk-write confirmations** — `move_note` reference rewrites require `confirmPath` to match the destination path, and non-dry-run `rename_tag` requires `confirmTag` to match the new tag. Clients that support MCP elicitation are also asked to re-type the destination path or new tag before the rewrite runs.
|
|
346
370
|
- **Atomic writes** — every note write (`create_note`, `append`, `prepend`, `update_frontmatter`, canvas mutations) stages content to a sibling temp file then renames onto the target, so a crash or kill mid-write never leaves a truncated file. Combined with per-path locks for the full read-modify-write cycle, concurrent callers can't lose each other's updates. The `install` subcommand uses the same pattern and keeps a backup of the previous config.
|
|
347
|
-
- **Rate limiting + CORS allowlist** — optional `--rate-limit` caps per-IP request volume; `--allow-origin` restricts browser-facing CORS. `/health` and `/version` stay reachable under load for monitoring.
|
|
371
|
+
- **Rate limiting + CORS allowlist** — optional `--rate-limit` caps per-IP request volume; `--allow-origin` restricts browser-facing CORS and refuses `*` unless bearer auth is enabled. `/health` and `/version` stay reachable under load for monitoring.
|
|
348
372
|
- **Request timeout** — HTTP POST requests are capped at 2 minutes of wall-clock time. Long-lived SSE GET streams are exempt so idle clients aren't reaped.
|
|
349
373
|
- **Process supervision** — `uncaughtException` exits cleanly so systemd/Docker/npx supervisors can restart; `unhandledRejection` logs but doesn't kill the process.
|
|
350
374
|
|
|
@@ -369,11 +393,11 @@ Tag extraction is similarly case-tolerant: `tags`, `Tags`, `TAGS`, `tag`, and `T
|
|
|
369
393
|
|
|
370
394
|
| Tool | Description | Key Parameters |
|
|
371
395
|
|------|-------------|----------------|
|
|
372
|
-
| `search_notes` |
|
|
396
|
+
| `search_notes` | Focus-ranked full-text search with query-centered snippets (cached) | `query`, `caseSensitive`, `maxResults`, `folder` |
|
|
373
397
|
| `get_note` | Read a note whole, or by `section` / `block` / `lines` | `path`, `section`, `block`, `lines` |
|
|
374
398
|
| `list_notes` | List notes in the vault or a folder | `folder`, `limit` |
|
|
375
399
|
| `get_daily_note` | Get today's (or a specific date's) daily note | `date` |
|
|
376
|
-
| `search_by_frontmatter` | Find notes by frontmatter property values | `property`, `value`, `folder` |
|
|
400
|
+
| `search_by_frontmatter` | Find notes by frontmatter property values with case-insensitive key/value matching | `property`, `value`, `folder` |
|
|
377
401
|
| `get_recent_notes` | Notes sorted by mtime; optional ISO-or-relative `since` filter | `limit`, `since`, `folder` |
|
|
378
402
|
| `get_vault_stats` | Vault counts, bytes, words, tag coverage, most-recent note | `folder` |
|
|
379
403
|
| `resolve_alias` | Translate frontmatter alias (or basename) to note path | `name`, `includeBasename` |
|
|
@@ -387,7 +411,7 @@ Tag extraction is similarly case-tolerant: `tags`, `Tags`, `TAGS`, `tag`, and `T
|
|
|
387
411
|
| `prepend_to_note` | Prepend content after frontmatter | `path`, `content` |
|
|
388
412
|
| `update_frontmatter` | Update frontmatter properties on a note | `path`, `properties` |
|
|
389
413
|
| `create_daily_note` | Create today's daily note from template | `date`, `content`, `templatePath` |
|
|
390
|
-
| `move_note` | Move or rename a note; rewrites references across the vault | `oldPath`, `newPath`, `updateLinks` |
|
|
414
|
+
| `move_note` | Move or rename a note; rewrites references across the vault | `oldPath`, `newPath`, `updateLinks`, `confirmPath` |
|
|
391
415
|
| `delete_note` | Delete a note (trash by default); optional elicitation on permanent | `path`, `permanent`, `removeReferences` |
|
|
392
416
|
|
|
393
417
|
### Section-level edits
|
|
@@ -406,7 +430,7 @@ Tag extraction is similarly case-tolerant: `tags`, `Tags`, `TAGS`, `tag`, and `T
|
|
|
406
430
|
|------|-------------|----------------|
|
|
407
431
|
| `get_tags` | Get all tags and their usage counts | `sortBy` |
|
|
408
432
|
| `search_by_tag` | Find all notes with a specific tag | `tag`, `includeContent` |
|
|
409
|
-
| `rename_tag` | Rewrite inline + frontmatter occurrences vault-wide; hierarchical | `oldName`, `newName`, `hierarchical`, `dryRun` |
|
|
433
|
+
| `rename_tag` | Rewrite inline + frontmatter occurrences vault-wide; hierarchical | `oldName`, `newName`, `hierarchical`, `dryRun`, `confirmTag` |
|
|
410
434
|
|
|
411
435
|
### Links & graph
|
|
412
436
|
|
|
@@ -423,7 +447,7 @@ Tag extraction is similarly case-tolerant: `tags`, `Tags`, `TAGS`, `tag`, and `T
|
|
|
423
447
|
| Tool | Description | Key Parameters |
|
|
424
448
|
|------|-------------|----------------|
|
|
425
449
|
| `list_canvases` | List all `.canvas` files in the vault | (none) |
|
|
426
|
-
| `read_canvas` | Read a `.canvas`
|
|
450
|
+
| `read_canvas` | Read a bounded `.canvas` node/edge summary | `path` |
|
|
427
451
|
| `add_canvas_node` | Add a node to a canvas | `canvasPath`, `type`, `content`, `x`, `y` |
|
|
428
452
|
| `add_canvas_edge` | Add an edge between two canvas nodes | `canvasPath`, `fromNode`, `toNode` |
|
|
429
453
|
|
|
@@ -433,7 +457,7 @@ Tag extraction is similarly case-tolerant: `tags`, `Tags`, `TAGS`, `tag`, and `T
|
|
|
433
457
|
|------|-------------|----------------|
|
|
434
458
|
| `list_bases` | Enumerate `.base` files in the vault | (none) |
|
|
435
459
|
| `read_base` | Parse a Base file (filters, properties, views) | `path` |
|
|
436
|
-
| `query_base` | Run a Base's filter DSL against the vault | `path`, `view`, `limit`, `includeFrontmatter` |
|
|
460
|
+
| `query_base` | Run a Base's filter DSL against the vault; unsupported filters warn and fail closed | `path`, `view`, `limit`, `includeFrontmatter` |
|
|
437
461
|
|
|
438
462
|
### Attachments
|
|
439
463
|
|
|
@@ -447,9 +471,9 @@ Tag extraction is similarly case-tolerant: `tags`, `Tags`, `TAGS`, `tag`, and `T
|
|
|
447
471
|
|
|
448
472
|
| Tool | Description | Key Parameters |
|
|
449
473
|
|------|-------------|----------------|
|
|
450
|
-
| `index_vault` | Build / refresh the embedding index (incremental, progress events) | `force`, `folder` |
|
|
474
|
+
| `index_vault` | Build / refresh the embedding index (incremental, progress events) | `force`, `folder`, `confirm` |
|
|
451
475
|
| `search_semantic` | Cosine search the embedding index for a natural-language query | `query`, `limit`, `folder`, `includeSnippet` |
|
|
452
|
-
| `find_similar_notes` | Surface notes most similar to a source note (no live API call) | `path`, `limit` |
|
|
476
|
+
| `find_similar_notes` | Surface notes most similar to a source note, anchored to its opening topic (no live API call) | `path`, `limit` |
|
|
453
477
|
|
|
454
478
|
---
|
|
455
479
|
|
|
@@ -487,7 +511,7 @@ All tool paths must be **vault-relative** (e.g. `notes/hello.md`), never absolut
|
|
|
487
511
|
|
|
488
512
|
### HTTP Transport Returns `401 Unauthorized`
|
|
489
513
|
|
|
490
|
-
The server was started with
|
|
514
|
+
The server was started with `MCP_HTTP_TOKEN` but the client isn't sending a matching `Authorization: Bearer <secret>` header. Verify the token value and that the header is present — comparison is case-sensitive and constant-time.
|
|
491
515
|
|
|
492
516
|
### HTTP Transport Returns `429 Too Many Requests`
|
|
493
517
|
|
|
@@ -520,6 +544,9 @@ npm install
|
|
|
520
544
|
# Build
|
|
521
545
|
npm run build
|
|
522
546
|
|
|
547
|
+
# Run the full local maintenance gate
|
|
548
|
+
npm run verify
|
|
549
|
+
|
|
523
550
|
# Run in development (watch mode)
|
|
524
551
|
npm run dev
|
|
525
552
|
|
|
@@ -527,6 +554,10 @@ npm run dev
|
|
|
527
554
|
OBSIDIAN_VAULT_PATH=/path/to/vault npm start
|
|
528
555
|
```
|
|
529
556
|
|
|
557
|
+
For the company-grade maintenance and R&D operating plan, see [docs/DEVELOPMENT_GOAL.md](./docs/DEVELOPMENT_GOAL.md).
|
|
558
|
+
|
|
559
|
+
GitHub Actions workflows are manual-only in this repository. Use `npm run verify` as the required local gate before merging or publishing.
|
|
560
|
+
|
|
530
561
|
### Project Structure
|
|
531
562
|
|
|
532
563
|
```
|
|
@@ -548,7 +579,7 @@ src/
|
|
|
548
579
|
chunker.ts # Heading-aware chunking for embeddings
|
|
549
580
|
embedding-providers.ts# Ollama + OpenAI providers
|
|
550
581
|
embedding-store.ts # Persistent vector index, cosine search
|
|
551
|
-
index-cache.ts # mtime-keyed content cache (in-memory
|
|
582
|
+
index-cache.ts # mtime-keyed content cache (in-memory)
|
|
552
583
|
progress.ts # MCP progress-notification helper
|
|
553
584
|
mime.ts # extension -> MIME map for attachments
|
|
554
585
|
dates.ts # Moment-style date format for daily-note filenames
|
|
@@ -621,8 +652,8 @@ npm run lint:fix # auto-fix
|
|
|
621
652
|
block (and don't expose subsequent content to wikilink rewriting).
|
|
622
653
|
- **`/health` no longer leaks the live session count when a Bearer
|
|
623
654
|
token is configured.** Status + version stay public for monitoring;
|
|
624
|
-
`sessions` is dropped in authenticated deployments.
|
|
625
|
-
|
|
655
|
+
`sessions` is dropped in authenticated deployments. In 4.0.0 and
|
|
656
|
+
later, HTTP always starts with bearer auth, so the field stays hidden.
|
|
626
657
|
- **`constantTimeEqual` is now fully length-safe** (pads both inputs
|
|
627
658
|
to a fixed width before comparing), and the regex / parser hardening
|
|
628
659
|
list also closed: `resolveWikilink` proximity tie-break for
|
|
@@ -665,11 +696,11 @@ npm run lint:fix # auto-fix
|
|
|
665
696
|
- **Attachments.** `list_attachments`, `find_unused_attachments`, `get_attachment` (returns image / audio / blob bytes inline).
|
|
666
697
|
- **Tag renames vault-wide.** `rename_tag` rewrites both inline `#tag` and frontmatter `tags:` (hierarchical mode rebases nested sub-tags).
|
|
667
698
|
- **Folder-scoped permissions.** `OBSIDIAN_READ_PATHS` / `OBSIDIAN_WRITE_PATHS` allowlists.
|
|
668
|
-
- **
|
|
699
|
+
- **In-memory mtime cache.** Vault-wide scans (`get_tags`, `search_notes`, `search_by_tag`) reuse unchanged note content within the running process without persisting note bodies to disk.
|
|
669
700
|
- **Quick wins.** `get_recent_notes`, `get_vault_stats`, `resolve_alias`.
|
|
670
701
|
- **MCP prompts.** `daily-review`, `weekly-rollup`, `find-stale-notes`, `extract-action-items`, `build-moc`.
|
|
671
702
|
- **Progress notifications** on `rename_tag`, `find_unused_attachments`, `index_vault`.
|
|
672
|
-
- **
|
|
703
|
+
- **Bulk-write confirmation latches** on `delete_note(permanent: true)`, `move_note` reference rewrites, and non-dry-run `rename_tag`, plus elicitation prompts for clients that support them.
|
|
673
704
|
- **eslint** wired up with typescript-eslint flat config; `npm run lint` and `lint:fix`.
|
|
674
705
|
|
|
675
706
|
**v1.7.0** — `delete_note` reference handling:
|
package/build/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAuL/D,wBAAgB,cAAc,IAAI,WAAW,CAsB5C;AAED,wBAAsB,kBAAkB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAwDrF"}
|
package/build/config.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
|
-
import * as fsp from "fs/promises";
|
|
3
2
|
import * as path from "path";
|
|
4
3
|
import * as os from "os";
|
|
5
4
|
import { log } from "./lib/logger.js";
|
|
5
|
+
import { openVaultInternalFileForRead } from "./lib/vault.js";
|
|
6
|
+
const DAILY_NOTES_CONFIG_REL_PATH = ".obsidian/daily-notes.json";
|
|
7
|
+
const MAX_DAILY_NOTES_CONFIG_BYTES = 64 * 1024;
|
|
8
|
+
const MAX_DAILY_NOTES_CONFIG_FIELD_CHARS = 500;
|
|
6
9
|
function getObsidianConfigPath() {
|
|
7
10
|
const platform = os.platform();
|
|
8
11
|
if (platform === "win32") {
|
|
@@ -35,6 +38,18 @@ function isObsidianConfig(value) {
|
|
|
35
38
|
}
|
|
36
39
|
return true;
|
|
37
40
|
}
|
|
41
|
+
function isPlainObject(value) {
|
|
42
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
43
|
+
}
|
|
44
|
+
function boundedDailyConfigString(value) {
|
|
45
|
+
if (typeof value !== "string")
|
|
46
|
+
return undefined;
|
|
47
|
+
if (value.length > MAX_DAILY_NOTES_CONFIG_FIELD_CHARS)
|
|
48
|
+
return undefined;
|
|
49
|
+
if (value.includes("\0"))
|
|
50
|
+
return undefined;
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
38
53
|
function isValidVaultPath(vaultPath) {
|
|
39
54
|
try {
|
|
40
55
|
const obsidianDir = path.join(vaultPath, ".obsidian");
|
|
@@ -54,9 +69,8 @@ function resolveVaultFromEnv() {
|
|
|
54
69
|
}
|
|
55
70
|
const resolved = path.resolve(envPath);
|
|
56
71
|
if (!isValidVaultPath(resolved)) {
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
// make the error useless. This never goes into a tool response.
|
|
72
|
+
// The logger redacts the path before writing to stderr or forwarding to
|
|
73
|
+
// MCP clients, so this stays useful without leaking host layout.
|
|
60
74
|
log.warn("OBSIDIAN_VAULT_PATH is not a valid vault (missing .obsidian dir)", {
|
|
61
75
|
vaultPath: resolved,
|
|
62
76
|
});
|
|
@@ -160,10 +174,26 @@ export async function getDailyNoteConfig(vaultPath) {
|
|
|
160
174
|
format: "YYYY-MM-DD",
|
|
161
175
|
};
|
|
162
176
|
const resolvedVaultPath = vaultPath ?? getVaultConfig().vaultPath;
|
|
163
|
-
|
|
177
|
+
let dailyNotesConfigPath = "";
|
|
164
178
|
let raw;
|
|
165
179
|
try {
|
|
166
|
-
|
|
180
|
+
const opened = await openVaultInternalFileForRead(resolvedVaultPath, DAILY_NOTES_CONFIG_REL_PATH);
|
|
181
|
+
dailyNotesConfigPath = opened.fullPath;
|
|
182
|
+
const stats = opened.stats;
|
|
183
|
+
if (stats.size > MAX_DAILY_NOTES_CONFIG_BYTES) {
|
|
184
|
+
await opened.handle.close();
|
|
185
|
+
log.warn("Daily notes config exceeds size cap; using defaults", {
|
|
186
|
+
bytes: stats.size,
|
|
187
|
+
max: MAX_DAILY_NOTES_CONFIG_BYTES,
|
|
188
|
+
});
|
|
189
|
+
return defaults;
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
raw = await opened.handle.readFile("utf-8");
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
await opened.handle.close();
|
|
196
|
+
}
|
|
167
197
|
}
|
|
168
198
|
catch (err) {
|
|
169
199
|
if (err.code === "ENOENT")
|
|
@@ -176,10 +206,15 @@ export async function getDailyNoteConfig(vaultPath) {
|
|
|
176
206
|
}
|
|
177
207
|
try {
|
|
178
208
|
const parsed = JSON.parse(raw);
|
|
209
|
+
if (!isPlainObject(parsed))
|
|
210
|
+
return defaults;
|
|
211
|
+
const folder = boundedDailyConfigString(parsed.folder);
|
|
212
|
+
const format = boundedDailyConfigString(parsed.format);
|
|
213
|
+
const template = boundedDailyConfigString(parsed.template);
|
|
179
214
|
return {
|
|
180
|
-
folder:
|
|
181
|
-
format:
|
|
182
|
-
template
|
|
215
|
+
folder: folder ?? defaults.folder,
|
|
216
|
+
format: format ?? defaults.format,
|
|
217
|
+
template,
|
|
183
218
|
};
|
|
184
219
|
}
|
|
185
220
|
catch (err) {
|
package/build/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/build/http-server.d.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
export interface HttpServerOptions {
|
|
3
3
|
host: string;
|
|
4
4
|
port: number;
|
|
5
|
-
|
|
5
|
+
/** Required for HTTP transport. Use MCP_HTTP_TOKEN in the CLI path. */
|
|
6
|
+
bearerToken: string;
|
|
6
7
|
buildMcpServer: () => McpServer;
|
|
7
8
|
/** Install SIGINT/SIGTERM handlers + exit the process on shutdown. Default
|
|
8
9
|
* `true` for CLI use. Set `false` when embedding (e.g. inside an Obsidian
|
|
@@ -13,7 +14,8 @@ export interface HttpServerOptions {
|
|
|
13
14
|
/** Allowed CORS origins. Defaults to localhost-only patterns
|
|
14
15
|
* (`["http://localhost:*", "http://127.0.0.1:*", "http://[::1]:*"]`).
|
|
15
16
|
* Use an explicit list (e.g. `["https://claude.ai"]`) for browser-facing
|
|
16
|
-
* deployments. Pass `["*"]` to allow all origins
|
|
17
|
+
* deployments. Pass `["*"]` to allow all origins only when Bearer auth is
|
|
18
|
+
* configured.
|
|
17
19
|
* Requests from other origins still succeed (CORS is a browser-only
|
|
18
20
|
* restriction) but the browser will reject the response. */
|
|
19
21
|
allowedOrigins?: string[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAKpE,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,
|
|
1
|
+
{"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAKpE,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,uEAAuE;IACvE,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,SAAS,CAAC;IAChC;;wEAEoE;IACpE,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,sEAAsE;IACtE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;iEAM6D;IAC7D,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,sEAAsE;IACtE,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAkND,wBAAsB,eAAe,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAyVxF"}
|
package/build/http-server.js
CHANGED
|
@@ -21,29 +21,42 @@ class BodyTooLargeError extends Error {
|
|
|
21
21
|
this.name = "BodyTooLargeError";
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
+
function declaredBodyTooLarge(req) {
|
|
25
|
+
const contentLength = req.headers["content-length"];
|
|
26
|
+
if (typeof contentLength !== "string" || !/^\d+$/.test(contentLength))
|
|
27
|
+
return false;
|
|
28
|
+
return BigInt(contentLength) > BigInt(MAX_BODY_BYTES);
|
|
29
|
+
}
|
|
24
30
|
function readBody(req) {
|
|
25
31
|
return new Promise((resolve, reject) => {
|
|
26
32
|
let size = 0;
|
|
27
|
-
let
|
|
33
|
+
let settled = false;
|
|
28
34
|
const chunks = [];
|
|
35
|
+
const fail = (err) => {
|
|
36
|
+
if (settled)
|
|
37
|
+
return;
|
|
38
|
+
settled = true;
|
|
39
|
+
chunks.length = 0;
|
|
40
|
+
reject(err);
|
|
41
|
+
};
|
|
29
42
|
req.on("data", (chunk) => {
|
|
30
|
-
if (
|
|
43
|
+
if (settled)
|
|
31
44
|
return;
|
|
32
45
|
size += chunk.length;
|
|
33
46
|
if (size > MAX_BODY_BYTES) {
|
|
34
|
-
// Stop buffering
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
chunks.length = 0;
|
|
47
|
+
// Stop buffering and let the caller return 413 immediately; keep the
|
|
48
|
+
// request stream flowing so the socket can drain without a hard reset.
|
|
49
|
+
fail(new BodyTooLargeError());
|
|
50
|
+
req.resume();
|
|
39
51
|
}
|
|
40
52
|
else {
|
|
41
53
|
chunks.push(chunk);
|
|
42
54
|
}
|
|
43
55
|
});
|
|
44
56
|
req.on("end", () => {
|
|
45
|
-
if (
|
|
46
|
-
return
|
|
57
|
+
if (settled)
|
|
58
|
+
return;
|
|
59
|
+
settled = true;
|
|
47
60
|
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
48
61
|
if (!raw)
|
|
49
62
|
return resolve(undefined);
|
|
@@ -54,7 +67,7 @@ function readBody(req) {
|
|
|
54
67
|
reject(err instanceof Error ? err : new Error("Invalid JSON body"));
|
|
55
68
|
}
|
|
56
69
|
});
|
|
57
|
-
req.on("error",
|
|
70
|
+
req.on("error", (err) => fail(err));
|
|
58
71
|
});
|
|
59
72
|
}
|
|
60
73
|
function setSecurityHeaders(req, res) {
|
|
@@ -73,6 +86,14 @@ function sendJson(res, status, body) {
|
|
|
73
86
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
74
87
|
res.end(JSON.stringify(body));
|
|
75
88
|
}
|
|
89
|
+
function parseRequestUrl(req) {
|
|
90
|
+
try {
|
|
91
|
+
return new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
76
97
|
// Constant-time string compare that does not leak the expected token's
|
|
77
98
|
// length. Both inputs are padded to a fixed comparison width (the longer
|
|
78
99
|
// of the two) before `timingSafeEqual`, so the compare time is the same
|
|
@@ -189,6 +210,12 @@ export async function startHttpServer(opts) {
|
|
|
189
210
|
if (opts.bearerToken !== undefined && !bearerToken) {
|
|
190
211
|
throw new Error("HTTP bearer token cannot be empty");
|
|
191
212
|
}
|
|
213
|
+
if (!bearerToken) {
|
|
214
|
+
throw new Error("HTTP bearer token is required. Set MCP_HTTP_TOKEN in the CLI or pass bearerToken when embedding.");
|
|
215
|
+
}
|
|
216
|
+
const allowedOrigins = opts.allowedOrigins && opts.allowedOrigins.length > 0
|
|
217
|
+
? opts.allowedOrigins
|
|
218
|
+
: ["http://localhost:*", "http://127.0.0.1:*", "http://[::1]:*"];
|
|
192
219
|
const transports = new Map();
|
|
193
220
|
const lastActivity = new Map();
|
|
194
221
|
const touch = (sid) => { lastActivity.set(sid, Date.now()); };
|
|
@@ -198,9 +225,6 @@ export async function startHttpServer(opts) {
|
|
|
198
225
|
// `initialize` builds a fresh server below; GC reclaims it once the
|
|
199
226
|
// transport closes (Protocol._onclose clears the transport reference).
|
|
200
227
|
// See https://github.com/rps321321/obsidian-mcp-pro/issues/8.
|
|
201
|
-
const allowedOrigins = opts.allowedOrigins && opts.allowedOrigins.length > 0
|
|
202
|
-
? opts.allowedOrigins
|
|
203
|
-
: ["http://localhost:*", "http://127.0.0.1:*", "http://[::1]:*"];
|
|
204
228
|
if (allowedOrigins.includes("*")) {
|
|
205
229
|
log.warn("CORS configured with wildcard origin '*'. Consider restricting to specific origins for production deployments.");
|
|
206
230
|
}
|
|
@@ -254,6 +278,16 @@ export async function startHttpServer(opts) {
|
|
|
254
278
|
}
|
|
255
279
|
setCors(req, res, allowedOrigins);
|
|
256
280
|
setSecurityHeaders(req, res);
|
|
281
|
+
const url = parseRequestUrl(req);
|
|
282
|
+
if (!url) {
|
|
283
|
+
log.warn("Rejected malformed HTTP request URL", {
|
|
284
|
+
method: req.method,
|
|
285
|
+
path: req.url ?? "",
|
|
286
|
+
ip: clientIp(req),
|
|
287
|
+
});
|
|
288
|
+
sendJson(res, 400, { error: "Malformed request URL" });
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
257
291
|
const requestOrigin = req.headers.origin;
|
|
258
292
|
if (typeof requestOrigin === "string" && !originAllowed(requestOrigin, allowedOrigins)) {
|
|
259
293
|
log.warn("Rejected request from disallowed Origin", {
|
|
@@ -275,7 +309,6 @@ export async function startHttpServer(opts) {
|
|
|
275
309
|
// need to stay reachable for monitoring even under load.
|
|
276
310
|
if (rateLimiter) {
|
|
277
311
|
const ip = clientIp(req);
|
|
278
|
-
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
279
312
|
const exempt = url.pathname === "/health" || url.pathname === "/version";
|
|
280
313
|
if (!exempt && !rateLimiter.check(ip)) {
|
|
281
314
|
res.setHeader("Retry-After", "60");
|
|
@@ -283,7 +316,6 @@ export async function startHttpServer(opts) {
|
|
|
283
316
|
return;
|
|
284
317
|
}
|
|
285
318
|
}
|
|
286
|
-
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
287
319
|
if (url.pathname !== "/mcp") {
|
|
288
320
|
if (url.pathname === "/health") {
|
|
289
321
|
// When a Bearer token is configured (production deployments),
|
|
@@ -294,8 +326,6 @@ export async function startHttpServer(opts) {
|
|
|
294
326
|
status: "ok",
|
|
295
327
|
version: opts.version ?? "",
|
|
296
328
|
};
|
|
297
|
-
if (!bearerToken)
|
|
298
|
-
body.sessions = transports.size;
|
|
299
329
|
sendJson(res, 200, body);
|
|
300
330
|
return;
|
|
301
331
|
}
|
|
@@ -352,6 +382,11 @@ export async function startHttpServer(opts) {
|
|
|
352
382
|
sendJson(res, 415, { error: "Unsupported Media Type: expected application/json" });
|
|
353
383
|
return;
|
|
354
384
|
}
|
|
385
|
+
if (declaredBodyTooLarge(req)) {
|
|
386
|
+
req.resume();
|
|
387
|
+
sendJson(res, 413, { error: "Request body too large" });
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
355
390
|
let body;
|
|
356
391
|
try {
|
|
357
392
|
body = await readBody(req);
|
|
@@ -450,9 +485,6 @@ export async function startHttpServer(opts) {
|
|
|
450
485
|
allowedOrigins: allowedOrigins.join(","),
|
|
451
486
|
rateLimitPerMinute: opts.rateLimitPerMinute ?? 0,
|
|
452
487
|
});
|
|
453
|
-
if (!bearerToken) {
|
|
454
|
-
log.warn("WARNING: HTTP server starting without authentication. Set MCP_HTTP_TOKEN to enable bearer token auth.");
|
|
455
|
-
}
|
|
456
488
|
const installSignals = opts.installSignalHandlers ?? true;
|
|
457
489
|
const onSignal = () => {
|
|
458
490
|
void stop().finally(() => {
|