@supermodeltools/mcp-server 0.6.4 → 0.7.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 +149 -0
- package/dist/filtering.js +22 -0
- package/dist/index.js +9 -0
- package/dist/server.js +39 -40
- package/dist/tools/create-supermodel-graph.js +13 -186
- package/dist/tools/graph-tools.js +277 -0
- package/dist/utils/api-helpers.js +185 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -294,6 +294,92 @@ Analyzes code structure, dependencies, and relationships across a repository. Us
|
|
|
294
294
|
- Cleans up temporary files automatically
|
|
295
295
|
- Cross-platform compatible
|
|
296
296
|
|
|
297
|
+
### Individual Graph Tools
|
|
298
|
+
|
|
299
|
+
For targeted analysis, use these specialized tools instead of the comprehensive `explore_codebase`:
|
|
300
|
+
|
|
301
|
+
#### `get_call_graph`
|
|
302
|
+
|
|
303
|
+
Generate a function-level call graph showing caller/callee relationships.
|
|
304
|
+
|
|
305
|
+
**Use this to:**
|
|
306
|
+
- Find all functions that call a specific function
|
|
307
|
+
- Find all functions called by a specific function
|
|
308
|
+
- Trace call chains through the codebase
|
|
309
|
+
- Understand function dependencies
|
|
310
|
+
|
|
311
|
+
**Parameters:**
|
|
312
|
+
|
|
313
|
+
| Argument | Type | Required | Description |
|
|
314
|
+
|----------|------|----------|-------------|
|
|
315
|
+
| `directory` | string | Yes | Path to repository directory |
|
|
316
|
+
| `jq_filter` | string | No | jq filter for custom data extraction |
|
|
317
|
+
|
|
318
|
+
#### `get_dependency_graph`
|
|
319
|
+
|
|
320
|
+
Generate a module-level dependency graph showing import relationships.
|
|
321
|
+
|
|
322
|
+
**Use this to:**
|
|
323
|
+
- Understand module dependencies
|
|
324
|
+
- Find circular dependencies
|
|
325
|
+
- Identify tightly coupled modules
|
|
326
|
+
- Plan module extraction or refactoring
|
|
327
|
+
|
|
328
|
+
**Parameters:**
|
|
329
|
+
|
|
330
|
+
| Argument | Type | Required | Description |
|
|
331
|
+
|----------|------|----------|-------------|
|
|
332
|
+
| `directory` | string | Yes | Path to repository directory |
|
|
333
|
+
| `jq_filter` | string | No | jq filter for custom data extraction |
|
|
334
|
+
|
|
335
|
+
#### `get_domain_graph`
|
|
336
|
+
|
|
337
|
+
Generate a high-level domain classification graph.
|
|
338
|
+
|
|
339
|
+
**Use this to:**
|
|
340
|
+
- Understand the architectural domains in a codebase
|
|
341
|
+
- See how code is organized into logical areas
|
|
342
|
+
- Get a bird's-eye view of system structure
|
|
343
|
+
- Identify domain boundaries
|
|
344
|
+
|
|
345
|
+
**Parameters:**
|
|
346
|
+
|
|
347
|
+
| Argument | Type | Required | Description |
|
|
348
|
+
|----------|------|----------|-------------|
|
|
349
|
+
| `directory` | string | Yes | Path to repository directory |
|
|
350
|
+
| `jq_filter` | string | No | jq filter for custom data extraction |
|
|
351
|
+
|
|
352
|
+
#### `get_parse_graph`
|
|
353
|
+
|
|
354
|
+
Generate an AST-level parse graph with fine-grained code structure.
|
|
355
|
+
|
|
356
|
+
**Use this to:**
|
|
357
|
+
- Analyze detailed code structure
|
|
358
|
+
- Find specific syntax patterns
|
|
359
|
+
- Understand class/function definitions at AST level
|
|
360
|
+
- Support precise refactoring operations
|
|
361
|
+
|
|
362
|
+
**Parameters:**
|
|
363
|
+
|
|
364
|
+
| Argument | Type | Required | Description |
|
|
365
|
+
|----------|------|----------|-------------|
|
|
366
|
+
| `directory` | string | Yes | Path to repository directory |
|
|
367
|
+
| `jq_filter` | string | No | jq filter for custom data extraction |
|
|
368
|
+
|
|
369
|
+
### Choosing the Right Tool
|
|
370
|
+
|
|
371
|
+
| Tool | Best For | Output Size |
|
|
372
|
+
|------|----------|-------------|
|
|
373
|
+
| `explore_codebase` | Comprehensive analysis with built-in queries | Largest - all graph types |
|
|
374
|
+
| `get_call_graph` | Function call tracing, debugging | Medium - functions only |
|
|
375
|
+
| `get_dependency_graph` | Module refactoring, circular deps | Small - modules only |
|
|
376
|
+
| `get_domain_graph` | Architecture overview | Smallest - domains only |
|
|
377
|
+
| `get_parse_graph` | AST analysis, precise refactoring | Large - full AST |
|
|
378
|
+
|
|
379
|
+
**Tip:** Start with `get_domain_graph` for a quick architecture overview, then drill down with `get_call_graph` or `get_dependency_graph` for specific areas.
|
|
380
|
+
|
|
381
|
+
> **Note:** All graph tools accept a `directory` parameter and an optional `jq_filter`. If you start the server with a default working directory (`node dist/index.js /path/to/repo`), the `directory` argument can be omitted from tool calls.
|
|
382
|
+
|
|
297
383
|
## Tool Performance & Timeout Requirements
|
|
298
384
|
|
|
299
385
|
The `explore_codebase` tool analyzes your entire repository to build a comprehensive code graph. Analysis time scales with repository size and complexity.
|
|
@@ -502,6 +588,69 @@ To enable verbose logging, set the `DEBUG` environment variable:
|
|
|
502
588
|
|
|
503
589
|
Benchmark this MCP server using [mcpbr](https://github.com/caspianmoon/mcpbr-benchmark-caching) with the provided [`mcpbr-config.yaml`](./mcpbr-config.yaml) configuration.
|
|
504
590
|
|
|
591
|
+
## Local Development & Testing
|
|
592
|
+
|
|
593
|
+
### Building from Source
|
|
594
|
+
|
|
595
|
+
```bash
|
|
596
|
+
git clone https://github.com/supermodeltools/mcp.git
|
|
597
|
+
cd mcp
|
|
598
|
+
npm install
|
|
599
|
+
npm run build
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
### Running Locally
|
|
603
|
+
|
|
604
|
+
```bash
|
|
605
|
+
# Start the MCP server
|
|
606
|
+
node dist/index.js
|
|
607
|
+
|
|
608
|
+
# Or with a default working directory
|
|
609
|
+
node dist/index.js /path/to/repo
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
### Testing Tools Locally
|
|
613
|
+
|
|
614
|
+
Run the integration tests to verify the server and tools:
|
|
615
|
+
|
|
616
|
+
```bash
|
|
617
|
+
# Run all tests including integration tests
|
|
618
|
+
npm test
|
|
619
|
+
|
|
620
|
+
# Run only integration tests
|
|
621
|
+
npm test -- src/server.integration.test.ts
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
### Using MCP Inspector
|
|
625
|
+
|
|
626
|
+
For interactive testing, use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector):
|
|
627
|
+
|
|
628
|
+
```bash
|
|
629
|
+
# Install the inspector
|
|
630
|
+
npm install -g @modelcontextprotocol/inspector
|
|
631
|
+
|
|
632
|
+
# Run with your server
|
|
633
|
+
npx @modelcontextprotocol/inspector node dist/index.js
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
This opens a web UI where you can:
|
|
637
|
+
- See all available tools
|
|
638
|
+
- Call tools with custom arguments
|
|
639
|
+
- View responses in real-time
|
|
640
|
+
|
|
641
|
+
### Running Tests
|
|
642
|
+
|
|
643
|
+
```bash
|
|
644
|
+
# Run all tests
|
|
645
|
+
npm test
|
|
646
|
+
|
|
647
|
+
# Run with coverage
|
|
648
|
+
npm run test:coverage
|
|
649
|
+
|
|
650
|
+
# Type checking
|
|
651
|
+
npm run typecheck
|
|
652
|
+
```
|
|
653
|
+
|
|
505
654
|
## Links
|
|
506
655
|
|
|
507
656
|
- [API Documentation](https://docs.supermodeltools.com)
|
package/dist/filtering.js
CHANGED
|
@@ -5,8 +5,19 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.maybeFilter = maybeFilter;
|
|
7
7
|
exports.isJqError = isJqError;
|
|
8
|
+
/**
|
|
9
|
+
* jq filtering utilities for JSON response transformation.
|
|
10
|
+
* Provides optional jq filter application to API responses.
|
|
11
|
+
* @module filtering
|
|
12
|
+
*/
|
|
8
13
|
// @ts-nocheck
|
|
9
14
|
const jq_web_1 = __importDefault(require("jq-web"));
|
|
15
|
+
/**
|
|
16
|
+
* Optionally applies a jq filter to a response object.
|
|
17
|
+
* @param jqFilter - The jq filter string, or undefined to skip filtering
|
|
18
|
+
* @param response - The JSON response to filter
|
|
19
|
+
* @returns The filtered response, or the original response if no filter provided
|
|
20
|
+
*/
|
|
10
21
|
async function maybeFilter(jqFilter, response) {
|
|
11
22
|
if (jqFilter && typeof jqFilter === 'string') {
|
|
12
23
|
return await jq(response, jqFilter);
|
|
@@ -15,9 +26,20 @@ async function maybeFilter(jqFilter, response) {
|
|
|
15
26
|
return response;
|
|
16
27
|
}
|
|
17
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Applies a jq filter to JSON data.
|
|
31
|
+
* @param json - The JSON data to filter
|
|
32
|
+
* @param jqFilter - The jq filter expression
|
|
33
|
+
* @returns The filtered result
|
|
34
|
+
*/
|
|
18
35
|
async function jq(json, jqFilter) {
|
|
19
36
|
return (await jq_web_1.default).json(json, jqFilter);
|
|
20
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Type guard to check if an error is a jq parsing error.
|
|
40
|
+
* @param error - The error to check
|
|
41
|
+
* @returns True if the error is a jq-related error with stderr output
|
|
42
|
+
*/
|
|
21
43
|
function isJqError(error) {
|
|
22
44
|
return error instanceof Error && 'stderr' in error;
|
|
23
45
|
}
|
package/dist/index.js
CHANGED
|
@@ -34,8 +34,17 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
};
|
|
35
35
|
})();
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
/**
|
|
38
|
+
* Entry point for the Supermodel MCP Server.
|
|
39
|
+
* Starts the MCP server with optional default working directory.
|
|
40
|
+
* @module index
|
|
41
|
+
*/
|
|
37
42
|
const server_1 = require("./server");
|
|
38
43
|
const logger = __importStar(require("./utils/logger"));
|
|
44
|
+
/**
|
|
45
|
+
* Main entry point that initializes and starts the MCP server.
|
|
46
|
+
* Accepts an optional workdir argument from the command line.
|
|
47
|
+
*/
|
|
39
48
|
async function main() {
|
|
40
49
|
// Parse command-line arguments to get optional default workdir
|
|
41
50
|
// Usage: node dist/index.js [workdir]
|
package/dist/server.js
CHANGED
|
@@ -37,10 +37,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.Server = void 0;
|
|
40
|
+
/**
|
|
41
|
+
* MCP Server implementation for the Supermodel codebase analysis tools.
|
|
42
|
+
* Provides JSON-RPC handlers for code graph generation and exploration.
|
|
43
|
+
* @module server
|
|
44
|
+
*/
|
|
40
45
|
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
41
46
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
42
47
|
const sdk_1 = require("@supermodeltools/sdk");
|
|
43
48
|
const create_supermodel_graph_1 = __importDefault(require("./tools/create-supermodel-graph"));
|
|
49
|
+
const graph_tools_1 = require("./tools/graph-tools");
|
|
44
50
|
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
45
51
|
const zip_repository_1 = require("./utils/zip-repository");
|
|
46
52
|
const undici_1 = require("undici");
|
|
@@ -77,53 +83,32 @@ class Server {
|
|
|
77
83
|
version: '0.0.1',
|
|
78
84
|
}, {
|
|
79
85
|
capabilities: { tools: {}, logging: {} },
|
|
80
|
-
instructions: `#
|
|
86
|
+
instructions: `# Supermodel: Code Graph Tools
|
|
81
87
|
|
|
82
|
-
|
|
83
|
-
- This API produces graphs of the code contained within a target directory.
|
|
84
|
-
- STRATEGY: Before debugging, planning, or analyzing a change to a code repository, generate a code graph. Use it to localize changes and find what files to search more efficiently than grep.
|
|
88
|
+
Generate code graphs to understand a codebase before making changes.
|
|
85
89
|
|
|
86
|
-
##
|
|
87
|
-
1. Generate a code graph of the given repository or a subset.
|
|
88
|
-
2. Analyze the nodes and relationships which appear to be related to your issue.
|
|
89
|
-
3. Analyze the broader context of these nodes in relationships within their domain and subdomain.
|
|
90
|
-
4. Use the graph like a diagram to navigate the codebase more efficiently than raw grep and to analyze the potential blast radius of any change.
|
|
91
|
-
|
|
92
|
-
## Planning Strategy
|
|
93
|
-
1. Generate a code graph of the given repository or a subset.
|
|
94
|
-
2. Analyze relationships like dependencies, calls, and inheritance to identify the potential blast radius of a proposed change.
|
|
95
|
-
3. Examine other elements of the same Domain and Subdomain to look for patterns including best practices or anti-patterns.
|
|
96
|
-
4. Look at the nodes you plan to change and find their physical locations, allowing you to analyze more efficiently than blind grepping.
|
|
90
|
+
## Recommended Workflow
|
|
97
91
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
3. Examine leaf nodes to see the structure of the broader tree.
|
|
102
|
-
4. Use the graph like a map to navigate the codebase more efficiently than blind grepping.
|
|
92
|
+
1. Start with \`get_domain_graph\` to understand architecture
|
|
93
|
+
2. Use \`get_call_graph\` or \`get_dependency_graph\` to drill into specific areas
|
|
94
|
+
3. Use \`get_parse_graph\` only when you need full structural detail
|
|
103
95
|
|
|
104
|
-
|
|
96
|
+
Node IDs are consistent across all graph types. A function ID from \`get_domain_graph\` can be looked up in \`get_call_graph\` results to find its callers.
|
|
105
97
|
|
|
106
|
-
|
|
107
|
-
1. Identify the affected subsystem from the issue description
|
|
108
|
-
2. Analyze only that subdirectory (e.g., \`django/db\` instead of full repo)
|
|
109
|
-
3. This is faster, uses less memory, and avoids ZIP size limits
|
|
98
|
+
## Performance
|
|
110
99
|
|
|
111
|
-
|
|
112
|
-
-
|
|
113
|
-
-
|
|
100
|
+
- First call takes 30+ seconds (API processing time)
|
|
101
|
+
- Analyze subdirectories for faster results: \`src/auth\` instead of full repo
|
|
102
|
+
- Use \`jq_filter\` to extract only the data you need
|
|
114
103
|
|
|
115
|
-
##
|
|
104
|
+
## explore_codebase
|
|
116
105
|
|
|
117
|
-
|
|
118
|
-
- \`error.recoverable: true\` → wait briefly and retry the request
|
|
119
|
-
- \`error.reportable: true\` → this is a server-side bug, not your fault
|
|
106
|
+
Returns the complete graph with all data combined. Useful for comprehensive exports or integration with external tools. For iterative analysis, prefer the individual graph tools above.
|
|
120
107
|
|
|
121
|
-
|
|
122
|
-
1. Search the \`error.repo\` issues for the \`error.code\` to avoid duplicates
|
|
123
|
-
2. If no existing issue, open one with the error code, message, and any reproduction context
|
|
124
|
-
3. If you can identify and fix the root cause, fork the repo and open a PR
|
|
108
|
+
## Errors
|
|
125
109
|
|
|
126
|
-
|
|
110
|
+
- \`error.recoverable: true\` → retry after brief wait
|
|
111
|
+
- \`error.reportable: true\` → server bug, can be reported`,
|
|
127
112
|
});
|
|
128
113
|
const config = new sdk_1.Configuration({
|
|
129
114
|
basePath: process.env.SUPERMODEL_BASE_URL || 'https://api.supermodeltools.com',
|
|
@@ -143,15 +128,29 @@ This helps the maintainers fix bugs faster and avoids wasting your iteration bud
|
|
|
143
128
|
this.setupHandlers();
|
|
144
129
|
}
|
|
145
130
|
setupHandlers() {
|
|
131
|
+
// Collect all tools: the main explore_codebase tool plus individual graph tools
|
|
132
|
+
const allTools = [
|
|
133
|
+
create_supermodel_graph_1.default,
|
|
134
|
+
...graph_tools_1.graphTools,
|
|
135
|
+
];
|
|
136
|
+
// Create a map for quick handler lookup, checking for duplicates
|
|
137
|
+
const toolMap = new Map();
|
|
138
|
+
for (const t of allTools) {
|
|
139
|
+
if (toolMap.has(t.tool.name)) {
|
|
140
|
+
throw new Error(`Duplicate tool name: ${t.tool.name}`);
|
|
141
|
+
}
|
|
142
|
+
toolMap.set(t.tool.name, t);
|
|
143
|
+
}
|
|
146
144
|
this.server.server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
147
145
|
return {
|
|
148
|
-
tools:
|
|
146
|
+
tools: allTools.map(t => t.tool),
|
|
149
147
|
};
|
|
150
148
|
});
|
|
151
149
|
this.server.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
152
150
|
const { name, arguments: args } = request.params;
|
|
153
|
-
|
|
154
|
-
|
|
151
|
+
const tool = toolMap.get(name);
|
|
152
|
+
if (tool) {
|
|
153
|
+
return tool.handler(this.client, args, this.defaultWorkdir);
|
|
155
154
|
}
|
|
156
155
|
throw new Error(`Unknown tool: ${name}`);
|
|
157
156
|
});
|
|
@@ -34,18 +34,15 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.handler = exports.tool = exports.metadata = void 0;
|
|
37
|
-
exports.classifyApiError = classifyApiError;
|
|
38
37
|
const promises_1 = require("fs/promises");
|
|
39
|
-
const
|
|
40
|
-
const crypto_1 = require("crypto");
|
|
38
|
+
const buffer_1 = require("buffer");
|
|
41
39
|
const path_1 = require("path");
|
|
42
40
|
const types_1 = require("../types");
|
|
43
41
|
const filtering_1 = require("../filtering");
|
|
44
42
|
const queries_1 = require("../queries");
|
|
45
43
|
const zip_repository_1 = require("../utils/zip-repository");
|
|
46
44
|
const logger = __importStar(require("../utils/logger"));
|
|
47
|
-
const
|
|
48
|
-
const REPORT_SUGGESTION = 'This may be a bug in the MCP server. You can help by opening an issue at https://github.com/supermodeltools/mcp/issues with the error details, or fork the repo and open a PR with a fix.';
|
|
45
|
+
const api_helpers_1 = require("../utils/api-helpers");
|
|
49
46
|
exports.metadata = {
|
|
50
47
|
resource: 'graphs',
|
|
51
48
|
operation: 'write',
|
|
@@ -202,49 +199,13 @@ Query types available: graph_status, summary, get_node, search, list_nodes, func
|
|
|
202
199
|
required: [],
|
|
203
200
|
},
|
|
204
201
|
};
|
|
205
|
-
/**
|
|
206
|
-
* Generate an idempotency key in format {repo}-{pathHash}:supermodel:{hash}
|
|
207
|
-
* Includes path hash to prevent collisions between same-named repos
|
|
208
|
-
*/
|
|
209
|
-
function generateIdempotencyKey(directory) {
|
|
210
|
-
const repoName = (0, path_1.basename)(directory);
|
|
211
|
-
const absolutePath = (0, path_1.resolve)(directory);
|
|
212
|
-
// Always include path hash to prevent collisions
|
|
213
|
-
const pathHash = (0, crypto_1.createHash)('sha1').update(absolutePath).digest('hex').substring(0, 7);
|
|
214
|
-
let hash;
|
|
215
|
-
let statusHash = '';
|
|
216
|
-
try {
|
|
217
|
-
// Get git commit hash
|
|
218
|
-
hash = (0, child_process_1.execSync)('git rev-parse --short HEAD', {
|
|
219
|
-
cwd: directory,
|
|
220
|
-
encoding: 'utf-8',
|
|
221
|
-
}).trim();
|
|
222
|
-
// Include working tree status in hash to detect uncommitted changes
|
|
223
|
-
const statusOutput = (0, child_process_1.execSync)('git status --porcelain', {
|
|
224
|
-
cwd: directory,
|
|
225
|
-
encoding: 'utf-8',
|
|
226
|
-
}).toString();
|
|
227
|
-
if (statusOutput) {
|
|
228
|
-
// Create hash of status output
|
|
229
|
-
statusHash = '-' + (0, crypto_1.createHash)('sha1')
|
|
230
|
-
.update(statusOutput)
|
|
231
|
-
.digest('hex')
|
|
232
|
-
.substring(0, 7);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
catch {
|
|
236
|
-
// Fallback for non-git directories: use path hash as main identifier
|
|
237
|
-
hash = pathHash;
|
|
238
|
-
}
|
|
239
|
-
return `${repoName}-${pathHash}:supermodel:${hash}${statusHash}`;
|
|
240
|
-
}
|
|
241
202
|
const handler = async (client, args, defaultWorkdir) => {
|
|
242
203
|
if (!args) {
|
|
243
204
|
args = {};
|
|
244
205
|
}
|
|
245
206
|
const { jq_filter, directory: providedDirectory, query, targetId, searchText, namePattern, filePathPrefix, labels, depth, relationshipTypes, limit, includeRaw, } = args;
|
|
246
207
|
// Use provided directory or fall back to default workdir
|
|
247
|
-
const directory = providedDirectory
|
|
208
|
+
const directory = providedDirectory ?? defaultWorkdir;
|
|
248
209
|
// Validate directory - check if explicitly invalid first
|
|
249
210
|
if (providedDirectory !== undefined && typeof providedDirectory !== 'string') {
|
|
250
211
|
logger.error('Invalid directory parameter:', providedDirectory);
|
|
@@ -274,7 +235,7 @@ const handler = async (client, args, defaultWorkdir) => {
|
|
|
274
235
|
logger.debug('Using default workdir:', directory);
|
|
275
236
|
}
|
|
276
237
|
// Generate idempotency key for API request
|
|
277
|
-
const idempotencyKey = generateIdempotencyKey(directory);
|
|
238
|
+
const idempotencyKey = (0, api_helpers_1.generateIdempotencyKey)(directory);
|
|
278
239
|
logger.debug('Auto-generated idempotency key:', idempotencyKey);
|
|
279
240
|
// Check if we can skip zipping (graph already cached)
|
|
280
241
|
// Use get() atomically to avoid TOCTOU race condition
|
|
@@ -308,7 +269,7 @@ const handler = async (client, args, defaultWorkdir) => {
|
|
|
308
269
|
const zipResult = await (0, zip_repository_1.zipRepository)(directory);
|
|
309
270
|
zipPath = zipResult.path;
|
|
310
271
|
cleanup = zipResult.cleanup;
|
|
311
|
-
logger.debug('Auto-zip complete:', zipResult.fileCount, 'files,', formatBytes(zipResult.sizeBytes));
|
|
272
|
+
logger.debug('Auto-zip complete:', zipResult.fileCount, 'files,', (0, api_helpers_1.formatBytes)(zipResult.sizeBytes));
|
|
312
273
|
}
|
|
313
274
|
catch (error) {
|
|
314
275
|
// Log full error details for debugging
|
|
@@ -366,8 +327,8 @@ const handler = async (client, args, defaultWorkdir) => {
|
|
|
366
327
|
code: 'ZIP_CREATION_FAILED',
|
|
367
328
|
recoverable: false,
|
|
368
329
|
reportable: true,
|
|
369
|
-
repo: REPORT_REPO,
|
|
370
|
-
suggestion: REPORT_SUGGESTION,
|
|
330
|
+
repo: api_helpers_1.REPORT_REPO,
|
|
331
|
+
suggestion: api_helpers_1.REPORT_SUGGESTION,
|
|
371
332
|
details: { directory: (0, path_1.basename)(directory), errorType: error.name || 'Error' },
|
|
372
333
|
});
|
|
373
334
|
}
|
|
@@ -406,18 +367,6 @@ const handler = async (client, args, defaultWorkdir) => {
|
|
|
406
367
|
}
|
|
407
368
|
};
|
|
408
369
|
exports.handler = handler;
|
|
409
|
-
/**
|
|
410
|
-
* Format bytes as human-readable string
|
|
411
|
-
*/
|
|
412
|
-
function formatBytes(bytes) {
|
|
413
|
-
if (bytes < 1024)
|
|
414
|
-
return `${bytes} B`;
|
|
415
|
-
if (bytes < 1024 * 1024)
|
|
416
|
-
return `${(bytes / 1024).toFixed(2)} KB`;
|
|
417
|
-
if (bytes < 1024 * 1024 * 1024)
|
|
418
|
-
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
419
|
-
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
420
|
-
}
|
|
421
370
|
/**
|
|
422
371
|
* Handle query-based requests when graph is already cached
|
|
423
372
|
* Uses the cached graph directly to avoid TOCTOU issues
|
|
@@ -484,7 +433,7 @@ async function handleQueryMode(client, params) {
|
|
|
484
433
|
}
|
|
485
434
|
catch (error) {
|
|
486
435
|
// Error details are already logged by fetchFromApi and logErrorResponse
|
|
487
|
-
return (0, types_1.asErrorResult)(classifyApiError(error));
|
|
436
|
+
return (0, types_1.asErrorResult)((0, api_helpers_1.classifyApiError)(error));
|
|
488
437
|
}
|
|
489
438
|
}
|
|
490
439
|
// Handle query errors
|
|
@@ -581,7 +530,7 @@ function logRequest(url, method, bodySize, idempotencyKey) {
|
|
|
581
530
|
logger.debug(` Method: ${method}`);
|
|
582
531
|
logger.debug(` URL: ${url}`);
|
|
583
532
|
logger.debug(` Idempotency-Key: ${idempotencyKey}`);
|
|
584
|
-
logger.debug(` Body size: ${formatBytes(bodySize)}`);
|
|
533
|
+
logger.debug(` Body size: ${(0, api_helpers_1.formatBytes)(bodySize)}`);
|
|
585
534
|
logger.debug(` Content-Type: multipart/form-data`);
|
|
586
535
|
}
|
|
587
536
|
/**
|
|
@@ -590,7 +539,7 @@ function logRequest(url, method, bodySize, idempotencyKey) {
|
|
|
590
539
|
function logResponse(status, statusText, responseSize, duration) {
|
|
591
540
|
logger.debug(`[${getTimestamp()}] [API RESPONSE]`);
|
|
592
541
|
logger.debug(` Status: ${status} ${statusText}`);
|
|
593
|
-
logger.debug(` Response size: ${formatBytes(responseSize)}`);
|
|
542
|
+
logger.debug(` Response size: ${(0, api_helpers_1.formatBytes)(responseSize)}`);
|
|
594
543
|
logger.debug(` Duration: ${duration}ms`);
|
|
595
544
|
}
|
|
596
545
|
/**
|
|
@@ -653,9 +602,9 @@ async function fetchFromApi(client, file, idempotencyKey) {
|
|
|
653
602
|
const startTime = Date.now();
|
|
654
603
|
logger.debug('Reading file:', file);
|
|
655
604
|
const fileBuffer = await (0, promises_1.readFile)(file);
|
|
656
|
-
const fileBlob = new Blob([fileBuffer], { type: 'application/zip' });
|
|
605
|
+
const fileBlob = new buffer_1.Blob([fileBuffer], { type: 'application/zip' });
|
|
657
606
|
const fileSize = fileBuffer.length;
|
|
658
|
-
logger.debug('File size:', formatBytes(fileSize));
|
|
607
|
+
logger.debug('File size:', (0, api_helpers_1.formatBytes)(fileSize));
|
|
659
608
|
// Get the base URL from environment or use default
|
|
660
609
|
const baseUrl = process.env.SUPERMODEL_BASE_URL || 'https://api.supermodeltools.com';
|
|
661
610
|
const apiUrl = `${baseUrl}/v1/graphs/supermodel`;
|
|
@@ -716,128 +665,6 @@ async function fetchFromApi(client, file, idempotencyKey) {
|
|
|
716
665
|
throw error;
|
|
717
666
|
}
|
|
718
667
|
}
|
|
719
|
-
/**
|
|
720
|
-
* Classify an API error into a structured error response.
|
|
721
|
-
* Extracts HTTP status, network conditions, and timeout signals
|
|
722
|
-
* to produce an agent-actionable error with recovery guidance.
|
|
723
|
-
*/
|
|
724
|
-
function classifyApiError(error) {
|
|
725
|
-
// Guard against non-Error throws (strings, nulls, plain objects)
|
|
726
|
-
if (!error || typeof error !== 'object') {
|
|
727
|
-
return {
|
|
728
|
-
type: 'internal_error',
|
|
729
|
-
message: typeof error === 'string' ? error : 'An unexpected error occurred.',
|
|
730
|
-
code: 'UNKNOWN_ERROR',
|
|
731
|
-
recoverable: false,
|
|
732
|
-
reportable: true,
|
|
733
|
-
repo: REPORT_REPO,
|
|
734
|
-
suggestion: REPORT_SUGGESTION,
|
|
735
|
-
details: { errorType: typeof error },
|
|
736
|
-
};
|
|
737
|
-
}
|
|
738
|
-
if (error.response) {
|
|
739
|
-
const status = error.response.status;
|
|
740
|
-
switch (status) {
|
|
741
|
-
case 401:
|
|
742
|
-
return {
|
|
743
|
-
type: 'authentication_error',
|
|
744
|
-
message: 'Invalid or missing API key.',
|
|
745
|
-
code: 'INVALID_API_KEY',
|
|
746
|
-
recoverable: false,
|
|
747
|
-
suggestion: 'Set the SUPERMODEL_API_KEY environment variable and restart the MCP server.',
|
|
748
|
-
details: { apiKeySet: !!process.env.SUPERMODEL_API_KEY, httpStatus: 401 },
|
|
749
|
-
};
|
|
750
|
-
case 403:
|
|
751
|
-
return {
|
|
752
|
-
type: 'authorization_error',
|
|
753
|
-
message: 'API key does not have permission for this operation.',
|
|
754
|
-
code: 'FORBIDDEN',
|
|
755
|
-
recoverable: false,
|
|
756
|
-
suggestion: 'Verify your API key has the correct permissions. Contact support if unexpected.',
|
|
757
|
-
details: { httpStatus: 403 },
|
|
758
|
-
};
|
|
759
|
-
case 404:
|
|
760
|
-
return {
|
|
761
|
-
type: 'not_found_error',
|
|
762
|
-
message: 'API endpoint not found.',
|
|
763
|
-
code: 'ENDPOINT_NOT_FOUND',
|
|
764
|
-
recoverable: false,
|
|
765
|
-
suggestion: 'Check SUPERMODEL_BASE_URL environment variable. Default: https://api.supermodeltools.com',
|
|
766
|
-
details: { baseUrl: process.env.SUPERMODEL_BASE_URL || 'https://api.supermodeltools.com', httpStatus: 404 },
|
|
767
|
-
};
|
|
768
|
-
case 429:
|
|
769
|
-
return {
|
|
770
|
-
type: 'rate_limit_error',
|
|
771
|
-
message: 'API rate limit exceeded.',
|
|
772
|
-
code: 'RATE_LIMITED',
|
|
773
|
-
recoverable: true,
|
|
774
|
-
suggestion: 'Wait 30-60 seconds and retry. Consider analyzing smaller subdirectories to reduce API calls.',
|
|
775
|
-
details: { httpStatus: 429 },
|
|
776
|
-
};
|
|
777
|
-
case 500:
|
|
778
|
-
case 502:
|
|
779
|
-
case 503:
|
|
780
|
-
case 504:
|
|
781
|
-
return {
|
|
782
|
-
type: 'internal_error',
|
|
783
|
-
message: `Supermodel API server error (HTTP ${status}).`,
|
|
784
|
-
code: 'SERVER_ERROR',
|
|
785
|
-
recoverable: true,
|
|
786
|
-
reportable: true,
|
|
787
|
-
repo: REPORT_REPO,
|
|
788
|
-
suggestion: 'The API may be temporarily unavailable. Wait a few minutes and retry. If persistent, open an issue at https://github.com/supermodeltools/mcp/issues with the error details, or fork the repo and open a PR with a fix.',
|
|
789
|
-
details: { httpStatus: status },
|
|
790
|
-
};
|
|
791
|
-
default: {
|
|
792
|
-
const isServerError = status >= 500;
|
|
793
|
-
return {
|
|
794
|
-
type: isServerError ? 'internal_error' : 'validation_error',
|
|
795
|
-
message: `API request failed with HTTP ${status}.`,
|
|
796
|
-
code: 'API_ERROR',
|
|
797
|
-
recoverable: isServerError,
|
|
798
|
-
...(isServerError && {
|
|
799
|
-
reportable: true,
|
|
800
|
-
repo: REPORT_REPO,
|
|
801
|
-
suggestion: 'The API may be temporarily unavailable. Wait a few minutes and retry. If persistent, open an issue at https://github.com/supermodeltools/mcp/issues with the error details, or fork the repo and open a PR with a fix.',
|
|
802
|
-
}),
|
|
803
|
-
...(!isServerError && { suggestion: 'Check the request parameters and base URL configuration.' }),
|
|
804
|
-
details: { httpStatus: status },
|
|
805
|
-
};
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
if (error.request) {
|
|
810
|
-
// Distinguish timeout from general network failure
|
|
811
|
-
if (error.code === 'UND_ERR_HEADERS_TIMEOUT' || error.code === 'UND_ERR_BODY_TIMEOUT' || error.message?.includes('timeout')) {
|
|
812
|
-
return {
|
|
813
|
-
type: 'timeout_error',
|
|
814
|
-
message: 'API request timed out. The codebase may be too large for a single analysis.',
|
|
815
|
-
code: 'REQUEST_TIMEOUT',
|
|
816
|
-
recoverable: true,
|
|
817
|
-
suggestion: 'Analyze a smaller subdirectory (e.g. directory="/repo/src/core") or increase SUPERMODEL_TIMEOUT_MS.',
|
|
818
|
-
};
|
|
819
|
-
}
|
|
820
|
-
return {
|
|
821
|
-
type: 'network_error',
|
|
822
|
-
message: 'No response from Supermodel API server.',
|
|
823
|
-
code: 'NO_RESPONSE',
|
|
824
|
-
recoverable: true,
|
|
825
|
-
suggestion: 'Check network connectivity. Verify the API is reachable at the configured base URL.',
|
|
826
|
-
details: { baseUrl: process.env.SUPERMODEL_BASE_URL || 'https://api.supermodeltools.com' },
|
|
827
|
-
};
|
|
828
|
-
}
|
|
829
|
-
// Catch-all for unexpected errors - include the actual message
|
|
830
|
-
return {
|
|
831
|
-
type: 'internal_error',
|
|
832
|
-
message: error.message || 'An unexpected error occurred.',
|
|
833
|
-
code: 'UNKNOWN_ERROR',
|
|
834
|
-
recoverable: false,
|
|
835
|
-
reportable: true,
|
|
836
|
-
repo: REPORT_REPO,
|
|
837
|
-
suggestion: REPORT_SUGGESTION,
|
|
838
|
-
details: { errorType: error.name || 'Error' },
|
|
839
|
-
};
|
|
840
|
-
}
|
|
841
668
|
/**
|
|
842
669
|
* Legacy mode: direct jq filtering on API response
|
|
843
670
|
*/
|
|
@@ -858,7 +685,7 @@ async function handleLegacyMode(client, file, idempotencyKey, jq_filter) {
|
|
|
858
685
|
});
|
|
859
686
|
}
|
|
860
687
|
// Error details are already logged by fetchFromApi and logErrorResponse
|
|
861
|
-
return (0, types_1.asErrorResult)(classifyApiError(error));
|
|
688
|
+
return (0, types_1.asErrorResult)((0, api_helpers_1.classifyApiError)(error));
|
|
862
689
|
}
|
|
863
690
|
}
|
|
864
691
|
exports.default = { metadata: exports.metadata, tool: exports.tool, handler: exports.handler };
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Individual graph type tools for targeted codebase analysis.
|
|
4
|
+
* Each tool calls a specific graph API endpoint for focused results.
|
|
5
|
+
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
+
}) : function(o, v) {
|
|
20
|
+
o["default"] = v;
|
|
21
|
+
});
|
|
22
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
+
var ownKeys = function(o) {
|
|
24
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
+
var ar = [];
|
|
26
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
+
return ar;
|
|
28
|
+
};
|
|
29
|
+
return ownKeys(o);
|
|
30
|
+
};
|
|
31
|
+
return function (mod) {
|
|
32
|
+
if (mod && mod.__esModule) return mod;
|
|
33
|
+
var result = {};
|
|
34
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
+
__setModuleDefault(result, mod);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
exports.graphTools = exports.parseGraphTool = exports.domainGraphTool = exports.dependencyGraphTool = exports.callGraphTool = void 0;
|
|
41
|
+
const promises_1 = require("fs/promises");
|
|
42
|
+
const buffer_1 = require("buffer");
|
|
43
|
+
const types_1 = require("../types");
|
|
44
|
+
const filtering_1 = require("../filtering");
|
|
45
|
+
const zip_repository_1 = require("../utils/zip-repository");
|
|
46
|
+
const logger = __importStar(require("../utils/logger"));
|
|
47
|
+
const api_helpers_1 = require("../utils/api-helpers");
|
|
48
|
+
const GRAPH_TYPES = [
|
|
49
|
+
{
|
|
50
|
+
name: 'call',
|
|
51
|
+
toolName: 'get_call_graph',
|
|
52
|
+
description: `Generate a call graph showing function-to-function call relationships.
|
|
53
|
+
|
|
54
|
+
Returns: Function nodes with "calls" relationships between them.
|
|
55
|
+
|
|
56
|
+
Use this to:
|
|
57
|
+
- Find all callers of a specific function
|
|
58
|
+
- Find all functions called by a specific function
|
|
59
|
+
- Trace execution flow through the codebase
|
|
60
|
+
- Debug by following call chains
|
|
61
|
+
|
|
62
|
+
Best for: Debugging, understanding "what calls what", tracing execution paths.`,
|
|
63
|
+
endpoint: '/v1/graphs/call',
|
|
64
|
+
operationId: 'generateCallGraph',
|
|
65
|
+
apiMethod: 'generateCallGraph',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'dependency',
|
|
69
|
+
toolName: 'get_dependency_graph',
|
|
70
|
+
description: `Generate a dependency graph showing import relationships between files.
|
|
71
|
+
|
|
72
|
+
Returns: File nodes with "IMPORTS" relationships between them.
|
|
73
|
+
|
|
74
|
+
Use this to:
|
|
75
|
+
- Map which files import which other files
|
|
76
|
+
- Find circular dependencies
|
|
77
|
+
- Understand module coupling
|
|
78
|
+
- Plan safe refactoring of imports
|
|
79
|
+
|
|
80
|
+
Best for: Refactoring, understanding module dependencies, finding import cycles.`,
|
|
81
|
+
endpoint: '/v1/graphs/dependency',
|
|
82
|
+
operationId: 'generateDependencyGraph',
|
|
83
|
+
apiMethod: 'generateDependencyGraph',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'domain',
|
|
87
|
+
toolName: 'get_domain_graph',
|
|
88
|
+
description: `Generate a domain classification graph showing high-level architecture.
|
|
89
|
+
|
|
90
|
+
Returns: Domains with descriptions, responsibilities, subdomains, and file/function/class assignments.
|
|
91
|
+
|
|
92
|
+
Use this to:
|
|
93
|
+
- Understand the architectural structure of a codebase
|
|
94
|
+
- See how code is organized into logical domains
|
|
95
|
+
- Identify domain boundaries and responsibilities
|
|
96
|
+
- Get a bird's-eye view before diving into details
|
|
97
|
+
|
|
98
|
+
Best for: New codebases, architecture overview, understanding system organization.`,
|
|
99
|
+
endpoint: '/v1/graphs/domain',
|
|
100
|
+
operationId: 'generateDomainGraph',
|
|
101
|
+
apiMethod: 'generateDomainGraph',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: 'parse',
|
|
105
|
+
toolName: 'get_parse_graph',
|
|
106
|
+
description: `Generate a full parse graph with all code structure elements.
|
|
107
|
+
|
|
108
|
+
Returns: All nodes (File, Directory, Class, Function, Type) and structural relationships (CONTAINS, DEFINES, DECLARES, IMPORTS).
|
|
109
|
+
|
|
110
|
+
Use this to:
|
|
111
|
+
- Get complete structural information about the codebase
|
|
112
|
+
- Find all classes, functions, and types
|
|
113
|
+
- Understand containment and definition relationships
|
|
114
|
+
- Support detailed refactoring analysis
|
|
115
|
+
|
|
116
|
+
Best for: Comprehensive analysis when you need the full code structure.`,
|
|
117
|
+
endpoint: '/v1/graphs/parse',
|
|
118
|
+
operationId: 'generateParseGraph',
|
|
119
|
+
apiMethod: 'generateParseGraph',
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
/**
|
|
123
|
+
* Create a tool definition and handler for a specific graph type
|
|
124
|
+
*/
|
|
125
|
+
function createGraphTool(config) {
|
|
126
|
+
const metadata = {
|
|
127
|
+
resource: 'graphs',
|
|
128
|
+
operation: 'write',
|
|
129
|
+
tags: [config.name],
|
|
130
|
+
httpMethod: 'post',
|
|
131
|
+
httpPath: config.endpoint,
|
|
132
|
+
operationId: config.operationId,
|
|
133
|
+
};
|
|
134
|
+
const tool = {
|
|
135
|
+
name: config.toolName,
|
|
136
|
+
description: config.description,
|
|
137
|
+
inputSchema: {
|
|
138
|
+
type: 'object',
|
|
139
|
+
properties: {
|
|
140
|
+
directory: {
|
|
141
|
+
type: 'string',
|
|
142
|
+
description: 'Path to the repository directory to analyze.',
|
|
143
|
+
},
|
|
144
|
+
jq_filter: {
|
|
145
|
+
type: 'string',
|
|
146
|
+
title: 'jq Filter',
|
|
147
|
+
description: 'Optional jq filter to extract specific data from the response.',
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
required: [],
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
const handler = async (client, args, defaultWorkdir) => {
|
|
154
|
+
if (!args) {
|
|
155
|
+
args = {};
|
|
156
|
+
}
|
|
157
|
+
const { jq_filter, directory: providedDirectory } = args;
|
|
158
|
+
if (providedDirectory !== undefined && typeof providedDirectory !== 'string') {
|
|
159
|
+
return (0, types_1.asErrorResult)({
|
|
160
|
+
type: 'validation_error',
|
|
161
|
+
message: 'Invalid "directory" parameter. Provide a valid directory path as a string.',
|
|
162
|
+
code: 'INVALID_DIRECTORY',
|
|
163
|
+
recoverable: false,
|
|
164
|
+
suggestion: 'Pass directory as a string path, e.g. directory="/workspace/my-repo".',
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (jq_filter !== undefined && typeof jq_filter !== 'string') {
|
|
168
|
+
return (0, types_1.asErrorResult)({
|
|
169
|
+
type: 'validation_error',
|
|
170
|
+
message: 'Invalid "jq_filter" parameter. Provide a jq filter string.',
|
|
171
|
+
code: 'INVALID_JQ_FILTER',
|
|
172
|
+
recoverable: false,
|
|
173
|
+
suggestion: 'Pass jq_filter as a string, e.g. jq_filter=".nodes".',
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
const directory = providedDirectory ?? defaultWorkdir;
|
|
177
|
+
if (!directory || typeof directory !== 'string') {
|
|
178
|
+
return (0, types_1.asErrorResult)({
|
|
179
|
+
type: 'validation_error',
|
|
180
|
+
message: 'No "directory" parameter provided and no default workdir configured.',
|
|
181
|
+
code: 'MISSING_DIRECTORY',
|
|
182
|
+
recoverable: false,
|
|
183
|
+
suggestion: 'Provide a directory path or start the MCP server with a workdir argument.',
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
const idempotencyKey = (0, api_helpers_1.generateIdempotencyKey)(directory, config.name);
|
|
187
|
+
logger.debug(`[${config.toolName}] Idempotency key:`, idempotencyKey);
|
|
188
|
+
// Create ZIP of repository
|
|
189
|
+
let zipPath;
|
|
190
|
+
let cleanup = null;
|
|
191
|
+
try {
|
|
192
|
+
const zipResult = await (0, zip_repository_1.zipRepository)(directory);
|
|
193
|
+
zipPath = zipResult.path;
|
|
194
|
+
cleanup = zipResult.cleanup;
|
|
195
|
+
logger.debug(`[${config.toolName}] ZIP created:`, zipResult.fileCount, 'files,', (0, api_helpers_1.formatBytes)(zipResult.sizeBytes));
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
const message = typeof error?.message === 'string' ? error.message : String(error);
|
|
199
|
+
if (message.includes('does not exist')) {
|
|
200
|
+
return (0, types_1.asErrorResult)({
|
|
201
|
+
type: 'not_found_error',
|
|
202
|
+
message: `Directory not found: ${directory}`,
|
|
203
|
+
code: 'DIRECTORY_NOT_FOUND',
|
|
204
|
+
recoverable: false,
|
|
205
|
+
suggestion: 'Verify the path exists.',
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
return (0, types_1.asErrorResult)({
|
|
209
|
+
type: 'internal_error',
|
|
210
|
+
message: `Failed to create ZIP archive: ${message}`,
|
|
211
|
+
code: 'ZIP_CREATION_FAILED',
|
|
212
|
+
recoverable: false,
|
|
213
|
+
reportable: true,
|
|
214
|
+
repo: api_helpers_1.REPORT_REPO,
|
|
215
|
+
suggestion: api_helpers_1.REPORT_SUGGESTION,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
const fileBuffer = await (0, promises_1.readFile)(zipPath);
|
|
220
|
+
const fileBlob = new buffer_1.Blob([fileBuffer], { type: 'application/zip' });
|
|
221
|
+
logger.debug(`[${config.toolName}] Calling API...`);
|
|
222
|
+
console.error(`[Supermodel] Generating ${config.name} graph...`);
|
|
223
|
+
// Call the appropriate API method via SupermodelClient
|
|
224
|
+
const apiMethod = client.graphs[config.apiMethod].bind(client.graphs);
|
|
225
|
+
const response = await apiMethod(fileBlob, { idempotencyKey });
|
|
226
|
+
console.error(`[Supermodel] ${config.name} graph complete.`);
|
|
227
|
+
// Apply optional jq filter
|
|
228
|
+
const result = await (0, filtering_1.maybeFilter)(jq_filter, response);
|
|
229
|
+
return (0, types_1.asTextContentResult)(result);
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
if ((0, filtering_1.isJqError)(error)) {
|
|
233
|
+
logger.error(`[${config.toolName}] jq filter error:`, error.message);
|
|
234
|
+
return (0, types_1.asErrorResult)({
|
|
235
|
+
type: 'validation_error',
|
|
236
|
+
message: `Invalid jq filter syntax: ${error.message}`,
|
|
237
|
+
code: 'INVALID_JQ_FILTER',
|
|
238
|
+
recoverable: false,
|
|
239
|
+
suggestion: 'Check jq filter syntax. Example: jq_filter=".nodes" or jq_filter=".graph.nodeCount"',
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
logger.error(`[${config.toolName}] API error:`, error.message);
|
|
243
|
+
return (0, types_1.asErrorResult)((0, api_helpers_1.classifyApiError)(error));
|
|
244
|
+
}
|
|
245
|
+
finally {
|
|
246
|
+
if (cleanup) {
|
|
247
|
+
try {
|
|
248
|
+
await cleanup();
|
|
249
|
+
}
|
|
250
|
+
catch (cleanupError) {
|
|
251
|
+
logger.warn(`[${config.toolName}] Cleanup failed:`, cleanupError);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
return { metadata, tool, handler };
|
|
257
|
+
}
|
|
258
|
+
// Helper to find graph type by name (safer than array indexing)
|
|
259
|
+
function getGraphType(name) {
|
|
260
|
+
const config = GRAPH_TYPES.find(t => t.name === name);
|
|
261
|
+
if (!config) {
|
|
262
|
+
throw new Error(`Unknown graph type: ${name}`);
|
|
263
|
+
}
|
|
264
|
+
return config;
|
|
265
|
+
}
|
|
266
|
+
// Create all graph tools
|
|
267
|
+
exports.callGraphTool = createGraphTool(getGraphType('call'));
|
|
268
|
+
exports.dependencyGraphTool = createGraphTool(getGraphType('dependency'));
|
|
269
|
+
exports.domainGraphTool = createGraphTool(getGraphType('domain'));
|
|
270
|
+
exports.parseGraphTool = createGraphTool(getGraphType('parse'));
|
|
271
|
+
// Export all tools as an array for easy registration
|
|
272
|
+
exports.graphTools = [
|
|
273
|
+
exports.callGraphTool,
|
|
274
|
+
exports.dependencyGraphTool,
|
|
275
|
+
exports.domainGraphTool,
|
|
276
|
+
exports.parseGraphTool,
|
|
277
|
+
];
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared utilities for API operations across graph tools.
|
|
4
|
+
* Extracted to eliminate code duplication between graph-tools.ts and create-supermodel-graph.ts.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.REPORT_SUGGESTION = exports.REPORT_REPO = void 0;
|
|
8
|
+
exports.formatBytes = formatBytes;
|
|
9
|
+
exports.generateIdempotencyKey = generateIdempotencyKey;
|
|
10
|
+
exports.classifyApiError = classifyApiError;
|
|
11
|
+
const child_process_1 = require("child_process");
|
|
12
|
+
const crypto_1 = require("crypto");
|
|
13
|
+
const path_1 = require("path");
|
|
14
|
+
exports.REPORT_REPO = 'https://github.com/supermodeltools/mcp.git';
|
|
15
|
+
exports.REPORT_SUGGESTION = 'This may be a bug in the MCP server. You can help by opening an issue at https://github.com/supermodeltools/mcp/issues with the error details, or fork the repo and open a PR with a fix.';
|
|
16
|
+
/**
|
|
17
|
+
* Format bytes as human-readable string
|
|
18
|
+
*/
|
|
19
|
+
function formatBytes(bytes) {
|
|
20
|
+
if (bytes < 1024)
|
|
21
|
+
return `${bytes} B`;
|
|
22
|
+
if (bytes < 1024 * 1024)
|
|
23
|
+
return `${(bytes / 1024).toFixed(2)} KB`;
|
|
24
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
25
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
26
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Generate an idempotency key in format {repo}-{pathHash}:{graphType}:{hash}
|
|
30
|
+
* Includes path hash to prevent collisions between same-named repos
|
|
31
|
+
*/
|
|
32
|
+
function generateIdempotencyKey(directory, graphType = 'supermodel') {
|
|
33
|
+
const repoName = (0, path_1.basename)(directory);
|
|
34
|
+
const absolutePath = (0, path_1.resolve)(directory);
|
|
35
|
+
// Always include path hash to prevent collisions
|
|
36
|
+
const pathHash = (0, crypto_1.createHash)('sha1').update(absolutePath).digest('hex').substring(0, 7);
|
|
37
|
+
let hash;
|
|
38
|
+
let statusHash = '';
|
|
39
|
+
try {
|
|
40
|
+
// Get git commit hash
|
|
41
|
+
hash = (0, child_process_1.execSync)('git rev-parse --short HEAD', {
|
|
42
|
+
cwd: directory,
|
|
43
|
+
encoding: 'utf-8',
|
|
44
|
+
}).trim();
|
|
45
|
+
// Include working tree status in hash to detect uncommitted changes
|
|
46
|
+
const statusOutput = (0, child_process_1.execSync)('git status --porcelain', {
|
|
47
|
+
cwd: directory,
|
|
48
|
+
encoding: 'utf-8',
|
|
49
|
+
}).toString();
|
|
50
|
+
if (statusOutput) {
|
|
51
|
+
// Create hash of status output
|
|
52
|
+
statusHash = '-' + (0, crypto_1.createHash)('sha1')
|
|
53
|
+
.update(statusOutput)
|
|
54
|
+
.digest('hex')
|
|
55
|
+
.substring(0, 7);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Fallback for non-git directories: use path hash as main identifier
|
|
60
|
+
hash = pathHash;
|
|
61
|
+
}
|
|
62
|
+
return `${repoName}-${pathHash}:${graphType}:${hash}${statusHash}`;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Classify an API error into a structured error response.
|
|
66
|
+
* Extracts HTTP status, network conditions, and timeout signals
|
|
67
|
+
* to produce an agent-actionable error with recovery guidance.
|
|
68
|
+
*/
|
|
69
|
+
function classifyApiError(error) {
|
|
70
|
+
// Guard against non-Error throws (strings, nulls, plain objects)
|
|
71
|
+
if (!error || typeof error !== 'object') {
|
|
72
|
+
return {
|
|
73
|
+
type: 'internal_error',
|
|
74
|
+
message: typeof error === 'string' ? error : 'An unexpected error occurred.',
|
|
75
|
+
code: 'UNKNOWN_ERROR',
|
|
76
|
+
recoverable: false,
|
|
77
|
+
reportable: true,
|
|
78
|
+
repo: exports.REPORT_REPO,
|
|
79
|
+
suggestion: exports.REPORT_SUGGESTION,
|
|
80
|
+
details: { errorType: typeof error },
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (error.response) {
|
|
84
|
+
const status = error.response.status;
|
|
85
|
+
switch (status) {
|
|
86
|
+
case 401:
|
|
87
|
+
return {
|
|
88
|
+
type: 'authentication_error',
|
|
89
|
+
message: 'Invalid or missing API key.',
|
|
90
|
+
code: 'INVALID_API_KEY',
|
|
91
|
+
recoverable: false,
|
|
92
|
+
suggestion: 'Set the SUPERMODEL_API_KEY environment variable and restart the MCP server.',
|
|
93
|
+
details: { apiKeySet: !!process.env.SUPERMODEL_API_KEY, httpStatus: 401 },
|
|
94
|
+
};
|
|
95
|
+
case 403:
|
|
96
|
+
return {
|
|
97
|
+
type: 'authorization_error',
|
|
98
|
+
message: 'API key does not have permission for this operation.',
|
|
99
|
+
code: 'FORBIDDEN',
|
|
100
|
+
recoverable: false,
|
|
101
|
+
suggestion: 'Verify your API key has the correct permissions. Contact support if unexpected.',
|
|
102
|
+
details: { httpStatus: 403 },
|
|
103
|
+
};
|
|
104
|
+
case 404:
|
|
105
|
+
return {
|
|
106
|
+
type: 'not_found_error',
|
|
107
|
+
message: 'API endpoint not found.',
|
|
108
|
+
code: 'ENDPOINT_NOT_FOUND',
|
|
109
|
+
recoverable: false,
|
|
110
|
+
suggestion: 'Check SUPERMODEL_BASE_URL environment variable. Default: https://api.supermodeltools.com',
|
|
111
|
+
details: { baseUrl: process.env.SUPERMODEL_BASE_URL || 'https://api.supermodeltools.com', httpStatus: 404 },
|
|
112
|
+
};
|
|
113
|
+
case 429:
|
|
114
|
+
return {
|
|
115
|
+
type: 'rate_limit_error',
|
|
116
|
+
message: 'API rate limit exceeded.',
|
|
117
|
+
code: 'RATE_LIMITED',
|
|
118
|
+
recoverable: true,
|
|
119
|
+
suggestion: 'Wait 30-60 seconds and retry. Consider analyzing smaller subdirectories to reduce API calls.',
|
|
120
|
+
details: { httpStatus: 429 },
|
|
121
|
+
};
|
|
122
|
+
case 500:
|
|
123
|
+
case 502:
|
|
124
|
+
case 503:
|
|
125
|
+
case 504:
|
|
126
|
+
return {
|
|
127
|
+
type: 'internal_error',
|
|
128
|
+
message: `Supermodel API server error (HTTP ${status}).`,
|
|
129
|
+
code: 'SERVER_ERROR',
|
|
130
|
+
recoverable: true,
|
|
131
|
+
reportable: true,
|
|
132
|
+
repo: exports.REPORT_REPO,
|
|
133
|
+
suggestion: 'The API may be temporarily unavailable. Wait a few minutes and retry. If persistent, open an issue at https://github.com/supermodeltools/mcp/issues with the error details, or fork the repo and open a PR with a fix.',
|
|
134
|
+
details: { httpStatus: status },
|
|
135
|
+
};
|
|
136
|
+
default: {
|
|
137
|
+
const isServerError = status >= 500;
|
|
138
|
+
return {
|
|
139
|
+
type: isServerError ? 'internal_error' : 'validation_error',
|
|
140
|
+
message: `API request failed with HTTP ${status}.`,
|
|
141
|
+
code: 'API_ERROR',
|
|
142
|
+
recoverable: isServerError,
|
|
143
|
+
...(isServerError && {
|
|
144
|
+
reportable: true,
|
|
145
|
+
repo: exports.REPORT_REPO,
|
|
146
|
+
suggestion: 'The API may be temporarily unavailable. Wait a few minutes and retry. If persistent, open an issue at https://github.com/supermodeltools/mcp/issues with the error details, or fork the repo and open a PR with a fix.',
|
|
147
|
+
}),
|
|
148
|
+
...(!isServerError && { suggestion: 'Check the request parameters and base URL configuration.' }),
|
|
149
|
+
details: { httpStatus: status },
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (error.request) {
|
|
155
|
+
// Distinguish timeout from general network failure
|
|
156
|
+
if (error.code === 'UND_ERR_HEADERS_TIMEOUT' || error.code === 'UND_ERR_BODY_TIMEOUT' || error.message?.includes('timeout')) {
|
|
157
|
+
return {
|
|
158
|
+
type: 'timeout_error',
|
|
159
|
+
message: 'API request timed out. The codebase may be too large for a single analysis.',
|
|
160
|
+
code: 'REQUEST_TIMEOUT',
|
|
161
|
+
recoverable: true,
|
|
162
|
+
suggestion: 'Analyze a smaller subdirectory (e.g. directory="/repo/src/core") or increase SUPERMODEL_TIMEOUT_MS.',
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
type: 'network_error',
|
|
167
|
+
message: 'No response from Supermodel API server.',
|
|
168
|
+
code: 'NO_RESPONSE',
|
|
169
|
+
recoverable: true,
|
|
170
|
+
suggestion: 'Check network connectivity. Verify the API is reachable at the configured base URL.',
|
|
171
|
+
details: { baseUrl: process.env.SUPERMODEL_BASE_URL || 'https://api.supermodeltools.com' },
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
// Catch-all for unexpected errors - include the actual message
|
|
175
|
+
return {
|
|
176
|
+
type: 'internal_error',
|
|
177
|
+
message: error.message || 'An unexpected error occurred.',
|
|
178
|
+
code: 'UNKNOWN_ERROR',
|
|
179
|
+
recoverable: false,
|
|
180
|
+
reportable: true,
|
|
181
|
+
repo: exports.REPORT_REPO,
|
|
182
|
+
suggestion: exports.REPORT_SUGGESTION,
|
|
183
|
+
details: { errorType: error.name || 'Error' },
|
|
184
|
+
};
|
|
185
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supermodeltools/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "MCP server for Supermodel API - code graph generation for AI agents",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
],
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@modelcontextprotocol/sdk": "^1.0.1",
|
|
41
|
-
"@supermodeltools/sdk": "0.
|
|
41
|
+
"@supermodeltools/sdk": "0.7.0",
|
|
42
42
|
"archiver": "^7.0.1",
|
|
43
43
|
"ignore": "^7.0.5",
|
|
44
44
|
"jq-web": "^0.6.2",
|