feat: add proper input schemas to all file operation tools

Fixed all file operation tools to match Obsidian CLI specification
with complete inputSchema definitions for tools/list exposure.

Changes per tool (verified via 'obsidian help <command>'):

1. obsidian_create_note:  Already fixed (name, path, content, template, overwrite, open, newtab)
2. obsidian_read_note:  Already fixed (file, path)
3. obsidian_append_to_note:  Already fixed (file, path, content, inline)
4. obsidian_prepend_to_note: Fixed - Added full schema (file, path, content, inline)
5. obsidian_delete_note: Fixed - Added full schema (file, path, permanent)
6. obsidian_move_note: Fixed - Added full schema (file, path, to)
7. obsidian_rename_note: Fixed - Added full schema (file, path, name)
8. obsidian_open_note: Fixed - Added full schema (file, path, newtab)

Command execution fixes:
- Changed all executeObsidianCommand('note', ...) to proper commands
  ('create', 'read', 'append', 'prepend', 'delete', 'move', 'rename', 'open')
- Changed parameter format from '--flag value' to 'param=value'
  (matches actual Obsidian CLI syntax)
- Removed identifier concatenation, now builds params properly:
  Before: ['command', identifier, '--flag', value]
  After: ['file=name'] or ['path=folder/note.md']

Removed tools:
- obsidian_duplicate_note: Not in Obsidian CLI spec
- obsidian_get_file_info: Not in Obsidian CLI (use 'file' command separately if needed)

Tool count reduced from 9 to 8 (removed non-existent commands).

All 8 file operation tools now have:
 Complete inputSchema with all parameters documented
 Correct command names matching Obsidian CLI
 Proper param=value format for CLI execution
 Required fields marked appropriately

Files changed:
- src/tools/file-operations.ts

Build:  0 errors
Impact: tools/list now returns complete schemas for all file ops

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-03-22 13:01:09 -05:00
parent 4181ef0b57
commit d556183388

View File

