Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ function getAccessToken() {
// Configure and run local MCP server (stdio transport)
async function run() {
const server = createMcpServer();
const { callTool } = await initDesignerAppBridge();
const { callTool } = await initDesignerAppBridge({ getClient });
registerMiscTools(server);
registerTools(server, getClient, getAccessToken);
registerDesignerTools(server, {
Expand Down
80 changes: 79 additions & 1 deletion src/modules/designerAppBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,23 @@ import express from "express";
import http from "http";
import { Socket, Server as SocketIOServer } from "socket.io";
import cors from "cors";
import { WebflowClient } from "webflow-api";
import { RPCType } from "../types/RPCType";
import { generateUUIDv4, getFreePort } from "../utils";
import { uploadAssetFromUrl } from "../tools/assetUpload";

type returnType = {
callTool: RPCType["callTool"];
};

type BridgeOptions = {
/**
* Optional Webflow Data API client accessor. Required to enable the
* `/api/upload-asset` HTTP endpoint; otherwise that route returns 500.
*/
getClient?: () => WebflowClient;
};

const START_PORT = 1338;
const END_PORT = 1638;

Expand Down Expand Up @@ -123,7 +133,11 @@ const initRPC = (io: SocketIOServer, port: number): returnType => {
};
};

export const initDesignerAppBridge = async (): Promise<returnType> => {
export const initDesignerAppBridge = async (
options: BridgeOptions = {},
): Promise<returnType> => {
const { getClient } = options;

// Initialize Express app
const app = express();
// Allow Private Network Access (Chrome requires this for localhost access from public origins)
Expand All @@ -141,6 +155,8 @@ export const initDesignerAppBridge = async (): Promise<returnType> => {
credentials: true,
}),
);
// Parse JSON bodies for the HTTP proxy endpoints
app.use(express.json({ limit: "10mb" }));

// Create HTTP server using the Express app
const server = http.createServer(app);
Expand All @@ -166,6 +182,68 @@ export const initDesignerAppBridge = async (): Promise<returnType> => {

const rpc = initRPC(io, port);

// ── HTTP Proxy for tool calls (hackathon) ─────────────────
// Allows external apps (e.g. Designer extensions) to call MCP tools
// via HTTP POST instead of stdio. Forwards to the Designer RPC bridge.
// POST /api/tool-call { toolName, args }
app.post("/api/tool-call", async (req, res) => {
try {
const { toolName, args } = req.body ?? {};
if (!toolName || !args) {
res.status(400).json({ error: "toolName and args required" });
return;
}
const result = await rpc.callTool(toolName, args);
res.json({ result });
} catch (err: any) {
res.status(500).json({ error: err.message ?? "Tool call failed" });
}
});

// GET /api/status — health check for the bridge
app.get("/api/status", (_, res) => {
res.json({ status: "ok", port });
});

// POST /api/upload-asset — upload image from URL to Webflow site
// { siteId, url, fileName?, altText? } → { assetId, fileName, hostedUrl? }
// Uses the Webflow Data API directly (bypasses the Designer RPC bridge),
// so getClient must be supplied when initializing the bridge.
app.post("/api/upload-asset", async (req, res) => {
try {
if (!getClient) {
res.status(500).json({
error:
"Asset upload endpoint is not configured — bridge was initialized without a Webflow client.",
});
return;
}
const { siteId, url, fileName, altText } = req.body ?? {};
if (!siteId || !url) {
res.status(400).json({ error: "siteId and url are required" });
return;
}
const result = await uploadAssetFromUrl(getClient(), {
siteId,
url,
fileName,
altText,
});
if (!result.success) {
res.status(500).json({ error: result.error });
return;
}
res.json({
assetId: result.assetId,
fileName: result.fileName,
hostedUrl: result.hostedUrl,
assetUrl: result.assetUrl,
});
} catch (err: any) {
res.status(500).json({ error: err.message ?? "Upload failed" });
}
});

return rpc;
} catch (e) {
return {
Expand Down
Loading