claude-figjam 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bridge.js ADDED
@@ -0,0 +1,86 @@
1
+ // claude-figjam/mcp-server/src/bridge.ts
2
+ import WebSocket, { WebSocketServer } from "ws";
3
+ import { randomUUID } from "node:crypto";
4
+ export class Bridge {
5
+ wss;
6
+ ws = null;
7
+ queue = [];
8
+ pending = new Map();
9
+ constructor(port) {
10
+ this.wss = new WebSocketServer({ port });
11
+ this.wss.on("connection", (ws) => {
12
+ this.ws = ws;
13
+ setTimeout(() => this.flushQueue(), 0);
14
+ ws.on("message", (data) => {
15
+ let msg;
16
+ try {
17
+ msg = JSON.parse(data.toString());
18
+ }
19
+ catch {
20
+ return; // ignore malformed messages
21
+ }
22
+ const cmd = this.pending.get(msg.id);
23
+ if (!cmd)
24
+ return;
25
+ this.pending.delete(msg.id);
26
+ if (msg.error)
27
+ cmd.reject(new Error(msg.error));
28
+ else
29
+ cmd.resolve(msg.result);
30
+ });
31
+ ws.on("close", () => {
32
+ this.ws = null;
33
+ for (const cmd of this.pending.values()) {
34
+ cmd.reject(new Error("Plugin disconnected"));
35
+ }
36
+ this.pending.clear();
37
+ });
38
+ });
39
+ }
40
+ flushQueue() {
41
+ while (this.queue.length > 0 && this.ws?.readyState === WebSocket.OPEN) {
42
+ const cmd = this.queue.shift();
43
+ this.dispatch(cmd);
44
+ }
45
+ }
46
+ dispatch(cmd) {
47
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
48
+ cmd.reject(new Error("No active connection"));
49
+ return;
50
+ }
51
+ this.pending.set(cmd.id, cmd);
52
+ const msg = {
53
+ id: cmd.id,
54
+ type: cmd.type,
55
+ params: cmd.params,
56
+ };
57
+ this.ws.send(JSON.stringify(msg));
58
+ }
59
+ execute(type, params) {
60
+ return new Promise((resolve, reject) => {
61
+ const cmd = {
62
+ id: randomUUID(),
63
+ type,
64
+ params,
65
+ resolve,
66
+ reject,
67
+ };
68
+ if (this.ws?.readyState === WebSocket.OPEN) {
69
+ this.dispatch(cmd);
70
+ }
71
+ else {
72
+ this.queue.push(cmd);
73
+ }
74
+ });
75
+ }
76
+ get connected() {
77
+ return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
78
+ }
79
+ close(callback) {
80
+ // Terminate all active connections so wss.close() can invoke its callback
81
+ for (const client of this.wss.clients) {
82
+ client.terminate();
83
+ }
84
+ this.wss.close(callback);
85
+ }
86
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ const args = process.argv.slice(2);
3
+ if (args.includes("--stdio")) {
4
+ // Claude Code is starting the MCP server — run it
5
+ await import("./index.js");
6
+ }
7
+ else if (args[0] === "setup") {
8
+ const { setup } = await import("./setup.js");
9
+ await setup();
10
+ }
11
+ else {
12
+ console.log("");
13
+ console.log("Claude FigJam MCP Server");
14
+ console.log("");
15
+ console.log("Usage:");
16
+ console.log(" npx claude-figjam setup Configure Claude Code for FigJam");
17
+ console.log(" npx claude-figjam --stdio Start the MCP server (used by Claude Code)");
18
+ console.log("");
19
+ }
20
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,70 @@
1
+ import express from "express";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { Bridge } from "./bridge.js";
6
+ import { registerTools } from "./tools.js";
7
+ const MCP_PORT = parseInt(process.env.FIGJAM_MCP_PORT || "3055", 10);
8
+ const WS_PORT = parseInt(process.env.FIGJAM_WS_PORT || "3766", 10);
9
+ const STDIO_MODE = process.argv.includes("--stdio");
10
+ const bridge = new Bridge(WS_PORT);
11
+ const mcpServer = new McpServer({ name: "claude-figjam", version: "1.0.0" });
12
+ registerTools(mcpServer, bridge);
13
+ if (STDIO_MODE) {
14
+ // Stdio transport: CC manages this process. All logs must go to stderr.
15
+ const app = express();
16
+ app.get("/health", (_req, res) => {
17
+ res.json({ status: "ok", pluginConnected: bridge.connected });
18
+ });
19
+ app.listen(MCP_PORT, "127.0.0.1", () => {
20
+ console.error("Claude FigJam MCP server running (stdio mode)");
21
+ console.error(` WebSocket: ws://localhost:${WS_PORT}`);
22
+ console.error(` Health: http://localhost:${MCP_PORT}/health`);
23
+ });
24
+ const transport = new StdioServerTransport();
25
+ await mcpServer.connect(transport);
26
+ }
27
+ else {
28
+ // HTTP/SSE mode: manual startup for development or CLI use.
29
+ const app = express();
30
+ app.use(express.json());
31
+ const transports = new Map();
32
+ app.get("/sse", async (req, res) => {
33
+ try {
34
+ const transport = new SSEServerTransport("/message", res);
35
+ transports.set(transport.sessionId, transport);
36
+ await mcpServer.connect(transport);
37
+ req.on("close", () => transports.delete(transport.sessionId));
38
+ }
39
+ catch (err) {
40
+ console.error("SSE connection failed:", err);
41
+ if (!res.headersSent)
42
+ res.status(500).end();
43
+ }
44
+ });
45
+ app.post("/message", async (req, res) => {
46
+ const sessionId = req.query.sessionId;
47
+ const transport = transports.get(sessionId);
48
+ if (!transport) {
49
+ res.status(404).json({ error: "Session not found" });
50
+ return;
51
+ }
52
+ try {
53
+ await transport.handlePostMessage(req, res);
54
+ }
55
+ catch (err) {
56
+ console.error("Message handling failed:", err);
57
+ if (!res.headersSent)
58
+ res.status(500).end();
59
+ }
60
+ });
61
+ app.get("/health", (_req, res) => {
62
+ res.json({ status: "ok", pluginConnected: bridge.connected });
63
+ });
64
+ app.listen(MCP_PORT, "127.0.0.1", () => {
65
+ console.log("Claude FigJam MCP server running");
66
+ console.log(` MCP (SSE): http://localhost:${MCP_PORT}/sse`);
67
+ console.log(` WebSocket: ws://localhost:${WS_PORT}`);
68
+ console.log(` Health: http://localhost:${MCP_PORT}/health`);
69
+ });
70
+ }
package/dist/setup.js ADDED
@@ -0,0 +1,75 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import readline from "readline";
4
+ export async function setup() {
5
+ const rl = readline.createInterface({
6
+ input: process.stdin,
7
+ output: process.stdout,
8
+ });
9
+ const question = (q) => new Promise((resolve) => rl.question(q, resolve));
10
+ console.log("");
11
+ console.log("Claude FigJam — Setup");
12
+ console.log("─────────────────────────────────────────");
13
+ console.log("Configures Claude Code to use the FigJam");
14
+ console.log("MCP server for live board editing.");
15
+ console.log("");
16
+ const defaultDir = process.cwd();
17
+ const answer = await question(`Claude Code project directory\n [${defaultDir}]: `);
18
+ const projectDir = path.resolve(answer.trim() || defaultDir);
19
+ rl.close();
20
+ if (!fs.existsSync(projectDir)) {
21
+ console.error(`\nDirectory not found: ${projectDir}`);
22
+ process.exit(1);
23
+ }
24
+ // Write .mcp.json (merge with existing)
25
+ const mcpPath = path.join(projectDir, ".mcp.json");
26
+ let mcpConfig = {};
27
+ if (fs.existsSync(mcpPath)) {
28
+ try {
29
+ mcpConfig = JSON.parse(fs.readFileSync(mcpPath, "utf8"));
30
+ }
31
+ catch {
32
+ console.error(`\nCould not parse existing ${mcpPath} — aborting to avoid overwriting it.`);
33
+ process.exit(1);
34
+ }
35
+ }
36
+ const command = process.platform === "win32" ? "npx.cmd" : "npx";
37
+ const servers = mcpConfig.mcpServers ?? {};
38
+ mcpConfig.mcpServers = {
39
+ ...servers,
40
+ "claude-figjam": {
41
+ command,
42
+ args: ["claude-figjam", "--stdio"],
43
+ },
44
+ };
45
+ fs.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + "\n");
46
+ console.log(`\n ✓ ${mcpPath}`);
47
+ // Write .claude/settings.local.json (merge with existing)
48
+ const claudeDir = path.join(projectDir, ".claude");
49
+ if (!fs.existsSync(claudeDir)) {
50
+ fs.mkdirSync(claudeDir, { recursive: true });
51
+ }
52
+ const settingsPath = path.join(claudeDir, "settings.local.json");
53
+ let settings = {};
54
+ if (fs.existsSync(settingsPath)) {
55
+ try {
56
+ settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
57
+ }
58
+ catch {
59
+ console.error(`\nCould not parse existing ${settingsPath} — aborting to avoid overwriting it.`);
60
+ process.exit(1);
61
+ }
62
+ }
63
+ settings.enableAllProjectMcpServers = true;
64
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
65
+ console.log(` ✓ ${settingsPath}`);
66
+ console.log("");
67
+ console.log("Setup complete.");
68
+ console.log("");
69
+ console.log("Next steps:");
70
+ console.log(" 1. Install the Claude FigJam plugin from Figma Community");
71
+ console.log(" 2. Open Claude Code in your project directory");
72
+ console.log(" 3. Send any message — the MCP server starts automatically");
73
+ console.log(" 4. Open a FigJam file and run the plugin — it connects within 3 seconds");
74
+ console.log("");
75
+ }
package/dist/tools.js ADDED
@@ -0,0 +1,249 @@
1
+ import { z } from "zod";
2
+ // Exported for tests
3
+ export const createNodeSchema = z.object({
4
+ type: z.enum(["sticky", "shape", "text", "section", "table", "code_block"]),
5
+ content: z.string().optional(),
6
+ x: z.number().optional(),
7
+ y: z.number().optional(),
8
+ width: z.number().optional(),
9
+ height: z.number().optional(),
10
+ color: z.string().optional(),
11
+ shape: z
12
+ .enum([
13
+ "square",
14
+ "rectangle",
15
+ "ellipse",
16
+ "diamond",
17
+ "triangle",
18
+ "parallelogram",
19
+ "star",
20
+ "cross",
21
+ ])
22
+ .optional(),
23
+ rows: z.number().int().positive().optional(),
24
+ cols: z.number().int().positive().optional(),
25
+ code: z.string().optional(),
26
+ language: z.string().optional(),
27
+ });
28
+ export const createConnectorSchema = z.object({
29
+ from_id: z.string(),
30
+ to_id: z.string(),
31
+ style: z.enum(["straight", "elbowed", "curved"]).optional(),
32
+ label: z.string().optional(),
33
+ arrow: z.enum(["forward", "back", "both", "none"]).optional(),
34
+ });
35
+ export const updateNodeSchema = z.object({
36
+ node_id: z.string(),
37
+ content: z.string().optional(),
38
+ x: z.number().optional(),
39
+ y: z.number().optional(),
40
+ width: z.number().optional(),
41
+ height: z.number().optional(),
42
+ color: z.string().optional(),
43
+ });
44
+ export const deleteNodesSchema = z.object({
45
+ node_ids: z.union([z.string(), z.array(z.string())]),
46
+ });
47
+ export function registerTools(server, bridge) {
48
+ server.tool("figjam_read_board", "Returns full board state: all nodes with IDs, types, positions, content, and colors.", {}, async () => {
49
+ try {
50
+ const result = await bridge.execute("read_board", {});
51
+ return {
52
+ content: [
53
+ { type: "text", text: JSON.stringify(result, null, 2) },
54
+ ],
55
+ };
56
+ }
57
+ catch (err) {
58
+ const message = err instanceof Error ? err.message : String(err);
59
+ return {
60
+ isError: true,
61
+ content: [{ type: "text", text: message }],
62
+ };
63
+ }
64
+ });
65
+ server.tool("figjam_create_node", "Creates any FigJam node (sticky, shape, text, section, table, code_block). Returns the new node ID.", {
66
+ type: z.enum([
67
+ "sticky",
68
+ "shape",
69
+ "text",
70
+ "section",
71
+ "table",
72
+ "code_block",
73
+ ]),
74
+ content: z.string().optional().describe("Text content"),
75
+ x: z.number().optional().describe("Canvas x position"),
76
+ y: z.number().optional().describe("Canvas y position"),
77
+ width: z.number().optional(),
78
+ height: z.number().optional(),
79
+ color: z.string().optional().describe("Hex color"),
80
+ shape: z
81
+ .enum([
82
+ "square",
83
+ "rectangle",
84
+ "ellipse",
85
+ "diamond",
86
+ "triangle",
87
+ "parallelogram",
88
+ "star",
89
+ "cross",
90
+ ])
91
+ .optional()
92
+ .describe("Required when type=shape"),
93
+ rows: z
94
+ .number()
95
+ .int()
96
+ .positive()
97
+ .optional()
98
+ .describe("Required when type=table"),
99
+ cols: z
100
+ .number()
101
+ .int()
102
+ .positive()
103
+ .optional()
104
+ .describe("Required when type=table"),
105
+ code: z.string().optional().describe("Code content when type=code_block"),
106
+ language: z
107
+ .string()
108
+ .optional()
109
+ .describe("Language identifier when type=code_block, e.g. typescript"),
110
+ }, async (params) => {
111
+ try {
112
+ const result = await bridge.execute("create_node", params);
113
+ return {
114
+ content: [{ type: "text", text: JSON.stringify(result) }],
115
+ };
116
+ }
117
+ catch (err) {
118
+ const message = err instanceof Error ? err.message : String(err);
119
+ return {
120
+ isError: true,
121
+ content: [{ type: "text", text: message }],
122
+ };
123
+ }
124
+ });
125
+ server.tool("figjam_create_connector", "Draws a connector (arrow/line) between two existing nodes by their IDs.", {
126
+ from_id: z.string().describe("Source node ID"),
127
+ to_id: z.string().describe("Target node ID"),
128
+ style: z
129
+ .enum(["straight", "elbowed", "curved"])
130
+ .optional()
131
+ .describe("Line style, default: elbowed"),
132
+ label: z
133
+ .string()
134
+ .optional()
135
+ .describe("Optional text at connector midpoint"),
136
+ arrow: z
137
+ .enum(["forward", "back", "both", "none"])
138
+ .optional()
139
+ .describe("Arrow direction, default: forward"),
140
+ }, async (params) => {
141
+ try {
142
+ const result = await bridge.execute("create_connector", params);
143
+ return {
144
+ content: [{ type: "text", text: JSON.stringify(result) }],
145
+ };
146
+ }
147
+ catch (err) {
148
+ const message = err instanceof Error ? err.message : String(err);
149
+ return {
150
+ isError: true,
151
+ content: [{ type: "text", text: message }],
152
+ };
153
+ }
154
+ });
155
+ server.tool("figjam_update_node", "Edit any property of an existing node. Pass only the fields that need to change.", {
156
+ node_id: z.string().describe("ID of the node to update"),
157
+ content: z.string().optional(),
158
+ x: z.number().optional(),
159
+ y: z.number().optional(),
160
+ width: z.number().optional(),
161
+ height: z.number().optional(),
162
+ color: z.string().optional().describe("Hex color"),
163
+ }, async (params) => {
164
+ try {
165
+ const result = await bridge.execute("update_node", params);
166
+ return {
167
+ content: [{ type: "text", text: JSON.stringify(result) }],
168
+ };
169
+ }
170
+ catch (err) {
171
+ const message = err instanceof Error ? err.message : String(err);
172
+ return {
173
+ isError: true,
174
+ content: [{ type: "text", text: message }],
175
+ };
176
+ }
177
+ });
178
+ server.tool("figjam_delete_nodes", "Removes one or more nodes by ID. Accepts a single ID string or array of IDs.", {
179
+ node_ids: z
180
+ .union([z.string(), z.array(z.string())])
181
+ .describe("Node ID or array of IDs to delete"),
182
+ }, async (params) => {
183
+ try {
184
+ const result = await bridge.execute("delete_nodes", params);
185
+ return {
186
+ content: [{ type: "text", text: JSON.stringify(result) }],
187
+ };
188
+ }
189
+ catch (err) {
190
+ const message = err instanceof Error ? err.message : String(err);
191
+ return {
192
+ isError: true,
193
+ content: [{ type: "text", text: message }],
194
+ };
195
+ }
196
+ });
197
+ server.tool("figjam_list_pages", "Returns all pages in the FigJam file with their IDs, names, and which one is currently active.", {}, async () => {
198
+ try {
199
+ const result = await bridge.execute("list_pages", {});
200
+ return {
201
+ content: [
202
+ { type: "text", text: JSON.stringify(result, null, 2) },
203
+ ],
204
+ };
205
+ }
206
+ catch (err) {
207
+ const message = err instanceof Error ? err.message : String(err);
208
+ return {
209
+ isError: true,
210
+ content: [{ type: "text", text: message }],
211
+ };
212
+ }
213
+ });
214
+ server.tool("figjam_switch_page", "Switches the active FigJam page by name or ID. After switching, figjam_read_board will return the new page's content.", {
215
+ name: z.string().optional().describe("Page name to switch to"),
216
+ id: z.string().optional().describe("Page ID to switch to"),
217
+ }, async (params) => {
218
+ try {
219
+ const result = await bridge.execute("switch_page", params);
220
+ return {
221
+ content: [{ type: "text", text: JSON.stringify(result) }],
222
+ };
223
+ }
224
+ catch (err) {
225
+ const message = err instanceof Error ? err.message : String(err);
226
+ return {
227
+ isError: true,
228
+ content: [{ type: "text", text: message }],
229
+ };
230
+ }
231
+ });
232
+ server.tool("figjam_get_selection", "Returns the nodes currently selected on the FigJam canvas.", {}, async () => {
233
+ try {
234
+ const result = await bridge.execute("get_selection", {});
235
+ return {
236
+ content: [
237
+ { type: "text", text: JSON.stringify(result, null, 2) },
238
+ ],
239
+ };
240
+ }
241
+ catch (err) {
242
+ const message = err instanceof Error ? err.message : String(err);
243
+ return {
244
+ isError: true,
245
+ content: [{ type: "text", text: message }],
246
+ };
247
+ }
248
+ });
249
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "claude-figjam",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for live FigJam board editing from Claude Code",
5
+ "type": "module",
6
+ "bin": {
7
+ "claude-figjam": "dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist/"
11
+ ],
12
+ "keywords": [
13
+ "claude",
14
+ "mcp",
15
+ "figjam",
16
+ "figma",
17
+ "claude-code"
18
+ ],
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/ABCreativeDesign/figma-plugins.git",
22
+ "directory": "claude-figjam/mcp-server"
23
+ },
24
+ "license": "MIT",
25
+ "scripts": {
26
+ "build": "tsc",
27
+ "start": "node dist/index.js",
28
+ "dev": "tsx src/index.ts",
29
+ "test": "node --experimental-vm-modules node_modules/.bin/jest"
30
+ },
31
+ "dependencies": {
32
+ "@modelcontextprotocol/sdk": "^1.0.0",
33
+ "express": "^4.18.0",
34
+ "ws": "^8.16.0",
35
+ "zod": "^3.22.0"
36
+ },
37
+ "devDependencies": {
38
+ "@types/express": "^4.17.0",
39
+ "@types/ws": "^8.5.10",
40
+ "@types/node": "^20.0.0",
41
+ "typescript": "^5.4.0",
42
+ "jest": "^29.7.0",
43
+ "@types/jest": "^29.5.0",
44
+ "ts-jest": "^29.1.0",
45
+ "tsx": "^4.7.0"
46
+ }
47
+ }