Skip to content

Commit 7325485

Browse files
jamesmosiervirat21
andauthored
Add support for Designer Extension APIs (#60)
* Update .gitignore to include .vscode, .cursor, and pnpm-lock.yaml * Add new schemas and tools for Designer API and Rules * Add dependencies for CORS, Express, and Socket.io; enhance MCP server with new designer and miscellaneous tool registrations * Remove optional modifier from component name in DE components registration schema * Add local tools registration for OSS MCP version and enhance RPC with local connection URL --------- Co-authored-by: Virat <[email protected]>
1 parent 052f892 commit 7325485

23 files changed

+3124
-2
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,6 @@ node_modules/
77
# System
88
.DS_Store
99
.idea/
10-
.vscode/
10+
.vscode/
11+
.cursor
12+
pnpm-lock.yaml

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,15 @@
1919
"dependencies": {
2020
"@modelcontextprotocol/sdk": "^1.8.0",
2121
"agents": "^0.0.59",
22+
"cors": "^2.8.5",
23+
"express": "^5.1.0",
24+
"socket.io": "^4.8.1",
2225
"webflow-api": "3.1.1",
2326
"zod": "^3.24.2"
2427
},
2528
"devDependencies": {
29+
"@types/cors": "^2.8.19",
30+
"@types/express": "^5.0.3",
2631
"@types/node": "^22.13.13",
2732
"concurrently": "^9.1.2",
2833
"nodemon": "^3.1.9",

src/index.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@
22

33
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
44
import { WebflowClient } from "webflow-api";
5-
import { createMcpServer, registerTools } from "./mcp";
5+
import {
6+
createMcpServer,
7+
registerDesignerTools,
8+
registerLocalTools,
9+
registerMiscTools,
10+
registerTools,
11+
} from "./mcp";
12+
import { initDesignerAppBridge } from "./modules/designerAppBridge";
613

714
// Verify WEBFLOW_TOKEN exists
815
if (!process.env.WEBFLOW_TOKEN) {
@@ -22,7 +29,19 @@ function getClient() {
2229
// Configure and run local MCP server (stdio transport)
2330
async function run() {
2431
const server = createMcpServer();
32+
const { callTool } = await initDesignerAppBridge();
33+
registerMiscTools(server);
2534
registerTools(server, getClient);
35+
registerDesignerTools(server, {
36+
callTool,
37+
getClient,
38+
});
39+
40+
//Only valid for OSS MCP Version.
41+
registerLocalTools(server, {
42+
callTool,
43+
getClient,
44+
});
2645

2746
const transport = new StdioServerTransport();
2847
await server.connect(transport);

src/mcp.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,19 @@ import {
44
registerAiChatTools,
55
registerCmsTools,
66
registerComponentsTools,
7+
registerDEAssetTools,
8+
registerDEComponentsTools,
9+
registerDEElementTools,
10+
registerDEPagesTools,
711
registerPagesTools,
812
registerScriptsTools,
913
registerSiteTools,
14+
registerDEStyleTools,
15+
registerDEVariableTools,
16+
registerRulesTools,
17+
registerLocalDeMCPConnectionTools,
1018
} from "./tools";
19+
import { RPCType } from "./types/RPCType";
1120

1221
const packageJson = require("../package.json") as any;
1322

@@ -43,3 +52,29 @@ export function registerTools(
4352
registerScriptsTools(server, getClient);
4453
registerSiteTools(server, getClient);
4554
}
55+
56+
export function registerDesignerTools(
57+
server: McpServer,
58+
rpc: RPCType
59+
) {
60+
registerDEAssetTools(server, rpc);
61+
registerDEComponentsTools(server, rpc);
62+
registerDEElementTools(server, rpc);
63+
registerDEPagesTools(server, rpc);
64+
registerDEStyleTools(server, rpc);
65+
registerDEVariableTools(server, rpc);
66+
}
67+
68+
export function registerMiscTools(server: McpServer) {
69+
registerRulesTools(server);
70+
}
71+
72+
/**
73+
* IMPORTANT: registerLocalTools is only valid for OSS MCP Version
74+
*/
75+
export function registerLocalTools(
76+
server: McpServer,
77+
rpc: RPCType
78+
) {
79+
registerLocalDeMCPConnectionTools(server, rpc);
80+
}

src/modules/designerAppBridge.ts

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import express from "express";
2+
import http from "http";
3+
import {
4+
Socket,
5+
Server as SocketIOServer,
6+
} from "socket.io";
7+
import cors from "cors";
8+
import { RPCType } from "../types/RPCType";
9+
import { generateUUIDv4, getFreePort } from "../utils";
10+
11+
type returnType = {
12+
callTool: RPCType["callTool"];
13+
};
14+
15+
const START_PORT = 1338;
16+
const END_PORT = 1638;
17+
18+
const initRPC = (
19+
io: SocketIOServer,
20+
port: number
21+
): returnType => {
22+
const url = `http://localhost:${port}`;
23+
const siteIdToSocketMap = new Map<string, Set<Socket>>();
24+
const pendingToolResponse = new Map<
25+
string,
26+
(response: any) => void
27+
>();
28+
29+
io.on("connection", (socket) => {
30+
const { siteId } = socket.handshake.query as {
31+
siteId: string;
32+
};
33+
if (!siteId) {
34+
socket.emit("error", "Site ID is required");
35+
setTimeout(() => {
36+
socket.disconnect();
37+
}, 1000);
38+
return;
39+
}
40+
41+
if (!siteIdToSocketMap.has(siteId)) {
42+
siteIdToSocketMap.set(siteId, new Set());
43+
}
44+
siteIdToSocketMap.get(siteId)!.add(socket);
45+
46+
socket.emit("connection-confirmation", {
47+
siteId,
48+
message: "Connected to Webflow MCP",
49+
});
50+
51+
socket.on("tool-call-response", (data) => {
52+
const { requestId, data: responseData } = data as {
53+
requestId: string;
54+
data: any;
55+
};
56+
if (!requestId) {
57+
return;
58+
}
59+
if (!pendingToolResponse.has(requestId)) {
60+
return;
61+
}
62+
const toolResponse =
63+
pendingToolResponse.get(requestId);
64+
if (toolResponse) {
65+
toolResponse(responseData);
66+
pendingToolResponse.delete(requestId);
67+
}
68+
});
69+
70+
socket.on("disconnect", () => {
71+
if (siteIdToSocketMap.has(siteId)) {
72+
siteIdToSocketMap.get(siteId)?.delete(socket);
73+
}
74+
});
75+
});
76+
const callTool = (toolName: string, args: any) => {
77+
if (toolName === "local_de_mcp_connection_tool") {
78+
return Promise.resolve({
79+
status: true,
80+
message: `Share this url with the user to connect to the Webflow Designer App. ${url}. Please share complete url with the USER.`,
81+
url,
82+
});
83+
}
84+
const { siteId } = args as any;
85+
if (!siteId) {
86+
return Promise.resolve({
87+
status: false,
88+
error: "Site ID is required",
89+
});
90+
}
91+
const requestId = `${siteId}-${generateUUIDv4()}`;
92+
return new Promise((resolve) => {
93+
if (
94+
siteIdToSocketMap.has(siteId) &&
95+
siteIdToSocketMap.get(siteId)!.size > 0
96+
) {
97+
const sockets = siteIdToSocketMap.get(siteId)!;
98+
for (const socket of sockets) {
99+
socket.emit("call-tool", {
100+
toolName,
101+
args,
102+
siteId,
103+
requestId,
104+
});
105+
}
106+
const cleanup = () => {
107+
clearTimeout(timerId);
108+
pendingToolResponse.delete(requestId);
109+
};
110+
const timerId = setTimeout(() => {
111+
cleanup();
112+
resolve({
113+
error: `Tool call timed out, Please check Webflow Designer MCP app is running on Webflow Designer or restart the Webflow Designer App. make sure you are using correct url ${url} on app.`,
114+
});
115+
}, 20000); //20 seconds
116+
const toolResponse = (data: any) => {
117+
cleanup();
118+
resolve(data);
119+
};
120+
pendingToolResponse.set(requestId, toolResponse);
121+
} else {
122+
resolve({
123+
status: false,
124+
error:
125+
"No active Designer app connection to the site, Please make sure Designer app is open and connected, or check the site id is valid, all site ids can be found using the sites_list tool.",
126+
});
127+
}
128+
});
129+
};
130+
131+
return {
132+
callTool,
133+
};
134+
};
135+
136+
export const initDesignerAppBridge =
137+
async (): Promise<returnType> => {
138+
// Initialize Express app
139+
const app = express();
140+
app.use(cors()); // Enable CORS for all routes
141+
142+
// Create HTTP server using the Express app
143+
const server = http.createServer(app);
144+
145+
// Initialize Socket.IO with the HTTP server
146+
const io = new SocketIOServer(server, {
147+
cors: {
148+
origin: "*", // Allow connections from any origin
149+
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"], // Allow specified HTTP methods
150+
},
151+
pingTimeout: 20000, // Close connection after 20s of inactivity
152+
transports: ["websocket", "polling"], // Enable both WebSocket and HTTP polling
153+
});
154+
155+
app.get("/", (_, res) => {
156+
res.send("Webflow MCP is running");
157+
});
158+
159+
try {
160+
const port = await getFreePort(START_PORT, END_PORT);
161+
server.listen(port);
162+
163+
const rpc = initRPC(io, port);
164+
165+
return rpc;
166+
} catch (e) {
167+
return {
168+
callTool: () => {
169+
return Promise.resolve({
170+
status: false,
171+
error: `Unable to find a free port to start the Webflow Designer App Bridge. Please make sure you have port ${START_PORT}-${END_PORT} free.`,
172+
});
173+
},
174+
};
175+
}
176+
};

src/schemas/DEElementIDSchema.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { z } from "zod";
2+
3+
export const DEElementIDSchema = {
4+
id: z
5+
.object({
6+
component: z
7+
.string()
8+
.describe(
9+
"The component id of the element to perform action on."
10+
),
11+
element: z
12+
.string()
13+
.describe(
14+
"The element id of the element to perform action on."
15+
),
16+
})
17+
.describe(
18+
"The id of the element to perform action on, you can find it from id field on element. e.g id:{component:123,element:456}."
19+
),
20+
};

0 commit comments

Comments
 (0)