Files
webmcp/build/webmcp.js
josedario87 603c547bfe Built-in tool _webmcp_browser-info para ver navegadores conectados
Agrega metadata tracking de browsers (userAgent, URL, hostname,
idioma, resolucion, timestamp) y tool browser-info para consultarla.
2026-02-13 00:36:51 -06:00

1764 lines
60 KiB
JavaScript

(() => {
// 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.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._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._updateStatus("connected", `Connected to ${this.currentChannel}`);
this._updateConnectionUI(true);
console.log("WebMCP connection established");
this._registerItemsWithServer();
});
this.socket.addEventListener("close", (event) => {
this.isConnected = false;
this._updateStatus("disconnected", "Disconnected");
this._updateConnectionUI(false);
console.log(`Connection closed: ${event.code} ${event.reason}`);
if (event.code === 1001 || event.code === 401) {
this._updateStatus("disconnected", "Authorization failed");
this.currentToken = "";
this.currentServer = "";
this.currentChannel = "";
sessionStorage.removeItem(this.SESSION_STORAGE_KEY);
}
});
this.socket.addEventListener("error", () => {
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