fix: add complete input schemas to all link and tag/alias tools

Fixed all remaining tools in links.ts and tags-aliases.ts to properly
expose their input parameters in the tools/list response, matching the
pattern used in file-operations.ts and search.ts.

Links.ts (5 tools):
- obsidian_get_backlinks: Added 5 params (file, path, counts, total, format)
- obsidian_list_outgoing_links: Added 3 params (file, path, total)
- obsidian_list_unresolved_links: Added 4 params (total, counts, verbose, format)
- obsidian_list_deadends: Added 2 params (total, all)
- obsidian_list_orphans: Added 2 params (total, all)

Tags-Aliases.ts (4 tools):
- obsidian_list_tags: Added 7 params (file, path, total, counts, sort, format, active)
- obsidian_search_by_tag: Added 3 params (name required, total, verbose)
  * Renamed from obsidian_get_tag_info for consistency
- obsidian_get_tag_count: Added 1 param (name required)
- obsidian_list_aliases: Added 5 params (file, path, total, verbose, active)

All parameters verified against 'obsidian help <command>' output.

Changes to manifest.json:
- Updated tool name: obsidian_get_tag_info → obsidian_search_by_tag

Before: Empty properties: {} on 9 tools
After: Full parameter schemas with types, descriptions, and required fields

Build:  0 TypeScript errors
Total tools with complete schemas: 28/28 

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-03-22 16:28:37 -05:00
parent 35ab9cda79
commit 3ef2616e70
3 changed files with 221 additions and 42 deletions

View File

