Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
da5ccfb
fix: bound and clean up MCP session resources
lzehrung Aug 15, 2026
502d33d
fix: coalesce MCP index refreshes
lzehrung Aug 15, 2026
2369d84
fix: bound query index generation retries
lzehrung Aug 15, 2026
3550041
fix: bound MCP HTTP body reads
lzehrung Aug 15, 2026
d29e108
fix: bound MCP tool execution
lzehrung Aug 15, 2026
d7b6975
refactor: expose MCP tool registry
lzehrung Aug 15, 2026
2238224
fix: bound and cancel impact analysis streaming
lzehrung Aug 15, 2026
ba5bd67
fix: deadline raw SQLite query execution
lzehrung Aug 15, 2026
b6f1e9a
refactor: route omission counting through shared bound helper
Aug 15, 2026
e9b4352
test: cover MCP session resource bounds and teardown
Aug 15, 2026
c9b7678
fix: derive MCP dispatch from tool registry
lzehrung Aug 15, 2026
5565d11
fix: serialize MCP refresh requests
lzehrung Aug 16, 2026
e1ad8e0
fix: hide legacy MCP aliases from tool listing
lzehrung Aug 16, 2026
bd463d8
fix: bound raw MCP query worker cleanup
lzehrung Aug 16, 2026
17699a8
fix: bound MCP HTTP and SQLite execution
lzehrung Aug 16, 2026
07aa2c5
fix: repair MCP session bounds and SQLite deadline contract regressions
lzehrung Aug 16, 2026
d4fc335
Merge branch 'fix/mcp-bounds-and-regressions' into fix/mcp-session-re…
lzehrung Aug 16, 2026
0dd41a7
Merge branch 'fix/mcp-stream-and-sqlite-deadline' into fix/mcp-sessio…
lzehrung Aug 16, 2026
b7f18c1
fix: preserve MCP SQLite cancellation wiring
lzehrung Aug 16, 2026
3c965ad
test: correlate fallback deadline workload
lzehrung Aug 16, 2026
aa0f233
fix: bound MCP refresh retries
lzehrung Aug 16, 2026
f0bd0f9
fix: close rejected MCP body connections
lzehrung Aug 16, 2026
fe348e6
Merge remote-tracking branch 'origin/main' into fix/mcp-session-resou…
lzehrung Aug 16, 2026
cf4c565
style: fix prettier formatting in agent-explore and sqlite-query-boun…
lzehrung Aug 16, 2026
575d7c3
fix: repair main-merge test regressions for MCP tool registry and bun…
lzehrung Aug 16, 2026
94e734d
fix: guard session manager capacity/eviction normalizers against non-…
lzehrung Aug 16, 2026
09d74c3
fix: address remaining MCP resource-bound review findings
lzehrung Aug 16, 2026
b32ed6a
fix: retain warmup capacity during cancelled initialization
lzehrung Aug 16, 2026
50dab03
docs: refresh JavaScript coverage report
lzehrung Aug 16, 2026
3e83311
docs: refresh JavaScript coverage report
lzehrung Aug 16, 2026
a83db0c
fix: release capacity after failed warmup initialization
lzehrung Aug 16, 2026
9f18730
docs: refresh JavaScript coverage report
lzehrung Aug 16, 2026
3f190d2
fix: address review resource-bound findings
lzehrung Aug 17, 2026
7cf4bb8
docs: normalize coverage report formatting
lzehrung Aug 17, 2026
1b92956
fix: resolve latest resource review findings
lzehrung Aug 17, 2026
b4f75fd
fix: close MCP resources on shutdown
lzehrung Aug 17, 2026
6ecc827
fix: drain rejected MCP bodies before close
lzehrung Aug 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion codegraph-skill/codegraph/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ Sensitive-file rules:

If MCP tools are available, prefer them over repeated CLI invocations. Use `explore`, `orient`, `workspace_symbols`, `search`, `get_file`, `get_symbol`, `packet_get`, `goto`, `refs`, `rename_preview`, `refactor_plan`, `calls`, `type_hierarchy`, `implementations`, `file_deps`, `path`, `impact`, `review`, `query_sqlite`, `refresh_index`, and `artifact_build`; fall back to the CLI when MCP is unavailable. Legacy `callers`/`callees`, `supertypes`/`subtypes`, and `deps`/`rdeps` names remain valid `tools/call` aliases.

codegraph uses the official MCP SDK v2 to serve current 2026-07-28 clients while retaining compatibility with 2025-era clients. MCP protocol connections and HTTP protocol sessions keep separate transport state, but all share the server's one warm codegraph analysis session for the configured root. Tool schemas reject unknown fields, and idle HTTP protocol sessions are evicted with a bounded session count.
codegraph uses the official MCP SDK v2 to serve current 2026-07-28 clients while retaining compatibility with 2025-era clients. MCP protocol connections and HTTP protocol sessions keep separate transport state, but all share the server's one warm codegraph analysis session for the configured root. Tool schemas reject unknown fields, each protocol session caps tool concurrency at 4 with a retryable busy error, HTTP request bodies time out after 30 seconds, and idle HTTP protocol sessions are evicted with a bounded session count. Cancellation responds to the caller promptly but retains the occupied slot until shared work settles, so abandoned calls cannot bypass that resource bound. Concurrent `refresh_index` calls serialize and honor each request's requested warmup.
On the first `tools/call`, codegraph can emit `notifications/message` and, when the request includes `_meta.progressToken`, `notifications/progress` before the final result. Stdio carries them inline, and modern Streamable HTTP clients that accept `text/event-stream` receive them as a stream until the terminal result frame.
HTTP enforces Host and Origin policies. A missing `Origin` is accepted for non-browser clients; unapproved, malformed, and opaque origins are rejected. This is not authentication: binding `--host` to a non-loopback address exposes an unauthenticated endpoint intended only for trusted networks or containers.

