2327 lines
80 KiB
JavaScript
2327 lines
80 KiB
JavaScript
/**
|
||
* WebMCP - Snippet to add MCP functionality to any website
|
||
*
|
||
* Shows as a small blue square in bottom right corner
|
||
* On click, expands to allow connection with token
|
||
* Auto-disconnects after 5 minutes of inactivity
|
||
*/
|
||
|
||
class WebMCP {
|
||
constructor(options = {}) {
|
||
// Options with defaults
|
||
this.options = {
|
||
color: '#007bff',
|
||
position: 'bottom-right',
|
||
size: '30px',
|
||
padding: '20px',
|
||
inactivityTimeout: 5 * 60 * 1000, // 5 minutes in milliseconds
|
||
headless: false,
|
||
...options
|
||
};
|
||
|
||
// Event emitter
|
||
this._listeners = {};
|
||
this._currentStatus = 'disconnected';
|
||
|
||
// State variables
|
||
this.isConnected = false;
|
||
this.isExpanded = false;
|
||
this.socket = null;
|
||
this.inactivityTimer = null;
|
||
this.availableTools = new Map();
|
||
this.availablePrompts = new Map();
|
||
this.availableResources = new Map();
|
||
this.samplingCallbacks = new Map(); // For storing sampling callbacks
|
||
this.currentToken = '';
|
||
this.currentServer = '';
|
||
this.currentChannel = '';
|
||
this.elementId = 'webmcp-widget-' + Math.random().toString(36).substr(2, 9);
|
||
this.registeredTools = new Set();
|
||
this.registeredPrompts = new Set();
|
||
this.registeredResources = new Set();
|
||
this._reconnectAttempts = 0;
|
||
this._maxReconnectAttempts = 3;
|
||
this._reconnectDelay = 1000;
|
||
this._lastConnectionToken = null;
|
||
|
||
// Storage keys for sessionStorage
|
||
this.SESSION_STORAGE_KEY = 'webmcp_token';
|
||
this.TOOLS_STORAGE_KEY = 'webmcp_tools';
|
||
this.PROMPTS_STORAGE_KEY = 'webmcp_prompts';
|
||
this.RESOURCES_STORAGE_KEY = 'webmcp_resources';
|
||
|
||
// Constants
|
||
this.REGISTER_PATH = '/register';
|
||
|
||
// Initialize
|
||
this._init();
|
||
}
|
||
|
||
/**
|
||
* Register an event listener
|
||
* @public
|
||
* @param {string} event - Event name
|
||
* @param {Function} callback - Callback function
|
||
* @returns {Function} Unsubscribe function
|
||
*/
|
||
on(event, callback) {
|
||
if (!this._listeners[event]) {
|
||
this._listeners[event] = [];
|
||
}
|
||
this._listeners[event].push(callback);
|
||
return () => this.off(event, callback);
|
||
}
|
||
|
||
/**
|
||
* Remove an event listener
|
||
* @public
|
||
* @param {string} event - Event name
|
||
* @param {Function} callback - Callback function to remove
|
||
*/
|
||
off(event, callback) {
|
||
if (!this._listeners[event]) return;
|
||
this._listeners[event] = this._listeners[event].filter(cb => cb !== callback);
|
||
}
|
||
|
||
/**
|
||
* Emit an event to all registered listeners
|
||
* @private
|
||
* @param {string} event - Event name
|
||
* @param {Object} data - Event data
|
||
*/
|
||
_emit(event, data) {
|
||
if (!this._listeners[event]) return;
|
||
this._listeners[event].forEach(cb => {
|
||
try { cb(data); } catch (e) { console.error(`Error in ${event} listener:`, e); }
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Get current connection info snapshot
|
||
* @public
|
||
* @returns {Object} Connection state snapshot
|
||
*/
|
||
getConnectionInfo() {
|
||
return {
|
||
isConnected: this.isConnected,
|
||
channel: this.currentChannel,
|
||
server: this.currentServer,
|
||
status: this._currentStatus,
|
||
tools: Array.from(this.availableTools.keys()),
|
||
prompts: Array.from(this.availablePrompts.keys()),
|
||
resources: Array.from(this.availableResources.keys()),
|
||
};
|
||
}
|
||
|
||
_format(s) {
|
||
return s.replace(/[.:]/g, '_');
|
||
}
|
||
|
||
/**
|
||
* Initialize the WebMCP widget
|
||
* @private
|
||
*/
|
||
_init() {
|
||
if (!this.options.headless) {
|
||
// Check if already initialized on this page
|
||
if (document.querySelector('[data-webmcp-widget]')) {
|
||
console.warn('WebMCP widget already initialized on this page');
|
||
return;
|
||
}
|
||
|
||
// Create and inject the widget
|
||
this._createWidget();
|
||
|
||
// Set up event listeners
|
||
this._setupEventListeners();
|
||
}
|
||
|
||
// Start inactivity timer
|
||
this._resetInactivityTimer();
|
||
|
||
// Check for stored token and connect if available
|
||
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');
|
||
|
||
// Set the connection properties directly
|
||
this.currentServer = connectionInfo.server;
|
||
this.currentChannel = `/${connectionInfo.channelHost || this._format(window.location.host)}`;
|
||
|
||
// Set the current token from connection info
|
||
if (connectionInfo.token.includes('{')) {
|
||
// It's already parsed JSON
|
||
const tokenData = JSON.parse(connectionInfo.token);
|
||
this.currentToken = tokenData.token;
|
||
} else {
|
||
// It's a base64 encoded string
|
||
try {
|
||
const jsonStr = atob(connectionInfo.token);
|
||
const tokenData = JSON.parse(jsonStr);
|
||
this.currentToken = tokenData.token;
|
||
} catch (e) {
|
||
this.currentToken = connectionInfo.token;
|
||
}
|
||
}
|
||
|
||
// Load stored items before connecting
|
||
this._loadStoredItems();
|
||
|
||
// Connect using the stored token
|
||
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 {
|
||
// Save tools
|
||
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));
|
||
|
||
// Save prompts
|
||
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));
|
||
|
||
// Save resources
|
||
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 {
|
||
// Load tools
|
||
const storedTools = sessionStorage.getItem(this.TOOLS_STORAGE_KEY);
|
||
if (storedTools) {
|
||
const toolsData = JSON.parse(storedTools);
|
||
Object.entries(toolsData).forEach(([name, tool]) => {
|
||
// Add to the available tools with placeholder execute function
|
||
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`;
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// Load prompts
|
||
const storedPrompts = sessionStorage.getItem(this.PROMPTS_STORAGE_KEY);
|
||
if (storedPrompts) {
|
||
const promptsData = JSON.parse(storedPrompts);
|
||
Object.entries(promptsData).forEach(([name, prompt]) => {
|
||
// Add to the available prompts with placeholder execute function
|
||
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`
|
||
}
|
||
}]
|
||
};
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// Load resources
|
||
const storedResources = sessionStorage.getItem(this.RESOURCES_STORAGE_KEY);
|
||
if (storedResources) {
|
||
const resourcesData = JSON.parse(storedResources);
|
||
Object.entries(resourcesData).forEach(([name, resource]) => {
|
||
// Add to the available resources with placeholder provide function
|
||
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: 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
|
||
});
|
||
|
||
// Update the UI
|
||
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() {
|
||
// Create main container
|
||
const container = document.createElement('div');
|
||
container.id = this.elementId;
|
||
container.dataset.webmcpWidget = true;
|
||
|
||
// Apply styles
|
||
Object.assign(container.style, {
|
||
position: 'fixed',
|
||
zIndex: '9999',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
fontFamily: 'Arial, sans-serif',
|
||
fontSize: '14px',
|
||
transition: 'all 0.3s ease'
|
||
});
|
||
|
||
// Set position based on option
|
||
this._setWidgetPosition(container);
|
||
|
||
// Create trigger button (blue square)
|
||
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'
|
||
});
|
||
|
||
// Create content panel (initially hidden) - positioned above the trigger
|
||
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'
|
||
});
|
||
|
||
// Add header with title and close button
|
||
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 = '×'; // × symbol
|
||
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);
|
||
|
||
// Add connection form
|
||
this._createConnectionForm(contentPanel);
|
||
|
||
// Add status indicator
|
||
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);
|
||
|
||
// Add connection panel
|
||
const connectionPanel = document.createElement('div');
|
||
connectionPanel.className = 'webmcp-connection-panel';
|
||
contentPanel.appendChild(connectionPanel);
|
||
|
||
// Create a single container for all registered items
|
||
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);
|
||
|
||
// Add features lists (initially empty)
|
||
// Tools list
|
||
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);
|
||
|
||
// Prompts list
|
||
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);
|
||
|
||
// Resources list
|
||
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);
|
||
|
||
// Add to main container and then to document - content panel first so it appears above trigger
|
||
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:
|
||
// Default to bottom-right
|
||
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',
|
||
});
|
||
|
||
// Token input field
|
||
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;
|
||
|
||
// Trigger button click - expand/collapse
|
||
const trigger = container.querySelector('.webmcp-trigger');
|
||
trigger.addEventListener('click', () => {
|
||
this._toggleExpanded();
|
||
});
|
||
|
||
// Close button click - collapse
|
||
const closeBtn = container.querySelector('.webmcp-close');
|
||
closeBtn.addEventListener('click', () => {
|
||
this._toggleExpanded(false);
|
||
});
|
||
|
||
// Connect button click
|
||
const connectBtn = container.querySelector('.webmcp-connect-btn');
|
||
connectBtn.addEventListener('click', () => {
|
||
const tokenInput = container.querySelector('.webmcp-token-input');
|
||
this.connect(tokenInput.value);
|
||
});
|
||
|
||
// Disconnect button click
|
||
const disconnectBtn = container.querySelector('.webmcp-disconnect-btn');
|
||
disconnectBtn.addEventListener('click', () => {
|
||
this.disconnect();
|
||
});
|
||
|
||
// User activity detection to reset inactivity timer
|
||
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) {
|
||
this._currentStatus = status;
|
||
this._emit('statusChange', { status, message: message || status });
|
||
|
||
const container = document.getElementById(this.elementId);
|
||
if (!container) return;
|
||
|
||
const statusIndicator = container.querySelector('.webmcp-status');
|
||
if (!statusIndicator) return;
|
||
|
||
// Clear existing classes
|
||
statusIndicator.classList.remove('connected', 'disconnected', 'connecting', 'pending-auth');
|
||
|
||
// Set new status
|
||
statusIndicator.textContent = message || status;
|
||
|
||
// Apply styling based on 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';
|
||
|
||
// Update the trigger button to show connected state
|
||
const trigger = container.querySelector('.webmcp-trigger');
|
||
trigger.innerHTML = '✓';
|
||
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';
|
||
|
||
// Reset the trigger button
|
||
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;
|
||
|
||
// Clear current list
|
||
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;
|
||
}
|
||
|
||
// Add each tool to the list
|
||
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;
|
||
|
||
// Clear current list
|
||
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;
|
||
}
|
||
|
||
// Add each prompt to the list
|
||
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;
|
||
|
||
// Clear current list
|
||
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;
|
||
}
|
||
|
||
// Add each resource to the list
|
||
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() {
|
||
// Clear existing timer
|
||
if (this.inactivityTimer) {
|
||
clearTimeout(this.inactivityTimer);
|
||
}
|
||
|
||
// Set new timer
|
||
this.inactivityTimer = setTimeout(() => {
|
||
this._handleInactivity();
|
||
}, this.options.inactivityTimeout);
|
||
}
|
||
|
||
/**
|
||
* Handle user inactivity
|
||
* @private
|
||
*/
|
||
_handleInactivity() {
|
||
console.log('Inactivity timeout reached, disconnecting');
|
||
|
||
// Disconnect if connected
|
||
if (this.isConnected) {
|
||
this.disconnect();
|
||
}
|
||
|
||
// Minimize UI
|
||
this._toggleExpanded(false);
|
||
|
||
// Clear the stored token
|
||
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;
|
||
}
|
||
|
||
// Store for reconnection attempts
|
||
this._lastConnectionToken = connectionToken;
|
||
|
||
// Update UI to show connecting state
|
||
this._updateStatus('connecting', 'Connecting...');
|
||
|
||
try {
|
||
// Process the connection token
|
||
if (!this._processConnectionToken(connectionToken)) {
|
||
return;
|
||
}
|
||
|
||
// Store the connection info in sessionStorage for page navigations
|
||
const connectionInfo = {
|
||
token: connectionToken,
|
||
server: this.currentServer,
|
||
host: this._format(window.location.host)
|
||
};
|
||
|
||
// Check if we have connection data already in sessionStorage
|
||
const storedConnectionInfo = sessionStorage.getItem(this.SESSION_STORAGE_KEY);
|
||
let skipRegistration = false;
|
||
|
||
if (storedConnectionInfo) {
|
||
try {
|
||
const connectionInfo = JSON.parse(storedConnectionInfo);
|
||
// If we already have a valid token and server, we can skip registration
|
||
if (connectionInfo.server === this.currentServer &&
|
||
connectionInfo.host === this._format(window.location.host)) {
|
||
skipRegistration = true;
|
||
}
|
||
} catch (error) {
|
||
console.error('Error parsing stored connection info:', error);
|
||
}
|
||
}
|
||
|
||
if (!skipRegistration) {
|
||
// First register with server
|
||
const response = await this._registerWithServer(connectionToken);
|
||
|
||
if (!response.token) {
|
||
this._updateStatus('disconnected', 'Registration failed');
|
||
return;
|
||
}
|
||
|
||
// Save the new token
|
||
connectionInfo.token = response.token;
|
||
this.currentToken = response.token;
|
||
|
||
sessionStorage.setItem(this.SESSION_STORAGE_KEY, JSON.stringify(connectionInfo));
|
||
}
|
||
|
||
// Now connect to the actual channel
|
||
const serverUrl = `${this.currentServer}${this.currentChannel}?token=${this.currentToken}`;
|
||
|
||
// Update UI
|
||
this._updateStatus('connecting', 'Connecting to channel...');
|
||
|
||
// Create WebSocket connection with the path and token
|
||
this.socket = new WebSocket(serverUrl);
|
||
|
||
// Set up socket event listeners
|
||
this._setupSocketListeners();
|
||
|
||
// Reset inactivity timer
|
||
this._resetInactivityTimer();
|
||
|
||
} catch (error) {
|
||
console.error('Connection error:', error);
|
||
this._updateStatus('disconnected', `Error: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Disconnect from WebSocket server
|
||
* @public
|
||
*/
|
||
disconnect() {
|
||
// Close the WebSocket connection if it exists
|
||
if (this.socket) {
|
||
this.socket.close();
|
||
this.socket = null;
|
||
}
|
||
|
||
this.isConnected = false;
|
||
this._updateStatus('disconnected', 'Disconnected');
|
||
this._updateConnectionUI(false);
|
||
|
||
// Reset state
|
||
this.currentToken = '';
|
||
this.currentServer = '';
|
||
this.currentChannel = '';
|
||
|
||
// Remove the token from sessionStorage
|
||
sessionStorage.removeItem(this.SESSION_STORAGE_KEY);
|
||
|
||
// Clear items from sessionStorage
|
||
this._clearStoredItems();
|
||
}
|
||
|
||
/**
|
||
* Process connection token
|
||
* @private
|
||
* @param {string} encodedToken - The encoded connection token
|
||
* @returns {boolean} - True if processing was successful
|
||
*/
|
||
_processConnectionToken(encodedToken) {
|
||
try {
|
||
// Decode the base64 token
|
||
const jsonStr = atob(encodedToken);
|
||
const connectionData = JSON.parse(jsonStr);
|
||
|
||
// Extract server and token
|
||
const {server, token} = connectionData;
|
||
|
||
if (!server || !token) {
|
||
this._updateStatus('disconnected', 'Invalid token');
|
||
return false;
|
||
}
|
||
|
||
// Store connection info
|
||
this.currentServer = server;
|
||
this.currentToken = token;
|
||
|
||
// Format channel based on hostname
|
||
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) {
|
||
// Update UI
|
||
this._updateStatus('pending-auth', 'Registering...');
|
||
|
||
// Connect to the registration endpoint
|
||
const regSocket = new WebSocket(`${this.currentServer}${this.REGISTER_PATH}`);
|
||
|
||
return new Promise((resolve, reject) => {
|
||
// Connection opened - send the token
|
||
regSocket.addEventListener('open', (event) => {
|
||
console.log('Registration connection established');
|
||
|
||
// Send the original encoded token back to the server
|
||
const jsonStr = atob(encodedToken);
|
||
const connectionData = JSON.parse(jsonStr);
|
||
connectionData.host = this._format(window.location.host);
|
||
regSocket.send(btoa(JSON.stringify(connectionData)));
|
||
});
|
||
|
||
// Listen for registration response
|
||
regSocket.addEventListener('message', (event) => {
|
||
try {
|
||
const message = JSON.parse(event.data);
|
||
|
||
if (message.type === 'registerSuccess' && message.token) {
|
||
console.log(`Registration successful: ${message.message}`);
|
||
|
||
// Registration complete, can now connect to channel
|
||
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);
|
||
}
|
||
});
|
||
|
||
// Handle registration errors
|
||
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'));
|
||
});
|
||
|
||
// Handle registration connection close
|
||
regSocket.addEventListener('close', (event) => {
|
||
console.log(`Registration connection closed: ${event.code} ${event.reason}`);
|
||
|
||
if (event.code !== 1000) {
|
||
// If it wasn't a normal closure, show an error
|
||
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;
|
||
}
|
||
|
||
// Set up socket open handler
|
||
this.socket.addEventListener('open', () => {
|
||
this.isConnected = true;
|
||
this._reconnectAttempts = 0;
|
||
this._updateStatus('connected', `Connected to ${this.currentChannel}`);
|
||
this._updateConnectionUI(true);
|
||
this._emit('connected', { channel: this.currentChannel, server: this.currentServer });
|
||
console.log('WebMCP connection established');
|
||
this._registerItemsWithServer();
|
||
});
|
||
|
||
// Set up socket close handler
|
||
this.socket.addEventListener('close', (event) => {
|
||
this.isConnected = false;
|
||
console.log(`Connection closed: ${event.code} ${event.reason}`);
|
||
|
||
// Normal closure — no retry
|
||
if (event.code === 1000) {
|
||
this._updateStatus('disconnected', 'Disconnected');
|
||
this._updateConnectionUI(false);
|
||
this._emit('disconnected', { code: event.code, reason: event.reason });
|
||
return;
|
||
}
|
||
|
||
// Abnormal closure — try to reconnect
|
||
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})...`);
|
||
this._emit('reconnecting', {
|
||
attempt: this._reconnectAttempts,
|
||
maxAttempts: this._maxReconnectAttempts,
|
||
delay
|
||
});
|
||
setTimeout(() => {
|
||
this.connect(this._lastConnectionToken);
|
||
}, delay);
|
||
return;
|
||
}
|
||
|
||
// Max retries exceeded or no token — give up
|
||
this._updateStatus('disconnected', 'Disconnected');
|
||
this._updateConnectionUI(false);
|
||
this._emit('disconnected', { code: event.code, reason: event.reason });
|
||
this.currentToken = '';
|
||
this.currentServer = '';
|
||
this.currentChannel = '';
|
||
sessionStorage.removeItem(this.SESSION_STORAGE_KEY);
|
||
});
|
||
|
||
// Set up socket error handler
|
||
this.socket.addEventListener('error', () => {
|
||
// Suppress noisy errors during reconnection attempts
|
||
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);
|
||
});
|
||
|
||
// Set up socket message handler
|
||
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}`);
|
||
this._emit('toolRegistered', { name: 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':
|
||
// Server is asking us to execute a tool
|
||
this._handleToolCall(message);
|
||
break;
|
||
|
||
case 'getPrompt':
|
||
// Server is asking us to provide a prompt
|
||
this._handleGetPrompt(message);
|
||
break;
|
||
|
||
case 'readResource':
|
||
// Server is asking us to provide a resource
|
||
this._handleReadResource(message);
|
||
break;
|
||
|
||
case 'createSamplingMessage':
|
||
// Server is asking us to create a sampling message
|
||
this._handleCreateSamplingMessage(message);
|
||
break;
|
||
|
||
case 'listTools':
|
||
// Server is asking for available tools
|
||
this._sendToolsList(message.id);
|
||
break;
|
||
|
||
case 'listPrompts':
|
||
// Server is asking for available prompts
|
||
this._sendPromptsList(message.id);
|
||
break;
|
||
|
||
case 'listResources':
|
||
// Server is asking for available resources
|
||
this._sendResourcesList(message.id);
|
||
break;
|
||
|
||
case 'ping':
|
||
// Respond to 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();
|
||
this._emit('toolRemoved', { name: message.name });
|
||
console.log(`Tool removed by server: ${message.name}`);
|
||
}
|
||
break;
|
||
|
||
case 'removeAllTools':
|
||
this.availableTools.clear();
|
||
this.registeredTools.clear();
|
||
this._saveItemsToStorage();
|
||
this._updateToolsList();
|
||
this._emit('toolRemoved', { name: '*' });
|
||
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}`);
|
||
this._emit('error', { message: 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` }] }
|
||
});
|
||
this._emit('toolCreated', { name });
|
||
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;
|
||
}
|
||
|
||
// Execute the tool
|
||
try {
|
||
const toolObj = this.availableTools.get(tool);
|
||
|
||
// Normalize result to MCP content format
|
||
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 }] };
|
||
};
|
||
|
||
// Call the tool's execute function
|
||
const result = toolObj.execute(args);
|
||
|
||
// Handle promises
|
||
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 {
|
||
// Send immediate result
|
||
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;
|
||
}
|
||
|
||
// Execute the prompt
|
||
try {
|
||
const promptObj = this.availablePrompts.get(name);
|
||
|
||
// Call the prompt's execute function
|
||
const result = promptObj.execute(args);
|
||
|
||
// Handle promises
|
||
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 {
|
||
// Send immediate result
|
||
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}`);
|
||
|
||
// Find resource that handles this URI
|
||
let resourceObj = null;
|
||
|
||
// First check for direct URI match
|
||
for (const resource of this.availableResources.values()) {
|
||
if (!resource.isTemplate && resource.uri === uri) {
|
||
resourceObj = resource;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// If no direct match, check for template match
|
||
if (!resourceObj) {
|
||
for (const resource of this.availableResources.values()) {
|
||
if (resource.isTemplate) {
|
||
// Simple check - if URI starts with template prefix (before any parameters)
|
||
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;
|
||
}
|
||
|
||
// Execute the resource provider
|
||
try {
|
||
// Call the resource's provide function
|
||
const result = resourceObj.provide(uri);
|
||
|
||
// Handle promises
|
||
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 {
|
||
// Send immediate result
|
||
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 = [];
|
||
|
||
// Split resources and templates
|
||
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 {
|
||
// Send the message directly through the WebSocket
|
||
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;
|
||
|
||
// Clear registration tracking sets - we'll re-register everything
|
||
this.registeredTools = new Set();
|
||
this.registeredPrompts = new Set();
|
||
this.registeredResources = new Set();
|
||
|
||
// Register all tools with the server
|
||
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}`);
|
||
});
|
||
|
||
// Register all prompts with the server
|
||
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}`);
|
||
});
|
||
|
||
// Register all resources with the server
|
||
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;
|
||
}
|
||
|
||
// Add the tool to local registry
|
||
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: {}
|
||
}
|
||
});
|
||
|
||
// Register the tool with the server if connected
|
||
if (this.isConnected) {
|
||
this._sendMessage({
|
||
type: 'registerTool',
|
||
name,
|
||
description: description || `Tool: ${name}`,
|
||
inputSchema: schema || {
|
||
type: "object",
|
||
properties: {}
|
||
},
|
||
});
|
||
|
||
this.registeredTools.add(name);
|
||
}
|
||
|
||
// Save to session storage
|
||
this._saveItemsToStorage();
|
||
|
||
// Update tools display
|
||
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;
|
||
}
|
||
|
||
// Add the prompt to local registry
|
||
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 || []
|
||
});
|
||
|
||
// Register the prompt with the server if connected
|
||
if (this.isConnected) {
|
||
this._sendMessage({
|
||
type: 'registerPrompt',
|
||
name,
|
||
description: description || `Prompt: ${name}`,
|
||
arguments: promptArgs || []
|
||
});
|
||
|
||
this.registeredPrompts.add(name);
|
||
}
|
||
|
||
// Save to session storage
|
||
this._saveItemsToStorage();
|
||
|
||
// Update prompts display
|
||
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;
|
||
|
||
// Add the resource to local registry
|
||
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: uri,
|
||
text: `Default implementation of resource ${name} for URI: ${uri}`,
|
||
mimeType: options.mimeType || "text/plain"
|
||
}]
|
||
};
|
||
}
|
||
});
|
||
|
||
// Register the resource with the server if connected
|
||
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);
|
||
}
|
||
|
||
// Save to session storage
|
||
this._saveItemsToStorage();
|
||
|
||
// Update resources display
|
||
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`);
|
||
|
||
// Create a modal dialog to show the sampling request
|
||
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'
|
||
});
|
||
|
||
// Create modal content
|
||
const modalContent = document.createElement('div');
|
||
Object.assign(modalContent.style, {
|
||
backgroundColor: 'white',
|
||
padding: '20px',
|
||
borderRadius: '5px',
|
||
maxWidth: '500px',
|
||
width: '90%',
|
||
maxHeight: '80%',
|
||
overflow: 'auto'
|
||
});
|
||
|
||
// Create header
|
||
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'
|
||
});
|
||
|
||
// Create content area to show messages
|
||
const content = document.createElement('div');
|
||
Object.assign(content.style, {
|
||
marginBottom: '15px',
|
||
maxHeight: '300px',
|
||
overflow: 'auto',
|
||
border: '1px solid #ddd',
|
||
padding: '10px',
|
||
backgroundColor: '#f9f9f9'
|
||
});
|
||
|
||
// Display messages
|
||
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';
|
||
}
|
||
|
||
// System prompt if available
|
||
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);
|
||
}
|
||
|
||
// Create response input
|
||
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'
|
||
});
|
||
|
||
// Create buttons
|
||
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'
|
||
});
|
||
|
||
// Add elements to modal
|
||
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);
|
||
|
||
// Focus the response input
|
||
responseInput.focus();
|
||
|
||
// Setup button handlers
|
||
submitButton.addEventListener('click', () => {
|
||
const responseText = responseInput.value.trim();
|
||
if (responseText) {
|
||
// Send response back to server
|
||
this._sendMessage({
|
||
id,
|
||
type: 'samplingResponse',
|
||
result: {
|
||
model: 'web-user-input',
|
||
role: 'assistant',
|
||
content: {
|
||
type: 'text',
|
||
text: responseText
|
||
}
|
||
}
|
||
});
|
||
|
||
// Remove modal
|
||
document.body.removeChild(modal);
|
||
} else {
|
||
alert('Please enter a response');
|
||
}
|
||
});
|
||
|
||
cancelButton.addEventListener('click', () => {
|
||
// Send error response
|
||
this._sendMessage({
|
||
id,
|
||
type: 'samplingResponse',
|
||
error: 'User cancelled sampling request'
|
||
});
|
||
|
||
// Remove modal
|
||
document.body.removeChild(modal);
|
||
});
|
||
}
|
||
}
|
||
|
||
// Export for module usage
|
||
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
|
||
module.exports = WebMCP;
|
||
}
|