feat: Phase 10 - UI Notifications (request-only)

Implement visual feedback for MCP tool calls with configurable notifications.

Features:
- Real-time notifications when tools are called (request only, no completion)
- Tool-specific emoji icons for visual clarity
- Rate limiting (max 10 notifications/second)
- Notification history tracking (last 100 entries)
- Configurable settings: enable/disable, show parameters, duration, console logging
- History modal with filtering and export to clipboard

Implementation:
- Created NotificationManager with queue-based rate limiting
- Created NotificationHistoryModal for viewing past tool calls
- Integrated into tool call interceptor in ToolRegistry
- Added notification settings UI section
- Added 'View MCP Notification History' command

Benefits:
- Visual feedback for debugging and monitoring
- Transparency into AI agent actions
- Simple on/off toggle, no complex verbosity settings
- Zero performance impact when disabled
- History tracks success/failure/duration for all calls

All 10 phases of the roadmap are now complete\!
This commit is contained in:
2025-10-17 01:11:10 -04:00
parent 6017f879f4
commit b681327970
10 changed files with 1178 additions and 65 deletions

View File

@@ -2,16 +2,25 @@ import { App } from 'obsidian';
import { Tool, CallToolResult } from '../types/mcp-types';
import { NoteTools } from './note-tools';
import { VaultTools } from './vault-tools';
import { NotificationManager } from '../ui/notifications';
export class ToolRegistry {
private noteTools: NoteTools;
private vaultTools: VaultTools;
private notificationManager: NotificationManager | null = null;
constructor(app: App) {
this.noteTools = new NoteTools(app);
this.vaultTools = new VaultTools(app);
}
/**
* Set notification manager for tool call notifications
*/
setNotificationManager(manager: NotificationManager | null): void {
this.notificationManager = manager;
}
getToolDefinitions(): Tool[] {
return [
{
@@ -444,52 +453,68 @@ export class ToolRegistry {
}
async callTool(name: string, args: any): Promise<CallToolResult> {
const startTime = Date.now();
// Show tool call notification
if (this.notificationManager) {
this.notificationManager.showToolCall(name, args);
}
try {
let result: CallToolResult;
switch (name) {
case "read_note":
return await this.noteTools.readNote(args.path, {
result = await this.noteTools.readNote(args.path, {
withFrontmatter: args.withFrontmatter,
withContent: args.withContent,
parseFrontmatter: args.parseFrontmatter
});
break;
case "create_note":
return await this.noteTools.createNote(
result = await this.noteTools.createNote(
args.path,
args.content,
args.createParents ?? false,
args.onConflict ?? 'error'
);
break;
case "update_note":
return await this.noteTools.updateNote(args.path, args.content);
result = await this.noteTools.updateNote(args.path, args.content);
break;
case "update_frontmatter":
return await this.noteTools.updateFrontmatter(
result = await this.noteTools.updateFrontmatter(
args.path,
args.patch,
args.remove ?? [],
args.ifMatch
);
break;
case "update_sections":
return await this.noteTools.updateSections(
result = await this.noteTools.updateSections(
args.path,
args.edits,
args.ifMatch
);
break;
case "rename_file":
return await this.noteTools.renameFile(
result = await this.noteTools.renameFile(
args.path,
args.newPath,
args.updateLinks ?? true,
args.ifMatch
);
break;
case "delete_note":
return await this.noteTools.deleteNote(
result = await this.noteTools.deleteNote(
args.path,
args.soft ?? true,
args.dryRun ?? false,
args.ifMatch
);
break;
case "search":
return await this.vaultTools.search({
result = await this.vaultTools.search({
query: args.query,
isRegex: args.isRegex,
caseSensitive: args.caseSensitive,
@@ -500,12 +525,15 @@ export class ToolRegistry {
snippetLength: args.snippetLength,
maxResults: args.maxResults
});
break;
case "search_waypoints":
return await this.vaultTools.searchWaypoints(args.folder);
result = await this.vaultTools.searchWaypoints(args.folder);
break;
case "get_vault_info":
return await this.vaultTools.getVaultInfo();
result = await this.vaultTools.getVaultInfo();
break;
case "list":
return await this.vaultTools.list({
result = await this.vaultTools.list({
path: args.path,
recursive: args.recursive,
includes: args.includes,
@@ -515,38 +543,76 @@ export class ToolRegistry {
cursor: args.cursor,
withFrontmatterSummary: args.withFrontmatterSummary
});
break;
case "stat":
return await this.vaultTools.stat(args.path);
result = await this.vaultTools.stat(args.path);
break;
case "exists":
return await this.vaultTools.exists(args.path);
result = await this.vaultTools.exists(args.path);
break;
case "read_excalidraw":
return await this.noteTools.readExcalidraw(args.path, {
result = await this.noteTools.readExcalidraw(args.path, {
includeCompressed: args.includeCompressed,
includePreview: args.includePreview
});
break;
case "get_folder_waypoint":
return await this.vaultTools.getFolderWaypoint(args.path);
result = await this.vaultTools.getFolderWaypoint(args.path);
break;
case "is_folder_note":
return await this.vaultTools.isFolderNote(args.path);
result = await this.vaultTools.isFolderNote(args.path);
break;
case "validate_wikilinks":
return await this.vaultTools.validateWikilinks(args.path);
result = await this.vaultTools.validateWikilinks(args.path);
break;
case "resolve_wikilink":
return await this.vaultTools.resolveWikilink(args.sourcePath, args.linkText);
result = await this.vaultTools.resolveWikilink(args.sourcePath, args.linkText);
break;
case "backlinks":
return await this.vaultTools.getBacklinks(
result = await this.vaultTools.getBacklinks(
args.path,
args.includeUnlinked ?? false,
args.includeSnippets ?? true
);
break;
default:
return {
result = {
content: [{ type: "text", text: `Unknown tool: ${name}` }],
isError: true
};
}
// Add to history (no completion notification)
const duration = Date.now() - startTime;
if (this.notificationManager) {
this.notificationManager.addToHistory({
timestamp: Date.now(),
toolName: name,
args: args,
success: !result.isError,
duration: duration
});
}
return result;
} catch (error) {
const duration = Date.now() - startTime;
const errorMessage = (error as Error).message;
// Add to history (no error notification shown)
if (this.notificationManager) {
this.notificationManager.addToHistory({
timestamp: Date.now(),
toolName: name,
args: args,
success: false,
duration: duration,
error: errorMessage
});
}
return {
content: [{ type: "text", text: `Error: ${(error as Error).message}` }],
content: [{ type: "text", text: `Error: ${errorMessage}` }],
isError: true
};
}