Expand Down
10 changes: 10 additions & 0 deletions docs/agent-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,8 +306,16 @@ const sameSession = await manager.getOrCreateSession("pr-123", {
manager.cleanupExpired();
const allStats = manager.getAllStats();
console.log(Boolean(pr1Session), Boolean(pr2Session), Boolean(sameSession), allStats);

manager.dispose();
```

`SessionManager` defaults to 32 live or initializing sessions and scans for expired sessions every 60 seconds. Set `{ maxSessions, evictionIntervalMs }` to tune those bounds; `maxSessions` also applies to net-new `warmup()` sessions.

- Capacity frees immediately after a ready session is disposed or expires, or after a canceled or failed initialization settles.
- Set `evictionIntervalMs: 0` to disable periodic cleanup.
- Call `manager.dispose()` when the manager is no longer needed; it disposes all sessions, stops the interval, and is terminal. `disposeAll()` remains reusable.

## Streaming impact analysis

Stream impact results as they are discovered so the agent can start reasoning before the full pass completes:
Expand Down Expand Up @@ -337,6 +345,8 @@ for await (const chunk of analyzeImpactStreaming(root, index, {
}
```

Handle `error` as terminal: an overfull bounded queue does not emit `complete`. Breaking iteration or calling `.return()` cancels background work at its next analysis boundary; a synchronous lookup already in progress cannot be interrupted.

Use the same pattern through a warm session when repeated review passes matter:

```ts
Expand Down
8 changes: 4 additions & 4 deletions docs/coverage/js.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ Source: `coverage/js/lcov.info`

| Metric | Hit | Found | Coverage |
| --------- | ----: | ----: | -------: |
| Lines | 27353 | 30141 | 90.75% |
| Functions | 4559 | 4839 | 94.21% |
| Branches | 20655 | 26070 | 79.23% |
| Lines | 27596 | 30378 | 90.84% |
| Functions | 4608 | 4895 | 94.14% |
| Branches | 20774 | 26174 | 79.37% |

## Least-covered Files

| File | Lines | Functions | Branches |
| ---------------------------------------------- | -----: | --------: | -------: |
| `src/sqlite/rawQueryWorker.ts` | 0.00% | 0.00% | 0.00% |
| `src/languages/definitions/htmlStub.ts` | 50.00% | 50.00% | n/a |
| `src/languages/definitions/javascript.ts` | 62.50% | 83.33% | 57.89% |
| `src/cli/explore.ts` | 62.50% | 100.00% | 50.00% |
Expand All @@ -33,7 +34,6 @@ Source: `coverage/js/lcov.info`
| `src/agent/query-index/workerPool.ts` | 70.97% | 62.50% | 56.25% |
| `src/agent/followUps.ts` | 72.15% | 77.78% | 53.91% |
| `src/cli/context.ts` | 72.22% | 60.78% | 75.25% |
| `src/indexer/imports/languageSpecific.ts` | 73.97% | 100.00% | 63.74% |

## Type-Only Or Re-Export Files

Expand Down
15 changes: 12 additions & 3 deletions docs/library-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,8 @@ console.log(packet.kind, refs.references, rows.rows, rows.freshness.state);
```

`serveCodegraphMcp()` (from `@lzehrung/codegraph/mcp`) starts the stdio server used by `codegraph mcp serve`. MCP is an agent ergonomics and cache layer over the same analysis engine, not a separate indexer. MCP file and artifact paths are confined after realpath resolution.

`CodegraphMcpServerOptions.mcpToolConcurrency` caps concurrent calls per protocol session (default `4`); saturation returns a retryable busy error. `httpBodyTimeoutMs` bounds HTTP request-body receipt (default `30_000` ms), returning HTTP 408 on expiry. Concurrent `refresh_index` calls serialize and each applies its requested `warmup` after the preceding refresh completes. Client cancellation returns promptly but retains its concurrency slot until shared work settles, so cancellation cannot create unbounded background work.
`query_sqlite` is read-only and row- and byte-bounded. It returns freshness metadata for fresh artifact reads, refreshes codegraph-owned SQLite artifacts after small edits when write access is enabled, and rejects stale artifact queries it cannot refresh safely.
`artifact_build` is disabled by default and requires `readOnly: false` or CLI `--allow-build`; it refuses to write outputs from a stale MCP index until `refresh_index` succeeds. MCP `orient` and `packet_get` calls use the server-configured root; they do not accept per-request root overrides.

Expand Down Expand Up @@ -710,7 +712,12 @@ console.log(mermaid);
## Read-only SQL from code

```ts
import { queryGraphSqliteRaw } from "@lzehrung/codegraph-core";
import {
queryGraphSqliteRaw,
SqliteQueryCancelledError,
SqliteQueryDeadlineExceededError,
SqliteQueryWorkerCleanupCapacityExceededError,
} from "@lzehrung/codegraph-core";

const result = await queryGraphSqliteRaw(
"./codegraph.sqlite",
Expand All @@ -721,7 +728,9 @@ const result = await queryGraphSqliteRaw(
console.log(result.columns, result.rows);
```

`queryGraphSqliteRaw()` is intentionally read-only. It accepts result-producing statements such as `SELECT` and `PRAGMA` and rejects mutating SQL. Pass `{ maxRows }` to bound raw result rows.
`queryGraphSqliteRaw()` is intentionally read-only. It accepts result-producing statements such as `SELECT` and `PRAGMA`, rejects mutating SQL, and bounds rows, cells, response bytes, and `{ deadlineMs }`.

The 10-second default deadline rejects the caller promptly and requests worker termination. A native SQLite step already in progress can continue in a bounded cleanup slot until it returns; degraded installs without the worker asset use a weaker in-process check after each iterator step. Callers can catch the exported `SqliteQueryDeadlineExceededError`, `SqliteQueryCancelledError`, and `SqliteQueryWorkerCleanupCapacityExceededError`; cancellation is a stable generic message so it exposes no MCP client details.

## SQL artifact facts

Expand Down Expand Up @@ -928,7 +937,7 @@ Use the exported TypeScript APIs when another program is composing deterministic

- `buildReviewReport()` returns a review bundle with `schemaVersion`, changed files, changed symbols, `graphDelta`, candidate tests, `riskSummary`, `reviewTasks`, an offline `markdownLinks` result for Markdown sources in the analysis scope when there are changes, optional duplicate sibling-check tasks, optional `sqlContext`, compatibility hints when available, and diagnostics. Accepts an optional third argument, `{ index?, loadIndex?, duplicateAnalysis?, loadDuplicateAnalysis? }`, so a caller that already holds a warm `ProjectIndex` (or wants to defer loading it until review work actually needs it) and, for repeated review calls, a `DuplicatePreparedAnalysis` from `prepareDuplicateAnalysis()` can skip redundant rebuilds. The MCP `review` tool uses the lazy forms to avoid paying index or duplicate-analysis cost on no-change reviews.
- `analyzeImpactFromDiff()` returns the full or compact impact report shape for batch consumers, including an offline `markdownLinks` result for Markdown sources in the analysis scope when diffs are non-empty and changed-symbol `callCompatibility` hints when available.
- `analyzeImpactStreaming()` emits progress and incremental chunks, then a final `complete.report` summary. Streaming always returns `format: "stream-summary"`. By default this includes the same key structured fields needed by pack builders: changed files, changed symbols, impacted items, Markdown link findings, suggestions, export summaries, re-export chains, ranked top impacts, surface area, clusters, cycles, graph edges, diagnostics, and warning text. Set `streamSummary: "light"` to drop suggestions, export summaries, re-export chains, ranked top impacts, graph metadata, cycles, clusters, and surface area from the final report.
- `analyzeImpactStreaming()` emits progress and incremental chunks, then a final `complete.report` summary on success. Streaming always returns `format: "stream-summary"`. By default this includes the same key structured fields needed by pack builders: changed files, changed symbols, impacted items, Markdown link findings, suggestions, export summaries, re-export chains, ranked top impacts, surface area, clusters, cycles, graph edges, diagnostics, and warning text. Set `streamSummary: "light"` to drop suggestions, export summaries, re-export chains, ranked top impacts, graph metadata, cycles, clusters, and surface area from the final report. A bounded queue overflow instead emits terminal `error` without `complete`; ending iteration early cancels later analysis batches, but cannot interrupt a synchronous lookup already in progress.

Review-pack builders should preserve symbol handles, diff snippets, callsites, `callCompatibility`, diagnostics, candidate-test confidence, impact reasons, and graph edge metadata. Render prose only at the final UI or prompt boundary.

Expand Down
8 changes: 6 additions & 2 deletions docs/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ Stdio servers exit when the client closes stdin, when an IPC parent disconnects,

HTTP protocol sessions track last activity, cap concurrent legacy sessions (default 32), and evict idle sessions on a timer (default 30 minutes). Capacity and idle eviction skip sessions with in-flight requests or open SSE streams; when every slot is active, a new `initialize` receives an actionable JSON-RPC capacity error instead of evicting a working client. Transport errors and protocol session closes also remove the session.

Each MCP protocol session permits four concurrent tool calls by default; a saturated session returns a retryable busy error rather than queueing unbounded work. The programmatic server options `mcpToolConcurrency` and `httpBodyTimeoutMs` tune that cap and the 30-second HTTP request-body deadline; an HTTP body that misses its deadline receives `408 Request Timeout`.

Client cancellation returns promptly, but does not discard shared index or artifact work. A cancelled call continues to occupy its concurrency slot until its underlying operation settles, preventing a burst of abandoned requests from exceeding the configured resource bound.

Use stdio for a client-owned subprocess. Use HTTP for one long-running codegraph process per repository, then point every MCP-capable IDE, terminal, or agent client at the same local URL. Exact config keys vary by client, but the MCP settings should use HTTP/Streamable HTTP transport plus the `/mcp` URL instead of a `command`/`args` stdio launch.

codegraph uses the official MCP SDK v2 to serve current 2026-07-28 clients while retaining compatibility with 2025-era clients. MCP protocol connections and HTTP protocol sessions keep separate transport state, but all share the server's one warm codegraph analysis session for the configured root. Tool request schemas set `additionalProperties: false` and reject unknown fields with an actionable invalid-parameter error instead of silently ignoring typos.
Expand Down Expand Up @@ -100,7 +104,7 @@ Text and hybrid searches reuse a prepared handle for `.codegraph-cache/index-v1/

If the sidecar is busy or unavailable, MCP uses the same exact in-memory matcher. [How it works](./how-it-works.md#cache-and-session-behavior) explains the search cache.

Use `refresh_index` to rebuild the snapshot, reset SQLite artifact state, or recover after a change burst exceeds automatic limits. With write access, `query_sqlite` refreshes codegraph SQLite artifacts after small edits; otherwise it refuses stale rows. `artifact_build` refuses stale indexes, so run `refresh_index` after large change bursts.
Use `refresh_index` to rebuild the snapshot, reset SQLite artifact state, or recover after a change burst exceeds automatic limits. Concurrent refresh requests serialize; each request runs its own requested `warmup` (`off`, `base`, or `symbols`) after an active refresh completes. With write access, `query_sqlite` refreshes codegraph SQLite artifacts after small edits; otherwise it refuses stale rows. `artifact_build` refuses stale indexes, so run `refresh_index` after large change bursts.
`get_file` reads live bytes from disk after path confinement. It does not require a fresh index; only an explicit `includeGraphContext: true` checks indexed freshness and adds direct graph context, so returned file bytes and `totalLines` remain live even when `freshness` reports stale context.
Tool schemas are flat JSON objects for broad client compatibility; argument combinations such as `refs` handle-vs-position mode are validated by the server. Legacy paired names (`callers`, `callees`, `supertypes`, `subtypes`, `deps`, and `rdeps`) remain accepted by `tools/call` as aliases, but only the unified tools appear in `tools/list`.

Expand Down Expand Up @@ -215,7 +219,7 @@ An MCP `explore` request whose entire query resolves to an indexed project-relat
- Tool calls do not accept per-request root overrides.
- Tools are read-only by default.
- `artifact_build` requires `--allow-build` and a fresh or auto-refreshed MCP index.
- `query_sqlite` rejects mutating SQL, recursive queries, synthetic payload functions, and stale artifact queries it cannot refresh safely.
- `query_sqlite` rejects mutating SQL, recursive queries, synthetic payload functions, and stale artifact queries it cannot refresh safely. Each query has a 10-second execution deadline.
- `get_file` rejects raw reads and structural text-config summaries over the 16 MiB input limit. Accepted reads use separate output-page bounds from `maxBytes`, `offset`, and `limit`; binary input is rejected, and sensitive formats require `allowSensitive: true` for raw values.
- SQLite responses are row- and byte-bounded.

Expand Down
18 changes: 16 additions & 2 deletions scripts/bundle-cli-lib.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ export function getBundlePaths(rootDir = defaultRootDir) {
rootDir,
entryPoint: path.join(rootDir, "dist", "cliBootstrap.js"),
workerEntryPoint: path.join(rootDir, "dist", "agent", "query-index", "queryIndexWorker.js"),
rawQueryWorkerEntryPoint: path.join(rootDir, "dist", "sqlite", "rawQueryWorker.js"),
unbundledCli: path.join(rootDir, "dist", "cli.js"),
outdir: path.join(rootDir, "dist", "bin"),
bundledEntry: path.join(rootDir, "dist", "bin", "cli.js"),
bundledWorker: path.join(rootDir, "dist", "bin", "queryIndexWorker.js"),
bundledRawQueryWorker: path.join(rootDir, "dist", "bin", "rawQueryWorker.js"),
};
}

Expand All @@ -37,12 +39,21 @@ export async function bundleCli({ rootDir = defaultRootDir, logLevel = "warning"
if (!fs.existsSync(paths.workerEntryPoint)) {
throw new Error(`Missing query worker build input: ${paths.workerEntryPoint}. Run tsc before bundling.`);
}
if (!fs.existsSync(paths.rawQueryWorkerEntryPoint)) {
throw new Error(
`Missing raw SQLite worker build input: ${paths.rawQueryWorkerEntryPoint}. Run tsc before bundling.`,
);
}

fs.rmSync(paths.outdir, { recursive: true, force: true });
fs.mkdirSync(paths.outdir, { recursive: true });

const result = await esbuild.build({
entryPoints: { cli: paths.entryPoint, queryIndexWorker: paths.workerEntryPoint },
entryPoints: {
cli: paths.entryPoint,
queryIndexWorker: paths.workerEntryPoint,
rawQueryWorker: paths.rawQueryWorkerEntryPoint,
},
bundle: true,
platform: "node",
format: "esm",
Expand All @@ -59,7 +70,7 @@ export async function bundleCli({ rootDir = defaultRootDir, logLevel = "warning"
});

const outputFiles = Object.keys(result.metafile.outputs).sort();
const selfContainedEntries = new Set([paths.bundledEntry, paths.bundledWorker]);
const selfContainedEntries = new Set([paths.bundledEntry, paths.bundledWorker, paths.bundledRawQueryWorker]);
const unexpectedOutputs = outputFiles.filter((file) => !selfContainedEntries.has(path.resolve(file)));
if (unexpectedOutputs.length) {
throw new Error(
Expand All @@ -73,6 +84,9 @@ export async function bundleCli({ rootDir = defaultRootDir, logLevel = "warning"
if (!fs.existsSync(paths.bundledWorker)) {
throw new Error(`Bundled query worker was not written to ${paths.bundledWorker}`);
}
if (!fs.existsSync(paths.bundledRawQueryWorker)) {
throw new Error(`Bundled raw SQLite worker was not written to ${paths.bundledRawQueryWorker}`);
}

return {
...paths,
Expand Down
8 changes: 7 additions & 1 deletion scripts/ensure-dist-for-tests-lib.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import fs from "node:fs";
import path from "node:path";

const requiredDistEntries = ["dist/index.js", "dist/cli.js", "dist/bin/cli.js", "dist/bin/queryIndexWorker.js"];
const requiredDistEntries = [
"dist/index.js",
"dist/cli.js",
"dist/bin/cli.js",
"dist/bin/queryIndexWorker.js",
"dist/bin/rawQueryWorker.js",
];
const freshnessInputs = [
"package.json",
"tsconfig.json",
Expand Down
5 changes: 4 additions & 1 deletion scripts/stage-core-package-lib.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ export const CORE_PACKAGE_ENTRIES = Object.freeze([
"languages.js",
]);

export const CORE_PACKAGE_EXTRA_FILES = Object.freeze(["agent/query-index/queryIndexWorker.js"]);
export const CORE_PACKAGE_EXTRA_FILES = Object.freeze([
"agent/query-index/queryIndexWorker.js",
"sqlite/rawQueryWorker.js",
]);

const IMPORT_PATTERN =
/(?:import|export)\s+(?:type\s+)?(?:[^;]*?\s+from\s+)?["'](\.[^"']+)["']|import\(["'](\.[^"']+)["']\)/g;
Expand Down
Loading