pkm-mcp-server 1.1.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.
Files changed (3) hide show
  1. package/helpers.js +45 -0
  2. package/index.js +3 -3
  3. package/package.json +1 -1
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 (for tasks)" },
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pkm-mcp-server",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
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": {