@chinchillaenterprises/mcp-dev-logger 1.2.0 → 2.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/dist/index.js CHANGED
@@ -3,27 +3,46 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { ListToolsRequestSchema, CallToolRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
5
  import { z } from "zod";
6
- import { spawn } from "child_process";
7
- import { writeFileSync, readFileSync, existsSync, appendFileSync, copyFileSync, promises as fs } from "fs";
6
+ import { spawn, exec } from "child_process";
7
+ import { writeFileSync, readFileSync, existsSync, appendFileSync, copyFileSync, readdirSync, statSync, mkdirSync } from "fs";
8
8
  import { join, resolve } from "path";
9
9
  import { tmpdir } from "os";
10
+ import { promisify } from "util";
11
+ const execAsync = promisify(exec);
10
12
  // Schema definitions
11
13
  const StartLogStreamingArgsSchema = z.object({
12
14
  command: z.string().optional().describe("Dev command to run (default: npm run dev)"),
13
- outputFile: z.string().optional().describe("File to write logs to (default: dev-server-logs.txt)"),
15
+ outputFile: z.string().optional().describe("File to write logs to (DEPRECATED: use structuredLogging instead)"),
14
16
  cwd: z.string().optional().describe("Working directory"),
15
- env: z.record(z.string()).optional().describe("Environment variables")
17
+ env: z.record(z.string()).optional().describe("Environment variables"),
18
+ processId: z.string().optional().describe("Custom process ID (auto-generated if not provided)"),
19
+ structuredLogging: z.boolean().optional().describe("Use organized logging with date-based sessions (default: true)"),
20
+ sessionDate: z.string().optional().describe("Session date for organized logging (YYYY-MM-DD, defaults to today)"),
21
+ logType: z.enum(["frontend", "backend", "amplify", "custom"]).optional().describe("Process type for standardized naming (auto-detected if not provided)")
22
+ });
23
+ const StopLogStreamingArgsSchema = z.object({
24
+ processId: z.string().optional().describe("Process ID to stop (if not provided, stops all processes)")
16
25
  });
17
26
  const RestartLogStreamingArgsSchema = z.object({
27
+ processId: z.string().describe("Process ID to restart"),
18
28
  clearLogs: z.boolean().optional().describe("Clear logs on restart (default: false)")
19
29
  });
20
30
  const TailLogsArgsSchema = z.object({
31
+ processId: z.string().optional().describe("Process ID to tail logs from (if not provided, tries to find single process)"),
21
32
  lines: z.number().optional().describe("Number of lines to return (default: 50)"),
22
33
  filter: z.string().optional().describe("Grep pattern to filter logs")
23
34
  });
24
35
  const ClearLogsArgsSchema = z.object({
36
+ processId: z.string().optional().describe("Process ID to clear logs for (if not provided, tries to find single process)"),
25
37
  backup: z.boolean().optional().describe("Backup logs before clearing (default: false)")
26
38
  });
39
+ const CheckRunningProcessesArgsSchema = z.object({
40
+ processType: z.enum(["frontend", "backend", "amplify", "custom"]).optional().describe("Type of process to check for conflicts"),
41
+ port: z.number().optional().describe("Specific port to check")
42
+ });
43
+ const DiscoverLogsArgsSchema = z.object({
44
+ sessionDate: z.string().optional().describe("Specific session date (YYYY-MM-DD) to discover logs for (defaults to most recent)")
45
+ });
27
46
  class DevLoggerServer {
28
47
  server;
29
48
  activeServers;
@@ -64,7 +83,8 @@ class DevLoggerServer {
64
83
  cwd: info.cwd,
65
84
  outputFile: info.outputFile,
66
85
  startTime: info.startTime,
67
- pid: info.pid
86
+ pid: info.pid,
87
+ processId: info.processId
68
88
  };
69
89
  }
70
90
  writeFileSync(this.pidFile, JSON.stringify(state, null, 2));
@@ -73,12 +93,317 @@ class DevLoggerServer {
73
93
  // Ignore errors saving state
74
94
  }
75
95
  }
