feat(serve): --no-telemetry turns telemetry off for one MCP server entry

A team that shares an MCP config had no way to opt one server entry out of
telemetry without an environment variable, which not every client lets a
shared config set. Issue 1908 asked for a flag.

serve --no-telemetry sets CODEGRAPH_TELEMETRY=0 for the process, the same
chokepoint --no-watch uses for CODEGRAPH_NO_WATCH, so there is no new
telemetry code path: telemetry reads the variable on every check, and the
background daemon a server spawns inherits its environment. A daemon that is
already running keeps its own setting until it exits, which TELEMETRY.md
states; codegraph telemetry off covers every process.

Changes on top of the contributor's commit: the test spawns the server with
the WASM runtime flags, as AGENTS.md requires on Windows (without them the
re-exec'd grandchild keeps the temp dir open past the child's exit), and the
changelog bullet states the limit and credits the author.

Validated with real proxy-to-daemon runs on Windows against opted-in private
homes, reading the telemetry queue each daemon left on exit:
- a flagged client starts the daemon: nothing recorded;
- control, no flag: the tool call is recorded;
- a flagged client joins a running unflagged daemon: its call is recorded,
  the documented limit;
- an unflagged client joins a flagged daemon: nothing recorded;
- codegraph telemetry off with a daemon running: nothing recorded.

