Stream macOS unified logging for a bounded window
logStreamCollects parsed macOS log entries for a bounded duration (up to 60 seconds) to capture user flows without Console.app setup.
Instructions
[mg.log] Wrap log stream --style compact for a bounded duration (≤60 s — MCP requests should not block longer). Returns parsed entries collected during the window. Useful for capturing a specific user flow without setting up a full Console.app session.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| durationSec | No | How long to listen for log entries (max 60 seconds — MCP requests should not block longer). Default 10. | |
| predicate | No | ||
| process | No | ||
| subsystem | No | ||
| level | No | default | |
| maxEntries | No |
Implementation Reference
- src/tools/logShow.ts:186-216 (handler)Main handler function for the logStream tool. Runs `log stream --style compact` with optional predicate/process/subsystem/level filters for a bounded duration. On timeout (expected), falls back to logStreamWithTimer which spawns directly and collects output until the deadline.
export async function logStream(input: LogStreamInput): Promise<LogShowResult> { const args = ["stream", "--style", "compact"]; args.push(...levelArgs(input.level ?? "default")); const predicate = buildPredicate(input); if (predicate) args.push("--predicate", predicate); // `log stream` runs forever; we kill it after durationSec via the runner's // timeout, then parse whatever was collected. const ms = (input.durationSec ?? 10) * 1000; let result; try { result = await runCommand("log", args, { timeoutMs: ms }); } catch (err) { // Timeout is expected — collect partial output via a fallback mode. // Our runCommand currently throws on timeout; we need a slightly different // approach: spawn directly and collect output until the deadline. return await logStreamWithTimer(args, ms, input.maxEntries ?? 500); } const max = input.maxEntries ?? 500; const entries = parseLogOutput(result.stdout, max); const byType: Record<string, number> = {}; for (const e of entries) byType[e.type] = (byType[e.type] ?? 0) + 1; return { ok: true, command: `log ${args.join(" ")}`, totalParsed: entries.length, byType, entries, truncated: false, }; } - src/tools/logShow.ts:53-68 (schema)Zod schema for logStream input: durationSec (max 60s), predicate, process, subsystem, level, maxEntries.
export const logStreamSchema = z.object({ durationSec: z .number() .int() .positive() .max(60) .default(10) .describe( "How long to listen for log entries (max 60 seconds — MCP requests should not block longer). Default 10.", ), predicate: z.string().optional(), process: z.string().optional(), subsystem: z.string().optional(), level: z.enum(["default", "info", "debug"]).default("default"), maxEntries: z.number().int().positive().default(500), }); - src/index.ts:383-395 (registration)Registration of the 'logStream' tool on the MCP server with title, description, inputSchema, and handler callback.
server.registerTool( "logStream", { title: "Stream macOS unified logging for a bounded window", description: "[mg.log] Wrap `log stream --style compact` for a bounded duration (≤60 s — MCP requests should not block longer). Returns parsed entries collected during the window. Useful for capturing a specific user flow without setting up a full Console.app session.", inputSchema: logStreamSchema.shape, }, async (input) => { const result = await logStream(input); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; }, ); - src/tools/logShow.ts:218-256 (helper)Fallback helper used when log stream times out: spawns `log` directly, collects stdout for the specified duration, then kills the child and parses collected output.
async function logStreamWithTimer( args: string[], ms: number, max: number, ): Promise<LogShowResult> { // Use spawn directly here so we can capture stdout up to the deadline // without throwing. const { spawn } = await import("node:child_process"); return new Promise((resolve, reject) => { const child = spawn("log", args); let stdout = ""; let stderr = ""; child.stdout.on("data", (chunk: Buffer) => { stdout += chunk.toString("utf8"); }); child.stderr.on("data", (chunk: Buffer) => { stderr += chunk.toString("utf8"); }); child.on("error", reject); const timer = setTimeout(() => { child.kill("SIGTERM"); }, ms); child.on("close", () => { clearTimeout(timer); const entries = parseLogOutput(stdout, max); const byType: Record<string, number> = {}; for (const e of entries) byType[e.type] = (byType[e.type] ?? 0) + 1; const totalLines = stdout.split(/\r?\n/).filter((l) => l.trim()).length; resolve({ ok: true, command: `log ${args.join(" ")}`, totalParsed: entries.length, byType, entries, truncated: totalLines > entries.length, }); }); }); } - src/runtime/exec.ts:21-39 (helper)runCommand utility used by logStream to spawn the `log` process with timeout support.
export function runCommand( cmd: string, args: string[], opts: RunCommandOptions = {}, ): Promise<CommandResult> { return new Promise((resolve, reject) => { const child = spawn(cmd, args, { cwd: opts.cwd }); let stdout = ""; let stderr = ""; let killedByTimeout = false; let timer: NodeJS.Timeout | undefined; if (opts.timeoutMs && opts.timeoutMs > 0) { timer = setTimeout(() => { killedByTimeout = true; child.kill("SIGTERM"); }, opts.timeoutMs); }