Skip to content
Merged
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
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,11 @@ Try it immediately on the bundled example: `node packages/cli/dist/index.js scan

| Package | Purpose |
|---------|---------|
| [`@coderadar/core`](packages/core) | The `LineageGraph` schema + query primitives (`matchComponentsByText`, `traceLineage`) |
| [`@coderadar/parser-react`](packages/parser-react) | ts-morph static parser — components (incl. `memo`/`forwardRef`), hooks, `fetch`/`axios`/react-query/SWR endpoints, `useState`/`useSelector`/`useContext`, JSX event handlers, rendered-text extraction |
| [`@coderadar/cli`](packages/cli) | `coderadar scan` · `find` · `trace` |
| [`@coderadar/core`](packages/core) | The `LineageGraph` schema + query primitives (`matchComponents`, `traceLineage`, `journeys`, `blastRadius`) |
| [`@coderadar/parser-react`](packages/parser-react) | ts-morph static parser — components (incl. `memo`/`forwardRef`), hooks, `fetch`/`axios`/react-query/SWR endpoints, `useState`/`useSelector`/`useContext`, JSX event handlers, rendered-text extraction, test coverage, response types |
| [`@coderadar/agent-sdk`](packages/agent-sdk) | `resolveContext` / `buildBundle` — classifies a ticket and assembles a budgeted context bundle (deterministic, no LLM) |
| [`@coderadar/mcp`](packages/mcp) | MCP server exposing `resolve_context` · `find_component` · `trace_lineage` · `journeys` · `blast_radius` over stdio |
| [`ui-lineage`](packages/cli) | Published CLI + library bundle: `scan` · `find` · `trace` · `journeys` · `impact` · `resolve` · `bundle`, plus the `ui-lineage-mcp` server bin |

### What the graph captures

Expand Down
13 changes: 7 additions & 6 deletions TRACKER.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

## Status

- **Current phase:** 5Context bundle & agent interface
- **Next step:** 5.7 — MCP server (→ Gate 5)
- **Done:** 0.1–0.4, 1.1–1.6, 2.1–2.5, 3.1–3.6, 4.1–4.6, 5.1–5.6
- **Gates passed:** Gate 0 (CI + red-path, #5/#6) · Gate 1 (precision 1.000, recall 0.895, zero poison) · Gate 2 (C1 instance attribution 1.000 · B1 4-level handler chains · C6 store writers↔readers · A9 portals — scorecard 137/0/0, precision & recall 1.000) · Gate 3 (B3 action effects · B4 routers · B6 cyclic journeys terminate · B7/B8 form & non-JSX events · G5 flag/role conditions — precision & recall 1.000) · Gate 4 (A4 rarity · A10 fuzzy/OCR · A1 structural · A6 subtree · E3 vision annotations · E2 aliases · G4 corrections — high-conf correct 1.000, ambiguity honesty 1.000, poison rate 0.000)
- **Current phase:** 6Lifecycle, scale, hardening
- **Next step:** 6.1 (Phase 5 complete — Gate 5 passed, M5 reached)
- **Done:** 0.1–0.4, 1.1–1.6, 2.1–2.5, 3.1–3.6, 4.1–4.6, 5.1–5.7
- **Gates passed:** Gate 0 (CI + red-path, #5/#6) · Gate 1 (precision 1.000, recall 0.895, zero poison) · Gate 2 (C1 instance attribution 1.000 · B1 4-level handler chains · C6 store writers↔readers · A9 portals — scorecard 137/0/0, precision & recall 1.000) · Gate 3 (B3 action effects · B4 routers · B6 cyclic journeys terminate · B7/B8 form & non-JSX events · G5 flag/role conditions — precision & recall 1.000) · Gate 4 (A4 rarity · A10 fuzzy/OCR · A1 structural · A6 subtree · E3 vision annotations · E2 aliases · G4 corrections — high-conf correct 1.000, ambiguity honesty 1.000, poison rate 0.000) · Gate 5 (F1 context bundle · F2 blast radius · F3 test coverage · F4 response schema · F5 git history · MCP server over stdio — scorecard 265/0/0, all honesty metrics 1.000; **M5 reached** — ticket in → budgeted context bundle out, over MCP)

## What CodeRadar is

Expand Down Expand Up @@ -303,10 +303,11 @@ The heart of the project. C1 and B1 live here.
**Accept:** integration test on this repo's own history; graceful empty section outside a git repo.
**Done:** `gitHistory(root, files, limit)` in agent-sdk — per-file `git log --follow` (via `execFileSync`, no network), commits deduped across files and sorted newest-first by committer time, `parsePrNumber` pulls `#NN` from merge/squash subjects. Any failure (not a repo, git missing, untracked path) yields `[]` — the bundle degrades gracefully. Bundle `history` section (default 5) is populated from the matched component's files + render subtree; `BundleCommit` gains optional `pr` (context-bundle schema regenerated, drift gate green). Integration tests run against this repo's own history; 5 agent-sdk unit tests + a bundle-wiring test. eval 265/0/0, gate OK.

