Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion codegraph-skill/codegraph/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ Treat duplicate leads and call-compatibility hints as review leads, not proof.
## MCP

If MCP tools are available, prefer them over repeated CLI invocations.
Use MCP `explore`, `orient`, `search`, `packet_get`, `goto`, `refs`, `deps`, `rdeps`, `path`, `impact`, `review`, and `query_sqlite` first.
Use MCP `explore`, `orient`, `search`, `get_file`, `packet_get`, `goto`, `refs`, `deps`, `rdeps`, `path`, `impact`, `review`, and `query_sqlite` first.
Use `get_file` for bounded live file reads; its content uses exact `number<TAB>line` formatting and may include direct importers, imports, and symbols.
After edits, check MCP response `freshness`: `refreshed` means Codegraph rebuilt before answering, and `stale` includes a reason plus bounded changed-file metadata before indexed context is trusted.
Fall back to CLI when MCP is unavailable.

Expand Down
78 changes: 39 additions & 39 deletions docs/coverage/js.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,52 +4,52 @@ Source: `coverage/js/lcov.info`

## Summary

| Metric | Hit | Found | Coverage |
| --- | ---: | ---: | ---: |
| Lines | 19093 | 21175 | 90.17% |
| Functions | 3193 | 3399 | 93.94% |
| Branches | 14402 | 18543 | 77.67% |
| Metric | Hit | Found | Coverage |
| --------- | ----: | ----: | -------: |
| Lines | 19093 | 21175 | 90.17% |
| Functions | 3193 | 3399 | 93.94% |
| Branches | 14402 | 18543 | 77.67% |

## Least-covered Files

| File | Lines | Functions | Branches |
| --- | ---: | ---: | ---: |
| `src/cli/navigation.ts` | 44.64% | 50.00% | 46.88% |
| `src/languages/definitions/htmlStub.ts` | 50.00% | 50.00% | n/a |
| `src/cli/index.ts` | 54.29% | 33.33% | 48.84% |
| `src/languages/definitions/javascript.ts` | 62.50% | 83.33% | 55.56% |
| `src/util/projectFiles/parsers.ts` | 64.29% | 76.92% | 41.67% |
| `src/languages/definitions/typescript.ts` | 64.71% | 81.82% | 43.75% |
| `src/indexer/parsed-cache.ts` | 64.71% | 100.00% | 58.33% |
| `src/indexer.ts` | 66.67% | 66.67% | 100.00% |
| `src/languages/definitions/css.ts` | 66.67% | 0.00% | n/a |
| `src/languages/definitions/html.ts` | 66.67% | 0.00% | n/a |
| `src/languages/definitions/less.ts` | 66.67% | 0.00% | n/a |
| `src/languages/definitions/sql.ts` | 66.67% | 0.00% | n/a |
| `src/languages/definitions/svelte.ts` | 66.67% | 0.00% | n/a |
| `src/languages/definitions/vue.ts` | 66.67% | 0.00% | n/a |
| `src/drift/report.ts` | 67.44% | 100.00% | 46.58% |
| `src/cli/graph.ts` | 67.55% | 63.16% | 43.11% |
| `src/agent/handles.ts` | 68.00% | 91.67% | 43.18% |
| `src/mcp/server.ts` | 68.49% | 78.26% | 54.60% |
| `src/cli/context.ts` | 69.32% | 66.67% | 65.04% |
| `src/impact/testPatterns.ts` | 69.70% | 63.64% | 61.54% |
| File | Lines | Functions | Branches |
| ----------------------------------------- | -----: | --------: | -------: |
| `src/cli/navigation.ts` | 44.64% | 50.00% | 46.88% |
| `src/languages/definitions/htmlStub.ts` | 50.00% | 50.00% | n/a |
| `src/cli/index.ts` | 54.29% | 33.33% | 48.84% |
| `src/languages/definitions/javascript.ts` | 62.50% | 83.33% | 55.56% |
| `src/util/projectFiles/parsers.ts` | 64.29% | 76.92% | 41.67% |
| `src/languages/definitions/typescript.ts` | 64.71% | 81.82% | 43.75% |
| `src/indexer/parsed-cache.ts` | 64.71% | 100.00% | 58.33% |
| `src/indexer.ts` | 66.67% | 66.67% | 100.00% |
| `src/languages/definitions/css.ts` | 66.67% | 0.00% | n/a |
| `src/languages/definitions/html.ts` | 66.67% | 0.00% | n/a |
| `src/languages/definitions/less.ts` | 66.67% | 0.00% | n/a |
| `src/languages/definitions/sql.ts` | 66.67% | 0.00% | n/a |
| `src/languages/definitions/svelte.ts` | 66.67% | 0.00% | n/a |
| `src/languages/definitions/vue.ts` | 66.67% | 0.00% | n/a |
| `src/drift/report.ts` | 67.44% | 100.00% | 46.58% |
| `src/cli/graph.ts` | 67.55% | 63.16% | 43.11% |
| `src/agent/handles.ts` | 68.00% | 91.67% | 43.18% |
| `src/mcp/server.ts` | 68.49% | 78.26% | 54.60% |
| `src/cli/context.ts` | 69.32% | 66.67% | 65.04% |
| `src/impact/testPatterns.ts` | 69.70% | 63.64% | 61.54% |

