Skip to content

Commit 2fd40ac

Browse files
committed
feat: update CallbackServer implementation
1 parent c7c2e4d commit 2fd40ac

File tree

3 files changed

+95
-75
lines changed

3 files changed

+95
-75
lines changed

src/index.ts

+1-10
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
55

66
import { CreateUiTool } from "./tools/create-ui.js";
77
import { LogoSearchTool } from "./tools/logo-search.js";
8-
import { callbackServer } from "./utils/callback-server.js";
98

10-
const VERSION = "0.0.17";
9+
const VERSION = "0.0.19";
1110

1211
const server = new McpServer({
1312
name: "21st-magic",
@@ -22,14 +21,6 @@ async function runServer() {
2221
const transport = new StdioServerTransport();
2322
console.log(`Starting server v${VERSION}...`);
2423

25-
// Initialize the callback server
26-
try {
27-
await callbackServer.startServer();
28-
console.log("Callback server initialized");
29-
} catch (error) {
30-
console.error("Failed to initialize callback server:", error);
31-
}
32-
3324
await server.connect(transport);
3425
console.log("Server started");
3526
}

src/tools/create-ui.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { z } from "zod";
22
import { BaseTool } from "../utils/base-tool.js";
33
import { twentyFirstClient } from "../utils/http-client.js";
4-
import { callbackServer } from "../utils/callback-server.js";
4+
import { CallbackServer } from "../utils/callback-server.js";
55

66
const UI_TOOL_NAME = "21st_magic_component_builder";
77
const UI_TOOL_DESCRIPTION = `
@@ -57,11 +57,12 @@ export class CreateUiTool extends BaseTool {
5757
}),
5858
]);
5959

60-
const { data } = await callbackServer.promptUser({
60+
const server = new CallbackServer();
61+
const { data } = await server.promptUser({
6162
initialData: {
62-
data1: responses[0],
63-
data2: responses[1],
64-
data3: responses[2],
63+
data1: responses[0].data,
64+
data2: responses[1].data,
65+
data3: responses[2].data,
6566
},
6667
});
6768

src/utils/callback-server.ts

+88-60
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { AddressInfo } from "net";
44
import open from "open";
55
import path from "path";
66
import { fileURLToPath } from "url";
7+
import net from "net";
78

89
const __filename = fileURLToPath(import.meta.url);
910
const __dirname = path.dirname(__filename);
@@ -14,102 +15,129 @@ export interface CallbackResponse {
1415

1516
export interface CallbackServerConfig {
1617
initialData?: any;
18+
timeout?: number;
1719
}
1820

19-
class CallbackServer {
20-
private static instance: CallbackServer;
21+
export class CallbackServer {
2122
private server: Server | null = null;
22-
private app: express.Express;
23+
private app = express();
2324
private port: number;
24-
private previewerPath: string;
25-
private responseHandlers: Map<
26-
string,
27-
{ resolve: (data: CallbackResponse) => void; initialData?: any }
28-
> = new Map();
25+
private sessionId = Math.random().toString(36).substring(7);
26+
private timeoutId?: NodeJS.Timeout;
2927

30-
private constructor(port = 3333) {
28+
constructor(port = 3333) {
3129
this.port = port;
32-
this.app = express();
33-
this.previewerPath = path.join(__dirname, "../../previewer");
34-
this.setupServer();
30+
this.setupRoutes();
3531
}
3632

37-
static getInstance(port?: number): CallbackServer {
38-
if (!CallbackServer.instance) {
39-
CallbackServer.instance = new CallbackServer(port);
40-
}
41-
return CallbackServer.instance;
42-
}
33+
private setupRoutes() {
34+
const previewerPath = path.join(__dirname, "../previewer");
4335

44-
private setupServer() {
4536
this.app.use(express.json());
46-
this.app.use(express.static(this.previewerPath));
47-
// app.use(
48-
// cors({
49-
// origin: "*",
50-
// methods: ["GET", "POST", "OPTIONS"],
51-
// allowedHeaders: ["Content-Type"],
52-
// })
53-
// );
37+
this.app.use(express.static(previewerPath));
5438

5539
this.app.get("/callback/:id", (req, res) => {
5640
const { id } = req.params;
57-
const initialData = this.responseHandlers.get(id)?.initialData;
58-
res.json({ status: "success", data: initialData });
41+
if (id === this.sessionId) {
42+
res.json({ status: "success", data: this.config?.initialData });
43+
} else {
44+
res.status(404).json({ status: "error", message: "Session not found" });
45+
}
5946
});
6047

6148
this.app.post("/callback/:id", (req, res) => {
6249
const { id } = req.params;
63-
const handler = this.responseHandlers.get(id);
50+
if (id === this.sessionId && this.promiseResolve) {
51+
if (this.timeoutId) clearTimeout(this.timeoutId);
6452

65-
if (handler) {
66-
const data = req.body || {};
67-
handler.resolve({ data });
68-
this.responseHandlers.delete(id);
53+
this.promiseResolve({ data: req.body || {} });
54+
this.shutdown();
6955
}
7056

7157
res.json({ status: "success" });
7258
});
7359

7460
this.app.get("*", (req, res) => {
75-
res.sendFile(path.join(this.previewerPath, "index.html"));
61+
res.sendFile(path.join(previewerPath, "index.html"));
7662
});
7763
}
7864

79-
async startServer(): Promise<void> {
80-
if (!this.server) {
81-
this.server = this.app.listen(this.port, "127.0.0.1", () => {
82-
const address = this.server?.address() as AddressInfo;
83-
const previewUrl = `http://127.0.0.1:${address.port}`;
84-
console.log(`Preview server running at ${previewUrl}`);
85-
});
65+
private async shutdown(): Promise<void> {
66+
if (this.server) {
67+
this.server.close();
68+
this.server = null;
69+
}
70+
if (this.timeoutId) {
71+
clearTimeout(this.timeoutId);
72+
}
73+
}
8674

