@21st-dev/magic
Version:
Magic MCP UI builder by 21st.dev
149 lines (148 loc) • 5.32 kB
JavaScript
import express from "express";
import open from "open";
import path from "path";
import { fileURLToPath } from "url";
import net from "net";
import { twentyFirstClient } from "./http-client.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export class CallbackServer {
server = null;
app = express();
port;
sessionId = Math.random().toString(36).substring(7);
timeoutId;
constructor(port = 3333) {
this.port = port;
this.setupRoutes();
}
setupRoutes() {
const previewerPath = path.join(__dirname, "../previewer");
this.app.use(express.json());
this.app.use(express.static(previewerPath));
this.app.get("/callback/:id", (req, res) => {
const { id } = req.params;
if (id === this.sessionId) {
res.json({ status: "success", data: this.config?.initialData });
}
else {
res.status(404).json({ status: "error", message: "Session not found" });
}
});
this.app.post("/callback/:id", (req, res) => {
const { id } = req.params;
if (id === this.sessionId && this.promiseResolve) {
if (this.timeoutId)
clearTimeout(this.timeoutId);
this.promiseResolve({ data: req.body || {} });
this.shutdown();
}
res.json({ status: "success" });
});
this.app.post("/fix-code-error/:id", async (req, res) => {
const { id } = req.params;
const { code, errorMessage } = req.body;
if (id !== this.sessionId) {
res
.status(404)
.json({ status: "error", message: "Session not found" });
return;
}
if (!code || !errorMessage) {
res
.status(400)
.json({ status: "error", message: "Missing code or errorMessage" });
return;
}
try {
const response = await twentyFirstClient.post("/api/fix-code-error", { code, errorMessage });
if (response.status === 200) {
res.json({ status: "success", data: response.data });
}
else {
res
.status(response.status)
.json({ status: "error", message: response.data || "API Error" });
}
}
catch (error) {
console.error("Error proxying /fix-code-error:", error);
res.status(500).json({
status: "error",
message: error.message || "Internal Server Error",
});
}
});
this.app.get("*", (req, res) => {
res.sendFile(path.join(previewerPath, "index.html"));
});
}
async shutdown() {
if (this.server) {
this.server.close();
this.server = null;
}
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
}
isPortAvailable(port) {
return new Promise((resolve) => {
const tester = net
.createServer()
.once("error", () => resolve(false))
.once("listening", () => {
tester.close();
resolve(true);
})
.listen(port, "127.0.0.1");
});
}
async findAvailablePort() {
let port = this.port;
for (let attempt = 0; attempt < 100; attempt++) {
if (await this.isPortAvailable(port)) {
return port;
}
port++;
}
throw new Error("Unable to find an available port after 100 attempts");
}
config;
promiseResolve;
promiseReject;
async promptUser(config = {}) {
const { initialData = null, timeout = 300000 } = config;
this.config = config;
try {
const availablePort = await this.findAvailablePort();
this.server = this.app.listen(availablePort, "127.0.0.1");
return new Promise((resolve, reject) => {
this.promiseResolve = resolve;
this.promiseReject = reject;
if (!this.server) {
reject(new Error("Failed to start server"));
return;
}
this.server.on("error", (error) => {
if (this.promiseReject)
this.promiseReject(error);
});
this.timeoutId = setTimeout(() => {
resolve({ data: { timedOut: true } });
this.shutdown();
}, timeout);
const url = `http://127.0.0.1:${availablePort}?id=${this.sessionId}`;
open(url).catch((error) => {
console.warn("Failed to open browser:", error);
resolve({ data: { browserOpenFailed: true } });
this.shutdown();
});
});
}
catch (error) {
await this.shutdown();
throw error;
}
}
}