Skip to content

Commit 8a3ec23

Browse files
committed
feat(mcp): add blueprint_scene tool + multi-tool dispatcher
1 parent 25fc698 commit 8a3ec23

4 files changed

Lines changed: 167 additions & 62 deletions

File tree

mcp/README.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ provider call is MCP-specific.
1414
- **Bring your own key.** The Anthropic key comes from the environment, never from
1515
a server or the extension's storage. Nothing phones home.
1616

17-
## Tool
17+
## Tools
1818

1919
### `scan_repo({ repo })`
2020

@@ -37,6 +37,22 @@ provider call is MCP-specific.
3737
}
3838
```
3939

40+
### `blueprint_scene({ repo })`
41+
42+
Maps how the repo is built and returns a laid-out graph — `nodes` (key parts) and
43+
`edges` (how they relate), with positions — ready for a `<DependencyGraph>`-style
44+
component. Heavier than `scan_repo`: it reads source and makes two model calls
45+
(atoms, then lineage).
46+
47+
```json
48+
{
49+
"id": "repo:...", "scope": "blueprint", "repoId": "honojs/hono",
50+
"nodes": [{ "id": "app", "label": "Hono app", "kind": "entrypoint", "x": 120, "y": 40 }],
51+
"edges": [{ "source": "app", "target": "router", "label": "depends-on" }],
52+
"camera": { "x": 0, "y": 0, "zoom": 1 }
53+
}
54+
```
55+
4056
## Setup
4157

4258
```bash
@@ -67,5 +83,5 @@ In `claude_desktop_config.json`:
6783

6884
- Unauthenticated GitHub requests are rate-limited. For heavier use, a `GITHUB_TOKEN`
6985
pass-through is a follow-up.
70-
- Next tools (follow-ups): `blueprint_scene` (the nodes/edges graph), `deep_dive`,
71-
multi-provider, and npm / PyPI / GitLab.
86+
- Next (follow-ups): `deep_dive` (the plain-English layer), multi-provider, and
87+
npm / PyPI / GitLab support; a `GITHUB_TOKEN` pass-through for higher rate limits.

mcp/blueprint-scene.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// blueprint_scene tool: a laid-out nodes/edges graph of how a repo is built —
2+
// ready for a <DependencyGraph>-style component.
3+
//
4+
// Pipeline (extension modules, verbatim): fetchRepoData + fetchSource ->
5+
// atoms prompt/parse -> lineage prompt/parse -> buildBlueprintScene. Two model
6+
// calls (atoms, then lineage); the optional "feynman" plain-English layer is
7+
// skipped since the scene only needs atoms + lineage. `facts` is null here —
8+
// the extension's local "runner" (measured metrics) isn't part of a headless MCP.
9+
10+
import { fetchRepoData } from '../fetcher.js';
11+
import { fetchSource, buildAtomsPrompt, parseAtoms, buildLineagePrompt, parseLineage } from '../deepdive.js';
12+
import { buildBlueprintScene } from '../blueprint-adapter.js';
13+
import { parseRepoInput } from './repo-input.js';
14+
import { callAnthropic } from './anthropic.js';
15+
16+
export const BLUEPRINT_TOOL = {
17+
name: 'blueprint_scene',
18+
description:
19+
'Map how a GitHub repo is built and return a laid-out graph: nodes (its key parts) and ' +
20+
'edges (how they relate), with positions. Use this to visualize a repository as a ' +
21+
'dependency / architecture diagram. Heavier than scan_repo (reads source, two model calls).',
22+
inputSchema: {
23+
type: 'object',
24+
properties: { repo: { type: 'string', description: 'A repo as owner/name or a GitHub URL' } },
25+
required: ['repo'],
26+
additionalProperties: false,
27+
},
28+
outputSchema: {
29+
type: 'object',
30+
properties: {
31+
id: { type: 'string' },
32+
scope: { type: 'string' },
33+
repoId: { type: 'string' },
34+
title: { type: 'string' },
35+
nodes: {
36+
type: 'array',
37+
items: {
38+
type: 'object',
39+
properties: {
40+
id: { type: 'string' },
41+
label: { type: 'string' },
42+
kind: { type: 'string' },
43+
x: { type: 'number' },
44+
y: { type: 'number' },
45+
},
46+
},
47+
},
48+
edges: {
49+
type: 'array',
50+
items: {
51+
type: 'object',
52+
properties: {
53+
source: { type: 'string' },
54+
target: { type: 'string' },
55+
label: { type: 'string' },
56+
},
57+
},
58+
},
59+
camera: { type: 'object' },
60+
},
61+
required: ['nodes', 'edges'],
62+
},
63+
};
64+
65+
export async function runBlueprintScene(args) {
66+
const { platform, repoId } = parseRepoInput(args?.repo);
67+
const repoData = await fetchRepoData(platform, repoId);
68+
const source = await fetchSource(platform, repoId);
69+
const { atoms } = parseAtoms(await callAnthropic(buildAtomsPrompt(repoData, source, null)));
70+
const lineage = parseLineage(await callAnthropic(buildLineagePrompt(atoms)));
71+
return buildBlueprintScene({ deepDive: { atoms, lineage }, repoId, title: repoId });
72+
}