96
+ generateProcessId(command, outputFile) {
97
+ // Create a readable process ID based on command and output file
98
+ const commandPart = command.split(' ')[0].replace(/[^a-zA-Z0-9]/g, '');
99
+ const filePart = outputFile.split('/').pop()?.replace(/[^a-zA-Z0-9]/g, '').replace(/\.(txt|log)$/, '') || 'default';
100
+ return `${commandPart}-${filePart}`;
101
+ }
102
+ findProcessOrDefault(processId) {
103
+ if (processId) {
104
+ return this.activeServers.has(processId) ? processId : null;
105
+ }
106
+ // If no processId provided and only one process running, use that
107
+ const activeIds = Array.from(this.activeServers.keys());
108
+ if (activeIds.length === 1) {
109
+ return activeIds[0];
110
+ }
111
+ return null;
112
+ }
113
+ async checkPortInUse(port) {
114
+ try {
115
+ // Use lsof to check what's using the port
116
+ const { stdout } = await execAsync(`lsof -i :${port} -P -t`);
117
+ const pid = parseInt(stdout.trim());
118
+ if (pid) {
119
+ // Get command for this PID
120
+ try {
121
+ const { stdout: cmdStdout } = await execAsync(`ps -p ${pid} -o command=`);
122
+ return {
123
+ inUse: true,
124
+ pid: pid,
125
+ command: cmdStdout.trim()
126
+ };
127
+ }
128
+ catch {
129
+ return { inUse: true, pid: pid };
130
+ }
131
+ }
132
+ }
133
+ catch (error) {
134
+ // Port is not in use or lsof failed
135
+ }
136
+ return { inUse: false };
137
+ }
138
+ detectProcessType(command) {
139
+ const cmd = command.toLowerCase();
140
+ if (cmd.includes('next dev') || cmd.includes('npm run dev') || cmd.includes('yarn dev') || cmd.includes('pnpm dev') || cmd.includes('vite')) {
141
+ return 'frontend';
142
+ }
143
+ if (cmd.includes('ampx sandbox') || cmd.includes('amplify sandbox')) {
144
+ return 'amplify';
145
+ }
146
+ if (cmd.includes('node server') || cmd.includes('express') || cmd.includes('fastify') || cmd.includes('api')) {
147
+ return 'backend';
148
+ }
149
+ return 'custom';
150
+ }
151
+ getCommonPorts(processType) {
152
+ switch (processType) {
153
+ case 'frontend':
154
+ return [3000, 3001, 5173, 5174, 8080, 8081];
155
+ case 'backend':
156
+ return [3001, 8000, 8080, 4000, 5000];
157
+ case 'amplify':
158
+ return []; // Amplify doesn't typically use fixed ports
159
+ default:
160
+ return [3000, 3001, 5173, 5174, 8000, 8080, 8081, 4000, 5000];
161
+ }
162
+ }
163
+ async checkRunningProcesses(processType, specificPort) {
164
+ const conflicts = [];
165
+ const recommendations = [];
166
+ // Ports to check
167
+ const portsToCheck = specificPort ? [specificPort] : this.getCommonPorts(processType);
168
+ // Check each port
169
+ for (const port of portsToCheck) {
170
+ const portCheck = await this.checkPortInUse(port);
171
+ if (portCheck.inUse) {
172
+ const detectedType = portCheck.command ? this.detectProcessType(portCheck.command) : 'unknown';
173
+ conflicts.push({
174
+ port: port,
175
+ pid: portCheck.pid || 0,
176
+ command: portCheck.command || 'Unknown process',
177
+ processType: detectedType
178
+ });
179
+ recommendations.push(`Port ${port} is busy with ${detectedType} server (PID ${portCheck.pid})`);
180
+ }
181
+ }
182
+ // Find available ports
183
+ const allCommonPorts = [3000, 3001, 3002, 5173, 5174, 8000, 8080, 8081, 4000, 5000];
184
+ const busyPorts = conflicts.map(c => c.port);
185
+ const availablePorts = allCommonPorts.filter(port => !busyPorts.includes(port));
186
+ // Add recommendations
187
+ if (conflicts.length > 0 && availablePorts.length > 0) {
188
+ recommendations.push(`Consider using port ${availablePorts[0]} instead`);
189
+ recommendations.push(`Or stop the existing process first`);
190
+ }
191
+ if (conflicts.length === 0) {
192
+ recommendations.push('No conflicts detected - safe to start new processes');
193
+ }
194
+ return {
195
+ status: 'success',
196
+ conflicts,
197
+ availablePorts: availablePorts.slice(0, 5), // Limit to first 5 available
198
+ recommendations
199
+ };
200
+ }
201
+ detectLogProcessType(fileName) {
202
+ const name = fileName.toLowerCase();
203
+ if (name.includes('frontend') || name.includes('next') || name.includes('react') || name.includes('vite')) {
204
+ return 'frontend';
205
+ }
206
+ if (name.includes('backend') || name.includes('api') || name.includes('server')) {
207
+ return 'backend';
208
+ }
209
+ if (name.includes('amplify') || name.includes('ampx')) {
210
+ return 'amplify';
211
+ }
212
+ return 'custom';
213
+ }
214
+ getLogFilesFromDirectory(dirPath) {
215
+ if (!existsSync(dirPath)) {
216
+ return [];
217
+ }
218
+ const logFiles = [];
219
+ try {
220
+ const files = readdirSync(dirPath);
221
+ for (const file of files) {
222
+ // Skip non-log files and hidden files
223
+ if (!file.endsWith('.log') && !file.endsWith('.txt') || file.startsWith('.')) {
224
+ continue;
225
+ }
226
+ const filePath = join(dirPath, file);
227
+ const stats = statSync(filePath);
228
+ // Get line count for log files
229
+ let lineCount;
230
+ try {
231
+ const content = readFileSync(filePath, 'utf8');
232
+ lineCount = content.split('\n').filter(line => line.trim()).length;
233
+ }
234
+ catch {
235
+ // Ignore errors reading file content
236
+ }
237
+ logFiles.push({
238
+ fileName: file,
239
+ filePath: filePath,
240
+ processType: this.detectLogProcessType(file),
241
+ size: stats.size,
242
+ lastModified: stats.mtime,
243
+ lineCount
244
+ });
245
+ }
246
+ }
247
+ catch (error) {
248
+ // Directory read error - return empty array
249
+ }
250
+ return logFiles.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
251
+ }
252
+ async discoverLogs(sessionDate) {
253
+ const cwd = process.cwd();
254
+ const logsDir = join(cwd, 'logs');
255
+ // If logs directory doesn't exist, check current directory for log files
256
+ if (!existsSync(logsDir)) {
257
+ const currentDirLogs = this.getLogFilesFromDirectory(cwd);
258
+ if (currentDirLogs.length === 0) {
259
+ return {
260
+ status: 'no_logs_found',
261
+ availableSessions: [],
262
+ recommendedLogs: ['No log files found. Start a dev server to create logs.'],
263
+ totalSessions: 0
264
+ };
265
+ }
266
+ // Return current directory logs as a session
267
+ return {
268
+ status: 'success',
269
+ mostRecentSession: {
270
+ sessionDate: new Date().toISOString().split('T')[0],
271
+ logFiles: currentDirLogs,
272
+ totalFiles: currentDirLogs.length,
273
+ sessionPath: cwd
274
+ },
275
+ availableSessions: ['current'],
276
+ recommendedLogs: currentDirLogs.map(f => `${f.processType}: ${f.fileName} (${f.lineCount || 0} lines)`),
277
+ totalSessions: 1
278
+ };
279
+ }
280
+ // Discover date-based sessions in logs directory
281
+ const sessions = [];
282
+ let mostRecentSession;
283
+ try {
284
+ const entries = readdirSync(logsDir);
285
+ for (const entry of entries) {
286
+ const entryPath = join(logsDir, entry);
287
+ const stats = statSync(entryPath);
288
+ if (stats.isDirectory() && /^\d{4}-\d{2}-\d{2}$/.test(entry)) {
289
+ sessions.push(entry);
290
+ }
291
+ }
292
+ // Sort sessions by date (most recent first)
293
+ sessions.sort((a, b) => b.localeCompare(a));
294
+ // Get the target session (specified or most recent)
295
+ const targetSession = sessionDate || sessions[0];
296
+ if (targetSession && sessions.includes(targetSession)) {
297
+ const sessionPath = join(logsDir, targetSession);
298
+ const logFiles = this.getLogFilesFromDirectory(sessionPath);
299
+ mostRecentSession = {
300
+ sessionDate: targetSession,
301
+ logFiles,
302
+ totalFiles: logFiles.length,
303
+ sessionPath
304
+ };
305
+ }
306
+ }
307
+ catch (error) {
308
+ // Directory read error
309
+ }
310
+ const recommendedLogs = [];
311
+ if (mostRecentSession) {
312
+ if (mostRecentSession.logFiles.length === 0) {
313
+ recommendedLogs.push(`Session ${mostRecentSession.sessionDate} has no log files`);
314
+ }
315
+ else {
316
+ recommendedLogs.push(`Most recent session: ${mostRecentSession.sessionDate}`);
317
+ recommendedLogs.push(...mostRecentSession.logFiles.map(f => `${f.processType}: ${f.fileName} (${f.lineCount || 0} lines, ${Math.round(f.size / 1024)}KB)`));
318
+ }
319
+ }
320
+ else if (sessions.length === 0) {
321
+ recommendedLogs.push('No organized log sessions found. Logs will be created in logs/YYYY-MM-DD/ when you start dev servers.');
322
+ }
323
+ else {
324
+ recommendedLogs.push('No logs found in recent sessions. Start dev servers to generate logs.');
325
+ }
326
+ return {
327
+ status: sessions.length > 0 ? 'success' : 'no_sessions_found',
328
+ mostRecentSession,
329
+ availableSessions: sessions,
330
+ recommendedLogs,
331
+ totalSessions: sessions.length
332
+ };
333
+ }
334
+ createStructuredLogPath(command, sessionDate, logType) {
335
+ const cwd = process.cwd();
336
+ const today = new Date().toISOString().split('T')[0];
337
+ const targetDate = sessionDate || today;
338
+ // Create logs directory if it doesn't exist
339
+ const logsDir = join(cwd, 'logs');
340
+ const sessionDir = join(logsDir, targetDate);
341
+ if (!existsSync(logsDir)) {
342
+ mkdirSync(logsDir, { recursive: true });
343
+ }
344
+ if (!existsSync(sessionDir)) {
345
+ mkdirSync(sessionDir, { recursive: true });
346
+ }
347
+ // Determine log type and generate standardized filename
348
+ const detectedType = logType || this.detectProcessType(command);
349
+ const standardizedName = this.generateStandardizedLogName(detectedType, command);
350
+ return join(sessionDir, standardizedName);
351
+ }
352
+ generateStandardizedLogName(logType, command) {
353
+ // Standardized naming conventions to prevent confusion
354
+ const timestamp = new Date().toTimeString().split(' ')[0].replace(/:/g, '-');
355
+ switch (logType) {
356
+ case 'frontend':
357
+ if (command.includes('next'))
358
+ return `frontend-nextjs-${timestamp}.log`;
359
+ if (command.includes('vite'))
360
+ return `frontend-vite-${timestamp}.log`;
361
+ if (command.includes('react'))
362
+ return `frontend-react-${timestamp}.log`;
363
+ return `frontend-dev-${timestamp}.log`;
364
+ case 'backend':
365
+ if (command.includes('express'))
366
+ return `backend-express-${timestamp}.log`;
367
+ if (command.includes('fastify'))
368
+ return `backend-fastify-${timestamp}.log`;
369
+ if (command.includes('node'))
370
+ return `backend-node-${timestamp}.log`;
371
+ return `backend-api-${timestamp}.log`;
372
+ case 'amplify':
373
+ if (command.includes('sandbox'))
374
+ return `amplify-sandbox-${timestamp}.log`;
375
+ if (command.includes('deploy'))
376
+ return `amplify-deploy-${timestamp}.log`;
377
+ return `amplify-dev-${timestamp}.log`;
378
+ default:
379
+ // Extract first word of command for custom processes
380
+ const cmdName = command.split(' ')[0].replace(/[^a-zA-Z0-9]/g, '');
381
+ return `custom-${cmdName}-${timestamp}.log`;
382
+ }
383
+ }
384
+ shouldUseStructuredLogging(args) {
385
+ // Use structured logging by default unless explicitly disabled
386
+ // or if legacy outputFile is provided without structuredLogging flag
387
+ if (args.structuredLogging === false)
388
+ return false;
389
+ if (args.outputFile && args.structuredLogging !== true)
390
+ return false;
391
+ return true;
392
+ }
76
393
  killAllProcesses() {
77
394
  for (const [id, info] of this.activeServers) {
78
395
  try {
79
- if (info.process && info.pid) {
80
- // Kill the process group (negative PID) to kill all children
81
- process.kill(-info.pid, 'SIGTERM');
396
+ if (info.process) {
397
+ // Remove all event listeners to prevent memory leaks
398
+ info.process.stdout?.removeAllListeners('data');
399
+ info.process.stderr?.removeAllListeners('data');
400
+ info.process.removeAllListeners('exit');
401
+ info.process.removeAllListeners('error');
402
+ // Kill the process
403
+ if (info.pid) {
404
+ // Kill the process group (negative PID) to kill all children
405
+ process.kill(-info.pid, 'SIGTERM');
406
+ }
82
407
  }
83
408
  }
84
409
  catch (error) {
@@ -100,7 +425,7 @@ class DevLoggerServer {
100
425
  const tools = [
101
426
  {
102
427
  name: "dev_start_log_streaming",
103
- description: "Start development server and stream logs to file",
428
+ description: "Start development server with organized logging (logs/YYYY-MM-DD/). Supports structured sessions and standardized file naming.",
104
429
  inputSchema: {
105
430
  type: "object",
106
431
  properties: {
@@ -110,7 +435,7 @@ class DevLoggerServer {
110
435
  },
111
436
  outputFile: {
112
437
  type: "string",
113
- description: "File to write logs to (default: dev-server-logs.txt)"
438
+ description: "File to write logs to (DEPRECATED: use structuredLogging instead)"
114
439
  },
115
440
  cwd: {
116
441
  type: "string",
@@ -122,37 +447,77 @@ class DevLoggerServer {
122
447
  additionalProperties: {
123
448
  type: "string"
124
449
  }
450
+ },
451
+ processId: {
452
+ type: "string",
453
+ description: "Custom process ID (auto-generated if not provided)"
454
+ },
455
+ structuredLogging: {
456
+ type: "boolean",
457
+ description: "Use organized logging with date-based sessions (default: true)"
458
+ },
459
+ sessionDate: {
460
+ type: "string",
461
+ description: "Session date for organized logging (YYYY-MM-DD, defaults to today)"
462
+ },
463
+ logType: {
464
+ type: "string",
465
+ enum: ["frontend", "backend", "amplify", "custom"],
466
+ description: "Process type for standardized naming (auto-detected if not provided)"
125
467
  }
126
468
  }
127
469
  }
128
470
  },
129
471
  {
130
- name: "dev_stop_log_streaming",
131
- description: "Stop development server and logging",
472
+ name: "dev_list_processes",
473
+ description: "List all running development processes",
132
474
  inputSchema: {
133
475
  type: "object",
134
476
  properties: {}
135
477
  }
136
478
  },
479
+ {
480
+ name: "dev_stop_log_streaming",
481
+ description: "Stop development server(s) and logging",
482
+ inputSchema: {
483
+ type: "object",
484
+ properties: {
485
+ processId: {
486
+ type: "string",
487
+ description: "Process ID to stop (if not provided, stops all processes)"
488
+ }
489
+ }
490
+ }
491
+ },
137
492
  {
138
493
  name: "dev_restart_log_streaming",
139
- description: "Restart the development server",
494
+ description: "Restart a specific development server",
140
495
  inputSchema: {
141
496
  type: "object",
142
497
  properties: {
498
+ processId: {
499
+ type: "string",
500
+ description: "Process ID to restart"
501
+ },
143
502
  clearLogs: {
144
503
  type: "boolean",
145
504
  description: "Clear logs on restart (default: false)"
146
505
  }
147
- }
506
+ },
507
+ required: ["processId"]
148
508
  }
149
509
  },
150
510
  {
151
511
  name: "dev_get_status",
152
- description: "Get status of development server",
512
+ description: "Get status of development server(s)",
153
513
  inputSchema: {
154
514
  type: "object",
155
- properties: {}
515
+ properties: {
516
+ processId: {
517
+ type: "string",
518
+ description: "Process ID to get status for (if not provided, shows all processes)"
519
+ }
520
+ }
156
521
  }
157
522
  },
158
523
  {
@@ -161,6 +526,10 @@ class DevLoggerServer {
161
526
  inputSchema: {
162
527
  type: "object",
163
528
  properties: {
529
+ processId: {
530
+ type: "string",
531
+ description: "Process ID to tail logs from (if not provided, tries to find single process)"
532
+ },
164
533
  lines: {
165
534
  type: "number",
166
535
  description: "Number of lines to return (default: 50)"
@@ -178,12 +547,47 @@ class DevLoggerServer {
178
547
  inputSchema: {
179
548
  type: "object",
180
549
  properties: {
550
+ processId: {
551
+ type: "string",
552
+ description: "Process ID to clear logs for (if not provided, tries to find single process)"
553
+ },
181
554
  backup: {
182
555
  type: "boolean",
183
556
  description: "Backup logs before clearing (default: false)"
184
557
  }
185
558
  }
186
559
  }
560
+ },
561
+ {
562
+ name: "dev_check_running_processes",
563
+ description: "Check for running development processes that might conflict with new servers",
564
+ inputSchema: {
565
+ type: "object",
566
+ properties: {
567
+ processType: {
568
+ type: "string",
569
+ enum: ["frontend", "backend", "amplify", "custom"],
570
+ description: "Type of process to check for conflicts"
571
+ },
572
+ port: {
573
+ type: "number",
574
+ description: "Specific port to check"
575
+ }
576
+ }
577
+ }
578
+ },
579
+ {
580
+ name: "dev_discover_logs",
581
+ description: "Auto-discover available log files and sessions in organized directory structure",
582
+ inputSchema: {
583
+ type: "object",
584
+ properties: {
585
+ sessionDate: {
586
+ type: "string",
587
+ description: "Specific session date (YYYY-MM-DD) to discover logs for (defaults to most recent)"
588
+ }
589
+ }
590
+ }
187
591
  }
188
592
  ];
189
593
  return { tools };
@@ -194,63 +598,29 @@ class DevLoggerServer {
194
598
  switch (name) {
195
599
  case "dev_start_log_streaming": {
196
600
  const validatedArgs = StartLogStreamingArgsSchema.parse(args);
197
- // Kill any existing process first
198
- this.killAllProcesses();
199
- // Wait a moment for processes to die
200
- await new Promise(resolve => setTimeout(resolve, 1000));
201
601
  const command = validatedArgs.command || "npm run dev";
202
- // Create date-based log directory structure
203
- const now = new Date();
204
- const year = now.getFullYear();
205
- const month = String(now.getMonth() + 1).padStart(2, '0');
206
- const day = String(now.getDate()).padStart(2, '0');
207
- const logDir = join('logs', year.toString(), month, day);
208
- // Ensure log directory exists
209
- await fs.mkdir(logDir, { recursive: true });
210
- // Auto-detect appropriate log filename based on command
211
- let autoFileName;
212
- if (command.includes('ampx sandbox --stream-function-logs')) {
213
- autoFileName = 'amplify-functions.txt';
214
- }
215
- else if (command.includes('ampx sandbox')) {
216
- autoFileName = 'amplify-sandbox.txt';
217
- }
218
- else if (command.includes('npm run dev')) {
219
- autoFileName = 'npm-dev.txt';
220
- }
221
- else if (command.includes('yarn dev')) {
222
- autoFileName = 'yarn-dev.txt';
223
- }
224
- else if (command.includes('next dev')) {
225
- autoFileName = 'nextjs-dev.txt';
226
- }
227
- else if (command.includes('npm start')) {
228
- autoFileName = 'npm-start.txt';
229
- }
230
- else if (command.includes('yarn start')) {
231
- autoFileName = 'yarn-start.txt';
232
- }
233
- else if (command.includes('vite')) {
234
- autoFileName = 'vite-dev.txt';
235
- }
236
- else if (command.includes('remix dev')) {
237
- autoFileName = 'remix-dev.txt';
238
- }
239
- else if (command.includes('astro dev')) {
240
- autoFileName = 'astro-dev.txt';
602
+ const cwd = resolve(validatedArgs.cwd || process.cwd());
603
+ // Determine output file path using structured logging or legacy approach
604
+ let outputFile;
605
+ if (this.shouldUseStructuredLogging(validatedArgs)) {
606
+ outputFile = this.createStructuredLogPath(command, validatedArgs.sessionDate, validatedArgs.logType);
241
607
  }
242
608
  else {
243
- // Generic fallback
244
- autoFileName = 'dev-server-logs.txt';
609
+ outputFile = resolve(validatedArgs.outputFile || "dev-server-logs.txt");
610
+ }
611
+ // Generate or use provided process ID
612
+ let processId = validatedArgs.processId || this.generateProcessId(command, outputFile);
613
+ // Ensure unique process ID
614
+ let counter = 1;
615
+ const originalProcessId = processId;
616
+ while (this.activeServers.has(processId)) {
617
+ processId = `${originalProcessId}-${counter}`;
618
+ counter++;
245
619
  }
246
- // Use provided filename or auto-detected one
247
- const baseFileName = validatedArgs.outputFile || autoFileName;
248
- const outputFile = resolve(join(logDir, baseFileName.replace(/^logs[\/\\]/, '')));
249
- const cwd = resolve(validatedArgs.cwd || process.cwd());
250
620
  // Parse command into program and args
251
621
  const [program, ...cmdArgs] = command.split(' ');
252
622
  // Create or clear the output file
253
- writeFileSync(outputFile, `[${new Date().toISOString()}] Starting: ${command}\n`);
623
+ writeFileSync(outputFile, `[${new Date().toISOString()}] Starting: ${command} (Process ID: ${processId})\n`);
254
624
  try {
255
625
  // Spawn the process (detached to avoid signal propagation)
256
626
  const devProcess = spawn(program, cmdArgs, {
@@ -259,39 +629,40 @@ class DevLoggerServer {
259
629
  detached: true,
260
630
  stdio: ['ignore', 'pipe', 'pipe']
261
631
  });
262
- const serverId = "default";
263
632
  const serverInfo = {
264
633
  process: devProcess,
265
634
  command: command,
266
635
  cwd: cwd,
267
636
  outputFile: outputFile,
268
637
  startTime: new Date(),
269
- pid: devProcess.pid
638
+ pid: devProcess.pid,
639
+ processId: processId
270
640
  };
271
- this.activeServers.set(serverId, serverInfo);
641
+ this.activeServers.set(processId, serverInfo);
272
642
  // Stream stdout to file
273
643
  devProcess.stdout?.on('data', (data) => {
274
644
  const timestamp = new Date().toISOString();
275
- const logEntry = `[${timestamp}] ${data}`;
645
+ const logEntry = `[${timestamp}] [${processId}] ${data}`;
276
646
  appendFileSync(outputFile, logEntry);
277
647
  });
278
648
  // Stream stderr to file
279
649
  devProcess.stderr?.on('data', (data) => {
280
650
  const timestamp = new Date().toISOString();
281
- const logEntry = `[${timestamp}] [ERROR] ${data}`;
651
+ const logEntry = `[${timestamp}] [${processId}] [ERROR] ${data}`;
282
652
  appendFileSync(outputFile, logEntry);
283
653
  });
284
654
  // Handle process exit
285
655
  devProcess.on('exit', (code, signal) => {
286
656
  const timestamp = new Date().toISOString();
287
- const exitMessage = `[${timestamp}] Process exited with code ${code} and signal ${signal}\n`;
657
+ const exitMessage = `[${timestamp}] [${processId}] Process exited with code ${code} and signal ${signal}\n`;
288
658
  appendFileSync(outputFile, exitMessage);
289
- this.activeServers.delete(serverId);
659
+ this.activeServers.delete(processId);
660
+ this.saveState();
290
661
  });
291
662
  // Handle process errors
292
663
  devProcess.on('error', (error) => {
293
664
  const timestamp = new Date().toISOString();
294
- const errorMessage = `[${timestamp}] Process error: ${error.message}\n`;
665
+ const errorMessage = `[${timestamp}] [${processId}] Process error: ${error.message}\n`;
295
666
  appendFileSync(outputFile, errorMessage);
296
667
  });
297
668
  this.saveState();
@@ -301,12 +672,12 @@ class DevLoggerServer {
301
672
  type: "text",
302
673
  text: JSON.stringify({
303
674
  status: "started",
675
+ processId: processId,
304
676
  pid: devProcess.pid,
305
677
  command: command,
306
678
  cwd: cwd,
307
679
  outputFile: outputFile,
308
- logPath: outputFile.replace(cwd + '/', ''),
309
- message: `Dev server started. Logs streaming to ${outputFile.replace(cwd + '/', '')}`
680
+ message: `Dev server started with process ID: ${processId}. Logs streaming to ${outputFile}`
310
681
  }, null, 2)
311
682
  }
312
683
  ]
@@ -316,27 +687,112 @@ class DevLoggerServer {
316
687
  throw new Error(`Failed to start dev server: ${error instanceof Error ? error.message : String(error)}`);
317
688
  }
318
689
  }
690
+ case "dev_list_processes": {
691
+ const processes = Array.from(this.activeServers.values()).map(info => ({
692
+ processId: info.processId,
693
+ pid: info.pid,
694
+ command: info.command,
695
+ cwd: info.cwd,
696
+ outputFile: info.outputFile,
697
+ startTime: info.startTime,
698
+ uptime: new Date().getTime() - info.startTime.getTime()
699
+ }));
700
+ return {
701
+ content: [
702
+ {
703
+ type: "text",
704
+ text: JSON.stringify({
705
+ status: "success",
706
+ totalProcesses: processes.length,
707
+ processes: processes
708
+ }, null, 2)
709
+ }
710
+ ]
711
+ };
712
+ }
319
713
  case "dev_stop_log_streaming": {
320
- const serverId = "default";
321
- const serverInfo = this.activeServers.get(serverId);
322
- if (!serverInfo) {
323
- return {
324
- content: [
325
- {
326
- type: "text",
327
- text: JSON.stringify({
328
- status: "not_running",
329
- message: "No dev server is currently running"
330
- }, null, 2)
331
- }
332
- ]
333
- };
714
+ const validatedArgs = StopLogStreamingArgsSchema.parse(args);
715
+ if (validatedArgs.processId) {
716
+ // Stop specific process
717
+ const serverInfo = this.activeServers.get(validatedArgs.processId);
718
+ if (!serverInfo) {
719
+ return {
720
+ content: [
721
+ {
722
+ type: "text",
723
+ text: JSON.stringify({
724
+ status: "not_found",
725
+ message: `Process with ID '${validatedArgs.processId}' not found`
726
+ }, null, 2)
727
+ }
728
+ ]
729
+ };
730
+ }
731
+ try {
732
+ // Remove all event listeners to prevent memory leaks
733
+ if (serverInfo.process) {
734
+ serverInfo.process.stdout?.removeAllListeners('data');
735
+ serverInfo.process.stderr?.removeAllListeners('data');
736
+ serverInfo.process.removeAllListeners('exit');
737
+ serverInfo.process.removeAllListeners('error');
738
+ }
739
+ if (serverInfo.pid) {
740
+ process.kill(serverInfo.pid, 'SIGTERM');
741
+ }
742
+ this.activeServers.delete(validatedArgs.processId);
743
+ this.saveState();
744
+ return {
745
+ content: [
746
+ {
747
+ type: "text",
748
+ text: JSON.stringify({
749
+ status: "stopped",
750
+ processId: validatedArgs.processId,
751
+ message: `Process '${validatedArgs.processId}' stopped successfully`
752
+ }, null, 2)
753
+ }
754
+ ]
755
+ };
756
+ }
757
+ catch (error) {
758
+ throw new Error(`Failed to stop process '${validatedArgs.processId}': ${error instanceof Error ? error.message : String(error)}`);
759
+ }
334
760
  }
335
- try {
336
- if (serverInfo.pid) {
337
- process.kill(serverInfo.pid, 'SIGTERM');
761
+ else {
762
+ // Stop all processes
763
+ if (this.activeServers.size === 0) {
764
+ return {
765
+ content: [
766
+ {
767
+ type: "text",
768
+ text: JSON.stringify({
769
+ status: "not_running",
770
+ message: "No dev servers are currently running"
771
+ }, null, 2)
772
+ }
773
+ ]
774
+ };
338
775
  }
339
- this.activeServers.delete(serverId);
776
+ const stoppedProcesses = [];
777
+ for (const [processId, serverInfo] of this.activeServers) {
778
+ try {
779
+ // Remove all event listeners to prevent memory leaks
780
+ if (serverInfo.process) {
781
+ serverInfo.process.stdout?.removeAllListeners('data');
782
+ serverInfo.process.stderr?.removeAllListeners('data');
783
+ serverInfo.process.removeAllListeners('exit');
784
+ serverInfo.process.removeAllListeners('error');
785
+ }
786
+ if (serverInfo.pid) {
787
+ process.kill(serverInfo.pid, 'SIGTERM');
788
+ }
789
+ stoppedProcesses.push(processId);
790
+ }
791
+ catch (error) {
792
+ // Continue with other processes even if one fails
793
+ }
794
+ }
795
+ this.activeServers.clear();
340
796
  this.saveState();
341
797
  return {
342
798
  content: [
@@ -344,28 +800,26 @@ class DevLoggerServer {
344
800
  type: "text",
345
801
  text: JSON.stringify({
346
802
  status: "stopped",
347
- message: "Dev server stopped successfully"
803
+ stoppedProcesses: stoppedProcesses,
804
+ message: `Stopped ${stoppedProcesses.length} processes`
348
805
  }, null, 2)
349
806
  }
350
807
  ]
351
808
  };
352
809
  }
353
- catch (error) {
354
- throw new Error(`Failed to stop dev server: ${error instanceof Error ? error.message : String(error)}`);
355
- }
356
810
  }
357
811
  case "dev_restart_log_streaming": {
358
812
  const validatedArgs = RestartLogStreamingArgsSchema.parse(args);
359
- const serverId = "default";
360
- const serverInfo = this.activeServers.get(serverId);
813
+ const processId = validatedArgs.processId;
814
+ const serverInfo = this.activeServers.get(processId);
361
815
  if (!serverInfo) {
362
816
  return {
363
817
  content: [
364
818
  {
365
819
  type: "text",
366
820
  text: JSON.stringify({
367
- status: "error",
368
- message: "No dev server to restart. Use dev_start_log_streaming first."
821
+ status: "not_found",
822
+ message: `Process with ID '${processId}' not found. Use dev_list_processes to see available processes.`
369
823
  }, null, 2)
370
824
  }
371
825
  ]
@@ -375,6 +829,13 @@ class DevLoggerServer {
375
829
  const { command, cwd, outputFile } = serverInfo;
376
830
  // Stop current process
377
831
  try {
832
+ // Remove all event listeners to prevent memory leaks
833
+ if (serverInfo.process) {
834
+ serverInfo.process.stdout?.removeAllListeners('data');
835
+ serverInfo.process.stderr?.removeAllListeners('data');
836
+ serverInfo.process.removeAllListeners('exit');
837
+ serverInfo.process.removeAllListeners('error');
838
+ }
378
839
  if (serverInfo.pid) {
379
840
  process.kill(serverInfo.pid, 'SIGTERM');
380
841
  }
@@ -386,22 +847,24 @@ class DevLoggerServer {
386
847
  if (validatedArgs.clearLogs && existsSync(outputFile)) {
387
848
  writeFileSync(outputFile, '');
388
849
  }
850
+ // Remove the old server info to ensure complete cleanup
851
+ this.activeServers.delete(processId);
389
852
  // Wait a moment for process to die
390
853
  await new Promise(resolve => setTimeout(resolve, 1000));
391
- // Restart with same configuration
392
- const startArgs = {
393
- command,
394
- outputFile,
395
- cwd
396
- };
397
854
  // Start new process
398
855
  const [program, ...cmdArgs] = command.split(' ');
399
- writeFileSync(outputFile, `[${new Date().toISOString()}] Restarting: ${command}\n`, { flag: 'a' });
856
+ const restartMessage = `[${new Date().toISOString()}] Restarting: ${command} (Process ID: ${processId})\n`;
857
+ if (validatedArgs.clearLogs) {
858
+ writeFileSync(outputFile, restartMessage);
859
+ }
860
+ else {
861
+ appendFileSync(outputFile, restartMessage);
862
+ }
400
863
  const devProcess = spawn(program, cmdArgs, {
401
864
  cwd: cwd,
402
865
  env: process.env,
403
- shell: true,
404
- detached: false
866
+ detached: true,
867
+ stdio: ['ignore', 'pipe', 'pipe']
405
868
  });
406
869
  const newServerInfo = {
407
870
  process: devProcess,
@@ -409,27 +872,35 @@ class DevLoggerServer {
409
872
  cwd: cwd,
410
873
  outputFile: outputFile,
411
874
  startTime: new Date(),
412
- pid: devProcess.pid
875
+ pid: devProcess.pid,
876
+ processId: processId
413
877
  };
414
- this.activeServers.set("default", newServerInfo);
878
+ this.activeServers.set(processId, newServerInfo);
415
879
  // Stream stdout to file
416
880
  devProcess.stdout?.on('data', (data) => {
417
881
  const timestamp = new Date().toISOString();
418
- const logEntry = `[${timestamp}] ${data}`;
882
+ const logEntry = `[${timestamp}] [${processId}] ${data}`;
419
883
  appendFileSync(outputFile, logEntry);
420
884
  });
421
885
  // Stream stderr to file
422
886
  devProcess.stderr?.on('data', (data) => {
423
887
  const timestamp = new Date().toISOString();
424
- const logEntry = `[${timestamp}] [ERROR] ${data}`;
888
+ const logEntry = `[${timestamp}] [${processId}] [ERROR] ${data}`;
425
889
  appendFileSync(outputFile, logEntry);
426
890
  });
427
891
  // Handle process exit
428
892
  devProcess.on('exit', (code, signal) => {
429
893
  const timestamp = new Date().toISOString();
430
- const exitMessage = `[${timestamp}] Process exited with code ${code} and signal ${signal}\n`;
894
+ const exitMessage = `[${timestamp}] [${processId}] Process exited with code ${code} and signal ${signal}\n`;
431
895
  appendFileSync(outputFile, exitMessage);
432
- this.activeServers.delete("default");
896
+ this.activeServers.delete(processId);
897
+ this.saveState();
898
+ });
899
+ // Handle process errors
900
+ devProcess.on('error', (error) => {
901
+ const timestamp = new Date().toISOString();
902
+ const errorMessage = `[${timestamp}] [${processId}] Process error: ${error.message}\n`;
903
+ appendFileSync(outputFile, errorMessage);
433
904
  });
434
905
  this.saveState();
435
906
  return {
@@ -438,62 +909,148 @@ class DevLoggerServer {
438
909
  type: "text",
439
910
  text: JSON.stringify({
440
911
  status: "restarted",
912
+ processId: processId,
441
913
  pid: devProcess.pid,
442
914
  command: command,
443
915
  outputFile: outputFile,
444
- message: "Dev server restarted successfully"
916
+ message: `Process '${processId}' restarted successfully`
445
917
  }, null, 2)
446
918
  }
447
919
  ]
448
920
  };
449
921
  }
450
922
  case "dev_get_status": {
451
- const serverId = "default";
452
- const serverInfo = this.activeServers.get(serverId);
453
- if (!serverInfo) {
923
+ const validatedArgs = { processId: args?.processId };
924
+ if (validatedArgs.processId) {
925
+ // Get status for specific process
926
+ const serverInfo = this.activeServers.get(validatedArgs.processId);
927
+ if (!serverInfo) {
928
+ return {
929
+ content: [
930
+ {
931
+ type: "text",
932
+ text: JSON.stringify({
933
+ status: "not_found",
934
+ message: `Process with ID '${validatedArgs.processId}' not found`
935
+ }, null, 2)
936
+ }
937
+ ]
938
+ };
939
+ }
940
+ const uptime = new Date().getTime() - serverInfo.startTime.getTime();
941
+ const uptimeSeconds = Math.floor(uptime / 1000);
942
+ const uptimeMinutes = Math.floor(uptimeSeconds / 60);
943
+ const uptimeHours = Math.floor(uptimeMinutes / 60);
454
944
  return {
455
945
  content: [
456
946
  {
457
947
  type: "text",
458
948
  text: JSON.stringify({
459
- status: "not_running",
460
- message: "No dev server is currently running"
949
+ status: "running",
950
+ processId: serverInfo.processId,
951
+ pid: serverInfo.pid,
952
+ command: serverInfo.command,
953
+ cwd: serverInfo.cwd,
954
+ outputFile: serverInfo.outputFile,
955
+ startTime: serverInfo.startTime,
956
+ uptime: {
957
+ hours: uptimeHours,
958
+ minutes: uptimeMinutes % 60,
959
+ seconds: uptimeSeconds % 60
960
+ }
461
961
  }, null, 2)
462
962
  }
463
963
  ]
464
964
  };
465
965
  }
466
- const uptime = new Date().getTime() - serverInfo.startTime.getTime();
467
- const uptimeSeconds = Math.floor(uptime / 1000);
468
- const uptimeMinutes = Math.floor(uptimeSeconds / 60);
469
- const uptimeHours = Math.floor(uptimeMinutes / 60);
470
- return {
471
- content: [
472
- {
473
- type: "text",
474
- text: JSON.stringify({
475
- status: "running",
476
- pid: serverInfo.pid,
477
- command: serverInfo.command,
478
- cwd: serverInfo.cwd,
479
- outputFile: serverInfo.outputFile,
480
- startTime: serverInfo.startTime,
481
- uptime: {
482
- hours: uptimeHours,
483
- minutes: uptimeMinutes % 60,
484
- seconds: uptimeSeconds % 60
966
+ else {
967
+ // Get status for all processes
968
+ if (this.activeServers.size === 0) {
969
+ return {
970
+ content: [
971
+ {
972
+ type: "text",
973
+ text: JSON.stringify({
974
+ status: "not_running",
975
+ message: "No dev servers are currently running"
976
+ }, null, 2)
485
977
  }
486
- }, null, 2)
487
- }
488
- ]
489
- };
978
+ ]
979
+ };
980
+ }
981
+ const processes = Array.from(this.activeServers.values()).map(serverInfo => {
982
+ const uptime = new Date().getTime() - serverInfo.startTime.getTime();
983
+ const uptimeSeconds = Math.floor(uptime / 1000);
984
+ const uptimeMinutes = Math.floor(uptimeSeconds / 60);
985
+ const uptimeHours = Math.floor(uptimeMinutes / 60);
986
+ return {
987
+ processId: serverInfo.processId,
988
+ pid: serverInfo.pid,
989
+ command: serverInfo.command,
990
+ cwd: serverInfo.cwd,
991
+ outputFile: serverInfo.outputFile,
992
+ startTime: serverInfo.startTime,
993
+ uptime: {
994
+ hours: uptimeHours,
995
+ minutes: uptimeMinutes % 60,
996
+ seconds: uptimeSeconds % 60
997
+ }
998
+ };
999
+ });
1000
+ return {
1001
+ content: [
1002
+ {
1003
+ type: "text",
1004
+ text: JSON.stringify({
1005
+ status: "running",
1006
+ totalProcesses: processes.length,
1007
+ processes: processes
1008
+ }, null, 2)
1009
+ }
1010
+ ]
1011
+ };
1012
+ }
490
1013
  }
491
1014
  case "dev_tail_logs": {
492
1015
  const validatedArgs = TailLogsArgsSchema.parse(args);
493
- const serverId = "default";
494
- const serverInfo = this.activeServers.get(serverId);
495
- // Use the active server's log file if available, otherwise default
496
- const outputFile = serverInfo?.outputFile || "dev-server-logs.txt";
1016
+ // Find the process or use the default behavior
1017
+ const processId = this.findProcessOrDefault(validatedArgs.processId);
1018
+ if (!processId && validatedArgs.processId) {
1019
+ return {
1020
+ content: [
1021
+ {
1022
+ type: "text",
1023
+ text: JSON.stringify({
1024
+ status: "not_found",
1025
+ message: `Process with ID '${validatedArgs.processId}' not found`
1026
+ }, null, 2)
1027
+ }
1028
+ ]
1029
+ };
1030
+ }
1031
+ if (!processId && this.activeServers.size > 1) {
1032
+ return {
1033
+ content: [
1034
+ {
1035
+ type: "text",
1036
+ text: JSON.stringify({
1037
+ status: "ambiguous",
1038
+ message: "Multiple processes running. Please specify processId or use dev_list_processes to see options.",
1039
+ availableProcesses: Array.from(this.activeServers.keys())
1040
+ }, null, 2)
1041
+ }
1042
+ ]
1043
+ };
1044
+ }
1045
+ // Get the output file
1046
+ let outputFile;
1047
+ if (processId) {
1048
+ const serverInfo = this.activeServers.get(processId);
1049
+ outputFile = serverInfo.outputFile;
1050
+ }
1051
+ else {
1052
+ outputFile = "dev-server-logs.txt"; // fallback
1053
+ }
497
1054
  if (!existsSync(outputFile)) {
498
1055
  return {
499
1056
  content: [
@@ -526,6 +1083,7 @@ class DevLoggerServer {
526
1083
  type: "text",
527
1084
  text: JSON.stringify({
528
1085
  status: "success",
1086
+ processId: processId || "default",
529
1087
  file: outputFile,
530
1088
  totalLines: lines.length,
531
1089
  filteredLines: filteredLines.length,
@@ -542,10 +1100,44 @@ class DevLoggerServer {
542
1100
  }
543
1101
  case "dev_clear_logs": {
544
1102
  const validatedArgs = ClearLogsArgsSchema.parse(args);
545
- const serverId = "default";
546
- const serverInfo = this.activeServers.get(serverId);
547
- // Use the active server's log file if available, otherwise default
548
- const outputFile = serverInfo?.outputFile || "dev-server-logs.txt";
1103
+ // Find the process or use the default behavior
1104
+ const processId = this.findProcessOrDefault(validatedArgs.processId);
1105
+ if (!processId && validatedArgs.processId) {
1106
+ return {
1107
+ content: [
1108
+ {
1109
+ type: "text",
1110
+ text: JSON.stringify({
1111
+ status: "not_found",
1112
+ message: `Process with ID '${validatedArgs.processId}' not found`
1113
+ }, null, 2)
1114
+ }
1115
+ ]
1116
+ };
1117
+ }
1118
+ if (!processId && this.activeServers.size > 1) {
1119
+ return {
1120
+ content: [
1121
+ {
1122
+ type: "text",
1123
+ text: JSON.stringify({
1124
+ status: "ambiguous",
1125
+ message: "Multiple processes running. Please specify processId or use dev_list_processes to see options.",
1126
+ availableProcesses: Array.from(this.activeServers.keys())
1127
+ }, null, 2)
1128
+ }
1129
+ ]
1130
+ };
1131
+ }
1132
+ // Get the output file
1133
+ let outputFile;
1134
+ if (processId) {
1135
+ const serverInfo = this.activeServers.get(processId);
1136
+ outputFile = serverInfo.outputFile;
1137
+ }
1138
+ else {
1139
+ outputFile = "dev-server-logs.txt"; // fallback
1140
+ }
549
1141
  if (!existsSync(outputFile)) {
550
1142
  return {
551
1143
  content: [
@@ -573,6 +1165,7 @@ class DevLoggerServer {
573
1165
  type: "text",
574
1166
  text: JSON.stringify({
575
1167
  status: "success",
1168
+ processId: processId || "default",
576
1169
  message: `Log file cleared: ${outputFile}`,
577
1170
  backup: validatedArgs.backup ? "Created backup" : "No backup created"
578
1171
  }, null, 2)
@@ -584,6 +1177,40 @@ class DevLoggerServer {
584
1177
  throw new Error(`Failed to clear logs: ${error instanceof Error ? error.message : String(error)}`);
585
1178
  }
586
1179
  }
1180
+ case "dev_check_running_processes": {
1181
+ const validatedArgs = CheckRunningProcessesArgsSchema.parse(args);
1182
+ try {
1183
+ const result = await this.checkRunningProcesses(validatedArgs.processType, validatedArgs.port);
1184
+ return {
1185
+ content: [
1186
+ {
1187
+ type: "text",
1188
+ text: JSON.stringify(result, null, 2)
1189
+ }
1190
+ ]
1191
+ };
1192
+ }
1193
+ catch (error) {
1194
+ throw new Error(`Failed to check running processes: ${error instanceof Error ? error.message : String(error)}`);
1195
+ }
1196
+ }
1197
+ case "dev_discover_logs": {
1198
+ const validatedArgs = DiscoverLogsArgsSchema.parse(args);
1199
+ try {
1200
+ const result = await this.discoverLogs(validatedArgs.sessionDate);
1201
+ return {
1202
+ content: [
1203
+ {
1204
+ type: "text",
1205
+ text: JSON.stringify(result, null, 2)
1206
+ }
1207
+ ]
1208
+ };
1209
+ }
1210
+ catch (error) {
1211
+ throw new Error(`Failed to discover logs: ${error instanceof Error ? error.message : String(error)}`);
1212
+ }
1213
+ }
587
1214
  default:
588
1215
  throw new Error(`Unknown tool: ${name}`);
589
1216
  }