obsidian-mcp-server 3.3.1 → 3.5.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.
Files changed (36) hide show
  1. package/AGENTS.md +7 -5
  2. package/CLAUDE.md +7 -5
  3. package/README.md +17 -7
  4. package/changelog/3.4.x/3.4.0.md +25 -0
  5. package/changelog/3.5.x/3.5.0.md +16 -0
  6. package/dist/mcp-server/resources/definitions/obsidian-tags.resource.d.ts +8 -2
  7. package/dist/mcp-server/resources/definitions/obsidian-tags.resource.d.ts.map +1 -1
  8. package/dist/mcp-server/resources/definitions/obsidian-tags.resource.js +10 -4
  9. package/dist/mcp-server/resources/definitions/obsidian-tags.resource.js.map +1 -1
  10. package/dist/mcp-server/tools/definitions/index.d.ts +21 -2
  11. package/dist/mcp-server/tools/definitions/index.d.ts.map +1 -1
  12. package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.d.ts +15 -6
  13. package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.d.ts.map +1 -1
  14. package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.js +90 -21
  15. package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.js.map +1 -1
  16. package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.d.ts +14 -1
  17. package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.d.ts.map +1 -1
  18. package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.js +85 -23
  19. package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.js.map +1 -1
  20. package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.d.ts +30 -2
  21. package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.d.ts.map +1 -1
  22. package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.js +57 -12
  23. package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.js.map +1 -1
  24. package/dist/services/obsidian/frontmatter-ops.d.ts +42 -1
  25. package/dist/services/obsidian/frontmatter-ops.d.ts.map +1 -1
  26. package/dist/services/obsidian/frontmatter-ops.js +98 -21
  27. package/dist/services/obsidian/frontmatter-ops.js.map +1 -1
  28. package/dist/services/obsidian/obsidian-service.d.ts +7 -0
  29. package/dist/services/obsidian/obsidian-service.d.ts.map +1 -1
  30. package/dist/services/obsidian/obsidian-service.js +205 -48
  31. package/dist/services/obsidian/obsidian-service.js.map +1 -1
  32. package/dist/services/obsidian/types.d.ts +8 -0
  33. package/dist/services/obsidian/types.d.ts.map +1 -1
  34. package/manifest.json +1 -1
  35. package/package.json +2 -2
  36. package/server.json +3 -3
package/AGENTS.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Agent Protocol
2
2
 
3
3
  **Server:** obsidian-mcp-server
4
- **Version:** 3.3.1
4
+ **Version:** 3.5.0
5
5
  **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.12.3`
6
6
  **Engines:** Bun ≥1.3.0, Node ≥24.0.0
7
7
  **MCP SDK:** `@modelcontextprotocol/server` ^2.0.0
@@ -47,7 +47,7 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
47
47
 
48
48
  ### Tool — `obsidian_list_tags`
49
49
 
50
- A small read-only tool that wraps a single upstream endpoint, normalizes the response into the output schema, and renders a markdown twin in `format()`.
50
+ A small read-only tool that wraps a single upstream endpoint, normalizes the response into the output schema, and renders a markdown twin in `format()`. Reduced for illustration — the live definition also carries `nameRegex` / `minCount` / `limit` inputs, the count-descending sort and cap they feed, an `errors[]` contract, and an `enrichment` block.
51
51
 
52
52
  ```ts
53
53
  import { tool, z } from '@cyanheads/mcp-ts-core';
@@ -66,7 +66,7 @@ export const obsidianListTags = tool('obsidian_list_tags', {
66
66
  count: z.number().describe('Usage count across the vault.'),
67
67
  }).describe('A tag with its usage count.'),
68
68
  )
69
- .describe('All tags in the vault, in upstream-provided order.'),
69
+ .describe('Matching tags ordered by `count` descending.'),
70
70
  }),
71
71
  auth: ['tool:obsidian_list_tags:read'],
72
72
 
@@ -216,8 +216,10 @@ Services that accept `ctx` use the same resolver for parity. The Obsidian servic
216
216
 
217
217
  ```ts
218
218
  // inside obsidian-service.ts
219
- throw notFound(`Not found: ${display}`, data('note_missing'));
220
- // where data(reason) does: { path, reason, ...ctx.recoveryFor(reason), upstream? }
219
+ throw notFound(`Not found: ${display}`, data('note_missing'), { cause });
220
+ // where data(reason) does: { path, reason, ...ctx.recoveryFor(reason) }
221
+ // The upstream body is never spread into `data` — it rides as `cause`, which
222
+ // is non-enumerable and so reaches the log without reaching the client.
221
223
  ```
222
224
 
223
225
  **Fallback for ad-hoc throws** (no contract entry fits, prototype tools, service-layer code without a contract): use error factories.
package/CLAUDE.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Agent Protocol
2
2
 
3
3
  **Server:** obsidian-mcp-server
4
- **Version:** 3.3.1
4
+ **Version:** 3.5.0
5
5
  **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.12.3`
6
6
  **Engines:** Bun ≥1.3.0, Node ≥24.0.0
7
7
  **MCP SDK:** `@modelcontextprotocol/server` ^2.0.0
@@ -47,7 +47,7 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
47
47
 
48
48
  ### Tool — `obsidian_list_tags`
49
49
 
50
- A small read-only tool that wraps a single upstream endpoint, normalizes the response into the output schema, and renders a markdown twin in `format()`.
50
+ A small read-only tool that wraps a single upstream endpoint, normalizes the response into the output schema, and renders a markdown twin in `format()`. Reduced for illustration — the live definition also carries `nameRegex` / `minCount` / `limit` inputs, the count-descending sort and cap they feed, an `errors[]` contract, and an `enrichment` block.
51
51
 
52
52
  ```ts
53
53
  import { tool, z } from '@cyanheads/mcp-ts-core';
