UI.html•21.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>