diff --git a/src/webmcp.d.ts b/src/webmcp.d.ts index ae5628e..f3ba81a 100644 --- a/src/webmcp.d.ts +++ b/src/webmcp.d.ts @@ -4,6 +4,7 @@ export interface WebMCPOptions { size?: string; padding?: string; inactivityTimeout?: number; + headless?: boolean; } export interface ToolInputSchema { @@ -28,6 +29,27 @@ export type ToolHandler = (args: any) => string | object | Promise object | Promise; export type ResourceHandler = (uri: string) => object | Promise; +export interface ConnectionInfo { + isConnected: boolean; + channel: string; + server: string; + status: string; + tools: string[]; + prompts: string[]; + resources: string[]; +} + +export interface WebMCPEventMap { + connected: { channel: string; server: string }; + disconnected: { code: number; reason: string }; + reconnecting: { attempt: number; maxAttempts: number; delay: number }; + statusChange: { status: string; message: string }; + toolRegistered: { name: string }; + toolCreated: { name: string }; + toolRemoved: { name: string }; + error: { message: string }; +} + declare class WebMCP { readonly isConnected: boolean; readonly isExpanded: boolean; @@ -37,6 +59,10 @@ declare class WebMCP { connect(connectionToken: string): Promise; disconnect(): void; + on(event: K, callback: (data: WebMCPEventMap[K]) => void): () => void; + off(event: K, callback: (data: WebMCPEventMap[K]) => void): void; + getConnectionInfo(): ConnectionInfo; + registerTool(name: string, description: string, schema: ToolInputSchema, executeFn: ToolHandler): void; unregisterTool(name: string): void; unregisterAllTools(): void; diff --git a/src/webmcp.js b/src/webmcp.js index a3a3f64..95bc438 100644 --- a/src/webmcp.js +++ b/src/webmcp.js @@ -15,9 +15,14 @@ class WebMCP { size: '30px', padding: '20px', inactivityTimeout: 5 * 60 * 1000, // 5 minutes in milliseconds + headless: false, ...options }; + // Event emitter + this._listeners = {}; + this._currentStatus = 'disconnected'; + // State variables this.isConnected = false; this.isExpanded = false; @@ -52,6 +57,62 @@ class WebMCP { this._init(); } + /** + * Register an event listener + * @public + * @param {string} event - Event name + * @param {Function} callback - Callback function + * @returns {Function} Unsubscribe function + */ + on(event, callback) { + if (!this._listeners[event]) { + this._listeners[event] = []; + } + this._listeners[event].push(callback); + return () => this.off(event, callback); + } + + /** + * Remove an event listener + * @public + * @param {string} event - Event name + * @param {Function} callback - Callback function to remove + */ + off(event, callback) { + if (!this._listeners[event]) return; + this._listeners[event] = this._listeners[event].filter(cb => cb !== callback); + } + + /** + * Emit an event to all registered listeners + * @private + * @param {string} event - Event name + * @param {Object} data - Event data + */ + _emit(event, data) { + if (!this._listeners[event]) return; + this._listeners[event].forEach(cb => { + try { cb(data); } catch (e) { console.error(`Error in ${event} listener:`, e); } + }); + } + + /** + * Get current connection info snapshot + * @public + * @returns {Object} Connection state snapshot + */ + getConnectionInfo() { + return { + isConnected: this.isConnected, + channel: this.currentChannel, + server: this.currentServer, + status: this._currentStatus, + tools: Array.from(this.availableTools.keys()), + prompts: Array.from(this.availablePrompts.keys()), + resources: Array.from(this.availableResources.keys()), + }; + } + _format(s) { return s.replace(/[.:]/g, '_'); } @@ -61,18 +122,20 @@ class WebMCP { * @private */ _init() { - // Check if already initialized on this page - if (document.querySelector('[data-webmcp-widget]')) { - console.warn('WebMCP widget already initialized on this page'); - return; + if (!this.options.headless) { + // Check if already initialized on this page + if (document.querySelector('[data-webmcp-widget]')) { + console.warn('WebMCP widget already initialized on this page'); + return; + } + + // Create and inject the widget + this._createWidget(); + + // Set up event listeners + this._setupEventListeners(); } - // Create and inject the widget - this._createWidget(); - - // Set up event listeners - this._setupEventListeners(); - // Start inactivity timer this._resetInactivityTimer(); @@ -668,6 +731,9 @@ class WebMCP { * @private */ _updateStatus(status, message) { + this._currentStatus = status; + this._emit('statusChange', { status, message: message || status }); + const container = document.getElementById(this.elementId); if (!container) return; @@ -1147,6 +1213,7 @@ class WebMCP { this._reconnectAttempts = 0; this._updateStatus('connected', `Connected to ${this.currentChannel}`); this._updateConnectionUI(true); + this._emit('connected', { channel: this.currentChannel, server: this.currentServer }); console.log('WebMCP connection established'); this._registerItemsWithServer(); }); @@ -1160,6 +1227,7 @@ class WebMCP { if (event.code === 1000) { this._updateStatus('disconnected', 'Disconnected'); this._updateConnectionUI(false); + this._emit('disconnected', { code: event.code, reason: event.reason }); return; } @@ -1169,6 +1237,11 @@ class WebMCP { const delay = this._reconnectDelay * this._reconnectAttempts; console.log(`Reconnecting (attempt ${this._reconnectAttempts}/${this._maxReconnectAttempts}) in ${delay}ms...`); this._updateStatus('connecting', `Reconectando (${this._reconnectAttempts}/${this._maxReconnectAttempts})...`); + this._emit('reconnecting', { + attempt: this._reconnectAttempts, + maxAttempts: this._maxReconnectAttempts, + delay + }); setTimeout(() => { this.connect(this._lastConnectionToken); }, delay); @@ -1178,6 +1251,7 @@ class WebMCP { // Max retries exceeded or no token — give up this._updateStatus('disconnected', 'Disconnected'); this._updateConnectionUI(false); + this._emit('disconnected', { code: event.code, reason: event.reason }); this.currentToken = ''; this.currentServer = ''; this.currentChannel = ''; @@ -1234,6 +1308,7 @@ class WebMCP { case 'toolRegistered': console.log(`Tool registered with server: ${message.name}`); + this._emit('toolRegistered', { name: message.name }); break; case 'promptRegistered': @@ -1298,6 +1373,7 @@ class WebMCP { this.registeredTools.delete(message.name); this._saveItemsToStorage(); this._updateToolsList(); + this._emit('toolRemoved', { name: message.name }); console.log(`Tool removed by server: ${message.name}`); } break; @@ -1307,6 +1383,7 @@ class WebMCP { this.registeredTools.clear(); this._saveItemsToStorage(); this._updateToolsList(); + this._emit('toolRemoved', { name: '*' }); console.log('All tools removed by server'); break; @@ -1336,6 +1413,7 @@ class WebMCP { case 'error': console.error(`Server error: ${message.message}`); + this._emit('error', { message: message.message }); break; default: @@ -1361,6 +1439,7 @@ class WebMCP { type: 'toolResponse', result: { content: [{ type: 'text', text: `Herramienta "${name}" registrada exitosamente` }] } }); + this._emit('toolCreated', { name }); console.log(`Tool created by agent: ${name}`); } catch (e) { this._sendMessage({