### [ ] 5.7 MCP server
### [x] 5.7 MCP server
**Failure modes:** G1 (surface), pipeline integration
**Build:** `@coderadar/mcp` exposing tools: `resolve_context(ticket)`, `find_component(terms)`, `trace_lineage(id)`, `journeys(id, depth)`, `blast_radius(id)` — thin wrappers over agent-sdk against a pre-built graph (path from env/config). Tool descriptions written for agent consumption (when to use which, what `ambiguous` means, that `disambiguation` should be relayed to a human).
**Accept:** MCP integration test via stdio client: scan fixture → each tool returns schema-valid envelopes; `ambiguous`/`declined` statuses round-trip. **Gate 5 passes.** *CodeRadar is now pluggable into the multi-agent system.*
**Done:** new `@coderadar/mcp` package (MCP SDK 1.29). `createServer(graph)` registers all five tools over `@modelcontextprotocol/sdk`'s `McpServer` with zod input schemas and agent-facing descriptions (`resolve_context` returns the full budgeted `ContextBundle`; each tool returns its QueryResult envelope as JSON text; descriptions state that `ambiguous`/`disambiguation` goes to a human). `resolve_context` = `buildBundle`; `find_component`/`trace_lineage`/`journeys`/`blast_radius` wrap core. Bin `coderadar-mcp` loads a graph from `CODERADAR_GRAPH` (or argv) and serves stdio; also shipped in the published `ui-lineage` bundle as the `ui-lineage-mcp` bin (MCP SDK + zod kept external). Integration test via a real stdio `Client`: lists all five tools and round-trips `ok`/`ambiguous`/`declined` over `a4-generic-text` (6 tests). **Gate 5 passed. M5 reached — ticket in → budgeted context bundle out, over MCP.**

---

Expand Down Expand Up @@ -368,6 +369,6 @@ Sketch level — detail before starting the phase, after v1 feedback.
| M2 | **Headline correct:** per-instance API attribution (C1) + handler resolution (B1) | 2 |
| M3 | n-level journeys, lazily expanded | 3 |
| M4 | Screenshot/text → ranked, calibrated, honest matches | 4 |
| M5 | **Pluggable node:** ticket in → budgeted context bundle out, over MCP | 5 |
| M5 | **Pluggable node:** ticket in → budgeted context bundle out, over MCP | 5 |
| M6 | Production-grade: incremental, fast, deterministic, versioned | 6 |
| M7 | Full-stack lineage: pixel → backend handler | 7 |
24 changes: 24 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,30 @@ its distance — so a change can be reviewed for what it might break:
▸ /users • onClick() ⇢ fetch /api/users
```

## MCP server

`ui-lineage` also ships an MCP (Model Context Protocol) server, so an agent can
query a pre-built graph over stdio. Build a graph, then point an MCP client at
the `ui-lineage-mcp` bin with the graph path in `CODERADAR_GRAPH`:

```jsonc
// e.g. an MCP client config
{
"mcpServers": {
"coderadar": {
"command": "ui-lineage-mcp",
"env": { "CODERADAR_GRAPH": "/abs/path/to/app.graph.json" }
}
}
}
```

Tools: `resolve_context(ticket)` (→ a budgeted context bundle), `find_component(terms)`,
`trace_lineage(id)`, `journeys(start, depth?)`, `blast_radius(node, depth?)`. Every
tool returns a QueryResult envelope — ranked candidates with confidence, or an
honest `ambiguous` / `declined`. There is no LLM in the server; it is a
deterministic context provider.

## Library

```ts
Expand Down
8 changes: 6 additions & 2 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"license": "MIT",
"type": "module",
"bin": {
"ui-lineage": "dist/index.js"
"ui-lineage": "dist/index.js",
"ui-lineage-mcp": "dist/mcp.js"
},
"main": "dist/lib.js",
"module": "dist/lib.js",
Expand Down Expand Up @@ -45,13 +46,16 @@
"prepublishOnly": "pnpm build"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.0",
"commander": "^13.0.0",
"ts-morph": "^24.0.0",
"yaml": "^2.9.0"
"yaml": "^2.9.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@coderadar/agent-sdk": "workspace:*",
"@coderadar/core": "workspace:*",
"@coderadar/mcp": "workspace:*",
"@coderadar/parser-react": "workspace:*",
"@coderadar/vision": "workspace:*",
"@types/node": "^22.20.1",
Expand Down
39 changes: 39 additions & 0 deletions packages/cli/src/mcp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env node
/**
* `ui-lineage-mcp` — the CodeRadar MCP server as a self-contained bin.
*
* Serves a pre-built lineage graph over stdio so an MCP-speaking agent can call
* resolve_context / find_component / trace_lineage / journeys / blast_radius.
* Build the graph first: `ui-lineage scan <dir> -o app.graph.json`, then point a
* client at `ui-lineage-mcp` with CODERADAR_GRAPH=app.graph.json (or pass the
* path as the first argument).
*/