87-
this.server.on("error", (error: Error) => {
88-
console.error("Server error:", error);
89-
});
75+
private isPortAvailable(port: number): Promise<boolean> {
76+
return new Promise((resolve) => {
77+
const tester = net
78+
.createServer()
79+
.once("error", () => resolve(false))
80+
.once("listening", () => {
81+
tester.close();
82+
resolve(true);
83+
})
84+
.listen(port, "127.0.0.1");
85+
});
86+
}
87+
88+
private async findAvailablePort(): Promise<number> {
89+
let port = this.port;
90+
for (let attempt = 0; attempt < 100; attempt++) {
91+
if (await this.isPortAvailable(port)) {
92+
return port;
93+
}
94+
port++;
9095
}
96+
throw new Error("Unable to find an available port after 100 attempts");
9197
}
9298

99+
private config?: CallbackServerConfig;
100+
private promiseResolve?: (value: CallbackResponse) => void;
101+
private promiseReject?: (reason: any) => void;
102+
93103
async promptUser(
94104
config: CallbackServerConfig = {}
95105
): Promise<CallbackResponse> {
96-
const { initialData = null } = config;
106+
const { initialData = null, timeout = 300000 } = config;
107+
this.config = config;
97108

98-
await this.startServer();
99-
const id = Math.random().toString(36).substring(7);
109+
try {
110+
// Find available port and start server
111+
const availablePort = await this.findAvailablePort();
112+
this.server = this.app.listen(availablePort, "127.0.0.1");
100113

101-
return new Promise<CallbackResponse>(async (resolve) => {
102-
this.responseHandlers.set(id, { resolve, initialData });
103-
104-
if (!this.server) {
105-
await this.startServer();
106-
}
114+
this.server.on("error", (error) => {
115+
if (this.promiseReject) this.promiseReject(error);
116+
});
107117

108-
const address = this.server!.address() as AddressInfo;
109-
const previewUrl = `http://127.0.0.1:${address.port}?id=${id}`;
110-
open(previewUrl);
111-
});
118+
// Create and return promise
119+
return new Promise<CallbackResponse>((resolve, reject) => {
120+
this.promiseResolve = resolve;
121+
this.promiseReject = reject;
122+
123+
// Set timeout
124+
this.timeoutId = setTimeout(() => {
125+
resolve({ data: { timedOut: true } });
126+
this.shutdown();
127+
}, timeout);
128+
129+
// Open browser
130+
const address = this.server!.address() as AddressInfo;
131+
const url = `http://127.0.0.1:${address.port}?id=${this.sessionId}`;
132+
133+
open(url).catch((error) => {
134+
reject(error);
135+
this.shutdown();
136+
});
137+
});
138+
} catch (error) {
139+
await this.shutdown();
140+
throw error;
141+
}
112142
}
113143
}
114-
115-
export const callbackServer = CallbackServer.getInstance();

0 commit comments

Comments
 (0)