devglow-mcp 1.0.1 → 1.0.4
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/deeplink.js +9 -1
- package/build/index.js +67 -5
- package/package.json +1 -1
package/build/deeplink.js
CHANGED
|
@@ -24,12 +24,14 @@ export async function startProject(id) {
|
|
|
24
24
|
export async function stopProject(id) {
|
|
25
25
|
await openDeepLink(buildUrl("stop", { id }));
|
|
26
26
|
}
|
|
27
|
-
export async function runProcess(name, path, command, port, source) {
|
|
27
|
+
export async function runProcess(name, path, command, port, source, description) {
|
|
28
28
|
const params = { name, path, command };
|
|
29
29
|
if (port !== undefined)
|
|
30
30
|
params.port = String(port);
|
|
31
31
|
if (source)
|
|
32
32
|
params.source = source;
|
|
33
|
+
if (description)
|
|
34
|
+
params.description = description;
|
|
33
35
|
await openDeepLink(buildUrl("run", params));
|
|
34
36
|
}
|
|
35
37
|
export async function stopProcess(name) {
|
|
@@ -38,3 +40,9 @@ export async function stopProcess(name) {
|
|
|
38
40
|
export async function exportLogs(id) {
|
|
39
41
|
await openDeepLink(buildUrl("export-logs", { id }));
|
|
40
42
|
}
|
|
43
|
+
export async function updateProject(id, description) {
|
|
44
|
+
const params = { id };
|
|
45
|
+
if (description !== undefined)
|
|
46
|
+
params.description = description;
|
|
47
|
+
await openDeepLink(buildUrl("update-project", params));
|
|
48
|
+
}
|
package/build/index.js
CHANGED
|
@@ -11,9 +11,12 @@ import { fileURLToPath } from "node:url";
|
|
|
11
11
|
import { dirname, join } from "node:path";
|
|
12
12
|
import { z } from "zod";
|
|
13
13
|
import { loadProjects, loadAiProcesses, loadStatuses, loadExportedLogs, } from "./storage.js";
|
|
14
|
-
import { startProject, stopProject, runProcess, stopProcess, exportLogs, } from "./deeplink.js";
|
|
14
|
+
import { startProject, stopProject, runProcess, stopProcess, exportLogs, updateProject, } from "./deeplink.js";
|
|
15
15
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
16
|
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
17
|
+
// --- Orphan process prevention constants ---
|
|
18
|
+
const IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
19
|
+
const IDLE_CHECK_INTERVAL_MS = 60 * 1000; // Check every 1 minute
|
|
17
20
|
// --- Server factory ---
|
|
18
21
|
// Each transport connection gets its own McpServer instance
|
|
19
22
|
function createMcpServer() {
|
|
@@ -195,21 +198,47 @@ function createMcpServer() {
|
|
|
195
198
|
title: "Run a new AI process",
|
|
196
199
|
description: "Create and start a new temporary process. Shows in DevGlow's AI Processes section. User can later [keep] or [dismiss] it.",
|
|
197
200
|
inputSchema: {
|
|
198
|
-
name: z.string().describe("Display name for the process"),
|
|
201
|
+
name: z.string().describe("Display name for the process. Do NOT include port number in the name — use the port parameter instead."),
|
|
199
202
|
path: z.string().describe("Working directory (supports ~/)"),
|
|
200
203
|
command: z.string().describe("Shell command to execute"),
|
|
201
204
|
port: z
|
|
202
205
|
.number()
|
|
203
206
|
.optional()
|
|
204
207
|
.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."),
|
|
208
|
+
description: z
|
|
209
|
+
.string()
|
|
210
|
+
.max(280)
|
|
211
|
+
.optional()
|
|
212
|
+
.describe("Short summary of what the app does (max 280 chars). Helps the user remember the project later. Write 1-2 plain sentences from the user's perspective."),
|
|
205
213
|
},
|
|
206
|
-
}, async ({ name, path, command, port }) => {
|
|
214
|
+
}, async ({ name, path, command, port, description }) => {
|
|
207
215
|
// Auto-detect port from command if not explicitly provided
|
|
208
216
|
const resolvedPort = port ?? detectPortFromCommand(command);
|
|
209
|
-
await runProcess(name, path, command, resolvedPort, "claude");
|
|
217
|
+
await runProcess(name, path, command, resolvedPort, "claude", description);
|
|
210
218
|
// Brief wait for DevGlow to register the process
|
|
211
219
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
212
|
-
const text = `AI process "${name}" started.\nCommand: ${command}\nPath: ${path}${resolvedPort ? `\nPort: ${resolvedPort}` : ""}`;
|
|
220
|
+
const text = `AI process "${name}" started.\nCommand: ${command}\nPath: ${path}${resolvedPort ? `\nPort: ${resolvedPort}` : ""}${description ? `\nDescription: ${description}` : ""}`;
|
|
221
|
+
return { content: [{ type: "text", text }] };
|
|
222
|
+
});
|
|
223
|
+
// ── Tool: update_project ──
|
|
224
|
+
server.registerTool("update_project", {
|
|
225
|
+
title: "Update a project's metadata",
|
|
226
|
+
description: "Update an existing DevGlow project's metadata (currently only description). Use this to fill in or refresh a project's description after registering it.",
|
|
227
|
+
inputSchema: {
|
|
228
|
+
id: z
|
|
229
|
+
.string()
|
|
230
|
+
.describe("Project id (call list_projects first to get the id)"),
|
|
231
|
+
description: z
|
|
232
|
+
.string()
|
|
233
|
+
.max(280)
|
|
234
|
+
.describe("Short summary of what the app does (max 280 chars). Pass an empty string to clear the existing description."),
|
|
235
|
+
},
|
|
236
|
+
}, async ({ id, description }) => {
|
|
237
|
+
await updateProject(id, description);
|
|
238
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
239
|
+
const text = description
|
|
240
|
+
? `Project ${id} description updated.`
|
|
241
|
+
: `Project ${id} description cleared.`;
|
|
213
242
|
return { content: [{ type: "text", text }] };
|
|
214
243
|
});
|
|
215
244
|
// ── Tool: stop_process ──
|
|
@@ -383,6 +412,39 @@ async function main() {
|
|
|
383
412
|
const transport = new StdioServerTransport();
|
|
384
413
|
await mcpServer.connect(transport);
|
|
385
414
|
console.error("devglow MCP server running on stdio");
|
|
415
|
+
// --- Orphan process prevention (stdio mode) ---
|
|
416
|
+
// When Claude Code exits abnormally, the npx → npm → node chain
|
|
417
|
+
// may not propagate stdin pipe close properly, leaving this process orphaned.
|
|
418
|
+
let lastActivityTime = Date.now();
|
|
419
|
+
let exiting = false;
|
|
420
|
+
let idleTimer;
|
|
421
|
+
function gracefulExit(reason) {
|
|
422
|
+
if (exiting)
|
|
423
|
+
return;
|
|
424
|
+
exiting = true;
|
|
425
|
+
console.error(`devglow-mcp: ${reason}, shutting down`);
|
|
426
|
+
clearInterval(idleTimer);
|
|
427
|
+
try {
|
|
428
|
+
mcpServer.close();
|
|
429
|
+
}
|
|
430
|
+
catch { }
|
|
431
|
+
process.exit(0);
|
|
432
|
+
}
|
|
433
|
+
// Track incoming messages for idle detection
|
|
434
|
+
process.stdin.on("data", () => {
|
|
435
|
+
lastActivityTime = Date.now();
|
|
436
|
+
});
|
|
437
|
+
// 1st defense: detect stdin pipe close (parent process gone)
|
|
438
|
+
process.stdin.resume();
|
|
439
|
+
process.stdin.on("end", () => gracefulExit("stdin ended"));
|
|
440
|
+
process.stdin.on("close", () => gracefulExit("stdin closed"));
|
|
441
|
+
// 2nd defense: idle timeout (no messages for 30 min)
|
|
442
|
+
idleTimer = setInterval(() => {
|
|
443
|
+
if (Date.now() - lastActivityTime >= IDLE_TIMEOUT_MS) {
|
|
444
|
+
gracefulExit(`idle timeout (${IDLE_TIMEOUT_MS / 60_000}m)`);
|
|
445
|
+
}
|
|
446
|
+
}, IDLE_CHECK_INTERVAL_MS);
|
|
447
|
+
idleTimer.unref();
|
|
386
448
|
}
|
|
387
449
|
}
|
|
388
450
|
const MAX_BODY_SIZE = 1024 * 1024; // 1MB
|