mcp/scan-repo.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// scan_repo tool: verdict-first analysis of a GitHub repo.
2+
// Pipeline (extension modules, verbatim): fetchRepoData -> buildPrompt -> (Anthropic) -> parseClaudeResponse.
3+
4+
import { fetchRepoData } from '../fetcher.js';
5+
import { buildPrompt } from '../prompt.js';
6+
import { parseClaudeResponse } from '../parser.js';
7+
import { parseRepoInput } from './repo-input.js';
8+
import { callAnthropic } from './anthropic.js';
9+
10+
export const SCAN_TOOL = {
11+
name: 'scan_repo',
12+
description:
13+
"Read a GitHub repo's real source and return RepoLens's verdict-first analysis: " +
14+
'overall fit, a health score, pros, cons, red flags, and capabilities. Use this when ' +
15+
'the user wants a structured read on whether to use a repository, not its README pitch.',
16+
inputSchema: {
17+
type: 'object',
18+
properties: { repo: { type: 'string', description: 'A repo as owner/name or a GitHub URL' } },
19+
required: ['repo'],
20+
additionalProperties: false,
21+
},
22+
outputSchema: {
23+
type: 'object',
24+
properties: {
25+
repoId: { type: 'string' },
26+
platform: { type: 'string' },
27+
language: { type: 'string' },
28+
license: { type: 'string' },
29+
stars: { type: 'number' },
30+
description: { type: 'string' },
31+
fit: { type: 'string', description: 'overall fit verdict' },
32+
health: { type: 'object', description: 'health score + signals' },
33+
pros: { type: 'array', items: { type: 'string' } },
34+
cons: { type: 'array', items: { type: 'string' } },
35+
red_flags: { type: 'array' },
36+
capabilities: { type: 'array', items: { type: 'string' } },
37+
},
38+
required: ['repoId', 'fit'],
39+
},
40+
};
41+
42+
export async function runScanRepo(args) {
43+
const { platform, repoId } = parseRepoInput(args?.repo);
44+
const repoData = await fetchRepoData(platform, repoId);
45+
const analysis = parseClaudeResponse(await callAnthropic(buildPrompt(repoData)));
46+
return {
47+
repoId: repoData.repoId,
48+
platform,
49+
language: repoData.language,
50+
license: repoData.license,
51+
stars: repoData.stars,
52+
description: repoData.description,
53+
...analysis,
54+
};
55+
}

mcp/server.js

Lines changed: 21 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,49 @@
11
#!/usr/bin/env node
2-
// RepoLens MCP server (thin proof). Exposes one tool, `scan_repo`, that reads a
3-
// GitHub repo's real source and returns RepoLens's verdict-first analysis as
4-
// structured JSON. Local stdio transport, bring-your-own Anthropic key.
2+
// RepoLens MCP server. Exposes RepoLens's repo analysis as MCP tools over local
3+
// stdio, bring-your-own Anthropic key. Each tool reuses the extension's own
4+
// pipeline modules; the only MCP-specific piece is the env-key Anthropic call.
55
//
6-
// The analysis pipeline is the extension's own modules, imported verbatim:
7-
// fetchRepoData -> buildPrompt -> (Anthropic) -> parseClaudeResponse
8-
// The only MCP-specific piece is the env-key Anthropic call (see ./anthropic.js).
6+
// Tools:
7+
// scan_repo — verdict-first analysis (fit/health/pros/cons/flags)
8+
// blueprint_scene — laid-out nodes/edges graph of how the repo is built
99

