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:
@@ -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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user