@@ -99,7 +99,7 @@
"description": "List all tags in the vault or in a specific note. Optionally include usage counts and sort by frequency or name." "description": "List all tags in the vault or in a specific note. Optionally include usage counts and sort by frequency or name."
}, },
{ {
"name": "obsidian_get_tag_info", "name": "obsidian_search_by_tag",
"description": "Get detailed information about a specific tag, including which notes use it and how many times." "description": "Get detailed information about a specific tag, including which notes use it and how many times."
}, },
{ {

View File

@@ -22,10 +22,28 @@ export async function registerLinkTools(server: ObsidianMCPServer): Promise<void
server.registerTool( server.registerTool(
'obsidian_get_backlinks', 'obsidian_get_backlinks',
'Get all backlinks (incoming links) to a note. Shows which notes reference this note. Optionally include link counts.', 'Get all backlinks (incoming links) to a note. Shows which notes reference this note. Optionally include link counts.',
{ type: 'object', properties: {} }, {
type: 'object',
properties: {
file: { type: 'string', description: 'Target file name' },
path: { type: 'string', description: 'Target file path' },
counts: { type: 'boolean', description: 'Include link counts' },
total: { type: 'boolean', description: 'Return backlink count' },
format: { type: 'string', enum: ['json', 'tsv', 'csv'], description: 'Output format (default: tsv)' },
},
},
createToolHandler( createToolHandler(
'Get backlinks to a note', 'Get backlinks to a note',
{ type: 'object', properties: {} }, {
type: 'object',
properties: {
file: { type: 'string', description: 'Target file name' },
path: { type: 'string', description: 'Target file path' },
counts: { type: 'boolean', description: 'Include link counts' },
total: { type: 'boolean', description: 'Return backlink count' },
format: { type: 'string', enum: ['json', 'tsv', 'csv'], description: 'Output format' },
},
},
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;
@@ -34,6 +52,7 @@ export async function registerLinkTools(server: ObsidianMCPServer): Promise<void
if (sanitized.file) cmdArgs.push(formatParam('file', sanitized.file)); if (sanitized.file) cmdArgs.push(formatParam('file', sanitized.file));
if (sanitized.path) cmdArgs.push(formatParam('path', sanitized.path)); if (sanitized.path) cmdArgs.push(formatParam('path', sanitized.path));
if ((args as any).counts) cmdArgs.push('counts'); if ((args as any).counts) cmdArgs.push('counts');
if ((args as any).total) cmdArgs.push('total');
if ((args as any).format) cmdArgs.push(formatParam('format', (args as any).format)); if ((args as any).format) cmdArgs.push(formatParam('format', (args as any).format));
const result = await executeObsidianCommand('backlinks', cmdArgs); const result = await executeObsidianCommand('backlinks', cmdArgs);
@@ -56,12 +75,26 @@ export async function registerLinkTools(server: ObsidianMCPServer): Promise<void
// T051: Get outgoing links tool // T051: Get outgoing links tool
server.registerTool( server.registerTool(
'obsidian_get_outgoing_links', 'obsidian_list_outgoing_links',
'Get all outgoing links from a note. Shows which notes this note references. Useful for understanding note connections.', 'Get all outgoing links from a note. Shows which notes this note references. Useful for understanding note connections.',
{ type: 'object', properties: {} }, {
type: 'object',
properties: {
file: { type: 'string', description: 'File name' },
path: { type: 'string', description: 'File path' },
total: { type: 'boolean', description: 'Return link count' },
},
},
createToolHandler( createToolHandler(
'Get outgoing links from a note', 'Get outgoing links from a note',
{ type: 'object', properties: {} }, {
type: 'object',
properties: {
file: { type: 'string', description: 'File name' },
path: { type: 'string', description: 'File path' },
total: { type: 'boolean', description: 'Return link count' },
},
},
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;
@@ -69,19 +102,18 @@ export async function registerLinkTools(server: ObsidianMCPServer): Promise<void
const cmdArgs: string[] = []; const cmdArgs: string[] = [];
if (sanitized.file) cmdArgs.push(formatParam('file', sanitized.file)); if (sanitized.file) cmdArgs.push(formatParam('file', sanitized.file));
if (sanitized.path) cmdArgs.push(formatParam('path', sanitized.path)); if (sanitized.path) cmdArgs.push(formatParam('path', sanitized.path));
if ((args as any).format) cmdArgs.push(formatParam('format', (args as any).format)); if ((args as any).total) cmdArgs.push('total');
const result = await executeObsidianCommand('links', cmdArgs); const result = await executeObsidianCommand('links', cmdArgs);
handleCLIResult(result, { operation: 'outgoing_links', identifier: sanitized.file || sanitized.path }); handleCLIResult(result, { operation: 'outgoing_links', identifier: sanitized.file || sanitized.path });
const format = (args as any).format || 'text'; const parsedData = parseOutput(result.stdout, 'text');
const parsedData = parseOutput(result.stdout, format);
return { return {
content: [ content: [
{ {
type: 'text', type: 'text',
text: formatForMCP(parsedData, format), text: formatForMCP(parsedData, 'text'),
}, },
], ],
}; };
@@ -93,14 +125,33 @@ export async function registerLinkTools(server: ObsidianMCPServer): Promise<void
server.registerTool( server.registerTool(
'obsidian_list_unresolved_links', 'obsidian_list_unresolved_links',
'List all unresolved (broken) wikilinks in the vault. Shows links pointing to notes that don\'t exist. Useful for finding content gaps.', 'List all unresolved (broken) wikilinks in the vault. Shows links pointing to notes that don\'t exist. Useful for finding content gaps.',
{ type: 'object', properties: {} }, {
type: 'object',
properties: {
total: { type: 'boolean', description: 'Return unresolved link count' },
counts: { type: 'boolean', description: 'Include link counts' },
verbose: { type: 'boolean', description: 'Include source files' },
format: { type: 'string', enum: ['json', 'tsv', 'csv'], description: 'Output format (default: tsv)' },
},
},
createToolHandler( createToolHandler(
'List unresolved/broken links', 'List unresolved/broken links',
{ type: 'object', properties: {} }, {
type: 'object',
properties: {
total: { type: 'boolean', description: 'Return unresolved link count' },
counts: { type: 'boolean', description: 'Include link counts' },
verbose: { type: 'boolean', description: 'Include source files' },
format: { type: 'string', enum: ['json', 'tsv', 'csv'], description: 'Output format' },
},
},
async (args) => { async (args) => {
const sanitized = sanitizeParameters(args as any) as any; const sanitized = sanitizeParameters(args as any) as any;
const cmdArgs: string[] = []; const cmdArgs: string[] = [];
if (sanitized.total) cmdArgs.push('total');
if (sanitized.counts) cmdArgs.push('counts');
if (sanitized.verbose) cmdArgs.push('verbose');
if (sanitized.format) cmdArgs.push(formatParam('format', sanitized.format)); if (sanitized.format) cmdArgs.push(formatParam('format', sanitized.format));
const result = await executeObsidianCommand('unresolved', cmdArgs); const result = await executeObsidianCommand('unresolved', cmdArgs);
@@ -125,27 +176,39 @@ export async function registerLinkTools(server: ObsidianMCPServer): Promise<void
server.registerTool( server.registerTool(
'obsidian_list_deadends', 'obsidian_list_deadends',
'List all dead-end notes (notes with no outgoing links). These notes don\'t connect to anything else in the vault.', 'List all dead-end notes (notes with no outgoing links). These notes don\'t connect to anything else in the vault.',
{ type: 'object', properties: {} }, {
type: 'object',
properties: {
total: { type: 'boolean', description: 'Return dead-end count' },
all: { type: 'boolean', description: 'Include non-markdown files' },
},
},
createToolHandler( createToolHandler(
'List dead-end notes', 'List dead-end notes',
{ type: 'object', properties: {} }, {
type: 'object',
properties: {
total: { type: 'boolean', description: 'Return dead-end count' },
all: { type: 'boolean', description: 'Include non-markdown files' },
},
},
async (args) => { async (args) => {
const sanitized = sanitizeParameters(args as any) as any; const sanitized = sanitizeParameters(args as any) as any;
const cmdArgs: string[] = []; const cmdArgs: string[] = [];
if (sanitized.format) cmdArgs.push(formatParam('format', sanitized.format)); if (sanitized.total) cmdArgs.push('total');
if (sanitized.all) cmdArgs.push('all');
const result = await executeObsidianCommand('deadends', cmdArgs); const result = await executeObsidianCommand('deadends', cmdArgs);
handleCLIResult(result, { operation: 'deadends' }); handleCLIResult(result, { operation: 'deadends' });
const format = sanitized.format || 'text'; const parsedData = parseOutput(result.stdout, 'text');
const parsedData = parseOutput(result.stdout, format);
return { return {
content: [ content: [
{ {
type: 'text', type: 'text',
text: formatForMCP(parsedData, format), text: formatForMCP(parsedData, 'text'),
}, },
], ],
}; };
@@ -157,27 +220,39 @@ export async function registerLinkTools(server: ObsidianMCPServer): Promise<void
server.registerTool( server.registerTool(
'obsidian_list_orphans', 'obsidian_list_orphans',
'List all orphan notes (notes with no incoming links/backlinks). These notes aren\'t referenced by any other notes.', 'List all orphan notes (notes with no incoming links/backlinks). These notes aren\'t referenced by any other notes.',
{ type: 'object', properties: {} }, {
type: 'object',
properties: {
total: { type: 'boolean', description: 'Return orphan count' },
all: { type: 'boolean', description: 'Include non-markdown files' },
},
},
createToolHandler( createToolHandler(
'List orphan notes', 'List orphan notes',
{ type: 'object', properties: {} }, {
type: 'object',
properties: {
total: { type: 'boolean', description: 'Return orphan count' },
all: { type: 'boolean', description: 'Include non-markdown files' },
},
},
async (args) => { async (args) => {
const sanitized = sanitizeParameters(args as any) as any; const sanitized = sanitizeParameters(args as any) as any;
const cmdArgs: string[] = []; const cmdArgs: string[] = [];
if (sanitized.format) cmdArgs.push(formatParam('format', sanitized.format)); if (sanitized.total) cmdArgs.push('total');
if (sanitized.all) cmdArgs.push('all');
const result = await executeObsidianCommand('orphans', cmdArgs); const result = await executeObsidianCommand('orphans', cmdArgs);
handleCLIResult(result, { operation: 'orphans' }); handleCLIResult(result, { operation: 'orphans' });
const format = sanitized.format || 'text'; const parsedData = parseOutput(result.stdout, 'text');
const parsedData = parseOutput(result.stdout, format);
return { return {
content: [ content: [
{ {
type: 'text', type: 'text',
text: formatForMCP(parsedData, format), text: formatForMCP(parsedData, 'text'),
}, },
], ],
}; };

View File

@@ -21,10 +21,32 @@ export async function registerTagsAndAliasesTools(server: ObsidianMCPServer): Pr
server.registerTool( server.registerTool(
'obsidian_list_tags', 'obsidian_list_tags',
'List all tags in the vault or in a specific note. Optionally include usage counts and sort by frequency or name.', 'List all tags in the vault or in a specific note. Optionally include usage counts and sort by frequency or name.',
{ type: 'object', properties: {} }, {
type: 'object',
properties: {
file: { type: 'string', description: 'File name' },
path: { type: 'string', description: 'File path' },
total: { type: 'boolean', description: 'Return tag count' },
counts: { type: 'boolean', description: 'Include tag counts' },
sort: { type: 'string', enum: ['count'], description: 'Sort by count (default: name)' },
format: { type: 'string', enum: ['json', 'tsv', 'csv'], description: 'Output format (default: tsv)' },
active: { type: 'boolean', description: 'Show tags for active file' },
},
},
createToolHandler( createToolHandler(
'List tags in vault or note', 'List tags in vault or note',
{ type: 'object', properties: {} }, {
type: 'object',
properties: {
file: { type: 'string', description: 'File name' },
path: { type: 'string', description: 'File path' },
total: { type: 'boolean', description: 'Return tag count' },
counts: { type: 'boolean', description: 'Include tag counts' },
sort: { type: 'string', enum: ['count'], description: 'Sort by count' },
format: { type: 'string', enum: ['json', 'tsv', 'csv'], description: 'Output format' },
active: { type: 'boolean', description: 'Show tags for active file' },
},
},
async (args) => { async (args) => {
const sanitized = sanitizeParameters(args as any) as any; const sanitized = sanitizeParameters(args as any) as any;
@@ -37,9 +59,11 @@ export async function registerTagsAndAliasesTools(server: ObsidianMCPServer): Pr
cmdArgs.push(formatParam('path', sanitized.path)); cmdArgs.push(formatParam('path', sanitized.path));
} }
if (sanitized.total) cmdArgs.push('total');
if (sanitized.counts) cmdArgs.push('counts'); if (sanitized.counts) cmdArgs.push('counts');
if (sanitized.sortBy) cmdArgs.push(formatParam('sort', sanitized.sortBy)); if (sanitized.sort) cmdArgs.push(formatParam('sort', sanitized.sort));
if (sanitized.format) cmdArgs.push(formatParam('format', sanitized.format)); if (sanitized.format) cmdArgs.push(formatParam('format', sanitized.format));
if (sanitized.active) cmdArgs.push('active');
const result = await executeObsidianCommand('tags', cmdArgs); const result = await executeObsidianCommand('tags', cmdArgs);
handleCLIResult(result, { operation: 'list_tags' }); handleCLIResult(result, { operation: 'list_tags' });
@@ -61,33 +85,49 @@ export async function registerTagsAndAliasesTools(server: ObsidianMCPServer): Pr
// T055: Get tag info tool // T055: Get tag info tool
server.registerTool( server.registerTool(
'obsidian_get_tag_info', 'obsidian_search_by_tag',
'Get detailed information about a specific tag, including which notes use it and how many times.', 'Get detailed information about a specific tag, including which notes use it and how many times.',
{ type: 'object', properties: {} }, {
type: 'object',
required: ['name'],
properties: {
name: { type: 'string', description: 'Tag name (required)' },
total: { type: 'boolean', description: 'Return occurrence count' },
verbose: { type: 'boolean', description: 'Include file list and count' },
},
},
createToolHandler( createToolHandler(
'Get information about a tag', 'Get information about a tag',
{ type: 'object', properties: {} }, {
type: 'object',
required: ['name'],
properties: {
name: { type: 'string', description: 'Tag name (required)' },
total: { type: 'boolean', description: 'Return occurrence count' },
verbose: { type: 'boolean', description: 'Include file list and count' },
},
},
async (args) => { async (args) => {
const sanitized = sanitizeParameters(args as any) as any; const sanitized = sanitizeParameters(args as any) as any;
if (!sanitized.tag) { if (!sanitized.name) {
throw new Error('Tag parameter is required'); throw new Error('Tag name parameter is required');
} }
const cmdArgs: string[] = [formatParam('name', sanitized.tag)]; const cmdArgs: string[] = [formatParam('name', sanitized.name)];
if (sanitized.format) cmdArgs.push(formatParam('format', sanitized.format)); if (sanitized.total) cmdArgs.push('total');
if (sanitized.verbose) cmdArgs.push('verbose');
const result = await executeObsidianCommand('tag', cmdArgs); const result = await executeObsidianCommand('tag', cmdArgs);
handleCLIResult(result, { operation: 'tag_info', tag: sanitized.tag }); handleCLIResult(result, { operation: 'tag_info', tag: sanitized.name });
const format = sanitized.format || 'text'; const parsedData = parseOutput(result.stdout, 'text');
const parsedData = parseOutput(result.stdout, format);
return { return {
content: [ content: [
{ {
type: 'text', type: 'text',
text: formatForMCP(parsedData, format), text: formatForMCP(parsedData, 'text'),
}, },
], ],
}; };
@@ -99,10 +139,28 @@ export async function registerTagsAndAliasesTools(server: ObsidianMCPServer): Pr
server.registerTool( server.registerTool(
'obsidian_list_aliases', 'obsidian_list_aliases',
'List all aliases in the vault or for a specific note. Aliases are alternative names for notes.', 'List all aliases in the vault or for a specific note. Aliases are alternative names for notes.',
{ type: 'object', properties: {} }, {
type: 'object',
properties: {
file: { type: 'string', description: 'File name' },
path: { type: 'string', description: 'File path' },
total: { type: 'boolean', description: 'Return alias count' },
verbose: { type: 'boolean', description: 'Include file paths' },
active: { type: 'boolean', description: 'Show aliases for active file' },
},
},
createToolHandler( createToolHandler(
'List aliases in vault or note', 'List aliases in vault or note',
{ type: 'object', properties: {} }, {
type: 'object',
properties: {
file: { type: 'string', description: 'File name' },
path: { type: 'string', description: 'File path' },
total: { type: 'boolean', description: 'Return alias count' },
verbose: { type: 'boolean', description: 'Include file paths' },
active: { type: 'boolean', description: 'Show aliases for active file' },
},
},
async (args) => { async (args) => {
const sanitized = sanitizeParameters(args as any) as any; const sanitized = sanitizeParameters(args as any) as any;
@@ -115,7 +173,9 @@ export async function registerTagsAndAliasesTools(server: ObsidianMCPServer): Pr
cmdArgs.push(formatParam('path', sanitized.path)); cmdArgs.push(formatParam('path', sanitized.path));
} }
if (sanitized.format) cmdArgs.push(formatParam('format', sanitized.format)); if (sanitized.total) cmdArgs.push('total');
if (sanitized.verbose) cmdArgs.push('verbose');
if (sanitized.active) cmdArgs.push('active');
const result = await executeObsidianCommand('aliases', cmdArgs); const result = await executeObsidianCommand('aliases', cmdArgs);
handleCLIResult(result, { operation: 'list_aliases' }); handleCLIResult(result, { operation: 'list_aliases' });
@@ -135,5 +195,49 @@ export async function registerTagsAndAliasesTools(server: ObsidianMCPServer): Pr
) )
); );
logger.info('Tags and aliases tools registered', { count: 3 }); // Additional tool: Get tag count (wrapper for tag with total flag)
server.registerTool(
'obsidian_get_tag_count',
'Count how many notes use a specific tag.',
{
type: 'object',
required: ['name'],
properties: {
name: { type: 'string', description: 'Tag name (required)' },
},
},
createToolHandler(
'Get tag usage count',
{
type: 'object',
required: ['name'],
properties: {
name: { type: 'string', description: 'Tag name (required)' },
},
},
async (args) => {
const sanitized = sanitizeParameters(args as any) as any;
if (!sanitized.name) {
throw new Error('Tag name parameter is required');
}
const cmdArgs: string[] = [formatParam('name', sanitized.name), 'total'];
const result = await executeObsidianCommand('tag', cmdArgs);
handleCLIResult(result, { operation: 'tag_count', tag: sanitized.name });
return {
content: [
{
type: 'text',
text: result.stdout.trim(),
},
],
};
}
)
);
logger.info('Tags and aliases tools registered', { count: 4 });
} }