/**
* Vault File Tools - CRUD operations for files in Obsidian vault
*/
import { z } from 'zod';
import { makeRequest, encodePath } from '../utils/api-client.js';
import {
NoteJson,
DocumentMap,
NOTE_JSON_CONTENT_TYPE,
DOCUMENT_MAP_CONTENT_TYPE,
PatchOperation,
PatchTargetType,
} from '../types/obsidian.js';
// Schema definitions for tool parameters
export const getFileSchema = z.object({
path: z.string().describe('Path to the file relative to vault root (e.g., "Notes/MyNote.md")'),
format: z.enum(['markdown', 'json', 'document-map']).optional()
.describe('Response format: "markdown" (default, raw content), "json" (parsed metadata), or "document-map" (headings, blocks, frontmatter fields)'),
});
export const createFileSchema = z.object({
path: z.string().describe('Path to the file relative to vault root (e.g., "Notes/NewNote.md")'),
content: z.string().describe('Content to write to the file'),
contentType: z.enum(['markdown', 'plain']).optional()
.describe('Content type: "markdown" (default) or "plain" for plain text'),
});
export const appendToFileSchema = z.object({
path: z.string().describe('Path to the file relative to vault root (e.g., "Notes/MyNote.md")'),
content: z.string().describe('Content to append to the file'),
});
export const patchFileSchema = z.object({
path: z.string().describe('Path to the file relative to vault root (e.g., "Notes/MyNote.md")'),
operation: z.enum(['append', 'prepend', 'replace']).describe('Operation to perform: append, prepend, or replace'),
targetType: z.enum(['heading', 'block', 'frontmatter']).describe('Type of target: heading, block, or frontmatter'),
target: z.string().describe('Target identifier (heading path with :: delimiter, block ID, or frontmatter field name)'),
content: z.string().describe('Content to insert'),
targetDelimiter: z.string().optional().describe('Delimiter for nested headings (default: "::")'),
trimTargetWhitespace: z.boolean().optional().describe('Trim whitespace from target before applying patch (default: false)'),
contentType: z.enum(['markdown', 'json']).optional()
.describe('Content type for the patch body (default: "markdown")'),
});
export const deleteFileSchema = z.object({
path: z.string().describe('Path to the file relative to vault root (e.g., "Notes/MyNote.md")'),
});
/**
* Get a file from the vault
*/
export async function getFile(args: z.infer<typeof getFileSchema>): Promise<string> {
const { path, format = 'markdown' } = args;
const encodedPath = encodePath(path);
let acceptHeader: string | undefined;
if (format === 'json') {
acceptHeader = NOTE_JSON_CONTENT_TYPE;
} else if (format === 'document-map') {
acceptHeader = DOCUMENT_MAP_CONTENT_TYPE;
}
const response = await makeRequest<string | NoteJson | DocumentMap>({
method: 'GET',
path: `/vault/${encodedPath}`,
accept: acceptHeader,
});
if (!response.success) {
return `Error: ${response.error || `Failed to get file (HTTP ${response.status})`}`;
}
if (typeof response.data === 'string') {
return response.data;
}
return JSON.stringify(response.data, null, 2);
}
/**
* Create or completely replace a file in the vault
*/
export async function createFile(args: z.infer<typeof createFileSchema>): Promise<string> {
const { path, content, contentType = 'markdown' } = args;
const encodedPath = encodePath(path);
const response = await makeRequest({
method: 'PUT',
path: `/vault/${encodedPath}`,
body: content,
contentType: contentType === 'markdown' ? 'text/markdown' : 'text/plain',
});
if (!response.success) {
return `Error: ${response.error || `Failed to create file (HTTP ${response.status})`}`;
}
return `Successfully created/updated file: ${path}`;
}
/**
* Append content to a file (creates if doesn't exist)
*/
export async function appendToFile(args: z.infer<typeof appendToFileSchema>): Promise<string> {
const { path, content } = args;
const encodedPath = encodePath(path);
const response = await makeRequest({
method: 'POST',
path: `/vault/${encodedPath}`,
body: content,
contentType: 'text/markdown',
});
if (!response.success) {
return `Error: ${response.error || `Failed to append to file (HTTP ${response.status})`}`;
}
return `Successfully appended content to file: ${path}`;
}
/**
* Partially update a file (heading/block/frontmatter operations)
*/
export async function patchFile(args: z.infer<typeof patchFileSchema>): Promise<string> {
const {
path,
operation,
targetType,
target,
content,
targetDelimiter,
trimTargetWhitespace,
contentType = 'markdown',
} = args;
const encodedPath = encodePath(path);
const headers: Record<string, string> = {
'Operation': operation,
'Target-Type': targetType,
'Target': target,
};
if (targetDelimiter) {
headers['Target-Delimiter'] = targetDelimiter;
}
if (trimTargetWhitespace !== undefined) {
headers['Trim-Target-Whitespace'] = trimTargetWhitespace ? 'true' : 'false';
}
const response = await makeRequest({
method: 'PATCH',
path: `/vault/${encodedPath}`,
body: content,
contentType: contentType === 'json' ? 'application/json' : 'text/markdown',
headers,
});
if (!response.success) {
return `Error: ${response.error || `Failed to patch file (HTTP ${response.status})`}`;
}
return `Successfully patched file: ${path} (${operation} ${targetType} "${target}")`;
}
/**
* Delete a file from the vault
*/
export async function deleteFile(args: z.infer<typeof deleteFileSchema>): Promise<string> {
const { path } = args;
const encodedPath = encodePath(path);
const response = await makeRequest({
method: 'DELETE',
path: `/vault/${encodedPath}`,
});
if (!response.success) {
return `Error: ${response.error || `Failed to delete file (HTTP ${response.status})`}`;
}
return `Successfully deleted file: ${path}`;
}
// Tool definitions for MCP server registration
export const vaultFileTools = [
{
name: 'vault_get_file',
description: 'Read and retrieve the content of any file from the Obsidian vault. Use this tool to READ, VIEW, or GET file contents. Returns raw markdown by default, or optionally returns parsed JSON with metadata (tags, frontmatter, stats), or a document map showing headings, blocks, and frontmatter fields. PREREQUISITE: Use vault_list first to discover file paths. NEXT STEPS: After reading, use vault_create_file to overwrite, vault_append_to_file to add content, or vault_patch_file to modify specific sections. Workflow: vault_list → vault_get_file → (vault_create_file / vault_append_to_file / vault_patch_file).',
inputSchema: getFileSchema,
handler: getFile,
},
{
name: 'vault_create_file',
description: 'CREATE or WRITE a new file in the vault, or completely REPLACE an existing file\'s content. Use this tool when you need to CREATE a new note, WRITE initial content to a file, or OVERWRITE the entire contents of an existing file. This is the primary tool for creating new files - use it whenever you need to write a complete file from scratch. PREREQUISITE: Use vault_list to find the correct directory location. IMPORTANT: This OVERWRITES existing files - use vault_append_to_file to add content instead. Workflow: vault_list → vault_create_file.',
inputSchema: createFileSchema,
handler: createFile,
},
{
name: 'vault_append_to_file',
description: 'ADD or APPEND content to the end of an existing file. If the file doesn\'t exist, it will be created automatically. Use this tool when you need to ADD content to the end of a file without modifying existing content. Perfect for adding new entries, notes, or content to the bottom of an existing file. RECOMMENDED: Use vault_get_file first to see current content. NOTE: Appends to END of file - use vault_patch_file to modify specific sections in the middle. Workflow: vault_get_file → vault_append_to_file.',
inputSchema: appendToFileSchema,
handler: appendToFile,
},
{
name: 'vault_patch_file',
description: 'UPDATE, MODIFY, or EDIT a specific section of a file by inserting content relative to a heading, block reference, or frontmatter field. Operations include: append (after target), prepend (before target), or replace (target content). Use this tool when you need to UPDATE specific sections without rewriting the entire file. Ideal for modifying headings, updating frontmatter fields, or inserting content at specific locations. PREREQUISITE: Use vault_get_file with format="document-map" to identify headings, blocks, or frontmatter field names. Workflow: vault_get_file (document-map) → vault_patch_file.',
inputSchema: patchFileSchema,
handler: patchFile,
},
{
name: 'vault_delete_file',
description: 'DELETE or REMOVE a file from the Obsidian vault. WARNING: This operation cannot be undone. Use this tool when you need to DELETE a file permanently from the vault. PREREQUISITE: Use vault_list to confirm the file exists and verify the correct file path. WARNING: Cannot be undone - verify path carefully! Workflow: vault_list → vault_delete_file.',
inputSchema: deleteFileSchema,
handler: deleteFile,
},
];