Original PR #1923 by @danusha2345.
Fixes #1908

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
danusha2345
2026-10-30 19:31:16 +00:00
committed by Colby Mchenry
co-authored by Claude Opus 5.5
parent 7889da6697
commit 47526d2078
4 changed files with 122 additions and 1 deletions
+2
View File
@@ -22,6 +22,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- `codegraph files --format package` and `codegraph_files` with `format: "package"` now list a project's files by the package that owns them, the nearest `package.json` or `composer.json` above each file that names one, so a monorepo splits into its workspace members with no setup and files nothing claims get a section of their own (#1871). Thanks @LuisAlbertoMK.
- CodeGraph's MCP server now gives agents and editors its display name and icon, so one that shows them, like the Codex app, lists CodeGraph by name and logo instead of a plain `codegraph`. Thanks @fedosov.
- `codegraph status` now shows how many of a language's files yielded symbols when that isn't all of them, and warns when none did, so a parser that isn't working no longer looks healthy, and `codegraph status --strict` exits non-zero on that or on an index that's missing, unfinished or out of date, so packaging and CI can gate on it (#1658). Thanks @ferrine.
- `codegraph serve --mcp --no-telemetry` turns telemetry off for one MCP server entry, so a shared MCP config can opt out without an environment variable; a background daemon that's already running for the project keeps its own setting until it exits. Thanks @danusha2345. (#1908)
### Fixes
+7
View File
@@ -20,6 +20,13 @@ export CODEGRAPH_TELEMETRY=0 # per-shell / per-CI override
export DO_NOT_TRACK=1 # the cross-tool standard — always honored
```
For one MCP server entry, add `--no-telemetry` to its arguments
(`"args": ["serve", "--mcp", "--no-telemetry"]`). It works like
`CODEGRAPH_TELEMETRY=0` for that server and for the background daemon it starts.
A daemon that is already running for the project, started by a server without
the flag, keeps its own setting until it exits; `codegraph telemetry off` covers
every process.
`codegraph telemetry status` shows the current state, what decided it, and your machine ID.
The interactive installer (`codegraph install`) asks up front with a visible default-on
toggle and never re-asks. If you never saw the installer (e.g. `npx` straight into `init`),
+106
View File
@@ -0,0 +1,106 @@
/**
* `codegraph serve --mcp --no-telemetry` records nothing (#1908).
*
* Drives the real stdio server (direct mode, a temp HOME holding an opted-in
* telemetry config, an unreachable endpoint so nothing leaves the machine) and
* reads the telemetry queue the server leaves behind on exit. The same run
* without the flag is the control: it must queue the tool call, or the
* flagged run's empty queue would prove nothing.
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { spawn } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { CodeGraph } from '../src';
import { WASM_RUNTIME_FLAGS } from '../src/extraction/wasm-runtime-flags';
const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js');
async function runOneToolCall(
home: string,
project: string,
extraArgs: string[],
): Promise<{ answered: boolean; queue: string }> {
const env: NodeJS.ProcessEnv = {
...process.env,
HOME: home,
USERPROFILE: home,
CODEGRAPH_NO_DAEMON: '1',
CODEGRAPH_NO_UPDATE_CHECK: '1',
CODEGRAPH_TELEMETRY_ENDPOINT: 'http://127.0.0.1:9/',
};
delete env.CODEGRAPH_TELEMETRY;
delete env.DO_NOT_TRACK;
const child = spawn(process.execPath, [...WASM_RUNTIME_FLAGS, BIN, 'serve', '--mcp', '--no-watch', '--path', project, ...extraArgs], {
env,
stdio: ['pipe', 'pipe', 'pipe'],
});
const exited = new Promise<void>((resolve) => child.on('exit', () => resolve()));
let out = '';
const answered = new Promise<void>((resolve) => {
child.stdout.on('data', (chunk) => {
out += chunk.toString('utf8');
if (out.includes('"id":2')) resolve();
});
});
const send = (msg: object) => child.stdin.write(JSON.stringify(msg) + '\n');
send({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'test', version: '0' } } });
send({ jsonrpc: '2.0', method: 'notifications/initialized' });
send({ jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'codegraph_explore', arguments: { query: 'greet' } } });
// A server that rejects the flag exits without answering; don't wait on it.
await Promise.race([answered, exited]);
child.stdin.end();
await exited;
const queue = path.join(home, '.codegraph', 'telemetry-queue.jsonl');
return {
answered: out.includes('"id":2'),
queue: fs.existsSync(queue) ? fs.readFileSync(queue, 'utf8') : '',
};
}
describe('serve --no-telemetry (#1908)', () => {
let root: string;
let project: string;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-no-telemetry-'));
project = path.join(root, 'project');
fs.mkdirSync(path.join(project, 'src'), { recursive: true });
fs.writeFileSync(path.join(project, 'src', 'a.ts'), 'export function greet() { return 1; }\n');
const cg = await CodeGraph.init(project, { index: true });
cg.close();
});
afterAll(() => {
fs.rmSync(root, { recursive: true, force: true });
});
function optedInHome(name: string): string {
const home = path.join(root, name);
fs.mkdirSync(path.join(home, '.codegraph'), { recursive: true });
fs.writeFileSync(
path.join(home, '.codegraph', 'telemetry.json'),
JSON.stringify({
enabled: true,
machine_id: 'test-machine',
consent_source: 'cli',
first_run_notice_shown: true,
updated_at: new Date().toISOString(),
}),
);
return home;
}
it('queues the tool call without the flag (control)', async () => {
const run = await runOneToolCall(optedInHome('home-on'), project, []);
expect(run.answered).toBe(true);
expect(run.queue).toContain('codegraph_explore');
}, 60_000);
it('queues nothing with --no-telemetry', async () => {
const run = await runOneToolCall(optedInHome('home-off'), project, ['--no-telemetry']);
expect(run.answered).toBe(true);
expect(run.queue).toBe('');
}, 60_000);
});
+7 -1
View File
@@ -2349,7 +2349,8 @@ program
.option('-p, --path <path>', 'Project path (optional for MCP mode, uses rootUri from client)')
.option('--mcp', 'Run as MCP server (stdio transport)')
.option('--no-watch', 'Disable the file watcher (no auto-sync; useful on slow filesystems like WSL2 /mnt drives)')
.action(async (options: { path?: string; mcp?: boolean; watch?: boolean }) => {
.option('--no-telemetry', 'Send no usage telemetry from this server (same as CODEGRAPH_TELEMETRY=0)')
.action(async (options: { path?: string; mcp?: boolean; watch?: boolean; telemetry?: boolean }) => {
const projectPath = options.path ? resolveProjectPath(options.path) : undefined;
// Commander sets watch=false when --no-watch is passed. Route it through
@@ -2357,6 +2358,11 @@ program
if (options.watch === false) {
process.env.CODEGRAPH_NO_WATCH = '1';
}
// Same for --no-telemetry (#1908): telemetry reads CODEGRAPH_TELEMETRY on
// every check, and a daemon this server spawns inherits the env.
if (options.telemetry === false) {
process.env.CODEGRAPH_TELEMETRY = '0';
}
try {
if (options.mcp) {