(() => { // src/webmcp.js var WebMCP = class { constructor(options = {}) { this.options = { color: "#007bff", position: "bottom-right", size: "30px", padding: "20px", inactivityTimeout: 5 * 60 * 1e3, // 5 minutes in milliseconds ...options }; this.isConnected = false; this.isExpanded = false; this.socket = null; this.inactivityTimer = null; this.availableTools = /* @__PURE__ */ new Map(); this.availablePrompts = /* @__PURE__ */ new Map(); this.availableResources = /* @__PURE__ */ new Map(); this.samplingCallbacks = /* @__PURE__ */ new Map(); this.currentToken = ""; this.currentServer = ""; this.currentChannel = ""; this.elementId = "webmcp-widget-" + Math.random().toString(36).substr(2, 9); this.registeredTools = /* @__PURE__ */ new Set(); this.registeredPrompts = /* @__PURE__ */ new Set(); this.registeredResources = /* @__PURE__ */ new Set(); this._reconnectAttempts = 0; this._maxReconnectAttempts = 3; this._reconnectDelay = 1e3; this._lastConnectionToken = null; this.SESSION_STORAGE_KEY = "webmcp_token"; this.TOOLS_STORAGE_KEY = "webmcp_tools"; this.PROMPTS_STORAGE_KEY = "webmcp_prompts"; this.RESOURCES_STORAGE_KEY = "webmcp_resources"; this.REGISTER_PATH = "/register"; this._init(); } _format(s) { return s.replace(/[.:]/g, "_"); } /** * Initialize the WebMCP widget * @private */ _init() { if (document.querySelector("[data-webmcp-widget]")) { console.warn("WebMCP widget already initialized on this page"); return; } this._createWidget(); this._setupEventListeners(); this._resetInactivityTimer(); this._checkStoredToken(); } /** * Check for stored connection info in sessionStorage and connect if found * @private */ _checkStoredToken() { const storedConnectionInfo = sessionStorage.getItem(this.SESSION_STORAGE_KEY); if (storedConnectionInfo) { try { const connectionInfo = JSON.parse(storedConnectionInfo); if (connectionInfo.token) { console.log("Found stored connection info, attempting to connect"); this.currentServer = connectionInfo.server; this.currentChannel = `/${connectionInfo.channelHost || this._format(window.location.host)}`; if (connectionInfo.token.includes("{")) { const tokenData = JSON.parse(connectionInfo.token); this.currentToken = tokenData.token; } else { try { const jsonStr = atob(connectionInfo.token); const tokenData = JSON.parse(jsonStr); this.currentToken = tokenData.token; } catch (e) { this.currentToken = connectionInfo.token; } } this._loadStoredItems(); this.connect(connectionInfo.token); } } catch (error) { console.error("Error parsing stored connection info:", error); sessionStorage.removeItem(this.SESSION_STORAGE_KEY); this._clearStoredItems(); } } } /** * Save tools, prompts, and resources to session storage * @private */ _saveItemsToStorage() { try { const toolsData = {}; this.availableTools.forEach((tool, name) => { toolsData[name] = { name: tool.name, description: tool.description, inputSchema: tool.inputSchema // We don't store the execution function as it can't be serialized }; }); sessionStorage.setItem(this.TOOLS_STORAGE_KEY, JSON.stringify(toolsData)); const promptsData = {}; this.availablePrompts.forEach((prompt, name) => { promptsData[name] = { name: prompt.name, description: prompt.description, arguments: prompt.arguments // We don't store the execution function as it can't be serialized }; }); sessionStorage.setItem(this.PROMPTS_STORAGE_KEY, JSON.stringify(promptsData)); const resourcesData = {}; this.availableResources.forEach((resource, name) => { resourcesData[name] = { name: resource.name, description: resource.description, uri: resource.uri, uriTemplate: resource.uriTemplate, isTemplate: resource.isTemplate, mimeType: resource.mimeType // We don't store the provide function as it can't be serialized }; }); sessionStorage.setItem(this.RESOURCES_STORAGE_KEY, JSON.stringify(resourcesData)); console.log("Saved items to session storage:", { tools: Object.keys(toolsData).length, prompts: Object.keys(promptsData).length, resources: Object.keys(resourcesData).length }); } catch (error) { console.error("Error saving items to session storage:", error); } } /** * Load tools, prompts, and resources from session storage * @private */ _loadStoredItems() { try { const storedTools = sessionStorage.getItem(this.TOOLS_STORAGE_KEY); if (storedTools) { const toolsData = JSON.parse(storedTools); Object.entries(toolsData).forEach(([name, tool]) => { this.availableTools.set(name, { ...tool, execute: function(args) { console.warn(`Tool ${name} was loaded from storage but has not been re-registered with an execution function`); return `Tool ${name} needs to be re-registered`; } }); }); } const storedPrompts = sessionStorage.getItem(this.PROMPTS_STORAGE_KEY); if (storedPrompts) { const promptsData = JSON.parse(storedPrompts); Object.entries(promptsData).forEach(([name, prompt]) => { this.availablePrompts.set(name, { ...prompt, execute: function(args) { console.warn(`Prompt ${name} was loaded from storage but has not been re-registered with an execution function`); return { messages: [{ role: "user", content: { type: "text", text: `Prompt ${name} needs to be re-registered` } }] }; } }); }); } const storedResources = sessionStorage.getItem(this.RESOURCES_STORAGE_KEY); if (storedResources) { const resourcesData = JSON.parse(storedResources); Object.entries(resourcesData).forEach(([name, resource]) => { this.availableResources.set(name, { ...resource, provide: function(uri) { console.warn(`Resource ${name} was loaded from storage but has not been re-registered with a provider function`); return { contents: [{ uri, text: `Resource ${name} needs to be re-registered`, mimeType: resource.mimeType || "text/plain" }] }; } }); }); } console.log("Loaded items from session storage:", { tools: this.availableTools.size, prompts: this.availablePrompts.size, resources: this.availableResources.size }); this._updateToolsList(); this._updatePromptsList(); this._updateResourcesList(); } catch (error) { console.error("Error loading items from session storage:", error); this._clearStoredItems(); } } /** * Clear all stored items from session storage * @private */ _clearStoredItems() { sessionStorage.removeItem(this.TOOLS_STORAGE_KEY); sessionStorage.removeItem(this.PROMPTS_STORAGE_KEY); sessionStorage.removeItem(this.RESOURCES_STORAGE_KEY); console.log("Cleared stored items from session storage"); } /** * Create and inject the WebMCP widget into the DOM * @private */ _createWidget() { const container = document.createElement("div"); container.id = this.elementId; container.dataset.webmcpWidget = true; Object.assign(container.style, { position: "fixed", zIndex: "9999", display: "flex", flexDirection: "column", fontFamily: "Arial, sans-serif", fontSize: "14px", transition: "all 0.3s ease" }); this._setWidgetPosition(container); const triggerButton = document.createElement("div"); triggerButton.className = "webmcp-trigger"; Object.assign(triggerButton.style, { width: this.options.size, height: this.options.size, backgroundColor: this.options.color, borderRadius: "4px", cursor: "pointer", boxShadow: "0 2px 10px rgba(0,0,0,0.2)", display: "flex", justifyContent: "center", alignItems: "center", alignSelf: "flex-end" }); const contentPanel = document.createElement("div"); contentPanel.className = "webmcp-content"; Object.assign(contentPanel.style, { backgroundColor: "#ffffff", border: "1px solid #e1e1e1", borderRadius: "5px", padding: "15px", marginBottom: "10px", boxShadow: "0 5px 15px rgba(0,0,0,0.1)", width: "250px", display: "none", overflow: "hidden", position: "absolute", bottom: "40px" }); const header = document.createElement("div"); Object.assign(header.style, { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "15px" }); const title = document.createElement("div"); title.textContent = "WebMCP"; Object.assign(title.style, { fontWeight: "bold", fontSize: "16px" }); const closeButton = document.createElement("button"); closeButton.innerHTML = "×"; closeButton.className = "webmcp-close"; Object.assign(closeButton.style, { background: "none", border: "none", cursor: "pointer", fontSize: "20px", padding: "0", lineHeight: "1", color: "#999" }); header.appendChild(title); header.appendChild(closeButton); contentPanel.appendChild(header); this._createConnectionForm(contentPanel); const statusIndicator = document.createElement("div"); statusIndicator.className = "webmcp-status"; statusIndicator.textContent = "Disconnected"; Object.assign(statusIndicator.style, { padding: "8px", borderRadius: "3px", backgroundColor: "#f8d7da", color: "#721c24", textAlign: "center", marginBottom: "10px", fontSize: "12px" }); contentPanel.appendChild(statusIndicator); const connectionPanel = document.createElement("div"); connectionPanel.className = "webmcp-connection-panel"; contentPanel.appendChild(connectionPanel); const registeredItemsContainer = document.createElement("div"); registeredItemsContainer.className = "webmcp-registered-items"; Object.assign(registeredItemsContainer.style, { marginTop: "15px", fontSize: "12px", display: "none", maxHeight: "200px", overflow: "auto", border: "1px solid #eee", borderRadius: "4px" }); contentPanel.appendChild(registeredItemsContainer); const toolsList = document.createElement("div"); toolsList.className = "webmcp-tools-list"; Object.assign(toolsList.style, { padding: "10px", borderBottom: "1px solid #eee" }); const toolsHeader = document.createElement("div"); toolsHeader.textContent = "Registered Tools:"; Object.assign(toolsHeader.style, { fontWeight: "bold", marginBottom: "5px" }); const toolsContainer = document.createElement("ul"); toolsContainer.className = "webmcp-tools-container"; Object.assign(toolsContainer.style, { listStyle: "none", padding: "0", margin: "0" }); toolsList.appendChild(toolsHeader); toolsList.appendChild(toolsContainer); registeredItemsContainer.appendChild(toolsList); const promptsList = document.createElement("div"); promptsList.className = "webmcp-prompts-list"; Object.assign(promptsList.style, { padding: "10px", borderBottom: "1px solid #eee" }); const promptsHeader = document.createElement("div"); promptsHeader.textContent = "Registered Prompts:"; Object.assign(promptsHeader.style, { fontWeight: "bold", marginBottom: "5px" }); const promptsContainer = document.createElement("ul"); promptsContainer.className = "webmcp-prompts-container"; Object.assign(promptsContainer.style, { listStyle: "none", padding: "0", margin: "0" }); promptsList.appendChild(promptsHeader); promptsList.appendChild(promptsContainer); registeredItemsContainer.appendChild(promptsList); const resourcesList = document.createElement("div"); resourcesList.className = "webmcp-resources-list"; Object.assign(resourcesList.style, { padding: "10px" }); const resourcesHeader = document.createElement("div"); resourcesHeader.textContent = "Registered Resources:"; Object.assign(resourcesHeader.style, { fontWeight: "bold", marginBottom: "5px" }); const resourcesContainer = document.createElement("ul"); resourcesContainer.className = "webmcp-resources-container"; Object.assign(resourcesContainer.style, { listStyle: "none", padding: "0", margin: "0" }); resourcesList.appendChild(resourcesHeader); resourcesList.appendChild(resourcesContainer); registeredItemsContainer.appendChild(resourcesList); container.appendChild(contentPanel); container.appendChild(triggerButton); document.body.appendChild(container); } /** * Set widget position based on option * @private */ _setWidgetPosition(container) { const { position, padding } = this.options; switch (position) { case "bottom-right": Object.assign(container.style, { bottom: padding, right: padding, alignItems: "flex-end" }); break; case "bottom-left": Object.assign(container.style, { bottom: padding, left: padding, alignItems: "flex-start" }); break; case "top-right": Object.assign(container.style, { top: padding, right: padding, alignItems: "flex-end" }); break; case "top-left": Object.assign(container.style, { top: padding, left: padding, alignItems: "flex-start" }); break; default: Object.assign(container.style, { bottom: padding, right: padding, alignItems: "flex-end" }); } } /** * Create the connection form * @private */ _createConnectionForm(container) { const form = document.createElement("div"); Object.assign(form.style, { marginBottom: "8px" }); const inputGroup = document.createElement("div"); Object.assign(inputGroup.style, { display: "flex", marginBottom: "8px" }); const tokenInput = document.createElement("input"); tokenInput.type = "text"; tokenInput.className = "webmcp-token-input"; tokenInput.placeholder = "Paste connection token"; Object.assign(tokenInput.style, { flex: "1", padding: "8px", border: "1px solid #ccc", borderRadius: "4px 0 0 4px", fontSize: "12px" }); const connectButton = document.createElement("button"); connectButton.className = "webmcp-connect-btn"; connectButton.textContent = "Connect"; Object.assign(connectButton.style, { padding: "8px 12px", backgroundColor: this.options.color, color: "white", border: "none", borderRadius: "0 4px 4px 0", cursor: "pointer", fontSize: "12px" }); inputGroup.appendChild(tokenInput); inputGroup.appendChild(connectButton); const disconnectButton = document.createElement("button"); disconnectButton.className = "webmcp-disconnect-btn"; disconnectButton.textContent = "Disconnect"; Object.assign(disconnectButton.style, { padding: "8px 12px", backgroundColor: "#dc3545", color: "white", border: "none", borderRadius: "4px", cursor: "pointer", fontSize: "12px", width: "100%", display: "none" }); form.appendChild(inputGroup); form.appendChild(disconnectButton); container.appendChild(form); } /** * Set up event listeners for the widget * @private */ _setupEventListeners() { const container = document.getElementById(this.elementId); if (!container) return; const trigger = container.querySelector(".webmcp-trigger"); trigger.addEventListener("click", () => { this._toggleExpanded(); }); const closeBtn = container.querySelector(".webmcp-close"); closeBtn.addEventListener("click", () => { this._toggleExpanded(false); }); const connectBtn = container.querySelector(".webmcp-connect-btn"); connectBtn.addEventListener("click", () => { const tokenInput = container.querySelector(".webmcp-token-input"); this.connect(tokenInput.value); }); const disconnectBtn = container.querySelector(".webmcp-disconnect-btn"); disconnectBtn.addEventListener("click", () => { this.disconnect(); }); document.addEventListener("mousemove", () => this._resetInactivityTimer()); document.addEventListener("keypress", () => this._resetInactivityTimer()); document.addEventListener("click", () => this._resetInactivityTimer()); document.addEventListener("scroll", () => this._resetInactivityTimer()); } /** * Toggle the expanded state of the widget * @private */ _toggleExpanded(force = null) { const container = document.getElementById(this.elementId); if (!container) return; const contentPanel = container.querySelector(".webmcp-content"); this.isExpanded = force !== null ? force : !this.isExpanded; if (this.isExpanded) { contentPanel.style.display = "block"; } else { contentPanel.style.display = "none"; } this._resetInactivityTimer(); } /** * Update the status indicator * @private */ _updateStatus(status, message) { const container = document.getElementById(this.elementId); if (!container) return; const statusIndicator = container.querySelector(".webmcp-status"); if (!statusIndicator) return; statusIndicator.classList.remove("connected", "disconnected", "connecting", "pending-auth"); statusIndicator.textContent = message || status; switch (status) { case "connected": Object.assign(statusIndicator.style, { backgroundColor: "#d4edda", color: "#155724" }); break; case "disconnected": Object.assign(statusIndicator.style, { backgroundColor: "#f8d7da", color: "#721c24" }); break; case "connecting": Object.assign(statusIndicator.style, { backgroundColor: "#fff3cd", color: "#856404" }); break; case "pending-auth": Object.assign(statusIndicator.style, { backgroundColor: "#d1ecf1", color: "#0c5460" }); break; } } /** * Update UI based on connection state * @private */ _updateConnectionUI(isConnected) { const container = document.getElementById(this.elementId); if (!container) return; const tokenInput = container.querySelector(".webmcp-token-input"); const connectBtn = container.querySelector(".webmcp-connect-btn"); const disconnectBtn = container.querySelector(".webmcp-disconnect-btn"); const registeredItemsContainer = container.querySelector(".webmcp-registered-items"); if (isConnected) { tokenInput.style.display = "none"; connectBtn.style.display = "none"; disconnectBtn.style.display = "block"; registeredItemsContainer.style.display = "block"; const trigger = container.querySelector(".webmcp-trigger"); trigger.innerHTML = "\u2713"; trigger.style.color = "white"; trigger.style.fontWeight = "bold"; } else { tokenInput.style.display = "block"; connectBtn.style.display = "block"; disconnectBtn.style.display = "none"; registeredItemsContainer.style.display = "none"; const trigger = container.querySelector(".webmcp-trigger"); trigger.innerHTML = ""; } } /** * Update tools list in UI * @private */ _updateToolsList() { const container = document.getElementById(this.elementId); if (!container) return; const toolsContainer = container.querySelector(".webmcp-tools-container"); if (!toolsContainer) return; toolsContainer.innerHTML = ""; if (this.availableTools.size === 0) { const emptyMessage = document.createElement("li"); emptyMessage.textContent = "No tools registered"; emptyMessage.style.fontStyle = "italic"; emptyMessage.style.color = "#666"; toolsContainer.appendChild(emptyMessage); return; } this.availableTools.forEach((tool, name) => { const toolItem = document.createElement("li"); Object.assign(toolItem.style, { padding: "5px 0", borderBottom: "1px solid #eee" }); const toolName = document.createElement("strong"); toolName.textContent = name; const toolDesc = document.createElement("div"); toolDesc.textContent = tool.description; toolDesc.style.fontSize = "10px"; toolDesc.style.color = "#666"; toolItem.appendChild(toolName); toolItem.appendChild(toolDesc); toolsContainer.appendChild(toolItem); }); } /** * Update prompts list in UI * @private */ _updatePromptsList() { const container = document.getElementById(this.elementId); if (!container) return; const promptsContainer = container.querySelector(".webmcp-prompts-container"); if (!promptsContainer) return; promptsContainer.innerHTML = ""; if (this.availablePrompts.size === 0) { const emptyMessage = document.createElement("li"); emptyMessage.textContent = "No prompts registered"; emptyMessage.style.fontStyle = "italic"; emptyMessage.style.color = "#666"; promptsContainer.appendChild(emptyMessage); return; } this.availablePrompts.forEach((prompt, name) => { const promptItem = document.createElement("li"); Object.assign(promptItem.style, { padding: "5px 0", borderBottom: "1px solid #eee" }); const promptName = document.createElement("strong"); promptName.textContent = name; const promptDesc = document.createElement("div"); promptDesc.textContent = prompt.description; promptDesc.style.fontSize = "10px"; promptDesc.style.color = "#666"; promptItem.appendChild(promptName); promptItem.appendChild(promptDesc); promptsContainer.appendChild(promptItem); }); } /** * Update resources list in UI * @private */ _updateResourcesList() { const container = document.getElementById(this.elementId); if (!container) return; const resourcesContainer = container.querySelector(".webmcp-resources-container"); if (!resourcesContainer) return; resourcesContainer.innerHTML = ""; if (this.availableResources.size === 0) { const emptyMessage = document.createElement("li"); emptyMessage.textContent = "No resources registered"; emptyMessage.style.fontStyle = "italic"; emptyMessage.style.color = "#666"; resourcesContainer.appendChild(emptyMessage); return; } this.availableResources.forEach((resource, name) => { const resourceItem = document.createElement("li"); Object.assign(resourceItem.style, { padding: "5px 0", borderBottom: "1px solid #eee" }); const resourceName = document.createElement("strong"); resourceName.textContent = name; const resourceDesc = document.createElement("div"); resourceDesc.textContent = resource.description + (resource.isTemplate ? " (Template)" : ""); resourceDesc.style.fontSize = "10px"; resourceDesc.style.color = "#666"; resourceItem.appendChild(resourceName); resourceItem.appendChild(resourceDesc); resourcesContainer.appendChild(resourceItem); }); } /** * Reset the inactivity timer * @private */ _resetInactivityTimer() { if (this.inactivityTimer) { clearTimeout(this.inactivityTimer); } this.inactivityTimer = setTimeout(() => { this._handleInactivity(); }, this.options.inactivityTimeout); } /** * Handle user inactivity * @private */ _handleInactivity() { console.log("Inactivity timeout reached, disconnecting"); if (this.isConnected) { this.disconnect(); } this._toggleExpanded(false); sessionStorage.removeItem(this.SESSION_STORAGE_KEY); } /** * Connect to the WebSocket server * @public * @param {string} connectionToken - The encoded connection token */ async connect(connectionToken) { if (!connectionToken) { this._updateStatus("disconnected", "Error: No token provided"); return; } this._lastConnectionToken = connectionToken; this._updateStatus("connecting", "Connecting..."); try { if (!this._processConnectionToken(connectionToken)) { return; } const connectionInfo = { token: connectionToken, server: this.currentServer, host: this._format(window.location.host) }; const storedConnectionInfo = sessionStorage.getItem(this.SESSION_STORAGE_KEY); let skipRegistration = false; if (storedConnectionInfo) { try { const connectionInfo2 = JSON.parse(storedConnectionInfo); if (connectionInfo2.server === this.currentServer && connectionInfo2.host === this._format(window.location.host)) { skipRegistration = true; } } catch (error) { console.error("Error parsing stored connection info:", error); } } if (!skipRegistration) { const response = await this._registerWithServer(connectionToken); if (!response.token) { this._updateStatus("disconnected", "Registration failed"); return; } connectionInfo.token = response.token; this.currentToken = response.token; sessionStorage.setItem(this.SESSION_STORAGE_KEY, JSON.stringify(connectionInfo)); } const serverUrl = `${this.currentServer}${this.currentChannel}?token=${this.currentToken}`; this._updateStatus("connecting", "Connecting to channel..."); this.socket = new WebSocket(serverUrl); this._setupSocketListeners(); this._resetInactivityTimer(); } catch (error) { console.error("Connection error:", error); this._updateStatus("disconnected", `Error: ${error.message}`); } } /** * Disconnect from WebSocket server * @public */ disconnect() { if (this.socket) { this.socket.close(); this.socket = null; } this.isConnected = false; this._updateStatus("disconnected", "Disconnected"); this._updateConnectionUI(false); this.currentToken = ""; this.currentServer = ""; this.currentChannel = ""; sessionStorage.removeItem(this.SESSION_STORAGE_KEY); this._clearStoredItems(); } /** * Process connection token * @private * @param {string} encodedToken - The encoded connection token * @returns {boolean} - True if processing was successful */ _processConnectionToken(encodedToken) { try { const jsonStr = atob(encodedToken); const connectionData = JSON.parse(jsonStr); const { server, token } = connectionData; if (!server || !token) { this._updateStatus("disconnected", "Invalid token"); return false; } this.currentServer = server; this.currentToken = token; this.currentChannel = `/${this._format(window.location.host)}`; return true; } catch (error) { this._updateStatus("disconnected", `Unable to parse token`); return false; } } /** * Register with server using connection token * @private * @param {string} encodedToken - The encoded connection token * @returns {Promise<{ token: string }>} - Resolves to true if registration was successful */ _registerWithServer(encodedToken) { this._updateStatus("pending-auth", "Registering..."); const regSocket = new WebSocket(`${this.currentServer}${this.REGISTER_PATH}`); return new Promise((resolve, reject) => { regSocket.addEventListener("open", (event) => { console.log("Registration connection established"); const jsonStr = atob(encodedToken); const connectionData = JSON.parse(jsonStr); connectionData.host = this._format(window.location.host); regSocket.send(btoa(JSON.stringify(connectionData))); }); regSocket.addEventListener("message", (event) => { try { const message = JSON.parse(event.data); if (message.type === "registerSuccess" && message.token) { console.log(`Registration successful: ${message.message}`); resolve({ token: message.token }); } else if (message.type === "error") { console.error(`Registration failed: ${message.message}`); this._updateStatus("disconnected", `Registration failed: ${message.message}`); reject(new Error(message.message)); } } catch (error) { console.error(`Error parsing registration response: ${error.message}`); this._updateStatus("disconnected", "Error parsing server response"); reject(error); } }); regSocket.addEventListener("error", (event) => { console.error("Registration connection error"); this._updateStatus("disconnected", "Registration connection error"); sessionStorage.removeItem(this.SESSION_STORAGE_KEY); reject(new Error("Connection error")); }); regSocket.addEventListener("close", (event) => { console.log(`Registration connection closed: ${event.code} ${event.reason}`); if (event.code !== 1e3) { this._updateStatus("disconnected", "Registration failed"); sessionStorage.removeItem(this.SESSION_STORAGE_KEY); reject(new Error("Connection closed")); } }); }); } /** * Set up WebSocket event listeners for direct connection * @private */ _setupSocketListeners() { if (!this.socket) { console.error("Cannot set up socket listeners: WebSocket not available"); return; } this.socket.addEventListener("open", () => { this.isConnected = true; this._reconnectAttempts = 0; this._updateStatus("connected", `Connected to ${this.currentChannel}`); this._updateConnectionUI(true); console.log("WebMCP connection established"); this._registerItemsWithServer(); }); this.socket.addEventListener("close", (event) => { this.isConnected = false; console.log(`Connection closed: ${event.code} ${event.reason}`); if (event.code === 1e3) { this._updateStatus("disconnected", "Disconnected"); this._updateConnectionUI(false); return; } if (this._reconnectAttempts < this._maxReconnectAttempts && this._lastConnectionToken) { this._reconnectAttempts++; 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})...`); setTimeout(() => { this.connect(this._lastConnectionToken); }, delay); return; } this._updateStatus("disconnected", "Disconnected"); this._updateConnectionUI(false); this.currentToken = ""; this.currentServer = ""; this.currentChannel = ""; sessionStorage.removeItem(this.SESSION_STORAGE_KEY); }); this.socket.addEventListener("error", () => { if (this._reconnectAttempts > 0) return; console.error("WebSocket error"); if (this.isConnected) { this._updateStatus("disconnected", "Connection error occurred"); } else { this._updateStatus("disconnected", "Connection failed"); } sessionStorage.removeItem(this.SESSION_STORAGE_KEY); }); this.socket.addEventListener("message", (event) => { try { const message = JSON.parse(event.data); this._handleServerMessage(message); } catch (error) { console.error(`Error parsing message: ${error.message}`); } }); } /** * Handle messages from the server * @private * @param {Object} message - The parsed message object */ _handleServerMessage(message) { switch (message.type) { case "welcome": console.log(`Server says: ${message.message}`); this._sendMessage({ type: "clientInfo", userAgent: navigator.userAgent, url: window.location.href, hostname: window.location.hostname, language: navigator.language, screenWidth: window.innerWidth, screenHeight: window.innerHeight, timestamp: Date.now() }); break; case "toolRegistered": console.log(`Tool registered with server: ${message.name}`); break; case "promptRegistered": console.log(`Prompt registered with server: ${message.name}`); break; case "resourceRegistered": console.log(`Resource registered with server: ${message.name}`); break; case "callTool": this._handleToolCall(message); break; case "getPrompt": this._handleGetPrompt(message); break; case "readResource": this._handleReadResource(message); break; case "createSamplingMessage": this._handleCreateSamplingMessage(message); break; case "listTools": this._sendToolsList(message.id); break; case "listPrompts": this._sendPromptsList(message.id); break; case "listResources": this._sendResourcesList(message.id); break; case "ping": this._sendMessage({ type: "pong", id: message.id, timestamp: Date.now() }); break; case "createTool": this._handleCreateTool(message); break; case "removeTool": if (message.name && this.availableTools.has(message.name)) { this.availableTools.delete(message.name); this.registeredTools.delete(message.name); this._saveItemsToStorage(); this._updateToolsList(); console.log(`Tool removed by server: ${message.name}`); } break; case "removeAllTools": this.availableTools.clear(); this.registeredTools.clear(); this._saveItemsToStorage(); this._updateToolsList(); console.log("All tools removed by server"); break; case "getClientInfo": this._sendMessage({ id: message.id, type: "clientInfoResponse", userAgent: navigator.userAgent, url: window.location.href, hostname: window.location.hostname, language: navigator.language, screenWidth: window.innerWidth, screenHeight: window.innerHeight, timestamp: Date.now() }); break; case "clipboardCopy": if (message.text && navigator.clipboard) { navigator.clipboard.writeText(message.text).then(() => { console.log("Token copiado al portapapeles"); }).catch((err) => { console.error("Error copiando al portapapeles:", err); }); } break; case "error": console.error(`Server error: ${message.message}`); break; default: console.warn(`Unknown message type: ${message.type}`); } } /** * Handle createTool request from server (via built-in agregar-tool) * @private * @param {Object} message - The parsed message object */ _handleCreateTool(message) { const { id, name, description, code, parametros } = message; try { let props = {}; if (parametros) props = JSON.parse(parametros); const schema = { type: "object", properties: props }; const fn = new Function("args", code); this.registerTool(name, description, schema, fn); this._sendMessage({ id, type: "toolResponse", result: { content: [{ type: "text", text: `Herramienta "${name}" registrada exitosamente` }] } }); console.log(`Tool created by agent: ${name}`); } catch (e) { this._sendMessage({ id, type: "toolResponse", error: `Error creando herramienta: ${e.message}` }); } } /** * Handle tool call from server * @private * @param {Object} message - The parsed message object */ _handleToolCall(message) { const { id, tool, arguments: args } = message; console.log(`Tool call: ${tool} with args:`, args); if (!this.availableTools.has(tool)) { this._sendMessage({ id, type: "toolResponse", error: `Tool not found: ${tool}` }); return; } try { const toolObj = this.availableTools.get(tool); const wrapResult = (raw) => { if (raw && raw.content && Array.isArray(raw.content)) return raw; const text = typeof raw === "string" ? raw : JSON.stringify(raw); return { content: [{ type: "text", text }] }; }; const result = toolObj.execute(args); if (result instanceof Promise) { result.then((resolvedResult) => { this._sendMessage({ id, type: "toolResponse", result: wrapResult(resolvedResult) }); }).catch((error) => { this._sendMessage({ id, type: "toolResponse", error: error.message || "Tool execution error" }); }); } else { this._sendMessage({ id, type: "toolResponse", result: wrapResult(result) }); } console.log(`Tool response sent for ${tool}`); } catch (error) { this._sendMessage({ id, type: "toolResponse", error: error.message || "Tool execution error" }); console.error(`Tool execution error:`, error); } } /** * Handle prompt request from server * @private * @param {Object} message - The parsed message object */ _handleGetPrompt(message) { const { id, name, arguments: args } = message; console.log(`Prompt request: ${name} with args:`, args); if (!this.availablePrompts.has(name)) { this._sendMessage({ id, type: "promptResponse", error: `Prompt not found: ${name}` }); return; } try { const promptObj = this.availablePrompts.get(name); const result = promptObj.execute(args); if (result instanceof Promise) { result.then((resolvedResult) => { this._sendMessage({ id, type: "promptResponse", result: resolvedResult }); }).catch((error) => { this._sendMessage({ id, type: "promptResponse", error: error.message || "Prompt execution error" }); }); } else { this._sendMessage({ id, type: "promptResponse", result }); } console.log(`Prompt response sent for ${name}`); } catch (error) { this._sendMessage({ id, type: "promptResponse", error: error.message || "Prompt execution error" }); console.error(`Prompt execution error:`, error); } } /** * Handle resource request from server * @private * @param {Object} message - The parsed message object */ _handleReadResource(message) { const { id, uri } = message; console.log(`Resource request: ${uri}`); let resourceObj = null; for (const resource of this.availableResources.values()) { if (!resource.isTemplate && resource.uri === uri) { resourceObj = resource; break; } } if (!resourceObj) { for (const resource of this.availableResources.values()) { if (resource.isTemplate) { const templatePrefix = resource.uriTemplate.split("{")[0]; if (uri.startsWith(templatePrefix)) { resourceObj = resource; break; } } } } if (!resourceObj) { this._sendMessage({ id, type: "resourceResponse", error: `No resource handler found for URI: ${uri}` }); return; } try { const result = resourceObj.provide(uri); if (result instanceof Promise) { result.then((resolvedResult) => { this._sendMessage({ id, type: "resourceResponse", result: resolvedResult }); }).catch((error) => { this._sendMessage({ id, type: "resourceResponse", error: error.message || "Resource read error" }); }); } else { this._sendMessage({ id, type: "resourceResponse", result }); } console.log(`Resource response sent for ${uri}`); } catch (error) { this._sendMessage({ id, type: "resourceResponse", error: error.message || "Resource read error" }); console.error(`Resource read error:`, error); } } /** * Send available tools list * @private * @param {string} requestId - The request ID to respond to */ _sendToolsList(requestId) { const toolsList = Array.from(this.availableTools.values()).map((tool) => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema })); this._sendMessage({ id: requestId, type: "listToolsResponse", tools: toolsList }); console.log(`Sent tools list: ${toolsList.length} tools`); } /** * Send available prompts list * @private * @param {string} requestId - The request ID to respond to */ _sendPromptsList(requestId) { const promptsList = Array.from(this.availablePrompts.values()).map((prompt) => ({ name: prompt.name, description: prompt.description, arguments: prompt.arguments })); this._sendMessage({ id: requestId, type: "listPromptsResponse", prompts: promptsList }); console.log(`Sent prompts list: ${promptsList.length} prompts`); } /** * Send available resources list * @private * @param {string} requestId - The request ID to respond to */ _sendResourcesList(requestId) { const resources = []; const resourceTemplates = []; this.availableResources.forEach((resource) => { if (resource.isTemplate) { resourceTemplates.push({ name: resource.name, description: resource.description, uriTemplate: resource.uriTemplate, mimeType: resource.mimeType }); } else { resources.push({ name: resource.name, description: resource.description, uri: resource.uri, mimeType: resource.mimeType }); } }); this._sendMessage({ id: requestId, type: "listResourcesResponse", resources, resourceTemplates }); console.log(`Sent resources list: ${resources.length} resources, ${resourceTemplates.length} templates`); } /** * Send a message to the server via direct WebSocket * @private * @param {Object} message - The message object to send */ _sendMessage(message) { if (!this.isConnected || !this.socket) { console.error("Cannot send message: not connected"); return; } try { this.socket.send(JSON.stringify(message)); return Promise.resolve(); } catch (error) { console.error(`Error sending message: ${error.message}`); return Promise.reject(error); } } /** * Register all items with server that were registered while disconnected * @private */ _registerItemsWithServer() { if (!this.isConnected) return; this.registeredTools = /* @__PURE__ */ new Set(); this.registeredPrompts = /* @__PURE__ */ new Set(); this.registeredResources = /* @__PURE__ */ new Set(); this.availableTools.forEach((tool, name) => { this._sendMessage({ type: "registerTool", name, description: tool.description, inputSchema: tool.inputSchema }); this.registeredTools.add(name); console.log(`Registering tool with server: ${name}`); }); this.availablePrompts.forEach((prompt, name) => { this._sendMessage({ type: "registerPrompt", name, description: prompt.description, arguments: prompt.arguments }); this.registeredPrompts.add(name); console.log(`Registering prompt with server: ${name}`); }); this.availableResources.forEach((resource, name) => { this._sendMessage({ type: "registerResource", name, description: resource.description, uri: resource.uri, uriTemplate: resource.uriTemplate, isTemplate: resource.isTemplate, mimeType: resource.mimeType }); this.registeredResources.add(name); console.log(`Registering resource with server: ${name}`); }); } /** * Register a tool * @public * @param {string} name - The name of the tool * @param {string} description - The description of the tool * @param {Object} schema - The schema for the tool's input * @param {Function} executeFn - The function to execute when the tool is called */ registerTool(name, description, schema, executeFn) { if (!name) { console.error("Tool name is required"); return; } this.availableTools.set(name, { name, description: description || `Tool: ${name}`, execute: executeFn || function(args) { return `Default implementation of ${name} with args: ${JSON.stringify(args)}`; }, inputSchema: schema || { type: "object", properties: {} } }); if (this.isConnected) { this._sendMessage({ type: "registerTool", name, description: description || `Tool: ${name}`, inputSchema: schema || { type: "object", properties: {} } }); this.registeredTools.add(name); } this._saveItemsToStorage(); this._updateToolsList(); console.log(`Tool registered: ${name}`); } /** * Unregister a tool * @public * @param {string} name - The name of the tool to unregister */ unregisterTool(name) { if (!name) { console.error("Tool name is required"); return; } if (!this.availableTools.has(name)) { console.warn(`Tool not found: ${name}`); return; } this.availableTools.delete(name); if (this.isConnected) { this._sendMessage({ type: "unregisterTool", name }); } this.registeredTools.delete(name); this._saveItemsToStorage(); this._updateToolsList(); console.log(`Tool unregistered: ${name}`); } /** * Unregister all tools at once (single notification) * @public */ unregisterAllTools() { if (this.availableTools.size === 0) return; this.availableTools.clear(); this.registeredTools.clear(); if (this.isConnected) { this._sendMessage({ type: "unregisterAllTools" }); } this._saveItemsToStorage(); this._updateToolsList(); console.log("All tools unregistered"); } /** * Register a prompt * @public * @param {string} name - The name of the prompt * @param {string} description - The description of the prompt * @param {Array} promptArgs - The arguments for the prompt * @param {Function} executeFn - The function to execute when the prompt is called */ registerPrompt(name, description, promptArgs, executeFn) { if (!name) { console.error("Prompt name is required"); return; } this.availablePrompts.set(name, { name, description: description || `Prompt: ${name}`, execute: executeFn || function(args) { return { messages: [{ role: "user", content: { type: "text", text: `Default implementation of prompt ${name} with args: ${JSON.stringify(args)}` } }] }; }, arguments: promptArgs || [] }); if (this.isConnected) { this._sendMessage({ type: "registerPrompt", name, description: description || `Prompt: ${name}`, arguments: promptArgs || [] }); this.registeredPrompts.add(name); } this._saveItemsToStorage(); this._updatePromptsList(); console.log(`Prompt registered: ${name}`); } /** * Unregister a prompt * @public * @param {string} name - The name of the prompt to unregister */ unregisterPrompt(name) { if (!name) { console.error("Prompt name is required"); return; } if (!this.availablePrompts.has(name)) { console.warn(`Prompt not found: ${name}`); return; } this.availablePrompts.delete(name); if (this.isConnected) { this._sendMessage({ type: "unregisterPrompt", name }); } this.registeredPrompts.delete(name); this._saveItemsToStorage(); this._updatePromptsList(); console.log(`Prompt unregistered: ${name}`); } /** * Register a resource * @public * @param {string} name - The name of the resource * @param {string} description - The description of the resource * @param {Object} options - The resource options including uri, uriTemplate, and mimeType * @param {Function} provideFn - The function to execute when the resource is requested */ registerResource(name, description, options, provideFn) { if (!name) { console.error("Resource name is required"); return; } if (!options.uri && !options.uriTemplate) { console.error("Either uri or uriTemplate is required for a resource"); return; } const isTemplate = !!options.uriTemplate; this.availableResources.set(name, { name, description: description || `Resource: ${name}`, uri: options.uri, uriTemplate: options.uriTemplate, isTemplate, mimeType: options.mimeType, provide: provideFn || function(uri) { return { contents: [{ uri, text: `Default implementation of resource ${name} for URI: ${uri}`, mimeType: options.mimeType || "text/plain" }] }; } }); if (this.isConnected) { this._sendMessage({ type: "registerResource", name, description: description || `Resource: ${name}`, uri: options.uri, uriTemplate: options.uriTemplate, isTemplate, mimeType: options.mimeType }); this.registeredResources.add(name); } this._saveItemsToStorage(); this._updateResourcesList(); console.log(`Resource registered: ${name}`); } /** * Unregister a resource * @public * @param {string} name - The name of the resource to unregister */ unregisterResource(name) { if (!name) { console.error("Resource name is required"); return; } if (!this.availableResources.has(name)) { console.warn(`Resource not found: ${name}`); return; } this.availableResources.delete(name); if (this.isConnected) { this._sendMessage({ type: "unregisterResource", name }); } this.registeredResources.delete(name); this._saveItemsToStorage(); this._updateResourcesList(); console.log(`Resource unregistered: ${name}`); } /** * Handle sampling message creation request * @private * @param {Object} message - The parsed message object */ _handleCreateSamplingMessage(message) { const { id, messages, systemPrompt, includeContext, temperature, maxTokens, stopSequences, metadata, modelPreferences } = message; console.log(`Sampling request received with ${messages?.length || 0} messages`); const modal = document.createElement("div"); Object.assign(modal.style, { position: "fixed", top: "0", left: "0", width: "100%", height: "100%", backgroundColor: "rgba(0, 0, 0, 0.5)", display: "flex", justifyContent: "center", alignItems: "center", zIndex: "10000" }); const modalContent = document.createElement("div"); Object.assign(modalContent.style, { backgroundColor: "white", padding: "20px", borderRadius: "5px", maxWidth: "500px", width: "90%", maxHeight: "80%", overflow: "auto" }); const header = document.createElement("h3"); header.textContent = "Sampling Request"; Object.assign(header.style, { margin: "0 0 15px 0", padding: "0 0 10px 0", borderBottom: "1px solid #ddd" }); const content = document.createElement("div"); Object.assign(content.style, { marginBottom: "15px", maxHeight: "300px", overflow: "auto", border: "1px solid #ddd", padding: "10px", backgroundColor: "#f9f9f9" }); if (messages && messages.length > 0) { messages.forEach((msg) => { const msgDiv = document.createElement("div"); Object.assign(msgDiv.style, { marginBottom: "10px", padding: "5px", borderRadius: "3px", backgroundColor: msg.role === "user" ? "#e1f5fe" : "#f1f8e9" }); const roleSpan = document.createElement("strong"); roleSpan.textContent = msg.role === "user" ? "User: " : "Assistant: "; const contentSpan = document.createElement("span"); if (msg.content.type === "text") { contentSpan.textContent = msg.content.text; } else if (msg.content.type === "image") { contentSpan.textContent = "[Image data]"; } msgDiv.appendChild(roleSpan); msgDiv.appendChild(contentSpan); content.appendChild(msgDiv); }); } else { content.textContent = "No messages provided in sampling request"; } if (systemPrompt) { const sysPromptDiv = document.createElement("div"); Object.assign(sysPromptDiv.style, { marginBottom: "10px", padding: "5px", backgroundColor: "#fff8e1" }); const sysPromptLabel = document.createElement("strong"); sysPromptLabel.textContent = "System Prompt: "; const sysPromptContent = document.createElement("span"); sysPromptContent.textContent = systemPrompt; sysPromptDiv.appendChild(sysPromptLabel); sysPromptDiv.appendChild(sysPromptContent); content.appendChild(sysPromptDiv); } const responseLabel = document.createElement("label"); responseLabel.textContent = "Assistant Response:"; Object.assign(responseLabel.style, { display: "block", marginBottom: "5px", fontWeight: "bold" }); const responseInput = document.createElement("textarea"); Object.assign(responseInput.style, { width: "100%", minHeight: "100px", padding: "10px", marginBottom: "15px", boxSizing: "border-box" }); const buttonContainer = document.createElement("div"); Object.assign(buttonContainer.style, { display: "flex", justifyContent: "space-between" }); const submitButton = document.createElement("button"); submitButton.textContent = "Submit Response"; Object.assign(submitButton.style, { padding: "8px 15px", backgroundColor: "#4CAF50", color: "white", border: "none", borderRadius: "4px", cursor: "pointer" }); const cancelButton = document.createElement("button"); cancelButton.textContent = "Cancel"; Object.assign(cancelButton.style, { padding: "8px 15px", backgroundColor: "#f44336", color: "white", border: "none", borderRadius: "4px", cursor: "pointer" }); buttonContainer.appendChild(cancelButton); buttonContainer.appendChild(submitButton); modalContent.appendChild(header); modalContent.appendChild(content); modalContent.appendChild(responseLabel); modalContent.appendChild(responseInput); modalContent.appendChild(buttonContainer); modal.appendChild(modalContent); document.body.appendChild(modal); responseInput.focus(); submitButton.addEventListener("click", () => { const responseText = responseInput.value.trim(); if (responseText) { this._sendMessage({ id, type: "samplingResponse", result: { model: "web-user-input", role: "assistant", content: { type: "text", text: responseText } } }); document.body.removeChild(modal); } else { alert("Please enter a response"); } }); cancelButton.addEventListener("click", () => { this._sendMessage({ id, type: "samplingResponse", error: "User cancelled sampling request" }); document.body.removeChild(modal); }); } }; if (typeof module !== "undefined" && typeof module.exports !== "undefined") { module.exports = WebMCP; } })(); //# sourceMappingURL=webmcp.js.map