Procházet zdrojové kódy

fix(mcp): silence the daemon-attach log by default (#618) (#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 #640 by @mturac.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Colby Mchenry před 2 týdny
rodič
revize
10defecc4b

+ 1 - 0
CHANGELOG.md

@@ -26,6 +26,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ### Fixes
 
+- 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)
 - 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)
 - 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)
 - 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)

+ 38 - 0
__tests__/daemon-attach-log.test.ts

@@ -0,0 +1,38 @@
+/**
+ * #618 — the "attached to shared daemon" line is benign INFO, but MCP hosts
+ * render server stderr at error level (and tack on an `undefined` data field),
+ * so on every session start a healthy attach showed up as `[error] … undefined`.
+ * It's now gated behind CODEGRAPH_MCP_LOG_ATTACH=1 — silent by default, opt-in
+ * for debugging. Approach from #640 by @mturac.
+ */
+import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
+import { logAttachedDaemon } from '../src/mcp/proxy';
+
+const hello = { pid: 4242, codegraph: '9.9.9' } as any;
+
+describe('daemon attach log gating (#618)', () => {
+  let spy: ReturnType<typeof vi.spyOn>;
+
+  beforeEach(() => {
+    spy = vi.spyOn(process.stderr, 'write').mockImplementation((() => true) as any);
+  });
+
+  afterEach(() => {
+    spy.mockRestore();
+    delete process.env.CODEGRAPH_MCP_LOG_ATTACH;
+  });
+
+  it('is silent by default (no [error]/undefined noise in MCP hosts)', () => {
+    delete process.env.CODEGRAPH_MCP_LOG_ATTACH;
+    logAttachedDaemon('/tmp/cg.sock', hello);
+    expect(spy).not.toHaveBeenCalled();
+  });
+
+  it('logs the attach line only when CODEGRAPH_MCP_LOG_ATTACH=1 (opt-in debug)', () => {
+    process.env.CODEGRAPH_MCP_LOG_ATTACH = '1';
+    logAttachedDaemon('/tmp/cg.sock', hello);
+    const out = spy.mock.calls.map((c) => String(c[0])).join('');
+    expect(out).toContain('Attached to shared daemon on /tmp/cg.sock');
+    expect(out).toContain('pid 4242');
+  });
+});

+ 4 - 1
__tests__/mcp-daemon.test.ts

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

+ 22 - 6
src/mcp/proxy.ts

@@ -32,6 +32,26 @@ import type { MCPEngine } from './engine';
 /** Default poll cadence for the PPID watchdog (same as the direct server). */
 const DEFAULT_PPID_POLL_MS = 5000;
 
+/**
+ * Env var that opts INTO the "attached to shared daemon" log line. Off by
+ * default: the line is benign INFO, but MCP hosts render any server stderr at
+ * error level (and append an `undefined` data field), so on every session start
+ * a healthy attach showed up as `[error] … undefined`. Set to `1` to surface it
+ * when debugging daemon attach. (#618; approach from #640 by @mturac)
+ */
+const LOG_ATTACH_ENV = 'CODEGRAPH_MCP_LOG_ATTACH';
+
+/**
+ * Log a successful daemon attach — gated behind {@link LOG_ATTACH_ENV} so it is
+ * silent by default (see #618). Exported for tests.
+ */
+export function logAttachedDaemon(socketPath: string, hello: DaemonHello): void {
+  if (process.env[LOG_ATTACH_ENV] !== '1') return;
+  process.stderr.write(
+    `[CodeGraph MCP] Attached to shared daemon on ${socketPath} (pid ${hello.pid}, v${hello.codegraph}).\n`
+  );
+}
+
 export interface ProxyResult {
   /**
    * `proxied` — successfully attached to a same-version daemon and piped
@@ -89,9 +109,7 @@ export async function runProxy(
     return { outcome: 'fallback-needed', reason: 'version mismatch' };
   }
 
-  process.stderr.write(
-    `[CodeGraph MCP] Attached to shared daemon on ${socketPath} (pid ${hello.pid}, v${hello.codegraph}).\n`
-  );
+  logAttachedDaemon(socketPath, hello);
 
   sendClientHello(socket);
   startPpidWatchdog(socket);
@@ -130,9 +148,7 @@ export async function connectWithHello(
     socket.destroy();
     return 'version-mismatch';
   }
-  process.stderr.write(
-    `[CodeGraph MCP] Attached to shared daemon on ${socketPath} (pid ${hello.pid}, v${hello.codegraph}).\n`
-  );
+  logAttachedDaemon(socketPath, hello);
   sendClientHello(socket);
   return socket;
 }