Skip to content

Commit 10defec

Browse files
colbymchenryclaude
andauthored
fix(mcp): silence the daemon-attach log by default (colbymchenry#618) (colbymchenry#725)
The "Attached to shared daemon" line is benign INFO, but it was written to stderr — and MCP hosts render all server stderr at error level (and append an `undefined` data field), so on every session start a healthy attach showed up as `[error] … undefined`. It is now gated behind CODEGRAPH_MCP_LOG_ATTACH=1: silent by default, opt-in for debugging daemon attach. Both attach sites (runProxy + connectWithHello) route through one helper. The daemon integration tests opt the harness into the log so their attach assertions still observe a successful attach. Re-applies the approach from colbymchenry#640 by @mturac. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7fd8b4c commit 10defec

4 files changed

Lines changed: 65 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
2626

2727
### Fixes
2828

29+
- The shared background server no longer logs a scary-looking `[error] … undefined` line on every session start. Attaching to the shared daemon is normal, healthy behavior, but the informational message was being surfaced by MCP hosts (Claude Code and others) as an error; it's now silent by default — set `CODEGRAPH_MCP_LOG_ATTACH=1` to surface it when debugging daemon attach. Thanks @mturac. (#618)
2930
- On Windows, CodeGraph's background processes no longer pile up without bound and saturate CPU over a long session. When the editor or agent that launched CodeGraph exited, its helper process couldn't tell its parent had gone — Windows reports process lineage differently than macOS and Linux — so the helper kept running, the shared background server never saw the client disconnect, and its idle timer never fired to shut it down. CodeGraph now detects parent-process exit directly on Windows, so helpers and the idle background server wind down promptly, the same as they already did on macOS and Linux. (#692, #576, #680)
3031
- The shared background server has two further safeguards against ever lingering: it now drops a client the moment it detects that client's process is gone (even if the disconnect arrived uncleanly — a force-quit or a dropped connection that never closed the socket), and it won't stay running indefinitely with clients attached but no activity. Together these guarantee it always winds down, on every platform. (#692)
3132
- A session no longer loses CodeGraph when the shared background server is restarted out from under it — for example when your MCP host (opencode and others) stops and restarts the server as you open another session. Previously the affected session's connection died silently and any request in flight at that moment hung; now CodeGraph keeps that session working by serving it locally, so the tools stay available without restarting the session. (#662)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* #618 — the "attached to shared daemon" line is benign INFO, but MCP hosts
3+
* render server stderr at error level (and tack on an `undefined` data field),
4+
* so on every session start a healthy attach showed up as `[error] … undefined`.
5+
* It's now gated behind CODEGRAPH_MCP_LOG_ATTACH=1 — silent by default, opt-in
6+
* for debugging. Approach from #640 by @mturac.
7+
*/
8+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
9+
import { logAttachedDaemon } from '../src/mcp/proxy';
10+
11+
const hello = { pid: 4242, codegraph: '9.9.9' } as any;
12+
13+
describe('daemon attach log gating (#618)', () => {
14+
let spy: ReturnType<typeof vi.spyOn>;
15+
16+
beforeEach(() => {
17+
spy = vi.spyOn(process.stderr, 'write').mockImplementation((() => true) as any);
18+
});
19+
20+
afterEach(() => {
21+
spy.mockRestore();
22+
delete process.env.CODEGRAPH_MCP_LOG_ATTACH;
23+
});
24+
25+
it('is silent by default (no [error]/undefined noise in MCP hosts)', () => {
26+
delete process.env.CODEGRAPH_MCP_LOG_ATTACH;
27+
logAttachedDaemon('/tmp/cg.sock', hello);
28+
expect(spy).not.toHaveBeenCalled();
29+
});
30+
31+
it('logs the attach line only when CODEGRAPH_MCP_LOG_ATTACH=1 (opt-in debug)', () => {
32+
process.env.CODEGRAPH_MCP_LOG_ATTACH = '1';
33+
logAttachedDaemon('/tmp/cg.sock', hello);
34+
const out = spy.mock.calls.map((c) => String(c[0])).join('');
35+
expect(out).toContain('Attached to shared daemon on /tmp/cg.sock');
36+
expect(out).toContain('pid 4242');
37+
});
38+
});

__tests__/mcp-daemon.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ function spawnServer(cwd: string, env: NodeJS.ProcessEnv = {}): SpawnedServer {
5252
const child = spawn(process.execPath, [BIN, 'serve', '--mcp'], {
5353
cwd,
5454
stdio: ['pipe', 'pipe', 'pipe'],
55-
env: { ...process.env, ...env },
55+
// #618: the daemon-attach log line is now off by default; opt the test
56+
// harness into it (CODEGRAPH_MCP_LOG_ATTACH=1) so the attach assertions
57+
// below can still observe a successful attach. A per-test env still wins.
58+
env: { CODEGRAPH_MCP_LOG_ATTACH: '1', ...process.env, ...env },
5659
}) as ChildProcessWithoutNullStreams;
5760
// Swallow spawn/EPIPE errors so killing a child mid-write can't surface as an
5861
// unhandled error that crashes the vitest worker.

src/mcp/proxy.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,26 @@ import type { MCPEngine } from './engine';
3232
/** Default poll cadence for the PPID watchdog (same as the direct server). */
3333
const DEFAULT_PPID_POLL_MS = 5000;
3434

35+
/**
36+
* Env var that opts INTO the "attached to shared daemon" log line. Off by
37+
* default: the line is benign INFO, but MCP hosts render any server stderr at
38+
* error level (and append an `undefined` data field), so on every session start
39+
* a healthy attach showed up as `[error] … undefined`. Set to `1` to surface it
40+
* when debugging daemon attach. (#618; approach from #640 by @mturac)
41+
*/
42+
const LOG_ATTACH_ENV = 'CODEGRAPH_MCP_LOG_ATTACH';
43+
44+
/**
45+
* Log a successful daemon attach — gated behind {@link LOG_ATTACH_ENV} so it is
46+
* silent by default (see #618). Exported for tests.
47+
*/
48+
export function logAttachedDaemon(socketPath: string, hello: DaemonHello): void {
49+
if (process.env[LOG_ATTACH_ENV] !== '1') return;
50+
process.stderr.write(
51+
`[CodeGraph MCP] Attached to shared daemon on ${socketPath} (pid ${hello.pid}, v${hello.codegraph}).\n`
52+
);
53+
}
54+
3555
export interface ProxyResult {
3656
/**
3757
* `proxied` — successfully attached to a same-version daemon and piped
@@ -89,9 +109,7 @@ export async function runProxy(
89109
return { outcome: 'fallback-needed', reason: 'version mismatch' };
90110
}
91111

92-
process.stderr.write(
93-
`[CodeGraph MCP] Attached to shared daemon on ${socketPath} (pid ${hello.pid}, v${hello.codegraph}).\n`
94-
);
112+
logAttachedDaemon(socketPath, hello);
95113

96114
sendClientHello(socket);
97115
startPpidWatchdog(socket);
@@ -130,9 +148,7 @@ export async function connectWithHello(
130148
socket.destroy();
131149
return 'version-mismatch';
132150
}
133-
process.stderr.write(
134-
`[CodeGraph MCP] Attached to shared daemon on ${socketPath} (pid ${hello.pid}, v${hello.codegraph}).\n`
135-
);
151+
logAttachedDaemon(socketPath, hello);
136152
sendClientHello(socket);
137153
return socket;
138154
}

0 commit comments

Comments
 (0)