@@ -66,7 +66,7 @@ export const obsidianListTags = tool('obsidian_list_tags', {
66
66
  count: z.number().describe('Usage count across the vault.'),
67
67
  }).describe('A tag with its usage count.'),
68
68
  )
69
- .describe('All tags in the vault, in upstream-provided order.'),
69
+ .describe('Matching tags ordered by `count` descending.'),
70
70
  }),
71
71
  auth: ['tool:obsidian_list_tags:read'],
72
72
 
@@ -216,8 +216,10 @@ Services that accept `ctx` use the same resolver for parity. The Obsidian servic
216
216
 
217
217
  ```ts
218
218
  // inside obsidian-service.ts
219
- throw notFound(`Not found: ${display}`, data('note_missing'));
220
- // where data(reason) does: { path, reason, ...ctx.recoveryFor(reason), upstream? }
219
+ throw notFound(`Not found: ${display}`, data('note_missing'), { cause });
220
+ // where data(reason) does: { path, reason, ...ctx.recoveryFor(reason) }
221
+ // The upstream body is never spread into `data` — it rides as `cause`, which
222
+ // is non-enumerable and so reaches the log without reaching the client.
221
223
  ```
222
224
 
223
225
  **Fallback for ad-hoc throws** (no contract entry fits, prototype tools, service-layer code without a contract): use error factories.
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  <div align="center">
9
9
 
