Summary
Calling get_vault_file(filename, format: "json") on any note whose frontmatter contains a list (YAML sequence) under keys like aliases, tags, up, down, same, next, previous, backlog, cssclasses, etc. fails with a Zod validation error:
frontmatter.aliases must be a string (was an object)
This makes format: "json" unusable on any vault that follows standard Obsidian Flavored Markdown (OFM) conventions, where these fields are routinely arrays.
Reproduction
Minimal note example.md:
---
title: Example
aliases:
- Example
- ex
tags:
- topic/example
- source/user
up:
- "[[Parent Note]]"
---
body text
Call:
await mcp.call("get_vault_file", {
filename: "example.md",
format: "json"
});
Observed:
MCP error: arguments.frontmatter.aliases must be a string (was an object)
(The error says "was an object" because Zod categorises arrays as object in its default diagnostic — the actual YAML value is an array.)
Reproducible on every note with at least one list-valued frontmatter key. format: "markdown" (default) works fine; format: "json" is the only failing mode.
Root cause (best guess from the error message)
The Zod schema used to validate the NoteJson response from Local REST API appears to type frontmatter field values as z.string(), whereas YAML (and OFM) allow string | string[] | number | boolean | null | object. The plugin serialises arrays faithfully; the wrapper rejects them at the validation boundary before they reach the caller.
Local REST API itself is fine — a direct curl with Accept: application/vnd.olrapi.note+json returns the arrays intact.
Suggested fix
Relax the schema for each known multi-value frontmatter field to accept either a string or an array of strings, and fall back to z.unknown() (or z.any()) for arbitrary keys:
const Scalar = z.union([z.string(), z.number(), z.boolean(), z.null()]);
const FrontmatterValue = z.union([Scalar, z.array(Scalar), z.unknown()]);
const Frontmatter = z.record(z.string(), FrontmatterValue);
Or, even simpler, treat frontmatter as opaque:
const Frontmatter = z.record(z.string(), z.unknown());
Downstream consumers (including LLM agents) are well-equipped to handle dynamic frontmatter shapes; strict typing at the wrapper level costs more than it buys.
Impact
On any vault that uses OFM conventions (multi-value tags / aliases / hierarchical links), the MCP client currently has no way to get a structured projection of a note — the only json mode throws, and markdown returns the full body, which defeats the purpose when the caller only needs the frontmatter.
Related
A separate issue, see #82, tracks the broader request to expose heading/block/frontmatter/document-map partial reads — this fix is complementary: once Zod stops rejecting valid payloads, format: "json" becomes a viable shape for the "partial read of frontmatter" use case.
Environment
obsidian-mcp-tools 0.2.27
obsidian-local-rest-api 3.6.1
- Obsidian 1.7.x on macOS
- Consumed via Claude Code / Cowork
Summary
Calling
get_vault_file(filename, format: "json")on any note whose frontmatter contains a list (YAML sequence) under keys likealiases,tags,up,down,same,next,previous,backlog,cssclasses, etc. fails with a Zod validation error:This makes
format: "json"unusable on any vault that follows standard Obsidian Flavored Markdown (OFM) conventions, where these fields are routinely arrays.Reproduction
Minimal note
example.md:Call:
Observed:
(The error says "was an object" because Zod categorises arrays as
objectin its default diagnostic — the actual YAML value is an array.)Reproducible on every note with at least one list-valued frontmatter key.
format: "markdown"(default) works fine;format: "json"is the only failing mode.Root cause (best guess from the error message)
The Zod schema used to validate the
NoteJsonresponse from Local REST API appears to type frontmatter field values asz.string(), whereas YAML (and OFM) allowstring | string[] | number | boolean | null | object. The plugin serialises arrays faithfully; the wrapper rejects them at the validation boundary before they reach the caller.Local REST API itself is fine — a direct
curlwithAccept: application/vnd.olrapi.note+jsonreturns the arrays intact.Suggested fix
Relax the schema for each known multi-value frontmatter field to accept either a string or an array of strings, and fall back to
z.unknown()(orz.any()) for arbitrary keys:Or, even simpler, treat frontmatter as opaque:
Downstream consumers (including LLM agents) are well-equipped to handle dynamic frontmatter shapes; strict typing at the wrapper level costs more than it buys.
Impact
On any vault that uses OFM conventions (multi-value tags / aliases / hierarchical links), the MCP client currently has no way to get a structured projection of a note — the only
jsonmode throws, andmarkdownreturns the full body, which defeats the purpose when the caller only needs the frontmatter.Related
A separate issue, see #82, tracks the broader request to expose
heading/block/frontmatter/document-mappartial reads — this fix is complementary: once Zod stops rejecting valid payloads,format: "json"becomes a viable shape for the "partial read of frontmatter" use case.Environment
obsidian-mcp-tools0.2.27obsidian-local-rest-api3.6.1