@@ -28,22 +28,90 @@ export async function registerFileOperationTools(server: ObsidianMCPServer): Pro
server.registerTool( server.registerTool(
'obsidian_create_note', '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.', '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( createToolHandler(
'Create a new note in the Obsidian vault', '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) => { async (args) => {
const validated = createNoteSchema.parse(args); const validated = createNoteSchema.parse(args);
const sanitized = sanitizeParameters(validated) as any; const sanitized = sanitizeParameters(validated) as any;
const cmdArgs: string[] = ['create-note', sanitized.name as string]; const cmdArgs: string[] = [];
if (sanitized.path) cmdArgs.push('--path', sanitized.path as string);
if (sanitized.content) cmdArgs.push('--content', sanitized.content as string); // Add name or path parameter
if (sanitized.template) cmdArgs.push('--template', sanitized.template as string); if (sanitized.name) cmdArgs.push(`name=${sanitized.name as string}`);
if (sanitized.overwrite) cmdArgs.push('--overwrite'); if (sanitized.path) cmdArgs.push(`path=${sanitized.path as string}`);
if (sanitized.open) cmdArgs.push('--open'); 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 }); handleCLIResult(result, { operation: 'create_note', name: sanitized.name });
return { return {
@@ -62,19 +130,44 @@ export async function registerFileOperationTools(server: ObsidianMCPServer): Pro
server.registerTool( server.registerTool(
'obsidian_read_note', 'obsidian_read_note',
'Read the content of a note from the Obsidian vault. Specify either the note name (file) or full path (path).', '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( createToolHandler(
'Read the content of a note', '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) => { async (args) => {
const validated = readNoteSchema.parse(args) as any; const validated = readNoteSchema.parse(args) as any;
const sanitized = sanitizeParameters(validated) as any; const sanitized = sanitizeParameters(validated) as any;
const identifier = sanitized.file || sanitized.path; const cmdArgs: string[] = [];
const cmdArgs: string[] = ['read-note', identifier as 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); const result = await executeObsidianCommand('read', cmdArgs);
handleCLIResult(result, { operation: 'read_note', identifier }); handleCLIResult(result, { operation: 'read_note', identifier: sanitized.file || sanitized.path });
return { return {
content: [ content: [
@@ -92,20 +185,64 @@ export async function registerFileOperationTools(server: ObsidianMCPServer): Pro
server.registerTool( server.registerTool(
'obsidian_append_to_note', '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.', '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( createToolHandler(
'Append content to the end of a note', '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) => { async (args) => {
const validated = appendPrependSchema.parse(args) as any; const validated = appendPrependSchema.parse(args) as any;
const sanitized = sanitizeParameters(validated) as any; const sanitized = sanitizeParameters(validated) as any;
const identifier = sanitized.file || sanitized.path; const cmdArgs: string[] = [];
const cmdArgs: string[] = ['append', identifier as string, '--content', sanitized.content as string]; if (sanitized.file) cmdArgs.push(`file=${sanitized.file as string}`);
if (sanitized.inline) cmdArgs.push('--inline'); 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); const result = await executeObsidianCommand('append', cmdArgs);
handleCLIResult(result, { operation: 'append', identifier }); handleCLIResult(result, { operation: 'append', identifier: sanitized.file || sanitized.path });
return { return {
content: [ content: [
@@ -119,23 +256,45 @@ export async function registerFileOperationTools(server: ObsidianMCPServer): Pro
) )
); );
// T035: Prepend to note tool // T035: Prepend to note tool
server.registerTool( server.registerTool(
'obsidian_prepend_to_note', 'obsidian_prepend_to_note',
'Prepend content to the beginning of an existing note. Specify either file name or path, and the content to prepend.', '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', 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)' },
},
},
createToolHandler( createToolHandler(
'Prepend content to the beginning of a note', '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) => { async (args) => {
const validated = appendPrependSchema.parse(args) as any; const validated = appendPrependSchema.parse(args) as any;
const sanitized = sanitizeParameters(validated) as any; const sanitized = sanitizeParameters(validated) as any;
const identifier = sanitized.file || sanitized.path; const cmdArgs: string[] = [];
const cmdArgs: string[] = ['prepend', identifier as string, '--content', sanitized.content as 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); const result = await executeObsidianCommand('prepend', cmdArgs);
handleCLIResult(result, { operation: 'prepend', identifier }); handleCLIResult(result, { operation: 'prepend', identifier: sanitized.file || sanitized.path });
return { return {
content: [ content: [
@@ -153,20 +312,35 @@ export async function registerFileOperationTools(server: ObsidianMCPServer): Pro
server.registerTool( server.registerTool(
'obsidian_delete_note', '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.', '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( createToolHandler(
'Delete a note from the vault', '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) => { async (args) => {
const validated = deleteNoteSchema.parse(args) as any; const validated = deleteNoteSchema.parse(args) as any;
const sanitized = sanitizeParameters(validated) as any; const sanitized = sanitizeParameters(validated) as any;
const identifier = sanitized.file || sanitized.path; const cmdArgs: string[] = [];
const cmdArgs: string[] = ['delete', identifier as string]; if (sanitized.file) cmdArgs.push(`file=${sanitized.file as string}`);
if (sanitized.permanent) cmdArgs.push('--permanent'); if (sanitized.path) cmdArgs.push(`path=${sanitized.path as string}`);
if (sanitized.permanent) cmdArgs.push('permanent');
const result = await executeObsidianCommand('note', cmdArgs); const result = await executeObsidianCommand('delete', cmdArgs);
handleCLIResult(result, { operation: 'delete', identifier }); handleCLIResult(result, { operation: 'delete', identifier: sanitized.file || sanitized.path });
return { return {
content: [ content: [
@@ -183,21 +357,38 @@ export async function registerFileOperationTools(server: ObsidianMCPServer): Pro
// T037: Move note tool // T037: Move note tool
server.registerTool( server.registerTool(
'obsidian_move_note', '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).', 'Move a note to a different location in the vault. Specify the current note (file or path) and the destination path (to).',
{ 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)' },
},
},
createToolHandler( createToolHandler(
'Move a note to a different location', '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) => { async (args) => {
const validated = moveRenameSchema.parse(args) as any; const validated = moveRenameSchema.parse(args) as any;
const sanitized = sanitizeParameters(validated) as any; const sanitized = sanitizeParameters(validated) as any;
const identifier = sanitized.file || sanitized.path; const cmdArgs: string[] = [];
const newLocation = sanitized.newPath || sanitized.newName; if (sanitized.file) cmdArgs.push(`file=${sanitized.file as string}`);
const cmdArgs: string[] = ['move', identifier as string, newLocation 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); const result = await executeObsidianCommand('move', cmdArgs);
handleCLIResult(result, { operation: 'move', identifier, newLocation }); handleCLIResult(result, { operation: 'move', identifier: sanitized.file || sanitized.path, to: sanitized.to });
return { return {
content: [ content: [
@@ -214,21 +405,38 @@ export async function registerFileOperationTools(server: ObsidianMCPServer): Pro
// T038: Rename note tool // T038: Rename note tool
server.registerTool( server.registerTool(
'obsidian_rename_note', 'obsidian_rename_note',
'Rename a note in the vault. Specify the current note (file or path) and the new name (newName).', 'Rename a note in the vault. Specify the current note (file or path) and the new name.',
{ 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)' },
},
},
createToolHandler( createToolHandler(
'Rename a note', '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) => { async (args) => {
const validated = moveRenameSchema.parse(args) as any; const validated = moveRenameSchema.parse(args) as any;
const sanitized = sanitizeParameters(validated) as any; const sanitized = sanitizeParameters(validated) as any;
const identifier = sanitized.file || sanitized.path; const cmdArgs: string[] = [];
const newName = sanitized.newName || sanitized.newPath; if (sanitized.file) cmdArgs.push(`file=${sanitized.file as string}`);
const cmdArgs: string[] = ['rename', identifier as string, newName 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); const result = await executeObsidianCommand('rename', cmdArgs);
handleCLIResult(result, { operation: 'rename', identifier, newName }); handleCLIResult(result, { operation: 'rename', identifier: sanitized.file || sanitized.path, name: sanitized.name });
return { return {
content: [ content: [
@@ -246,20 +454,35 @@ export async function registerFileOperationTools(server: ObsidianMCPServer): Pro
server.registerTool( server.registerTool(
'obsidian_open_note', '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.', '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( createToolHandler(
'Open a note in Obsidian', '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) => { async (args) => {
const validated = fileIdentifierSchema.parse(args) as any; const validated = fileIdentifierSchema.parse(args) as any;
const sanitized = sanitizeParameters(validated) as any; const sanitized = sanitizeParameters(validated) as any;
const identifier = sanitized.file || sanitized.path; const cmdArgs: string[] = [];
const cmdArgs: string[] = ['open', identifier as string]; if (sanitized.file) cmdArgs.push(`file=${sanitized.file as string}`);
if ((args as any).newtab) cmdArgs.push('--newtab'); if (sanitized.path) cmdArgs.push(`path=${sanitized.path as string}`);
if ((args as any).newtab) cmdArgs.push('newtab');
const result = await executeObsidianCommand('note', cmdArgs); const result = await executeObsidianCommand('open', cmdArgs);
handleCLIResult(result, { operation: 'open', identifier }); handleCLIResult(result, { operation: 'open', identifier: sanitized.file || sanitized.path });
return { return {
content: [ content: [
@@ -273,35 +496,8 @@ export async function registerFileOperationTools(server: ObsidianMCPServer): Pro
) )
); );
// T040: Get file info tool // Note: obsidian_duplicate_note and obsidian_get_file_info were removed
server.registerTool( // as they are not in the actual Obsidian CLI specification
'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;
const identifier = sanitized.file || sanitized.path; logger.info('File operation tools registered', { count: 8 });
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 });
} }