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.
- package/AGENTS.md +7 -5
- package/CLAUDE.md +7 -5
- package/README.md +17 -7
- package/changelog/3.4.x/3.4.0.md +25 -0
- package/changelog/3.5.x/3.5.0.md +16 -0
- package/dist/mcp-server/resources/definitions/obsidian-tags.resource.d.ts +8 -2
- package/dist/mcp-server/resources/definitions/obsidian-tags.resource.d.ts.map +1 -1
- package/dist/mcp-server/resources/definitions/obsidian-tags.resource.js +10 -4
- package/dist/mcp-server/resources/definitions/obsidian-tags.resource.js.map +1 -1
- package/dist/mcp-server/tools/definitions/index.d.ts +21 -2
- package/dist/mcp-server/tools/definitions/index.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.d.ts +15 -6
- package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.js +90 -21
- package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.d.ts +14 -1
- package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.js +85 -23
- package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.d.ts +30 -2
- package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.js +57 -12
- package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.js.map +1 -1
- package/dist/services/obsidian/frontmatter-ops.d.ts +42 -1
- package/dist/services/obsidian/frontmatter-ops.d.ts.map +1 -1
- package/dist/services/obsidian/frontmatter-ops.js +98 -21
- package/dist/services/obsidian/frontmatter-ops.js.map +1 -1
- package/dist/services/obsidian/obsidian-service.d.ts +7 -0
- package/dist/services/obsidian/obsidian-service.d.ts.map +1 -1
- package/dist/services/obsidian/obsidian-service.js +205 -48
- package/dist/services/obsidian/obsidian-service.js.map +1 -1
- package/dist/services/obsidian/types.d.ts +8 -0
- package/dist/services/obsidian/types.d.ts.map +1 -1
- package/manifest.json +1 -1
- package/package.json +2 -2
- 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.
|
|
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('
|
|
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)
|
|
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.
|
|
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('
|
|
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)
|
|
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
|
-
[](./CHANGELOG.md) [](./LICENSE) [](https://github.com/users/cyanheads/packages/container/package/obsidian-mcp-server) [](https://modelcontextprotocol.io/) [](https://www.npmjs.com/package/obsidian-mcp-server) [](https://www.typescriptlang.org/) [](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
|
|
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` |
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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('
|
|
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
|
|
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").
|
|
212
|
+
appliedFilters: import("zod").ZodObject<{
|
|
211
213
|
nameRegex: import("zod").ZodOptional<import("zod").ZodString>;
|
|
212
|
-
|
|
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
|
|
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 —
|
|
3
|
-
*
|
|
4
|
-
* `
|
|
5
|
-
*
|
|
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.
|
|
21
|
+
appliedFilters: z.ZodObject<{
|
|
18
22
|
nameRegex: z.ZodOptional<z.ZodString>;
|
|
19
|
-
|
|
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
|
|
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 —
|
|
3
|
-
*
|
|
4
|
-
* `
|
|
5
|
-
*
|
|
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:
|
|
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('
|
|
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
|
-
.
|
|
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
|
|
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
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
89
|
-
const
|
|
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":"AAAA
|
|
1
|
+
{"version":3,"file":"obsidian-list-tags.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/obsidian-list-tags.tool.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAC7E,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAEjE,sFAAsF;AACtF,MAAM,iBAAiB,GAAG,GAAG,CAAC;AAC9B;;;;GAIG;AACH,MAAM,aAAa,GAAG,MAAM,CAAC;AAE7B,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC,oBAAoB,EAAE;IACzD,WAAW,EAAE,yGAAyG,iBAAiB,6VAA6V;IACpe,WAAW,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE;IACzD,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC;QACd,SAAS,EAAE,CAAC;aACT,MAAM,EAAE;aACR,QAAQ,EAAE;aACV,QAAQ,CACP,uQAAuQ,CACxQ;QACH,QAAQ,EAAE,CAAC;aACR,MAAM,EAAE;aACR,GAAG,EAAE;aACL,GAAG,CAAC,CAAC,CAAC;aACN,QAAQ,EAAE;aACV,QAAQ,CACP,qJAAqJ,CACtJ;QACH,KAAK,EAAE,CAAC;aACL,MAAM,EAAE;aACR,GAAG,EAAE;aACL,GAAG,CAAC,CAAC,CAAC;aACN,GAAG,CAAC,aAAa,CAAC;aAClB,OAAO,CAAC,iBAAiB,CAAC;aAC1B,QAAQ,CACP,oJAAoJ,aAAa,gIAAgI,CAClS;KACJ,CAAC;IACF,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,CACP,kHAAkH,CACnH;QACH,cAAc,EAAE,CAAC;aACd,MAAM,CAAC;YACN,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,2CAA2C,CAAC;YACtF,QAAQ,EAAE,CAAC;iBACR,MAAM,EAAE;iBACR,QAAQ,EAAE;iBACV,QAAQ,CAAC,yEAAyE,CAAC;YACtF,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,8BAA8B,CAAC;SAC3D,CAAC;aACD,QAAQ,CAAC,6CAA6C,CAAC;KAC3D,CAAC;IACF,UAAU,EAAE;QACV,MAAM,EAAE,CAAC;aACN,MAAM,EAAE;aACR,QAAQ,EAAE;aACV,QAAQ,CACP,yKAAyK,CAC1K;QACH,SAAS,EAAE,CAAC;aACT,OAAO,EAAE;aACT,QAAQ,EAAE;aACV,QAAQ,CAAC,uEAAuE,CAAC;QACpF,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,0BAA0B,CAAC;QACjE,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,+BAA+B,CAAC;KACrE;IACD,IAAI,EAAE,CAAC,8BAA8B,CAAC;IACtC,MAAM,EAAE;QACN;YACE,MAAM,EAAE,eAAe;YACvB,IAAI,EAAE,gBAAgB,CAAC,eAAe;YACtC,IAAI,EAAE,2DAA2D;YACjE,QAAQ,EACN,wFAAwF;SAC3F;QACD;YACE,MAAM,EAAE,cAAc;YACtB,IAAI,EAAE,gBAAgB,CAAC,eAAe;YACtC,IAAI,EAAE,sJAAsJ;YAC5J,QAAQ,EACN,mIAAmI;SACtI;KACF;IAED,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG;QACtB,IAAI,KAAyB,CAAC;QAC9B,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;YACpB,MAAM,WAAW,GAAG,oBAAoB,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;YAC1D,IAAI,WAAW,EAAE,CAAC;gBAChB,MAAM,GAAG,CAAC,IAAI,CAAC,cAAc,EAAE,qBAAqB,WAAW,EAAE,EAAE;oBACjE,SAAS,EAAE,KAAK,CAAC,SAAS;oBAC1B,GAAG,GAAG,CAAC,WAAW,CAAC,cAAc,CAAC;iBACnC,CAAC,CAAC;YACL,CAAC;YACD,IAAI,CAAC;gBACH,KAAK,GAAG,IAAI,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;YACtC,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,GAAG,CAAC,IAAI,CACZ,eAAe,EACf,sBAAuB,GAAa,CAAC,OAAO,EAAE,EAC9C,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,GAAG,GAAG,CAAC,WAAW,CAAC,eAAe,CAAC,EAAE,EACnE,EAAE,KAAK,EAAE,GAAG,EAAE,CACf,CAAC;YACJ,CAAC;QACH,CAAC;QAED,MAAM,GAAG,GAAG,kBAAkB,EAAE,CAAC;QACjC,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,IAAI,CAAC,CAAC;QAErC;;;;;WAKG;QACH,MAAM,UAAU,GAAG,CAAC,MAAM,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;aACzC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;aAClD,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,QAAQ,CAAC;aAClC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QAErE,MAAM,IAAI,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QAE7F,MAAM,cAAc,GAAG;YACrB,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC1D,GAAG,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACrC,KAAK,EAAE,KAAK,CAAC,KAAK;SACnB,CAAC;QAEF,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC5B;;;;eAIG;YACH,MAAM,SAAS,GAAG,eAAe,CAAC,EAAE,GAAG,cAAc,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;YAC3E,GAAG,CAAC,MAAM,CAAC,MAAM,CACf,SAAS;gBACP,CAAC,CAAC,mBAAmB,SAAS,oDAAoD;gBAClF,CAAC,CAAC,oDAAoD,CACzD,CAAC;QACJ,CAAC;aAAM,IAAI,UAAU,CAAC,MAAM,GAAG,KAAK,CAAC,KAAK,EAAE,CAAC;YAC3C,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC;gBACnB,KAAK,EAAE,IAAI,CAAC,MAAM;gBAClB,GAAG,EAAE,KAAK,CAAC,KAAK;gBAChB,QAAQ,EAAE,eAAe,IAAI,CAAC,MAAM,iBAAiB,UAAU,CAAC,MAAM,wCAAwC,aAAa,0DAA0D;aACtL,CAAC,CAAC;QACL,CAAC;QAED,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC;IAClC,CAAC;IAED,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE;QACjB,MAAM,MAAM,GAAG,eAAe,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;QACtD,MAAM,KAAK,GAAG,CAAC,KAAK,MAAM,CAAC,IAAI,CAAC,MAAM,UAAU,MAAM,CAAC,CAAC,CAAC,MAAM,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAChF,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACf,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI;gBAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC;QAC3E,CAAC;QACD,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACpD,CAAC;CACF,CAAC,CAAC;AAEH,mEAAmE;AACnE,SAAS,eAAe,CAAC,OAIxB;IACC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,OAAO,CAAC,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,eAAe,OAAO,CAAC,SAAS,IAAI,CAAC,CAAC;IACxE,IAAI,OAAO,CAAC,QAAQ,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,YAAY,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC/E,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,SAAS,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;IACtE,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC3B,CAAC"}
|