Skip to main content
Glama
jagoff

obsidian-mcp-complete

by jagoff

obsidian_query_notes

Read-only

Query your Obsidian notes using tags, folders, frontmatter, or links. Filter, sort, and limit results to find specific notes quickly.

Instructions

Structured Dataview-like query over local notes: tags, folders, linksTo, frontmatter equality/regex, sort, limit.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
vaultNoOptional configured vault name. Defaults to the server default vault.
tagsNo
foldersNo
linksToNo
frontmatterNo
sortByNopath
sortDirectionNoasc
limitNo
offsetNo

Implementation Reference

  • src/tools.ts:342-361 (registration)
    Registration of the 'obsidian_query_notes' tool with Zod schema (tags, folders, linksTo, frontmatter, sortBy, sortDirection, limit, offset) and handler that calls queryNotes from search.ts.
    tool(
      "obsidian_query_notes",
      "Structured Dataview-like query over local notes: tags, folders, linksTo, frontmatter equality/regex, sort, limit.",
      {
        vault: vaultArg,
        tags: z.array(z.string()).optional(),
        folders: z.array(z.string()).optional(),
        linksTo: z.string().optional(),
        frontmatter: z.record(z.unknown()).optional(),
        sortBy: z.enum(["path", "title", "mtime", "size"]).optional().default("path"),
        sortDirection: z.enum(["asc", "desc"]).optional().default("asc"),
        limit: z.number().int().min(1).max(500).optional().default(100),
        offset: z.number().int().min(0).optional().default(0),
      },
      async (args) => {
        const all = queryNotes(await loadNotes(vaults, args.vault), { ...args, limit: args.offset + args.limit });
        return { total: all.length, offset: args.offset, notes: all.slice(args.offset, args.offset + args.limit) };
      },
      { readOnlyHint: true },
    );
  • Core implementation of the queryNotes function that filters notes by tags, folders, linksTo, and frontmatter, then sorts and paginates results.
    export function queryNotes(
      notes: NoteRecord[],
      options: {
        tags?: string[];
        folders?: string[];
        linksTo?: string;
        frontmatter?: Record<string, unknown>;
        sortBy?: "path" | "title" | "mtime" | "size";
        sortDirection?: "asc" | "desc";
        limit: number;
      },
    ): Array<Omit<NoteRecord, "content"> & { links: string[] }> {
      const tags = (options.tags ?? []).map((t) => t.replace(/^#/, "").toLowerCase());
      const folders = (options.folders ?? []).map((f) => f.replace(/^\/+/, "").toLowerCase());
      const linksTo = options.linksTo?.replace(/\.md$/i, "").toLowerCase();
      const rows = notes
        .filter((note) => {
          if (tags.length > 0 && !tags.every((tag) => note.tags.some((t) => t.toLowerCase() === tag || t.toLowerCase().startsWith(`${tag}/`)))) return false;
          if (folders.length > 0 && !folders.some((folder) => note.path.toLowerCase().startsWith(folder))) return false;
          if (options.frontmatter) {
            for (const [key, expected] of Object.entries(options.frontmatter)) {
              if (!frontmatterMatches(note.frontmatter[key], expected)) return false;
            }
          }
          if (linksTo) {
            const links = extractWikiLinks(note.content).map((link) => link.target.replace(/\.md$/i, "").toLowerCase());
            if (!links.includes(linksTo) && !links.includes(noteStem(`${linksTo}.md`).toLowerCase())) return false;
          }
          return true;
        })
        .map((note) => {
          const { content: _content, ...rest } = note;
          return { ...rest, links: extractWikiLinks(note.content).map((link) => link.target) };
        });
      const sortBy = options.sortBy ?? "path";
      rows.sort((a, b) => {
        const av = a[sortBy];
        const bv = b[sortBy];
        const cmp = typeof av === "number" && typeof bv === "number" ? av - bv : String(av).localeCompare(String(bv));
        return options.sortDirection === "desc" ? -cmp : cmp;
      });
      return rows.slice(0, options.limit);
    }
  • The frontmatterMatches helper function used by queryNotes to compare frontmatter values (supports arrays, regex patterns with /pattern/ syntax, and case-insensitive string comparison).
    function frontmatterMatches(actual: unknown, expected: unknown): boolean {
      if (Array.isArray(actual)) return actual.some((value) => frontmatterMatches(value, expected));
      if (Array.isArray(expected)) return expected.some((value) => frontmatterMatches(actual, value));
      if (typeof expected === "string" && expected.startsWith("/") && expected.endsWith("/")) {
        return new RegExp(expected.slice(1, -1), "i").test(String(actual ?? ""));
      }
      return String(actual ?? "").toLowerCase() === String(expected ?? "").toLowerCase();
    }
  • The local 'tool' helper function used to register MCP tools with annotations, wrapping the server.tool call.
    export function registerObsidianTools(server: McpServer, vaults: VaultManager, config: ObsidianMcpConfig): void {
      vaults.onInvalidate = invalidateNotesCache;
      const pretty = config.pretty;
      const tool = <S extends ToolShape>(
        name: string,
        description: string,
        schema: S,
        handler: (args: z.objectOutputType<S, z.ZodTypeAny>) => Promise<unknown> | unknown,
        annotations: { readOnlyHint?: boolean; destructiveHint?: boolean; idempotentHint?: boolean } = {},
      ) => {
        (server.tool as any)(
          name,
          description,
          schema,
          {
            readOnlyHint: annotations.readOnlyHint ?? false,
            destructiveHint: annotations.destructiveHint ?? false,
            idempotentHint: annotations.idempotentHint ?? false,
            openWorldHint: false,
          },
          async (args: unknown) => jsonResult(await handler(args as z.objectOutputType<S, z.ZodTypeAny>), pretty),
        );
      };
  • The loadNotes function that reads all markdown files from the vault, used by the queryNotes handler to get data.
    export async function loadNotes(vaults: VaultManager, vault?: string | null): Promise<NoteRecord[]> {
      const vaultName = vaults.getVault(vault).name;
      const cached = notesCache.get(vaultName);
      if (cached && Date.now() - cached.timestamp < NOTES_CACHE_TTL) return cached.notes;
      const files = await vaults.markdownFiles(vault);
      const records: NoteRecord[] = [];
      for (const file of files) {
        const read = await vaults.readText(file, vault);
        const parsed = parseFrontmatter(read.text);
        records.push({
          path: read.path,
          title: titleFor(read.path, read.text, parsed.data),
          content: read.text,
          frontmatter: parsed.data,
          tags: extractAllTags(read.text),
          headings: extractHeadings(read.text),
          mtime: read.stat.mtime.toISOString(),
          size: read.stat.size,
        });
      }
      notesCache.set(vaultName, { notes: records, timestamp: Date.now() });
      return records;
    }
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true and destructiveHint=false, indicating a safe read operation. The description adds that it's a 'query', confirming non-destructive intent, but does not disclose important behaviors like whether it returns full note content or just metadata, nor does it mention pagination or rate limits. Still, annotation coverage lowers the burden.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single sentence of 15 words, front-loading the core concept 'structured Dataview-like query' and listing key parameters. No redundancy or wasted words.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Without an output schema, the description does not explain the return value structure (e.g., whether it returns note paths, full note bodies, or metadata). It also omits details on pagination and the meaning of 'frontmatter equality/regex'. For a tool with nested objects and 9 parameters, more context is needed.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is only 11%, so the description must compensate. It mentions 'tags, folders, linksTo, frontmatter equality/regex, sort, limit', covering most parameters but leaving out details like the format of frontmatter queries or how offset works. This is adequate but not thorough.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states it performs a 'Structured Dataview-like query' and lists key filter criteria (tags, folders, linksTo, frontmatter). This makes the tool's purpose distinct from write operations, but it doesn't explicitly differentiate from siblings like obsidian_search_dataview or obsidian_smart_search.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No guidance is provided on when to use this tool versus alternatives (e.g., full-text search via obsidian_search). The context signals include many sibling tools, but the description offers no when-to-use or when-not-to-use advice.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jagoff/obsidian-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server