Skip to content

Commit 4d4660f

Browse files
Merge pull request #33 from New1Direction/feat/mcp-scan-proof
feat(mcp): local MCP server — scan_repo + blueprint_scene (proof)
2 parents e58f031 + 8a3ec23 commit 4d4660f

9 files changed

Lines changed: 1568 additions & 0 deletions

File tree

mcp/README.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# RepoLens MCP server
2+
3+
A local [MCP](https://modelcontextprotocol.io) server that exposes RepoLens's repo
4+
analysis as a tool. An LLM client (Claude Desktop, Cursor, etc.) calls `scan_repo`
5+
and gets RepoLens's verdict-first JSON back — ready to render as components.
6+
7+
This is a **thin proof**: one tool, GitHub-only, Anthropic-only. It reuses the
8+
extension's own pipeline (`fetcher.js``prompt.js``parser.js`); only the
9+
provider call is MCP-specific.
10+
11+
## What stays true
12+
13+
- **Local only.** Runs over stdio, spawned by your client. No hosted backend.
14+
- **Bring your own key.** The Anthropic key comes from the environment, never from
15+
a server or the extension's storage. Nothing phones home.
16+
17+
## Tools
18+
19+
### `scan_repo({ repo })`
20+
21+
`repo` is `owner/name` or a GitHub URL. Returns the analysis JSON:
22+
23+
```json
24+
{
25+
"repoId": "honojs/hono",
26+
"platform": "github",
27+
"language": "TypeScript",
28+
"license": "MIT",
29+
"stars": 21000,
30+
"description": "Small, fast web framework for the edges.",
31+
"fit": "strong",
32+
"health": { "score": 92 },
33+
"pros": ["..."],
34+
"cons": ["..."],
35+
"red_flags": [],
36+
"capabilities": ["routing", "middleware", "edge"]
37+
}
38+
```
39+
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+
56+
## Setup
57+
58+
```bash
59+
cd mcp
60+
npm install
61+
export ANTHROPIC_API_KEY=sk-ant-... # required
62+
export ANTHROPIC_MODEL=claude-sonnet-4-6 # optional override
63+
node server.js # speaks MCP over stdio
64+
```
65+
66+
### Add to Claude Desktop
67+
68+
In `claude_desktop_config.json`:
69+
70+
```json
71+
{
72+
"mcpServers": {
73+
"repolens": {
74+
"command": "node",
75+
"args": ["/absolute/path/to/repolens/mcp/server.js"],
76+
"env": { "ANTHROPIC_API_KEY": "sk-ant-..." }
77+
}
78+
}
79+
}
80+
```
81+
82+
## Notes
83+
84+
- Unauthenticated GitHub requests are rate-limited. For heavier use, a `GITHUB_TOKEN`
85+
pass-through is a follow-up.
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/anthropic.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Minimal Anthropic Messages API call for the MCP server. The key comes from the
2+
// environment (ANTHROPIC_API_KEY) — never chrome.storage, never a hosted backend —
3+
// so the server stays local and bring-your-own-key. Mirrors the extension's
4+
// callAnthropic shape (background.js): same endpoint, version header, and default model.
5+
6+
const ENDPOINT = 'https://api.anthropic.com/v1/messages';
7+
const DEFAULT_MODEL = 'claude-sonnet-4-6';
8+
9+
/**
10+
* @param {string} prompt - the fully-assembled analysis prompt.
11+
* @returns {Promise<string>} the model's text response.
12+
*/
13+
export async function callAnthropic(prompt) {
14+
const key = process.env.ANTHROPIC_API_KEY;
15+
if (!key) throw new Error('ANTHROPIC_API_KEY is not set in the environment');
16+
const model = process.env.ANTHROPIC_MODEL || DEFAULT_MODEL;
17+
18+
const res = await fetch(ENDPOINT, {
19+
method: 'POST',
20+
headers: {
21+
'content-type': 'application/json',
22+
'anthropic-version': '2023-06-01',
23+
'x-api-key': key,
24+
},
25+
body: JSON.stringify({
26+
model,
27+
max_tokens: 4096,
28+
messages: [{ role: 'user', content: prompt }],
29+
}),
30+
});
31+
32+
if (!res.ok) {
33+
const detail = await res.text().catch(() => '');
34+
throw new Error(`Anthropic API ${res.status}: ${detail.slice(0, 300)}`);
35+
}
36+
37+
const data = await res.json();
38+
const text = (data.content || []).map((b) => b.text || '').join('').trim();
39+
if (!text) throw new Error('Anthropic returned an empty response');
40+
return text;
41+
}

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+
}

0 commit comments

Comments
 (0)