devglow-mcp 1.0.0 → 1.0.1
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 +120 -78
- package/build/storage.js +11 -11
- package/package.json +2 -1
package/build/index.js
CHANGED
|
@@ -4,16 +4,22 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
4
4
|
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
5
5
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
6
6
|
import { createServer } from "node:http";
|
|
7
|
+
import { createConnection } from "node:net";
|
|
7
8
|
import { randomUUID } from "node:crypto";
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
8
12
|
import { z } from "zod";
|
|
9
13
|
import { loadProjects, loadAiProcesses, loadStatuses, loadExportedLogs, } from "./storage.js";
|
|
10
14
|
import { startProject, stopProject, runProcess, stopProcess, exportLogs, } from "./deeplink.js";
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
11
17
|
// --- Server factory ---
|
|
12
18
|
// Each transport connection gets its own McpServer instance
|
|
13
19
|
function createMcpServer() {
|
|
14
20
|
const server = new McpServer({
|
|
15
21
|
name: "devglow-mcp",
|
|
16
|
-
version:
|
|
22
|
+
version: pkg.version,
|
|
17
23
|
}, {
|
|
18
24
|
instructions: [
|
|
19
25
|
"devglow is the user's local process manager (macOS menu bar app).",
|
|
@@ -34,9 +40,9 @@ function createMcpServer() {
|
|
|
34
40
|
description: "List all registered user projects and AI temporary processes with their status.",
|
|
35
41
|
inputSchema: {},
|
|
36
42
|
}, async () => {
|
|
37
|
-
const projects = loadProjects();
|
|
38
|
-
const aiProcesses = loadAiProcesses();
|
|
39
|
-
const statuses = loadStatuses();
|
|
43
|
+
const projects = await loadProjects();
|
|
44
|
+
const aiProcesses = await loadAiProcesses();
|
|
45
|
+
const statuses = await loadStatuses();
|
|
40
46
|
const statusMap = new Map(statuses.map((s) => [s.id, s]));
|
|
41
47
|
let text = "";
|
|
42
48
|
if (projects.length > 0) {
|
|
@@ -72,7 +78,7 @@ function createMcpServer() {
|
|
|
72
78
|
id: z.string().describe("Project or AI process ID"),
|
|
73
79
|
},
|
|
74
80
|
}, async ({ id }) => {
|
|
75
|
-
const statuses = loadStatuses();
|
|
81
|
+
const statuses = await loadStatuses();
|
|
76
82
|
const status = statuses.find((s) => s.id === id);
|
|
77
83
|
if (!status) {
|
|
78
84
|
return {
|
|
@@ -96,11 +102,16 @@ function createMcpServer() {
|
|
|
96
102
|
}, async ({ id, lines }) => {
|
|
97
103
|
// Trigger log export via deep link
|
|
98
104
|
await exportLogs(id);
|
|
99
|
-
//
|
|
100
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
101
|
-
const logs = loadExportedLogs(id);
|
|
105
|
+
// Retry up to 5 times (100ms intervals) to read exported logs
|
|
102
106
|
const limit = lines ?? 100;
|
|
103
|
-
|
|
107
|
+
let recent = [];
|
|
108
|
+
for (let i = 0; i < 5; i++) {
|
|
109
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
110
|
+
const logs = await loadExportedLogs(id);
|
|
111
|
+
recent = logs.slice(-limit);
|
|
112
|
+
if (recent.length > 0)
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
104
115
|
if (recent.length === 0) {
|
|
105
116
|
return {
|
|
106
117
|
content: [
|
|
@@ -138,7 +149,7 @@ function createMcpServer() {
|
|
|
138
149
|
id: z.string().describe("Project ID (from list_projects)"),
|
|
139
150
|
},
|
|
140
151
|
}, async ({ id }) => {
|
|
141
|
-
const projects = loadProjects();
|
|
152
|
+
const projects = await loadProjects();
|
|
142
153
|
const project = projects.find((p) => p.id === id);
|
|
143
154
|
if (!project) {
|
|
144
155
|
return {
|
|
@@ -146,10 +157,17 @@ function createMcpServer() {
|
|
|
146
157
|
};
|
|
147
158
|
}
|
|
148
159
|
await startProject(id);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
160
|
+
// Retry up to 5 times (200ms intervals) to check if started
|
|
161
|
+
let running = "may still be starting";
|
|
162
|
+
for (let i = 0; i < 5; i++) {
|
|
163
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
164
|
+
const statuses = await loadStatuses();
|
|
165
|
+
const status = statuses.find((s) => s.id === id);
|
|
166
|
+
if (status?.running) {
|
|
167
|
+
running = "started";
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
153
171
|
return {
|
|
154
172
|
content: [
|
|
155
173
|
{
|
|
@@ -189,7 +207,8 @@ function createMcpServer() {
|
|
|
189
207
|
// Auto-detect port from command if not explicitly provided
|
|
190
208
|
const resolvedPort = port ?? detectPortFromCommand(command);
|
|
191
209
|
await runProcess(name, path, command, resolvedPort, "claude");
|
|
192
|
-
|
|
210
|
+
// Brief wait for DevGlow to register the process
|
|
211
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
193
212
|
const text = `AI process "${name}" started.\nCommand: ${command}\nPath: ${path}${resolvedPort ? `\nPort: ${resolvedPort}` : ""}`;
|
|
194
213
|
return { content: [{ type: "text", text }] };
|
|
195
214
|
});
|
|
@@ -220,7 +239,6 @@ function detectPortFromCommand(command) {
|
|
|
220
239
|
}
|
|
221
240
|
function checkPort(port) {
|
|
222
241
|
return new Promise((resolve) => {
|
|
223
|
-
const { createConnection } = require("net");
|
|
224
242
|
const socket = createConnection({ port, host: "127.0.0.1" });
|
|
225
243
|
socket.setTimeout(1000);
|
|
226
244
|
socket.on("connect", () => {
|
|
@@ -262,71 +280,80 @@ async function main() {
|
|
|
262
280
|
const sseTransports = new Map();
|
|
263
281
|
const streamableTransports = new Map();
|
|
264
282
|
const httpServer = createServer(async (req, res) => {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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));
|
|
283
|
+
try {
|
|
284
|
+
const url = new URL(req.url || "", `http://localhost:${port}`);
|
|
285
|
+
// CORS headers
|
|
286
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
287
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
288
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, mcp-session-id");
|
|
289
|
+
if (req.method === "OPTIONS") {
|
|
290
|
+
res.writeHead(204);
|
|
291
|
+
res.end();
|
|
281
292
|
return;
|
|
282
293
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
294
|
+
// --- Streamable HTTP: /mcp ---
|
|
295
|
+
if (url.pathname === "/mcp") {
|
|
296
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
297
|
+
if (sessionId && streamableTransports.has(sessionId)) {
|
|
298
|
+
const transport = streamableTransports.get(sessionId);
|
|
299
|
+
await transport.handleRequest(req, res, await parseBody(req));
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (!sessionId && req.method === "POST") {
|
|
303
|
+
const transport = new StreamableHTTPServerTransport({
|
|
304
|
+
sessionIdGenerator: () => randomUUID(),
|
|
305
|
+
onsessioninitialized: (sid) => {
|
|
306
|
+
streamableTransports.set(sid, transport);
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
transport.onclose = () => {
|
|
310
|
+
const sid = transport.sessionId;
|
|
311
|
+
if (sid)
|
|
312
|
+
streamableTransports.delete(sid);
|
|
313
|
+
};
|
|
314
|
+
const mcpServer = createMcpServer();
|
|
315
|
+
await mcpServer.connect(transport);
|
|
316
|
+
await transport.handleRequest(req, res, await parseBody(req));
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
320
|
+
res.end(JSON.stringify({ error: "Bad request" }));
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
// --- SSE (legacy): /sse + /messages ---
|
|
324
|
+
if (req.method === "GET" && url.pathname === "/sse") {
|
|
325
|
+
const sseTransport = new SSEServerTransport("/messages", res);
|
|
326
|
+
sseTransports.set(sseTransport.sessionId, sseTransport);
|
|
327
|
+
res.on("close", () => {
|
|
328
|
+
sseTransports.delete(sseTransport.sessionId);
|
|
289
329
|
});
|
|
290
|
-
transport.onclose = () => {
|
|
291
|
-
const sid = transport.sessionId;
|
|
292
|
-
if (sid)
|
|
293
|
-
streamableTransports.delete(sid);
|
|
294
|
-
};
|
|
295
330
|
const mcpServer = createMcpServer();
|
|
296
|
-
await mcpServer.connect(
|
|
297
|
-
await transport.handleRequest(req, res, await parseBody(req));
|
|
331
|
+
await mcpServer.connect(sseTransport);
|
|
298
332
|
return;
|
|
299
333
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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" }));
|
|
334
|
+
if (req.method === "POST" && url.pathname === "/messages") {
|
|
335
|
+
const sessionId = url.searchParams.get("sessionId");
|
|
336
|
+
const sseTransport = sessionId
|
|
337
|
+
? sseTransports.get(sessionId)
|
|
338
|
+
: undefined;
|
|
339
|
+
if (!sseTransport) {
|
|
340
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
341
|
+
res.end(JSON.stringify({ error: "No transport found for sessionId" }));
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
await sseTransport.handlePostMessage(req, res, await parseBody(req));
|
|
323
345
|
return;
|
|
324
346
|
}
|
|
325
|
-
|
|
326
|
-
|
|
347
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
348
|
+
res.end("Not found");
|
|
349
|
+
}
|
|
350
|
+
catch (err) {
|
|
351
|
+
if (!res.headersSent) {
|
|
352
|
+
const status = err instanceof Error && err.message === "Request body too large" ? 413 : 500;
|
|
353
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
354
|
+
res.end(JSON.stringify({ error: err instanceof Error ? err.message : "Internal server error" }));
|
|
355
|
+
}
|
|
327
356
|
}
|
|
328
|
-
res.writeHead(404);
|
|
329
|
-
res.end("Not found");
|
|
330
357
|
});
|
|
331
358
|
httpServer.listen(port, () => {
|
|
332
359
|
console.error(`devglow MCP server running on http://localhost:${port}\n` +
|
|
@@ -334,10 +361,18 @@ async function main() {
|
|
|
334
361
|
` SSE (legacy): http://localhost:${port}/sse`);
|
|
335
362
|
});
|
|
336
363
|
process.on("SIGINT", async () => {
|
|
337
|
-
for (const [, t] of sseTransports)
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
364
|
+
for (const [, t] of sseTransports) {
|
|
365
|
+
try {
|
|
366
|
+
await t.close();
|
|
367
|
+
}
|
|
368
|
+
catch { }
|
|
369
|
+
}
|
|
370
|
+
for (const [, t] of streamableTransports) {
|
|
371
|
+
try {
|
|
372
|
+
await t.close();
|
|
373
|
+
}
|
|
374
|
+
catch { }
|
|
375
|
+
}
|
|
341
376
|
httpServer.close();
|
|
342
377
|
process.exit(0);
|
|
343
378
|
});
|
|
@@ -350,10 +385,17 @@ async function main() {
|
|
|
350
385
|
console.error("devglow MCP server running on stdio");
|
|
351
386
|
}
|
|
352
387
|
}
|
|
388
|
+
const MAX_BODY_SIZE = 1024 * 1024; // 1MB
|
|
353
389
|
function parseBody(req) {
|
|
354
|
-
return new Promise((resolve) => {
|
|
390
|
+
return new Promise((resolve, reject) => {
|
|
355
391
|
let data = "";
|
|
356
|
-
req.on("data", (chunk) =>
|
|
392
|
+
req.on("data", (chunk) => {
|
|
393
|
+
data += chunk;
|
|
394
|
+
if (data.length > MAX_BODY_SIZE) {
|
|
395
|
+
req.destroy();
|
|
396
|
+
reject(new Error("Request body too large"));
|
|
397
|
+
}
|
|
398
|
+
});
|
|
357
399
|
req.on("end", () => {
|
|
358
400
|
try {
|
|
359
401
|
resolve(JSON.parse(data));
|
package/build/storage.js
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFile } from "fs/promises";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
const DATA_DIR = join(homedir(), "Library", "Application Support", "app.devglow.macos");
|
|
5
5
|
const TMP_DIR = join(DATA_DIR, "tmp");
|
|
6
6
|
// ── File readers ──
|
|
7
|
-
function readJsonFile(filename, fallback) {
|
|
7
|
+
async function readJsonFile(filename, fallback) {
|
|
8
8
|
try {
|
|
9
9
|
const filePath = join(DATA_DIR, filename);
|
|
10
|
-
const content =
|
|
10
|
+
const content = await readFile(filePath, "utf-8");
|
|
11
11
|
return JSON.parse(content);
|
|
12
12
|
}
|
|
13
13
|
catch {
|
|
14
14
|
return fallback;
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
-
export function loadProjects() {
|
|
18
|
-
const data = readJsonFile("projects.json", {
|
|
17
|
+
export async function loadProjects() {
|
|
18
|
+
const data = await readJsonFile("projects.json", {
|
|
19
19
|
projects: [],
|
|
20
20
|
});
|
|
21
21
|
return data.projects;
|
|
22
22
|
}
|
|
23
|
-
export function loadAiProcesses() {
|
|
24
|
-
const data = readJsonFile("ai_processes.json", {
|
|
23
|
+
export async function loadAiProcesses() {
|
|
24
|
+
const data = await readJsonFile("ai_processes.json", {
|
|
25
25
|
processes: [],
|
|
26
26
|
});
|
|
27
27
|
return data.processes;
|
|
28
28
|
}
|
|
29
|
-
export function loadStatuses() {
|
|
30
|
-
const data = readJsonFile("status.json", {
|
|
29
|
+
export async function loadStatuses() {
|
|
30
|
+
const data = await readJsonFile("status.json", {
|
|
31
31
|
statuses: [],
|
|
32
32
|
});
|
|
33
33
|
return data.statuses;
|
|
34
34
|
}
|
|
35
|
-
export function loadExportedLogs(projectId) {
|
|
35
|
+
export async function loadExportedLogs(projectId) {
|
|
36
36
|
try {
|
|
37
37
|
const filePath = join(TMP_DIR, `logs-${projectId}.json`);
|
|
38
|
-
const content =
|
|
38
|
+
const content = await readFile(filePath, "utf-8");
|
|
39
39
|
return JSON.parse(content);
|
|
40
40
|
}
|
|
41
41
|
catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devglow-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "MCP server for DevGlow — manage local dev processes through AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"dev": "tsc --watch"
|
|
15
15
|
},
|
|
16
16
|
"keywords": ["mcp", "devglow", "ai", "claude", "process-manager"],
|
|
17
|
+
"os": ["darwin"],
|
|
17
18
|
"license": "MIT",
|
|
18
19
|
"dependencies": {
|
|
19
20
|
"@modelcontextprotocol/sdk": "^1.26.0",
|