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
2 changes: 2 additions & 0 deletions docs/mcp-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

17 tools available via MCP stdio.

Operation tools return JSON text payloads. Invalid operation inputs return `isError: true` with `{ "error": "..." }` using the same descriptor validation messages as CLI bad-argument exits.

## 1. codebase_overview

High-level summary of the entire codebase.
Expand Down
1 change: 1 addition & 0 deletions llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -447,3 +447,4 @@ Gate modes: all, new-only. Returns pass/warn/fail verdict, findings, and summary
- **JSON mode**: `--json` outputs stable JSON schema to stdout.
- **Exit codes**: 0 = success, 1 = runtime error, 2 = bad args/usage.
- **MCP mode**: `codebase-intelligence <path>` (no subcommand) starts MCP stdio server.
- **MCP operation errors**: Invalid operation inputs return `isError: true` with JSON text `{ "error": "..." }` using the same descriptor validation messages as CLI bad-argument exits.
5 changes: 3 additions & 2 deletions roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,13 +273,14 @@ Collapse CLI + MCP operation duplication into one descriptor registry before add
- Reuse registry schemas in CLI coercion.
- Move CLI failures over descriptor-level validation errors.
- Add CLI registry parity coverage for representative descriptor runs and invalid input.
- Expand CH-P1-01 coverage from overview/representative CLI/MCP operations to every operation.
- Expand CH-P1-02 coverage for descriptor validation, CLI parse failure, and cache reuse through registry-adapted commands.

**Remaining:**

- Use one graph-load pipeline with progress callbacks.
- Move text/SARIF/markdown formatting over result objects into formatters.
- Expand CH-P1-01 from overview/representative CLI/MCP operations to every operation.
- Expand CH-P1-02 coverage from CLI invalid input to success, parse failure, and cache reuse through every registry adapter.
- Extend CH-P1-02 coverage to MCP/stdio graph-load behavior after the shared graph-load pipeline exists.

### Type/Shape Layer

Expand Down
45 changes: 36 additions & 9 deletions src/community/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,46 +6,73 @@ import type { CodebaseGraph, Cluster } from "../types/index.js";
export function detectCommunities(graph: CodebaseGraph): Cluster[] {
const undirected = new Graph({ type: "undirected" });

for (const node of graph.nodes) {
const fileNodes = graph.nodes
.filter((node) => node.type === "file")
.sort((a, b) => a.id.localeCompare(b.id));

for (const node of fileNodes) {
if (node.type !== "file") continue;
if (!undirected.hasNode(node.id)) {
undirected.addNode(node.id, { module: node.module });
}
}

const dependencyEdges = new Map<string, { source: string; target: string; weight: number }>();

for (const edge of graph.edges) {
if (!undirected.hasNode(edge.source) || !undirected.hasNode(edge.target)) continue;
if (edge.source === edge.target) continue;
if (!undirected.hasEdge(edge.source, edge.target) && !undirected.hasEdge(edge.target, edge.source)) {
undirected.addEdge(edge.source, edge.target, { weight: edge.weight });

const source = edge.source.localeCompare(edge.target) <= 0 ? edge.source : edge.target;
const target = source === edge.source ? edge.target : edge.source;
const key = `${source}\t${target}`;
const existing = dependencyEdges.get(key);
if (existing) {
existing.weight += edge.weight;
} else {
dependencyEdges.set(key, { source, target, weight: edge.weight });
}
}

const sortedDependencyEdges = [...dependencyEdges.values()].sort((a, b) =>
`${a.source}\t${a.target}`.localeCompare(`${b.source}\t${b.target}`)
);

for (const edge of sortedDependencyEdges) {
undirected.addEdge(edge.source, edge.target, { weight: edge.weight });
}

if (undirected.order === 0) return [];

const communities = louvain(undirected) as Record<string, number>;
const communities = louvain(undirected, { randomWalk: false }) as Record<string, number>;

const clusterMap = new Map<number, string[]>();
for (const [nodeId, clusterId] of Object.entries(communities)) {
for (const [nodeId, clusterId] of Object.entries(communities).sort(([a], [b]) => a.localeCompare(b))) {
const existing = clusterMap.get(clusterId) ?? [];
existing.push(nodeId);
clusterMap.set(clusterId, existing);
}

const clusters: Cluster[] = [];
for (const [clusterId, files] of clusterMap) {
const clusters: Array<Omit<Cluster, "id">> = [];
for (const files of clusterMap.values()) {
files.sort((a, b) => a.localeCompare(b));
const commonModule = findDominantModule(files, graph);
const cohesion = computeClusterCohesion(files, graph);

clusters.push({
id: `cluster-${clusterId}`,
name: commonModule,
files,
cohesion,
});
}

return clusters.sort((a, b) => b.files.length - a.files.length);
return clusters
.sort((a, b) =>
b.files.length - a.files.length ||
a.name.localeCompare(b.name) ||
(a.files[0] ?? "").localeCompare(b.files[0] ?? "")
)
.map((cluster, index) => ({ id: `cluster-${index}`, ...cluster }));
}

function findDominantModule(files: string[], graph: CodebaseGraph): string {
Expand Down
18 changes: 15 additions & 3 deletions src/mcp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,28 @@ function errorPayload(
return nextSteps ? { ...payload, nextSteps } : payload;
}

function mcpInputSchema<TInput extends object>(
operation: Operation<TInput, unknown>,
): z.ZodType<Record<string, unknown>> {
const shape: z.ZodRawShape = {};
for (const [key, schema] of Object.entries(operation.inputShape)) {
shape[key] = schema.catch((context: { input: unknown }) => context.input);
}
return z.object(shape).passthrough();
}

function registerOperationTool<TInput extends object, TResult>(
server: McpServer,
graph: CodebaseGraph,
operation: Operation<TInput, TResult>,
options: OperationToolOptions<TResult> = {},
): void {
server.tool(
server.registerTool(
operation.mcpTool,
operation.description,
operation.inputShape,
{
description: operation.description,
inputSchema: mcpInputSchema(operation),
},
async (rawInput) => {
const parsed = parseOperationInput(operation, rawInput);
if (!parsed.ok) {
Expand Down
10 changes: 9 additions & 1 deletion tests/helpers/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import { registerTools } from "../../src/mcp/index.js";
import { setGraph, setIndexedHead, setRoot } from "../../src/server/graph-store.js";
import { getFixturePipeline, getFixtureSrcPath } from "./pipeline.js";

export interface ToolPayload {
interface ToolPayload {
payload: Record<string, unknown>;
isError: boolean;
}

export interface FixtureMcp {
listTools(): Promise<string[]>;
listToolMetadata(): Promise<Array<{ name: string; inputSchema: unknown }>>;
callTool(name: string, args?: Record<string, unknown>): Promise<Record<string, unknown>>;
callToolWithMeta(name: string, args?: Record<string, unknown>): Promise<ToolPayload>;
}
Expand Down Expand Up @@ -59,6 +60,13 @@ export async function createFixtureMcp(rootDir = getFixtureSrcPath()): Promise<F
const result = await client.listTools();
return result.tools.map((tool) => tool.name);
},
async listToolMetadata(): Promise<Array<{ name: string; inputSchema: unknown }>> {
const result = await client.listTools();
return result.tools.map((tool) => ({
name: tool.name,
inputSchema: tool.inputSchema,
}));
},
async callTool(name: string, args: Record<string, unknown> = {}): Promise<Record<string, unknown>> {
const result = await client.callTool({ name, arguments: args });
return parsePayload(firstTextContent(result));
Expand Down
Loading
Loading