diff --git a/src/tools/file-operations.ts b/src/tools/file-operations.ts index f922f23..41821b0 100644 --- a/src/tools/file-operations.ts +++ b/src/tools/file-operations.ts @@ -28,22 +28,90 @@ export async function registerFileOperationTools(server: ObsidianMCPServer): Pro server.registerTool( 'obsidian_create_note', 'Create a new note in the Obsidian vault. Optionally specify path, content, template, and whether to overwrite existing notes or open after creation.', - { type: 'object', properties: {} }, + { + type: 'object', + properties: { + name: { + type: 'string', + description: 'File name for the new note', + }, + path: { + type: 'string', + description: 'Full file path (alternative to name)', + }, + content: { + type: 'string', + description: 'Initial content for the note (optional)', + }, + template: { + type: 'string', + description: 'Template name to use (optional)', + }, + overwrite: { + type: 'boolean', + description: 'Overwrite if file exists (optional)', + }, + open: { + type: 'boolean', + description: 'Open file after creating (optional)', + }, + newtab: { + type: 'boolean', + description: 'Open in new tab (optional)', + }, + }, + }, createToolHandler( 'Create a new note in the Obsidian vault', - { type: 'object', properties: {} }, + { + type: 'object', + properties: { + name: { + type: 'string', + description: 'File name for the new note', + }, + path: { + type: 'string', + description: 'Full file path (alternative to name)', + }, + content: { + type: 'string', + description: 'Initial content for the note (optional)', + }, + template: { + type: 'string', + description: 'Template name to use (optional)', + }, + overwrite: { + type: 'boolean', + description: 'Overwrite if file exists (optional)', + }, + open: { + type: 'boolean', + description: 'Open file after creating (optional)', + }, + newtab: { + type: 'boolean', + description: 'Open in new tab (optional)', + }, + }, + }, async (args) => { const validated = createNoteSchema.parse(args); const sanitized = sanitizeParameters(validated) as any; - const cmdArgs: string[] = ['create-note', sanitized.name as string]; - if (sanitized.path) cmdArgs.push('--path', sanitized.path as string); - if (sanitized.content) cmdArgs.push('--content', sanitized.content as string); - if (sanitized.template) cmdArgs.push('--template', sanitized.template as string); - if (sanitized.overwrite) cmdArgs.push('--overwrite'); - if (sanitized.open) cmdArgs.push('--open'); + const cmdArgs: string[] = []; + + // Add name or path parameter + if (sanitized.name) cmdArgs.push(`name=${sanitized.name as string}`); + if (sanitized.path) cmdArgs.push(`path=${sanitized.path as string}`); + if (sanitized.content) cmdArgs.push(`content=${sanitized.content as string}`); + if (sanitized.template) cmdArgs.push(`template=${sanitized.template as string}`); + if (sanitized.overwrite) cmdArgs.push('overwrite'); + if (sanitized.open) cmdArgs.push('open'); + if (sanitized.newtab) cmdArgs.push('newtab'); - const result = await executeObsidianCommand('note', cmdArgs); + const result = await executeObsidianCommand('create', cmdArgs); handleCLIResult(result, { operation: 'create_note', name: sanitized.name }); return { @@ -62,19 +130,44 @@ export async function registerFileOperationTools(server: ObsidianMCPServer): Pro server.registerTool( 'obsidian_read_note', 'Read the content of a note from the Obsidian vault. Specify either the note name (file) or full path (path).', - { type: 'object', properties: {} }, + { + type: 'object', + properties: { + file: { + type: 'string', + description: 'File name (resolves like wikilinks)', + }, + path: { + type: 'string', + description: 'Exact file path (folder/note.md)', + }, + }, + }, createToolHandler( 'Read the content of a note', - { type: 'object', properties: {} }, + { + type: 'object', + properties: { + file: { + type: 'string', + description: 'File name (resolves like wikilinks)', + }, + path: { + type: 'string', + description: 'Exact file path (folder/note.md)', + }, + }, + }, async (args) => { const validated = readNoteSchema.parse(args) as any; const sanitized = sanitizeParameters(validated) as any; - const identifier = sanitized.file || sanitized.path; - const cmdArgs: string[] = ['read-note', identifier as string]; + const cmdArgs: string[] = []; + if (sanitized.file) cmdArgs.push(`file=${sanitized.file as string}`); + if (sanitized.path) cmdArgs.push(`path=${sanitized.path as string}`); - const result = await executeObsidianCommand('note', cmdArgs); - handleCLIResult(result, { operation: 'read_note', identifier }); + const result = await executeObsidianCommand('read', cmdArgs); + handleCLIResult(result, { operation: 'read_note', identifier: sanitized.file || sanitized.path }); return { content: [ @@ -92,20 +185,64 @@ export async function registerFileOperationTools(server: ObsidianMCPServer): Pro server.registerTool( 'obsidian_append_to_note', 'Append content to the end of an existing note. Specify either file name or path, and the content to append. Use inline flag to append without a new line.', - { type: 'object', properties: {} }, + { + type: 'object', + required: ['content'], + properties: { + file: { + type: 'string', + description: 'File name (resolves like wikilinks)', + }, + path: { + type: 'string', + description: 'Exact file path (folder/note.md)', + }, + content: { + type: 'string', + description: 'Content to append (required)', + }, + inline: { + type: 'boolean', + description: 'Append without newline (optional)', + }, + }, + }, createToolHandler( 'Append content to the end of a note', - { type: 'object', properties: {} }, + { + type: 'object', + required: ['content'], + properties: { + file: { + type: 'string', + description: 'File name (resolves like wikilinks)', + }, + path: { + type: 'string', + description: 'Exact file path (folder/note.md)', + }, + content: { + type: 'string', + description: 'Content to append (required)', + }, + inline: { + type: 'boolean', + description: 'Append without newline (optional)', + }, + }, + }, async (args) => { const validated = appendPrependSchema.parse(args) as any; const sanitized = sanitizeParameters(validated) as any; - const identifier = sanitized.file || sanitized.path; - const cmdArgs: string[] = ['append', identifier as string, '--content', sanitized.content as string]; - if (sanitized.inline) cmdArgs.push('--inline'); + const cmdArgs: string[] = []; + if (sanitized.file) cmdArgs.push(`file=${sanitized.file as string}`); + if (sanitized.path) cmdArgs.push(`path=${sanitized.path as string}`); + cmdArgs.push(`content=${sanitized.content as string}`); + if (sanitized.inline) cmdArgs.push('inline'); - const result = await executeObsidianCommand('note', cmdArgs); - handleCLIResult(result, { operation: 'append', identifier }); + const result = await executeObsidianCommand('append', cmdArgs); + handleCLIResult(result, { operation: 'append', identifier: sanitized.file || sanitized.path }); return { content: [ @@ -119,23 +256,45 @@ export async function registerFileOperationTools(server: ObsidianMCPServer): Pro ) ); + // T035: Prepend to note tool server.registerTool( 'obsidian_prepend_to_note', - 'Prepend content to the beginning of an existing note. Specify either file name or path, and the content to prepend.', - { type: 'object', properties: {} }, + 'Prepend content to the beginning of an existing note. Specify either file name or path, and the content to prepend. Use inline flag to prepend without a new line.', + { + type: 'object', + required: ['content'], + properties: { + file: { type: 'string', description: 'File name (resolves like wikilinks)' }, + path: { type: 'string', description: 'Exact file path (folder/note.md)' }, + content: { type: 'string', description: 'Content to prepend (required)' }, + inline: { type: 'boolean', description: 'Prepend without newline (optional)' }, + }, + }, createToolHandler( 'Prepend content to the beginning of a note', - { type: 'object', properties: {} }, + { + type: 'object', + required: ['content'], + properties: { + file: { type: 'string', description: 'File name (resolves like wikilinks)' }, + path: { type: 'string', description: 'Exact file path (folder/note.md)' }, + content: { type: 'string', description: 'Content to prepend (required)' }, + inline: { type: 'boolean', description: 'Prepend without newline (optional)' }, + }, + }, async (args) => { const validated = appendPrependSchema.parse(args) as any; const sanitized = sanitizeParameters(validated) as any; - const identifier = sanitized.file || sanitized.path; - const cmdArgs: string[] = ['prepend', identifier as string, '--content', sanitized.content as string]; + const cmdArgs: string[] = []; + if (sanitized.file) cmdArgs.push(`file=${sanitized.file as string}`); + if (sanitized.path) cmdArgs.push(`path=${sanitized.path as string}`); + cmdArgs.push(`content=${sanitized.content as string}`); + if (sanitized.inline) cmdArgs.push('inline'); - const result = await executeObsidianCommand('note', cmdArgs); - handleCLIResult(result, { operation: 'prepend', identifier }); + const result = await executeObsidianCommand('prepend', cmdArgs); + handleCLIResult(result, { operation: 'prepend', identifier: sanitized.file || sanitized.path }); return { content: [ @@ -153,20 +312,35 @@ export async function registerFileOperationTools(server: ObsidianMCPServer): Pro server.registerTool( 'obsidian_delete_note', 'Delete a note from the Obsidian vault. By default moves to trash; use permanent flag for permanent deletion. Specify either file name or path.', - { type: 'object', properties: {} }, + { + type: 'object', + properties: { + file: { type: 'string', description: 'File name (resolves like wikilinks)' }, + path: { type: 'string', description: 'Exact file path (folder/note.md)' }, + permanent: { type: 'boolean', description: 'Skip trash, delete permanently (optional)' }, + }, + }, createToolHandler( 'Delete a note from the vault', - { type: 'object', properties: {} }, + { + type: 'object', + properties: { + file: { type: 'string', description: 'File name (resolves like wikilinks)' }, + path: { type: 'string', description: 'Exact file path (folder/note.md)' }, + permanent: { type: 'boolean', description: 'Skip trash, delete permanently (optional)' }, + }, + }, async (args) => { const validated = deleteNoteSchema.parse(args) as any; const sanitized = sanitizeParameters(validated) as any; - const identifier = sanitized.file || sanitized.path; - const cmdArgs: string[] = ['delete', identifier as string]; - if (sanitized.permanent) cmdArgs.push('--permanent'); + const cmdArgs: string[] = []; + if (sanitized.file) cmdArgs.push(`file=${sanitized.file as string}`); + if (sanitized.path) cmdArgs.push(`path=${sanitized.path as string}`); + if (sanitized.permanent) cmdArgs.push('permanent'); - const result = await executeObsidianCommand('note', cmdArgs); - handleCLIResult(result, { operation: 'delete', identifier }); + const result = await executeObsidianCommand('delete', cmdArgs); + handleCLIResult(result, { operation: 'delete', identifier: sanitized.file || sanitized.path }); return { content: [ @@ -183,21 +357,38 @@ export async function registerFileOperationTools(server: ObsidianMCPServer): Pro // T037: Move note tool server.registerTool( 'obsidian_move_note', - 'Move a note to a different location in the vault. Specify the current note (file or path) and the new path (newPath).', - { type: 'object', properties: {} }, + 'Move a note to a different location in the vault. Specify the current note (file or path) and the destination path (to).', + { + type: 'object', + required: ['to'], + properties: { + file: { type: 'string', description: 'File name (resolves like wikilinks)' }, + path: { type: 'string', description: 'Exact file path (folder/note.md)' }, + to: { type: 'string', description: 'Destination folder or path (required)' }, + }, + }, createToolHandler( 'Move a note to a different location', - { type: 'object', properties: {} }, + { + type: 'object', + required: ['to'], + properties: { + file: { type: 'string', description: 'File name (resolves like wikilinks)' }, + path: { type: 'string', description: 'Exact file path (folder/note.md)' }, + to: { type: 'string', description: 'Destination folder or path (required)' }, + }, + }, async (args) => { const validated = moveRenameSchema.parse(args) as any; const sanitized = sanitizeParameters(validated) as any; - const identifier = sanitized.file || sanitized.path; - const newLocation = sanitized.newPath || sanitized.newName; - const cmdArgs: string[] = ['move', identifier as string, newLocation as string]; + const cmdArgs: string[] = []; + if (sanitized.file) cmdArgs.push(`file=${sanitized.file as string}`); + if (sanitized.path) cmdArgs.push(`path=${sanitized.path as string}`); + cmdArgs.push(`to=${(sanitized.to || sanitized.newPath) as string}`); - const result = await executeObsidianCommand('note', cmdArgs); - handleCLIResult(result, { operation: 'move', identifier, newLocation }); + const result = await executeObsidianCommand('move', cmdArgs); + handleCLIResult(result, { operation: 'move', identifier: sanitized.file || sanitized.path, to: sanitized.to }); return { content: [ @@ -214,21 +405,38 @@ export async function registerFileOperationTools(server: ObsidianMCPServer): Pro // T038: Rename note tool server.registerTool( 'obsidian_rename_note', - 'Rename a note in the vault. Specify the current note (file or path) and the new name (newName).', - { type: 'object', properties: {} }, + 'Rename a note in the vault. Specify the current note (file or path) and the new name.', + { + type: 'object', + required: ['name'], + properties: { + file: { type: 'string', description: 'File name (resolves like wikilinks)' }, + path: { type: 'string', description: 'Exact file path (folder/note.md)' }, + name: { type: 'string', description: 'New file name (required)' }, + }, + }, createToolHandler( 'Rename a note', - { type: 'object', properties: {} }, + { + type: 'object', + required: ['name'], + properties: { + file: { type: 'string', description: 'File name (resolves like wikilinks)' }, + path: { type: 'string', description: 'Exact file path (folder/note.md)' }, + name: { type: 'string', description: 'New file name (required)' }, + }, + }, async (args) => { const validated = moveRenameSchema.parse(args) as any; const sanitized = sanitizeParameters(validated) as any; - const identifier = sanitized.file || sanitized.path; - const newName = sanitized.newName || sanitized.newPath; - const cmdArgs: string[] = ['rename', identifier as string, newName as string]; + const cmdArgs: string[] = []; + if (sanitized.file) cmdArgs.push(`file=${sanitized.file as string}`); + if (sanitized.path) cmdArgs.push(`path=${sanitized.path as string}`); + cmdArgs.push(`name=${(sanitized.name || sanitized.newName) as string}`); - const result = await executeObsidianCommand('note', cmdArgs); - handleCLIResult(result, { operation: 'rename', identifier, newName }); + const result = await executeObsidianCommand('rename', cmdArgs); + handleCLIResult(result, { operation: 'rename', identifier: sanitized.file || sanitized.path, name: sanitized.name }); return { content: [ @@ -246,20 +454,35 @@ export async function registerFileOperationTools(server: ObsidianMCPServer): Pro server.registerTool( 'obsidian_open_note', 'Open a note in the Obsidian application. Specify either file name or path. Use newtab flag to open in a new tab.', - { type: 'object', properties: {} }, + { + type: 'object', + properties: { + file: { type: 'string', description: 'File name (resolves like wikilinks)' }, + path: { type: 'string', description: 'Exact file path (folder/note.md)' }, + newtab: { type: 'boolean', description: 'Open in new tab (optional)' }, + }, + }, createToolHandler( 'Open a note in Obsidian', - { type: 'object', properties: {} }, + { + type: 'object', + properties: { + file: { type: 'string', description: 'File name (resolves like wikilinks)' }, + path: { type: 'string', description: 'Exact file path (folder/note.md)' }, + newtab: { type: 'boolean', description: 'Open in new tab (optional)' }, + }, + }, async (args) => { const validated = fileIdentifierSchema.parse(args) as any; const sanitized = sanitizeParameters(validated) as any; - const identifier = sanitized.file || sanitized.path; - const cmdArgs: string[] = ['open', identifier as string]; - if ((args as any).newtab) cmdArgs.push('--newtab'); + const cmdArgs: string[] = []; + if (sanitized.file) cmdArgs.push(`file=${sanitized.file as string}`); + if (sanitized.path) cmdArgs.push(`path=${sanitized.path as string}`); + if ((args as any).newtab) cmdArgs.push('newtab'); - const result = await executeObsidianCommand('note', cmdArgs); - handleCLIResult(result, { operation: 'open', identifier }); + const result = await executeObsidianCommand('open', cmdArgs); + handleCLIResult(result, { operation: 'open', identifier: sanitized.file || sanitized.path }); return { content: [ @@ -273,35 +496,8 @@ export async function registerFileOperationTools(server: ObsidianMCPServer): Pro ) ); - // T040: Get file info tool - server.registerTool( - 'obsidian_get_file_info', - 'Get metadata and information about a note file. Specify either file name or path. Returns file size, dates, path, and other metadata.', - { type: 'object', properties: {} }, - createToolHandler( - 'Get information about a note file', - { type: 'object', properties: {} }, - async (args) => { - const validated = fileIdentifierSchema.parse(args) as any; - const sanitized = sanitizeParameters(validated) as any; + // Note: obsidian_duplicate_note and obsidian_get_file_info were removed + // as they are not in the actual Obsidian CLI specification - const identifier = sanitized.file || sanitized.path; - const cmdArgs: string[] = ['info', identifier as string]; - - const result = await executeObsidianCommand('note', cmdArgs); - handleCLIResult(result, { operation: 'file_info', identifier }); - - return { - content: [ - { - type: 'text', - text: formatForMCP(result.stdout, 'text'), - }, - ], - }; - } - ) - ); - - logger.info('File operation tools registered', { count: 9 }); + logger.info('File operation tools registered', { count: 8 }); }