UNPKG

obsidian-mcp-server

Version:

MCP server for Obsidian vaults — read, write, search, and surgically edit notes, tags, and frontmatter via the Local REST API plugin. STDIO or Streamable HTTP.

91 lines 4.72 kB
/** * @fileoverview obsidian_list_commands — list Obsidian command-palette commands. * Gated behind `OBSIDIAN_ENABLE_COMMANDS=true` alongside `obsidian_execute_command`. * Optional `nameRegex` post-filters the upstream payload before returning, since * vaults with Templater/Dataview/QuickAdd/Excalidraw routinely register 300+ * commands and the LLM-bound listing is otherwise mostly noise. * @module mcp-server/tools/definitions/obsidian-list-commands.tool */ import { tool, z } from '@cyanheads/mcp-ts-core'; import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors'; import { getObsidianService } from '../../../services/obsidian/obsidian-service.js'; import { nameRegexSafetyIssue } from './_shared/regex-safety.js'; export const obsidianListCommands = tool('obsidian_list_commands', { description: 'List the Obsidian command-palette commands available in the active vault, with their IDs and display names. Filter to a subset with the optional `nameRegex` matched against the display name.', annotations: { readOnlyHint: true, idempotentHint: true }, input: z.object({ nameRegex: z .string() .optional() .describe('Optional ECMAScript regex (no flags, ≤256 chars, no nested quantifiers like `(a+)+`) matched against the command display name (the field agents search by, not the slug-shaped `id`). Use character classes (`[Tt]emplater`) for case-insensitivity.'), }), output: z.object({ commands: z .array(z .object({ id: z.string().describe('Command ID — the slug used to invoke the command.'), name: z.string().describe('Display name of the command.'), }) .describe('An Obsidian command-palette entry.')) .describe('All commands registered in the active Obsidian instance.'), appliedFilters: z .object({ nameRegex: z.string().optional().describe('nameRegex filter applied to this listing.'), }) .optional() .describe('Active filters that produced this listing. Absent when no filter was applied.'), }), auth: ['tool:obsidian_list_commands:read'], errors: [ { reason: 'regex_invalid', code: JsonRpcErrorCode.ValidationError, when: 'The supplied `nameRegex` is not a valid ECMAScript regex.', recovery: 'Use a valid ECMAScript regex (e.g. `^Templater`), or omit nameRegex to disable filtering.', }, { reason: 'regex_unsafe', code: JsonRpcErrorCode.ValidationError, when: 'The supplied `nameRegex` is well-formed but exceeds the 256-character limit or contains nested quantifiers known to cause catastrophic backtracking.', recovery: 'Avoid nested quantifiers like `(a+)+` or `(.*)*`. Use a simpler pattern (e.g. `^Templater`), or omit nameRegex to disable filtering.', }, ], async handler(input, ctx) { let regex; if (input.nameRegex) { const safetyIssue = nameRegexSafetyIssue(input.nameRegex); if (safetyIssue) { throw ctx.fail('regex_unsafe', `Unsafe nameRegex: ${safetyIssue}`, { nameRegex: input.nameRegex, ...ctx.recoveryFor('regex_unsafe'), }); } try { regex = new RegExp(input.nameRegex); } catch (err) { throw ctx.fail('regex_invalid', `Invalid nameRegex: ${err.message}`, { nameRegex: input.nameRegex, ...ctx.recoveryFor('regex_invalid') }, { cause: err }); } } const svc = getObsidianService(); const commands = await svc.listCommands(ctx); const filtered = regex ? commands.filter((c) => regex.test(c.name)) : commands; return { commands: filtered, ...(input.nameRegex ? { appliedFilters: { nameRegex: input.nameRegex } } : {}), }; }, format: (result) => { const activeRegex = result.appliedFilters?.nameRegex; if (result.commands.length === 0) { const filterNote = activeRegex ? ` matching \`${activeRegex}\`` : ''; return [{ type: 'text', text: `_No commands available${filterNote}._` }]; } const filterSuffix = activeRegex ? ` · nameRegex=\`${activeRegex}\`` : ''; const lines = [`**${result.commands.length} commands**${filterSuffix}`, '']; for (const c of result.commands) lines.push(`- \`${c.id}\` — ${c.name}`); return [{ type: 'text', text: lines.join('\n') }]; }, }); //# sourceMappingURL=obsidian-list-commands.tool.js.map