## Type-Only Or Re-Export Files

These files have line records but no function or branch records, so they are tracked outside the runtime ranking.

| File | Lines | Functions | Branches |
| --- | ---: | ---: | ---: |
| `src/languages/definitions/jsFamily.ts` | 100.00% | n/a | n/a |
| `src/duplicate-keywords.ts` | 100.00% | n/a | n/a |
| `src/impact/types.ts` | 100.00% | n/a | n/a |
| `src/languages/definitions/adoc.ts` | 100.00% | n/a | n/a |
| `src/languages/definitions/astro.ts` | 100.00% | n/a | n/a |
| `src/languages/definitions/hbs.ts` | 100.00% | n/a | n/a |
| `src/languages/definitions/markdown.ts` | 100.00% | n/a | n/a |
| `src/languages/definitions/mdx.ts` | 100.00% | n/a | n/a |
| `src/languages/definitions/rst.ts` | 100.00% | n/a | n/a |
| `src/util/graphOnlyExtensions.ts` | 100.00% | n/a | n/a |
| File | Lines | Functions | Branches |
| --------------------------------------- | ------: | --------: | -------: |
| `src/languages/definitions/jsFamily.ts` | 100.00% | n/a | n/a |
| `src/duplicate-keywords.ts` | 100.00% | n/a | n/a |
| `src/impact/types.ts` | 100.00% | n/a | n/a |
| `src/languages/definitions/adoc.ts` | 100.00% | n/a | n/a |
| `src/languages/definitions/astro.ts` | 100.00% | n/a | n/a |
| `src/languages/definitions/hbs.ts` | 100.00% | n/a | n/a |
| `src/languages/definitions/markdown.ts` | 100.00% | n/a | n/a |
| `src/languages/definitions/mdx.ts` | 100.00% | n/a | n/a |
| `src/languages/definitions/rst.ts` | 100.00% | n/a | n/a |
| `src/util/graphOnlyExtensions.ts` | 100.00% | n/a | n/a |

Generated from LCOV by `node ./scripts/coverage-markdown.mjs`.
30 changes: 15 additions & 15 deletions docs/coverage/native.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,23 @@ Source: `coverage/native/lcov.info`

## Summary

| Metric | Hit | Found | Coverage |
| --- | ---: | ---: | ---: |
| Lines | 604 | 626 | 96.49% |
| Functions | 45 | 51 | 88.24% |
| Branches | 0 | 0 | n/a |
| Metric | Hit | Found | Coverage |
| --------- | --: | ----: | -------: |
| Lines | 604 | 626 | 96.49% |
| Functions | 45 | 51 | 88.24% |
| Branches | 0 | 0 | n/a |

## Least-covered Files

