Files
obsidian-mcp-server/src/ui/notifications.ts
Bill Ballou 3593291596 fix: address ObsidianReviewBot linting issues
- Add type guard for recursive parameter in notifications.ts to ensure
  only boolean values are stringified (prevents [object Object] output)
- Remove unused error variables from catch blocks across 5 files:
  - vault-tools.ts (5 instances)
  - frontmatter-utils.ts (3 instances)
  - search-utils.ts (2 instances)
  - waypoint-utils.ts (1 instance)
2026-01-31 20:32:24 -05:00

235 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { App, Notice } from 'obsidian';
import { MCPPluginSettings } from '../types/settings-types';
/**
* Notification history entry
*/
export interface NotificationHistoryEntry {
timestamp: number;
toolName: string;
args: Record<string, unknown>;
success: boolean;
duration?: number;
error?: string;
}
/**
* Tool icon mapping
*/
const TOOL_ICONS: Record<string, string> = {
read_note: '📖',
read_excalidraw: '📖',
create_note: '✏️',
update_note: '✏️',
update_frontmatter: '✏️',
update_sections: '✏️',
delete_note: '🗑️',
rename_file: '📝',
search: '🔍',
search_waypoints: '🔍',
list: '📋',
list_notes: '📋',
stat: '📊',
exists: '📊',
get_vault_info: '',
get_folder_waypoint: '🗺️',
is_folder_note: '📁',
validate_wikilinks: '🔗',
resolve_wikilink: '🔗',
backlinks: '🔗'
};
/**
* Notification manager for MCP tool calls
* Displays notifications in the Obsidian UI with rate limiting
*/
export class NotificationManager {
private app: App;
private settings: MCPPluginSettings;
private history: NotificationHistoryEntry[] = [];
private maxHistorySize = 100;
// Rate limiting
private notificationQueue: Array<() => void> = [];
private isProcessingQueue = false;
private maxNotificationsPerSecond = 10;
private notificationInterval = 1000 / this.maxNotificationsPerSecond; // 100ms between notifications
// Batching
private pendingToolCalls: Map<string, number> = new Map();
private batchTimeout: NodeJS.Timeout | null = null;
constructor(app: App, settings: MCPPluginSettings) {
this.app = app;
this.settings = settings;
}
/**
* Update settings reference
*/
updateSettings(settings: MCPPluginSettings): void {
this.settings = settings;
}
/**
* Show notification for tool call start
*/
showToolCall(toolName: string, args: Record<string, unknown>, duration?: number): void {
if (!this.shouldShowNotification()) {
return;
}
const icon = TOOL_ICONS[toolName] || '🔧';
const argsStr = this.formatArgs(args);
const message = argsStr
? `${icon} MCP Tool Called: ${toolName}\n${argsStr}`
: `${icon} MCP Tool Called: ${toolName}`;
this.queueNotification(() => {
new Notice(message, duration || this.settings.notificationDuration);
});
// Log to console if enabled
if (this.settings.logToConsole) {
console.debug(`[MCP] Tool call: ${toolName}`, args);
}
}
/**
* Add entry to notification history
*/
addToHistory(entry: NotificationHistoryEntry): void {
this.history.unshift(entry);
// Limit history size
if (this.history.length > this.maxHistorySize) {
this.history = this.history.slice(0, this.maxHistorySize);
}
}
/**
* Get notification history
*/
getHistory(): NotificationHistoryEntry[] {
return [...this.history];
}
/**
* Clear notification history
*/
clearHistory(): void {
this.history = [];
}
/**
* Clear all active notifications (not possible with Obsidian API)
*/
clearAll(): void {
// Obsidian doesn't provide API to clear notices
// This is a no-op for compatibility
}
/**
* Check if notification should be shown
*/
private shouldShowNotification(): boolean {
return this.settings.notificationsEnabled;
}
/**
* Format arguments for display
*/
private formatArgs(args: Record<string, unknown>): string {
if (!this.settings.showParameters) {
return '';
}
if (!args || Object.keys(args).length === 0) {
return '';
}
try {
// Extract key parameters for display
const keyParams: string[] = [];
if (args.path && typeof args.path === 'string') {
keyParams.push(`path: "${this.truncateString(args.path, 30)}"`);
}
if (args.query && typeof args.query === 'string') {
keyParams.push(`query: "${this.truncateString(args.query, 30)}"`);
}
if (args.folder && typeof args.folder === 'string') {
keyParams.push(`folder: "${this.truncateString(args.folder, 30)}"`);
}
if (typeof args.recursive === 'boolean') {
keyParams.push(`recursive: ${args.recursive}`);
}
// If no key params, show first 50 chars of JSON
if (keyParams.length === 0) {
const json = JSON.stringify(args);
return this.truncateString(json, 50);
}
return keyParams.join(', ');
} catch {
return '';
}
}
/**
* Truncate string to max length
*/
private truncateString(str: string, maxLength: number): string {
if (str.length <= maxLength) {
return str;
}
return str.substring(0, maxLength - 3) + '...';
}
/**
* Queue notification with rate limiting
*/
private queueNotification(notificationFn: () => void): void {
this.notificationQueue.push(notificationFn);
if (!this.isProcessingQueue) {
void this.processQueue();
}
}
/**
* Process notification queue with rate limiting
*/
private async processQueue(): Promise<void> {
if (this.isProcessingQueue) {
return;
}
this.isProcessingQueue = true;
while (this.notificationQueue.length > 0) {
const notificationFn = this.notificationQueue.shift();
if (notificationFn) {
notificationFn();
}
// Wait before processing next notification
if (this.notificationQueue.length > 0) {
await this.sleep(this.notificationInterval);
}
}
this.isProcessingQueue = false;
}
/**
* Sleep helper
*/
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}