pkm-mcp-server 1.0.0 → 1.1.1
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/CHANGELOG.md +10 -2
- package/helpers.js +49 -5
- package/index.js +3 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,12 +6,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.1.0] - 2026-02-23
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- `vault_peek` heading outline now displays as an indented tree instead of a flat list — hierarchy is conveyed by 2-space indentation per level, `#` markers stripped
|
|
13
|
+
- `vault_peek` heading outline no longer includes line number prefixes (`L13`, `L15`, etc.)
|
|
14
|
+
- `vault_peek` size line now always includes chunk count (e.g. "1 chunk", "3 chunks") instead of showing a separate `**Chunks:**` line only for multi-chunk files
|
|
15
|
+
|
|
9
16
|
## [1.0.0] - 2026-02-11
|
|
10
17
|
|
|
11
18
|
### Added
|
|
12
19
|
- MCP server with 18 tools for Obsidian vault interaction
|
|
13
20
|
- `vault_read` with pagination support (heading-based, tail lines, tail sections, chunk, line range); auto-redirects large files (>80k chars) to peek data; `force` param to bypass redirect (hard-capped at ~400k chars)
|
|
14
|
-
- `vault_peek` for inspecting file metadata and structure without reading full content (size, frontmatter, heading
|
|
21
|
+
- `vault_peek` for inspecting file metadata and structure without reading full content (size, frontmatter, indented heading tree, preview)
|
|
15
22
|
- `vault_write` with template-based note creation enforcing YAML frontmatter
|
|
16
23
|
- `vault_append` with positional insert (after heading, before heading, end of section)
|
|
17
24
|
- `vault_edit` for surgical single-occurrence string replacement
|
|
@@ -48,5 +55,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
48
55
|
- Atomic file creation in `vault_write` (`wx` flag) prevents race conditions
|
|
49
56
|
- Error messages sanitized to prevent leaking absolute vault paths
|
|
50
57
|
|
|
51
|
-
[Unreleased]: https://github.com/AdrianV101/Obsidian-MCP/compare/v1.
|
|
58
|
+
[Unreleased]: https://github.com/AdrianV101/Obsidian-MCP/compare/v1.1.0...HEAD
|
|
59
|
+
[1.1.0]: https://github.com/AdrianV101/Obsidian-MCP/compare/v1.0.0...v1.1.0
|
|
52
60
|
[1.0.0]: https://github.com/AdrianV101/Obsidian-MCP/releases/tag/v1.0.0
|
package/helpers.js
CHANGED
|
@@ -12,6 +12,39 @@ const PRIORITY_RANKS = { urgent: 3, high: 2, normal: 1, low: 0 };
|
|
|
12
12
|
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
13
13
|
const DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
14
14
|
|
|
15
|
+
// Canonical frontmatter field constraints, keyed by note type.
|
|
16
|
+
// Used by vault_write (template-based) and vault_update_frontmatter (existing files).
|
|
17
|
+
const FIELD_ENUMS = {
|
|
18
|
+
status: {
|
|
19
|
+
task: ["pending", "active", "done", "cancelled"],
|
|
20
|
+
},
|
|
21
|
+
priority: {
|
|
22
|
+
task: Object.keys(PRIORITY_RANKS), // low, normal, high, urgent
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Validate a frontmatter field value against FIELD_ENUMS for a given note type.
|
|
28
|
+
* Throws if the value is not in the allowed set.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} fieldName - the frontmatter field (e.g. "status")
|
|
31
|
+
* @param {*} value - the value being set
|
|
32
|
+
* @param {string} noteType - the note's type field (e.g. "task")
|
|
33
|
+
*/
|
|
34
|
+
export function validateFieldEnum(fieldName, value, noteType) {
|
|
35
|
+
if (value === null || value === undefined) return;
|
|
36
|
+
const enumsByType = FIELD_ENUMS[fieldName];
|
|
37
|
+
if (!enumsByType) return;
|
|
38
|
+
const allowed = enumsByType[noteType];
|
|
39
|
+
if (!allowed) return;
|
|
40
|
+
const strValue = String(value);
|
|
41
|
+
if (!allowed.includes(strValue)) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Invalid ${fieldName} "${strValue}" for type "${noteType}". Allowed values: ${allowed.join(", ")}`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
15
48
|
/**
|
|
16
49
|
* Compare two frontmatter values for sorting.
|
|
17
50
|
* Smart ordering: priority uses custom ranks, dates sort chronologically, strings use localeCompare.
|
|
@@ -314,11 +347,18 @@ export function substituteTemplateVariables(content, vars) {
|
|
|
314
347
|
}
|
|
315
348
|
}
|
|
316
349
|
|
|
350
|
+
// Extract note type from template for enum validation
|
|
351
|
+
const typeMatch = frontmatterSection.match(/^type:\s*(.+)$/m);
|
|
352
|
+
const noteType = typeMatch ? typeMatch[1].trim() : null;
|
|
353
|
+
|
|
317
354
|
for (const [key, value] of Object.entries(vars.frontmatter)) {
|
|
318
355
|
if (key === "tags") continue;
|
|
319
356
|
if (DANGEROUS_KEYS.has(key)) {
|
|
320
357
|
throw new Error(`Disallowed frontmatter key: "${key}"`);
|
|
321
358
|
}
|
|
359
|
+
if (noteType && typeof value === "string") {
|
|
360
|
+
validateFieldEnum(key, value, noteType);
|
|
361
|
+
}
|
|
322
362
|
if (typeof value === "string") {
|
|
323
363
|
if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(key)) {
|
|
324
364
|
throw new Error(`Invalid frontmatter key: "${key}". Keys must start with a letter and contain only letters, digits, hyphens, or underscores.`);
|
|
@@ -746,10 +786,7 @@ export function formatPeek(peekData, { redirected = false } = {}) {
|
|
|
746
786
|
const parts = [];
|
|
747
787
|
|
|
748
788
|
parts.push(`## File: ${filePath}`);
|
|
749
|
-
parts.push(`**Size:** ${sizeChars.toLocaleString()} chars, ${sizeLines.toLocaleString()} lines`);
|
|
750
|
-
if (totalChunks > 1) {
|
|
751
|
-
parts.push(`**Chunks:** ${totalChunks} (use \`chunk\` param to read by chunk)`);
|
|
752
|
-
}
|
|
789
|
+
parts.push(`**Size:** ${sizeChars.toLocaleString()} chars, ${sizeLines.toLocaleString()} lines, ${totalChunks} ${totalChunks === 1 ? "chunk" : "chunks"}`);
|
|
753
790
|
|
|
754
791
|
if (frontmatter) {
|
|
755
792
|
parts.push("");
|
|
@@ -764,7 +801,9 @@ export function formatPeek(peekData, { redirected = false } = {}) {
|
|
|
764
801
|
parts.push("");
|
|
765
802
|
parts.push("### Heading Outline");
|
|
766
803
|
for (const h of headings) {
|
|
767
|
-
|
|
804
|
+
const indent = " ".repeat(h.level - 1);
|
|
805
|
+
const title = h.heading.replace(/^#+\s*/, "");
|
|
806
|
+
parts.push(`${indent}${title} [${h.charCount} chars]`);
|
|
768
807
|
}
|
|
769
808
|
}
|
|
770
809
|
|
|
@@ -844,6 +883,11 @@ export function updateFrontmatter(content, fields) {
|
|
|
844
883
|
throw new Error("tags must be a non-empty array");
|
|
845
884
|
}
|
|
846
885
|
}
|
|
886
|
+
// Use the file's existing type (or the new type if being changed) for enum validation
|
|
887
|
+
const effectiveType = key === "type" ? undefined : (fields.type || parsed.type);
|
|
888
|
+
if (effectiveType) {
|
|
889
|
+
validateFieldEnum(key, value, String(effectiveType));
|
|
890
|
+
}
|
|
847
891
|
parsed[key] = value;
|
|
848
892
|
}
|
|
849
893
|
}
|
package/index.js
CHANGED
|
@@ -115,8 +115,8 @@ Pass custom <%...%> variables via the 'variables' parameter.`,
|
|
|
115
115
|
description: "Frontmatter fields to set (e.g., {tags: ['tag1', 'tag2'], status: 'active'})",
|
|
116
116
|
properties: {
|
|
117
117
|
tags: { type: "array", items: { type: "string" }, description: "Tags for the note (required)" },
|
|
118
|
-
status: { type: "string", description: "Note status" },
|
|
119
|
-
priority: { type: "string", description: "Priority level
|
|
118
|
+
status: { type: "string", description: "Note status. For tasks: pending, active, done, cancelled" },
|
|
119
|
+
priority: { type: "string", description: "Priority level. For tasks: low, normal, high, urgent" },
|
|
120
120
|
project: { type: "string", description: "Project name (for devlogs)" },
|
|
121
121
|
deciders: { type: "string", description: "Decision makers (for ADRs)" },
|
|
122
122
|
due: { type: "string", description: "Due date (for tasks)" },
|
|
@@ -157,7 +157,7 @@ Pass custom <%...%> variables via the 'variables' parameter.`,
|
|
|
157
157
|
},
|
|
158
158
|
{
|
|
159
159
|
name: "vault_update_frontmatter",
|
|
160
|
-
description: "Update YAML frontmatter fields in an existing note. Parses existing frontmatter, updates specified fields, preserves everything else. Set a field to null to remove it. Protected fields (type, created, tags) cannot be removed.",
|
|
160
|
+
description: "Update YAML frontmatter fields in an existing note. Parses existing frontmatter, updates specified fields, preserves everything else. Set a field to null to remove it. Protected fields (type, created, tags) cannot be removed. Field values are validated against the note's type (e.g. task status must be: pending, active, done, cancelled; task priority must be: low, normal, high, urgent).",
|
|
161
161
|
inputSchema: {
|
|
162
162
|
type: "object",
|
|
163
163
|
properties: {
|
package/package.json
CHANGED