10
- [![Version](https://img.shields.io/badge/Version-3.3.1-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/obsidian-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.0.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![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) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.3.0-blueviolet.svg?style=flat-square)](https://bun.sh/)
10
+ [![Version](https://img.shields.io/badge/Version-3.5.0-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/obsidian-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.0.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![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) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.3.0-blueviolet.svg?style=flat-square)](https://bun.sh/)
11
11
 
12
12
  </div>
13
13
 
@@ -29,13 +29,13 @@ Fourteen tools grouped by shape — readers fetch notes and metadata, writers cr
29
29
  |:----------|:------------|
30
30
  | `obsidian_get_note` | Read a note as raw content, full structured form (content + frontmatter + tags + stat, with optional outgoing links), structural document map, or a single section. |
31
31
  | `obsidian_list_notes` | List notes and subdirectories under a vault path. Recursive walk (default depth 2, max depth 20; 1000-entry cap) with optional `extension` and `nameRegex` filters. |
32
- | `obsidian_list_tags` | List every tag found across the vault with usage counts, including hierarchical parents. Optional `nameRegex` post-filters the result set. |
32
+ | `obsidian_list_tags` | List vault tags with usage counts, including hierarchical parents. Ordered by count descending and capped at `limit` (default 200, max 10000), with the withheld remainder disclosed. Optional `nameRegex` and `minCount` narrow the set first. |
33
33
  | `obsidian_list_commands` | List Obsidian command-palette commands, optionally filtered by `nameRegex` on display name. **Opt-in via `OBSIDIAN_ENABLE_COMMANDS=true`** (paired with `obsidian_execute_command`). |
34
34
  | `obsidian_search_notes` | Search the vault by text, JSONLogic, or BM25-ranked Omnisearch (when the plugin is reachable). Results paginate via opaque cursors. |
35
35
  | `obsidian_write_note` | Create a note, replace a single section in place, or — with `overwrite: true` — clobber an existing file. Refuses whole-file writes against an existing path by default. |
36
36
  | `obsidian_append_to_note` | Append content to a note. Without `section`, creates the file if missing. With `section`, appends to a specific heading, block, or frontmatter field (file must exist). |
37
37
  | `obsidian_patch_note` | Surgical `append` / `prepend` / `replace` against a heading, block reference, or frontmatter field. |
38
- | `obsidian_replace_in_note` | Body-wide search-replace inside a single note. Literal or regex matching with whole-word, whitespace-flexible, and case-sensitivity options; supports capture-group replacement. |
38
+ | `obsidian_replace_in_note` | Search-replace inside a single note, scoped to the body by default. Literal or regex matching with whole-word, whitespace-flexible, and case-sensitivity options; supports capture-group replacement. |
39
39
  | `obsidian_manage_frontmatter` | Atomic `get` / `set` / `delete` on a single frontmatter key. |
40
40
  | `obsidian_manage_tags` | Add, remove, or list tags. Defaults to the frontmatter `tags:` array; `location: 'inline'` or `'both'` opts into mutating the note body. |
41
41
  | `obsidian_delete_note` | Permanently delete a note. Always asks the user to confirm first — the call is answered with a confirmation request and retried with the answer. |
@@ -60,7 +60,7 @@ Pair the document-map projection with `obsidian_patch_note` to discover edit tar
60
60
  Up to three search modes selected by `mode`:
61
61
 
62
62
  - `text` — substring match with surrounding context windows. `contextLength` controls characters of context per side of each match (default 100; bump it for more context per hit). Optional `pathPrefix` filter (text mode only — passing `pathPrefix` in any other mode is rejected with `path_prefix_invalid_mode`).
63
- - `jsonlogic` — JSONLogic tree evaluated against `path`, `content`, `frontmatter.<key>`, `tags`, and `stat.{ctime,mtime,size}`; custom `glob` and `regexp` operators
63
+ - `jsonlogic` — JSONLogic tree evaluated against `path`, `content`, `frontmatter.<key>`, `tags`, and `stat.{ctime,mtime,size}`; custom `glob` and `regexp` operators, both taking `[PATTERN, VALUE]` — pattern first, then the field reference: `{"glob": ["Projects/*.md", {"var": "path"}]}`. The reverse order compiles the note's own field as the pattern: `glob` then matches nothing, and `regexp` fails outright on whatever the field parses as. This is also how backlinks are expressed, since there is no dedicated tool or upstream endpoint for them: `{"regexp": ["\\[\\[Target Note(\\||#|\\]\\])", {"var": "content"}]}` returns every note whose body wikilinks `Target Note`.
64
64
  - `omnisearch` — BM25-ranked search via the community [Omnisearch](https://github.com/scambier/obsidian-omnisearch) plugin. Supports quoted phrases, `-exclusion`, `path:` / `ext:` filters, typo tolerance, and PDF + OCR coverage (via [Text Extractor](https://github.com/scambier/obsidian-text-extractor)). Only present in the mode enum when the plugin's HTTP server is reachable at startup; the upstream hard-caps results at 50 — narrow the query to surface more (the response carries `truncated: true` when the cap was likely hit).
65
65
 
66
66
  Results paginate via opaque cursors per the [MCP 2025-11-25 spec](https://modelcontextprotocol.io/specification/2025-11-25/utils/pagination): omit `cursor` for the first page, then pass `nextCursor` from the prior response. Every result carries `totalCount` (post-path-policy, pre-pagination); `nextCursor` is omitted on the last page. Text-mode hits are additionally clipped per file at `maxMatchesPerHit` (default 10) so a single match-heavy note can't blow the response budget — clipped hits carry `truncated: true` and `totalMatches`.
@@ -106,7 +106,15 @@ Use `obsidian_get_note` with `format: "document-map"` to discover what targets e
106
106
 
107
107
  ### `obsidian_replace_in_note`
108
108
 
109
- Body-wide search-replace for edits that don't fit `obsidian_patch_note`'s structural targets. The note is fetched, replacements are applied sequentially (each sees the previous output), and the result is written back in a single `PUT`.
109
+ Search-replace for edits that don't fit `obsidian_patch_note`'s structural targets. The note is fetched, replacements are applied sequentially (each sees the previous output), and the result is written back in a single `PUT`.
110
+
111
+ `scope` selects what the replacements run over:
112
+
113
+ - `body` (default) — the text after the YAML frontmatter block. The block is re-attached from the original bytes, so it comes back byte-identical.
114
+ - `frontmatter` — only the YAML between the `---` fences. The fences themselves are never matched.
115
+ - `both` — each replacement runs over the frontmatter and then the body; `perReplacement[]` reports `bodyCount` and `frontmatterCount` separately.
116
+
117
+ With the frontmatter in scope, the rewritten YAML is re-parsed before anything is written: if it no longer parses as a mapping of properties, the call fails with `frontmatter_invalid` and the note keeps its original bytes. That check catches YAML that breaks — an unquoted `:` in a scalar, a list marker rewritten into an alias, a stray quote. It cannot catch an edit that stays well-formed while meaning something else, such as a substring collision that renames a key or a replacement that drops a scalar's quotes and changes its type. Prefer `obsidian_manage_frontmatter` for typed edits to a single property.
110
118
 
111
119
  Per-replacement options:
112
120
 
@@ -114,7 +122,7 @@ Per-replacement options:
114
122
  - `caseSensitive` — when `false`, match case-insensitively
115
123
  - `wholeWord` — wrap the pattern in `\b…\b`; works in both literal and regex modes
116
124
  - `flexibleWhitespace` — substitute any run of whitespace in `search` with `\s+`. Literal mode only — has no effect when `useRegex: true` (express it directly).
117
- - `replaceAll` — when `false`, only the first match is replaced
125
+ - `replaceAll` — when `false`, only the first match is replaced. Under `scope: 'both'` that one substitution goes to the frontmatter when it matches there, and to the body otherwise.
118
126
 
119
127
  Literal mode preserves `$1` / `$&` in the replacement verbatim — only `useRegex: true` expands capture-group references.
120
128
 
@@ -130,6 +138,8 @@ Add, remove, or list tags on a note. Operates on one of two representations, def
130
138
 
131
139
  `add` ensures the tag is present in the requested location(s); `remove` strips it; `list` ignores the input `tags` array. Inline `#tag` occurrences inside fenced code blocks are intentionally left alone.
132
140
 
141
+ Inline mode reads and writes the note body only — a `#` inside a YAML scalar is frontmatter, so it is neither listed as an inline tag nor rewritten by a removal. Removing an inline tag takes exactly one adjacent horizontal space with it — the one before the tag, or the one after when no space precedes it; every other byte survives, including nested list indentation, four-space indented code blocks, trailing two-space hard line breaks, and table cell padding.
142
+
133
143
  ---
134
144
 
135
145
  ### `obsidian_delete_note`
@@ -181,7 +191,7 @@ The startup banner logs the active scope so operators can verify their config at
181
191
  | Resource | `obsidian://tags` | All tags found across the vault, with usage counts. |
182
192
  | Resource | `obsidian://status` | Server reachability, auth status, plugin/Obsidian version info, and the plugin manifest. |
183
193
 
184
- All resource data is also reachable via tools — `obsidian_get_note` for `obsidian://vault/{+path}`, `obsidian_list_tags` for `obsidian://tags`. Resources exist for clients that prefer attaching a specific note or vault snapshot to a conversation.
194
+ All resource data is also reachable via tools — `obsidian_get_note` for `obsidian://vault/{+path}`, `obsidian_list_tags` for `obsidian://tags`. Resources exist for clients that prefer attaching a specific note or vault snapshot to a conversation. The tag pair is not a mirror: `obsidian://tags` keeps snapshot semantics and returns the upstream payload whole and unsorted, while `obsidian_list_tags` orders by count and caps.
185
195
 
186
196
  ## Features
187
197
 
@@ -0,0 +1,25 @@
1
+ ---
2
+ summary: "obsidian_list_tags now sorts by count and caps at limit by default; JSONLogic glob/regexp docs and text-search offsets are fixed; upstream error text no longer reaches clients."
3
+ breaking: true
4
+ security: true
5
+ ---
6
+
7
+ # 3.4.0 — 2026-08-22
8
+
9
+ ## Changed
10
+
11
+ - **`obsidian_list_tags` now orders by usage count (descending, ties by name) and caps the response at `limit`** (default 200, max 10000), disclosing how many tags were withheld ([#110](https://github.com/cyanheads/obsidian-mcp-server/issues/110)). A zero-argument call previously returned the vault's entire tag list in upstream order; existing callers now see at most 200, most-used first. New `minCount` drops the single-use tail. `obsidian://tags` is unchanged — still an unsorted, uncapped snapshot.
12
+
13
+ ### Dependency bumps
14
+
15
+ - `vitest` ^4.1.10 → ^4.1.11
16
+
17
+ ## Fixed
18
+
19
+ - **`obsidian_search_notes`'s `glob`/`regexp` JSONLogic operators were documented backwards everywhere they appear** — the `logic` field, both `mode` descriptions, and the `logic_required` recovery hint now state the actual `[PATTERN, VALUE]` order, and a rejected tree classifies as `logic_invalid` instead of a generic 400 ([#113](https://github.com/cyanheads/obsidian-mcp-server/issues/113)).
20
+ - **Text-search hit offsets were documented and computed as `context`-relative but are actually relative to the matched subject** (the note body, or its basename for a filename match) — new `contextStart`/`contextEnd` index `context` directly, and `format()` now fences rendered note excerpts, Omnisearch excerpts, and JSONLogic results wide enough that backticks in vault text can't break out of the code block ([#114](https://github.com/cyanheads/obsidian-mcp-server/issues/114)).
21
+ - **An oversized `contextLength` on a broad `obsidian_search_notes` query failed with an untyped internal error** — it now throws a typed `context_length_too_large` `ValidationError` naming `contextLength` and the query itself as the two levers to narrow ([#115](https://github.com/cyanheads/obsidian-mcp-server/issues/115)).
22
+
23
+ ## Security
24
+
25
+ - **Upstream error text no longer reaches clients.** `ObsidianService#throwForStatus` dropped `data.upstream` and `data.body` from every branch — Local REST API error bodies can carry vault note paths, note body content, and absolute filesystem paths regardless of `OBSIDIAN_READ_PATHS` scoping. Errors now carry only server-authored messages plus `path`, `status`, and the calling tool's contract `reason` ([#116](https://github.com/cyanheads/obsidian-mcp-server/issues/116), [#104](https://github.com/cyanheads/obsidian-mcp-server/issues/104)).
@@ -0,0 +1,16 @@
1
+ ---
2
+ summary: "obsidian_manage_tags inline removal and obsidian_replace_in_note no longer corrupt notes — whitespace collapse and frontmatter overwrite are both fixed."
3
+ breaking: true
4
+ security: false
5
+ ---
6
+
7
+ # 3.5.0 — 2026-08-22
8
+
9
+ ## Changed
10
+
11
+ - **`obsidian_replace_in_note` gains a `scope` input (`body` (default) | `frontmatter` | `both`)** — replacements now run over the note body only by default, with the YAML frontmatter block spliced off and re-attached verbatim; a replacement whose only match was in the frontmatter now reports zero matches and writes nothing, silently rather than as an error. Anchored patterns (`^`/`$`) bind to the body or the YAML rather than the whole file, so a pattern spanning the fence boundary is unmatchable under every scope. `perReplacement[]` reports `bodyCount`/`frontmatterCount` alongside `count` ([#112](https://github.com/cyanheads/obsidian-mcp-server/issues/112)).
12
+ - **A `scope` of `"frontmatter"` or `"both"` re-parses the rewritten YAML before writing** — a result that no longer parses as a mapping fails the whole call with the new `frontmatter_invalid` (`ValidationError`) and nothing is written; the `---` fences themselves are never matched under any scope ([#112](https://github.com/cyanheads/obsidian-mcp-server/issues/112)).
13
+
14
+ ## Fixed
15
+
16
+ - **`obsidian_manage_tags` inline tag removal no longer collapses whitespace across the whole note** — the frontmatter block is spliced off and re-attached byte-identical, the unanchored whitespace-collapse regexes are gone, and a removal takes exactly one adjacent space with the tag; nested list indentation, code-block indentation, hard line breaks, and table padding all survive untouched. Removal also now clears every repeated occurrence of the same tag in one call, where a single pass previously missed later occurrences ([#111](https://github.com/cyanheads/obsidian-mcp-server/issues/111)).
@@ -1,6 +1,12 @@
1
1
  /**
2
- * @fileoverview obsidian://tags — vault tag listing with usage counts.
3
- * Mirrors `obsidian_list_tags` for clients that prefer attaching resources.
2
+ * @fileoverview obsidian://tags — vault tag listing with usage counts, for
3
+ * clients that prefer attaching resources.
4
+ *
5
+ * Not a mirror of `obsidian_list_tags`: this is a snapshot of the upstream
6
+ * `/tags/` payload, returned whole and in upstream order. The tool shapes the
7
+ * same payload for an LLM caller — count-descending, capped, filterable — and
8
+ * that shaping deliberately lives in the tool handler rather than the shared
9
+ * `ObsidianService.listTags` both call.
4
10
  * @module mcp-server/resources/definitions/obsidian-tags.resource
5
11
  */
6
12
  import { z } from '@cyanheads/mcp-ts-core';
@@ -1 +1 @@
1
- {"version":3,"file":"obsidian-tags.resource.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/resources/definitions/obsidian-tags.resource.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAY,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAGrD,eAAO,MAAM,YAAY;;;;;6BAyBvB,CAAC"}
1
+ {"version":3,"file":"obsidian-tags.resource.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/resources/definitions/obsidian-tags.resource.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAY,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAGrD,eAAO,MAAM,YAAY;;;;;6BAyBvB,CAAC"}
@@ -1,13 +1,19 @@
1
1
  /**
2
- * @fileoverview obsidian://tags — vault tag listing with usage counts.
3
- * Mirrors `obsidian_list_tags` for clients that prefer attaching resources.
2
+ * @fileoverview obsidian://tags — vault tag listing with usage counts, for
3
+ * clients that prefer attaching resources.
4
+ *
5
+ * Not a mirror of `obsidian_list_tags`: this is a snapshot of the upstream
6
+ * `/tags/` payload, returned whole and in upstream order. The tool shapes the
7
+ * same payload for an LLM caller — count-descending, capped, filterable — and
8
+ * that shaping deliberately lives in the tool handler rather than the shared
9
+ * `ObsidianService.listTags` both call.
4
10
  * @module mcp-server/resources/definitions/obsidian-tags.resource
5
11
  */
6
12
  import { resource, z } from '@cyanheads/mcp-ts-core';
7
13
  import { getObsidianService } from '../../../services/obsidian/obsidian-service.js';
8
14
  export const obsidianTags = resource('obsidian://tags', {
9
15
  name: 'obsidian-tags',
10
- description: 'All tags found in the Obsidian vault, with usage counts. Includes hierarchical parents (e.g. `work` for `work/tasks`).',
16
+ description: 'All tags found in the Obsidian vault, with usage counts, in upstream order and uncapped — a full snapshot. Includes hierarchical parents (e.g. `work` for `work/tasks`). Use the `obsidian_list_tags` tool for a count-ranked, capped, filterable view.',
11
17
  mimeType: 'application/json',
12
18
  params: z.object({}),
13
19
  output: z.object({
@@ -18,7 +24,7 @@ export const obsidianTags = resource('obsidian://tags', {
18
24
  count: z.number().describe('Usage count across the vault.'),
19
25
  })
20
26
  .describe('A tag with its usage count.'))
21
- .describe('All tags in the vault.'),
27
+ .describe('Every tag in the vault, in upstream-provided order.'),
22
28
  }),
23
29
  auth: ['resource:obsidian-tags:read'],
24
30
  async handler(_params, ctx) {
@@ -1 +1 @@
1
- {"version":3,"file":"obsidian-tags.resource.js","sourceRoot":"","sources":["../../../../src/mcp-server/resources/definitions/obsidian-tags.resource.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAE7E,MAAM,CAAC,MAAM,YAAY,GAAG,QAAQ,CAAC,iBAAiB,EAAE;IACtD,IAAI,EAAE,eAAe;IACrB,WAAW,EACT,wHAAwH;IAC1H,QAAQ,EAAE,kBAAkB;IAC5B,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC;IACpB,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC;QACf,IAAI,EAAE,CAAC;aACJ,KAAK,CACJ,CAAC;aACE,MAAM,CAAC;YACN,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,mCAAmC,CAAC;YAC9D,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,+BAA+B,CAAC;SAC5D,CAAC;aACD,QAAQ,CAAC,6BAA6B,CAAC,CAC3C;aACA,QAAQ,CAAC,wBAAwB,CAAC;KACtC,CAAC;IACF,IAAI,EAAE,CAAC,6BAA6B,CAAC;IAErC,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG;QACxB,MAAM,GAAG,GAAG,kBAAkB,EAAE,CAAC;QACjC,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QACrC,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC;IACvE,CAAC;CACF,CAAC,CAAC"}
1
+ {"version":3,"file":"obsidian-tags.resource.js","sourceRoot":"","sources":["../../../../src/mcp-server/resources/definitions/obsidian-tags.resource.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAE7E,MAAM,CAAC,MAAM,YAAY,GAAG,QAAQ,CAAC,iBAAiB,EAAE;IACtD,IAAI,EAAE,eAAe;IACrB,WAAW,EACT,yPAAyP;IAC3P,QAAQ,EAAE,kBAAkB;IAC5B,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC;IACpB,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC;QACf,IAAI,EAAE,CAAC;aACJ,KAAK,CACJ,CAAC;aACE,MAAM,CAAC;YACN,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,mCAAmC,CAAC;YAC9D,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,+BAA+B,CAAC;SAC5D,CAAC;aACD,QAAQ,CAAC,6BAA6B,CAAC,CAC3C;aACA,QAAQ,CAAC,qDAAqD,CAAC;KACnE,CAAC;IACF,IAAI,EAAE,CAAC,6BAA6B,CAAC;IAErC,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG;QACxB,MAAM,GAAG,GAAG,kBAAkB,EAAE,CAAC;QACjC,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QACrC,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC;IACvE,CAAC;CACF,CAAC,CAAC"}
@@ -202,14 +202,18 @@ export declare const readToolDefinitions: (import("@cyanheads/mcp-ts-core").Tool
202
202
  readonly notice: import("zod").ZodOptional<import("zod").ZodString>;
203
203
  }> | import("@cyanheads/mcp-ts-core").ToolDefinition<import("zod").ZodObject<{
204
204
  nameRegex: import("zod").ZodOptional<import("zod").ZodString>;
205
+ minCount: import("zod").ZodOptional<import("zod").ZodNumber>;
206
+ limit: import("zod").ZodDefault<import("zod").ZodNumber>;
205
207
  }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
206
208
  tags: import("zod").ZodArray<import("zod").ZodObject<{
207
209
  name: import("zod").ZodString;
208
210
  count: import("zod").ZodNumber;
209
211
  }, import("zod/v4/core").$strip>>;
210
- appliedFilters: import("zod").ZodOptional<import("zod").ZodObject<{
212
+ appliedFilters: import("zod").ZodObject<{
211
213
  nameRegex: import("zod").ZodOptional<import("zod").ZodString>;
212
- }, import("zod/v4/core").$strip>>;
214
+ minCount: import("zod").ZodOptional<import("zod").ZodNumber>;
215
+ limit: import("zod").ZodNumber;
216
+ }, import("zod/v4/core").$strip>;
213
217
  }, import("zod/v4/core").$strip>, readonly [{
214
218
  readonly reason: "regex_invalid";
215
219
  readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.ValidationError;
@@ -222,6 +226,9 @@ export declare const readToolDefinitions: (import("@cyanheads/mcp-ts-core").Tool
222
226
  readonly recovery: "Avoid nested quantifiers like `(a+)+` or `(.*)*`. Use a simpler pattern (e.g. `^mcp/.*`), or omit nameRegex to disable filtering.";
223
227
  }], {
224
228
  readonly notice: import("zod").ZodOptional<import("zod").ZodString>;
229
+ readonly truncated: import("zod").ZodOptional<import("zod").ZodBoolean>;
230
+ readonly shown: import("zod").ZodOptional<import("zod").ZodNumber>;
231
+ readonly cap: import("zod").ZodOptional<import("zod").ZodNumber>;
225
232
  }> | import("@cyanheads/mcp-ts-core").ToolDefinition<import("zod").ZodObject<{
226
233
  path: import("zod").ZodString;
227
234
  failIfMissing: import("zod").ZodDefault<import("zod").ZodBoolean>;
@@ -557,6 +564,11 @@ export declare const writeToolDefinitions: (import("@cyanheads/mcp-ts-core").Too
557
564
  }>;
558
565
  date: import("zod").ZodOptional<import("zod").ZodString>;
559
566
  }, import("zod/v4/core").$strip>], "type">;
567
+ scope: import("zod").ZodDefault<import("zod").ZodEnum<{
568
+ body: "body";
569
+ both: "both";
570
+ frontmatter: "frontmatter";
571
+ }>>;
560
572
  replacements: import("zod").ZodArray<import("zod").ZodObject<{
561
573
  search: import("zod").ZodString;
562
574
  replace: import("zod").ZodString;
@@ -572,6 +584,8 @@ export declare const writeToolDefinitions: (import("@cyanheads/mcp-ts-core").Too
572
584
  perReplacement: import("zod").ZodArray<import("zod").ZodObject<{
573
585
  search: import("zod").ZodString;
574
586
  count: import("zod").ZodNumber;
587
+ bodyCount: import("zod").ZodNumber;
588
+ frontmatterCount: import("zod").ZodNumber;
575
589
  }, import("zod/v4/core").$strip>>;
576
590
  previousSizeInBytes: import("zod").ZodNumber;
577
591
  currentSizeInBytes: import("zod").ZodNumber;
@@ -590,6 +604,11 @@ export declare const writeToolDefinitions: (import("@cyanheads/mcp-ts-core").Too
590
604
  readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.ValidationError;
591
605
  readonly when: "A `useRegex: true` replacement supplied a `search` pattern that is well-formed but exceeds the 1024-character limit or contains nested quantifiers known to cause catastrophic backtracking against the note body.";
592
606
  readonly recovery: "Avoid nested quantifiers like `(a+)+` or `(.*)*`. Use a simpler pattern, or set useRegex to false to match `search` as a literal string.";
607
+ }, {
608
+ readonly reason: "frontmatter_invalid";
609
+ readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.ValidationError;
610
+ readonly when: "A `scope` of \"frontmatter\" or \"both\" produced YAML that no longer parses as a mapping of properties. Nothing is written — the note keeps its original bytes. The check reads the rewritten YAML only, so a replacement that renames a key or changes a scalar's type while still parsing is not caught by it.";
611
+ readonly recovery: "Narrow the search so it cannot match inside the YAML, or leave scope at \"body\" and edit the property with obsidian_manage_frontmatter.";
593
612
  }, {
594
613
  readonly reason: "note_missing";
595
614
  readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.NotFound;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAgBH,OAAO,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AAEvE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAK/B,CAAC;AAEF,kFAAkF;AAClF,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAQhC,CAAC;AAEF,mHAAmH;AACnH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAiD,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAgBH,OAAO,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AAEvE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAK/B,CAAC;AAEF,kFAAkF;AAClF,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAQhC,CAAC;AAEF,mHAAmH;AACnH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAiD,CAAC"}
@@ -1,22 +1,28 @@
1
1
  /**
2
- * @fileoverview obsidian_list_tags — all tags found in the vault, with usage counts.
3
- * Mirrors the plugin's `/tags/` endpoint; counts are upstream-provided. Optional
4
- * `nameRegex` post-filters the upstream payload before returning to keep the
5
- * LLM-bound response small on vaults with long-tail tag noise.
2
+ * @fileoverview obsidian_list_tags — the most-used tags in the vault, with usage counts.
3
+ * Wraps the plugin's `/tags/` endpoint, then shapes it for an LLM caller: `nameRegex`
4
+ * and `minCount` narrow the candidate set, the survivors are ordered by usage, and
5
+ * `limit` keeps the exploratory zero-argument call bounded on a vault with a long
6
+ * single-use tail. The `obsidian://tags` resource wraps the same endpoint but keeps
7
+ * snapshot semantics — unsorted, uncapped — so the two surfaces differ by design.
6
8
  * @module mcp-server/tools/definitions/obsidian-list-tags.tool
7
9
  */
8
10
  import { z } from '@cyanheads/mcp-ts-core';
9
11
  import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
10
12
  export declare const obsidianListTags: import("@cyanheads/mcp-ts-core").ToolDefinition<z.ZodObject<{
11
13
  nameRegex: z.ZodOptional<z.ZodString>;
14
+ minCount: z.ZodOptional<z.ZodNumber>;
15
+ limit: z.ZodDefault<z.ZodNumber>;
12
16
  }, z.core.$strip>, z.ZodObject<{
13
17
  tags: z.ZodArray<z.ZodObject<{
14
18
  name: z.ZodString;
15
19
  count: z.ZodNumber;
16
20
  }, z.core.$strip>>;
17
- appliedFilters: z.ZodOptional<z.ZodObject<{
21
+ appliedFilters: z.ZodObject<{
18
22
  nameRegex: z.ZodOptional<z.ZodString>;
19
- }, z.core.$strip>>;
23
+ minCount: z.ZodOptional<z.ZodNumber>;
24
+ limit: z.ZodNumber;
25
+ }, z.core.$strip>;
20
26
  }, z.core.$strip>, readonly [{
21
27
  readonly reason: "regex_invalid";
22
28
  readonly code: JsonRpcErrorCode.ValidationError;
@@ -29,5 +35,8 @@ export declare const obsidianListTags: import("@cyanheads/mcp-ts-core").ToolDefi
29
35
  readonly recovery: "Avoid nested quantifiers like `(a+)+` or `(.*)*`. Use a simpler pattern (e.g. `^mcp/.*`), or omit nameRegex to disable filtering.";
30
36
  }], {
31
37
  readonly notice: z.ZodOptional<z.ZodString>;
38
+ readonly truncated: z.ZodOptional<z.ZodBoolean>;
39
+ readonly shown: z.ZodOptional<z.ZodNumber>;
40
+ readonly cap: z.ZodOptional<z.ZodNumber>;
32
41
  }>;
33
42
  //# sourceMappingURL=obsidian-list-tags.tool.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"obsidian-list-tags.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/obsidian-list-tags.tool.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAIjE,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;EAuG3B,CAAC"}
1
+ {"version":3,"file":"obsidian-list-tags.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/obsidian-list-tags.tool.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAajE,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgK3B,CAAC"}
@@ -1,22 +1,45 @@
1
1
  /**
2
- * @fileoverview obsidian_list_tags — all tags found in the vault, with usage counts.
3
- * Mirrors the plugin's `/tags/` endpoint; counts are upstream-provided. Optional
4
- * `nameRegex` post-filters the upstream payload before returning to keep the
5
- * LLM-bound response small on vaults with long-tail tag noise.
2
+ * @fileoverview obsidian_list_tags — the most-used tags in the vault, with usage counts.
3
+ * Wraps the plugin's `/tags/` endpoint, then shapes it for an LLM caller: `nameRegex`
4
+ * and `minCount` narrow the candidate set, the survivors are ordered by usage, and
5
+ * `limit` keeps the exploratory zero-argument call bounded on a vault with a long
6
+ * single-use tail. The `obsidian://tags` resource wraps the same endpoint but keeps
7
+ * snapshot semantics — unsorted, uncapped — so the two surfaces differ by design.
6
8
  * @module mcp-server/tools/definitions/obsidian-list-tags.tool
7
9
  */
8
10
  import { tool, z } from '@cyanheads/mcp-ts-core';
9
11
  import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
10
12
  import { getObsidianService } from '../../../services/obsidian/obsidian-service.js';
11
13
  import { nameRegexSafetyIssue } from './_shared/regex-safety.js';
14
+ /** Bounded so the first exploratory call cannot return the whole tag distribution. */
15
+ const DEFAULT_TAG_LIMIT = 200;
16
+ /**
17
+ * Set far above any plausible vault's tag cardinality so "give me everything" is
18
+ * reachable by asking for a number, with no `0`-means-uncapped sentinel — to a
19
+ * model reading the schema, `0` reads as "return none".
20
+ */
21
+ const MAX_TAG_LIMIT = 10_000;
12
22
  export const obsidianListTags = tool('obsidian_list_tags', {
13
- description: 'List every tag found across the vault, with usage counts. Includes hierarchical parents — `work/tasks` contributes to both `work` and `work/tasks`. Filter to a subset with the optional `nameRegex`. To find notes by tag, use `obsidian_search_notes` in jsonlogic mode (e.g. `{"in": ["work", {"var": "tags"}]}`).',
23
+ description: `List the vault's tags with usage counts, ordered by count descending and capped at \`limit\` (default ${DEFAULT_TAG_LIMIT}) — the response says so when it withheld any. Includes hierarchical parents: \`work/tasks\` contributes to both \`work\` and \`work/tasks\`. Narrow with \`nameRegex\` for a known prefix or \`minCount\` to drop the single-use tail. To find notes by tag, use \`obsidian_search_notes\` in jsonlogic mode (e.g. \`{"in": ["work", {"var": "tags"}]}\`).`,
14
24
  annotations: { readOnlyHint: true, idempotentHint: true },
15
25
  input: z.object({
16
26
  nameRegex: z
17
27
  .string()
18
28
  .optional()
19
29
  .describe('Optional ECMAScript regex (no flags, ≤256 chars, no nested quantifiers like `(a+)+`) matched against the bare tag name (no leading `#`). Hierarchical tags like `work/tasks` are matched as the full string. Use character classes (`[Mm]cp`) for case-insensitivity.'),
30
+ minCount: z
31
+ .number()
32
+ .int()
33
+ .min(0)
34
+ .optional()
35
+ .describe('Keep only tags used at least this many times. Omit (or pass 0) for no filter. On a vault with a long single-use tail this drops it in one argument.'),
36
+ limit: z
37
+ .number()
38
+ .int()
39
+ .min(1)
40
+ .max(MAX_TAG_LIMIT)
41
+ .default(DEFAULT_TAG_LIMIT)
42
+ .describe(`Maximum tags to return, applied after \`nameRegex\` and \`minCount\` and after ordering by count descending, so the cap keeps the most-used. Max ${MAX_TAG_LIMIT} — high enough to return any real vault whole. When the cap bites, the response reports \`truncated\`, \`shown\`, and \`cap\`.`),
20
43
  }),
21
44
  output: z.object({
22
45
  tags: z
@@ -26,19 +49,29 @@ export const obsidianListTags = tool('obsidian_list_tags', {
26
49
  count: z.number().describe('Usage count across the vault.'),
27
50
  })
28
51
  .describe('A tag with its usage count.'))
29
- .describe('All tags in the vault, in upstream-provided order.'),
52
+ .describe('Matching tags ordered by `count` descending, ties broken by name ascending, truncated to `appliedFilters.limit`.'),
30
53
  appliedFilters: z
31
54
  .object({
32
55
  nameRegex: z.string().optional().describe('nameRegex filter applied to this listing.'),
56
+ minCount: z
57
+ .number()
58
+ .optional()
59
+ .describe('minCount filter applied to this listing. Absent when none was supplied.'),
60
+ limit: z.number().describe('Cap applied to this listing.'),
33
61
  })
34
- .optional()
35
- .describe('Active filters that produced this listing. Absent when no filter was applied.'),
62
+ .describe('Filters and cap that produced this listing.'),
36
63
  }),
37
64
  enrichment: {
38
65
  notice: z
39
66
  .string()
40
67
  .optional()
41
- .describe('Guidance when no tags matched the applied filter, or the vault has no tags.'),
68
+ .describe('Guidance when no tags matched the applied filters, when the vault has no tags, or — since `ctx.enrich.truncated` routes its guidance here — when the cap withheld some.'),
69
+ truncated: z
70
+ .boolean()
71
+ .optional()
72
+ .describe('True when more tags matched the filters than `limit` allowed through.'),
73
+ shown: z.number().optional().describe('Number of tags returned.'),
74
+ cap: z.number().optional().describe('The `limit` that was applied.'),
42
75
  },
43
76
  auth: ['tool:obsidian_list_tags:read'],
44
77
  errors: [
@@ -73,21 +106,46 @@ export const obsidianListTags = tool('obsidian_list_tags', {
73
106
  }
74
107
  }
75
108
  const svc = getObsidianService();
76
- const tags = await svc.listTags(ctx);
77
- const filtered = regex ? tags.filter((t) => regex.test(t.name)) : tags;
78
- if (filtered.length === 0) {
79
- const filterNote = input.nameRegex ? ` matching \`${input.nameRegex}\`` : '';
80
- ctx.enrich.notice(`No tags found${filterNote}. ${input.nameRegex ? 'Try a broader nameRegex or omit it to list all tags.' : 'The vault may have no tagged notes.'}`);
81
- }
82
- return {
83
- tags: filtered.map((t) => ({ name: t.name, count: t.count })),
84
- ...(input.nameRegex ? { appliedFilters: { nameRegex: input.nameRegex } } : {}),
109
+ const minCount = input.minCount ?? 0;
110
+ /**
111
+ * Order is load-bearing: narrow, then rank, then cap. Truncation is
112
+ * measured against the post-filter candidate count reporting the raw
113
+ * vault total would describe tags `minCount` had already excluded as
114
+ * things the cap withheld.
115
+ */
116
+ const candidates = (await svc.listTags(ctx))
117
+ .filter((t) => (regex ? regex.test(t.name) : true))
118
+ .filter((t) => t.count >= minCount)
119
+ .sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
120
+ const tags = candidates.slice(0, input.limit).map((t) => ({ name: t.name, count: t.count }));
121
+ const appliedFilters = {
122
+ ...(input.nameRegex ? { nameRegex: input.nameRegex } : {}),
123
+ ...(minCount > 0 ? { minCount } : {}),
124
+ limit: input.limit,
85
125
  };
126
+ if (candidates.length === 0) {
127
+ /**
128
+ * Keyed on the narrowing filters only. `limit` is always present and
129
+ * never causes an empty result, so folding it in here would make the
130
+ * "this vault has no tags at all" case unreachable.
131
+ */
132
+ const narrowing = describeFilters({ ...appliedFilters, limit: undefined });
133
+ ctx.enrich.notice(narrowing
134
+ ? `No tags matched ${narrowing}. Loosen or drop the filters to widen the listing.`
135
+ : 'No tags found. The vault may have no tagged notes.');
136
+ }
137
+ else if (candidates.length > input.limit) {
138
+ ctx.enrich.truncated({
139
+ shown: tags.length,
140
+ cap: input.limit,
141
+ guidance: `Showing the ${tags.length} most-used of ${candidates.length} matching tags. Raise \`limit\` (max ${MAX_TAG_LIMIT}) for more, or narrow with \`nameRegex\` / \`minCount\`.`,
142
+ });
143
+ }
144
+ return { tags, appliedFilters };
86
145
  },
87
146
  format: (result) => {
88
- const activeRegex = result.appliedFilters?.nameRegex;
89
- const filterSuffix = activeRegex ? ` · nameRegex=\`${activeRegex}\`` : '';
90
- const lines = [`**${result.tags.length} tags**${filterSuffix}`];
147
+ const suffix = describeFilters(result.appliedFilters);
148
+ const lines = [`**${result.tags.length} tags**${suffix ? ` · ${suffix}` : ''}`];
91
149
  if (result.tags.length > 0) {
92
150
  lines.push('');
93
151
  for (const t of result.tags)
@@ -96,4 +154,15 @@ export const obsidianListTags = tool('obsidian_list_tags', {
96
154
  return [{ type: 'text', text: lines.join('\n') }];
97
155
  },
98
156
  });
157
+ /** Human-readable echo of the filters and cap behind a listing. */
158
+ function describeFilters(filters) {
159
+ const parts = [];
160
+ if (filters.nameRegex)
161
+ parts.push(`nameRegex=\`${filters.nameRegex}\``);
162
+ if (filters.minCount !== undefined)
163
+ parts.push(`minCount=${filters.minCount}`);
164
+ if (filters.limit !== undefined)
165
+ parts.push(`limit=${filters.limit}`);
166
+ return parts.join(' · ');
167
+ }
99
168
  //# sourceMappingURL=obsidian-list-tags.tool.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"obsidian-list-tags.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/obsidian-list-tags.tool.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"obsidian-list-tags.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/obsidian-list-tags.tool.ts"],"names":[],"mappings":"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"}