Skip to main content
Glama

Sketch Context MCP

UI.html21.5 kB
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Sketch Context MCP</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 20px; color: #333333; background-color: #f5f5f5; } .container { display: flex; flex-direction: column; height: 100%; } h1 { font-size: 16px; font-weight: 600; margin-bottom: 10px; color: #191919; } h2 { font-size: 14px; font-weight: 600; margin-top: 20px; margin-bottom: 8px; color: #191919; } button { background-color: #F8A01D; border: none; color: white; padding: 8px 12px; border-radius: 6px; margin-top: 8px; margin-bottom: 8px; cursor: pointer; font-size: 14px; transition: background-color 0.2s; } button:hover { background-color: #E59018; } button.secondary { background-color: #e0e0e0; color: #333333; } button.secondary:hover { background-color: #d0d0d0; } button:disabled { background-color: #cccccc; color: #666666; cursor: not-allowed; } input { border: 1px solid #cccccc; border-radius: 4px; padding: 8px; margin-bottom: 12px; font-size: 14px; width: 100%; box-sizing: border-box; background-color: #ffffff; color: #333333; } label { display: block; margin-bottom: 4px; font-size: 12px; font-weight: 500; color: #666666; } .status { margin-top: 16px; padding: 12px; border-radius: 6px; font-size: 14px; } .status.connected { background-color: #e6f7ee; color: #0f7b45; } .status.disconnected { background-color: #fbe9e7; color: #c62828; } .status.info { background-color: #e3f2fd; color: #1565c0; } .section { margin-bottom: 24px; } .hidden { display: none; } .logo { width: 50px; height: 50px; } .header { display: flex; align-items: center; margin-bottom: 16px; } .header-text { margin-left: 12px; } .header-text h1 { margin: 0; font-size: 16px; } .header-text p { margin: 4px 0 0 0; font-size: 12px; color: #666666; } .tabs { display: flex; border-bottom: 1px solid #e0e0e0; margin-bottom: 16px; } .tab { padding: 8px 16px; cursor: pointer; font-size: 14px; font-weight: 500; color: #666666; } .tab.active { border-bottom: 2px solid #F8A01D; color: #F8A01D; } .tab-content { display: none; } .tab-content.active { display: block; } .link { color: #F8A01D; text-decoration: none; cursor: pointer; } .link:hover { text-decoration: underline; } .header-logo { padding: 16px; border-radius: 16px; background-color: #ffffff; display: flex; align-items: center; justify-content: center; } .header-logo-image { width: 24px; height: 24px; object-fit: contain; } .selection-item { padding: 8px; margin-bottom: 8px; border: 1px solid #e0e0e0; border-radius: 4px; background-color: #ffffff; } .selection-item-header { display: flex; justify-content: space-between; font-weight: 500; margin-bottom: 4px; } .selection-item-type { color: #666666; font-size: 12px; } .selection-item-details { color: #666666; font-size: 12px; } #selection-list { max-height: 200px; overflow-y: auto; margin-bottom: 12px; } pre { background-color: #f0f0f0; padding: 8px; border-radius: 4px; overflow-x: auto; font-size: 12px; } </style> </head> <body> <div class="container"> <div class="header"> <div class="header-logo"> <img class="header-logo-image" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAOISURBVHgBrZdLbExRGMf/d++dmU5nKFOdYqpT08aziCdJI7qwaES6IKTEo0EsJCSCHQtiYVMSlrZWXhuJZ4gFjW66QOzaeDS0wqTT6cx0eu/1P/fctDO9r+n8kl/OPfec+/3Od75z7nfIQ36z7pnmZPj+kJHSCZRoCGTBf47ql8AKPPyj8tqekRGaTHR06o0IwVFOaV2AQfnw/2KKdmrGCt7UXu9KGKx0l0bYUUbJKVXjKQxR5Q36GDk9qAh06eKFbgLOoWHiWMKaQCCDdOxMNPUFbUO3mAw+jEo3A0xJQYoynGAiUn+0vfnm+FhW8Ly93krGSDXSQwzPVJ9sqXYPDBss0rFspmcaaKMccINijw3clBXMWIHDNEsFbvJ8z3hG0WfSFqVbBrGe4G/5QXyuaYZe5nFYx0PtUV/u7hLrEzjcXikcb3VQK7jMnZjQ+xIdMpwgPNwbsGvF1i3SQK21G4WTTolYJoxM6kjYITa9W7MXFBpnVGJEJh9dO4gN26axpn4Gpi3PfBhSP+Bi0234d/EjpwecPz8Ptcf2IvT1B1ovv0BgYw3WrF3liNpgUTgxDyP1REtaWdRoGHfTHHiK87Fxx1JU5E9Fv+8uel+1YoQzVOTP9h5A9bHd6Lv7Fs+PXcCsukrU1u13RLn35/Wq1ZXXWo9N6KMc+6KK6vI8lJdY9+jqB+/7fvgbWtDZ/QsDzVdRVFboGlz14B7D2Nir/r7vMHSZ6QpVFhXYdXHJdOt3XsHUvBxsi3NTDTO5Xu17OoPfsZeIlsZyc5iqiOBa6+oxXFmB2sZNKCtfiL4bn3D78lNrKI6fQdmSIvT0+nGj+RneXHkVq+NJcIx6qWz1orzdGxR4KRX3FV5hbK25Fo01NfDMXABPvgetN55i+NG3h3UDnnKjzHKTmzC/fsLX0ABfzyByPGVYXL3YvsD5pYVYvHwBPnd04dbrDvjDURf4pAiVIWLOY+lmTgbEVFm9fBEK87Kta7P32n2E+oectC8QJ9mRQ2pxzW+VyvEKG7eYihLdCcOhXV9ahfb4/KaZhGPFdSnmMIkdUYvHJdvOSQXRdLydNJNMTw76Vs4eG5Ll4qlIK4lD1C6XkjGSOlFLx0uFbLV0vBR/K0fKfn5cOnb71OFT+w+Kcq0gkMWyI4KPKN0UaUwc6OyJ4bvw0iMbJgjBHyh2BKP40FnNAAAAAElFTkSuQmCC" /> </div> <div class="header-text"> <h1>Sketch Context MCP</h1> <p>Connect Sketch to Cursor AI using MCP</p> </div> </div> <div class="tabs"> <div id="tab-connection" class="tab active">Connection</div> <div id="tab-selection" class="tab">Selection</div> <div id="tab-about" class="tab">About</div> </div> <div id="content-connection" class="tab-content active"> <div class="section"> <label for="port">WebSocket Server Port</label> <div style="display: flex; gap: 8px"> <input type="number" id="port" placeholder="3333" value="3333" min="1024" max="65535" /> <button id="btn-connect" class="primary">Connect</button> </div> </div> <div id="connection-status" class="status disconnected"> Not connected to Cursor MCP server </div> <div class="section"> <button id="btn-disconnect" class="secondary" disabled> Disconnect </button> </div> <div class="section"> <h2>Cursor Connection Guide</h2> <p>To connect this plugin to Cursor:</p> <ol> <li>Start the WebSocket server by clicking Connect above</li> <li>Open Cursor and go to Settings > Features > Context</li> <li>Add a new MCP server with URL: <code id="server-url">http://localhost:3333/sse</code></li> <li>Click "Connect" in Cursor</li> </ol> </div> </div> <div id="content-selection" class="tab-content"> <div class="section"> <h2>Selected Layers</h2> <div id="selection-list"></div> <button id="btn-copy-ids" class="primary">Copy Selection IDs</button> <button id="btn-refresh-selection" class="secondary">Refresh Selection</button> </div> </div> <div id="content-about" class="tab-content"> <div class="section"> <h2>About Sketch Context MCP</h2> <p> This plugin allows Cursor AI to communicate with Sketch, enabling AI-assisted design operations. </p> <p>Version: 1.0.0</p> <h2>How to Use</h2> <ol> <li>Connect to the WebSocket server using the Connection tab</li> <li>Select elements in Sketch and use the Selection tab to copy their IDs</li> <li>Use these IDs in Cursor to reference specific elements</li> </ol> <h2>Documentation</h2> <p> For more information, visit the <a class="link" onclick="openURL('https://github.com/yourusername/sketch-context-mcp')"> GitHub repository </a> </p> </div> </div> </div> <script> // WebSocket connection state const state = { connected: false, socket: null, serverPort: 3333, pendingRequests: new Map(), channel: null, }; // UI Elements const portInput = document.getElementById("port"); const connectButton = document.getElementById("btn-connect"); const disconnectButton = document.getElementById("btn-disconnect"); const connectionStatus = document.getElementById("connection-status"); const serverUrl = document.getElementById("server-url"); const selectionList = document.getElementById("selection-list"); const copyIdsButton = document.getElementById("btn-copy-ids"); const refreshSelectionButton = document.getElementById("btn-refresh-selection"); // Tabs const tabs = document.querySelectorAll(".tab"); const tabContents = document.querySelectorAll(".tab-content"); // Initialize UI function updateConnectionStatus(isConnected, message) { state.connected = isConnected; connectionStatus.innerHTML = message || (isConnected ? "Connected to Cursor MCP server" : "Not connected to Cursor MCP server"); connectionStatus.className = `status ${ isConnected ? "connected" : "disconnected" }`; connectButton.disabled = isConnected; disconnectButton.disabled = !isConnected; portInput.disabled = isConnected; } // Connect to WebSocket server async function connectToServer(port) { try { if (state.connected && state.socket) { updateConnectionStatus(true, "Already connected to server"); return; } state.serverPort = port; state.socket = new WebSocket(`ws://localhost:${port}`); serverUrl.textContent = `http://localhost:${port}/sse`; state.socket.onopen = () => { // Generate random channel name const channelName = generateChannelName(); console.log("Joining channel:", channelName); state.channel = channelName; // Join the channel state.socket.send( JSON.stringify({ type: "join", channel: channelName.trim(), }) ); updateConnectionStatus(false, "Connected to server, joining channel..."); connectionStatus.className = "status info"; }; state.socket.onmessage = (event) => { try { const data = JSON.parse(event.data); console.log("Received message:", data); if (data.type === "system") { // Successfully joined channel if (data.message && data.message.result) { state.connected = true; const channelName = data.channel; updateConnectionStatus( true, `Connected to server on port ${port} in channel: <strong>${channelName}</strong>` ); // Notify the plugin code window.postMessage("notify", { message: `Connected to Cursor MCP server on port ${port} in channel: ${channelName}`, }); } } else if (data.type === "error") { console.error("Error:", data.message); updateConnectionStatus(false, `Error: ${data.message}`); state.socket.close(); } handleSocketMessage(data); } catch (error) { console.error("Error parsing message:", error); } }; state.socket.onclose = () => { state.connected = false; state.socket = null; updateConnectionStatus(false, "Disconnected from server"); }; state.socket.onerror = (error) => { console.error("WebSocket error:", error); updateConnectionStatus(false, "Connection error"); state.connected = false; state.socket = null; }; } catch (error) { console.error("Connection error:", error); updateConnectionStatus( false, `Connection error: ${error.message || "Unknown error"}` ); } } // Disconnect from websocket server function disconnectFromServer() { if (state.socket) { state.socket.close(); state.socket = null; state.connected = false; updateConnectionStatus(false, "Disconnected from server"); } } // Handle messages from the WebSocket async function handleSocketMessage(payload) { const data = payload.message; if (!data) return; console.log("handleSocketMessage", data); // If it's a response to a previous request if (data.id && state.pendingRequests.has(data.id)) { const { resolve, reject } = state.pendingRequests.get(data.id); state.pendingRequests.delete(data.id); if (data.error) { reject(new Error(data.error)); } else { resolve(data.result); } return; } // If it's a new command if (data.command) { try { // Send the command to the plugin code window.postMessage("execute-command", { id: data.id, command: data.command, params: data.params, }); } catch (error) { // Send error back to WebSocket sendErrorResponse( data.id, error.message || "Error executing command" ); } } } // Send a command to the WebSocket server async function sendCommand(command, params) { return new Promise((resolve, reject) => { if (!state.connected || !state.socket) { reject(new Error("Not connected to server")); return; } const id = generateId(); state.pendingRequests.set(id, { resolve, reject }); state.socket.send( JSON.stringify({ id, type: "message", channel: state.channel, message: { id, command, params, }, }) ); // Set timeout to reject the promise after 30 seconds setTimeout(() => { if (state.pendingRequests.has(id)) { state.pendingRequests.delete(id); reject(new Error("Request timed out")); } }, 30000); }); } // Send success response back to WebSocket function sendSuccessResponse(id, result) { if (!state.connected || !state.socket) { console.error("Cannot send response: socket not connected"); return; } state.socket.send( JSON.stringify({ id, type: "message", channel: state.channel, message: { id, result, }, }) ); } // Send error response back to WebSocket function sendErrorResponse(id, errorMessage) { if (!state.connected || !state.socket) { console.error("Cannot send error response: socket not connected"); return; } state.socket.send( JSON.stringify({ id, type: "message", channel: state.channel, message: { id, error: errorMessage, }, }) ); } // Helper to generate unique IDs function generateId() { return ( Date.now().toString(36) + Math.random().toString(36).substr(2, 5) ); } // Helper to generate channel names function generateChannelName() { const characters = "abcdefghijklmnopqrstuvwxyz0123456789"; let result = ""; for (let i = 0; i < 8; i++) { result += characters.charAt( Math.floor(Math.random() * characters.length) ); } return result; } // Function to open URLs function openURL(url) { window.postMessage("open-url", { url }); } // Function to update selection list function updateSelectionList(selection) { selectionList.innerHTML = ""; if (!selection || !selection.length) { selectionList.innerHTML = "<p>No layers selected</p>"; return; } selection.forEach(item => { const itemElement = document.createElement("div"); itemElement.className = "selection-item"; const header = document.createElement("div"); header.className = "selection-item-header"; const name = document.createElement("span"); name.textContent = item.name; const type = document.createElement("span"); type.className = "selection-item-type"; type.textContent = item.type; header.appendChild(name); header.appendChild(type); const details = document.createElement("div"); details.className = "selection-item-details"; details.textContent = `ID: ${item.id}`; itemElement.appendChild(header); itemElement.appendChild(details); selectionList.appendChild(itemElement); }); } // Function to copy selection IDs function copySelectionIDs() { window.postMessage("copy-selection", {}); } // Function to refresh selection function refreshSelection() { window.postMessage("refresh-selection", {}); } // Tab switching tabs.forEach((tab) => { tab.addEventListener("click", () => { tabs.forEach((t) => t.classList.remove("active")); tabContents.forEach((c) => c.classList.remove("active")); tab.classList.add("active"); const contentId = "content-" + tab.id.split("-")[1]; document.getElementById(contentId).classList.add("active"); // If selection tab is opened, refresh the selection if (tab.id === "tab-selection") { refreshSelection(); } }); }); // Connect to server connectButton.addEventListener("click", () => { const port = parseInt(portInput.value, 10) || 3333; updateConnectionStatus(false, "Connecting..."); connectionStatus.className = "status info"; connectToServer(port); }); // Disconnect from server disconnectButton.addEventListener("click", () => { updateConnectionStatus(false, "Disconnecting..."); connectionStatus.className = "status info"; disconnectFromServer(); }); // Copy selection IDs copyIdsButton.addEventListener("click", copySelectionIDs); // Refresh selection refreshSelectionButton.addEventListener("click", refreshSelection); // Communication with the plugin code window.addEventListener("message", (event) => { if (event.data && event.data.pluginMessage) { const message = event.data.pluginMessage; switch (message.type) { case "selection-updated": updateSelectionList(message.selection); break; case "connection-status": updateConnectionStatus(message.connected, message.message); break; case "command-result": // Forward the result from plugin code back to WebSocket sendSuccessResponse(message.id, message.result); break; case "command-error": // Forward the error from plugin code back to WebSocket sendErrorResponse(message.id, message.error); break; } } }); // Helper function to send messages to the plugin code window.postMessage = function(type, data) { window.webkit.messageHandlers.sketchPlugin.postMessage({ type: type, ...data }); }; </script> </body> </html>

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jshmllr/Sketch-Context-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server