Skip to content

Commit 3e03db7

Browse files
feat(mcp): codemap://files/{path} + codemap://symbols/{name} resources (#67)
§ 1.8 from research note. Two new MCP / HTTP resources reusing existing substrate (no schema bump): codemap://files/{path} - Per-file roll-up: {path, language, line_count, symbols, imports, exports, coverage}. - imports.specifiers parsed inline (callers don't need JSON.parse). - coverage is {measured_symbols, avg_coverage_pct, per_symbol} when ingested, else null. - URI-encode the path; query is fail-closed (returns undefined when path not in index — distinguishes "unknown URI" from "valid URI, empty roll-up"). codemap://symbols/{name} - Returns {matches, disambiguation?} envelope (reuses findSymbolsByName + buildShowResult from show-engine — same shape as `show <name>` per PR #39). - Optional ?in=<path-prefix> query parameter mirrors `show --in` (directory prefix or exact file). Uses the WHATWG URL parser. - Empty name → undefined; multi-match → disambiguation block. Caching policy: - Catalog-style resources (recipes, schema, skill) lazy-cache (existing pattern). - Data-shaped resources (files, symbols) read LIVE every call — no caching. Index can change between requests under --watch; caching would silently serve stale data. Both available over MCP read_resource AND HTTP GET /resources/ {encoded-uri} via the existing dispatcher (no new transport plumbing needed; readResource() switch extended). Tests: - 9 new tests in resource-handlers.test.ts cover both endpoints + listResources advertising. Pattern mirrors mcp-server.test.ts (use initCodemap + resolveCodemapConfig + tmpdir fixture). - bun run check passes (format, lint, typecheck, all 23 golden queries). Rule 10 lockstep: both templates/agents/ and .agents/ codemap rule + skill updated: - Rule body — Resources bullet extended with new URIs + caching policy. - Skill body — per-URI bullet for each new resource with shape spec. Patch changeset: pre-v1; additive resources; no schema bump; no breaking change to existing resource consumers. Files: 7 changed (1 src impl, 1 src test, 4 lockstep, 1 changeset).
1 parent f121d84 commit 3e03db7

7 files changed

Lines changed: 348 additions & 6 deletions

File tree

