Skip to content
This repository was archived by the owner on May 13, 2026. It is now read-only.
This repository was archived by the owner on May 13, 2026. It is now read-only.

[Bug] get_vault_file(format: "json") fails Zod validation on any note with array-valued frontmatter (aliases, tags, up, …) #81

Description

@folotp

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions