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 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: "1.0.0",
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
- // Wait for DevGlow to write the file
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
- const recent = logs.slice(-limit);
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
- 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";
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
- await new Promise((resolve) => setTimeout(resolve, 1000));
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
- 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));
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
- if (!sessionId && req.method === "POST") {
284
- const transport = new StreamableHTTPServerTransport({
285
- sessionIdGenerator: () => randomUUID(),
286
- onsessioninitialized: (sid) => {
287
- streamableTransports.set(sid, transport);
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(transport);
297
- await transport.handleRequest(req, res, await parseBody(req));
331
+ await mcpServer.connect(sseTransport);
298
332
  return;
299
333
  }
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" }));
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
- await sseTransport.handlePostMessage(req, res, await parseBody(req));
326
- return;
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
- await t.close();
339
- for (const [, t] of streamableTransports)
340
- await t.close();
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) => (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 { readFileSync } from "fs";
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 = readFileSync(filePath, "utf-8");
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 = readFileSync(filePath, "utf-8");
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.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",