.agents/rules/codemap.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ Validation: SQL is rejected at load time if it starts with DML/DDL (DELETE/DROP/
6565
- **Tools:** `query` / `query_batch` / `query_recipe` / `audit` / `save_baseline` / `list_baselines` / `drop_baseline` / `context` / `validate` / `show` / `snippet` / `impact`. Snake_case keys (Codemap convention matching MCP spec examples + reference servers — spec is convention-agnostic; CLI stays kebab).
6666
- **`query_batch` (MCP-only):** N statements in one round-trip. Items are `string | {sql, summary?, changed_since?, group_by?}` — string form inherits batch-wide flag defaults, object form overrides on a per-key basis. Per-statement errors are isolated.
6767
- **`save_baseline` (polymorphic):** one tool, `{name, sql? | recipe?}` with runtime exclusivity check (mirrors the CLI's single `--save-baseline=<name>` verb).
68-
- **Resources:** `codemap://recipes` (catalog), `codemap://recipes/{id}` (one recipe), `codemap://schema` (live DDL from `sqlite_schema`), `codemap://skill` (bundled SKILL.md text). Lazy-cached on first `read_resource`.
68+
- **Resources:** `codemap://recipes` (catalog), `codemap://recipes/{id}` (one recipe), `codemap://schema` (live DDL from `sqlite_schema`), `codemap://skill` (bundled SKILL.md text), `codemap://files/{path}` (per-file roll-up: symbols, imports, exports, coverage), `codemap://symbols/{name}` (symbol lookup with `{matches, disambiguation?}` envelope; `?in=<path-prefix>` filter mirrors `show --in`). Catalog resources lazy-cache on first `read_resource`; `files/` and `symbols/` read live every call (no caching) since the index can change between requests under `--watch`.
6969
- **Output shape uniformity:** every tool returns the JSON envelope its CLI counterpart's `--json` would print — no re-mapping. Schema additions to the CLI envelope propagate to MCP automatically.
7070

7171
For developing the MCP server itself: `src/cli/cmd-mcp.ts` (CLI shell) + `src/application/mcp-server.ts` (engine). See [`docs/architecture.md` § MCP wiring](../../docs/architecture.md#cli-usage).

.agents/skills/codemap/SKILL.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ Each emitted delta carries its own `base` metadata so mixed-baseline audits are
8181
- **`codemap://recipes/{id}`** — single recipe `{id, description, body?, sql, actions?, source, shadows?}`. Replaces `--print-sql <id>`.
8282
- **`codemap://schema`** — DDL of every table in `.codemap/index.db` (queried live from `sqlite_schema`).
8383
- **`codemap://skill`** — full text of bundled `templates/agents/skills/codemap/SKILL.md`. Agents that don't preload the skill at session start can fetch it here.
84+
- **`codemap://files/{path}`** — per-file roll-up. Returns `{path, language, line_count, symbols, imports, exports, coverage}` where `imports.specifiers` is parsed JSON and `coverage` is `{measured_symbols, avg_coverage_pct, per_symbol}` or `null` when the file has no measured coverage. URI-encode the path. Reads live (no caching).
85+
- **`codemap://symbols/{name}`** — symbol lookup by exact name. Returns `{matches, disambiguation?}` envelope (same shape as `show <name>` per PR #39). Optional `?in=<path-prefix>` query parameter mirrors `show --in <path>` (directory prefix or exact file). Reads live.
8486

8587
**Implementation:** `src/cli/cmd-mcp.ts` (CLI shell — argv + lifecycle) + `src/application/mcp-server.ts` (transport — SDK glue). Tool bodies live in `src/application/tool-handlers.ts` (pure transport-agnostic — same handlers `codemap serve` dispatches over HTTP); resource fetchers in `src/application/resource-handlers.ts`. Mirrors the `cmd-audit.ts ↔ audit-engine.ts` seam. `--changed-since` git lookups are memoised per `(root, ref)` pair across batch items so a `query_batch` of N items sharing the same ref does one git invocation, not N.
8688

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"@stainless-code/codemap": patch
3+
---
4+
5+
feat(mcp): add `codemap://files/{path}` + `codemap://symbols/{name}` resources (research note § 1.8)
6+
7+
Two new MCP / HTTP resources for direct agent reads — saves the recipe-compose round-trip when the agent just wants "everything about this file" or "where is this symbol?".
8+
9+
- **`codemap://files/{path}`** — per-file roll-up. Returns `{path, language, line_count, symbols, imports, exports, coverage}`. `imports.specifiers` parsed inline (callers don't have to JSON.parse). `coverage` is `{measured_symbols, avg_coverage_pct, per_symbol}` when coverage was ingested, else `null`. URI-encode the path.
10+
- **`codemap://symbols/{name}`** — symbol lookup by exact name. Returns `{matches, disambiguation?}` envelope (same shape as the `show` verb per PR #39). Optional `?in=<path-prefix>` query parameter mirrors `show --in <path>` (directory prefix or exact file).
11+
12+
Both reuse existing infrastructure (no schema bump): `codemap://files/` queries the existing tables; `codemap://symbols/` reuses `findSymbolsByName` + `buildShowResult` from `application/show-engine.ts`.
13+
14+
**Caching policy:** catalog-style resources (`recipes`, `schema`, `skill`) lazy-cache as before. Data-shaped resources (`files/`, `symbols/`) read live every call — no caching, since the index can change between requests under `--watch`.
15+
16+
Both available over MCP `read_resource` and HTTP `GET /resources/{encoded-uri}` via the existing dispatcher (no new transport plumbing).
17+
18+
Agent rule + skill lockstep updated per `docs/README.md` Rule 10 — both `templates/agents/` and `.agents/` codemap rule + skill mention the new resource templates + caching policy.
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2+
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
3+
import { tmpdir } from "node:os";
4+
import { join } from "node:path";
5+
6+
import { resolveCodemapConfig } from "../config";
7+
import { closeDb, createTables, openDb } from "../db";
8+
import { initCodemap } from "../runtime";
9+
import {
10+
_resetResourceCachesForTests,
11+
listResources,
12+
readResource,
13+
} from "./resource-handlers";
14+
15+
let benchDir: string;
16+
17+
beforeEach(() => {
18+
benchDir = mkdtempSync(join(tmpdir(), "codemap-resource-test-"));
19+
mkdirSync(join(benchDir, "src"), { recursive: true });
20+
initCodemap(resolveCodemapConfig(benchDir, undefined));
21+
_resetResourceCachesForTests();
22+
23+
const db = openDb();
24+
try {
25+
createTables(db);
26+
db.run(
27+
`INSERT INTO files (path, content_hash, size, line_count, language, last_modified, indexed_at)
28+
VALUES
29+
('src/foo.ts', 'h1', 100, 30, 'ts', 1, 1),
30+
('src/bar.ts', 'h2', 80, 20, 'ts', 1, 1),
31+
('src/legacy/foo.ts', 'h3', 50, 15, 'ts', 1, 1)`,
32+
);
33+
db.run(
34+
`INSERT INTO symbols (file_path, name, kind, line_start, line_end, signature, is_exported, is_default_export)
35+
VALUES
36+
('src/foo.ts', 'foo', 'function', 5, 15, 'function foo(): void', 1, 0),
37+
('src/foo.ts', 'helper', 'function', 20, 25, 'function helper(): string', 0, 0),
38+
('src/legacy/foo.ts', 'foo', 'function', 1, 50, 'function foo(arg: string): number', 0, 0)`,
39+
);
40+
db.run(
41+
`INSERT INTO imports (file_path, source, resolved_path, specifiers, is_type_only, line_number)
42+
VALUES ('src/foo.ts', './bar', 'src/bar.ts', '["bar"]', 0, 1)`,
43+
);
44+
db.run(
45+
`INSERT INTO exports (file_path, name, kind, is_default)
46+
VALUES ('src/foo.ts', 'foo', 'value', 0)`,
47+
);
48+
} finally {
49+
closeDb(db);
50+
}
51+
});
52+
53+
afterEach(() => {
54+
rmSync(benchDir, { recursive: true, force: true });
55+
_resetResourceCachesForTests();
56+
});
57+
58+
describe("readResource — codemap://files/{path}", () => {
59+
it("returns per-file roll-up with symbols / imports / exports", () => {
60+
const r = readResource("codemap://files/src/foo.ts");
61+
expect(r).toBeDefined();
62+
expect(r?.mimeType).toBe("application/json");
63+
const payload = JSON.parse(r!.text);
64+
expect(payload.path).toBe("src/foo.ts");
65+
expect(payload.language).toBe("ts");
66+
expect(payload.symbols).toHaveLength(2);
67+
expect(payload.imports).toHaveLength(1);
68+
expect(payload.imports[0].specifiers).toEqual(["bar"]);
69+
expect(payload.exports).toHaveLength(1);
70+
expect(payload.coverage).toBeNull();
71+
});
72+
73+
it("returns undefined when path not in the index", () => {
74+
expect(readResource("codemap://files/no-such-file.ts")).toBeUndefined();
75+
});
76+
77+
it("URI-decodes the path", () => {
78+
const r = readResource("codemap://files/src%2Flegacy%2Ffoo.ts");
79+
expect(r).toBeDefined();
80+
expect(JSON.parse(r!.text).path).toBe("src/legacy/foo.ts");
81+
});
82+
});
83+
84+
describe("readResource — codemap://symbols/{name}", () => {
85+
it("returns single match envelope when the name is unique", () => {
86+
const r = readResource("codemap://symbols/helper");
87+
expect(r).toBeDefined();
88+
const payload = JSON.parse(r!.text);
89+
expect(payload.matches).toHaveLength(1);
90+
expect(payload.matches[0].name).toBe("helper");
91+
expect(payload.disambiguation).toBeUndefined();
92+
});
93+
94+
it("returns disambiguation envelope on multi-match", () => {
95+
const r = readResource("codemap://symbols/foo");
96+
expect(r).toBeDefined();
97+
const payload = JSON.parse(r!.text);
98+
expect(payload.matches).toHaveLength(2);
99+
expect(payload.disambiguation).toBeDefined();
100+
expect(payload.disambiguation.n).toBe(2);
101+
expect(payload.disambiguation.files).toContain("src/foo.ts");
102+
expect(payload.disambiguation.files).toContain("src/legacy/foo.ts");
103+
});
104+
105+
it("filters by ?in=<path-prefix>", () => {
106+
const r = readResource("codemap://symbols/foo?in=src/legacy");
107+
expect(r).toBeDefined();
108+
const payload = JSON.parse(r!.text);
109+
expect(payload.matches).toHaveLength(1);
110+
expect(payload.matches[0].file_path).toBe("src/legacy/foo.ts");
111+
});
112+
113+
it("returns empty matches for unknown name", () => {
114+
const r = readResource("codemap://symbols/no-such-symbol");
115+
expect(r).toBeDefined();
116+
expect(JSON.parse(r!.text).matches).toEqual([]);
117+
});
118+
119+
it("returns undefined when name is empty", () => {
120+
expect(readResource("codemap://symbols/")).toBeUndefined();
121+
});
122+
});
123+
124+
describe("listResources", () => {
125+
it("advertises the new files / symbols templates", () => {
126+
const list = listResources();
127+
const uris = list.map((r) => r.uri);
128+
expect(uris).toContain("codemap://files/{path}");
129+
expect(uris).toContain("codemap://symbols/{name}");
130+
});
131+
});

0 commit comments

Comments
 (0)