devglow-mcp 0.1.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.
@@ -0,0 +1,40 @@
1
+ import { execFile } from "child_process";
2
+ function openDeepLink(url) {
3
+ return new Promise((resolve, reject) => {
4
+ execFile("open", [url], (error) => {
5
+ if (error) {
6
+ reject(new Error(`Failed to open deep link: ${error.message}`));
7
+ }
8
+ else {
9
+ resolve();
10
+ }
11
+ });
12
+ });
13
+ }
14
+ function buildUrl(action, params) {
15
+ const query = Object.entries(params)
16
+ .filter(([, v]) => v !== undefined && v !== "")
17
+ .map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
18
+ .join("&");
19
+ return `devglow://${action}${query ? "?" + query : ""}`;
20
+ }
21
+ export async function startProject(id) {
22
+ await openDeepLink(buildUrl("start", { id }));
23
+ }
24
+ export async function stopProject(id) {
25
+ await openDeepLink(buildUrl("stop", { id }));
26
+ }
27
+ export async function runProcess(name, path, command, port, source) {
28
+ const params = { name, path, command };
29
+ if (port !== undefined)
30
+ params.port = String(port);
31
+ if (source)
32
+ params.source = source;
33
+ await openDeepLink(buildUrl("run", params));
34
+ }
35
+ export async function stopProcess(name) {
36
+ await openDeepLink(buildUrl("stop-process", { name }));
37
+ }
38
+ export async function exportLogs(id) {
39
+ await openDeepLink(buildUrl("export-logs", { id }));
40
+ }
package/build/index.js ADDED
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { loadProjects, loadAiProcesses, loadStatuses, loadExportedLogs, } from "./storage.js";
6
+ import { startProject, stopProject, runProcess, stopProcess, exportLogs, } from "./deeplink.js";
7
+ const server = new McpServer({
8
+ name: "devglow-mcp",
9
+ version: "0.1.0",
10
+ }, {
11
+ instructions: [
12
+ "devglow is the user's local process manager (macOS menu bar app).",
13
+ "",
14
+ "## When starting a process",
15
+ "- Call `list_projects` first to check if the user already has a registered project.",
16
+ "- If found, use `start_project` (runs the user's registered command as-is).",
17
+ "- Only use `run_process` when no matching project exists.",
18
+ "",
19
+ "## Reading logs",
20
+ "- `get_logs` triggers a log export from DevGlow, waits briefly, then reads the file.",
21
+ "- If logs are empty, DevGlow may not have finished writing. Try again after a moment.",
22
+ ].join("\n"),
23
+ });
24
+ // ── Tool: list_projects ──
25
+ server.registerTool("list_projects", {
26
+ title: "List all projects",
27
+ description: "List all registered user projects and AI temporary processes with their status.",
28
+ inputSchema: {},
29
+ }, async () => {
30
+ const projects = loadProjects();
31
+ const aiProcesses = loadAiProcesses();
32
+ const statuses = loadStatuses();
33
+ const statusMap = new Map(statuses.map((s) => [s.id, s]));
34
+ let text = "";
35
+ if (projects.length > 0) {
36
+ text += "## Projects\n";
37
+ for (const p of projects) {
38
+ const status = statusMap.get(p.id);
39
+ const running = status?.running ? "running" : "stopped";
40
+ const pid = status?.pid ? ` (PID: ${status.pid})` : "";
41
+ const port = p.port ? ` :${p.port}` : "";
42
+ text += `- [${running}] ${p.name}${port}${pid} — ${p.command} (id: ${p.id})\n`;
43
+ }
44
+ }
45
+ if (aiProcesses.length > 0) {
46
+ text += "\n## AI Processes\n";
47
+ for (const p of aiProcesses) {
48
+ const status = statusMap.get(p.id);
49
+ const running = status?.running ? "running" : "stopped";
50
+ const pid = status?.pid ? ` (PID: ${status.pid})` : "";
51
+ const port = p.port ? ` :${p.port}` : "";
52
+ text += `- [${running}] ${p.name}${port}${pid} — ${p.command} [${p.source}] (id: ${p.id})\n`;
53
+ }
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
+ return {
99
+ content: [
100
+ {
101
+ type: "text",
102
+ text: `No logs found for ID: ${id}. The process may not have produced output yet.`,
103
+ },
104
+ ],
105
+ };
106
+ }
107
+ const text = recent
108
+ .map((l) => `[${l.timestamp}] [${l.level}] ${l.message}`)
109
+ .join("\n");
110
+ return { content: [{ type: "text", text }] };
111
+ });
112
+ // ── Tool: check_port ──
113
+ server.registerTool("check_port", {
114
+ title: "Check port availability",
115
+ description: "Check if a TCP port is in use on localhost.",
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) {
137
+ return {
138
+ content: [{ type: "text", text: `Project not found: ${id}` }],
139
+ };
140
+ }
141
+ await startProject(id);
142
+ await new Promise((resolve) => setTimeout(resolve, 1000));
143
+ const statuses = loadStatuses();
144
+ const status = statuses.find((s) => s.id === id);
145
+ const running = status?.running ? "started" : "may still be starting";
146
+ return {
147
+ content: [
148
+ {
149
+ type: "text",
150
+ text: `Project "${project.name}" ${running}. Command: ${project.command}`,
151
+ },
152
+ ],
153
+ };
154
+ });
155
+ // ── Tool: stop_project ──
156
+ server.registerTool("stop_project", {
157
+ title: "Stop a project",
158
+ description: "Stop a running project or AI process.",
159
+ inputSchema: {
160
+ id: z.string().describe("Project or AI process ID"),
161
+ },
162
+ }, async ({ id }) => {
163
+ await stopProject(id);
164
+ return {
165
+ content: [{ type: "text", text: `Stop signal sent for: ${id}` }],
166
+ };
167
+ });
168
+ // ── Tool: run_process ──
169
+ server.registerTool("run_process", {
170
+ title: "Run a new AI process",
171
+ description: "Create and start a new temporary process. Shows in DevGlow's AI Processes section. User can later [keep] or [dismiss] it.",
172
+ inputSchema: {
173
+ name: z.string().describe("Display name for the process"),
174
+ path: z.string().describe("Working directory (supports ~/)"),
175
+ command: z.string().describe("Shell command to execute"),
176
+ port: z.number().optional().describe("Port number (optional)"),
177
+ },
178
+ }, async ({ name, path, command, port }) => {
179
+ await runProcess(name, path, command, port, "claude");
180
+ await new Promise((resolve) => setTimeout(resolve, 1000));
181
+ const text = `AI process "${name}" started.\nCommand: ${command}\nPath: ${path}${port ? `\nPort: ${port}` : ""}`;
182
+ return { content: [{ type: "text", text }] };
183
+ });
184
+ // ── Tool: stop_process ──
185
+ server.registerTool("stop_process", {
186
+ title: "Stop an AI process",
187
+ description: "Stop a running AI temporary process by name.",
188
+ inputSchema: {
189
+ name: z.string().describe("AI process name"),
190
+ },
191
+ }, async ({ name }) => {
192
+ await stopProcess(name);
193
+ return {
194
+ content: [
195
+ { type: "text", text: `Stop signal sent for AI process: "${name}"` },
196
+ ],
197
+ };
198
+ });
199
+ // ── Utility ──
200
+ function checkPort(port) {
201
+ return new Promise((resolve) => {
202
+ const { createConnection } = require("net");
203
+ const socket = createConnection({ port, host: "127.0.0.1" });
204
+ socket.setTimeout(1000);
205
+ socket.on("connect", () => {
206
+ socket.destroy();
207
+ resolve(true);
208
+ });
209
+ socket.on("timeout", () => {
210
+ socket.destroy();
211
+ resolve(false);
212
+ });
213
+ socket.on("error", () => {
214
+ resolve(false);
215
+ });
216
+ });
217
+ }
218
+ // ── Main ──
219
+ async function main() {
220
+ const transport = new StdioServerTransport();
221
+ await server.connect(transport);
222
+ console.error("devglow MCP server running on stdio");
223
+ }
224
+ main().catch((error) => {
225
+ console.error("Fatal error:", error);
226
+ process.exit(1);
227
+ });
@@ -0,0 +1,44 @@
1
+ import { readFileSync } from "fs";
2
+ import { join } from "path";
3
+ import { homedir } from "os";
4
+ const DATA_DIR = join(homedir(), "Library", "Application Support", "app.devglow.macos");
5
+ const TMP_DIR = join(DATA_DIR, "tmp");
6
+ // ── File readers ──
7
+ function readJsonFile(filename, fallback) {
8
+ try {
9
+ const filePath = join(DATA_DIR, filename);
10
+ const content = readFileSync(filePath, "utf-8");
11
+ return JSON.parse(content);
12
+ }
13
+ catch {
14
+ return fallback;
15
+ }
16
+ }
17
+ export function loadProjects() {
18
+ const data = readJsonFile("projects.json", {
19
+ projects: [],
20
+ });
21
+ return data.projects;
22
+ }
23
+ export function loadAiProcesses() {
24
+ const data = readJsonFile("ai_processes.json", {
25
+ processes: [],
26
+ });
27
+ return data.processes;
28
+ }
29
+ export function loadStatuses() {
30
+ const data = readJsonFile("status.json", {
31
+ statuses: [],
32
+ });
33
+ return data.statuses;
34
+ }
35
+ export function loadExportedLogs(projectId) {
36
+ try {
37
+ const filePath = join(TMP_DIR, `logs-${projectId}.json`);
38
+ const content = readFileSync(filePath, "utf-8");
39
+ return JSON.parse(content);
40
+ }
41
+ catch {
42
+ return [];
43
+ }
44
+ }
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "devglow-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for DevGlow — manage local dev processes through AI agents",
5
+ "type": "module",
6
+ "bin": {
7
+ "devglow-mcp": "build/index.js"
8
+ },
9
+ "files": [
10
+ "build"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc && chmod 755 build/index.js",
14
+ "dev": "tsc --watch"
15
+ },
16
+ "keywords": ["mcp", "devglow", "ai", "claude", "process-manager"],
17
+ "license": "MIT",
18
+ "dependencies": {
19
+ "@modelcontextprotocol/sdk": "^1.26.0",
20
+ "zod": "^3.25.0"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^22.19.2",
24
+ "typescript": "^5.9.3"
25
+ }
26
+ }