- Retry con backoff (hasta 3 intentos, delay incremental) - Suprimir errores de consola durante reintentos - Mostrar estado "Reconectando" en el widget - Si todos los reintentos fallan, limpiar sesion y mostrar form de token
1783 lines
60 KiB
JavaScript
1783 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._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
|