@ebowwa/workspace-mcp 1.0.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.
package/dist/index.js ADDED
@@ -0,0 +1,658 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js";
5
+ import { exec } from "child_process";
6
+ import { promisify } from "util";
7
+ import { readdir, readFile, stat } from "fs/promises";
8
+ import { join } from "path";
9
+ import * as psList from "ps-list";
10
+ const execAsync = promisify(exec);
11
+ class WorkspaceMCPServer {
12
+ server;
13
+ constructor() {
14
+ this.server = new Server({
15
+ name: "workspace-mcp",
16
+ version: "1.0.0",
17
+ }, {
18
+ capabilities: {
19
+ tools: {},
20
+ },
21
+ });
22
+ this.setupTools();
23
+ this.setupErrorHandling();
24
+ }
25
+ setupErrorHandling() {
26
+ this.server.onerror = (error) => console.error("[MCP Error]", error);
27
+ process.on("SIGINT", async () => {
28
+ await this.server.close();
29
+ process.exit(0);
30
+ });
31
+ }
32
+ setupTools() {
33
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
34
+ tools: [
35
+ {
36
+ name: "workspace_dirs_list",
37
+ description: "List all directories in the workspace root",
38
+ inputSchema: {
39
+ type: "object",
40
+ properties: {
41
+ includeProcesses: {
42
+ type: "boolean",
43
+ description: "Include running processes for each directory",
44
+ default: false,
45
+ },
46
+ },
47
+ },
48
+ },
49
+ {
50
+ name: "workspace_dirs_get_info",
51
+ description: "Get detailed information about a specific workspace directory",
52
+ inputSchema: {
53
+ type: "object",
54
+ properties: {
55
+ dirPath: {
56
+ type: "string",
57
+ description: "Path to the directory",
58
+ },
59
+ },
60
+ required: ["dirPath"],
61
+ },
62
+ },
63
+ {
64
+ name: "workspace_dirs_run_command",
65
+ description: "Run a command in a specific workspace directory",
66
+ inputSchema: {
67
+ type: "object",
68
+ properties: {
69
+ dirPath: {
70
+ type: "string",
71
+ description: "Path to the directory",
72
+ },
73
+ command: {
74
+ type: "string",
75
+ description: "Command to run (e.g., 'npm install', 'npm run dev')",
76
+ },
77
+ timeout: {
78
+ type: "number",
79
+ description: "Timeout in milliseconds (default: 30000)",
80
+ default: 30000,
81
+ },
82
+ },
83
+ required: ["dirPath", "command"],
84
+ },
85
+ },
86
+ {
87
+ name: "workspace_services_list",
88
+ description: "List all running development services and their status",
89
+ inputSchema: {
90
+ type: "object",
91
+ properties: {
92
+ filter: {
93
+ type: "string",
94
+ description: "Filter services by name or type",
95
+ },
96
+ },
97
+ },
98
+ },
99
+ {
100
+ name: "workspace_services_manage",
101
+ description: "Start, stop, or restart a development service",
102
+ inputSchema: {
103
+ type: "object",
104
+ properties: {
105
+ serviceName: {
106
+ type: "string",
107
+ description: "Name of the service to manage",
108
+ },
109
+ action: {
110
+ type: "string",
111
+ enum: ["start", "stop", "restart", "status"],
112
+ description: "Action to perform on the service",
113
+ },
114
+ dirPath: {
115
+ type: "string",
116
+ description: "Path to the directory (if applicable)",
117
+ },
118
+ },
119
+ required: ["serviceName", "action"],
120
+ },
121
+ },
122
+ {
123
+ name: "workspace_files_read",
124
+ description: "Read a file from a workspace directory",
125
+ inputSchema: {
126
+ type: "object",
127
+ properties: {
128
+ filePath: {
129
+ type: "string",
130
+ description: "Path to the file to read",
131
+ },
132
+ encoding: {
133
+ type: "string",
134
+ description: "File encoding (default: utf8)",
135
+ default: "utf8",
136
+ },
137
+ },
138
+ required: ["filePath"],
139
+ },
140
+ },
141
+ {
142
+ name: "check_integrations",
143
+ description: "Check the health of all integrations (GitHub CLI, Doppler, Node, etc.)",
144
+ inputSchema: {
145
+ type: "object",
146
+ properties: {
147
+ verbose: {
148
+ type: "boolean",
149
+ description: "Include detailed diagnostic information",
150
+ default: false,
151
+ },
152
+ },
153
+ },
154
+ },
155
+ ],
156
+ }));
157
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
158
+ const { name, arguments: args } = request.params;
159
+ try {
160
+ switch (name) {
161
+ case "workspace_dirs_list":
162
+ return await this.listProjects(args?.includeProcesses);
163
+ case "workspace_dirs_get_info":
164
+ return await this.getProjectInfo(args?.dirPath);
165
+ case "workspace_dirs_run_command":
166
+ return await this.runProjectCommand(args?.dirPath, args?.command, args?.timeout);
167
+ case "workspace_services_list":
168
+ return await this.listServices(args?.filter);
169
+ case "workspace_services_manage":
170
+ return await this.manageService(args?.serviceName, args?.action, args?.dirPath);
171
+ case "workspace_files_read":
172
+ return await this.readProjectFile(args?.filePath, args?.encoding);
173
+ case "check_integrations":
174
+ return await this.checkIntegrations(args?.verbose);
175
+ default:
176
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
177
+ }
178
+ }
179
+ catch (error) {
180
+ throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`);
181
+ }
182
+ });
183
+ }
184
+ async listProjects(includeProcesses = false) {
185
+ try {
186
+ const rootDir = "/root";
187
+ const entries = await readdir(rootDir, { withFileTypes: true });
188
+ const projects = [];
189
+ for (const entry of entries) {
190
+ if (!entry.isDirectory())
191
+ continue;
192
+ const projectPath = join(rootDir, entry.name);
193
+ const projectStat = await stat(projectPath);
194
+ // Skip hidden directories and common system dirs
195
+ if (entry.name.startsWith('.') ||
196
+ ['node_modules', '.git', '.npm', '.cache', '.local'].includes(entry.name)) {
197
+ continue;
198
+ }
199
+ const hasPackageJson = await this.checkFileExists(join(projectPath, "package.json"));
200
+ const isGitRepo = await this.checkFileExists(join(projectPath, ".git"));
201
+ let projectType = "unknown";
202
+ if (hasPackageJson)
203
+ projectType = "node";
204
+ else if (await this.checkFileExists(join(projectPath, "go.mod")))
205
+ projectType = "go";
206
+ else if (await this.checkFileExists(join(projectPath, "requirements.txt")) ||
207
+ await this.checkFileExists(join(projectPath, "pyproject.toml")))
208
+ projectType = "python";
209
+ else if (entry.name.includes("mcp-"))
210
+ projectType = "mcp-server";
211
+ const project = {
212
+ name: entry.name,
213
+ path: projectPath,
214
+ type: projectType,
215
+ lastModified: projectStat.mtime,
216
+ hasPackageJson,
217
+ };
218
+ if (includeProcesses) {
219
+ project.processes = await this.getProjectProcesses(projectPath);
220
+ }
221
+ projects.push(project);
222
+ }
223
+ return {
224
+ content: [
225
+ {
226
+ type: "text",
227
+ text: JSON.stringify({
228
+ projects: projects.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime()),
229
+ count: projects.length,
230
+ }, null, 2),
231
+ },
232
+ ],
233
+ };
234
+ }
235
+ catch (error) {
236
+ throw new Error(`Failed to list projects: ${error}`);
237
+ }
238
+ }
239
+ async getProjectInfo(dirPath) {
240
+ try {
241
+ const projectStat = await stat(dirPath);
242
+ const hasPackageJson = await this.checkFileExists(join(dirPath, "package.json"));
243
+ const isGitRepo = await this.checkFileExists(join(dirPath, ".git"));
244
+ let packageInfo = null;
245
+ if (hasPackageJson) {
246
+ try {
247
+ const packageContent = await readFile(join(dirPath, "package.json"), "utf8");
248
+ packageInfo = JSON.parse(packageContent);
249
+ }
250
+ catch (e) {
251
+ // Ignore package.json read errors
252
+ }
253
+ }
254
+ let gitStatus = null;
255
+ if (isGitRepo) {
256
+ try {
257
+ const { stdout } = await execAsync(`cd "${dirPath}" && git status --porcelain`);
258
+ gitStatus = {
259
+ clean: stdout.trim() === "",
260
+ changes: stdout.split('\n').filter(line => line.trim()).length,
261
+ };
262
+ }
263
+ catch (e) {
264
+ gitStatus = { error: "Failed to get git status" };
265
+ }
266
+ }
267
+ const processes = await this.getProjectProcesses(dirPath);
268
+ return {
269
+ content: [
270
+ {
271
+ type: "text",
272
+ text: JSON.stringify({
273
+ path: dirPath,
274
+ lastModified: projectStat.mtime,
275
+ hasPackageJson,
276
+ isGitRepo,
277
+ packageInfo,
278
+ gitStatus,
279
+ processes,
280
+ }, null, 2),
281
+ },
282
+ ],
283
+ };
284
+ }
285
+ catch (error) {
286
+ throw new Error(`Failed to get project info: ${error}`);
287
+ }
288
+ }
289
+ async runProjectCommand(dirPath, command, timeout = 30000) {
290
+ try {
291
+ const { stdout, stderr } = await execAsync(`cd "${dirPath}" && ${command}`, {
292
+ timeout,
293
+ maxBuffer: 1024 * 1024, // 1MB buffer
294
+ });
295
+ return {
296
+ content: [
297
+ {
298
+ type: "text",
299
+ text: JSON.stringify({
300
+ command,
301
+ dirPath,
302
+ stdout,
303
+ stderr,
304
+ success: true,
305
+ }, null, 2),
306
+ },
307
+ ],
308
+ };
309
+ }
310
+ catch (error) {
311
+ return {
312
+ content: [
313
+ {
314
+ type: "text",
315
+ text: JSON.stringify({
316
+ command,
317
+ dirPath,
318
+ error: error.message,
319
+ stdout: error.stdout || "",
320
+ stderr: error.stderr || "",
321
+ success: false,
322
+ }, null, 2),
323
+ },
324
+ ],
325
+ };
326
+ }
327
+ }
328
+ async listServices(filter) {
329
+ try {
330
+ const processes = await psList.default();
331
+ const services = [];
332
+ const servicePatterns = [
333
+ { name: "node", pattern: /node/ },
334
+ { name: "npm", pattern: /npm/ },
335
+ { name: "python", pattern: /python/ },
336
+ { name: "uvicorn", pattern: /uvicorn/ },
337
+ { name: "mcp-server", pattern: /mcp.*server/ },
338
+ { name: "next", pattern: /next/ },
339
+ { name: "vercel", pattern: /vercel/ },
340
+ ];
341
+ for (const process of processes) {
342
+ if (!process.cmd)
343
+ continue;
344
+ for (const service of servicePatterns) {
345
+ if (service.pattern.test(process.cmd)) {
346
+ const serviceInfo = {
347
+ name: service.name,
348
+ status: "running",
349
+ pid: process.pid,
350
+ cpu: process.cpu,
351
+ memory: process.memory,
352
+ command: process.cmd,
353
+ };
354
+ if (!filter || service.name.includes(filter) || process.cmd.includes(filter)) {
355
+ services.push(serviceInfo);
356
+ }
357
+ break;
358
+ }
359
+ }
360
+ }
361
+ return {
362
+ content: [
363
+ {
364
+ type: "text",
365
+ text: JSON.stringify({
366
+ services,
367
+ count: services.length,
368
+ timestamp: new Date().toISOString(),
369
+ }, null, 2),
370
+ },
371
+ ],
372
+ };
373
+ }
374
+ catch (error) {
375
+ throw new Error(`Failed to list services: ${error}`);
376
+ }
377
+ }
378
+ async manageService(serviceName, action, dirPath) {
379
+ try {
380
+ let command = "";
381
+ const cwd = dirPath || "/root";
382
+ switch (action) {
383
+ case "start":
384
+ if (serviceName.includes("mcp")) {
385
+ command = `cd "${cwd}" && npm start`;
386
+ }
387
+ else if (serviceName === "next") {
388
+ command = `cd "${cwd}" && npm run dev`;
389
+ }
390
+ else {
391
+ throw new Error(`Don't know how to start service: ${serviceName}`);
392
+ }
393
+ break;
394
+ case "stop":
395
+ // Find and kill the process
396
+ const { stdout } = await execAsync(`ps aux | grep "${serviceName}" | grep -v grep | awk '{print $2}'`);
397
+ if (stdout.trim()) {
398
+ const pids = stdout.trim().split('\n');
399
+ for (const pid of pids) {
400
+ await execAsync(`kill ${pid}`);
401
+ }
402
+ }
403
+ return {
404
+ content: [
405
+ {
406
+ type: "text",
407
+ text: JSON.stringify({
408
+ action: "stopped",
409
+ serviceName,
410
+ pids: stdout.trim().split('\n'),
411
+ success: true,
412
+ }, null, 2),
413
+ },
414
+ ],
415
+ };
416
+ case "restart":
417
+ // Stop then start
418
+ await this.manageService(serviceName, "stop", dirPath);
419
+ await new Promise(resolve => setTimeout(resolve, 2000));
420
+ return await this.manageService(serviceName, "start", dirPath);
421
+ case "status":
422
+ const services = await this.listServices(serviceName);
423
+ return services;
424
+ default:
425
+ throw new Error(`Unknown action: ${action}`);
426
+ }
427
+ if (command) {
428
+ const { stdout, stderr } = await execAsync(command);
429
+ return {
430
+ content: [
431
+ {
432
+ type: "text",
433
+ text: JSON.stringify({
434
+ action,
435
+ serviceName,
436
+ dirPath: cwd,
437
+ command,
438
+ stdout,
439
+ stderr,
440
+ success: true,
441
+ }, null, 2),
442
+ },
443
+ ],
444
+ };
445
+ }
446
+ }
447
+ catch (error) {
448
+ throw new Error(`Failed to manage service ${serviceName}: ${error}`);
449
+ }
450
+ }
451
+ async readProjectFile(filePath, encoding = "utf8") {
452
+ try {
453
+ const content = await readFile(filePath, encoding);
454
+ const stats = await stat(filePath);
455
+ return {
456
+ content: [
457
+ {
458
+ type: "text",
459
+ text: JSON.stringify({
460
+ filePath,
461
+ content,
462
+ size: stats.size,
463
+ lastModified: stats.mtime,
464
+ encoding,
465
+ }, null, 2),
466
+ },
467
+ ],
468
+ };
469
+ }
470
+ catch (error) {
471
+ throw new Error(`Failed to read file ${filePath}: ${error}`);
472
+ }
473
+ }
474
+ async checkFileExists(filePath) {
475
+ try {
476
+ await stat(filePath);
477
+ return true;
478
+ }
479
+ catch {
480
+ return false;
481
+ }
482
+ }
483
+ async getProjectProcesses(projectPath) {
484
+ try {
485
+ const processes = await psList.default();
486
+ const projectProcesses = [];
487
+ for (const process of processes) {
488
+ if (process.cmd && process.cmd.includes(projectPath)) {
489
+ projectProcesses.push(`${process.name} (PID: ${process.pid})`);
490
+ }
491
+ }
492
+ return projectProcesses;
493
+ }
494
+ catch {
495
+ return [];
496
+ }
497
+ }
498
+ async checkIntegrations(verbose = false) {
499
+ const results = {
500
+ timestamp: new Date().toISOString(),
501
+ integrations: {},
502
+ summary: {
503
+ total: 0,
504
+ healthy: 0,
505
+ degraded: 0,
506
+ failed: 0
507
+ }
508
+ };
509
+ // Check Node.js
510
+ try {
511
+ const { stdout: nodeVersion } = await execAsync("node --version");
512
+ results.integrations.node = {
513
+ status: "healthy",
514
+ version: nodeVersion.trim(),
515
+ method: "cli"
516
+ };
517
+ results.summary.healthy++;
518
+ }
519
+ catch (error) {
520
+ results.integrations.node = {
521
+ status: "failed",
522
+ error: `Node.js not accessible: ${error}`,
523
+ method: "cli"
524
+ };
525
+ results.summary.failed++;
526
+ }
527
+ results.summary.total++;
528
+ // Check GitHub CLI
529
+ try {
530
+ // First try JSON auth status
531
+ const { stdout: ghStatus } = await execAsync("gh auth status --json 2>/dev/null || echo 'not_authenticated'", {
532
+ timeout: 5000
533
+ });
534
+ let authStatus = false;
535
+ if (ghStatus.includes('"isAuthenticated": true')) {
536
+ authStatus = true;
537
+ }
538
+ else {
539
+ // Fallback: test a simple GitHub API call
540
+ try {
541
+ const { stdout: testCall } = await execAsync("gh api user --jq '.login' 2>/dev/null", { timeout: 3000 });
542
+ authStatus = testCall.trim().length > 0;
543
+ }
544
+ catch (testError) {
545
+ authStatus = false;
546
+ }
547
+ }
548
+ results.integrations.github_cli = {
549
+ status: authStatus ? "healthy" : "degraded",
550
+ authenticated: authStatus,
551
+ details: authStatus ? "GitHub CLI working" : "GitHub CLI not authenticated",
552
+ method: "cli"
553
+ };
554
+ if (authStatus) {
555
+ results.summary.healthy++;
556
+ }
557
+ else {
558
+ results.summary.degraded++;
559
+ }
560
+ }
561
+ catch (error) {
562
+ results.integrations.github_cli = {
563
+ status: "failed",
564
+ error: `GitHub CLI error: ${error}`,
565
+ method: "cli"
566
+ };
567
+ results.summary.failed++;
568
+ }
569
+ results.summary.total++;
570
+ // Check GitHub MCP Server (if available)
571
+ try {
572
+ const { stdout } = await execAsync("ps aux | grep -i 'github-mcp' | grep -v grep || echo 'not_running'", {
573
+ timeout: 3000
574
+ });
575
+ const mcpRunning = !stdout.includes('not_running') && stdout.trim().length > 0;
576
+ results.integrations.github_mcp = {
577
+ status: mcpRunning ? "healthy" : "degraded",
578
+ running: mcpRunning,
579
+ details: mcpRunning ? "GitHub MCP server detected" : "GitHub MCP server not detected",
580
+ method: "mcp"
581
+ };
582
+ if (mcpRunning) {
583
+ results.summary.healthy++;
584
+ }
585
+ else {
586
+ results.summary.degraded++;
587
+ }
588
+ }
589
+ catch (error) {
590
+ results.integrations.github_mcp = {
591
+ status: "failed",
592
+ error: `GitHub MCP check failed: ${error}`,
593
+ method: "mcp"
594
+ };
595
+ results.summary.failed++;
596
+ }
597
+ results.summary.total++;
598
+ // Check Doppler CLI
599
+ try {
600
+ const { stdout: dopplerVersion } = await execAsync("doppler --version", {
601
+ timeout: 5000
602
+ });
603
+ results.integrations.doppler = {
604
+ status: "healthy",
605
+ version: dopplerVersion.trim(),
606
+ method: "cli"
607
+ };
608
+ results.summary.healthy++;
609
+ }
610
+ catch (error) {
611
+ results.integrations.doppler = {
612
+ status: "failed",
613
+ error: `Doppler CLI not accessible: ${error}`,
614
+ method: "cli"
615
+ };
616
+ results.summary.failed++;
617
+ }
618
+ results.summary.total++;
619
+ // Add verbose details if requested
620
+ if (verbose) {
621
+ results.integrations.system_info = {
622
+ platform: process.platform,
623
+ node_version: process.version,
624
+ environment_vars: {
625
+ GITHUB_TOKEN: process.env.GITHUB_TOKEN ? 'set' : 'not_set',
626
+ DOPPLER_TOKEN: process.env.DOPPLER_TOKEN ? 'set' : 'not_set'
627
+ }
628
+ };
629
+ }
630
+ return {
631
+ content: [
632
+ {
633
+ type: "text",
634
+ text: JSON.stringify(results, null, verbose ? 2 : 0),
635
+ },
636
+ ],
637
+ };
638
+ }
639
+ async run() {
640
+ const transport = new StdioServerTransport();
641
+ await this.server.connect(transport);
642
+ console.error("Workspace MCP server running on stdio");
643
+ // Keep the process alive and handle graceful shutdown
644
+ process.on('SIGINT', () => {
645
+ console.error('Received SIGINT, shutting down gracefully...');
646
+ process.exit(0);
647
+ });
648
+ process.on('SIGTERM', () => {
649
+ console.error('Received SIGTERM, shutting down gracefully...');
650
+ process.exit(0);
651
+ });
652
+ // Prevent the process from exiting
653
+ process.stdin.resume();
654
+ }
655
+ }
656
+ const server = new WorkspaceMCPServer();
657
+ server.run().catch(console.error);
658
+ //# sourceMappingURL=index.js.map