@21st-dev/magic
Version:
Magic MCP UI builder by 21st.dev
248 lines (247 loc) • 9.48 kB
JavaScript
import { createServer } from "http";
import open from "open";
import path from "path";
import { fileURLToPath } from "url";
import net from "net";
import { twentyFirstClient } from "./http-client.js";
import fs from "fs";
import { parse as parseUrl } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export class CallbackServer {
server = null;
port;
sessionId = Math.random().toString(36).substring(7);
timeoutId;
mimeTypes = {
".html": "text/html",
".js": "text/javascript",
".css": "text/css",
".json": "application/json",
".png": "image/png",
".jpg": "image/jpg",
".gif": "image/gif",
".ico": "image/x-icon",
};
constructor(port = 3333) {
this.port = port;
}
parseBodyJson(req) {
return new Promise((resolve) => {
let body = "";
req.on("data", (chunk) => {
body += chunk.toString();
});
req.on("end", () => {
try {
const data = body ? JSON.parse(body) : {};
resolve(data);
}
catch (e) {
resolve({});
}
});
});
}
getRouteParams(url, pattern) {
const urlParts = url.split("/").filter(Boolean);
const patternParts = pattern.split("/").filter(Boolean);
if (urlParts.length !== patternParts.length)
return null;
const params = {};
for (let i = 0; i < patternParts.length; i++) {
if (patternParts[i].startsWith(":")) {
const paramName = patternParts[i].substring(1);
params[paramName] = urlParts[i];
}
else if (patternParts[i] !== urlParts[i]) {
return null;
}
}
return params;
}
async serveStatic(res, filepath) {
try {
const stat = await fs.promises.stat(filepath);
if (stat.isDirectory()) {
filepath = path.join(filepath, "index.html");
}
const ext = path.extname(filepath);
const contentType = this.mimeTypes[ext] || "application/octet-stream";
const content = await fs.promises.readFile(filepath);
res.writeHead(200, { "Content-Type": contentType });
res.end(content, "utf-8");
}
catch (err) {
if (err.code === "ENOENT") {
const previewerPath = path.join(__dirname, "../previewer");
const indexPath = path.join(previewerPath, "index.html");
if (filepath !== indexPath) {
await this.serveStatic(res, indexPath);
}
else {
res.writeHead(404);
res.end("Not found");
}
}
else {
res.writeHead(500);
res.end("Internal server error");
}
}
}
handleRequest = async (req, res) => {
const urlInfo = parseUrl(req.url || "/");
const pathname = urlInfo.pathname || "/";
// Handle callback route
if (req.method === "GET" && pathname.startsWith("/callback/")) {
const params = this.getRouteParams(pathname, "/callback/:id");
if (params && params.id === this.sessionId) {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "success", data: this.config?.initialData }));
return;
}
else {
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "error", message: "Session not found" }));
return;
}
}
// Handle callback post
if (req.method === "POST" && pathname.startsWith("/callback/")) {
const params = this.getRouteParams(pathname, "/callback/:id");
if (params && params.id === this.sessionId && this.promiseResolve) {
if (this.timeoutId)
clearTimeout(this.timeoutId);
const body = await this.parseBodyJson(req);
this.promiseResolve({ data: body || {} });
this.shutdown();
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "success" }));
return;
}
else {
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "error", message: "Session not found" }));
return;
}
}
// Handle fix-code-error route
if (req.method === "POST" && pathname.startsWith("/fix-code-error/")) {
const params = this.getRouteParams(pathname, "/fix-code-error/:id");
if (!params || params.id !== this.sessionId) {
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "error", message: "Session not found" }));
return;
}
const body = await this.parseBodyJson(req);
const { code, errorMessage } = body;
if (!code || !errorMessage) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({
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.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "success", data: response.data }));
}
else {
res.writeHead(response.status, {
"Content-Type": "application/json",
});
res.end(JSON.stringify({
status: "error",
message: response.data || "API Error",
}));
}
}
catch (error) {
console.error("Error proxying /fix-code-error:", error);
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({
status: "error",
message: error.message || "Internal Server Error",
}));
}
return;
}
// Serve static files or send index.html
const previewerPath = path.join(__dirname, "../previewer");
const filePath = path.join(previewerPath, pathname === "/" ? "index.html" : pathname);
await this.serveStatic(res, filePath);
};
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 = createServer(this.handleRequest);
this.server.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;
}
}
}