Skip to content

Commit 60cd72e

Browse files
committed
refactor: streamline log management and server error handling
- Refactored the `LogStore` class by removing baseline timestamp management to simplify log clearing operations. - Updated the `clearLogs` tool to enforce hard clearing of logs, removing the scope parameter. - Enhanced the `getLogs` tool by removing auto-baseline and stacked mode options, improving clarity in log retrieval. - Added informative messages for users when no logs are found, suggesting available projects for better user experience. - Improved error handling in the MCP server to better manage port conflicts during server startup.
1 parent d165e7c commit 60cd72e

5 files changed

Lines changed: 63 additions & 92 deletions

File tree

packages/mcp/src/schemas/logs.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,11 @@ export const GetLogsArgs = {
88
limit: z.number().int().min(1).max(5000).optional().describe('Max number of entries to return'),
99
contains: z.string().optional().describe('Substring filter on entry.text'),
1010
sinceMs: z.number().nonnegative().optional().describe('Only entries with time >= sinceMs'),
11-
project: z.string().optional().describe('Project name to filter logs'),
12-
autoBaseline: z.boolean().optional().default(true).describe('Automatically baseline after retrieval to show only new logs next time'),
13-
stackedMode: z.boolean().optional().default(false).describe('Disable auto-baseline to accumulate logs across calls')
11+
project: z.string().optional().describe('Project name to filter logs')
1412
} satisfies z.ZodRawShape;
1513

1614
export const ClearLogsArgs = {
1715
session: z.string().optional().describe('8-char session id prefix to clear only one session'),
18-
scope: z.enum(['soft','hard']).optional().default('hard').describe('soft: set baseline (non-destructive), hard: delete entries'),
1916
project: z.string().optional().describe('Project name to clear only that project\'s logs')
2017
} satisfies z.ZodRawShape;
2118

