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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"frontmatter-ops.d.ts","sourceRoot":"","sources":["../../../src/services/obsidian/frontmatter-ops.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"frontmatter-ops.d.ts","sourceRoot":"","sources":["../../../src/services/obsidian/frontmatter-ops.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,oFAAoF;AACpF,MAAM,WAAW,MAAM;IACrB,yFAAyF;IACzF,IAAI,EAAE,MAAM,CAAC;IACb,uFAAuF;IACvF,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,OAAO,CAAC;IACxB,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IACb,gGAAgG;IAChG,GAAG,EAAE,MAAM,CAAC;IACZ,gFAAgF;IAChF,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAa9C;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAQ1E;AAyBD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAOzE;AAED,MAAM,WAAW,kBAAkB;IACjC,sEAAsE;IACtE,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAC;IAChB,+EAA+E;IAC/E,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,QAAQ,CAAC;AAC5C,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE5D;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,SAAS,EAAE,YAAY,EACvB,QAAQ,EAAE,WAAW,GACpB,kBAAkB,CAqBpB;AA2MD;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC;IACD,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB,CAkBA"}
|
|
@@ -5,17 +5,52 @@
|
|
|
5
5
|
* @module services/obsidian/frontmatter-ops
|
|
6
6
|
*/
|
|
7
7
|
import { isMap, parseDocument } from 'yaml';
|
|
8
|
-
const FM_RE =
|
|
9
|
-
|
|
8
|
+
const FM_RE = /^(---\r?\n)([\s\S]*?)(\r?\n---\r?\n?)/;
|
|
9
|
+
/**
|
|
10
|
+
* Split a note into its raw frontmatter prefix and its body. `raw + body`
|
|
11
|
+
* reconstructs the input byte for byte, which is what makes this the primitive
|
|
12
|
+
* for body-scoped mutations: rebuild with the original `raw` and the frontmatter
|
|
13
|
+
* block is untouched by construction. Rebuilding through `serializeFrontmatter`
|
|
14
|
+
* would instead re-emit the YAML and can reformat scalars nobody asked to change.
|
|
15
|
+
*
|
|
16
|
+
* A file whose fence is never closed, whose `---` sits below the first line, or
|
|
17
|
+
* whose opening `---` carries trailing whitespace has no frontmatter — the whole
|
|
18
|
+
* thing is body, which is also how Obsidian reads each of those shapes.
|
|
19
|
+
*/
|
|
20
|
+
export function splice(content) {
|
|
10
21
|
const m = FM_RE.exec(content);
|
|
11
|
-
if (!m)
|
|
12
|
-
return { hasFrontmatter: false, yamlText: '', body: content };
|
|
22
|
+
if (!m) {
|
|
23
|
+
return { hasFrontmatter: false, raw: '', open: '', yamlText: '', close: '', body: content };
|
|
24
|
+
}
|
|
13
25
|
return {
|
|
14
26
|
hasFrontmatter: true,
|
|
15
|
-
|
|
27
|
+
raw: m[0],
|
|
28
|
+
open: m[1] ?? '',
|
|
29
|
+
yamlText: m[2] ?? '',
|
|
30
|
+
close: m[3] ?? '',
|
|
16
31
|
body: content.slice(m[0].length),
|
|
17
32
|
};
|
|
18
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Describe why `yamlText` is not usable as a frontmatter block, or `undefined`
|
|
36
|
+
* when it is. Catches YAML that no longer parses and YAML that parses to
|
|
37
|
+
* something other than a mapping — the two states in which Obsidian reads no
|
|
38
|
+
* properties at all.
|
|
39
|
+
*
|
|
40
|
+
* It cannot catch an edit that stays well-formed while meaning something else:
|
|
41
|
+
* a renamed key, a scalar that re-parses as a different type. Those are valid
|
|
42
|
+
* YAML and pass this check.
|
|
43
|
+
*/
|
|
44
|
+
export function frontmatterParseError(yamlText) {
|
|
45
|
+
const doc = parseDocument(yamlText);
|
|
46
|
+
const first = doc.errors[0];
|
|
47
|
+
if (first)
|
|
48
|
+
return first.message;
|
|
49
|
+
if (doc.contents !== null && !isMap(doc.contents)) {
|
|
50
|
+
return 'Frontmatter must be a YAML mapping of properties.';
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
19
54
|
/**
|
|
20
55
|
* Re-emit the parsed frontmatter document between `---` fences, preserving the
|
|
21
56
|
* comments, quoting, and scalar formatting of every untouched node — the reason
|
|
@@ -133,8 +168,14 @@ function normalizeTagList(value) {
|
|
|
133
168
|
}
|
|
134
169
|
const FENCED_CODE_BLOCK = /(```[\s\S]*?```|~~~[\s\S]*?~~~)/g;
|
|
135
170
|
const INLINE_CODE = /(`[^`\n]+`)/g;
|
|
171
|
+
/**
|
|
172
|
+
* Inline `#tag` syntax lives in the body. The frontmatter block is spliced off
|
|
173
|
+
* first and re-attached verbatim, so a `#` inside a YAML scalar is neither read
|
|
174
|
+
* as a tag nor rewritten by a removal.
|
|
175
|
+
*/
|
|
136
176
|
function mutateInlineTags(content, tags, operation, applied, skipped) {
|
|
137
|
-
const
|
|
177
|
+
const { raw, body } = splice(content);
|
|
178
|
+
const segments = splitProtectedSegments(body);
|
|
138
179
|
let updatedNonCode = false;
|
|
139
180
|
if (operation === 'add') {
|
|
140
181
|
for (const tag of tags) {
|
|
@@ -152,16 +193,23 @@ function mutateInlineTags(content, tags, operation, applied, skipped) {
|
|
|
152
193
|
.map((t) => `#${t}`)
|
|
153
194
|
.join(' ');
|
|
154
195
|
if (additions.length > 0) {
|
|
196
|
+
/**
|
|
197
|
+
* The separator turns on everything that precedes the insertion point in
|
|
198
|
+
* the finished note — `raw` and every segment — not on any one of them.
|
|
199
|
+
* A note that is nothing but frontmatter has an empty body while `raw`
|
|
200
|
+
* already ends in a newline, and a note whose body ends at a closing code
|
|
201
|
+
* fence ends in a protected segment the tag must not be glued to.
|
|
202
|
+
*/
|
|
203
|
+
const preceding = raw + segments.map((s) => s.text).join('');
|
|
204
|
+
const sep = preceding.endsWith('\n') ? '' : '\n';
|
|
155
205
|
const trailing = segments.length > 0 ? (segments[segments.length - 1] ?? null) : null;
|
|
156
206
|
if (trailing && !trailing.protected) {
|
|
157
|
-
const sep = trailing.text.endsWith('\n') ? '' : '\n';
|
|
158
207
|
trailing.text = `${trailing.text}${sep}${additions}\n`;
|
|
159
|
-
updatedNonCode = true;
|
|
160
208
|
}
|
|
161
209
|
else {
|
|
162
|
-
segments.push({ protected: false, text:
|
|
163
|
-
updatedNonCode = true;
|
|
210
|
+
segments.push({ protected: false, text: `${sep}${additions}\n` });
|
|
164
211
|
}
|
|
212
|
+
updatedNonCode = true;
|
|
165
213
|
}
|
|
166
214
|
}
|
|
167
215
|
else {
|
|
@@ -171,23 +219,47 @@ function mutateInlineTags(content, tags, operation, applied, skipped) {
|
|
|
171
219
|
for (const s of segments) {
|
|
172
220
|
if (s.protected)
|
|
173
221
|
continue;
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
222
|
+
/**
|
|
223
|
+
* One pass is not enough: the regex consumes the space after the tag,
|
|
224
|
+
* and that space is the left boundary the next occurrence needs, so a
|
|
225
|
+
* single pass stops at the first of two same tags separated by one
|
|
226
|
+
* space. Repeat until the segment stops changing — every pass drops at
|
|
227
|
+
* least the tag itself, so this terminates.
|
|
228
|
+
*/
|
|
229
|
+
for (let next = s.text.replace(re, removeAt); next !== s.text; next = s.text.replace(re, removeAt)) {
|
|
230
|
+
s.text = next;
|
|
178
231
|
found = true;
|
|
179
|
-
updatedNonCode = true;
|
|
180
232
|
}
|
|
181
233
|
}
|
|
182
|
-
if (found)
|
|
234
|
+
if (found) {
|
|
183
235
|
applied.add(tag);
|
|
184
|
-
|
|
236
|
+
updatedNonCode = true;
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
185
239
|
skipped.add(tag);
|
|
240
|
+
}
|
|
186
241
|
}
|
|
187
242
|
}
|
|
188
243
|
if (!updatedNonCode)
|
|
189
244
|
return content;
|
|
190
|
-
return segments.map((s) => s.text).join('');
|
|
245
|
+
return raw + segments.map((s) => s.text).join('');
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Close the gap a removed tag leaves without touching anything else. Exactly one
|
|
249
|
+
* adjacent horizontal space goes with the tag — the one before it when there is
|
|
250
|
+
* one, otherwise the one after — so neighbouring words neither jam together nor
|
|
251
|
+
* end up separated by a widened gap. Everything outside that span survives byte
|
|
252
|
+
* for byte: list and code-block indentation, a trailing two-space hard line
|
|
253
|
+
* break, table cell padding.
|
|
254
|
+
*/
|
|
255
|
+
function removeAt(full, leading, trailing, offset, whole) {
|
|
256
|
+
if (!/^[ \t]$/.test(leading))
|
|
257
|
+
return leading;
|
|
258
|
+
// A space is all that marks the start of an immediately following tag —
|
|
259
|
+
// taking it would silently stop that tag being one.
|
|
260
|
+
if (trailing === '' && whole[offset + full.length] === '#')
|
|
261
|
+
return leading;
|
|
262
|
+
return trailing;
|
|
191
263
|
}
|
|
192
264
|
function splitProtectedSegments(content) {
|
|
193
265
|
const segments = [];
|
|
@@ -209,16 +281,21 @@ function splitProtectedSegments(content) {
|
|
|
209
281
|
}
|
|
210
282
|
return segments;
|
|
211
283
|
}
|
|
284
|
+
/** Captures the character before the tag and the single horizontal space after it, if any. */
|
|
212
285
|
function makeInlineTagRegex(tag) {
|
|
213
286
|
const escaped = tag.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
|
|
214
|
-
return new RegExp(`(^|[^\\w/])#${escaped}(?![\\w/-])`, 'g');
|
|
287
|
+
return new RegExp(`(^|[^\\w/])#${escaped}(?![\\w/-])([ \\t]?)`, 'g');
|
|
215
288
|
}
|
|
216
|
-
/**
|
|
289
|
+
/**
|
|
290
|
+
* Read-only helpers for `obsidian_manage_tags list`. Inline tags are read from
|
|
291
|
+
* the body only, matching where a removal can actually reach — a `#` inside a
|
|
292
|
+
* YAML scalar is frontmatter, not a tag.
|
|
293
|
+
*/
|
|
217
294
|
export function listTagsFromContent(content, frontmatter) {
|
|
218
295
|
const fmTags = normalizeTagList(frontmatter.tags);
|
|
219
296
|
const inline = [];
|
|
220
297
|
const seen = new Set();
|
|
221
|
-
for (const seg of splitProtectedSegments(content)) {
|
|
298
|
+
for (const seg of splitProtectedSegments(splice(content).body)) {
|
|
222
299
|
if (seg.protected)
|
|
223
300
|
continue;
|
|
224
301
|
const re = /(^|[^\w/])#([a-zA-Z][\w/-]*)/g;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"frontmatter-ops.js","sourceRoot":"","sources":["../../../src/services/obsidian/frontmatter-ops.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAiB,KAAK,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAE3D,MAAM,KAAK,GAAG,
|
|
1
|
+
{"version":3,"file":"frontmatter-ops.js","sourceRoot":"","sources":["../../../src/services/obsidian/frontmatter-ops.ts"],"names":[],"mappings":"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"}
|
|
@@ -86,6 +86,13 @@ export declare class ObsidianService {
|
|
|
86
86
|
listFiles(ctx: Context, dirPath?: string): Promise<FileListing>;
|
|
87
87
|
listTags(ctx: Context): Promise<ObsidianTag[]>;
|
|
88
88
|
listCommands(ctx: Context): Promise<ObsidianCommand[]>;
|
|
89
|
+
/**
|
|
90
|
+
* Text search. The upstream materializes one context window per matching
|
|
91
|
+
* note before it responds, so an oversized `contextLength` on a broad query
|
|
92
|
+
* exhausts V8's string capacity and comes back as an opaque 500 — caught
|
|
93
|
+
* here and re-thrown typed, the way `searchOmnisearch` re-throws its own
|
|
94
|
+
* reachability failure.
|
|
95
|
+
*/
|
|
89
96
|
searchText(ctx: Context, query: string, contextLength?: number): Promise<TextSearchHit[]>;
|
|
90
97
|
searchJsonLogic(ctx: Context, logic: Record<string, unknown>): Promise<StructuredSearchHit[]>;
|
|
91
98
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"obsidian-service.d.ts","sourceRoot":"","sources":["../../../src/services/obsidian/obsidian-service.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"obsidian-service.d.ts","sourceRoot":"","sources":["../../../src/services/obsidian/obsidian-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* `target` discriminator, and classifies errors for the framework.
|
|
5
5
|
* @module services/obsidian/obsidian-service
|
|
6
6
|
*/
|
|
7
|
-
import { conflict, forbidden, notFound, serviceUnavailable, unauthorized, validationError, } from '@cyanheads/mcp-ts-core/errors';
|
|
8
|
-
import {
|
|
7
|
+
import { conflict, forbidden, JsonRpcErrorCode, McpError, notFound, serviceUnavailable, unauthorized, validationError, } from '@cyanheads/mcp-ts-core/errors';
|
|
8
|
+
import { httpStatusToErrorCode, withRetry } from '@cyanheads/mcp-ts-core/utils';
|
|
9
9
|
import { Agent, fetch as undiciFetch } from 'undici';
|
|
10
10
|
import { getServerConfig } from '../../config/server-config.js';
|
|
11
11
|
import { PathPolicy } from './path-policy.js';
|
|
@@ -301,14 +301,45 @@ export class ObsidianService {
|
|
|
301
301
|
return body.commands ?? [];
|
|
302
302
|
}
|
|
303
303
|
// ── Search ───────────────────────────────────────────────────────────────
|
|
304
|
+
/**
|
|
305
|
+
* Text search. The upstream materializes one context window per matching
|
|
306
|
+
* note before it responds, so an oversized `contextLength` on a broad query
|
|
307
|
+
* exhausts V8's string capacity and comes back as an opaque 500 — caught
|
|
308
|
+
* here and re-thrown typed, the way `searchOmnisearch` re-throws its own
|
|
309
|
+
* reachability failure.
|
|
310
|
+
*/
|
|
304
311
|
async searchText(ctx, query, contextLength = 100) {
|
|
305
312
|
const params = new URLSearchParams({ query, contextLength: String(contextLength) });
|
|
306
|
-
|
|
313
|
+
let res;
|
|
314
|
+
try {
|
|
315
|
+
res = await this.#request(ctx, `/search/simple/?${params}`, { method: 'POST' });
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
if (!isStringCapacityOverflow(err))
|
|
319
|
+
throw err;
|
|
320
|
+
/**
|
|
321
|
+
* Built fresh rather than decorated: `contextLength` is the actionable
|
|
322
|
+
* fact and only this frame knows it. `cause` keeps the caught error —
|
|
323
|
+
* and through it the upstream body `#throwForStatus` attached — on the
|
|
324
|
+
* server side for the log record.
|
|
325
|
+
*/
|
|
326
|
+
throw validationError(`The Local REST API ran out of string capacity assembling match contexts for this search (contextLength ${contextLength}). It builds a context window for every matching note before responding, so the ceiling tracks contextLength multiplied by the number of matches rather than either alone.`, {
|
|
327
|
+
reason: 'context_length_too_large',
|
|
328
|
+
contextLength,
|
|
329
|
+
...ctx.recoveryFor('context_length_too_large'),
|
|
330
|
+
}, { cause: err });
|
|
331
|
+
}
|
|
307
332
|
const raw = (await res.json());
|
|
308
333
|
// Upstream returns a constant `score` that carries no ranking signal for
|
|
309
334
|
// text mode — drop it on the way out so consumers don't mistake it for
|
|
310
335
|
// relevance. Omnisearch is the source of real BM25 ranking.
|
|
311
|
-
return raw.map((h) => ({
|
|
336
|
+
return raw.map((h) => ({
|
|
337
|
+
filename: h.filename,
|
|
338
|
+
matches: h.matches.map((m) => ({
|
|
339
|
+
context: m.context,
|
|
340
|
+
match: { ...m.match, ...contextRelativeSpan(m, h.filename, contextLength, query) },
|
|
341
|
+
})),
|
|
342
|
+
}));
|
|
312
343
|
}
|
|
313
344
|
async searchJsonLogic(ctx, logic) {
|
|
314
345
|
const res = await this.#request(ctx, '/search/', {
|
|
@@ -613,81 +644,133 @@ export class ObsidianService {
|
|
|
613
644
|
...ctx.recoveryFor('path_is_directory'),
|
|
614
645
|
});
|
|
615
646
|
}
|
|
647
|
+
/**
|
|
648
|
+
* Classify an upstream error response and throw.
|
|
649
|
+
*
|
|
650
|
+
* **Containment invariant.** Four things leave this method on the wire and
|
|
651
|
+
* nothing else: a message authored in this file, the request path the caller
|
|
652
|
+
* supplied (`data.path`), the HTTP status (`data.status`, default branch),
|
|
653
|
+
* and the calling tool's own contract `reason` + `recovery`. The upstream's
|
|
654
|
+
* response body never crosses, in any branch, under any key.
|
|
655
|
+
*
|
|
656
|
+
* That is stricter than redaction, deliberately. The Local REST API
|
|
657
|
+
* interleaves genuine diagnostics with vault data in one string: a rejected
|
|
658
|
+
* JSONLogic tree names the note the plugin was evaluating when it gave up
|
|
659
|
+
* (`(while processing <path>)`) and, when `regexp`'s operands are reversed,
|
|
660
|
+
* quotes that note's body back as the pattern it failed to compile; 5xx
|
|
661
|
+
* bodies relay Node `fs` errors carrying the vault's absolute on-disk
|
|
662
|
+
* location. `PathPolicy` gates the success path only, so none of that is
|
|
663
|
+
* scoped to what the caller may read. The two halves cannot be separated
|
|
664
|
+
* reliably — the pattern echo is delimited by characters the note body may
|
|
665
|
+
* itself contain, so any parse that keeps "the diagnostic half" fails open
|
|
666
|
+
* on exactly the input a caller controls, and failing open is the wrong
|
|
667
|
+
* direction for a containment control. So containment wins: the upstream
|
|
668
|
+
* text is read here only to *classify*, and what the caller receives is this
|
|
669
|
+
* file's own account of the failure. A misclassification therefore costs
|
|
670
|
+
* accuracy, never containment.
|
|
671
|
+
*
|
|
672
|
+
* The body is not discarded. It rides as `cause`, which the framework's
|
|
673
|
+
* error handler walks into the log record and neither client surface
|
|
674
|
+
* serializes — `ErrorOptions` makes `cause` non-enumerable, the tool error
|
|
675
|
+
* envelope is built from `code`/`message`/`data` alone, and so is the
|
|
676
|
+
* JSON-RPC error object the SDK emits for resources.
|
|
677
|
+
*
|
|
678
|
+
* Issues #104 and #116.
|
|
679
|
+
*/
|
|
616
680
|
async #throwForStatus(res, path, ctx) {
|
|
617
681
|
const text = await this.#readBodySafe(res);
|
|
618
682
|
const body = parseJsonObject(text);
|
|
619
683
|
const display = displayPath(path);
|
|
620
|
-
|
|
684
|
+
/** Classification input and log payload only — never a wire value. */
|
|
685
|
+
const upstreamMsg = typeof body?.message === 'string' ? body.message : text.trim();
|
|
686
|
+
const cause = new UpstreamErrorText(upstreamMsg);
|
|
621
687
|
const data = (reason) => ({
|
|
622
688
|
path: display,
|
|
623
689
|
...(reason !== undefined ? { reason, ...ctx.recoveryFor(reason) } : {}),
|
|
624
|
-
...(upstream ? { upstream } : {}),
|
|
625
690
|
});
|
|
626
691
|
switch (res.status) {
|
|
627
692
|
case 401:
|
|
628
|
-
throw unauthorized('Obsidian Local REST API rejected the API key. Verify OBSIDIAN_API_KEY matches the value in Obsidian → Settings → Local REST API.', data());
|
|
693
|
+
throw unauthorized('Obsidian Local REST API rejected the API key. Verify OBSIDIAN_API_KEY matches the value in Obsidian → Settings → Local REST API.', data(), { cause });
|
|
629
694
|
case 403:
|
|
630
|
-
throw forbidden('Obsidian Local REST API forbids this request. Check the plugin permissions.', data());
|
|
695
|
+
throw forbidden('Obsidian Local REST API forbids this request. Check the plugin permissions.', data(), { cause });
|
|
631
696
|
case 404: {
|
|
632
697
|
if (path.startsWith('/active/')) {
|
|
633
|
-
throw notFound('No file is currently active in Obsidian — open a file in the app first.', data('no_active_file'));
|
|
698
|
+
throw notFound('No file is currently active in Obsidian — open a file in the app first.', data('no_active_file'), { cause });
|
|
634
699
|
}
|
|
635
700
|
if (path.startsWith('/periodic/')) {
|
|
636
|
-
const periodMatch = /^\/periodic\/(daily|weekly|monthly|quarterly|yearly)\//.exec(path);
|
|
637
|
-
const period = periodMatch?.[1] ?? 'periodic';
|
|
638
701
|
const dateMatch = /\/(\d{4})\/(\d{2})\/(\d{2})\/?$/.exec(path);
|
|
639
702
|
const suffix = dateMatch ? ` for ${dateMatch[1]}-${dateMatch[2]}-${dateMatch[3]}` : '';
|
|
640
|
-
throw notFound(`No ${
|
|
703
|
+
throw notFound(`No ${periodOf(path)} note found${suffix}. Check that the Periodic Notes plugin is enabled and the note exists.`, data('periodic_not_found'), { cause });
|
|
641
704
|
}
|
|
642
705
|
if (path.startsWith('/commands/')) {
|
|
643
|
-
throw notFound(`Unknown Obsidian command: ${display}. Use \`obsidian_list_commands\` to discover valid command IDs.`, data('command_unknown'));
|
|
706
|
+
throw notFound(`Unknown Obsidian command: ${display}. Use \`obsidian_list_commands\` to discover valid command IDs.`, data('command_unknown'), { cause });
|
|
644
707
|
}
|
|
645
|
-
throw notFound(`Not found: ${display}`, data('note_missing'));
|
|
708
|
+
throw notFound(`Not found: ${display}`, data('note_missing'), { cause });
|
|
646
709
|
}
|
|
647
710
|
case 405:
|
|
648
|
-
throw validationError(`${display} cannot accept this method (often: the path is a directory, not a file).`, data('path_is_directory'));
|
|
711
|
+
throw validationError(`${display} cannot accept this method (often: the path is a directory, not a file).`, data('path_is_directory'), { cause });
|
|
649
712
|
case 400: {
|
|
650
|
-
|
|
713
|
+
/**
|
|
714
|
+
* A malformed JSONLogic tree is rejected by the plugin's query parser
|
|
715
|
+
* with a bare 400. Keyed on the exact route rather than the message,
|
|
716
|
+
* because the upstream wording varies with which parse stage failed
|
|
717
|
+
* and `/search/simple/` (text mode) shares the `/search/` prefix
|
|
718
|
+
* without sharing the cause.
|
|
719
|
+
*
|
|
720
|
+
* The uncompilable-`regexp` case is named separately: it is the
|
|
721
|
+
* ordinary failure of a hand-written backlink query, and it is also
|
|
722
|
+
* the one whose upstream text quotes a note body. Classifying it here
|
|
723
|
+
* keeps the diagnostic a caller needs without relaying the pattern
|
|
724
|
+
* that carries it. The `logic_invalid` contract recovery supplies the
|
|
725
|
+
* operand order, and the framework mirrors it onto both surfaces.
|
|
726
|
+
*/
|
|
727
|
+
if (routeOf(path) === '/search/') {
|
|
728
|
+
throw validationError(/invalid regular expression/i.test(upstreamMsg)
|
|
729
|
+
? 'The Local REST API could not compile a `regexp` operand in the JSONLogic tree. `regexp` takes `[PATTERN, VALUE]` — with the operands reversed, the note field is compiled as the pattern.'
|
|
730
|
+
: 'The Local REST API could not evaluate the JSONLogic tree.', data('logic_invalid'), { cause });
|
|
731
|
+
}
|
|
651
732
|
// Content-preexists is a more specific case nested inside the broader
|
|
652
733
|
// "could not be applied" family — branch on it first so retries with
|
|
653
734
|
// identical content surface the right reason and recovery (toggle
|
|
654
735
|
// `applyIfContentPreexists`) instead of misleading section-miss copy.
|
|
655
736
|
if (/content-already-preexists-in-target/i.test(upstreamMsg)) {
|
|
656
|
-
throw validationError(`The supplied content already appears at the target in ${display}. Pass \`applyIfContentPreexists: true\` to force-apply, or change the content.`, data('content_preexists'));
|
|
737
|
+
throw validationError(`The supplied content already appears at the target in ${display}. Pass \`applyIfContentPreexists: true\` to force-apply, or change the content.`, data('content_preexists'), { cause });
|
|
657
738
|
}
|
|
658
739
|
// The Local REST API returns a "could not be applied to the target
|
|
659
740
|
// content" / "invalid-target" message when a PATCH names a section that
|
|
660
741
|
// doesn't exist. Translate to actionable guidance.
|
|
661
742
|
const isTargetMiss = /\bcould not be applied\b|\binvalid-target\b/i.test(upstreamMsg);
|
|
662
743
|
if (isTargetMiss) {
|
|
663
|
-
throw validationError(`Section target not found in ${display}. Use \`obsidian_get_note\` with \`format: "document-map"\` to list available headings, blocks, and frontmatter fields, then retry with one of those locators.`, data('section_target_missing'));
|
|
744
|
+
throw validationError(`Section target not found in ${display}. Use \`obsidian_get_note\` with \`format: "document-map"\` to list available headings, blocks, and frontmatter fields, then retry with one of those locators.`, data('section_target_missing'), { cause });
|
|
664
745
|
}
|
|
665
746
|
// Periodic Notes plugin returns 400 with "Specified period is not enabled"
|
|
666
747
|
// when the requested period (daily/weekly/monthly/...) is disabled in the
|
|
667
748
|
// user's plugin settings. Distinct from periodic_not_found (404) — caller
|
|
668
749
|
// can enable it or fall back to an explicit path target.
|
|
669
750
|
if (path.startsWith('/periodic/') && /\bnot enabled\b/i.test(upstreamMsg)) {
|
|
670
|
-
throw validationError(
|
|
751
|
+
throw validationError(`The ${periodOf(path)} period is not enabled in Obsidian's Periodic Notes plugin settings. Enable it there, or address the note by an explicit vault path instead.`, data('periodic_disabled'), { cause });
|
|
671
752
|
}
|
|
672
|
-
throw validationError(
|
|
753
|
+
throw validationError(`Obsidian Local REST API rejected the request to ${display} as malformed (HTTP 400).`, data(), { cause });
|
|
673
754
|
}
|
|
674
755
|
default: {
|
|
675
756
|
/**
|
|
676
|
-
* Unhandled 4xx and all 5xx
|
|
677
|
-
*
|
|
678
|
-
* 502/503→ServiceUnavailable, 504→Timeout)
|
|
679
|
-
*
|
|
680
|
-
* and
|
|
757
|
+
* Unhandled 4xx and all 5xx. `httpStatusToErrorCode` supplies the same
|
|
758
|
+
* canonical mapping `httpErrorFromResponse` would (500/501 →
|
|
759
|
+
* InternalError, 502/503 → ServiceUnavailable, 504 → Timeout) without
|
|
760
|
+
* that helper's response-derived extras: it builds its message from
|
|
761
|
+
* the upstream's reason phrase and mirrors the body onto
|
|
762
|
+
* `data.body`/`data.responseBody`, which is the #104 leak.
|
|
763
|
+
*
|
|
764
|
+
* `Retry-After` is re-attached by hand: it is a duration, never vault
|
|
765
|
+
* data, and `withRetry` reads `data.retryAfter` to pace its backoff, so
|
|
766
|
+
* dropping it with the rest would quietly change retry timing.
|
|
681
767
|
*/
|
|
682
|
-
const
|
|
683
|
-
throw
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
...(truncated !== undefined ? { body: truncated } : {}),
|
|
689
|
-
},
|
|
690
|
-
});
|
|
768
|
+
const retryAfter = res.headers.get('retry-after');
|
|
769
|
+
throw new McpError(httpStatusToErrorCode(res.status) ?? JsonRpcErrorCode.InternalError, `Obsidian Local REST API returned HTTP ${res.status}.`, {
|
|
770
|
+
...data(),
|
|
771
|
+
status: res.status,
|
|
772
|
+
...(retryAfter !== null ? { retryAfter } : {}),
|
|
773
|
+
}, { cause });
|
|
691
774
|
}
|
|
692
775
|
}
|
|
693
776
|
}
|
|
@@ -734,6 +817,93 @@ export function encodeVaultPath(path) {
|
|
|
734
817
|
}
|
|
735
818
|
return segments.map((seg) => encodeURIComponent(seg)).join('/');
|
|
736
819
|
}
|
|
820
|
+
/**
|
|
821
|
+
* Locate the match span inside the `context` window the upstream ships with it.
|
|
822
|
+
*
|
|
823
|
+
* `start`/`end` are offsets into whichever subject the plugin matched, and
|
|
824
|
+
* there are two. For a **body match** the subject is the note text and the
|
|
825
|
+
* window is `body.slice(max(0, start - contextLength), min(len, end + contextLength))`,
|
|
826
|
+
* so the span sits `min(start, contextLength)` characters into `context`. For a
|
|
827
|
+
* **filename match** the subject is the note's basename and the plugin returns
|
|
828
|
+
* that basename whole as `context`, so the span sits at `start` — which runs
|
|
829
|
+
* past `min(start, contextLength)` as soon as the name is longer than the
|
|
830
|
+
* window. A single hit's `matches[]` can carry both kinds. Verified against
|
|
831
|
+
* Local REST API v5.0.3.
|
|
832
|
+
*
|
|
833
|
+
* `context === basename` is the cheap separator, but it is not decisive: a body
|
|
834
|
+
* window whose left edge is trimmed can coincide with the basename (a note whose
|
|
835
|
+
* own name is quoted in its body, matched so the window lands on that quote),
|
|
836
|
+
* and then the two readings disagree and the filename one slices the wrong text
|
|
837
|
+
* — often past the end of `context` entirely. Where they disagree, settle it on
|
|
838
|
+
* the text rather than the coincidence: the plugin matches whole query tokens,
|
|
839
|
+
* so the correct reading reproduces one. An inconclusive check keeps the
|
|
840
|
+
* filename reading, which is what the coincidence test alone would have picked.
|
|
841
|
+
*/
|
|
842
|
+
function contextRelativeSpan(m, filename, contextLength, query) {
|
|
843
|
+
const span = m.match.end - m.match.start;
|
|
844
|
+
const bodyStart = Math.min(m.match.start, contextLength);
|
|
845
|
+
const basename = (filename.split('/').pop() ?? filename).replace(/\.[^./]+$/, '');
|
|
846
|
+
const at = (i) => ({ contextStart: i, contextEnd: i + span });
|
|
847
|
+
if (m.context !== basename || m.match.start === bodyStart)
|
|
848
|
+
return at(bodyStart);
|
|
849
|
+
const reproducesToken = (i) => {
|
|
850
|
+
const slice = m.context.slice(i, i + span);
|
|
851
|
+
return slice.length === span && query.toLowerCase().includes(slice.toLowerCase());
|
|
852
|
+
};
|
|
853
|
+
return reproducesToken(m.match.start) || !reproducesToken(bodyStart)
|
|
854
|
+
? at(m.match.start)
|
|
855
|
+
: at(bodyStart);
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* The upstream error body, carried as the `cause` of everything
|
|
859
|
+
* `#throwForStatus` throws.
|
|
860
|
+
*
|
|
861
|
+
* `cause` is the one channel that reaches the server's logs without reaching
|
|
862
|
+
* the client, which is what lets the containment invariant hold while the
|
|
863
|
+
* text stays available for debugging and for the classifiers below. The body
|
|
864
|
+
* is the `Error` message so the framework's cause-chain extractor records it.
|
|
865
|
+
*/
|
|
866
|
+
class UpstreamErrorText extends Error {
|
|
867
|
+
constructor(text) {
|
|
868
|
+
super(text);
|
|
869
|
+
this.name = 'UpstreamErrorText';
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* The upstream body behind an error, when `#throwForStatus` raised it. Walks
|
|
874
|
+
* the `cause` chain rather than reading one level: `withRetry` re-wraps an
|
|
875
|
+
* exhausted error, so on a retry-safe method the carrier sits a level deeper
|
|
876
|
+
* than the throw site left it.
|
|
877
|
+
*/
|
|
878
|
+
function upstreamTextOf(err) {
|
|
879
|
+
let current = err;
|
|
880
|
+
for (let depth = 0; depth < 4; depth++) {
|
|
881
|
+
if (current instanceof UpstreamErrorText)
|
|
882
|
+
return current.message;
|
|
883
|
+
if (!(current instanceof Error))
|
|
884
|
+
return;
|
|
885
|
+
current = current.cause;
|
|
886
|
+
}
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* True for the V8 `RangeError: Invalid string length` the Local REST API
|
|
891
|
+
* surfaces (as an opaque HTTP 500) when it cannot concatenate the response.
|
|
892
|
+
* Read off the `cause` carrier: since the containment invariant on
|
|
893
|
+
* `#throwForStatus`, the upstream text appears on neither the message nor
|
|
894
|
+
* `data`, and this is the only place it survives.
|
|
895
|
+
*/
|
|
896
|
+
function isStringCapacityOverflow(err) {
|
|
897
|
+
return /invalid string length/i.test(upstreamTextOf(err) ?? '');
|
|
898
|
+
}
|
|
899
|
+
/** The period segment of a `/periodic/…` request path, for message copy. */
|
|
900
|
+
function periodOf(urlPath) {
|
|
901
|
+
return /^\/periodic\/(daily|weekly|monthly|quarterly|yearly)\//.exec(urlPath)?.[1] ?? 'periodic';
|
|
902
|
+
}
|
|
903
|
+
/** The request path with its query string dropped — the route alone. */
|
|
904
|
+
function routeOf(urlPath) {
|
|
905
|
+
return urlPath.split('?')[0] ?? urlPath;
|
|
906
|
+
}
|
|
737
907
|
/**
|
|
738
908
|
* Convert an internal URL path (e.g. `/vault/Projects/My%20Note.md`) to the
|
|
739
909
|
* vault-relative form a caller would recognize. Used in error messages so the
|
|
@@ -742,7 +912,7 @@ export function encodeVaultPath(path) {
|
|
|
742
912
|
function displayPath(urlPath) {
|
|
743
913
|
if (urlPath.startsWith('/active/'))
|
|
744
914
|
return '(active file)';
|
|
745
|
-
const noQuery = urlPath
|
|
915
|
+
const noQuery = routeOf(urlPath);
|
|
746
916
|
let decoded;
|
|
747
917
|
try {
|
|
748
918
|
decoded = decodeURIComponent(noQuery);
|
|
@@ -764,19 +934,6 @@ function displayPath(urlPath) {
|
|
|
764
934
|
}
|
|
765
935
|
return decoded;
|
|
766
936
|
}
|
|
767
|
-
/**
|
|
768
|
-
* Trim the upstream error body down to a safe, user-presentable shape — drops
|
|
769
|
-
* `errorCode` and any other plugin-internal fields that would otherwise leak
|
|
770
|
-
* into JSON-RPC `error.data`.
|
|
771
|
-
*/
|
|
772
|
-
function safeUpstream(body, text) {
|
|
773
|
-
if (body?.message)
|
|
774
|
-
return { message: body.message };
|
|
775
|
-
const trimmed = text.trim();
|
|
776
|
-
if (trimmed)
|
|
777
|
-
return { message: trimmed.length > 200 ? `${trimmed.slice(0, 200)}…` : trimmed };
|
|
778
|
-
return;
|
|
779
|
-
}
|
|
780
937
|
/**
|
|
781
938
|
* Read the `Content-Length` header from a HEAD response and parse it as a
|
|
782
939
|
* non-negative integer byte count. Throws when the upstream omits the header
|