/**
* MCP `list_changed` notification emitter
*
* Triggers Claude to refresh tool results without reconnection.
* Implements debouncing to prevent notification spam.
*
* This eliminates the need for 30-second polling loops,
* reducing API calls by ~90% and providing instant updates.
*/
class NotificationEmitter {
constructor(server) {
this.server = server;
this.debounceTimers = {};
}
/**
* Emit list_changed notification with debouncing
* @param {string} reason - Why notification is being sent (for logging/debugging)
* @param {number} debounceMs - Debounce window in milliseconds (default: 1000ms)
*/
emitChange(reason, debounceMs = 1000) {
// Debounce to prevent notification spam
// If we get multiple changes of the same type within the window,
// only emit one notification
if (this.debounceTimers[reason]) {
clearTimeout(this.debounceTimers[reason]);
}
this.debounceTimers[reason] = setTimeout(() => {
try {
this.server.notification({
method: 'notifications/list_changed'
});
delete this.debounceTimers[reason];
} catch (e) {
// Silent fail - notifications are best-effort
// If notification fails, Claude will continue working normally
}
}, debounceMs);
}
/**
* Emit immediately without debouncing
* Use for urgent updates like direct mentions
*/
emitImmediate() {
try {
this.server.notification({
method: 'notifications/list_changed'
});
} catch (e) {
// Silent fail
}
}
/**
* Cancel pending notifications for a specific reason
* Useful when shutting down or cleaning up
*/
cancel(reason) {
if (this.debounceTimers[reason]) {
clearTimeout(this.debounceTimers[reason]);
delete this.debounceTimers[reason];
}
}
/**
* Cancel all pending notifications
*/
cancelAll() {
Object.values(this.debounceTimers).forEach(timer => clearTimeout(timer));
this.debounceTimers = {};
}
}
module.exports = NotificationEmitter;