| File | Lines | Functions | Branches |
| --- | ---: | ---: | ---: |
| `packages/codegraph-native/src/lib.rs` | 89.47% | 70.00% | n/a |
| `packages/codegraph-native/src/parser_pool.rs` | 92.86% | 66.67% | n/a |
| `packages/codegraph-native/src/duplicate_tokens.rs` | 93.20% | 100.00% | n/a |
| `packages/codegraph-native/src/projection.rs` | 97.50% | 100.00% | n/a |
| `packages/codegraph-native/src/query.rs` | 99.31% | 92.86% | n/a |
| `packages/codegraph-native/src/duplicate_keywords.rs` | 100.00% | 100.00% | n/a |
| `packages/codegraph-native/src/languages.rs` | 100.00% | 100.00% | n/a |
| `packages/codegraph-native/src/types.rs` | 100.00% | 100.00% | n/a |
| File | Lines | Functions | Branches |
| ----------------------------------------------------- | ------: | --------: | -------: |
| `packages/codegraph-native/src/lib.rs` | 89.47% | 70.00% | n/a |
| `packages/codegraph-native/src/parser_pool.rs` | 92.86% | 66.67% | n/a |
| `packages/codegraph-native/src/duplicate_tokens.rs` | 93.20% | 100.00% | n/a |
| `packages/codegraph-native/src/projection.rs` | 97.50% | 100.00% | n/a |
| `packages/codegraph-native/src/query.rs` | 99.31% | 92.86% | n/a |
| `packages/codegraph-native/src/duplicate_keywords.rs` | 100.00% | 100.00% | n/a |
| `packages/codegraph-native/src/languages.rs` | 100.00% | 100.00% | n/a |
| `packages/codegraph-native/src/types.rs` | 100.00% | 100.00% | n/a |

Generated from LCOV by `node ./scripts/coverage-markdown.mjs`.
6 changes: 4 additions & 2 deletions docs/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ The server exposes the same bounded primitives as the CLI and library session la
- `orient`: compact first-turn repo context.
- `packet_get`: bounded evidence packet by file path, symbol name, SQL object name, or stable target.
- `search`: deterministic ranked search across paths, symbols, chunks, SQL objects, and graph context.
- `get_file`: bounded project file read.
- `get_file`: bounded project file read with `offset`/`limit` line pagination, exact `number<TAB>line` content, and optional direct graph context.
- `get_symbol`: resolve a stable search or explain handle.
- `goto`: definition lookup by file position.
- `refs`: references by handle or file position.
Expand All @@ -52,6 +52,7 @@ The server exposes the same bounded primitives as the CLI and library session la
MCP keeps one Codegraph session warm for the configured root. That makes follow-up calls cheaper than separate CLI invocations. Startup is lazy unless `--warmup` or `--warmup-symbols` is passed.
Before index-backed tool calls, MCP checks whether discovered files changed since the warm snapshot. Small changes refresh the session automatically, and responses include `freshness.state` as `fresh`, `refreshed`, or `stale`; stale responses also include `changedFileCount`, `omittedChangedFileCount`, and a bounded changed-file sample.
Use `refresh_index` when you need to force a rebuild, reset SQLite artifact state, or refresh after a change burst that exceeds the automatic refresh limits. `query_sqlite` refreshes Codegraph-owned SQLite artifacts after small edits when write access is enabled; otherwise it refuses to serve stale artifact rows.
`get_file` reads live bytes from disk after path confinement, then adds freshness metadata and graph context when the file is indexed. Its `content` field uses `lineFormat: "number-tab-line"`: no padding, decimal line number, tab, then source line.
Tool schemas are flat JSON objects for broad client compatibility; argument combinations such as `refs` handle-vs-position mode are validated by the server.

## Safety
Expand All @@ -61,6 +62,7 @@ Tool schemas are flat JSON objects for broad client compatibility; argument comb
- Tools are read-only by default.
- `artifact_build` requires `--allow-build`.
- `query_sqlite` rejects mutating SQL, recursive queries, synthetic payload functions, and stale artifact queries it cannot refresh safely.
- `get_file` reads are byte- and line-bounded with `maxBytes`, `offset`, and `limit`.
- SQLite responses are row- and byte-bounded.

## Installer
Expand Down Expand Up @@ -208,7 +210,7 @@ When Codegraph MCP tools are available to an agent:

