|
| 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 | + }; |
0 commit comments