devglow-mcp 0.9.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.js +320 -189
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -1,206 +1,215 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
5
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
6
|
+
import { createServer } from "node:http";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
4
8
|
import { z } from "zod";
|
|
5
9
|
import { loadProjects, loadAiProcesses, loadStatuses, loadExportedLogs, } from "./storage.js";
|
|
6
10
|
import { startProject, stopProject, runProcess, stopProcess, exportLogs, } from "./deeplink.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
11
|
+
// --- Server factory ---
|
|
12
|
+
// Each transport connection gets its own McpServer instance
|
|
13
|
+
function createMcpServer() {
|
|
14
|
+
const server = new McpServer({
|
|
15
|
+
name: "devglow-mcp",
|
|
16
|
+
version: "1.0.0",
|
|
17
|
+
}, {
|
|
18
|
+
instructions: [
|
|
19
|
+
"devglow is the user's local process manager (macOS menu bar app).",
|
|
20
|
+
"",
|
|
21
|
+
"## When starting a process",
|
|
22
|
+
"- Call `list_projects` first to check if the user already has a registered project.",
|
|
23
|
+
"- If found, use `start_project` (runs the user's registered command as-is).",
|
|
24
|
+
"- Only use `run_process` when no matching project exists.",
|
|
25
|
+
"",
|
|
26
|
+
"## Reading logs",
|
|
27
|
+
"- `get_logs` triggers a log export from DevGlow, waits briefly, then reads the file.",
|
|
28
|
+
"- If logs are empty, DevGlow may not have finished writing. Try again after a moment.",
|
|
29
|
+
].join("\n"),
|
|
30
|
+
});
|
|
31
|
+
// ── Tool: list_projects ──
|
|
32
|
+
server.registerTool("list_projects", {
|
|
33
|
+
title: "List all projects",
|
|
34
|
+
description: "List all registered user projects and AI temporary processes with their status.",
|
|
35
|
+
inputSchema: {},
|
|
36
|
+
}, async () => {
|
|
37
|
+
const projects = loadProjects();
|
|
38
|
+
const aiProcesses = loadAiProcesses();
|
|
39
|
+
const statuses = loadStatuses();
|
|
40
|
+
const statusMap = new Map(statuses.map((s) => [s.id, s]));
|
|
41
|
+
let text = "";
|
|
42
|
+
if (projects.length > 0) {
|
|
43
|
+
text += "## Projects\n";
|
|
44
|
+
for (const p of projects) {
|
|
45
|
+
const status = statusMap.get(p.id);
|
|
46
|
+
const running = status?.running ? "running" : "stopped";
|
|
47
|
+
const pid = status?.pid ? ` (PID: ${status.pid})` : "";
|
|
48
|
+
const port = p.port ? ` :${p.port}` : "";
|
|
49
|
+
text += `- [${running}] ${p.name}${port}${pid} — ${p.command} (id: ${p.id})\n`;
|
|
50
|
+
}
|
|
43
51
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
if (aiProcesses.length > 0) {
|
|
53
|
+
text += "\n## AI Processes\n";
|
|
54
|
+
for (const p of aiProcesses) {
|
|
55
|
+
const status = statusMap.get(p.id);
|
|
56
|
+
const running = status?.running ? "running" : "stopped";
|
|
57
|
+
const pid = status?.pid ? ` (PID: ${status.pid})` : "";
|
|
58
|
+
const port = p.port ? ` :${p.port}` : "";
|
|
59
|
+
text += `- [${running}] ${p.name}${port}${pid} — ${p.command} [${p.source}] (id: ${p.id})\n`;
|
|
60
|
+
}
|
|
53
61
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
},
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
},
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
62
|
+
if (!text) {
|
|
63
|
+
text = "No projects or AI processes registered.";
|
|
64
|
+
}
|
|
65
|
+
return { content: [{ type: "text", text }] };
|
|
66
|
+
});
|
|
67
|
+
// ── Tool: get_status ──
|
|
68
|
+
server.registerTool("get_status", {
|
|
69
|
+
title: "Get project status",
|
|
70
|
+
description: "Get the running status of a specific project or AI process.",
|
|
71
|
+
inputSchema: {
|
|
72
|
+
id: z.string().describe("Project or AI process ID"),
|
|
73
|
+
},
|
|
74
|
+
}, async ({ id }) => {
|
|
75
|
+
const statuses = loadStatuses();
|
|
76
|
+
const status = statuses.find((s) => s.id === id);
|
|
77
|
+
if (!status) {
|
|
78
|
+
return {
|
|
79
|
+
content: [{ type: "text", text: `No status found for ID: ${id}` }],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const text = `ID: ${status.id}\nRunning: ${status.running}\nPID: ${status.pid ?? "N/A"}\nSource: ${status.source}`;
|
|
83
|
+
return { content: [{ type: "text", text }] };
|
|
84
|
+
});
|
|
85
|
+
// ── Tool: get_logs ──
|
|
86
|
+
server.registerTool("get_logs", {
|
|
87
|
+
title: "Get process logs",
|
|
88
|
+
description: "Export and retrieve recent logs from a running or stopped process.",
|
|
89
|
+
inputSchema: {
|
|
90
|
+
id: z.string().describe("Project or AI process ID"),
|
|
91
|
+
lines: z
|
|
92
|
+
.number()
|
|
93
|
+
.optional()
|
|
94
|
+
.describe("Number of recent lines to return (default: 100)"),
|
|
95
|
+
},
|
|
96
|
+
}, async ({ id, lines }) => {
|
|
97
|
+
// Trigger log export via deep link
|
|
98
|
+
await exportLogs(id);
|
|
99
|
+
// Wait for DevGlow to write the file
|
|
100
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
101
|
+
const logs = loadExportedLogs(id);
|
|
102
|
+
const limit = lines ?? 100;
|
|
103
|
+
const recent = logs.slice(-limit);
|
|
104
|
+
if (recent.length === 0) {
|
|
105
|
+
return {
|
|
106
|
+
content: [
|
|
107
|
+
{
|
|
108
|
+
type: "text",
|
|
109
|
+
text: `No logs found for ID: ${id}. The process may not have produced output yet.`,
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const text = recent
|
|
115
|
+
.map((l) => `[${l.timestamp}] [${l.level}] ${l.message}`)
|
|
116
|
+
.join("\n");
|
|
117
|
+
return { content: [{ type: "text", text }] };
|
|
118
|
+
});
|
|
119
|
+
// ── Tool: check_port ──
|
|
120
|
+
server.registerTool("check_port", {
|
|
121
|
+
title: "Check port availability",
|
|
122
|
+
description: "Check if a TCP port is in use on localhost.",
|
|
123
|
+
inputSchema: {
|
|
124
|
+
port: z.number().describe("Port number to check"),
|
|
125
|
+
},
|
|
126
|
+
}, async ({ port }) => {
|
|
127
|
+
const inUse = await checkPort(port);
|
|
128
|
+
const text = inUse
|
|
129
|
+
? `Port ${port} is in use.`
|
|
130
|
+
: `Port ${port} is available.`;
|
|
131
|
+
return { content: [{ type: "text", text }] };
|
|
132
|
+
});
|
|
133
|
+
// ── Tool: start_project ──
|
|
134
|
+
server.registerTool("start_project", {
|
|
135
|
+
title: "Start a registered project",
|
|
136
|
+
description: "Start a user-registered project using its configured command. Cannot modify the command.",
|
|
137
|
+
inputSchema: {
|
|
138
|
+
id: z.string().describe("Project ID (from list_projects)"),
|
|
139
|
+
},
|
|
140
|
+
}, async ({ id }) => {
|
|
141
|
+
const projects = loadProjects();
|
|
142
|
+
const project = projects.find((p) => p.id === id);
|
|
143
|
+
if (!project) {
|
|
144
|
+
return {
|
|
145
|
+
content: [{ type: "text", text: `Project not found: ${id}` }],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
await startProject(id);
|
|
149
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
150
|
+
const statuses = loadStatuses();
|
|
151
|
+
const status = statuses.find((s) => s.id === id);
|
|
152
|
+
const running = status?.running ? "started" : "may still be starting";
|
|
98
153
|
return {
|
|
99
154
|
content: [
|
|
100
155
|
{
|
|
101
156
|
type: "text",
|
|
102
|
-
text: `
|
|
157
|
+
text: `Project "${project.name}" ${running}. Command: ${project.command}`,
|
|
103
158
|
},
|
|
104
159
|
],
|
|
105
160
|
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
inputSchema: {
|
|
117
|
-
port: z.number().describe("Port number to check"),
|
|
118
|
-
},
|
|
119
|
-
}, async ({ port }) => {
|
|
120
|
-
const inUse = await checkPort(port);
|
|
121
|
-
const text = inUse
|
|
122
|
-
? `Port ${port} is in use.`
|
|
123
|
-
: `Port ${port} is available.`;
|
|
124
|
-
return { content: [{ type: "text", text }] };
|
|
125
|
-
});
|
|
126
|
-
// ── Tool: start_project ──
|
|
127
|
-
server.registerTool("start_project", {
|
|
128
|
-
title: "Start a registered project",
|
|
129
|
-
description: "Start a user-registered project using its configured command. Cannot modify the command.",
|
|
130
|
-
inputSchema: {
|
|
131
|
-
id: z.string().describe("Project ID (from list_projects)"),
|
|
132
|
-
},
|
|
133
|
-
}, async ({ id }) => {
|
|
134
|
-
const projects = loadProjects();
|
|
135
|
-
const project = projects.find((p) => p.id === id);
|
|
136
|
-
if (!project) {
|
|
161
|
+
});
|
|
162
|
+
// ── Tool: stop_project ──
|
|
163
|
+
server.registerTool("stop_project", {
|
|
164
|
+
title: "Stop a project",
|
|
165
|
+
description: "Stop a running project or AI process.",
|
|
166
|
+
inputSchema: {
|
|
167
|
+
id: z.string().describe("Project or AI process ID"),
|
|
168
|
+
},
|
|
169
|
+
}, async ({ id }) => {
|
|
170
|
+
await stopProject(id);
|
|
137
171
|
return {
|
|
138
|
-
content: [{ type: "text", text: `
|
|
172
|
+
content: [{ type: "text", text: `Stop signal sent for: ${id}` }],
|
|
139
173
|
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
})
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
.describe("Port number the process will listen on. Check package.json scripts or config files for --port flags. Important for DevGlow to show the correct port badge."),
|
|
180
|
-
},
|
|
181
|
-
}, async ({ name, path, command, port }) => {
|
|
182
|
-
// Auto-detect port from command if not explicitly provided
|
|
183
|
-
const resolvedPort = port ?? detectPortFromCommand(command);
|
|
184
|
-
await runProcess(name, path, command, resolvedPort, "claude");
|
|
185
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
186
|
-
const text = `AI process "${name}" started.\nCommand: ${command}\nPath: ${path}${resolvedPort ? `\nPort: ${resolvedPort}` : ""}`;
|
|
187
|
-
return { content: [{ type: "text", text }] };
|
|
188
|
-
});
|
|
189
|
-
// ── Tool: stop_process ──
|
|
190
|
-
server.registerTool("stop_process", {
|
|
191
|
-
title: "Stop an AI process",
|
|
192
|
-
description: "Stop a running AI temporary process by name.",
|
|
193
|
-
inputSchema: {
|
|
194
|
-
name: z.string().describe("AI process name"),
|
|
195
|
-
},
|
|
196
|
-
}, async ({ name }) => {
|
|
197
|
-
await stopProcess(name);
|
|
198
|
-
return {
|
|
199
|
-
content: [
|
|
200
|
-
{ type: "text", text: `Stop signal sent for AI process: "${name}"` },
|
|
201
|
-
],
|
|
202
|
-
};
|
|
203
|
-
});
|
|
174
|
+
});
|
|
175
|
+
// ── Tool: run_process ──
|
|
176
|
+
server.registerTool("run_process", {
|
|
177
|
+
title: "Run a new AI process",
|
|
178
|
+
description: "Create and start a new temporary process. Shows in DevGlow's AI Processes section. User can later [keep] or [dismiss] it.",
|
|
179
|
+
inputSchema: {
|
|
180
|
+
name: z.string().describe("Display name for the process"),
|
|
181
|
+
path: z.string().describe("Working directory (supports ~/)"),
|
|
182
|
+
command: z.string().describe("Shell command to execute"),
|
|
183
|
+
port: z
|
|
184
|
+
.number()
|
|
185
|
+
.optional()
|
|
186
|
+
.describe("Port number the process will listen on. Check package.json scripts or config files for --port flags. Important for DevGlow to show the correct port badge."),
|
|
187
|
+
},
|
|
188
|
+
}, async ({ name, path, command, port }) => {
|
|
189
|
+
// Auto-detect port from command if not explicitly provided
|
|
190
|
+
const resolvedPort = port ?? detectPortFromCommand(command);
|
|
191
|
+
await runProcess(name, path, command, resolvedPort, "claude");
|
|
192
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
193
|
+
const text = `AI process "${name}" started.\nCommand: ${command}\nPath: ${path}${resolvedPort ? `\nPort: ${resolvedPort}` : ""}`;
|
|
194
|
+
return { content: [{ type: "text", text }] };
|
|
195
|
+
});
|
|
196
|
+
// ── Tool: stop_process ──
|
|
197
|
+
server.registerTool("stop_process", {
|
|
198
|
+
title: "Stop an AI process",
|
|
199
|
+
description: "Stop a running AI temporary process by name.",
|
|
200
|
+
inputSchema: {
|
|
201
|
+
name: z.string().describe("AI process name"),
|
|
202
|
+
},
|
|
203
|
+
}, async ({ name }) => {
|
|
204
|
+
await stopProcess(name);
|
|
205
|
+
return {
|
|
206
|
+
content: [
|
|
207
|
+
{ type: "text", text: `Stop signal sent for AI process: "${name}"` },
|
|
208
|
+
],
|
|
209
|
+
};
|
|
210
|
+
});
|
|
211
|
+
return server;
|
|
212
|
+
}
|
|
204
213
|
// ── Utility ──
|
|
205
214
|
function detectPortFromCommand(command) {
|
|
206
215
|
// Match patterns: --port 3000, --port=3000, -p 3000, -p=3000
|
|
@@ -227,11 +236,133 @@ function checkPort(port) {
|
|
|
227
236
|
});
|
|
228
237
|
});
|
|
229
238
|
}
|
|
239
|
+
// ── Args ──
|
|
240
|
+
function parseArgs() {
|
|
241
|
+
const args = process.argv.slice(2);
|
|
242
|
+
let transport = "stdio";
|
|
243
|
+
let port = 26215;
|
|
244
|
+
for (let i = 0; i < args.length; i++) {
|
|
245
|
+
if (args[i] === "--transport" && args[i + 1]) {
|
|
246
|
+
const value = args[i + 1];
|
|
247
|
+
// "sse" is an alias for "http" (backwards compat)
|
|
248
|
+
transport = value === "sse" || value === "http" ? "http" : "stdio";
|
|
249
|
+
i++;
|
|
250
|
+
}
|
|
251
|
+
else if (args[i] === "--port" && args[i + 1]) {
|
|
252
|
+
port = parseInt(args[i + 1], 10);
|
|
253
|
+
i++;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return { transport, port };
|
|
257
|
+
}
|
|
230
258
|
// ── Main ──
|
|
231
259
|
async function main() {
|
|
232
|
-
const transport =
|
|
233
|
-
|
|
234
|
-
|
|
260
|
+
const { transport: mode, port } = parseArgs();
|
|
261
|
+
if (mode === "http") {
|
|
262
|
+
const sseTransports = new Map();
|
|
263
|
+
const streamableTransports = new Map();
|
|
264
|
+
const httpServer = createServer(async (req, res) => {
|
|
265
|
+
const url = new URL(req.url || "", `http://localhost:${port}`);
|
|
266
|
+
// CORS headers
|
|
267
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
268
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
269
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, mcp-session-id");
|
|
270
|
+
if (req.method === "OPTIONS") {
|
|
271
|
+
res.writeHead(204);
|
|
272
|
+
res.end();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
// --- Streamable HTTP: /mcp ---
|
|
276
|
+
if (url.pathname === "/mcp") {
|
|
277
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
278
|
+
if (sessionId && streamableTransports.has(sessionId)) {
|
|
279
|
+
const transport = streamableTransports.get(sessionId);
|
|
280
|
+
await transport.handleRequest(req, res, await parseBody(req));
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (!sessionId && req.method === "POST") {
|
|
284
|
+
const transport = new StreamableHTTPServerTransport({
|
|
285
|
+
sessionIdGenerator: () => randomUUID(),
|
|
286
|
+
onsessioninitialized: (sid) => {
|
|
287
|
+
streamableTransports.set(sid, transport);
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
transport.onclose = () => {
|
|
291
|
+
const sid = transport.sessionId;
|
|
292
|
+
if (sid)
|
|
293
|
+
streamableTransports.delete(sid);
|
|
294
|
+
};
|
|
295
|
+
const mcpServer = createMcpServer();
|
|
296
|
+
await mcpServer.connect(transport);
|
|
297
|
+
await transport.handleRequest(req, res, await parseBody(req));
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
301
|
+
res.end(JSON.stringify({ error: "Bad request" }));
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
// --- SSE (legacy): /sse + /messages ---
|
|
305
|
+
if (req.method === "GET" && url.pathname === "/sse") {
|
|
306
|
+
const sseTransport = new SSEServerTransport("/messages", res);
|
|
307
|
+
sseTransports.set(sseTransport.sessionId, sseTransport);
|
|
308
|
+
res.on("close", () => {
|
|
309
|
+
sseTransports.delete(sseTransport.sessionId);
|
|
310
|
+
});
|
|
311
|
+
const mcpServer = createMcpServer();
|
|
312
|
+
await mcpServer.connect(sseTransport);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (req.method === "POST" && url.pathname === "/messages") {
|
|
316
|
+
const sessionId = url.searchParams.get("sessionId");
|
|
317
|
+
const sseTransport = sessionId
|
|
318
|
+
? sseTransports.get(sessionId)
|
|
319
|
+
: undefined;
|
|
320
|
+
if (!sseTransport) {
|
|
321
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
322
|
+
res.end(JSON.stringify({ error: "No transport found for sessionId" }));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
await sseTransport.handlePostMessage(req, res, await parseBody(req));
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
res.writeHead(404);
|
|
329
|
+
res.end("Not found");
|
|
330
|
+
});
|
|
331
|
+
httpServer.listen(port, () => {
|
|
332
|
+
console.error(`devglow MCP server running on http://localhost:${port}\n` +
|
|
333
|
+
` Streamable HTTP: http://localhost:${port}/mcp\n` +
|
|
334
|
+
` SSE (legacy): http://localhost:${port}/sse`);
|
|
335
|
+
});
|
|
336
|
+
process.on("SIGINT", async () => {
|
|
337
|
+
for (const [, t] of sseTransports)
|
|
338
|
+
await t.close();
|
|
339
|
+
for (const [, t] of streamableTransports)
|
|
340
|
+
await t.close();
|
|
341
|
+
httpServer.close();
|
|
342
|
+
process.exit(0);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
// stdio: single connection, single server
|
|
347
|
+
const mcpServer = createMcpServer();
|
|
348
|
+
const transport = new StdioServerTransport();
|
|
349
|
+
await mcpServer.connect(transport);
|
|
350
|
+
console.error("devglow MCP server running on stdio");
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function parseBody(req) {
|
|
354
|
+
return new Promise((resolve) => {
|
|
355
|
+
let data = "";
|
|
356
|
+
req.on("data", (chunk) => (data += chunk));
|
|
357
|
+
req.on("end", () => {
|
|
358
|
+
try {
|
|
359
|
+
resolve(JSON.parse(data));
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
resolve(undefined);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
});
|
|
235
366
|
}
|
|
236
367
|
main().catch((error) => {
|
|
237
368
|
console.error("Fatal error:", error);
|