Modo headless, event emitter y state getters en el cliente WebMCP
Permite crear instancias sin widget DOM (headless: true), suscribirse a eventos de conexion/tools (on/off) y consultar el estado con getConnectionInfo().
This commit is contained in:
26
src/webmcp.d.ts
vendored
26
src/webmcp.d.ts
vendored
@@ -4,6 +4,7 @@ export interface WebMCPOptions {
|
|||||||
size?: string;
|
size?: string;
|
||||||
padding?: string;
|
padding?: string;
|
||||||
inactivityTimeout?: number;
|
inactivityTimeout?: number;
|
||||||
|
headless?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ToolInputSchema {
|
export interface ToolInputSchema {
|
||||||
@@ -28,6 +29,27 @@ export type ToolHandler = (args: any) => string | object | Promise<string | obje
|
|||||||
export type PromptHandler = (args: any) => object | Promise<object>;
|
export type PromptHandler = (args: any) => object | Promise<object>;
|
||||||
export type ResourceHandler = (uri: string) => object | Promise<object>;
|
export type ResourceHandler = (uri: string) => object | Promise<object>;
|
||||||
|
|
||||||
|
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 {
|
declare class WebMCP {
|
||||||
readonly isConnected: boolean;
|
readonly isConnected: boolean;
|
||||||
readonly isExpanded: boolean;
|
readonly isExpanded: boolean;
|
||||||
@@ -37,6 +59,10 @@ declare class WebMCP {
|
|||||||
connect(connectionToken: string): Promise<void>;
|
connect(connectionToken: string): Promise<void>;
|
||||||
disconnect(): void;
|
disconnect(): void;
|
||||||
|
|
||||||
|
on<K extends keyof WebMCPEventMap>(event: K, callback: (data: WebMCPEventMap[K]) => void): () => void;
|
||||||
|
off<K extends keyof WebMCPEventMap>(event: K, callback: (data: WebMCPEventMap[K]) => void): void;
|
||||||
|
getConnectionInfo(): ConnectionInfo;
|
||||||
|
|
||||||
registerTool(name: string, description: string, schema: ToolInputSchema, executeFn: ToolHandler): void;
|
registerTool(name: string, description: string, schema: ToolInputSchema, executeFn: ToolHandler): void;
|
||||||
unregisterTool(name: string): void;
|
unregisterTool(name: string): void;
|
||||||
unregisterAllTools(): void;
|
unregisterAllTools(): void;
|
||||||
|
|||||||
@@ -15,9 +15,14 @@ class WebMCP {
|
|||||||
size: '30px',
|
size: '30px',
|
||||||
padding: '20px',
|
padding: '20px',
|
||||||
inactivityTimeout: 5 * 60 * 1000, // 5 minutes in milliseconds
|
inactivityTimeout: 5 * 60 * 1000, // 5 minutes in milliseconds
|
||||||
|
headless: false,
|
||||||
...options
|
...options
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Event emitter
|
||||||
|
this._listeners = {};
|
||||||
|
this._currentStatus = 'disconnected';
|
||||||
|
|
||||||
// State variables
|
// State variables
|
||||||
this.isConnected = false;
|
this.isConnected = false;
|
||||||
this.isExpanded = false;
|
this.isExpanded = false;
|
||||||
@@ -52,6 +57,62 @@ class WebMCP {
|
|||||||
this._init();
|
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) {
|
_format(s) {
|
||||||
return s.replace(/[.:]/g, '_');
|
return s.replace(/[.:]/g, '_');
|
||||||
}
|
}
|
||||||
@@ -61,18 +122,20 @@ class WebMCP {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_init() {
|
_init() {
|
||||||
// Check if already initialized on this page
|
if (!this.options.headless) {
|
||||||
if (document.querySelector('[data-webmcp-widget]')) {
|
// Check if already initialized on this page
|
||||||
console.warn('WebMCP widget already initialized on this page');
|
if (document.querySelector('[data-webmcp-widget]')) {
|
||||||
return;
|
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
|
// Start inactivity timer
|
||||||
this._resetInactivityTimer();
|
this._resetInactivityTimer();
|
||||||
|
|
||||||
@@ -668,6 +731,9 @@ class WebMCP {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_updateStatus(status, message) {
|
_updateStatus(status, message) {
|
||||||
|
this._currentStatus = status;
|
||||||
|
this._emit('statusChange', { status, message: message || status });
|
||||||
|
|
||||||
const container = document.getElementById(this.elementId);
|
const container = document.getElementById(this.elementId);
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
@@ -1147,6 +1213,7 @@ class WebMCP {
|
|||||||
this._reconnectAttempts = 0;
|
this._reconnectAttempts = 0;
|
||||||
this._updateStatus('connected', `Connected to ${this.currentChannel}`);
|
this._updateStatus('connected', `Connected to ${this.currentChannel}`);
|
||||||
this._updateConnectionUI(true);
|
this._updateConnectionUI(true);
|
||||||
|
this._emit('connected', { channel: this.currentChannel, server: this.currentServer });
|
||||||
console.log('WebMCP connection established');
|
console.log('WebMCP connection established');
|
||||||
this._registerItemsWithServer();
|
this._registerItemsWithServer();
|
||||||
});
|
});
|
||||||
@@ -1160,6 +1227,7 @@ class WebMCP {
|
|||||||
if (event.code === 1000) {
|
if (event.code === 1000) {
|
||||||
this._updateStatus('disconnected', 'Disconnected');
|
this._updateStatus('disconnected', 'Disconnected');
|
||||||
this._updateConnectionUI(false);
|
this._updateConnectionUI(false);
|
||||||
|
this._emit('disconnected', { code: event.code, reason: event.reason });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1169,6 +1237,11 @@ class WebMCP {
|
|||||||
const delay = this._reconnectDelay * this._reconnectAttempts;
|
const delay = this._reconnectDelay * this._reconnectAttempts;
|
||||||
console.log(`Reconnecting (attempt ${this._reconnectAttempts}/${this._maxReconnectAttempts}) in ${delay}ms...`);
|
console.log(`Reconnecting (attempt ${this._reconnectAttempts}/${this._maxReconnectAttempts}) in ${delay}ms...`);
|
||||||
this._updateStatus('connecting', `Reconectando (${this._reconnectAttempts}/${this._maxReconnectAttempts})...`);
|
this._updateStatus('connecting', `Reconectando (${this._reconnectAttempts}/${this._maxReconnectAttempts})...`);
|
||||||
|
this._emit('reconnecting', {
|
||||||
|
attempt: this._reconnectAttempts,
|
||||||
|
maxAttempts: this._maxReconnectAttempts,
|
||||||
|
delay
|
||||||
|
});
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.connect(this._lastConnectionToken);
|
this.connect(this._lastConnectionToken);
|
||||||
}, delay);
|
}, delay);
|
||||||
@@ -1178,6 +1251,7 @@ class WebMCP {
|
|||||||
// Max retries exceeded or no token — give up
|
// Max retries exceeded or no token — give up
|
||||||
this._updateStatus('disconnected', 'Disconnected');
|
this._updateStatus('disconnected', 'Disconnected');
|
||||||
this._updateConnectionUI(false);
|
this._updateConnectionUI(false);
|
||||||
|
this._emit('disconnected', { code: event.code, reason: event.reason });
|
||||||
this.currentToken = '';
|
this.currentToken = '';
|
||||||
this.currentServer = '';
|
this.currentServer = '';
|
||||||
this.currentChannel = '';
|
this.currentChannel = '';
|
||||||
@@ -1234,6 +1308,7 @@ class WebMCP {
|
|||||||
|
|
||||||
case 'toolRegistered':
|
case 'toolRegistered':
|
||||||
console.log(`Tool registered with server: ${message.name}`);
|
console.log(`Tool registered with server: ${message.name}`);
|
||||||
|
this._emit('toolRegistered', { name: message.name });
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'promptRegistered':
|
case 'promptRegistered':
|
||||||
@@ -1298,6 +1373,7 @@ class WebMCP {
|
|||||||
this.registeredTools.delete(message.name);
|
this.registeredTools.delete(message.name);
|
||||||
this._saveItemsToStorage();
|
this._saveItemsToStorage();
|
||||||
this._updateToolsList();
|
this._updateToolsList();
|
||||||
|
this._emit('toolRemoved', { name: message.name });
|
||||||
console.log(`Tool removed by server: ${message.name}`);
|
console.log(`Tool removed by server: ${message.name}`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -1307,6 +1383,7 @@ class WebMCP {
|
|||||||
this.registeredTools.clear();
|
this.registeredTools.clear();
|
||||||
this._saveItemsToStorage();
|
this._saveItemsToStorage();
|
||||||
this._updateToolsList();
|
this._updateToolsList();
|
||||||
|
this._emit('toolRemoved', { name: '*' });
|
||||||
console.log('All tools removed by server');
|
console.log('All tools removed by server');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -1336,6 +1413,7 @@ class WebMCP {
|
|||||||
|
|
||||||
case 'error':
|
case 'error':
|
||||||
console.error(`Server error: ${message.message}`);
|
console.error(`Server error: ${message.message}`);
|
||||||
|
this._emit('error', { message: message.message });
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -1361,6 +1439,7 @@ class WebMCP {
|
|||||||
type: 'toolResponse',
|
type: 'toolResponse',
|
||||||
result: { content: [{ type: 'text', text: `Herramienta "${name}" registrada exitosamente` }] }
|
result: { content: [{ type: 'text', text: `Herramienta "${name}" registrada exitosamente` }] }
|
||||||
});
|
});
|
||||||
|
this._emit('toolCreated', { name });
|
||||||
console.log(`Tool created by agent: ${name}`);
|
console.log(`Tool created by agent: ${name}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this._sendMessage({
|
this._sendMessage({
|
||||||
|
|||||||
Reference in New Issue
Block a user