1. Start with `explore` for a broad question.
2. Use `orient` when you need a compact first-turn map rather than a question answer.
3. Use `search` to find anchors and `packet_get`, `refs`, `goto`, `deps`, `rdeps`, or `path` for focused follow-up.
3. Use `search` to find anchors and `get_file`, `packet_get`, `refs`, `goto`, `deps`, `rdeps`, or `path` for focused follow-up.
4. Check `freshness` on MCP responses after edits; `refreshed` means the answer used an updated snapshot, and `stale` includes a reason plus a bounded changed-file sample.
5. Use `impact` and `review` for git-range risk analysis.
6. Use `query_sqlite` only for read-only artifact inspection; rebuild the artifact when it reports stale state.
Expand Down
128 changes: 123 additions & 5 deletions src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@ import {
} from "./sqliteGuard.js";
import {
DEFAULT_FILE_BYTES,
DEFAULT_FILE_LINES,
DEFAULT_MCP_COLLECTION_LIMIT,
listCodegraphMcpTools,
MAX_FILE_BYTES,
MAX_FILE_LINES,
MAX_MCP_COLLECTION_LIMIT,
MCP_TOOLS,
} from "./tools.js";
Expand Down Expand Up @@ -73,6 +75,100 @@ import {
} from "./security.js";

export type CodegraphMcpWarmupMode = "off" | "base" | "symbols";
type CodegraphMcpFileGraphContext = {
usedBy: string[];
imports: string[];
symbols: Array<{ name: string; kind: string; line: number }>;
};

type CodegraphMcpFileViewResponse = {
schemaVersion: 1;
file: string;
offset: number;
limit: number;
totalLines: number;
content: string;
lineFormat: "number-tab-line";
text: string;
truncated: boolean;
graphContext?: CodegraphMcpFileGraphContext;
page?: { nextOffset?: number };
};

function splitFileLines(text: string): string[] {
return text.split("\n");
}

function formatNumberedLines(lines: readonly string[], offset: number, limit: number): string {
const start = Math.max(0, offset - 1);
return lines
.slice(start, start + limit)
.map((line, index) => `${start + index + 1}\t${line}`)
.join("\n");
}

function uniqueSorted(values: Iterable<string>): string[] {
return Array.from(new Set(values)).sort((left, right) => left.localeCompare(right));
}

function buildFileGraphContext(
snapshot: AgentProjectSnapshot,
file: string,
relative: (file: string) => string,
): CodegraphMcpFileGraphContext | undefined {
const moduleIndex = snapshot.index.byFile.get(file);
if (!moduleIndex) return undefined;

const usedBy = uniqueSorted(
snapshot.fileGraph.edges
.filter((edge) => edge.to.type === "file" && edge.to.path === file)
.map((edge) => relative(edge.from)),
);
const imports = uniqueSorted(
moduleIndex.imports.map((importBinding) => {
const resolved = importBinding.resolved;
if (typeof resolved === "string") return relative(resolved);
if (resolved) return resolved.external;
return importBinding.from;
}),
);
const symbols = moduleIndex.locals
.map((symbol) => ({ name: symbol.localName, kind: symbol.kind, line: symbol.range.start.line }))
.sort((left, right) => left.line - right.line || left.name.localeCompare(right.name));

return { usedBy, imports, symbols };
}

function buildFileView(args: {
file: string;
text: string;
truncated: boolean;
offset: number;
limit: number;
graphContext?: CodegraphMcpFileGraphContext | undefined;
}): CodegraphMcpFileViewResponse {
const lines = splitFileLines(args.text);
const start = Math.max(0, args.offset - 1);
let nextOffset: number | undefined;
if (start + args.limit < lines.length) {
nextOffset = start + args.limit + 1;
} else if (args.truncated) {
nextOffset = lines.length + 1;
}
return {
schemaVersion: 1,
file: args.file,
offset: args.offset,
limit: args.limit,
totalLines: lines.length,
content: formatNumberedLines(lines, args.offset, args.limit),
lineFormat: "number-tab-line",
text: args.text,
truncated: args.truncated,
...(args.graphContext ? { graphContext: args.graphContext } : {}),
...(nextOffset ? { page: { nextOffset } } : {}),
};
}

