cartogopher 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -0
- package/bin/cartogopher.js +95 -0
- package/lib/.gitkeep +0 -0
- package/lib/mcp-server.js +868 -0
- package/package.json +44 -0
- package/share/cartogopher/SKILL.md +113 -0
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# cartogopher
|
|
2
|
+
|
|
3
|
+
AI-native code intelligence: parses codebases into a searchable graph of functions, types, endpoints, and call relationships. Includes both a CLI and an MCP server.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g cartogopher
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
npm only downloads the binary for your platform (darwin-arm64, darwin-x64, linux-x64, or linux-arm64).
|
|
12
|
+
|
|
13
|
+
Try it without installing globally:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx cartogopher bake
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick start (Claude Code)
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
export CARTOGOPHER_API_KEY=cg_live_your_key
|
|
23
|
+
cd your-project
|
|
24
|
+
cartogopher bake # parse the codebase (~10–30s)
|
|
25
|
+
cartogopher install-skill # drop the skill into .claude/skills/cartogopher/
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Now any Claude Code agent in this project knows how to use cartogopher. It uses the regular `Bash` tool to call `cartogopher search`, `cartogopher impact`, etc.
|
|
29
|
+
|
|
30
|
+
For a system-wide install (works in every project):
|
|
31
|
+
```bash
|
|
32
|
+
cartogopher install-skill --global # → ~/.claude/skills/cartogopher/SKILL.md
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## MCP server (alternative to the skill)
|
|
36
|
+
|
|
37
|
+
If you'd rather plug in as an MCP server instead of a skill:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
cartogopher mcp
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Configure your MCP client to invoke that command. **The skill is generally cheaper to run** — see bench results in the repo's `tests/bench/engrambench/results/`.
|
|
44
|
+
|
|
45
|
+
## Docs
|
|
46
|
+
|
|
47
|
+
- API key: https://cartogopher.com/download
|
|
48
|
+
- Docs: https://cartogopher.com/docs
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const { spawnSync } = require("child_process");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
|
|
8
|
+
const PLATFORM_PACKAGES = {
|
|
9
|
+
"darwin-arm64": "@cartogopher/darwin-arm64",
|
|
10
|
+
"darwin-x64": "@cartogopher/darwin-x64",
|
|
11
|
+
"linux-x64": "@cartogopher/linux-x64",
|
|
12
|
+
"linux-arm64": "@cartogopher/linux-arm64",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function resolveBinary() {
|
|
16
|
+
const key = `${process.platform}-${process.arch}`;
|
|
17
|
+
const pkg = PLATFORM_PACKAGES[key];
|
|
18
|
+
if (!pkg) {
|
|
19
|
+
console.error(
|
|
20
|
+
`cartogopher: unsupported platform ${key}. Supported: ${Object.keys(PLATFORM_PACKAGES).join(", ")}`
|
|
21
|
+
);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let pkgJsonPath;
|
|
26
|
+
try {
|
|
27
|
+
pkgJsonPath = require.resolve(`${pkg}/package.json`);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.error(
|
|
30
|
+
`cartogopher: platform package '${pkg}' is not installed.\n` +
|
|
31
|
+
`This usually means npm skipped optional dependencies, or the platform package failed to download.\n` +
|
|
32
|
+
`Try: npm install -g ${pkg}`
|
|
33
|
+
);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const exeName = process.platform === "win32" ? "cartogopher.exe" : "cartogopher";
|
|
38
|
+
const binary = path.join(path.dirname(pkgJsonPath), "bin", exeName);
|
|
39
|
+
if (!fs.existsSync(binary)) {
|
|
40
|
+
console.error(`cartogopher: binary missing at ${binary}`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
return binary;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function mcpScriptPath() {
|
|
47
|
+
return path.join(__dirname, "..", "lib", "mcp-server.js");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function bundledSkillPath() {
|
|
51
|
+
return path.join(__dirname, "..", "share", "cartogopher", "SKILL.md");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function installSkill(argv) {
|
|
55
|
+
// Usage: cartogopher install-skill [--global] [--dest <dir>]
|
|
56
|
+
const flagGlobal = argv.includes("--global");
|
|
57
|
+
const destIdx = argv.indexOf("--dest");
|
|
58
|
+
const targetRoot = destIdx >= 0 && argv[destIdx + 1]
|
|
59
|
+
? path.resolve(argv[destIdx + 1])
|
|
60
|
+
: flagGlobal
|
|
61
|
+
? path.join(process.env.HOME || "", ".claude", "skills", "cartogopher")
|
|
62
|
+
: path.join(process.cwd(), ".claude", "skills", "cartogopher");
|
|
63
|
+
|
|
64
|
+
const src = bundledSkillPath();
|
|
65
|
+
if (!fs.existsSync(src)) {
|
|
66
|
+
console.error(`cartogopher install-skill: bundled SKILL.md not found at ${src}`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fs.mkdirSync(targetRoot, { recursive: true });
|
|
71
|
+
const dest = path.join(targetRoot, "SKILL.md");
|
|
72
|
+
fs.copyFileSync(src, dest);
|
|
73
|
+
console.log(`Installed cartogopher skill → ${dest}`);
|
|
74
|
+
console.log("Claude Code agents in this project can now load it via /cartogopher.");
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const args = process.argv.slice(2);
|
|
79
|
+
if (args[0] === "install-skill") {
|
|
80
|
+
installSkill(args.slice(1));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const binary = resolveBinary();
|
|
84
|
+
const env = { ...process.env, CARTOGOPHER_MCP_SCRIPT: mcpScriptPath() };
|
|
85
|
+
|
|
86
|
+
const result = spawnSync(binary, args, {
|
|
87
|
+
stdio: "inherit",
|
|
88
|
+
env,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (result.error) {
|
|
92
|
+
console.error(`cartogopher: failed to exec binary: ${result.error.message}`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
process.exit(result.status === null ? 1 : result.status);
|
package/lib/.gitkeep
ADDED
|
File without changes
|
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
|
|
4
|
+
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
5
|
+
const {
|
|
6
|
+
CallToolRequestSchema,
|
|
7
|
+
ListToolsRequestSchema,
|
|
8
|
+
} = require('@modelcontextprotocol/sdk/types.js');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { execSync, execFileSync } = require('child_process');
|
|
12
|
+
|
|
13
|
+
class CartoGopherMCP {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.rootDir = this.findProjectRoot();
|
|
16
|
+
this.bakesDir = path.join(this.rootDir, 'bakes');
|
|
17
|
+
this.docMode = false;
|
|
18
|
+
this.binary = this.findBinary();
|
|
19
|
+
|
|
20
|
+
if (process.env.DEBUG) {
|
|
21
|
+
console.error(`MCP v4 Server initialized:`);
|
|
22
|
+
console.error(` Root dir: ${this.rootDir}`);
|
|
23
|
+
console.error(` Bakes dir: ${this.bakesDir}`);
|
|
24
|
+
console.error(` Binary: ${this.binary || 'NOT FOUND'}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
this.server = new Server(
|
|
28
|
+
{ name: 'cartogopher-mcp-v4', version: '4.3.0' },
|
|
29
|
+
{ capabilities: { tools: {} } }
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
this.setupHandlers();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
36
|
+
// Project & Binary Discovery (reused from v1)
|
|
37
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
38
|
+
|
|
39
|
+
findProjectRoot() {
|
|
40
|
+
if (process.env.CARTOGOPHER_ROOT) return process.env.CARTOGOPHER_ROOT;
|
|
41
|
+
if (process.env.CURSOR_WORKSPACE) {
|
|
42
|
+
const ws = process.env.CURSOR_WORKSPACE;
|
|
43
|
+
if (this.isValidProject(ws)) return ws;
|
|
44
|
+
}
|
|
45
|
+
const cwd = process.cwd();
|
|
46
|
+
if (this.isValidProject(cwd)) return cwd;
|
|
47
|
+
const gitRoot = this.getGitRoot();
|
|
48
|
+
if (gitRoot && this.isValidProject(gitRoot)) return gitRoot;
|
|
49
|
+
return cwd;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
getGitRoot() {
|
|
53
|
+
try {
|
|
54
|
+
return execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();
|
|
55
|
+
} catch { return null; }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
isValidProject(dir) {
|
|
59
|
+
try {
|
|
60
|
+
const indicators = [
|
|
61
|
+
'go.mod', 'go.sum', 'main.go', 'requirements.txt', 'setup.py',
|
|
62
|
+
'pyproject.toml', 'Pipfile', 'Cargo.toml', 'Cargo.lock',
|
|
63
|
+
'package.json', 'tsconfig.json', 'fast.yaml', '.git', 'Makefile', 'Dockerfile',
|
|
64
|
+
];
|
|
65
|
+
if (indicators.some(f => fs.existsSync(path.join(dir, f)))) return true;
|
|
66
|
+
const exts = ['.go', '.py', '.rs', '.js', '.ts', '.html', '.css', '.vue', '.jsx', '.tsx'];
|
|
67
|
+
try {
|
|
68
|
+
if (fs.readdirSync(dir).some(f => exts.some(e => f.endsWith(e)))) return true;
|
|
69
|
+
} catch {}
|
|
70
|
+
if (fs.existsSync(path.join(dir, 'bakes'))) return true;
|
|
71
|
+
return false;
|
|
72
|
+
} catch { return false; }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
findBinary() {
|
|
76
|
+
const candidates = [
|
|
77
|
+
process.env.CARTOGOPHER_INSTALL_ROOT ? path.join(process.env.CARTOGOPHER_INSTALL_ROOT, 'cartogopher') : null,
|
|
78
|
+
path.join(this.rootDir, 'cartogopher'),
|
|
79
|
+
path.join(path.dirname(path.dirname(process.argv[1])), 'cartogopher'),
|
|
80
|
+
'cartogopher',
|
|
81
|
+
].filter(Boolean);
|
|
82
|
+
|
|
83
|
+
for (const p of candidates) {
|
|
84
|
+
if (p === 'cartogopher') {
|
|
85
|
+
try { execSync('which cartogopher', { stdio: 'pipe' }); return p; } catch { continue; }
|
|
86
|
+
} else if (fs.existsSync(p)) {
|
|
87
|
+
return p;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
94
|
+
// CLI Execution Helper
|
|
95
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
96
|
+
|
|
97
|
+
execCLI(args) {
|
|
98
|
+
if (!this.binary) {
|
|
99
|
+
throw new Error('cartogopher binary not found. Install with: go install github.com/jakenesler/CartoGopher/cmd/cartogopher@latest');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const env = { ...process.env };
|
|
103
|
+
if (process.env.CARTOGOPHER_API_KEY) {
|
|
104
|
+
env.CARTOGOPHER_API_KEY = process.env.CARTOGOPHER_API_KEY;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (process.env.DEBUG) {
|
|
108
|
+
console.error(`execCLI: ${this.binary} ${args.join(' ')}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const stdout = execFileSync(this.binary, args, {
|
|
113
|
+
timeout: 30000,
|
|
114
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
115
|
+
encoding: 'utf8',
|
|
116
|
+
cwd: this.rootDir,
|
|
117
|
+
env,
|
|
118
|
+
});
|
|
119
|
+
return stdout.trim();
|
|
120
|
+
} catch (err) {
|
|
121
|
+
const msg = err.stderr ? err.stderr.toString().trim() : err.message;
|
|
122
|
+
throw new Error(msg || 'CLI command failed');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Resolve bakeDir from MCP args → --out value. Strips /latest suffix since CLI expects parent. */
|
|
127
|
+
resolveBakeDir(args) {
|
|
128
|
+
let dir = args?.bakeDir || args?.path || null;
|
|
129
|
+
if (!dir) return null;
|
|
130
|
+
// Strip trailing /latest — CLI expects the parent bakes dir
|
|
131
|
+
return dir.replace(/\/latest\/?$/, '');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
135
|
+
// Tool Schemas (identical to v1 for backward compatibility)
|
|
136
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
137
|
+
|
|
138
|
+
getToolDefinitions() {
|
|
139
|
+
// bakeDir is accepted by most tools as an optional override; documented once
|
|
140
|
+
// here rather than on every field to keep system-prompt cost down.
|
|
141
|
+
const bakeDir = { type: 'string' };
|
|
142
|
+
|
|
143
|
+
return [
|
|
144
|
+
// ── Setup / modes ─────────────────────────────────────────────
|
|
145
|
+
{
|
|
146
|
+
name: 'llm_instructions',
|
|
147
|
+
description: 'Setup notes for this project.',
|
|
148
|
+
inputSchema: { type: 'object', properties: { path: { type: 'string' } } },
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: 'doc_mode_enable',
|
|
152
|
+
description: 'Verbose mode for doc generation; pair with doc_mode_disable.',
|
|
153
|
+
inputSchema: { type: 'object', properties: {} },
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: 'doc_mode_disable',
|
|
157
|
+
description: 'Exit verbose doc mode.',
|
|
158
|
+
inputSchema: { type: 'object', properties: {} },
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
// ── Core code intel ───────────────────────────────────────────
|
|
162
|
+
{
|
|
163
|
+
name: 'shake',
|
|
164
|
+
description: 'Repo overview: structure, packages, entry points.',
|
|
165
|
+
inputSchema: { type: 'object', properties: { path: { type: 'string' } } },
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: 'bake',
|
|
169
|
+
description: 'Full bake.json (large; prefer narrower tools).',
|
|
170
|
+
inputSchema: { type: 'object', properties: { bakeDir } },
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: 'symbol',
|
|
174
|
+
description: 'Function/type details: signature, calls, complexity, file:line.',
|
|
175
|
+
inputSchema: {
|
|
176
|
+
type: 'object',
|
|
177
|
+
properties: { name: { type: 'string' }, bakeDir },
|
|
178
|
+
required: ['name'],
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: 'full_symbol',
|
|
183
|
+
description: 'Verbose symbol details (needs doc_mode_enable).',
|
|
184
|
+
inputSchema: {
|
|
185
|
+
type: 'object',
|
|
186
|
+
properties: { name: { type: 'string' }, bakeDir },
|
|
187
|
+
required: ['name'],
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: 'search',
|
|
192
|
+
description: 'AST search for functions, types, packages.',
|
|
193
|
+
inputSchema: {
|
|
194
|
+
type: 'object',
|
|
195
|
+
properties: {
|
|
196
|
+
q: { type: 'string' },
|
|
197
|
+
limit: { type: 'integer' },
|
|
198
|
+
package: { type: 'string' },
|
|
199
|
+
bakeDir,
|
|
200
|
+
},
|
|
201
|
+
required: ['q'],
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: 'supersearch',
|
|
206
|
+
description: 'AST search with context/pattern filters (identifiers, calls, assigns, strings, etc.).',
|
|
207
|
+
inputSchema: {
|
|
208
|
+
type: 'object',
|
|
209
|
+
properties: {
|
|
210
|
+
query: { type: 'string' },
|
|
211
|
+
context: { type: 'string', description: 'all|strings|comments|identifiers|map-keys|imports|assignments' },
|
|
212
|
+
pattern: { type: 'string', description: 'call|assign|return|map-access' },
|
|
213
|
+
limit: { type: 'integer' },
|
|
214
|
+
show_context: { type: 'integer' },
|
|
215
|
+
exclude_tests: { type: 'boolean' },
|
|
216
|
+
package: { type: 'string' },
|
|
217
|
+
languages: { type: 'string' },
|
|
218
|
+
},
|
|
219
|
+
required: ['query'],
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: 'file_functions',
|
|
224
|
+
description: 'Functions in a file (no full read).',
|
|
225
|
+
inputSchema: {
|
|
226
|
+
type: 'object',
|
|
227
|
+
properties: {
|
|
228
|
+
file: { type: 'string' },
|
|
229
|
+
include_summaries: { type: 'boolean' },
|
|
230
|
+
bakeDir,
|
|
231
|
+
},
|
|
232
|
+
required: ['file'],
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: 'related_to',
|
|
237
|
+
description: 'Call graph: callers + callees of a symbol.',
|
|
238
|
+
inputSchema: {
|
|
239
|
+
type: 'object',
|
|
240
|
+
properties: {
|
|
241
|
+
symbol: { type: 'string' },
|
|
242
|
+
depth: { type: 'integer' },
|
|
243
|
+
bakeDir,
|
|
244
|
+
},
|
|
245
|
+
required: ['symbol'],
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: 'impact',
|
|
250
|
+
description: 'Refactor blast radius: callers, endpoints, CRUD, risk score.',
|
|
251
|
+
inputSchema: {
|
|
252
|
+
type: 'object',
|
|
253
|
+
properties: {
|
|
254
|
+
symbol: { type: 'string' },
|
|
255
|
+
depth: { type: 'integer' },
|
|
256
|
+
bakeDir,
|
|
257
|
+
},
|
|
258
|
+
required: ['symbol'],
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
name: 'api_surface',
|
|
263
|
+
description: 'Exported functions and types.',
|
|
264
|
+
inputSchema: {
|
|
265
|
+
type: 'object',
|
|
266
|
+
properties: {
|
|
267
|
+
package: { type: 'string' },
|
|
268
|
+
limit: { type: 'integer' },
|
|
269
|
+
bakeDir,
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
name: 'package_summary',
|
|
275
|
+
description: 'Package stats + top-complexity functions.',
|
|
276
|
+
inputSchema: {
|
|
277
|
+
type: 'object',
|
|
278
|
+
properties: { package: { type: 'string' }, bakeDir },
|
|
279
|
+
required: ['package'],
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
name: 'crud_operations',
|
|
284
|
+
description: 'CRUD by entity.',
|
|
285
|
+
inputSchema: {
|
|
286
|
+
type: 'object',
|
|
287
|
+
properties: { entity: { type: 'string' }, bakeDir },
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
name: 'architecture_map',
|
|
292
|
+
description: 'Directory map; with `intent`, suggests placement.',
|
|
293
|
+
inputSchema: {
|
|
294
|
+
type: 'object',
|
|
295
|
+
properties: { intent: { type: 'string' }, bakeDir },
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
name: 'suggest_placement',
|
|
300
|
+
description: 'Where to place a new function.',
|
|
301
|
+
inputSchema: {
|
|
302
|
+
type: 'object',
|
|
303
|
+
properties: {
|
|
304
|
+
function_name: { type: 'string' },
|
|
305
|
+
function_type: { type: 'string', description: 'handler|service|repository|model|util|test' },
|
|
306
|
+
related_to: { type: 'string' },
|
|
307
|
+
bakeDir,
|
|
308
|
+
},
|
|
309
|
+
required: ['function_name', 'function_type'],
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
name: 'find_docs',
|
|
314
|
+
description: 'Find README/.env/config files.',
|
|
315
|
+
inputSchema: {
|
|
316
|
+
type: 'object',
|
|
317
|
+
properties: {
|
|
318
|
+
type: { type: 'string', description: 'readme|env|config|all' },
|
|
319
|
+
root: { type: 'string' },
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
// ── API endpoints ─────────────────────────────────────────────
|
|
325
|
+
{
|
|
326
|
+
name: 'api_trace',
|
|
327
|
+
description: 'Trace endpoint: frontend → handler → CRUD.',
|
|
328
|
+
inputSchema: {
|
|
329
|
+
type: 'object',
|
|
330
|
+
properties: {
|
|
331
|
+
endpoint: { type: 'string' },
|
|
332
|
+
method: { type: 'string' },
|
|
333
|
+
bakeDir,
|
|
334
|
+
},
|
|
335
|
+
required: ['endpoint'],
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
name: 'all_endpoints',
|
|
340
|
+
description: 'All HTTP endpoints (backend + frontend-called).',
|
|
341
|
+
inputSchema: {
|
|
342
|
+
type: 'object',
|
|
343
|
+
properties: {
|
|
344
|
+
include_frontend: { type: 'boolean' },
|
|
345
|
+
include_backend: { type: 'boolean' },
|
|
346
|
+
bakeDir,
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
|
|
351
|
+
// ── File ops ──────────────────────────────────────────────────
|
|
352
|
+
{
|
|
353
|
+
name: 'slice',
|
|
354
|
+
description: 'Read lines [start, end] of a file.',
|
|
355
|
+
inputSchema: {
|
|
356
|
+
type: 'object',
|
|
357
|
+
properties: {
|
|
358
|
+
file: { type: 'string' },
|
|
359
|
+
start: { type: 'integer' },
|
|
360
|
+
end: { type: 'integer' },
|
|
361
|
+
root: { type: 'string' },
|
|
362
|
+
},
|
|
363
|
+
required: ['file', 'start', 'end'],
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
name: 'patch',
|
|
368
|
+
description: 'Replace lines [start, end], or create new file (omit start/end).',
|
|
369
|
+
inputSchema: {
|
|
370
|
+
type: 'object',
|
|
371
|
+
properties: {
|
|
372
|
+
file: { type: 'string' },
|
|
373
|
+
function_name: { type: 'string' },
|
|
374
|
+
start: { type: 'integer' },
|
|
375
|
+
end: { type: 'integer' },
|
|
376
|
+
new_content: { type: 'string' },
|
|
377
|
+
root: { type: 'string' },
|
|
378
|
+
},
|
|
379
|
+
required: ['file', 'new_content'],
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
|
|
383
|
+
// ── Frontend (single tool, many ops) ──────────────────────────
|
|
384
|
+
{
|
|
385
|
+
name: 'frontend',
|
|
386
|
+
description: 'Frontend (Vue/React/Svelte/Angular/Proto/GraphQL). See `op`.',
|
|
387
|
+
inputSchema: {
|
|
388
|
+
type: 'object',
|
|
389
|
+
properties: {
|
|
390
|
+
op: {
|
|
391
|
+
type: 'string',
|
|
392
|
+
enum: ['summary', 'search', 'template', 'script', 'renderers', 'component', 'props', 'store', 'composables', 'deps', 'react', 'react_component', 'react_hooks', 'svelte', 'svelte_component', 'svelte_stores', 'angular', 'angular_component', 'angular_services', 'angular_routes', 'proto', 'proto_service', 'proto_messages', 'graphql', 'graphql_type', 'graphql_operations'],
|
|
393
|
+
},
|
|
394
|
+
name: { type: 'string' },
|
|
395
|
+
type: { type: 'string', description: 'For search: template|script|all' },
|
|
396
|
+
depth: { type: 'integer' },
|
|
397
|
+
limit: { type: 'integer' },
|
|
398
|
+
bakeDir,
|
|
399
|
+
},
|
|
400
|
+
required: ['op'],
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
// ── External specs (OpenAPI / GraphQL) and academic papers ───
|
|
405
|
+
{
|
|
406
|
+
name: 'navigator',
|
|
407
|
+
description: 'Fetched OpenAPI specs. ops: list, search, endpoint.',
|
|
408
|
+
inputSchema: {
|
|
409
|
+
type: 'object',
|
|
410
|
+
properties: {
|
|
411
|
+
op: { type: 'string', enum: ['list', 'search', 'endpoint'] },
|
|
412
|
+
query: { type: 'string' },
|
|
413
|
+
spec: { type: 'string' },
|
|
414
|
+
path: { type: 'string' },
|
|
415
|
+
method: { type: 'string' },
|
|
416
|
+
limit: { type: 'integer' },
|
|
417
|
+
bakeDir,
|
|
418
|
+
},
|
|
419
|
+
required: ['op'],
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
name: 'gqlnav',
|
|
424
|
+
description: 'Fetched GraphQL schemas. ops: list, search, type, summary.',
|
|
425
|
+
inputSchema: {
|
|
426
|
+
type: 'object',
|
|
427
|
+
properties: {
|
|
428
|
+
op: { type: 'string', enum: ['list', 'search', 'type', 'summary'] },
|
|
429
|
+
query: { type: 'string' },
|
|
430
|
+
spec: { type: 'string' },
|
|
431
|
+
name: { type: 'string' },
|
|
432
|
+
kind: { type: 'string', description: 'type|input|enum|query|mutation|subscription|all' },
|
|
433
|
+
limit: { type: 'integer' },
|
|
434
|
+
bakeDir,
|
|
435
|
+
},
|
|
436
|
+
required: ['op'],
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
name: 'scholar',
|
|
441
|
+
description: 'Academic papers via Semantic Scholar / OpenAlex. ops: search, paper, citations, list.',
|
|
442
|
+
inputSchema: {
|
|
443
|
+
type: 'object',
|
|
444
|
+
properties: {
|
|
445
|
+
op: { type: 'string', enum: ['search', 'paper', 'citations', 'list'] },
|
|
446
|
+
query: { type: 'string' },
|
|
447
|
+
id: { type: 'string', description: 'S2 ID, DOI, ArXiv, or OpenAlex ID' },
|
|
448
|
+
direction: { type: 'string', description: 'citations|references' },
|
|
449
|
+
source: { type: 'string', description: 's2|openalex' },
|
|
450
|
+
year: { type: 'string' },
|
|
451
|
+
fields: { type: 'string' },
|
|
452
|
+
open_access: { type: 'boolean' },
|
|
453
|
+
limit: { type: 'integer' },
|
|
454
|
+
refresh: { type: 'boolean' },
|
|
455
|
+
bakeDir,
|
|
456
|
+
},
|
|
457
|
+
required: ['op'],
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
|
|
461
|
+
// ── Architecture analysis ─────────────────────────────────────
|
|
462
|
+
{
|
|
463
|
+
name: 'decompose',
|
|
464
|
+
description: 'Suggest microservice boundaries from call graph + CRUD ownership.',
|
|
465
|
+
inputSchema: { type: 'object', properties: { bakeDir } },
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
name: 'architecture_smells',
|
|
469
|
+
description: 'Anti-patterns: god functions, cyclic deps, mega-modules, N+1, layer violations.',
|
|
470
|
+
inputSchema: {
|
|
471
|
+
type: 'object',
|
|
472
|
+
properties: {
|
|
473
|
+
max_complexity: { type: 'integer' },
|
|
474
|
+
max_fan_out: { type: 'integer' },
|
|
475
|
+
max_fan_in: { type: 'integer' },
|
|
476
|
+
max_pkg_funcs: { type: 'integer' },
|
|
477
|
+
bakeDir,
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
];
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
setupHandlers() {
|
|
485
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
486
|
+
tools: this.getToolDefinitions(),
|
|
487
|
+
}));
|
|
488
|
+
|
|
489
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
490
|
+
const { name, arguments: args = {} } = request.params;
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
const result = this.handleToolCall(name, args);
|
|
494
|
+
return { content: [{ type: 'text', text: result }] };
|
|
495
|
+
} catch (error) {
|
|
496
|
+
return {
|
|
497
|
+
content: [{ type: 'text', text: `Error: ${error.message}` }],
|
|
498
|
+
isError: true,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
handleToolCall(name, args = {}) {
|
|
505
|
+
if (name === 'doc_mode_enable') {
|
|
506
|
+
this.docMode = true;
|
|
507
|
+
return 'Documentation mode ENABLED. Full responses active. Remember to disable when done.';
|
|
508
|
+
}
|
|
509
|
+
if (name === 'doc_mode_disable') {
|
|
510
|
+
this.docMode = false;
|
|
511
|
+
return 'Documentation mode DISABLED. Back to ultra-compact mode.';
|
|
512
|
+
}
|
|
513
|
+
// Collapsed-family tools dispatch by args.op; legacy <family>_<op> names
|
|
514
|
+
// still route to the same handlers so existing callers don't break.
|
|
515
|
+
if (name === 'navigator' || name.startsWith('navigator_')) {
|
|
516
|
+
const op = name === 'navigator' ? (args.op || 'list') : name.substring('navigator_'.length);
|
|
517
|
+
return this.handleNavigatorTool(`navigator_${op}`, args);
|
|
518
|
+
}
|
|
519
|
+
if (name === 'gqlnav' || name.startsWith('gqlnav_')) {
|
|
520
|
+
const op = name === 'gqlnav' ? (args.op || 'list') : name.substring('gqlnav_'.length);
|
|
521
|
+
return this.handleGQLNavTool(`gqlnav_${op}`, args);
|
|
522
|
+
}
|
|
523
|
+
if (name === 'scholar' || name.startsWith('scholar_')) {
|
|
524
|
+
const op = name === 'scholar' ? (args.op || 'search') : name.substring('scholar_'.length);
|
|
525
|
+
return this.handleScholarTool(`scholar_${op}`, args);
|
|
526
|
+
}
|
|
527
|
+
return this.handleCoreTool(name, args);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
handleCoreTool(name, args = {}) {
|
|
531
|
+
let result;
|
|
532
|
+
switch (name) {
|
|
533
|
+
case 'llm_instructions': {
|
|
534
|
+
const a = ['llm-instructions'];
|
|
535
|
+
a.push('--root', args?.path || this.rootDir);
|
|
536
|
+
result = this.execCLI(a);
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
case 'shake': {
|
|
540
|
+
const a = ['shake-summary'];
|
|
541
|
+
const out = this.resolveBakeDir(args);
|
|
542
|
+
if (out) a.push('--out', out);
|
|
543
|
+
result = this.execCLI(a);
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
case 'bake': {
|
|
547
|
+
const a = ['bake-summary'];
|
|
548
|
+
const out = this.resolveBakeDir(args);
|
|
549
|
+
if (out) a.push('--out', out);
|
|
550
|
+
result = this.execCLI(a);
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
case 'full_symbol':
|
|
554
|
+
case 'symbol': {
|
|
555
|
+
const a = ['symbol', args.name];
|
|
556
|
+
const out = this.resolveBakeDir(args);
|
|
557
|
+
if (out) a.push('--out', out);
|
|
558
|
+
result = this.execCLI(a);
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
case 'search': {
|
|
562
|
+
const a = ['search', args.q];
|
|
563
|
+
if (args.package) a.push('--pkg', args.package);
|
|
564
|
+
if (args.limit) a.push('-l', String(args.limit));
|
|
565
|
+
const out = this.resolveBakeDir(args);
|
|
566
|
+
if (out) a.push('--out', out);
|
|
567
|
+
result = this.execCLI(a);
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
case 'supersearch': {
|
|
571
|
+
const a = ['search', args.query];
|
|
572
|
+
if (args.context) a.push('--in', args.context);
|
|
573
|
+
if (args.pattern) a.push('--pattern', args.pattern);
|
|
574
|
+
if (args.package) a.push('--pkg', args.package);
|
|
575
|
+
if (args.limit) a.push('-l', String(args.limit));
|
|
576
|
+
if (args.show_context) a.push('-C', String(args.show_context));
|
|
577
|
+
if (args.exclude_tests) a.push('--exclude-test');
|
|
578
|
+
if (args.languages) a.push('--lang', args.languages);
|
|
579
|
+
const out = this.resolveBakeDir(args);
|
|
580
|
+
if (out) a.push('--out', out);
|
|
581
|
+
result = this.execCLI(a);
|
|
582
|
+
break;
|
|
583
|
+
}
|
|
584
|
+
case 'slice': {
|
|
585
|
+
const a = ['slice', args.file, '--start', String(args.start), '--end', String(args.end)];
|
|
586
|
+
a.push('--root', args.root || this.rootDir);
|
|
587
|
+
result = this.execCLI(a);
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
590
|
+
case 'patch': {
|
|
591
|
+
const a = ['patch', args.file, '--content', args.new_content];
|
|
592
|
+
if (args.start) a.push('--start', String(args.start));
|
|
593
|
+
if (args.end) a.push('--end', String(args.end));
|
|
594
|
+
if (args.function_name) a.push('--function', args.function_name);
|
|
595
|
+
a.push('--root', args.root || this.rootDir);
|
|
596
|
+
result = this.execCLI(a);
|
|
597
|
+
break;
|
|
598
|
+
}
|
|
599
|
+
case 'api_surface': {
|
|
600
|
+
const a = ['api-surface'];
|
|
601
|
+
if (args.package) a.push('--package', args.package);
|
|
602
|
+
if (args.limit) a.push('--limit', String(args.limit));
|
|
603
|
+
const out = this.resolveBakeDir(args);
|
|
604
|
+
if (out) a.push('--out', out);
|
|
605
|
+
result = this.execCLI(a);
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
case 'related_to': {
|
|
609
|
+
const a = ['related-to', args.symbol];
|
|
610
|
+
if (args.depth) a.push('--depth', String(args.depth));
|
|
611
|
+
const out = this.resolveBakeDir(args);
|
|
612
|
+
if (out) a.push('--out', out);
|
|
613
|
+
result = this.execCLI(a);
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
case 'package_summary': {
|
|
617
|
+
const a = ['package-summary', args.package];
|
|
618
|
+
const out = this.resolveBakeDir(args);
|
|
619
|
+
if (out) a.push('--out', out);
|
|
620
|
+
result = this.execCLI(a);
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
case 'crud_operations': {
|
|
624
|
+
const a = ['crud'];
|
|
625
|
+
if (args.entity) a.push('--entity', args.entity);
|
|
626
|
+
const out = this.resolveBakeDir(args);
|
|
627
|
+
if (out) a.push('--out', out);
|
|
628
|
+
result = this.execCLI(a);
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
case 'architecture_map': {
|
|
632
|
+
const a = ['architecture-map'];
|
|
633
|
+
if (args.intent) a.push('--intent', args.intent);
|
|
634
|
+
const out = this.resolveBakeDir(args);
|
|
635
|
+
if (out) a.push('--out', out);
|
|
636
|
+
result = this.execCLI(a);
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
case 'file_functions': {
|
|
640
|
+
const a = ['file-functions', args.file];
|
|
641
|
+
const out = this.resolveBakeDir(args);
|
|
642
|
+
if (out) a.push('--out', out);
|
|
643
|
+
result = this.execCLI(a);
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
case 'suggest_placement': {
|
|
647
|
+
const a = ['suggest-placement', '--name', args.function_name, '--type', args.function_type];
|
|
648
|
+
if (args.related_to) a.push('--related-to', args.related_to);
|
|
649
|
+
const out = this.resolveBakeDir(args);
|
|
650
|
+
if (out) a.push('--out', out);
|
|
651
|
+
result = this.execCLI(a);
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
case 'find_docs': {
|
|
655
|
+
const a = ['find-docs'];
|
|
656
|
+
if (args.type) a.push('--type', args.type);
|
|
657
|
+
result = this.execCLI(a);
|
|
658
|
+
break;
|
|
659
|
+
}
|
|
660
|
+
case 'frontend': {
|
|
661
|
+
const a = ['frontend', args.op];
|
|
662
|
+
if (args.name) a.push('--name', args.name);
|
|
663
|
+
if (args.type) a.push('--search-type', args.type);
|
|
664
|
+
if (args.depth) a.push('--depth', String(args.depth));
|
|
665
|
+
if (args.limit) a.push('--limit', String(args.limit));
|
|
666
|
+
const out = this.resolveBakeDir(args);
|
|
667
|
+
if (out) a.push('--out', out);
|
|
668
|
+
result = this.execCLI(a);
|
|
669
|
+
break;
|
|
670
|
+
}
|
|
671
|
+
case 'api_trace': {
|
|
672
|
+
const a = ['api-trace', args.endpoint];
|
|
673
|
+
if (args.method) a.push('--method', args.method);
|
|
674
|
+
const out = this.resolveBakeDir(args);
|
|
675
|
+
if (out) a.push('--out', out);
|
|
676
|
+
result = this.execCLI(a);
|
|
677
|
+
break;
|
|
678
|
+
}
|
|
679
|
+
case 'all_endpoints': {
|
|
680
|
+
const a = ['all-endpoints'];
|
|
681
|
+
if (args.include_frontend === false) a.push('--frontend=false');
|
|
682
|
+
if (args.include_backend === false) a.push('--backend=false');
|
|
683
|
+
const out = this.resolveBakeDir(args);
|
|
684
|
+
if (out) a.push('--out', out);
|
|
685
|
+
result = this.execCLI(a);
|
|
686
|
+
break;
|
|
687
|
+
}
|
|
688
|
+
case 'impact': {
|
|
689
|
+
if (!args.symbol) throw new Error('Missing required parameter: symbol');
|
|
690
|
+
const a = ['impact', args.symbol];
|
|
691
|
+
if (args.depth) a.push('--depth', String(args.depth));
|
|
692
|
+
const out = this.resolveBakeDir(args);
|
|
693
|
+
if (out) a.push('--out', out);
|
|
694
|
+
result = this.execCLI(a);
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
case 'decompose': {
|
|
698
|
+
const a = ['decompose'];
|
|
699
|
+
const out = this.resolveBakeDir(args);
|
|
700
|
+
if (out) a.push('--out', out);
|
|
701
|
+
result = this.execCLI(a);
|
|
702
|
+
break;
|
|
703
|
+
}
|
|
704
|
+
case 'architecture_smells': {
|
|
705
|
+
const a = ['architecture-smells'];
|
|
706
|
+
if (args.max_complexity) a.push('--max-complexity', String(args.max_complexity));
|
|
707
|
+
if (args.max_fan_out) a.push('--max-fan-out', String(args.max_fan_out));
|
|
708
|
+
if (args.max_fan_in) a.push('--max-fan-in', String(args.max_fan_in));
|
|
709
|
+
if (args.max_pkg_funcs) a.push('--max-pkg-funcs', String(args.max_pkg_funcs));
|
|
710
|
+
const out = this.resolveBakeDir(args);
|
|
711
|
+
if (out) a.push('--out', out);
|
|
712
|
+
result = this.execCLI(a);
|
|
713
|
+
break;
|
|
714
|
+
}
|
|
715
|
+
default:
|
|
716
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
717
|
+
}
|
|
718
|
+
return result;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
handleNavigatorTool(name, args = {}) {
|
|
722
|
+
let result;
|
|
723
|
+
switch (name) {
|
|
724
|
+
case 'navigator_list': {
|
|
725
|
+
const a = ['navigator', 'list'];
|
|
726
|
+
const out = this.resolveBakeDir(args);
|
|
727
|
+
if (out) a.push('--out', out);
|
|
728
|
+
result = this.execCLI(a);
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
731
|
+
case 'navigator_search': {
|
|
732
|
+
const a = ['navigator', 'search', args.query];
|
|
733
|
+
if (args.spec) a.push('--spec', args.spec);
|
|
734
|
+
if (args.method) a.push('--method', args.method);
|
|
735
|
+
if (args.limit) a.push('--limit', String(args.limit));
|
|
736
|
+
const out = this.resolveBakeDir(args);
|
|
737
|
+
if (out) a.push('--out', out);
|
|
738
|
+
result = this.execCLI(a);
|
|
739
|
+
break;
|
|
740
|
+
}
|
|
741
|
+
case 'navigator_endpoint': {
|
|
742
|
+
const a = ['navigator', 'endpoint', args.spec, args.path];
|
|
743
|
+
if (args.method) a.push('--method', args.method);
|
|
744
|
+
const out = this.resolveBakeDir(args);
|
|
745
|
+
if (out) a.push('--out', out);
|
|
746
|
+
result = this.execCLI(a);
|
|
747
|
+
break;
|
|
748
|
+
}
|
|
749
|
+
default:
|
|
750
|
+
throw new Error(`Unknown navigator tool: ${name}`);
|
|
751
|
+
}
|
|
752
|
+
return result;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
handleGQLNavTool(name, args = {}) {
|
|
756
|
+
let result;
|
|
757
|
+
switch (name) {
|
|
758
|
+
case 'gqlnav_summary': {
|
|
759
|
+
if (!args.spec) throw new Error('Missing required parameter: spec (schema name from gqlnav_list)');
|
|
760
|
+
const a = ['gql-nav', 'summary', args.spec];
|
|
761
|
+
const out = this.resolveBakeDir(args);
|
|
762
|
+
if (out) a.push('--out', out);
|
|
763
|
+
result = this.execCLI(a);
|
|
764
|
+
break;
|
|
765
|
+
}
|
|
766
|
+
case 'gqlnav_list': {
|
|
767
|
+
const a = ['gql-nav', 'list'];
|
|
768
|
+
const out = this.resolveBakeDir(args);
|
|
769
|
+
if (out) a.push('--out', out);
|
|
770
|
+
result = this.execCLI(a);
|
|
771
|
+
break;
|
|
772
|
+
}
|
|
773
|
+
case 'gqlnav_search': {
|
|
774
|
+
if (!args.query) throw new Error('Missing required parameter: query');
|
|
775
|
+
const a = ['gql-nav', 'search', args.query];
|
|
776
|
+
if (args.spec) a.push('--spec', args.spec);
|
|
777
|
+
if (args.kind) a.push('--kind', args.kind);
|
|
778
|
+
if (args.limit) a.push('--limit', String(args.limit));
|
|
779
|
+
const out = this.resolveBakeDir(args);
|
|
780
|
+
if (out) a.push('--out', out);
|
|
781
|
+
result = this.execCLI(a);
|
|
782
|
+
break;
|
|
783
|
+
}
|
|
784
|
+
case 'gqlnav_type': {
|
|
785
|
+
if (!args.spec) throw new Error('Missing required parameter: spec (schema name from gqlnav_list)');
|
|
786
|
+
if (!args.name) throw new Error('Missing required parameter: name (type/query/mutation name)');
|
|
787
|
+
const a = ['gql-nav', 'type', args.spec, args.name];
|
|
788
|
+
const out = this.resolveBakeDir(args);
|
|
789
|
+
if (out) a.push('--out', out);
|
|
790
|
+
result = this.execCLI(a);
|
|
791
|
+
break;
|
|
792
|
+
}
|
|
793
|
+
default:
|
|
794
|
+
throw new Error(`Unknown gqlnav tool: ${name}`);
|
|
795
|
+
}
|
|
796
|
+
return result;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
handleScholarTool(name, args = {}) {
|
|
800
|
+
let result;
|
|
801
|
+
switch (name) {
|
|
802
|
+
case 'scholar_search': {
|
|
803
|
+
if (!args.query) throw new Error('Missing required parameter: query');
|
|
804
|
+
const a = ['scholar', 'search', args.query];
|
|
805
|
+
if (args.source) a.push('--source', args.source);
|
|
806
|
+
if (args.year) a.push('--year', args.year);
|
|
807
|
+
if (args.fields) a.push('--fields', args.fields);
|
|
808
|
+
if (args.open_access) a.push('--oa');
|
|
809
|
+
if (args.limit) a.push('--limit', String(args.limit));
|
|
810
|
+
if (args.refresh) a.push('--refresh');
|
|
811
|
+
const out = this.resolveBakeDir(args);
|
|
812
|
+
if (out) a.push('--out', out);
|
|
813
|
+
result = this.execCLI(a);
|
|
814
|
+
break;
|
|
815
|
+
}
|
|
816
|
+
case 'scholar_paper': {
|
|
817
|
+
if (!args.id) throw new Error('Missing required parameter: id (paper ID, DOI, ArXiv ID, or OpenAlex ID)');
|
|
818
|
+
const a = ['scholar', 'paper', args.id];
|
|
819
|
+
if (args.source) a.push('--source', args.source);
|
|
820
|
+
if (args.refresh) a.push('--refresh');
|
|
821
|
+
const out = this.resolveBakeDir(args);
|
|
822
|
+
if (out) a.push('--out', out);
|
|
823
|
+
result = this.execCLI(a);
|
|
824
|
+
break;
|
|
825
|
+
}
|
|
826
|
+
case 'scholar_citations': {
|
|
827
|
+
if (!args.id) throw new Error('Missing required parameter: id (paper ID)');
|
|
828
|
+
const direction = args.direction || 'citations';
|
|
829
|
+
const a = ['scholar', direction, args.id];
|
|
830
|
+
if (args.source) a.push('--source', args.source);
|
|
831
|
+
if (args.limit) a.push('--limit', String(args.limit));
|
|
832
|
+
if (args.refresh) a.push('--refresh');
|
|
833
|
+
const out = this.resolveBakeDir(args);
|
|
834
|
+
if (out) a.push('--out', out);
|
|
835
|
+
result = this.execCLI(a);
|
|
836
|
+
break;
|
|
837
|
+
}
|
|
838
|
+
case 'scholar_list': {
|
|
839
|
+
const a = ['scholar', 'list'];
|
|
840
|
+
const out = this.resolveBakeDir(args);
|
|
841
|
+
if (out) a.push('--out', out);
|
|
842
|
+
result = this.execCLI(a);
|
|
843
|
+
break;
|
|
844
|
+
}
|
|
845
|
+
default:
|
|
846
|
+
throw new Error(`Unknown scholar tool: ${name}`);
|
|
847
|
+
}
|
|
848
|
+
return result;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
852
|
+
// Server Lifecycle
|
|
853
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
854
|
+
|
|
855
|
+
async run() {
|
|
856
|
+
const transport = new StdioServerTransport();
|
|
857
|
+
await this.server.connect(transport);
|
|
858
|
+
|
|
859
|
+
if (process.env.DEBUG) {
|
|
860
|
+
console.error('cartogopher MCP v4 server started');
|
|
861
|
+
console.error(` Root: ${this.rootDir}`);
|
|
862
|
+
console.error(` Binary: ${this.binary || 'NOT FOUND'}`);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const server = new CartoGopherMCP();
|
|
868
|
+
server.run().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cartogopher",
|
|
3
|
+
"version": "5.0.0",
|
|
4
|
+
"description": "AI-native code intelligence: parses codebases into a searchable graph of functions, types, endpoints, and call relationships. CLI + MCP server.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"cartogopher": "bin/cartogopher.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin",
|
|
10
|
+
"lib",
|
|
11
|
+
"share",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://cartogopher.com",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/JakeNesler/CartoGopher.git"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/JakeNesler/CartoGopher/issues"
|
|
24
|
+
},
|
|
25
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
26
|
+
"author": "Jake Nesler",
|
|
27
|
+
"keywords": [
|
|
28
|
+
"code-intelligence",
|
|
29
|
+
"ast",
|
|
30
|
+
"mcp",
|
|
31
|
+
"llm",
|
|
32
|
+
"code-search",
|
|
33
|
+
"static-analysis"
|
|
34
|
+
],
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@modelcontextprotocol/sdk": "^1.18.2"
|
|
37
|
+
},
|
|
38
|
+
"optionalDependencies": {
|
|
39
|
+
"@cartogopher/darwin-arm64": "5.0.0",
|
|
40
|
+
"@cartogopher/darwin-x64": "5.0.0",
|
|
41
|
+
"@cartogopher/linux-x64": "5.0.0",
|
|
42
|
+
"@cartogopher/linux-arm64": "5.0.0"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cartogopher
|
|
3
|
+
description: Code intelligence CLI for analyzing codebases. Use when exploring code architecture, searching for functions, tracing API endpoints, understanding call graphs, or analyzing dependencies. Supports 20+ languages.
|
|
4
|
+
argument-hint: [command] [args]
|
|
5
|
+
allowed-tools: Bash(cartogopher:*) Read
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# CartoGopher CLI
|
|
9
|
+
|
|
10
|
+
Parses codebases into a searchable graph of functions, types, endpoints, and call relationships. Local-only — runs on a SQLite store on disk; no servers to set up.
|
|
11
|
+
|
|
12
|
+
## Setup
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
cartogopher bake # parse the current project (~10-30s for typical repos)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
That's it. Re-run `cartogopher bake` (or use `cartogopher watch`) when files change; it's incremental.
|
|
19
|
+
|
|
20
|
+
## Commands
|
|
21
|
+
|
|
22
|
+
### Search
|
|
23
|
+
```bash
|
|
24
|
+
cartogopher search "CreateUser" # name search
|
|
25
|
+
cartogopher search "webhook" --in=strings # search string literals only
|
|
26
|
+
cartogopher search "TODO" --in=comments # search comments only
|
|
27
|
+
cartogopher search "Delete" --pattern=call # find function call sites
|
|
28
|
+
cartogopher search "handles payment" --mode=semantic # semantic vector search
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Symbol lookup
|
|
32
|
+
```bash
|
|
33
|
+
cartogopher symbol CreateUser # function details: signature, calls, file, complexity
|
|
34
|
+
cartogopher symbol UserService # type details: fields, methods
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Call graph
|
|
38
|
+
```bash
|
|
39
|
+
cartogopher related-to HandleRequest # callers + callees
|
|
40
|
+
cartogopher related-to HandleRequest --depth=3
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Impact analysis (refactor blast radius)
|
|
44
|
+
```bash
|
|
45
|
+
cartogopher impact DeleteUser # transitive callers, affected endpoints, files, risk score
|
|
46
|
+
cartogopher impact DeleteUser --depth=5
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Architecture
|
|
50
|
+
```bash
|
|
51
|
+
cartogopher architecture-map # directory map with purpose inference
|
|
52
|
+
cartogopher architecture-smells # detect anti-patterns (god functions, N+1, mega-modules)
|
|
53
|
+
cartogopher decompose # suggest microservice boundaries
|
|
54
|
+
cartogopher suggest-placement NewHandler service # where to put new code
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### API endpoints
|
|
58
|
+
```bash
|
|
59
|
+
cartogopher all-endpoints # every HTTP endpoint in the codebase
|
|
60
|
+
cartogopher api-trace /api/users # trace endpoint: frontend -> handler -> CRUD
|
|
61
|
+
cartogopher api-surface --package=controllers
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### CRUD analysis
|
|
65
|
+
```bash
|
|
66
|
+
cartogopher crud # entity CRUD breakdown
|
|
67
|
+
cartogopher crud --entity=User
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### File operations
|
|
71
|
+
```bash
|
|
72
|
+
cartogopher file-functions controllers/auth.go # list functions in a file (no full read)
|
|
73
|
+
cartogopher slice auth.go --start=10 --end=50 # read specific lines
|
|
74
|
+
cartogopher patch auth.go --start=10 --end=20 --content="new code"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Frontend (Vue / React / Svelte / Angular / Proto / GraphQL)
|
|
78
|
+
```bash
|
|
79
|
+
cartogopher frontend summary
|
|
80
|
+
cartogopher frontend component UserCard
|
|
81
|
+
cartogopher frontend search "payment"
|
|
82
|
+
cartogopher frontend deps UserCard
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Overview
|
|
86
|
+
```bash
|
|
87
|
+
cartogopher shake # full repo overview (Shake.readme)
|
|
88
|
+
cartogopher package-summary controllers # package stats + top-complexity functions
|
|
89
|
+
cartogopher find-docs # discover README/.env/config files
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Common flags
|
|
93
|
+
|
|
94
|
+
| Flag | Description |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `--root=PATH` | Project root (default: `.`) |
|
|
97
|
+
| `--limit=N` | Max results (default: 20) |
|
|
98
|
+
| `--depth=N` | Call graph / impact depth (default: 2) |
|
|
99
|
+
| `--package=PKG` | Filter by package |
|
|
100
|
+
| `--lang=LANG` | Filter by language |
|
|
101
|
+
| `--in=CONTEXT` | Search context: `all`, `strings`, `comments`, `identifiers`, `map-keys`, `imports` |
|
|
102
|
+
| `--pattern=TYPE` | Search pattern: `call`, `assign`, `return`, `map-access` |
|
|
103
|
+
| `--mode=semantic` | Vector search (requires `bake` to have populated embeddings) |
|
|
104
|
+
|
|
105
|
+
## When to use what
|
|
106
|
+
|
|
107
|
+
- **"Where does X get used?"** → `related-to X` or `search X --pattern=call`
|
|
108
|
+
- **"What would break if I rename X?"** → `impact X`
|
|
109
|
+
- **"What does this codebase look like?"** → `shake` or `architecture-map`
|
|
110
|
+
- **"Are there any code smells?"** → `architecture-smells`
|
|
111
|
+
- **"What's in this file?"** → `file-functions path/to/file` (don't `Read` the whole file)
|
|
112
|
+
- **"What endpoints exist?"** → `all-endpoints`, then `api-trace` on a specific one
|
|
113
|
+
- **"What does this function call?"** → `symbol FunctionName`
|