1010
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
1111
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
1212
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
1313

14-
import { fetchRepoData } from '../fetcher.js';
15-
import { buildPrompt } from '../prompt.js';
16-
import { parseClaudeResponse } from '../parser.js';
17-
import { parseRepoInput } from './repo-input.js';
18-
import { callAnthropic } from './anthropic.js';
14+
import { SCAN_TOOL, runScanRepo } from './scan-repo.js';
15+
import { BLUEPRINT_TOOL, runBlueprintScene } from './blueprint-scene.js';
1916

20-
const SCAN_TOOL = {
21-
name: 'scan_repo',
22-
description:
23-
"Read a GitHub repo's real source and return RepoLens's verdict-first analysis: " +
24-
'overall fit, a health score, pros, cons, red flags, and capabilities. Use this when ' +
25-
'the user wants a structured read on whether to use a repository, not its README pitch.',
26-
inputSchema: {
27-
type: 'object',
28-
properties: {
29-
repo: { type: 'string', description: 'A repo as owner/name or a GitHub URL' },
30-
},
31-
required: ['repo'],
32-
additionalProperties: false,
33-
},
34-
outputSchema: {
35-
type: 'object',
36-
properties: {
37-
repoId: { type: 'string' },
38-
platform: { type: 'string' },
39-
language: { type: 'string' },
40-
license: { type: 'string' },
41-
stars: { type: 'number' },
42-
description: { type: 'string' },
43-
fit: { type: 'string', description: 'overall fit verdict' },
44-
health: { type: 'object', description: 'health score + signals' },
45-
pros: { type: 'array', items: { type: 'string' } },
46-
cons: { type: 'array', items: { type: 'string' } },
47-
red_flags: { type: 'array' },
48-
capabilities: { type: 'array', items: { type: 'string' } },
49-
},
50-
required: ['repoId', 'fit'],
51-
},
17+
const TOOLS = {
18+
[SCAN_TOOL.name]: { def: SCAN_TOOL, run: runScanRepo },
19+
[BLUEPRINT_TOOL.name]: { def: BLUEPRINT_TOOL, run: runBlueprintScene },
5220
};
5321

5422
const server = new Server(
5523
{ name: 'repolens', version: '0.1.0' },
5624
{ capabilities: { tools: {} } },
5725
);
5826

59-
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [SCAN_TOOL] }));
27+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
28+
tools: Object.values(TOOLS).map((t) => t.def),
29+
}));
6030

6131
server.setRequestHandler(CallToolRequestSchema, async (req) => {
62-
if (req.params.name !== 'scan_repo') {
32+
const tool = TOOLS[req.params.name];
33+
if (!tool) {
6334
return { isError: true, content: [{ type: 'text', text: `Unknown tool: ${req.params.name}` }] };
6435
}
6536
try {
66-
const { platform, repoId } = parseRepoInput(req.params.arguments?.repo);
67-
const repoData = await fetchRepoData(platform, repoId);
68-
const text = await callAnthropic(buildPrompt(repoData));
69-
const analysis = parseClaudeResponse(text);
70-
const result = {
71-
repoId: repoData.repoId,
72-
platform,
73-
language: repoData.language,
74-
license: repoData.license,
75-
stars: repoData.stars,
76-
description: repoData.description,
77-
...analysis,
78-
};
37+
const result = await tool.run(req.params.arguments || {});
7938
return {
8039
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
8140
structuredContent: result,
8241
};
8342
} catch (err) {
84-
return { isError: true, content: [{ type: 'text', text: `scan_repo failed: ${err.message}` }] };
43+
return {
44+
isError: true,
45+
content: [{ type: 'text', text: `${req.params.name} failed: ${err.message}` }],
46+
};
8547
}
8648
});
8749

0 commit comments

Comments
 (0)