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.
97 lines • 5 kB
JavaScript
/**
* @fileoverview obsidian_delete_note — permanently delete a note. Calls
* `ctx.elicit` to confirm with the user when the client supports it; falls
* back to the destructive-hint annotation otherwise.
* @module mcp-server/tools/definitions/obsidian-delete-note.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 { TargetSchema } from './_shared/schemas.js';
export const obsidianDeleteNote = tool('obsidian_delete_note', {
description: 'Permanently delete a note from the vault. Confirms with the user before deleting when the client supports interactive confirmation. Recovery requires the local trash in Obsidian — there is no API-level undo.',
annotations: { destructiveHint: true },
input: z.object({
target: TargetSchema.describe('Which note to delete.'),
}),
output: z.object({
path: z.string().describe('Resolved vault-relative path of the deleted note.'),
deleted: z.boolean().describe('True when the file was removed.'),
previousSizeInBytes: z
.number()
.describe('Byte size of the note immediately before deletion. The destructive blast radius — useful for confirming the agent destroyed what it expected.'),
currentSizeInBytes: z
.number()
.describe('Always 0 after a successful delete — the file no longer exists.'),
}),
auth: ['tool:obsidian_delete_note:write'],
errors: [
{
reason: 'path_forbidden',
code: JsonRpcErrorCode.Forbidden,
when: 'The target path is outside OBSIDIAN_WRITE_PATHS, or OBSIDIAN_READ_ONLY=true denies all writes.',
recovery: 'Use a path inside the configured write scope. The error data echoes the active scope.',
},
{
reason: 'cancelled',
code: JsonRpcErrorCode.InvalidRequest,
when: 'User declined the deletion via interactive elicitation.',
recovery: 'Re-run the tool when the user is ready to confirm deletion.',
},
{
reason: 'note_missing',
code: JsonRpcErrorCode.NotFound,
when: 'The vault path does not resolve to an existing note.',
recovery: 'Verify the path with obsidian_list_notes or use obsidian_search_notes to locate the note.',
},
{
reason: 'no_active_file',
code: JsonRpcErrorCode.NotFound,
when: 'Target was `active` but no file is currently open in Obsidian.',
recovery: 'Call obsidian_open_in_ui to focus a file, or pass an explicit path target instead.',
},
{
reason: 'periodic_not_found',
code: JsonRpcErrorCode.NotFound,
when: 'Target was `periodic` but no matching periodic note exists.',
recovery: 'Pass an explicit path target — periodic notes must already exist.',
},
{
reason: 'periodic_disabled',
code: JsonRpcErrorCode.ValidationError,
when: "Target was `periodic` but the requested period is not enabled in Obsidian's Periodic Notes plugin settings.",
recovery: "Pass an explicit path target — the requested period is disabled in the operator's Periodic Notes plugin.",
},
],
async handler(input, ctx) {
const svc = getObsidianService();
const path = await svc.resolvePath(ctx, input.target);
const pathTarget = { type: 'path', path };
/**
* Probe size before showing the elicitation prompt so the user sees how
* much they're about to destroy. Throws `note_missing` if the file is
* already gone — preempts a confusing post-elicit DELETE 404.
*/
const previousSizeInBytes = await svc.getSize(ctx, pathTarget);
if (ctx.elicit) {
const confirmed = await ctx.elicit(`Permanently delete '${path}' (${previousSizeInBytes} bytes)? This cannot be undone via the API; recovery would require Obsidian's local trash.`, z.object({
confirm: z.boolean().describe('Set to true to delete the note. Any other value cancels.'),
}));
if (confirmed.action !== 'accept' || confirmed.content?.confirm !== true) {
throw ctx.fail('cancelled', 'Deletion cancelled by user.', {
path,
...ctx.recoveryFor('cancelled'),
});
}
}
await svc.deleteNote(ctx, pathTarget);
return { path, deleted: true, previousSizeInBytes, currentSizeInBytes: 0 };
},
format: (result) => [
{
type: 'text',
text: `**Deleted ${result.path}** (size: ${result.previousSizeInBytes} → ${result.currentSizeInBytes} bytes)`,
},
],
});
//# sourceMappingURL=obsidian-delete-note.tool.js.map