pkm-mcp-server 1.1.0 → 1.2.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/CHANGELOG.md +20 -1
- package/README.md +3 -3
- package/handlers.js +16 -0
- package/helpers.js +45 -0
- package/index.js +37 -3
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.2.0] - 2026-03-17
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `vault_capture` tool — signal a PKM-worthy capture (decision, task, research, bug); returns immediately while a background hook creates the note
|
|
13
|
+
- PKM hook scripts for Claude Code integration:
|
|
14
|
+
- SessionStart hook (`session-start.js`) — loads project context from vault automatically
|
|
15
|
+
- PostToolUse hook (`capture-handler.sh`) — spawns Sonnet agent for explicit `vault_capture` calls
|
|
16
|
+
- Stop hook (`stop-sweep.sh`) — spawns Haiku agent for passive decision/task sweep at session end
|
|
17
|
+
- Enum validation for task `status` (pending/active/done/cancelled) and `priority` (low/normal/high/urgent) fields in `vault_write` and `vault_update_frontmatter` — non-task note types are unaffected
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- `sqlite-vec` upgraded from pinned alpha (0.1.7-alpha.2) to stable release (0.1.7)
|
|
21
|
+
- `@modelcontextprotocol/sdk` updated to 1.27.1
|
|
22
|
+
- `better-sqlite3` updated to 12.8.0
|
|
23
|
+
|
|
24
|
+
### Security
|
|
25
|
+
- Resolved 7 transitive dependency vulnerabilities (hono, @hono/node-server, ajv, express-rate-limit, flatted, minimatch, qs)
|
|
26
|
+
|
|
9
27
|
## [1.1.0] - 2026-02-23
|
|
10
28
|
|
|
11
29
|
### Changed
|
|
@@ -55,6 +73,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
55
73
|
- Atomic file creation in `vault_write` (`wx` flag) prevents race conditions
|
|
56
74
|
- Error messages sanitized to prevent leaking absolute vault paths
|
|
57
75
|
|
|
58
|
-
[Unreleased]: https://github.com/AdrianV101/Obsidian-MCP/compare/v1.
|
|
76
|
+
[Unreleased]: https://github.com/AdrianV101/Obsidian-MCP/compare/v1.2.0...HEAD
|
|
77
|
+
[1.2.0]: https://github.com/AdrianV101/Obsidian-MCP/compare/v1.1.0...v1.2.0
|
|
59
78
|
[1.1.0]: https://github.com/AdrianV101/Obsidian-MCP/compare/v1.0.0...v1.1.0
|
|
60
79
|
[1.0.0]: https://github.com/AdrianV101/Obsidian-MCP/releases/tag/v1.0.0
|
package/README.md
CHANGED
|
@@ -36,12 +36,12 @@ https://github.com/user-attachments/assets/58ad9c9b-d987-4728-89e7-33de20b73a38
|
|
|
36
36
|
| `vault_recent` | Recently modified files |
|
|
37
37
|
| `vault_links` | Wikilink analysis (incoming/outgoing) |
|
|
38
38
|
| `vault_neighborhood` | Graph exploration via BFS wikilink traversal |
|
|
39
|
-
| `vault_query` | Query notes by YAML frontmatter (type, status, tags, dates, custom fields, sorting) |
|
|
39
|
+
| `vault_query` | Query notes by YAML frontmatter (type, status, tags/tags_any, dates, custom fields, sorting) |
|
|
40
40
|
| `vault_tags` | Discover tags with counts; folder scoping, glob filters, inline tag parsing |
|
|
41
41
|
| `vault_activity` | Session activity log for cross-conversation memory |
|
|
42
42
|
| `vault_trash` | Soft-delete to `.trash/` (Obsidian convention), warns about broken incoming links |
|
|
43
43
|
| `vault_move` | Move/rename files with automatic wikilink updating across vault |
|
|
44
|
-
| `vault_update_frontmatter` | Atomic YAML frontmatter updates (set, create, remove fields) |
|
|
44
|
+
| `vault_update_frontmatter` | Atomic YAML frontmatter updates (set, create, remove fields; validates enum fields by note type) |
|
|
45
45
|
|
|
46
46
|
### Fuzzy Path Resolution
|
|
47
47
|
|
|
@@ -211,7 +211,7 @@ All paths passed to tools are relative to vault root. The server includes path s
|
|
|
211
211
|
|
|
212
212
|
## How It Works
|
|
213
213
|
|
|
214
|
-
**Note creation** is template-based. `vault_write` loads templates from `05-Templates/`, substitutes Templater-compatible variables (`<% tp.date.now("YYYY-MM-DD") %>`, `<% tp.file.title %>`), and validates required frontmatter fields (`type`, `created`, `tags`).
|
|
214
|
+
**Note creation** is template-based. `vault_write` loads templates from `05-Templates/`, substitutes Templater-compatible variables (`<% tp.date.now("YYYY-MM-DD") %>`, `<% tp.file.title %>`), and validates required frontmatter fields (`type`, `created`, `tags`). Optional frontmatter fields — `status`, `priority`, `project`, `deciders`, `due`, `source` — can be set per template type. Task notes enforce enum validation on `status` (pending/active/done/cancelled) and `priority` (low/normal/high/urgent).
|
|
215
215
|
|
|
216
216
|
**Semantic search** embeds notes on startup and watches for changes via `fs.watch`. Long notes are chunked by `##` headings. The index is a regenerable cache stored in `.obsidian/` so it syncs across machines via Obsidian Sync. The initial sync runs in the background — search is available immediately but may return incomplete results until sync finishes (a progress message is shown).
|
|
217
217
|
|
package/handlers.js
CHANGED
|
@@ -848,6 +848,21 @@ export async function createHandlers({ vaultPath, templateRegistry, semanticInde
|
|
|
848
848
|
};
|
|
849
849
|
}
|
|
850
850
|
|
|
851
|
+
async function handleCapture(args) {
|
|
852
|
+
const { type, title, content } = args;
|
|
853
|
+
if (!type || !title || !content) {
|
|
854
|
+
throw new Error(
|
|
855
|
+
`vault_capture requires type, title, and content. Got: type=${type || "(missing)"}, title=${title || "(missing)"}, content=${content ? "provided" : "(missing)"}`
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
return {
|
|
859
|
+
content: [{
|
|
860
|
+
type: "text",
|
|
861
|
+
text: `Capture queued: [${type}] ${title}`
|
|
862
|
+
}]
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
851
866
|
return new Map([
|
|
852
867
|
["vault_read", handleRead],
|
|
853
868
|
["vault_write", handleWrite],
|
|
@@ -867,5 +882,6 @@ export async function createHandlers({ vaultPath, templateRegistry, semanticInde
|
|
|
867
882
|
["vault_trash", handleTrash],
|
|
868
883
|
["vault_move", handleMove],
|
|
869
884
|
["vault_update_frontmatter", handleUpdateFrontmatter],
|
|
885
|
+
["vault_capture", handleCapture],
|
|
870
886
|
]);
|
|
871
887
|
}
|
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.`);
|
|
@@ -843,6 +883,11 @@ export function updateFrontmatter(content, fields) {
|
|
|
843
883
|
throw new Error("tags must be a non-empty array");
|
|
844
884
|
}
|
|
845
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
|
+
}
|
|
846
891
|
parsed[key] = value;
|
|
847
892
|
}
|
|
848
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: {
|
|
@@ -344,6 +344,40 @@ Pass custom <%...%> variables via the 'variables' parameter.`,
|
|
|
344
344
|
},
|
|
345
345
|
required: ["old_path", "new_path"]
|
|
346
346
|
}
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
name: "vault_capture",
|
|
350
|
+
description: "Signal that something is worth capturing in the PKM vault. " +
|
|
351
|
+
"Returns immediately — a background agent handles the actual note creation. " +
|
|
352
|
+
"Use this when you identify a decision, task, or research finding worth preserving.",
|
|
353
|
+
inputSchema: {
|
|
354
|
+
type: "object",
|
|
355
|
+
properties: {
|
|
356
|
+
type: {
|
|
357
|
+
type: "string",
|
|
358
|
+
enum: ["adr", "task", "research", "bug"],
|
|
359
|
+
description: "The type of capture: adr (decision), task, research (finding/pattern), bug (issue/fix)"
|
|
360
|
+
},
|
|
361
|
+
title: {
|
|
362
|
+
type: "string",
|
|
363
|
+
description: "Brief descriptive title (e.g., 'Use sqlite-vec over Chroma')"
|
|
364
|
+
},
|
|
365
|
+
content: {
|
|
366
|
+
type: "string",
|
|
367
|
+
description: "The substance of the capture — context, rationale, details. 1-5 sentences."
|
|
368
|
+
},
|
|
369
|
+
priority: {
|
|
370
|
+
type: "string",
|
|
371
|
+
enum: ["low", "normal", "high", "urgent"],
|
|
372
|
+
description: "Priority level (tasks only, default: normal)"
|
|
373
|
+
},
|
|
374
|
+
project: {
|
|
375
|
+
type: "string",
|
|
376
|
+
description: "Project name for vault routing (e.g., 'Obsidian-MCP'). If omitted, inferred from session context."
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
required: ["type", "title", "content"]
|
|
380
|
+
}
|
|
347
381
|
}
|
|
348
382
|
];
|
|
349
383
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pkm-mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "MCP server for Obsidian vault integration with Claude Code — 18 tools for notes, search, and graph traversal",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"exports": {
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
55
55
|
"better-sqlite3": "^12.6.2",
|
|
56
56
|
"js-yaml": "^4.1.0",
|
|
57
|
-
"sqlite-vec": "0.1.7
|
|
57
|
+
"sqlite-vec": "^0.1.7"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@eslint/js": "^10.0.1",
|