import { loadGraph } from "@coderadar/core";
import { createServer } from "@coderadar/mcp";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

async function main(): Promise<void> {
const graphPath = process.env["CODERADAR_GRAPH"] ?? process.argv[2];
if (graphPath === undefined) {
process.stderr.write(
"ui-lineage-mcp: set CODERADAR_GRAPH (or pass a path) to a graph built with `ui-lineage scan`.\n",
);
process.exit(1);
}
let graph;
try {
graph = loadGraph(graphPath);
} catch (error) {
process.stderr.write(`ui-lineage-mcp: cannot load ${graphPath}: ${String(error)}\n`);
process.exit(1);
}
const server = createServer(graph);
await server.connect(new StdioServerTransport());
process.stderr.write(`ui-lineage-mcp ready — ${graph.nodes.length} nodes from ${graphPath}\n`);
}

main().catch((error: unknown) => {
process.stderr.write(`ui-lineage-mcp: fatal ${String(error)}\n`);
process.exit(1);
});
3 changes: 2 additions & 1 deletion packages/cli/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { defineConfig } from "tsup";
export default defineConfig({
entry: {
index: "src/index.ts", // CLI bin (keeps its #!/usr/bin/env node shebang)
mcp: "src/mcp.ts", // ui-lineage-mcp bin (MCP stdio server)
lib: "src/lib.ts", // library entry
vision: "src/vision.ts", // ui-lineage/vision subpath
},
Expand All @@ -18,5 +19,5 @@ export default defineConfig({
sourcemap: true,
noExternal: [/^@coderadar\//],
// @anthropic-ai/sdk is a lazy, optional runtime dep of the vision adapter.
external: ["ts-morph", "yaml", "commander", "@anthropic-ai/sdk"],
external: ["ts-morph", "yaml", "commander", "@anthropic-ai/sdk", "@modelcontextprotocol/sdk", "zod"],
});
39 changes: 39 additions & 0 deletions packages/mcp/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "@coderadar/mcp",
"version": "0.2.0",
"private": true,
"description": "Model Context Protocol server exposing CodeRadar as a context-provider node: resolve_context, find_component, trace_lineage, journeys, blast_radius over a pre-built graph.",
"license": "MIT",
"type": "module",
"main": "dist/server.js",
"types": "dist/server.d.ts",
"exports": {
".": {
"types": "./dist/server.d.ts",
"default": "./dist/server.js"
}
},
"bin": {
"coderadar-mcp": "dist/index.js"
},
"files": [
"dist"
],
"scripts": {
"build": "tsc -p tsconfig.json",
"typecheck": "tsc -p tsconfig.json --noEmit",
"test": "vitest run"
},
"dependencies": {
"@coderadar/agent-sdk": "workspace:*",
"@coderadar/core": "workspace:*",
"@modelcontextprotocol/sdk": "^1.12.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@coderadar/parser-react": "workspace:*",
"@types/node": "^22.20.1",
"typescript": "^5.7.0",
"vitest": "^3.2.7"
}
}
40 changes: 40 additions & 0 deletions packages/mcp/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env node
/**
* CodeRadar MCP stdio entry point.
*
* Loads a pre-built lineage graph (path from the CODERADAR_GRAPH env var, or the
* first CLI argument) and serves it over stdio. Build the graph first with
* `ui-lineage scan <dir> -o app.graph.json`, then point this server at it.
*/

import { loadGraph } from "@coderadar/core";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

import { createServer } from "./server.js";

async function main(): Promise<void> {
const graphPath = process.env["CODERADAR_GRAPH"] ?? process.argv[2];
if (graphPath === undefined) {
process.stderr.write(
"CodeRadar MCP: set CODERADAR_GRAPH (or pass a path) to a graph built with `ui-lineage scan`.\n",
);
process.exit(1);
}

let graph;
try {
graph = loadGraph(graphPath);
} catch (error) {
process.stderr.write(`CodeRadar MCP: cannot load ${graphPath}: ${String(error)}\n`);
process.exit(1);
}

const server = createServer(graph);
await server.connect(new StdioServerTransport());
process.stderr.write(`CodeRadar MCP ready — ${graph.nodes.length} nodes from ${graphPath}\n`);
}

main().catch((error: unknown) => {
process.stderr.write(`CodeRadar MCP: fatal ${String(error)}\n`);
process.exit(1);
});
106 changes: 106 additions & 0 deletions packages/mcp/src/mcp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";

import { saveGraph } from "@coderadar/core";
import { resolveHookEdges, scanReact } from "@coderadar/parser-react";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { afterAll, beforeAll, describe, expect, it } from "vitest";

const here = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(here, "../../..");
const fixtureApp = path.join(repoRoot, "eval/fixtures/a4-generic-text/app");
const serverEntry = path.join(here, "../dist/index.js");

let client: Client;
let transport: StdioClientTransport;
let graphPath: string;

// Parse the single text-content envelope a tool returns.
async function call(name: string, args: Record<string, unknown>): Promise<Record<string, unknown>> {
const result = (await client.callTool({ name, arguments: args })) as {
content: { type: string; text: string }[];
};
const text = result.content.find((c) => c.type === "text")?.text ?? "{}";
return JSON.parse(text) as Record<string, unknown>;
}

beforeAll(async () => {
// Build a real graph from a fixture and persist it for the server to load.
const graph = resolveHookEdges(scanReact({ root: fixtureApp }));
graphPath = path.join(fs.mkdtempSync(path.join(os.tmpdir(), "coderadar-mcp-")), "graph.json");
saveGraph(graph, graphPath);

client = new Client({ name: "coderadar-mcp-test", version: "0.0.0" });
transport = new StdioClientTransport({
command: process.execPath,
args: [serverEntry],
env: { ...process.env, CODERADAR_GRAPH: graphPath },
});
await client.connect(transport);
}, 30_000);

afterAll(async () => {
await client?.close();
});

describe("CodeRadar MCP server (TRACKER 5.7, Gate 5)", () => {
it("advertises all five tools", async () => {
const { tools } = await client.listTools();
expect(tools.map((t) => t.name).sort()).toEqual([
"blast_radius",
"find_component",
"journeys",
"resolve_context",
"trace_lineage",
]);
// Descriptions are written for the calling agent.
expect(tools.find((t) => t.name === "resolve_context")?.description).toMatch(/bundle/i);
});

it("resolve_context returns a matched context bundle for a UI ticket", async () => {
const bundle = await call("resolve_context", {
ticket: "the Card number field on the billing form is broken",
});
expect(bundle["status"]).toBe("matched");
expect(Array.isArray(bundle["match"])).toBe(true);
expect(bundle["budget"]).toBeDefined();
});

it("resolve_context declines an out-of-domain (backend) ticket", async () => {
const bundle = await call("resolve_context", {
ticket: "the SQL migration on the orders table failed in production",
});
expect(bundle["status"]).toBe("declined");
});

it("find_component round-trips ok / ambiguous / declined envelopes", async () => {
const ok = await call("find_component", { terms: ["Card number"] });
expect(ok["status"]).toBe("ok");
expect(Array.isArray(ok["candidates"])).toBe(true);

const ambiguous = await call("find_component", { terms: ["Save"] });
expect(ambiguous["status"]).toBe("ambiguous");
expect(typeof ambiguous["disambiguation"]).toBe("string");

const declined = await call("find_component", { terms: ["zzqqnope"] });
expect(declined["status"]).toBe("declined");
expect(declined["declineReason"]).toBe("no-signal");
});

it("trace_lineage resolves a component name and declines unknown ids", async () => {
const ok = await call("trace_lineage", { id: "BillingForm" });
expect(ok["status"]).toBe("ok");
const missing = await call("trace_lineage", { id: "NoSuchComponent" });
expect(missing["status"]).toBe("declined");
expect(missing["declineReason"]).toBe("not-found");
});

it("journeys and blast_radius return ok envelopes, declining unknown targets", async () => {
expect((await call("journeys", { start: "BillingForm" }))["status"]).toBe("ok");
expect((await call("blast_radius", { node: "BillingForm" }))["status"]).toBe("ok");
expect((await call("blast_radius", { node: "NoSuchNode" }))["status"]).toBe("declined");
});
});
Loading
Loading