<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Claude MCP Plugin</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
margin: 0;
font-size: 12px;
padding: 16px;
color: #400B55;
background-color: #FDF8FF;
}
.dark {
color: #fff;
background-color: #2c2c2c;
}
.mcp-plugin__mode {
position: absolute;
bottom: -8px;
right: -4px;
cursor: pointer;
transition: ease 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
font-size: 10px;
opacity: 0.4;
}
.mcp-plugin__mode:hover {
opacity: 1;
}
.mcp-plugin {
position: relative;
display: flex;
flex-direction: column;
gap: 24px;
height: 100%;
}
.mcp-plugin__header {
display: flex;
flex-direction: column;
gap: 4px;
}
.mcp-plugin__header-title {
font-size: 14px;
margin: 0;
}
.mcp-plugin__header-description {
font-size: 12px;
margin: 0;
}
.dark .mcp-plugin__header-description {
color: #CCCCCC;
}
.mcp-plugin__content {
display: flex;
flex-direction: column;
gap: 16px;
}
.mcp-plugin__connection-status {
padding: 8px;
border-radius: 4px;
height: 32px;
line-height: 1.35;
}
.mcp-plugin__content-activity {
display: flex;
}
.mcp-plugin__connection-status.connected {
background: #D8FFBB;
color: #223005;
}
.mcp-plugin__connection-status.disconnected {
background-color: #FFC8BD;
color: #280B06;
}
.mcp-plugin__connection-status.info {
background-color: #BBFFFD;
color: #0B4240;
}
.mcp-plugin__channel-name {
font-weight: bold;
text-decoration: underline;
cursor: pointer;
position: relative;
}
.mcp-plugin__channel-name:hover {
text-decoration: none;
}
.mcp-plugin__channel-name::after {
content: "Click to copy";
position: absolute;
bottom: 16px;
left: 50%;
transform: translateX(-50%);
background-color: #223005;
color: #fff;
padding: 4px 8px;
border-radius: 4px;
font-size: 10px;
font-weight: normal;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
pointer-events: none;
z-index: 100;
}
.dark .mcp-plugin__channel-name::after {
background-color: #2c2c2c;
}
.mcp-plugin__channel-name:hover::after {
opacity: 1;
visibility: visible;
}
.mcp-plugin__button {
display: flex;
align-items: center;
justify-content: center;
height: 32px;
width: 96px;
border: 1px solid #732392;
background-color: #732392;
color: #fff;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: ease 0.2s;
margin-left: auto;
}
.dark .mcp-plugin__button {
border-color: #5467F7;
background-color: #5467F7;
color: #ffffff;
}
.mcp-plugin__button:hover {
background-color: transparent;
color: #732392;
}
.dark .mcp-plugin__button:hover {
background-color: transparent;
color: #ffffff;
border-color: #5467F7;
}
.mcp-plugin__button--secondary {
background-color: transparent;
color: #732392;
}
.dark .mcp-plugin__button--secondary {
background-color: transparent;
color: #fff;
border-color: #5467F7;
}
.mcp-plugin__button--secondary:hover {
background-color: #732392;
color: #fff;
}
.dark .mcp-plugin__button--secondary:hover {
background-color: #5467F7;
color: #ffffff;
border-color: #5467F7;
}
.mcp-plugin__button:disabled {
display: none;
}
.hidden {
display: none !important;
}
.mcp-plugin__progress {
display: flex;
flex-direction: column;
width: calc(100% - 112px);
gap: 4px;
}
.mcp-plugin__progress-bar {
width: 100%;
height: 4px;
background-color: #EDD0F8;
border-radius: 4px;
}
.dark .mcp-plugin__progress-bar {
background-color: #575757;
}
.mcp-plugin__progress-bar-fill {
width: 0%;
height: 4px;
background-color: #732392;
border-radius: 4px;
transition: width 0.3s;
}
.dark .mcp-plugin__progress-bar-fill {
background-color: #5467F7;
}
.mcp-plugin__progress-indicators {
display: flex;
justify-content: space-between;
gap: 16px;
font-size: 10px;
}
.dark .mcp-plugin__progress-indicators {
color: #ABABAB;
}
#progress-status {
display: none;
}
.operation-error {
color: #f87359ff;
}
</style>
</head>
<body class="dark">
<div class="container mcp-plugin">
<div class="mcp-plugin__mode">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="#400b55"
class="mcp-plugin__mode-moon hidden"
viewBox="0 -960 960 960"><path d="M483.11-116q-76.3 0-143.05-28.93-66.76-28.93-116.48-78.65t-78.65-116.48Q116-406.81 116-483.11q0-125.27 75.12-223.93 75.11-98.65 198.19-129.65-10.08 96.31 20.34 181.27 30.43 84.96 97.12 151.65 64.69 64.69 151.65 94.62 86.96 29.92 178.27 19.84-29.38 121.08-128.85 197.19Q608.38-116 483.11-116m-.11-52q88 0 164-45t115-122.11q-83-5.01-158.5-39.56T469-468.3q-60-60.08-94-134.89T335-762q-77 41-122 116.18-45 75.19-45 162.82 0 131.25 91.88 223.12Q351.75-168 483-168m-14-300.38"/></svg>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="#fff"
class="mcp-plugin__mode-sun"
viewBox="0 -960 960 960">
<path d="M454-794.31V-902h52v107.69zM454-58v-107.69h52V-58zm340.31-396v-52H902v52zM58-454v-52h107.69v52zm660.31-227.15-37.16-37.16L759-798.38 798.38-759zM202-162.62 162.62-202l79.07-76.85 37.16 37.16zm557 0-77.85-79.07 37.16-37.16L798.38-202zM241.69-681.15 162.62-759 202-798.38l76.85 80.07zm82.54 356.92Q260-388.46 260-480t64.23-155.77T480-700t155.77 64.23T700-480t-64.23 155.77T480-260t-155.77-64.23M599-360.5q49-49.5 49-119.75 0-70.24-48.75-119-48.76-48.75-119-48.75Q410-648 361-599.25q-49 48.76-49 119 0 70.25 48.75 119.75 48.76 49.5 119 49.5Q550-311 599-360.5M481-481"/></svg>
</div>
<div class="header mcp-plugin__header">
<h1 class="mcp-plugin__header-title">🤖🎨 Claude Talk to Figma</h1>
<p class="mcp-plugin__header-description">AI agents reading and modifing Figma designs</p>
</div>
<div class="content mcp-plugin__content">
<div id="connection-status" class="disconnected mcp-plugin__connection-status">
Disconnected from server…<br>Try to reconnect clicking the button
</div>
<div class="mcp-plugin__content-activity">
<div id="progress-container" class="hidden mcp-plugin__progress">
<div class="mcp-plugin__progress-bar">
<div id="progress-bar" class="mcp-plugin__progress-bar-fill"></div>
</div>
<div class="mcp-plugin__progress-indicators">
<div id="progress-status">Not started</div>
<div id="progress-message" class="mcp-plugin__progress-message">No operation in progress</div>
<div id="progress-percentage">0%</div>
</div>
</div>
<button id="btn-connect" class="mcp-plugin__button mcp-plugin__button--primary">Connect</button>
<button id="btn-disconnect" class="mcp-plugin__button mcp-plugin__button--secondary" disabled>
Disconnect
</button>
</div>
</div>
</div>
<script>
/**
* ClaudeMCPPlugin Class
* Managed WebSocket connection, Figma communication, and UI state.
*/
class ClaudeMCPPlugin {
constructor() {
this.state = {
connected: false,
socket: null,
serverPort: 3055,
pendingRequests: new Map(),
channel: null,
};
// Cache DOM elements
this.ui = {
connectBtn: document.getElementById("btn-connect"),
disconnectBtn: document.getElementById("btn-disconnect"),
connectionStatus: document.getElementById("connection-status"),
progressContainer: document.getElementById("progress-container"),
progressBar: document.getElementById("progress-bar"),
progressMessage: document.getElementById("progress-message"),
progressStatus: document.getElementById("progress-status"),
progressPercentage: document.getElementById("progress-percentage"),
modeToggle: document.querySelector(".mcp-plugin__mode"),
modeSun: document.querySelector(".mcp-plugin__mode-sun"),
modeMoon: document.querySelector(".mcp-plugin__mode-moon"),
};
this.init();
}
init() {
// Event Listeners
this.ui.connectBtn.addEventListener("click", () => this.connect());
this.ui.disconnectBtn.addEventListener("click", () => this.disconnect());
// Clipboard feature using event delegation
this.ui.connectionStatus.addEventListener("click", (e) => {
if (e.target.classList.contains("mcp-plugin__channel-name")) {
this.copyToClipboard(`Connect to Figma, channel ${e.target.textContent}`);
}
});
// Mode toggle
this.ui.modeToggle.addEventListener("click", () => this.toggleMode());
// Figma messages
window.onmessage = (event) => this.handleFigmaMessage(event.data.pluginMessage);
}
/**
* Update the connection status UI
*/
updateStatus(isConnected, message) {
this.state.connected = isConnected;
this.ui.connectionStatus.innerHTML =
message ||
(isConnected
? "Connected to Claude MCP server"
: "Not connected to Claude MCP server");
this.ui.connectionStatus.className = `mcp-plugin__connection-status ${
isConnected ? "connected" : (message && message.includes("Connecting") ? "info" : "disconnected")
}`;
this.ui.connectBtn.disabled = isConnected;
this.ui.disconnectBtn.disabled = !isConnected;
}
/**
* Connect to WebSocket server
*/
async connect(port = 3055) {
try {
if (this.state.connected && this.state.socket) {
this.updateStatus(true, "Already connected to server");
return;
}
this.updateStatus(false, "Connecting...");
this.ui.connectionStatus.className = "mcp-plugin__connection-status info";
this.state.serverPort = port;
this.state.socket = new WebSocket(`ws://localhost:${port}`);
this.state.socket.onopen = () => {
const channelName = this.generateChannelName();
console.log("Joining channel:", channelName);
this.state.channel = channelName;
this.state.socket.send(
JSON.stringify({
type: "join",
channel: channelName.trim(),
})
);
};
this.state.socket.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
console.log("Received message:", data);
if (data.type === "system") {
if (data.message && data.message.result) {
this.state.connected = true;
const channelName = data.channel;
this.updateStatus(
true,
`Connected on port ${port}!<br>Copy the channel ID: <span class="mcp-plugin__channel-name" title="Click to copy">${channelName}</span>`
);
parent.postMessage(
{
pluginMessage: {
type: "notify",
message: `Connected on port ${port}. In channel: ${channelName}`,
},
},
"*"
);
}
} else if (data.type === "error") {
console.error("Error:", data.message);
this.updateStatus(false, `Error: ${data.message}`);
this.state.socket.close();
}
this.handleSocketMessage(data);
} catch (error) {
console.error("Error parsing message:", error);
}
};
this.state.socket.onclose = () => {
this.state.connected = false;
this.state.socket = null;
this.updateStatus(false, "Disconnected from server…<br>Try to reconnect clicking the button");
};
this.state.socket.onerror = (error) => {
console.error("WebSocket error:", error);
this.updateStatus(false, "Connection error");
this.state.connected = false;
this.state.socket = null;
};
} catch (error) {
console.error("Connection error:", error);
this.updateStatus(
false,
`Connection error: ${error.message || "Unknown error"}`
);
}
}
/**
* Disconnect from WebSocket server
*/
disconnect() {
if (this.state.socket) {
this.updateStatus(false, "Disconnecting...");
this.ui.connectionStatus.className = "mcp-plugin__connection-status info";
this.state.socket.close();
this.state.socket = null;
this.state.connected = false;
}
}
/**
* Handle messages from the WebSocket server
*/
async 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 && this.state.pendingRequests.has(data.id)) {
const { resolve, reject } = this.state.pendingRequests.get(data.id);
this.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 {
parent.postMessage(
{
pluginMessage: {
type: "execute-command",
id: data.id,
command: data.command,
params: data.params,
},
},
"*"
);
} catch (error) {
this.sendErrorResponse(data.id, error.message || "Error executing command");
}
}
}
/**
* Handle messages from Figma
*/
handleFigmaMessage(message) {
if (!message) return;
console.log("Received message from plugin:", message);
switch (message.type) {
case "connection-status":
this.updateStatus(message.connected, message.message);
break;
case "auto-connect":
this.connect();
break;
case "auto-disconnect":
this.disconnect();
break;
case "command-result":
this.sendSuccessResponse(message.id, message.result);
break;
case "command-error":
this.sendErrorResponse(message.id, message.error);
break;
case "command_progress":
this.updateProgress(message);
this.sendProgressUpdate(message);
break;
}
}
/**
* Send success response to server
*/
sendSuccessResponse(id, result) {
if (!this.state.connected || !this.state.socket) return;
this.state.socket.send(
JSON.stringify({
id,
type: "message",
channel: this.state.channel,
message: { id, result },
})
);
}
/**
* Send error response to server
*/
sendErrorResponse(id, errorMessage) {
if (!this.state.connected || !this.state.socket) return;
this.state.socket.send(
JSON.stringify({
id,
type: "message",
channel: this.state.channel,
message: { id, error: errorMessage },
})
);
}
/**
* Update Progress UI
*/
updateProgress(data) {
this.ui.progressContainer.classList.remove("hidden");
const progress = data.progress || 0;
this.ui.progressBar.style.width = `${progress}%`;
this.ui.progressPercentage.textContent = `${progress}%`;
this.ui.progressMessage.textContent = data.message || "Operation in progress";
if (data.status === 'started' || data.status === 'in_progress') {
this.ui.progressStatus.textContent = data.status === 'started' ? "Started" : "In Progress";
this.ui.progressStatus.className = "";
} else if (data.status === 'completed') {
this.ui.progressStatus.textContent = "Completed";
this.ui.progressStatus.className = "operation-complete";
setTimeout(() => this.ui.progressContainer.classList.add("hidden"), 5000);
} else if (data.status === 'error') {
this.ui.progressStatus.textContent = "Error";
this.ui.progressStatus.className = "operation-error";
}
}
/**
* Relay progress update to server
*/
sendProgressUpdate(data) {
if (!this.state.connected || !this.state.socket) return;
this.state.socket.send(
JSON.stringify({
id: data.commandId,
type: "progress_update",
channel: this.state.channel,
message: {
id: data.commandId,
type: "progress_update",
data: data
}
})
);
}
/**
* Copy text to clipboard and notify
*/
async copyToClipboard(text) {
try {
// Try modern API first
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
this.notifyCopied(text);
} else {
throw new Error("Clipboard API not available");
}
} catch (err) {
// Fallback to execCommand('copy')
try {
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.position = "fixed";
textArea.style.left = "-9999px";
textArea.style.top = "0";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
const successful = document.execCommand('copy');
document.body.removeChild(textArea);
if (successful) {
this.notifyCopied(text);
} else {
throw new Error("execCommand('copy') failed");
}
} catch (fallbackErr) {
console.error("All copy methods failed:", fallbackErr);
}
}
}
/**
* Toggle between dark and light mode
*/
toggleMode() {
const isDark = document.body.classList.toggle("dark");
if (isDark) {
this.ui.modeSun.classList.remove("hidden");
this.ui.modeMoon.classList.add("hidden");
} else {
this.ui.modeSun.classList.add("hidden");
this.ui.modeMoon.classList.remove("hidden");
}
}
/**
* Notify Figma about clipboard success
*/
notifyCopied(text) {
parent.postMessage({
pluginMessage: {
type: "notify",
message: `Channel ID copied: ${text}`,
},
}, "*");
}
/**
* Utils
*/
generateId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
}
generateChannelName() {
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
let res = "";
for (let i = 0; i < 8; i++) res += chars.charAt(Math.floor(Math.random() * chars.length));
return res;
}
}
// Initialize the plugin
const plugin = new ClaudeMCPPlugin();
</script>
</body>
</html>