export type CodegraphMcpHandlerOptions = {
root: string;
Expand Down Expand Up @@ -129,8 +225,11 @@ export type CodegraphMcpHandlers = {
}) => Promise<CodegraphMcpFreshResult<AgentPacketResponse>>;
get_file: (request: {
file: string;
offset?: number | undefined;
limit?: number | undefined;
maxBytes?: number | undefined;
}) => Promise<CodegraphMcpFreshResult<{ file: string; text: string; truncated: boolean }>>;
includeGraphContext?: boolean | undefined;
}) => Promise<CodegraphMcpFreshResult<CodegraphMcpFileViewResponse>>;
get_symbol: (request: { handle: string }) => Promise<CodegraphMcpFreshResult<AgentExplanation["target"]>>;
goto: (request: { file: string; line: number; column: number }) => Promise<CodegraphMcpFreshResult<GoToResult>>;
refs: (
Expand Down Expand Up @@ -266,6 +365,10 @@ function createCodegraphMcpHandlersForSession(
if (typeof limit !== "number" || !Number.isFinite(limit)) return fallback;
return Math.min(max, Math.max(0, Math.floor(limit)));
};
const boundedPositiveLimit = (limit: number | undefined, fallback: number, max: number): number => {
if (typeof limit !== "number" || !Number.isFinite(limit)) return fallback;
return Math.min(max, Math.max(1, Math.floor(limit)));
};
const staleFreshness = (files: readonly string[], reason: string): AgentFreshnessResult => {
const changedFiles = [...files].sort();
const boundedChangedFiles = changedFiles.slice(0, MAX_MCP_FRESHNESS_CHANGED_FILES);
Expand Down Expand Up @@ -500,14 +603,26 @@ function createCodegraphMcpHandlersForSession(

get_file: async (request) =>
await withFreshness(async () => {
const resolvedFile = await resolveReadableFile(await realRoot, root, request.file);
const maxBytes = boundedLimit(request.maxBytes, DEFAULT_FILE_BYTES, MAX_FILE_BYTES);
const rootRealPath = await realRoot;
const resolvedFile = await resolveReadableFile(rootRealPath, root, request.file);
const maxBytes = boundedPositiveLimit(request.maxBytes, DEFAULT_FILE_BYTES, MAX_FILE_BYTES);
const offset = boundedPositiveLimit(request.offset, 1, Number.MAX_SAFE_INTEGER);
const lineLimit = boundedPositiveLimit(request.limit, DEFAULT_FILE_LINES, MAX_FILE_LINES);
const read = await readFilePrefix(resolvedFile.realPath, maxBytes);
return {
let graphContext: CodegraphMcpFileGraphContext | undefined;
if (request.includeGraphContext ?? true) {
const projectFile = await resolveProjectFile(rootRealPath, root, request.file);
const snapshot = await session.loadProject({ symbolGraph: "skip" });
graphContext = buildFileGraphContext(snapshot, projectFile, relative);
}
return buildFileView({
file: resolvedFile.displayPath,
text: read.text,
truncated: read.truncated,
};
offset,
limit: lineLimit,
...(graphContext ? { graphContext } : {}),
});
}),

get_symbol: async (request) =>
Expand Down Expand Up @@ -994,7 +1109,10 @@ const packetGetSchema = z.object({

const getFileSchema = z.object({
file: z.string(),
offset: z.number().int().positive().optional(),
limit: z.number().int().positive().max(MAX_FILE_LINES).optional(),
maxBytes: z.number().int().positive().optional(),
includeGraphContext: z.boolean().optional(),
});

const handleSchema = z.object({
Expand Down
12 changes: 10 additions & 2 deletions src/mcp/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { DEFAULT_SQLITE_ROW_LIMIT, MAX_SQLITE_ROW_LIMIT } from "./sqliteGuard.js

export const DEFAULT_FILE_BYTES = 80_000;
export const MAX_FILE_BYTES = 500_000;
export const DEFAULT_FILE_LINES = 2000;
export const MAX_FILE_LINES = 10_000;
export const DEFAULT_MCP_COLLECTION_LIMIT = 100;
export const MAX_MCP_COLLECTION_LIMIT = 500;

Expand Down Expand Up @@ -84,9 +86,15 @@ export const MCP_TOOLS: Tool[] = [
},
{
name: "get_file",
description: "Read a bounded project file by relative path.",
description: "Read a bounded project file by relative path with line pagination and optional graph context.",
inputSchema: objectSchema(
{ file: stringProperty, maxBytes: { type: "integer", minimum: 1, maximum: MAX_FILE_BYTES } },
{
file: stringProperty,
offset: { type: "integer", minimum: 1, default: 1 },
limit: { type: "integer", minimum: 1, maximum: MAX_FILE_LINES, default: DEFAULT_FILE_LINES },
maxBytes: { type: "integer", minimum: 1, maximum: MAX_FILE_BYTES },
includeGraphContext: { type: "boolean" },
},
["file"],
),
},
Expand Down
Loading