packages/mcp/src/server.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -354,20 +354,20 @@ export async function startIngestOnlyServer(
354354
actualPort = await listenWithResult(nodeServer, opts.host, opts.port);
355355
} catch (err: any) {
356356
const isAddrInUse = err && (err.code === 'EADDRINUSE' || String(err.message || '').includes('EADDRINUSE'));
357-
if (isAddrInUse) {
358-
const base = `http://${opts.host}:${opts.port}`;
359-
// If an ingest server already responds to /health, reuse it silently
360-
try {
361-
const ctrl = new AbortController();
362-
const t = setTimeout(() => ctrl.abort(), 400);
363-
const res = await fetch(`${base}/health`, { signal: ctrl.signal as any, cache: 'no-store' as any });
364-
clearTimeout(t);
365-
if (res && res.ok) {
366-
// eslint-disable-next-line no-console
367-
console.error(`Ingest server already running at ${base}${opts.logsRoute}. Reusing existing instance.`);
368-
return; // Treat as success
369-
}
370-
} catch {}
357+
if (isAddrInUse) {
358+
const base = `http://${opts.host}:${opts.port}`;
359+
// If an ingest server already responds to /health, reuse it silently
360+
try {
361+
const ctrl = new AbortController();
362+
const t = setTimeout(() => ctrl.abort(), 400);
363+
const res = await fetch(`${base}/health`, { signal: ctrl.signal as any, cache: 'no-store' as any });
364+
clearTimeout(t);
365+
if (res && res.ok) {
366+
// eslint-disable-next-line no-console
367+
console.error(`Ingest server already running at ${base}${opts.logsRoute}. Reusing existing instance.`);
368+
return; // Treat as success
369+
}
370+
} catch {}
371371
// eslint-disable-next-line no-console
372372
console.error(`Failed to start ingest-only server: Port in use at ${base}${opts.logsRoute}`);
373373
}

packages/mcp/src/store.ts

Lines changed: 4 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ const DEFAULT_BUFFER = 1000;
1717
export class LogStore {
1818
private entries: LogEntry[] = [];
1919
private max: number;
20-
private baselineTimestamps: Map<string, number> = new Map(); // For soft clear
2120

2221
constructor(max = DEFAULT_BUFFER) {
2322
this.max = Math.max(50, max | 0);
@@ -34,59 +33,27 @@ export class LogStore {
3433
}
3534

3635
clear(options?: { session?: string; scope?: 'soft' | 'hard'; project?: string }) {
37-
const scope = options?.scope || 'hard';
3836
const session = options?.session;
3937
const project = options?.project;
4038

41-
if (scope === 'soft') {
42-
if (project && project.trim()) {
43-
this.baselineTimestamps.set(`project:${project}`, Date.now());
44-
} else if (session && session.trim()) {
45-
this.baselineTimestamps.set(`session:${session.slice(0, 8)}`, Date.now());
46-
} else {
47-
// Global soft baseline (discouraged for multi-project, kept for backward compat)
48-
this.baselineTimestamps.set('__global__', Date.now());
49-
}
50-
return;
51-
}
52-
53-
// Hard clear
5439
if (project && project.trim()) {
55-
// Remove only entries for this project and drop its baseline
40+
// Remove only entries for this project
5641
const p = project;
5742
this.entries = this.entries.filter(e => (e.project || '') !== p);
58-
this.baselineTimestamps.delete(`project:${p}`);
5943
return;
6044
}
6145

6246
if (session && session.trim()) {
6347
const s = session.slice(0, 8);
6448
this.entries = this.entries.filter(e => (e.sessionId || '').slice(0, 8) !== s);
65-
this.baselineTimestamps.delete(`session:${s}`);
6649
return;
6750
}
6851

69-
// Global hard clear
52+
// Global clear
7053
this.entries.length = 0;
71-
this.baselineTimestamps.clear();
72-
}
73-
74-
/** Set a baseline timestamp without deleting entries. If session omitted, use global baseline. */
75-
baseline(session?: string, when: number = Date.now()) {
76-
const key = session ? `session:${session.slice(0, 8)}` : '__global__';
77-
this.baselineTimestamps.set(key, when);
78-
}
79-
80-
/** Set a baseline for a specific project (recommended for multi-project use). */
81-
baselineProject(project: string, when: number = Date.now()) {
82-
if (!project || !project.trim()) return;
83-
this.baselineTimestamps.set(`project:${project}`, when);
8454
}
8555

86-
/** Set a global baseline (discouraged for multi-project; kept for backward compatibility). */
87-
baselineGlobal(when: number = Date.now()) {
88-
this.baselineTimestamps.set('__global__', when);
89-
}
56+
// Baseline methods removed - logs persist until buffer limit
9057

9158
toText(session?: string): string {
9259
return this.snapshot(session).map((e) => {
@@ -106,24 +73,7 @@ export class LogStore {
10673
snapshot(session?: string): LogEntry[] {
10774
let items = this.entries.slice();
10875
if (session) items = items.filter(e => (e.sessionId || '').slice(0, 8) === session);
109-
110-
const globalTs = this.baselineTimestamps.get('__global__') || 0;
111-
const sessionTs = session ? (this.baselineTimestamps.get(`session:${session}`) || 0) : 0;
112-
113-
const filtered = items.filter((e) => {
114-
const t = e.time || 0;
115-
const projectTs = e.project ? (this.baselineTimestamps.get(`project:${e.project}`) || 0) : 0;
116-
// Session baseline only applies when caller filtered by that session.
117-
const threshold = Math.max(globalTs, sessionTs, projectTs);
118-
return t === 0 || t >= threshold;
119-
});
120-
121-
if (filtered.length === 0 && this.entries.length > 0 && (globalTs || sessionTs)) {
122-
// eslint-disable-next-line no-console
123-
console.warn(`All ${this.entries.length} logs filtered out by baseline. Check timestamp format or baseline scope.`);
124-
}
125-
126-
return filtered;
76+
return items;
12777
}
12878
}
12979

packages/mcp/src/tools/clearLogs.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,12 @@ export function registerClearLogsTool(ctx: McpToolContext) {
1111
ClearLogsSchema,
1212
async (args, _extra) => {
1313
const safeArgs = args || {} as any;
14-
const { session, scope = 'hard', project } = safeArgs as typeof ClearLogsSchema['_output'];
14+
const { session, project } = safeArgs as typeof ClearLogsSchema['_output'];
1515
const validSession = validateSessionId(session);
1616

17-
store.clear({ session: validSession, scope, project });
17+
store.clear({ session: validSession, scope: 'hard', project });
1818

19-
let message = 'Browser log buffer ';
20-
message += scope === 'soft' ? 'baseline set' : 'cleared';
19+
let message = 'Browser log buffer cleared';
2120
if (project) message += ` for project ${project}`;
2221
else if (validSession) message += ` for session ${validSession}`;
2322
message += '.';

packages/mcp/src/tools/getLogs.ts

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,13 @@ export function registerGetLogsTool(ctx: McpToolContext) {
2020
contains,
2121
sinceMs,
2222
project,
23-
autoBaseline = true,
24-
stackedMode = false
23+
// Removed: autoBaseline and stackedMode
2524
} = safeArgs as typeof GetLogsSchema['_output'];
2625

2726
const validSession = validateSessionId(session);
2827
const validSince = typeof sinceMs === 'number' && sinceMs >= 0 ? sinceMs : undefined;
2928

30-
// Standardize: let store handle session filtering
29+
// Get logs with optional session filter
3130
let items = store.snapshot(validSession);
3231
if (validSince) items = items.filter(e => !e.time || e.time >= validSince);
3332
if (level?.length) items = items.filter(e => level.includes(e.level));
@@ -53,6 +52,9 @@ export function registerGetLogsTool(ctx: McpToolContext) {
5352
.filter(([, t]) => t > 0 && (now - t) <= recentWindowMs)
5453
.map(([p]) => p);
5554
if (active.length === 1) autoProject = active[0];
55+
} else if (projectsAll.size === 1) {
56+
// If only one project exists, auto-select it
57+
autoProject = Array.from(projectsAll)[0];
5658
}
5759
}
5860

@@ -62,6 +64,22 @@ export function registerGetLogsTool(ctx: McpToolContext) {
6264
// Multi-project awareness: if no explicit project and multiple projects exist, show grouped preview
6365
const uniqueProjects = new Set(limited.map(e => e.project || '')); uniqueProjects.delete('');
6466
let text: string;
67+
68+
// If no logs found, provide helpful message
69+
if (limited.length === 0) {
70+
const allProjects = new Set(store.snapshot().map(e => e.project || '').filter(p => p));
71+
if (allProjects.size > 0) {
72+
text = `No logs found matching your criteria. Available projects: ${Array.from(allProjects).join(', ')}\n\nTry: get_logs with { project: "<project-name>" }`;
73+
} else {
74+
text = 'No logs available yet.';
75+
}
76+
return {
77+
content: [
78+
{ type: 'text' as const, text }
79+
]
80+
};
81+
}
82+
6583
if (!project && !autoProject && uniqueProjects.size > 1) {
6684
const groups = Array.from(uniqueProjects.values());
6785
const byProject: Record<string, typeof limited> = {};
@@ -111,23 +129,30 @@ export function registerGetLogsTool(ctx: McpToolContext) {
111129
}).join('\n');
112130
}
113131

114-
// Auto-baseline unless stackedMode (prefer project/session scopes; avoid global)
115-
try {
116-
if (autoBaseline && !stackedMode) {
117-
if (project) {
118-
store.baselineProject(project);
119-
} else if (autoProject) {
120-
store.baselineProject(autoProject);
121-
} else if (validSession) {
122-
store.baseline(validSession);
123-
}
124-
// No global baseline by default to avoid cross-project interference
132+
// Add timestamp info to help users understand log freshness
133+
const now = Date.now();
134+
const oldestTime = limited.find(e => e.time)?.time || 0;
135+
const newestTime = limited.findLast(e => e.time)?.time || 0;
136+
137+
let timestampInfo = '';
138+
if (oldestTime && newestTime) {
139+
const ageMs = now - oldestTime;
140+
const ageSec = Math.floor(ageMs / 1000);
141+
const ageMin = Math.floor(ageSec / 60);
142+
143+
const rangeMs = newestTime - oldestTime;
144+
const rangeSec = Math.floor(rangeMs / 1000);
145+
146+
if (ageMin > 0) {
147+
timestampInfo = `\n\n[Log range: ${rangeSec}s, oldest: ${ageMin}m ago]`;
148+
} else {
149+
timestampInfo = `\n\n[Log range: ${rangeSec}s, oldest: ${ageSec}s ago]`;
125150
}
126-
} catch {}
151+
}
127152

128153
return {
129154
content: [
130-
{ type: 'text' as const, text }
155+
{ type: 'text' as const, text: text + timestampInfo }
131156
]
132157
};
133158
}

0 commit comments

Comments
 (0)