Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { FileSystemService } from "./src/filesystem.js";
import { FrontmatterHandler } from "./src/frontmatter.js";
import { FrontmatterHandler, parseFrontmatter } from "./src/frontmatter.js";
import { PathFilter } from "./src/pathfilter.js";
import { SearchService } from "./src/search.js";
import { readFileSync } from "fs";
Expand Down Expand Up @@ -482,10 +482,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
}

case "write_note": {
const fm = parseFrontmatter(trimmedArgs.frontmatter);
await fileSystem.writeNote({
path: trimmedArgs.path,
content: trimmedArgs.content,
frontmatter: trimmedArgs.frontmatter,
...(fm !== undefined && { frontmatter: fm }),
mode: trimmedArgs.mode || 'overwrite'
});
return {
Expand Down Expand Up @@ -624,9 +625,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
}

case "update_frontmatter": {
const fm = parseFrontmatter(trimmedArgs.frontmatter);
if (!fm) {
throw new Error('frontmatter is required');
}
await fileSystem.updateFrontmatter({
path: trimmedArgs.path,
frontmatter: trimmedArgs.frontmatter,
frontmatter: fm,
merge: trimmedArgs.merge
});
return {
Expand Down
45 changes: 43 additions & 2 deletions src/frontmatter.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { test, expect } from "vitest";
import { FrontmatterHandler } from "./frontmatter.js";
import { test, expect, describe } from "vitest";
import { FrontmatterHandler, parseFrontmatter } from "./frontmatter.js";

const handler = new FrontmatterHandler();

Expand Down Expand Up @@ -107,3 +107,44 @@ Some content here.`;
expect(result).toContain("tags:");
expect(result).toContain("# Content");
});

describe("parseFrontmatter", () => {
test("returns undefined for null and undefined", () => {
expect(parseFrontmatter(null)).toBeUndefined();
expect(parseFrontmatter(undefined)).toBeUndefined();
});

test("passes through a plain object", () => {
const obj = { tags: ["test"], title: "Hello" };
expect(parseFrontmatter(obj)).toBe(obj);
});

test("parses a JSON string into an object", () => {
const input = '{"tags": ["test"], "title": "Hello"}';
expect(parseFrontmatter(input)).toEqual({ tags: ["test"], title: "Hello" });
});

test("parses an empty JSON object string", () => {
expect(parseFrontmatter("{}")).toEqual({});
});

test("throws for a non-JSON string", () => {
expect(() => parseFrontmatter("not json")).toThrow("frontmatter must be a JSON object");
});

test("throws for a JSON array string", () => {
expect(() => parseFrontmatter('[1, 2, 3]')).toThrow("frontmatter must be a JSON object");
});

test("throws for a JSON primitive string", () => {
expect(() => parseFrontmatter('"just a string"')).toThrow("frontmatter must be a JSON object");
});

test("throws for an array value", () => {
expect(() => parseFrontmatter([1, 2, 3])).toThrow("frontmatter must be a JSON object");
});

test("throws for a number value", () => {
expect(() => parseFrontmatter(42)).toThrow("frontmatter must be a JSON object");
});
});
26 changes: 26 additions & 0 deletions src/frontmatter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,32 @@
import matter from 'gray-matter';
import type { ParsedNote, FrontmatterValidationResult } from './types.js';

/**
* Parse a frontmatter value that may be a JSON string (LLM clients sometimes
* pass frontmatter as a serialized JSON string instead of an object).
* Returns undefined if the value is null/undefined, or throws if invalid.
*/
export function parseFrontmatter(value: any): Record<string, any> | undefined {
if (value === undefined || value === null) {
return undefined;
}
if (typeof value === 'object' && !Array.isArray(value)) {
return value;
}
if (typeof value === 'string') {
try {
const parsed = JSON.parse(value);
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
return parsed;
}
} catch {
// not valid JSON
}
throw new Error('frontmatter must be a JSON object, got a string that is not valid JSON');
}
throw new Error(`frontmatter must be a JSON object, got ${typeof value}`);
}

export class FrontmatterHandler {
parse(content: string): ParsedNote {
try {
Expand Down