devglow-mcp 0.9.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 +363 -190
- package/build/storage.js +11 -11
- package/package.json +2 -1
package/build/index.js
CHANGED
|
@@ -1,206 +1,234 @@
|
|
|
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 { createConnection } from "node:net";
|
|
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";
|
|
4
12
|
import { z } from "zod";
|
|
5
13
|
import { loadProjects, loadAiProcesses, loadStatuses, loadExportedLogs, } from "./storage.js";
|
|
6
14
|
import { startProject, stopProject, runProcess, stopProcess, exportLogs, } from "./deeplink.js";
|
|
7
|
-
const
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
17
|
+
// --- Server factory ---
|
|
18
|
+
// Each transport connection gets its own McpServer instance
|
|
19
|
+
function createMcpServer() {
|
|
20
|
+
const server = new McpServer({
|
|
21
|
+
name: "devglow-mcp",
|
|
22
|
+
version: pkg.version,
|
|
23
|
+
}, {
|
|
24
|
+
instructions: [
|
|
25
|
+
"devglow is the user's local process manager (macOS menu bar app).",
|
|
26
|
+
"",
|
|
27
|
+
"## When starting a process",
|
|
28
|
+
"- Call `list_projects` first to check if the user already has a registered project.",
|
|
29
|
+
"- If found, use `start_project` (runs the user's registered command as-is).",
|
|
30
|
+
"- Only use `run_process` when no matching project exists.",
|
|
31
|
+
"",
|
|
32
|
+
"## Reading logs",
|
|
33
|
+
"- `get_logs` triggers a log export from DevGlow, waits briefly, then reads the file.",
|
|
34
|
+
"- If logs are empty, DevGlow may not have finished writing. Try again after a moment.",
|
|
35
|
+
].join("\n"),
|
|
36
|
+
});
|
|
37
|
+
// ── Tool: list_projects ──
|
|
38
|
+
server.registerTool("list_projects", {
|
|
39
|
+
title: "List all projects",
|
|
40
|
+
description: "List all registered user projects and AI temporary processes with their status.",
|
|
41
|
+
inputSchema: {},
|
|
42
|
+
}, async () => {
|
|
43
|
+
const projects = await loadProjects();
|
|
44
|
+
const aiProcesses = await loadAiProcesses();
|
|
45
|
+
const statuses = await loadStatuses();
|
|
46
|
+
const statusMap = new Map(statuses.map((s) => [s.id, s]));
|
|
47
|
+
let text = "";
|
|
48
|
+
if (projects.length > 0) {
|
|
49
|
+
text += "## Projects\n";
|
|
50
|
+
for (const p of projects) {
|
|
51
|
+
const status = statusMap.get(p.id);
|
|
52
|
+
const running = status?.running ? "running" : "stopped";
|
|
53
|
+
const pid = status?.pid ? ` (PID: ${status.pid})` : "";
|
|
54
|
+
const port = p.port ? ` :${p.port}` : "";
|
|
55
|
+
text += `- [${running}] ${p.name}${port}${pid} — ${p.command} (id: ${p.id})\n`;
|
|
56
|
+
}
|
|
43
57
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
if (aiProcesses.length > 0) {
|
|
59
|
+
text += "\n## AI Processes\n";
|
|
60
|
+
for (const p of aiProcesses) {
|
|
61
|
+
const status = statusMap.get(p.id);
|
|
62
|
+
const running = status?.running ? "running" : "stopped";
|
|
63
|
+
const pid = status?.pid ? ` (PID: ${status.pid})` : "";
|
|
64
|
+
const port = p.port ? ` :${p.port}` : "";
|
|
65
|
+
text += `- [${running}] ${p.name}${port}${pid} — ${p.command} [${p.source}] (id: ${p.id})\n`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (!text) {
|
|
69
|
+
text = "No projects or AI processes registered.";
|
|
70
|
+
}
|
|
71
|
+
return { content: [{ type: "text", text }] };
|
|
72
|
+
});
|
|
73
|
+
// ── Tool: get_status ──
|
|
74
|
+
server.registerTool("get_status", {
|
|
75
|
+
title: "Get project status",
|
|
76
|
+
description: "Get the running status of a specific project or AI process.",
|
|
77
|
+
inputSchema: {
|
|
78
|
+
id: z.string().describe("Project or AI process ID"),
|
|
79
|
+
},
|
|
80
|
+
}, async ({ id }) => {
|
|
81
|
+
const statuses = await loadStatuses();
|
|
82
|
+
const status = statuses.find((s) => s.id === id);
|
|
83
|
+
if (!status) {
|
|
84
|
+
return {
|
|
85
|
+
content: [{ type: "text", text: `No status found for ID: ${id}` }],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const text = `ID: ${status.id}\nRunning: ${status.running}\nPID: ${status.pid ?? "N/A"}\nSource: ${status.source}`;
|
|
89
|
+
return { content: [{ type: "text", text }] };
|
|
90
|
+
});
|
|
91
|
+
// ── Tool: get_logs ──
|
|
92
|
+
server.registerTool("get_logs", {
|
|
93
|
+
title: "Get process logs",
|
|
94
|
+
description: "Export and retrieve recent logs from a running or stopped process.",
|
|
95
|
+
inputSchema: {
|
|
96
|
+
id: z.string().describe("Project or AI process ID"),
|
|
97
|
+
lines: z
|
|
98
|
+
.number()
|
|
99
|
+
.optional()
|
|
100
|
+
.describe("Number of recent lines to return (default: 100)"),
|
|
101
|
+
},
|
|
102
|
+
}, async ({ id, lines }) => {
|
|
103
|
+
// Trigger log export via deep link
|
|
104
|
+
await exportLogs(id);
|
|
105
|
+
// Retry up to 5 times (100ms intervals) to read exported logs
|
|
106
|
+
const limit = lines ?? 100;
|
|
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
|
+
}
|
|
115
|
+
if (recent.length === 0) {
|
|
116
|
+
return {
|
|
117
|
+
content: [
|
|
118
|
+
{
|
|
119
|
+
type: "text",
|
|
120
|
+
text: `No logs found for ID: ${id}. The process may not have produced output yet.`,
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const text = recent
|
|
126
|
+
.map((l) => `[${l.timestamp}] [${l.level}] ${l.message}`)
|
|
127
|
+
.join("\n");
|
|
128
|
+
return { content: [{ type: "text", text }] };
|
|
129
|
+
});
|
|
130
|
+
// ── Tool: check_port ──
|
|
131
|
+
server.registerTool("check_port", {
|
|
132
|
+
title: "Check port availability",
|
|
133
|
+
description: "Check if a TCP port is in use on localhost.",
|
|
134
|
+
inputSchema: {
|
|
135
|
+
port: z.number().describe("Port number to check"),
|
|
136
|
+
},
|
|
137
|
+
}, async ({ port }) => {
|
|
138
|
+
const inUse = await checkPort(port);
|
|
139
|
+
const text = inUse
|
|
140
|
+
? `Port ${port} is in use.`
|
|
141
|
+
: `Port ${port} is available.`;
|
|
142
|
+
return { content: [{ type: "text", text }] };
|
|
143
|
+
});
|
|
144
|
+
// ── Tool: start_project ──
|
|
145
|
+
server.registerTool("start_project", {
|
|
146
|
+
title: "Start a registered project",
|
|
147
|
+
description: "Start a user-registered project using its configured command. Cannot modify the command.",
|
|
148
|
+
inputSchema: {
|
|
149
|
+
id: z.string().describe("Project ID (from list_projects)"),
|
|
150
|
+
},
|
|
151
|
+
}, async ({ id }) => {
|
|
152
|
+
const projects = await loadProjects();
|
|
153
|
+
const project = projects.find((p) => p.id === id);
|
|
154
|
+
if (!project) {
|
|
155
|
+
return {
|
|
156
|
+
content: [{ type: "text", text: `Project not found: ${id}` }],
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
await startProject(id);
|
|
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
|
+
}
|
|
53
170
|
}
|
|
54
|
-
}
|
|
55
|
-
if (!text) {
|
|
56
|
-
text = "No projects or AI processes registered.";
|
|
57
|
-
}
|
|
58
|
-
return { content: [{ type: "text", text }] };
|
|
59
|
-
});
|
|
60
|
-
// ── Tool: get_status ──
|
|
61
|
-
server.registerTool("get_status", {
|
|
62
|
-
title: "Get project status",
|
|
63
|
-
description: "Get the running status of a specific project or AI process.",
|
|
64
|
-
inputSchema: {
|
|
65
|
-
id: z.string().describe("Project or AI process ID"),
|
|
66
|
-
},
|
|
67
|
-
}, async ({ id }) => {
|
|
68
|
-
const statuses = loadStatuses();
|
|
69
|
-
const status = statuses.find((s) => s.id === id);
|
|
70
|
-
if (!status) {
|
|
71
|
-
return {
|
|
72
|
-
content: [{ type: "text", text: `No status found for ID: ${id}` }],
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
const text = `ID: ${status.id}\nRunning: ${status.running}\nPID: ${status.pid ?? "N/A"}\nSource: ${status.source}`;
|
|
76
|
-
return { content: [{ type: "text", text }] };
|
|
77
|
-
});
|
|
78
|
-
// ── Tool: get_logs ──
|
|
79
|
-
server.registerTool("get_logs", {
|
|
80
|
-
title: "Get process logs",
|
|
81
|
-
description: "Export and retrieve recent logs from a running or stopped process.",
|
|
82
|
-
inputSchema: {
|
|
83
|
-
id: z.string().describe("Project or AI process ID"),
|
|
84
|
-
lines: z
|
|
85
|
-
.number()
|
|
86
|
-
.optional()
|
|
87
|
-
.describe("Number of recent lines to return (default: 100)"),
|
|
88
|
-
},
|
|
89
|
-
}, async ({ id, lines }) => {
|
|
90
|
-
// Trigger log export via deep link
|
|
91
|
-
await exportLogs(id);
|
|
92
|
-
// Wait for DevGlow to write the file
|
|
93
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
94
|
-
const logs = loadExportedLogs(id);
|
|
95
|
-
const limit = lines ?? 100;
|
|
96
|
-
const recent = logs.slice(-limit);
|
|
97
|
-
if (recent.length === 0) {
|
|
98
171
|
return {
|
|
99
172
|
content: [
|
|
100
173
|
{
|
|
101
174
|
type: "text",
|
|
102
|
-
text: `
|
|
175
|
+
text: `Project "${project.name}" ${running}. Command: ${project.command}`,
|
|
103
176
|
},
|
|
104
177
|
],
|
|
105
178
|
};
|
|
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) {
|
|
179
|
+
});
|
|
180
|
+
// ── Tool: stop_project ──
|
|
181
|
+
server.registerTool("stop_project", {
|
|
182
|
+
title: "Stop a project",
|
|
183
|
+
description: "Stop a running project or AI process.",
|
|
184
|
+
inputSchema: {
|
|
185
|
+
id: z.string().describe("Project or AI process ID"),
|
|
186
|
+
},
|
|
187
|
+
}, async ({ id }) => {
|
|
188
|
+
await stopProject(id);
|
|
137
189
|
return {
|
|
138
|
-
content: [{ type: "text", text: `
|
|
190
|
+
content: [{ type: "text", text: `Stop signal sent for: ${id}` }],
|
|
139
191
|
};
|
|
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
|
-
|
|
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
|
-
});
|
|
192
|
+
});
|
|
193
|
+
// ── Tool: run_process ──
|
|
194
|
+
server.registerTool("run_process", {
|
|
195
|
+
title: "Run a new AI process",
|
|
196
|
+
description: "Create and start a new temporary process. Shows in DevGlow's AI Processes section. User can later [keep] or [dismiss] it.",
|
|
197
|
+
inputSchema: {
|
|
198
|
+
name: z.string().describe("Display name for the process"),
|
|
199
|
+
path: z.string().describe("Working directory (supports ~/)"),
|
|
200
|
+
command: z.string().describe("Shell command to execute"),
|
|
201
|
+
port: z
|
|
202
|
+
.number()
|
|
203
|
+
.optional()
|
|
204
|
+
.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."),
|
|
205
|
+
},
|
|
206
|
+
}, async ({ name, path, command, port }) => {
|
|
207
|
+
// Auto-detect port from command if not explicitly provided
|
|
208
|
+
const resolvedPort = port ?? detectPortFromCommand(command);
|
|
209
|
+
await runProcess(name, path, command, resolvedPort, "claude");
|
|
210
|
+
// Brief wait for DevGlow to register the process
|
|
211
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
212
|
+
const text = `AI process "${name}" started.\nCommand: ${command}\nPath: ${path}${resolvedPort ? `\nPort: ${resolvedPort}` : ""}`;
|
|
213
|
+
return { content: [{ type: "text", text }] };
|
|
214
|
+
});
|
|
215
|
+
// ── Tool: stop_process ──
|
|
216
|
+
server.registerTool("stop_process", {
|
|
217
|
+
title: "Stop an AI process",
|
|
218
|
+
description: "Stop a running AI temporary process by name.",
|
|
219
|
+
inputSchema: {
|
|
220
|
+
name: z.string().describe("AI process name"),
|
|
221
|
+
},
|
|
222
|
+
}, async ({ name }) => {
|
|
223
|
+
await stopProcess(name);
|
|
224
|
+
return {
|
|
225
|
+
content: [
|
|
226
|
+
{ type: "text", text: `Stop signal sent for AI process: "${name}"` },
|
|
227
|
+
],
|
|
228
|
+
};
|
|
229
|
+
});
|
|
230
|
+
return server;
|
|
231
|
+
}
|
|
204
232
|
// ── Utility ──
|
|
205
233
|
function detectPortFromCommand(command) {
|
|
206
234
|
// Match patterns: --port 3000, --port=3000, -p 3000, -p=3000
|
|
@@ -211,7 +239,6 @@ function detectPortFromCommand(command) {
|
|
|
211
239
|
}
|
|
212
240
|
function checkPort(port) {
|
|
213
241
|
return new Promise((resolve) => {
|
|
214
|
-
const { createConnection } = require("net");
|
|
215
242
|
const socket = createConnection({ port, host: "127.0.0.1" });
|
|
216
243
|
socket.setTimeout(1000);
|
|
217
244
|
socket.on("connect", () => {
|
|
@@ -227,11 +254,157 @@ function checkPort(port) {
|
|
|
227
254
|
});
|
|
228
255
|
});
|
|
229
256
|
}
|
|
257
|
+
// ── Args ──
|
|
258
|
+
function parseArgs() {
|
|
259
|
+
const args = process.argv.slice(2);
|
|
260
|
+
let transport = "stdio";
|
|
261
|
+
let port = 26215;
|
|
262
|
+
for (let i = 0; i < args.length; i++) {
|
|
263
|
+
if (args[i] === "--transport" && args[i + 1]) {
|
|
264
|
+
const value = args[i + 1];
|
|
265
|
+
// "sse" is an alias for "http" (backwards compat)
|
|
266
|
+
transport = value === "sse" || value === "http" ? "http" : "stdio";
|
|
267
|
+
i++;
|
|
268
|
+
}
|
|
269
|
+
else if (args[i] === "--port" && args[i + 1]) {
|
|
270
|
+
port = parseInt(args[i + 1], 10);
|
|
271
|
+
i++;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return { transport, port };
|
|
275
|
+
}
|
|
230
276
|
// ── Main ──
|
|
231
277
|
async function main() {
|
|
232
|
-
const transport =
|
|
233
|
-
|
|
234
|
-
|
|
278
|
+
const { transport: mode, port } = parseArgs();
|
|
279
|
+
if (mode === "http") {
|
|
280
|
+
const sseTransports = new Map();
|
|
281
|
+
const streamableTransports = new Map();
|
|
282
|
+
const httpServer = createServer(async (req, res) => {
|
|
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();
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
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);
|
|
329
|
+
});
|
|
330
|
+
const mcpServer = createMcpServer();
|
|
331
|
+
await mcpServer.connect(sseTransport);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
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));
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
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
|
+
}
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
httpServer.listen(port, () => {
|
|
359
|
+
console.error(`devglow MCP server running on http://localhost:${port}\n` +
|
|
360
|
+
` Streamable HTTP: http://localhost:${port}/mcp\n` +
|
|
361
|
+
` SSE (legacy): http://localhost:${port}/sse`);
|
|
362
|
+
});
|
|
363
|
+
process.on("SIGINT", async () => {
|
|
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
|
+
}
|
|
376
|
+
httpServer.close();
|
|
377
|
+
process.exit(0);
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
// stdio: single connection, single server
|
|
382
|
+
const mcpServer = createMcpServer();
|
|
383
|
+
const transport = new StdioServerTransport();
|
|
384
|
+
await mcpServer.connect(transport);
|
|
385
|
+
console.error("devglow MCP server running on stdio");
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const MAX_BODY_SIZE = 1024 * 1024; // 1MB
|
|
389
|
+
function parseBody(req) {
|
|
390
|
+
return new Promise((resolve, reject) => {
|
|
391
|
+
let data = "";
|
|
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
|
+
});
|
|
399
|
+
req.on("end", () => {
|
|
400
|
+
try {
|
|
401
|
+
resolve(JSON.parse(data));
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
resolve(undefined);
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
});
|
|
235
408
|
}
|
|
236
409
|
main().catch((error) => {
|
|
237
410
|
console.error("Fatal error:", error);
|
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": "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",
|