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
87 changes: 87 additions & 0 deletions mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# RepoLens MCP server

A local [MCP](https://modelcontextprotocol.io) server that exposes RepoLens's repo
analysis as a tool. An LLM client (Claude Desktop, Cursor, etc.) calls `scan_repo`
and gets RepoLens's verdict-first JSON back — ready to render as components.

This is a **thin proof**: one tool, GitHub-only, Anthropic-only. It reuses the
extension's own pipeline (`fetcher.js` → `prompt.js` → `parser.js`); only the
provider call is MCP-specific.

## What stays true

- **Local only.** Runs over stdio, spawned by your client. No hosted backend.
- **Bring your own key.** The Anthropic key comes from the environment, never from
a server or the extension's storage. Nothing phones home.

## Tools

### `scan_repo({ repo })`

`repo` is `owner/name` or a GitHub URL. Returns the analysis JSON:

```json
{
"repoId": "honojs/hono",
"platform": "github",
"language": "TypeScript",
"license": "MIT",
"stars": 21000,
"description": "Small, fast web framework for the edges.",
"fit": "strong",
"health": { "score": 92 },
"pros": ["..."],
"cons": ["..."],
"red_flags": [],
"capabilities": ["routing", "middleware", "edge"]
}
```

### `blueprint_scene({ repo })`

Maps how the repo is built and returns a laid-out graph — `nodes` (key parts) and
`edges` (how they relate), with positions — ready for a `<DependencyGraph>`-style
component. Heavier than `scan_repo`: it reads source and makes two model calls
(atoms, then lineage).

```json
{
"id": "repo:...", "scope": "blueprint", "repoId": "honojs/hono",
"nodes": [{ "id": "app", "label": "Hono app", "kind": "entrypoint", "x": 120, "y": 40 }],
"edges": [{ "source": "app", "target": "router", "label": "depends-on" }],
"camera": { "x": 0, "y": 0, "zoom": 1 }
}
```

## Setup

```bash
cd mcp
npm install
export ANTHROPIC_API_KEY=sk-ant-... # required
export ANTHROPIC_MODEL=claude-sonnet-4-6 # optional override
node server.js # speaks MCP over stdio
```

### Add to Claude Desktop

In `claude_desktop_config.json`:

```json
{
"mcpServers": {
"repolens": {
"command": "node",
"args": ["/absolute/path/to/repolens/mcp/server.js"],
"env": { "ANTHROPIC_API_KEY": "sk-ant-..." }
}
}
}
```

## Notes

- Unauthenticated GitHub requests are rate-limited. For heavier use, a `GITHUB_TOKEN`
pass-through is a follow-up.
- Next (follow-ups): `deep_dive` (the plain-English layer), multi-provider, and
npm / PyPI / GitLab support; a `GITHUB_TOKEN` pass-through for higher rate limits.
41 changes: 41 additions & 0 deletions mcp/anthropic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Minimal Anthropic Messages API call for the MCP server. The key comes from the
// environment (ANTHROPIC_API_KEY) — never chrome.storage, never a hosted backend —
// so the server stays local and bring-your-own-key. Mirrors the extension's
// callAnthropic shape (background.js): same endpoint, version header, and default model.

const ENDPOINT = 'https://api.anthropic.com/v1/messages';
const DEFAULT_MODEL = 'claude-sonnet-4-6';

/**
* @param {string} prompt - the fully-assembled analysis prompt.
* @returns {Promise<string>} the model's text response.
*/
export async function callAnthropic(prompt) {
const key = process.env.ANTHROPIC_API_KEY;
if (!key) throw new Error('ANTHROPIC_API_KEY is not set in the environment');
const model = process.env.ANTHROPIC_MODEL || DEFAULT_MODEL;

const res = await fetch(ENDPOINT, {
method: 'POST',
headers: {
'content-type': 'application/json',
'anthropic-version': '2023-06-01',
'x-api-key': key,
},
body: JSON.stringify({
model,
max_tokens: 4096,
messages: [{ role: 'user', content: prompt }],
}),
});

if (!res.ok) {
const detail = await res.text().catch(() => '');
throw new Error(`Anthropic API ${res.status}: ${detail.slice(0, 300)}`);
}

const data = await res.json();
const text = (data.content || []).map((b) => b.text || '').join('').trim();
if (!text) throw new Error('Anthropic returned an empty response');
return text;
}
72 changes: 72 additions & 0 deletions mcp/blueprint-scene.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// blueprint_scene tool: a laid-out nodes/edges graph of how a repo is built —
// ready for a <DependencyGraph>-style component.
//
// Pipeline (extension modules, verbatim): fetchRepoData + fetchSource ->
// atoms prompt/parse -> lineage prompt/parse -> buildBlueprintScene. Two model
// calls (atoms, then lineage); the optional "feynman" plain-English layer is
// skipped since the scene only needs atoms + lineage. `facts` is null here —
// the extension's local "runner" (measured metrics) isn't part of a headless MCP.

import { fetchRepoData } from '../fetcher.js';
import { fetchSource, buildAtomsPrompt, parseAtoms, buildLineagePrompt, parseLineage } from '../deepdive.js';
import { buildBlueprintScene } from '../blueprint-adapter.js';
import { parseRepoInput } from './repo-input.js';
import { callAnthropic } from './anthropic.js';

export const BLUEPRINT_TOOL = {
name: 'blueprint_scene',
description:
'Map how a GitHub repo is built and return a laid-out graph: nodes (its key parts) and ' +
'edges (how they relate), with positions. Use this to visualize a repository as a ' +
'dependency / architecture diagram. Heavier than scan_repo (reads source, two model calls).',
inputSchema: {
type: 'object',
properties: { repo: { type: 'string', description: 'A repo as owner/name or a GitHub URL' } },
required: ['repo'],
additionalProperties: false,
},
outputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
scope: { type: 'string' },
repoId: { type: 'string' },
title: { type: 'string' },
nodes: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'string' },
label: { type: 'string' },
kind: { type: 'string' },
x: { type: 'number' },
y: { type: 'number' },
},
},
},
edges: {
type: 'array',
items: {
type: 'object',
properties: {
source: { type: 'string' },
target: { type: 'string' },
label: { type: 'string' },
},
},
},
camera: { type: 'object' },
},
required: ['nodes', 'edges'],
},
};

export async function runBlueprintScene(args) {
const { platform, repoId } = parseRepoInput(args?.repo);
const repoData = await fetchRepoData(platform, repoId);
const source = await fetchSource(platform, repoId);
const { atoms } = parseAtoms(await callAnthropic(buildAtomsPrompt(repoData, source, null)));
const lineage = parseLineage(await callAnthropic(buildLineagePrompt(atoms)));
return buildBlueprintScene({ deepDive: { atoms, lineage }, repoId, title: repoId });
}
Loading
Loading