@supermodeltools/mcp-server 0.9.6 → 0.10.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 +35 -28
- package/dist/server.js +61 -11
- package/dist/tools/explore-function.js +307 -0
- package/dist/tools/find-connections.js +136 -0
- package/dist/tools/symbol-context.js +16 -1
- package/dist/tools/tool-variants.js +255 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://modelcontextprotocol.io)
|
|
5
5
|
[](https://github.com/supermodeltools/mcp/actions/workflows/ci.yml)
|
|
6
6
|
|
|
7
|
-
MCP server that gives AI agents instant codebase understanding via the [Supermodel API](https://docs.supermodeltools.com).
|
|
7
|
+
MCP server that gives AI agents instant codebase understanding via the [Supermodel API](https://docs.supermodeltools.com). Pre-computed code graphs enable sub-second responses for symbol lookups, call-graph traversal, and cross-subsystem analysis.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
@@ -59,6 +59,7 @@ Get your API key from the [Supermodel Dashboard](https://dashboard.supermodeltoo
|
|
|
59
59
|
| `SUPERMODEL_CACHE_DIR` | Directory for pre-computed graph cache files (optional) |
|
|
60
60
|
| `SUPERMODEL_TIMEOUT_MS` | API request timeout in ms (default: 900000 / 15 min) |
|
|
61
61
|
| `SUPERMODEL_NO_API_FALLBACK` | Set to disable on-demand API calls; cache-only mode (optional) |
|
|
62
|
+
| `SUPERMODEL_EXPERIMENT` | Experiment mode. Set to `graphrag` to enable GraphRAG tools (optional) |
|
|
62
63
|
|
|
63
64
|
### Global Setup (Recommended)
|
|
64
65
|
|
|
@@ -132,56 +133,62 @@ Tools will use this directory automatically if no explicit `directory` parameter
|
|
|
132
133
|
|
|
133
134
|
## Tools
|
|
134
135
|
|
|
135
|
-
### `
|
|
136
|
+
### `symbol_context` (Default Mode)
|
|
136
137
|
|
|
137
|
-
|
|
138
|
+
Deep dive on a specific function, class, or method. Given a symbol name, instantly returns its definition location, source code, all callers, all callees, domain membership, and related symbols in the same file.
|
|
138
139
|
|
|
139
140
|
**Output includes:**
|
|
140
|
-
-
|
|
141
|
-
-
|
|
142
|
-
-
|
|
143
|
-
-
|
|
141
|
+
- Definition location (file, line range) and source code
|
|
142
|
+
- Callers (who calls this symbol)
|
|
143
|
+
- Callees (what this symbol calls)
|
|
144
|
+
- Architectural domain membership
|
|
145
|
+
- Related symbols in the same file
|
|
146
|
+
- File import statistics
|
|
144
147
|
|
|
145
148
|
**Parameters:**
|
|
146
149
|
|
|
147
150
|
| Argument | Type | Required | Description |
|
|
148
151
|
|----------|------|----------|-------------|
|
|
152
|
+
| `symbol` | string | No* | Name of the function, class, or method. Supports `ClassName.method` syntax and partial matching. |
|
|
153
|
+
| `symbols` | string[] | No* | Array of symbol names for batch lookup. More efficient than multiple calls. |
|
|
149
154
|
| `directory` | string | No | Path to repository directory. Omit if server was started with a default workdir. |
|
|
155
|
+
| `brief` | boolean | No | Return compact output (no source code). Recommended for 3+ symbols. |
|
|
156
|
+
|
|
157
|
+
\* Either `symbol` or `symbols` must be provided.
|
|
150
158
|
|
|
151
159
|
**Example prompts:**
|
|
152
|
-
- "
|
|
153
|
-
- "What
|
|
160
|
+
- "Look up the symbol `filter_queryset` in this codebase"
|
|
161
|
+
- "What calls `QuerySet.filter` and what does it call?"
|
|
154
162
|
|
|
155
|
-
###
|
|
163
|
+
### GraphRAG Mode (Experimental)
|
|
156
164
|
|
|
157
|
-
|
|
165
|
+
Activate with `SUPERMODEL_EXPERIMENT=graphrag`. Replaces `symbol_context` with a graph-oriented tool for call-graph traversal and cross-subsystem analysis.
|
|
158
166
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
-
|
|
162
|
-
- Callees (what this symbol calls)
|
|
163
|
-
- Architectural domain membership
|
|
164
|
-
- Related symbols in the same file
|
|
165
|
-
- File import statistics
|
|
167
|
+
#### `explore_function`
|
|
168
|
+
|
|
169
|
+
BFS traversal of a function, class, or method call graph. Shows source code, callers, callees, and cross-subsystem boundaries with `← DIFFERENT SUBSYSTEM` markers.
|
|
166
170
|
|
|
167
171
|
**Parameters:**
|
|
168
172
|
|
|
169
173
|
| Argument | Type | Required | Description |
|
|
170
174
|
|----------|------|----------|-------------|
|
|
171
|
-
| `symbol` | string | Yes |
|
|
172
|
-
| `
|
|
175
|
+
| `symbol` | string | Yes | Function, class, or method name to explore. Supports partial matching and `ClassName.method` syntax. |
|
|
176
|
+
| `direction` | string | No | `downstream` (callees), `upstream` (callers), or `both` (default). |
|
|
177
|
+
| `depth` | number | No | Hops to follow: 1–3 (default: 2). |
|
|
178
|
+
| `directory` | string | No | Repository path. |
|
|
173
179
|
|
|
174
|
-
**
|
|
175
|
-
- "Look up the symbol `filter_queryset` in this codebase"
|
|
176
|
-
- "What calls `QuerySet.filter` and what does it call?"
|
|
180
|
+
**Output:** Readable narrative showing upstream/downstream neighbors with domain context at each hop.
|
|
177
181
|
|
|
178
182
|
### Recommended Workflow
|
|
179
183
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
184
|
+
**Default mode:**
|
|
185
|
+
1. Identify symbols from the issue and call `symbol_context` to explore them (batch via `symbols` array or parallel calls)
|
|
186
|
+
2. Use Read/Grep to examine source code at identified locations
|
|
187
|
+
3. Start editing by turn 3. Max 3 MCP calls total.
|
|
188
|
+
|
|
189
|
+
**GraphRAG mode:**
|
|
190
|
+
1. Identify key symbols from the issue, call `explore_function` to understand their call-graph context. Issue multiple calls in parallel (read-only, safe).
|
|
191
|
+
2. Use the cross-subsystem markers and source code from the response to start editing. Max 2 MCP calls total.
|
|
185
192
|
|
|
186
193
|
## Pre-computed Graphs
|
|
187
194
|
|
package/dist/server.js
CHANGED
|
@@ -49,7 +49,9 @@ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
|
49
49
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
50
50
|
const sdk_1 = require("@supermodeltools/sdk");
|
|
51
51
|
const overview_1 = require("./tools/overview");
|
|
52
|
-
const symbol_context_1 =
|
|
52
|
+
const symbol_context_1 = __importStar(require("./tools/symbol-context"));
|
|
53
|
+
const tool_variants_1 = require("./tools/tool-variants");
|
|
54
|
+
const explore_function_1 = __importDefault(require("./tools/explore-function"));
|
|
53
55
|
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
54
56
|
const zip_repository_1 = require("./utils/zip-repository");
|
|
55
57
|
const graph_cache_1 = require("./cache/graph-cache");
|
|
@@ -81,13 +83,32 @@ class Server {
|
|
|
81
83
|
constructor(defaultWorkdir, options) {
|
|
82
84
|
this.defaultWorkdir = defaultWorkdir;
|
|
83
85
|
this.options = options;
|
|
86
|
+
const experiment = process.env.SUPERMODEL_EXPERIMENT;
|
|
84
87
|
// Note: noApiFallback is deferred to start() so startup precaching can use the API
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
const experimentInstructions = {
|
|
89
|
+
'minimal-instructions': 'Codebase analysis tool. Call symbol_context to look up functions/classes.',
|
|
90
|
+
'search-symbol': 'Codebase search tool. Use `search_symbol` alongside Grep and Read for parallel exploration.',
|
|
91
|
+
'split-tools': 'Codebase tools: `find_definition` locates symbols, `trace_calls` shows caller/callee graphs. Call them alongside Read, Grep, and Glob.',
|
|
92
|
+
'annotate': 'Codebase annotation tool. Fire `annotate` alongside your Read and Grep calls to enrich results with structural metadata.',
|
|
93
|
+
'graphrag': `# Supermodel: Codebase Intelligence
|
|
94
|
+
|
|
95
|
+
One read-only tool for instant call-graph exploration.
|
|
96
|
+
|
|
97
|
+
## Tool
|
|
98
|
+
- \`explore_function\`: BFS traversal of a function/class call graph. Returns source code, callers, callees, and cross-subsystem boundaries with ← DIFFERENT SUBSYSTEM markers. Supports partial matching and "ClassName.method" syntax.
|
|
99
|
+
|
|
100
|
+
## Workflow
|
|
101
|
+
1. Identify key symbols from the issue, call \`explore_function\` to understand their call-graph context. Issue multiple calls in parallel (read-only, safe).
|
|
102
|
+
2. Use file paths and source code from the response to start editing. Max 2 MCP calls total.
|
|
103
|
+
|
|
104
|
+
## Rules
|
|
105
|
+
- Do NOT use TodoWrite. Act directly.
|
|
106
|
+
- NEVER create standalone test scripts. Run the repo's existing test suite to verify.
|
|
107
|
+
- >2 MCP turns = diminishing returns. Get everything you need in one turn.`,
|
|
108
|
+
};
|
|
109
|
+
const instructions = experiment && experimentInstructions[experiment]
|
|
110
|
+
? experimentInstructions[experiment]
|
|
111
|
+
: `# Supermodel: Codebase Intelligence
|
|
91
112
|
|
|
92
113
|
One read-only tool for instant codebase understanding. Pre-computed graphs enable sub-second responses.
|
|
93
114
|
|
|
@@ -110,7 +131,13 @@ Run the full related test suite to catch regressions. Do NOT write standalone te
|
|
|
110
131
|
- \`symbol_context\`: Source, callers, callees, domain for any function/class/method.
|
|
111
132
|
Supports "Class.method", partial matching, and batch lookups via \`symbols\` array.
|
|
112
133
|
Use \`brief: true\` for compact output when looking up 3+ symbols.
|
|
113
|
-
Read-only — safe to call in parallel
|
|
134
|
+
Read-only — safe to call in parallel.`;
|
|
135
|
+
this.server = new mcp_js_1.McpServer({
|
|
136
|
+
name: 'supermodel_api',
|
|
137
|
+
version: '0.0.1',
|
|
138
|
+
}, {
|
|
139
|
+
capabilities: { tools: {}, logging: {} },
|
|
140
|
+
instructions,
|
|
114
141
|
});
|
|
115
142
|
const config = new sdk_1.Configuration({
|
|
116
143
|
basePath: process.env.SUPERMODEL_BASE_URL || 'https://api.supermodeltools.com',
|
|
@@ -130,9 +157,29 @@ Run the full related test suite to catch regressions. Do NOT write standalone te
|
|
|
130
157
|
this.setupHandlers();
|
|
131
158
|
}
|
|
132
159
|
setupHandlers() {
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
160
|
+
const experiment = process.env.SUPERMODEL_EXPERIMENT;
|
|
161
|
+
// Experiment variants: swap tool definitions to test parallel calling behavior
|
|
162
|
+
let allTools;
|
|
163
|
+
switch (experiment) {
|
|
164
|
+
case 'minimal-schema':
|
|
165
|
+
allTools = [{ tool: symbol_context_1.minimalTool, handler: symbol_context_1.default.handler }];
|
|
166
|
+
break;
|
|
167
|
+
case 'search-symbol':
|
|
168
|
+
allTools = [tool_variants_1.searchSymbolEndpoint];
|
|
169
|
+
break;
|
|
170
|
+
case 'split-tools':
|
|
171
|
+
allTools = [tool_variants_1.findDefinitionEndpoint, tool_variants_1.traceCallsEndpoint];
|
|
172
|
+
break;
|
|
173
|
+
case 'annotate':
|
|
174
|
+
allTools = [tool_variants_1.annotateEndpoint];
|
|
175
|
+
break;
|
|
176
|
+
case 'graphrag':
|
|
177
|
+
allTools = [explore_function_1.default];
|
|
178
|
+
break;
|
|
179
|
+
default:
|
|
180
|
+
allTools = [symbol_context_1.default];
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
136
183
|
// Create a map for quick handler lookup
|
|
137
184
|
const toolMap = new Map();
|
|
138
185
|
for (const t of allTools) {
|
|
@@ -170,6 +217,9 @@ Run the full related test suite to catch regressions. Do NOT write standalone te
|
|
|
170
217
|
injectOverviewInstructions(repoMap) {
|
|
171
218
|
if (repoMap.size === 0)
|
|
172
219
|
return;
|
|
220
|
+
// Skip overview injection during experiments to isolate variables (except graphrag)
|
|
221
|
+
if (process.env.SUPERMODEL_EXPERIMENT && process.env.SUPERMODEL_EXPERIMENT !== 'graphrag')
|
|
222
|
+
return;
|
|
173
223
|
// Only inject if there's exactly 1 unique graph (SWE-bench always has exactly 1 repo)
|
|
174
224
|
const uniqueGraphs = new Set([...repoMap.values()]);
|
|
175
225
|
if (uniqueGraphs.size !== 1)
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `explore_function` tool — BFS call-graph traversal with domain annotations.
|
|
4
|
+
*
|
|
5
|
+
* Ported from codegraph-graphrag prototype. Key differences from `symbol_context`:
|
|
6
|
+
* - Up to 3-hop BFS (vs 1-hop flat list)
|
|
7
|
+
* - Every neighbor shows subdomain + domain
|
|
8
|
+
* - ← DIFFERENT SUBSYSTEM marker for cross-boundary calls
|
|
9
|
+
* - Hierarchical output with "Via" sections for multi-hop paths
|
|
10
|
+
* - Source code included for the root symbol (saves a Read round-trip)
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.handler = exports.tool = void 0;
|
|
14
|
+
const types_1 = require("../types");
|
|
15
|
+
const graph_cache_1 = require("../cache/graph-cache");
|
|
16
|
+
const api_helpers_1 = require("../utils/api-helpers");
|
|
17
|
+
const symbol_context_1 = require("./symbol-context");
|
|
18
|
+
const constants_1 = require("../constants");
|
|
19
|
+
exports.tool = {
|
|
20
|
+
name: 'explore_function',
|
|
21
|
+
description: "Explore a function or class call-graph neighborhood. Returns source code, callers (upstream), callees (downstream), with subsystem/domain annotations and ← DIFFERENT SUBSYSTEM markers for cross-boundary calls. Accepts partial matching and ClassName.method syntax.",
|
|
22
|
+
inputSchema: {
|
|
23
|
+
type: 'object',
|
|
24
|
+
properties: {
|
|
25
|
+
symbol: {
|
|
26
|
+
type: 'string',
|
|
27
|
+
description: 'Function name to explore. Supports partial matching and "ClassName.method" syntax.',
|
|
28
|
+
},
|
|
29
|
+
direction: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
enum: ['downstream', 'upstream', 'both'],
|
|
32
|
+
description: 'Which direction to explore: downstream (callees), upstream (callers), or both. Default: both.',
|
|
33
|
+
},
|
|
34
|
+
depth: {
|
|
35
|
+
type: 'number',
|
|
36
|
+
minimum: 1,
|
|
37
|
+
maximum: 3,
|
|
38
|
+
description: 'How many hops to follow (1-3). Default: 2.',
|
|
39
|
+
},
|
|
40
|
+
directory: {
|
|
41
|
+
type: 'string',
|
|
42
|
+
description: 'Repository path (optional, uses default if omitted).',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
required: ['symbol'],
|
|
46
|
+
},
|
|
47
|
+
annotations: {
|
|
48
|
+
readOnlyHint: true,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Build a map from Subdomain name → parent Domain name by scanning partOf relationships.
|
|
53
|
+
* Computed once per call (fast — only scans Domain/Subdomain nodes).
|
|
54
|
+
*/
|
|
55
|
+
function buildSubdomainToParentMap(graph) {
|
|
56
|
+
const map = new Map();
|
|
57
|
+
const relationships = graph.raw.graph?.relationships || [];
|
|
58
|
+
for (const rel of relationships) {
|
|
59
|
+
if (rel.type !== 'partOf')
|
|
60
|
+
continue;
|
|
61
|
+
const startNode = graph.nodeById.get(rel.startNode);
|
|
62
|
+
const endNode = graph.nodeById.get(rel.endNode);
|
|
63
|
+
if (startNode?.labels?.[0] === 'Subdomain' &&
|
|
64
|
+
endNode?.labels?.[0] === 'Domain') {
|
|
65
|
+
const subName = startNode.properties?.name;
|
|
66
|
+
const domName = endNode.properties?.name;
|
|
67
|
+
if (subName && domName) {
|
|
68
|
+
map.set(subName, domName);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return map;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Resolve subdomain + domain for a node using domainIndex and the partOf map.
|
|
76
|
+
*/
|
|
77
|
+
function resolveDomain(graph, nodeId, subdomainToParent) {
|
|
78
|
+
let subdomain = null;
|
|
79
|
+
let domain = null;
|
|
80
|
+
for (const [name, data] of graph.domainIndex) {
|
|
81
|
+
if (!data.memberIds.includes(nodeId))
|
|
82
|
+
continue;
|
|
83
|
+
const domainNode = graph.nodeById.get(
|
|
84
|
+
// Find the node for this domain entry to check its label
|
|
85
|
+
[...graph.nameIndex.get(name.toLowerCase()) || []].find(id => {
|
|
86
|
+
const n = graph.nodeById.get(id);
|
|
87
|
+
return n?.labels?.[0] === 'Subdomain' || n?.labels?.[0] === 'Domain';
|
|
88
|
+
}) || '');
|
|
89
|
+
if (domainNode?.labels?.[0] === 'Subdomain') {
|
|
90
|
+
subdomain = name;
|
|
91
|
+
// Look up parent domain via partOf map
|
|
92
|
+
const parent = subdomainToParent.get(name);
|
|
93
|
+
if (parent)
|
|
94
|
+
domain = parent;
|
|
95
|
+
}
|
|
96
|
+
else if (domainNode?.labels?.[0] === 'Domain') {
|
|
97
|
+
domain = name;
|
|
98
|
+
}
|
|
99
|
+
// If we found a subdomain, that's the most specific — prefer it
|
|
100
|
+
if (subdomain)
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
// If we only found a domain directly (no subdomain), that's fine
|
|
104
|
+
return { subdomain, domain };
|
|
105
|
+
}
|
|
106
|
+
// ── Node description ──
|
|
107
|
+
function describeNode(graph, nodeId, refSubdomain, subdomainToParent) {
|
|
108
|
+
const node = graph.nodeById.get(nodeId);
|
|
109
|
+
if (!node)
|
|
110
|
+
return '(unknown)';
|
|
111
|
+
const name = node.properties?.name || '(unknown)';
|
|
112
|
+
const filePath = (0, graph_cache_1.normalizePath)(node.properties?.filePath || '');
|
|
113
|
+
const { subdomain, domain } = resolveDomain(graph, nodeId, subdomainToParent);
|
|
114
|
+
let loc = '';
|
|
115
|
+
if (subdomain && domain)
|
|
116
|
+
loc = `${subdomain} subsystem, ${domain} domain`;
|
|
117
|
+
else if (subdomain)
|
|
118
|
+
loc = `${subdomain} subsystem`;
|
|
119
|
+
else if (domain)
|
|
120
|
+
loc = `${domain} domain`;
|
|
121
|
+
let line = `\`${name}\``;
|
|
122
|
+
if (filePath)
|
|
123
|
+
line += ` — ${filePath}`;
|
|
124
|
+
if (loc)
|
|
125
|
+
line += ` — ${loc}`;
|
|
126
|
+
// Flag cross-subsystem edges
|
|
127
|
+
if (refSubdomain && subdomain && subdomain !== refSubdomain) {
|
|
128
|
+
line += ' ← DIFFERENT SUBSYSTEM';
|
|
129
|
+
}
|
|
130
|
+
return line;
|
|
131
|
+
}
|
|
132
|
+
// ── Handler ──
|
|
133
|
+
const handler = async (client, args, defaultWorkdir) => {
|
|
134
|
+
const symbolArg = typeof args?.symbol === 'string' ? args.symbol.trim() : '';
|
|
135
|
+
if (!symbolArg) {
|
|
136
|
+
return (0, types_1.asErrorResult)({
|
|
137
|
+
type: 'validation_error',
|
|
138
|
+
message: 'Missing required "symbol" parameter.',
|
|
139
|
+
code: 'MISSING_SYMBOL',
|
|
140
|
+
recoverable: false,
|
|
141
|
+
suggestion: 'Provide the name of a function to explore.',
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
const direction = args?.direction || 'both';
|
|
145
|
+
if (!['downstream', 'upstream', 'both'].includes(direction)) {
|
|
146
|
+
return (0, types_1.asErrorResult)({
|
|
147
|
+
type: 'validation_error',
|
|
148
|
+
message: `Invalid direction "${direction}". Must be "downstream", "upstream", or "both".`,
|
|
149
|
+
code: 'INVALID_DIRECTION',
|
|
150
|
+
recoverable: false,
|
|
151
|
+
suggestion: 'Use "downstream" (callees), "upstream" (callers), or "both".',
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
const depth = Math.min(3, Math.max(1, Number(args?.depth) || 2));
|
|
155
|
+
const rawDir = args?.directory;
|
|
156
|
+
const directory = (rawDir && rawDir.trim()) || defaultWorkdir || process.cwd();
|
|
157
|
+
if (!directory || typeof directory !== 'string') {
|
|
158
|
+
return (0, types_1.asErrorResult)({
|
|
159
|
+
type: 'validation_error',
|
|
160
|
+
message: 'No directory provided and no default workdir configured.',
|
|
161
|
+
code: 'MISSING_DIRECTORY',
|
|
162
|
+
recoverable: false,
|
|
163
|
+
suggestion: 'Provide a directory parameter or start the MCP server with a workdir argument.',
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
let graph;
|
|
167
|
+
try {
|
|
168
|
+
graph = await (0, graph_cache_1.resolveOrFetchGraph)(client, directory);
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
return (0, types_1.asErrorResult)((0, api_helpers_1.classifyApiError)(error));
|
|
172
|
+
}
|
|
173
|
+
// Resolve symbol name to a Function or Class node
|
|
174
|
+
const matches = (0, symbol_context_1.findSymbol)(graph, symbolArg);
|
|
175
|
+
const funcMatch = matches.find(n => n.labels?.[0] === 'Function' || n.labels?.[0] === 'Class');
|
|
176
|
+
if (!funcMatch) {
|
|
177
|
+
return (0, types_1.asErrorResult)({
|
|
178
|
+
type: 'not_found_error',
|
|
179
|
+
message: `No function or class matching "${symbolArg}" found in the code graph.`,
|
|
180
|
+
code: 'SYMBOL_NOT_FOUND',
|
|
181
|
+
recoverable: false,
|
|
182
|
+
suggestion: 'Try a different function name or use partial matching.',
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
const rootId = funcMatch.id;
|
|
186
|
+
const subdomainToParent = buildSubdomainToParentMap(graph);
|
|
187
|
+
const rootDomain = resolveDomain(graph, rootId, subdomainToParent);
|
|
188
|
+
const rootSubName = rootDomain.subdomain;
|
|
189
|
+
const lines = [];
|
|
190
|
+
lines.push(`## ${describeNode(graph, rootId, null, subdomainToParent)}`);
|
|
191
|
+
lines.push('');
|
|
192
|
+
// Include source code for the root symbol (saves agent a Read round-trip)
|
|
193
|
+
const rootSource = funcMatch.properties?.sourceCode;
|
|
194
|
+
if (rootSource) {
|
|
195
|
+
const sourceLines = rootSource.split('\n');
|
|
196
|
+
const truncated = sourceLines.length > constants_1.MAX_SOURCE_LINES;
|
|
197
|
+
const displayLines = truncated ? sourceLines.slice(0, constants_1.MAX_SOURCE_LINES) : sourceLines;
|
|
198
|
+
const startLine = funcMatch.properties?.startLine;
|
|
199
|
+
const filePath = (0, graph_cache_1.normalizePath)(funcMatch.properties?.filePath || '');
|
|
200
|
+
const ext = filePath.split('.').pop() || '';
|
|
201
|
+
lines.push(`### Source`);
|
|
202
|
+
lines.push('```' + ext);
|
|
203
|
+
if (startLine) {
|
|
204
|
+
displayLines.forEach((l, i) => { lines.push(`${startLine + i}: ${l}`); });
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
displayLines.forEach(l => { lines.push(l); });
|
|
208
|
+
}
|
|
209
|
+
if (truncated)
|
|
210
|
+
lines.push(`... (${sourceLines.length - constants_1.MAX_SOURCE_LINES} more lines)`);
|
|
211
|
+
lines.push('```');
|
|
212
|
+
lines.push('');
|
|
213
|
+
}
|
|
214
|
+
// Downstream BFS (callees)
|
|
215
|
+
if (direction === 'downstream' || direction === 'both') {
|
|
216
|
+
lines.push('### Functions it calls:');
|
|
217
|
+
let frontier = [rootId];
|
|
218
|
+
const visited = new Set([rootId]);
|
|
219
|
+
for (let d = 1; d <= depth; d++) {
|
|
220
|
+
const nextFrontier = [];
|
|
221
|
+
if (d === 1) {
|
|
222
|
+
const callees = (graph.callAdj.get(rootId)?.out || [])
|
|
223
|
+
.filter(id => { const l = graph.nodeById.get(id)?.labels?.[0]; return l === 'Function' || l === 'Class'; });
|
|
224
|
+
if (callees.length === 0) {
|
|
225
|
+
lines.push(' (none)');
|
|
226
|
+
}
|
|
227
|
+
for (const [i, cId] of callees.entries()) {
|
|
228
|
+
visited.add(cId);
|
|
229
|
+
nextFrontier.push(cId);
|
|
230
|
+
lines.push(` ${i + 1}. ${describeNode(graph, cId, rootSubName, subdomainToParent)}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
let anyFound = false;
|
|
235
|
+
for (const parentId of frontier) {
|
|
236
|
+
const parentNode = graph.nodeById.get(parentId);
|
|
237
|
+
const parentName = parentNode?.properties?.name || '(unknown)';
|
|
238
|
+
const callees = (graph.callAdj.get(parentId)?.out || [])
|
|
239
|
+
.filter(id => { const l = graph.nodeById.get(id)?.labels?.[0]; return l === 'Function' || l === 'Class'; })
|
|
240
|
+
.filter(id => !visited.has(id));
|
|
241
|
+
if (callees.length === 0)
|
|
242
|
+
continue;
|
|
243
|
+
anyFound = true;
|
|
244
|
+
lines.push(` Via \`${parentName}\`:`);
|
|
245
|
+
for (const cId of callees) {
|
|
246
|
+
visited.add(cId);
|
|
247
|
+
nextFrontier.push(cId);
|
|
248
|
+
lines.push(` → ${describeNode(graph, cId, rootSubName, subdomainToParent)}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (!anyFound) {
|
|
252
|
+
lines.push(` (no further calls at depth ${d})`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
frontier = nextFrontier;
|
|
256
|
+
}
|
|
257
|
+
lines.push('');
|
|
258
|
+
}
|
|
259
|
+
// Upstream BFS (callers)
|
|
260
|
+
if (direction === 'upstream' || direction === 'both') {
|
|
261
|
+
lines.push('### Functions that call it:');
|
|
262
|
+
let frontier = [rootId];
|
|
263
|
+
const visited = new Set([rootId]);
|
|
264
|
+
for (let d = 1; d <= depth; d++) {
|
|
265
|
+
const nextFrontier = [];
|
|
266
|
+
if (d === 1) {
|
|
267
|
+
const callers = (graph.callAdj.get(rootId)?.in || [])
|
|
268
|
+
.filter(id => { const l = graph.nodeById.get(id)?.labels?.[0]; return l === 'Function' || l === 'Class'; });
|
|
269
|
+
if (callers.length === 0) {
|
|
270
|
+
lines.push(' (none)');
|
|
271
|
+
}
|
|
272
|
+
for (const [i, cId] of callers.entries()) {
|
|
273
|
+
visited.add(cId);
|
|
274
|
+
nextFrontier.push(cId);
|
|
275
|
+
lines.push(` ${i + 1}. ${describeNode(graph, cId, rootSubName, subdomainToParent)}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
let anyFound = false;
|
|
280
|
+
for (const parentId of frontier) {
|
|
281
|
+
const parentNode = graph.nodeById.get(parentId);
|
|
282
|
+
const parentName = parentNode?.properties?.name || '(unknown)';
|
|
283
|
+
const callers = (graph.callAdj.get(parentId)?.in || [])
|
|
284
|
+
.filter(id => { const l = graph.nodeById.get(id)?.labels?.[0]; return l === 'Function' || l === 'Class'; })
|
|
285
|
+
.filter(id => !visited.has(id));
|
|
286
|
+
if (callers.length === 0)
|
|
287
|
+
continue;
|
|
288
|
+
anyFound = true;
|
|
289
|
+
lines.push(` Via \`${parentName}\`:`);
|
|
290
|
+
for (const cId of callers) {
|
|
291
|
+
visited.add(cId);
|
|
292
|
+
nextFrontier.push(cId);
|
|
293
|
+
lines.push(` → ${describeNode(graph, cId, rootSubName, subdomainToParent)}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (!anyFound) {
|
|
297
|
+
lines.push(` (no further callers at depth ${d})`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
frontier = nextFrontier;
|
|
301
|
+
}
|
|
302
|
+
lines.push('');
|
|
303
|
+
}
|
|
304
|
+
return (0, types_1.asTextContentResult)(lines.join('\n'));
|
|
305
|
+
};
|
|
306
|
+
exports.handler = handler;
|
|
307
|
+
exports.default = { tool: exports.tool, handler: exports.handler };
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `find_connections` tool — find how two subsystems/domains connect via call relationships.
|
|
4
|
+
*
|
|
5
|
+
* Ported from codegraph-graphrag prototype. Iterates domainIndex to find members
|
|
6
|
+
* of each domain, then scans callAdj for cross-domain call edges.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.handler = exports.tool = void 0;
|
|
10
|
+
const types_1 = require("../types");
|
|
11
|
+
const graph_cache_1 = require("../cache/graph-cache");
|
|
12
|
+
const api_helpers_1 = require("../utils/api-helpers");
|
|
13
|
+
exports.tool = {
|
|
14
|
+
name: 'find_connections',
|
|
15
|
+
description: 'Find how two subsystems/domains connect. Returns the functions that bridge them via call relationships.',
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
domain_a: {
|
|
20
|
+
type: 'string',
|
|
21
|
+
description: 'First domain or subdomain name.',
|
|
22
|
+
},
|
|
23
|
+
domain_b: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
description: 'Second domain or subdomain name.',
|
|
26
|
+
},
|
|
27
|
+
directory: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
description: 'Repository path (optional, uses default if omitted).',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
required: ['domain_a', 'domain_b'],
|
|
33
|
+
},
|
|
34
|
+
annotations: {
|
|
35
|
+
readOnlyHint: true,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Collect all Function node IDs that belong to a domain/subdomain (fuzzy name match).
|
|
40
|
+
*/
|
|
41
|
+
function collectDomainMembers(graph, query) {
|
|
42
|
+
const lower = query.toLowerCase();
|
|
43
|
+
const members = new Set();
|
|
44
|
+
for (const [name, data] of graph.domainIndex) {
|
|
45
|
+
if (!name.toLowerCase().includes(lower))
|
|
46
|
+
continue;
|
|
47
|
+
for (const id of data.memberIds) {
|
|
48
|
+
const node = graph.nodeById.get(id);
|
|
49
|
+
if (node?.labels?.[0] === 'Function') {
|
|
50
|
+
members.add(id);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return members;
|
|
55
|
+
}
|
|
56
|
+
const handler = async (client, args, defaultWorkdir) => {
|
|
57
|
+
const domainA = typeof args?.domain_a === 'string' ? args.domain_a.trim() : '';
|
|
58
|
+
const domainB = typeof args?.domain_b === 'string' ? args.domain_b.trim() : '';
|
|
59
|
+
if (!domainA || !domainB) {
|
|
60
|
+
return (0, types_1.asErrorResult)({
|
|
61
|
+
type: 'validation_error',
|
|
62
|
+
message: 'Missing required "domain_a" and/or "domain_b" parameters.',
|
|
63
|
+
code: 'MISSING_DOMAIN',
|
|
64
|
+
recoverable: false,
|
|
65
|
+
suggestion: 'Provide two domain or subdomain names to find connections between.',
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const rawDir = args?.directory;
|
|
69
|
+
const directory = (rawDir && rawDir.trim()) || defaultWorkdir || process.cwd();
|
|
70
|
+
if (!directory || typeof directory !== 'string') {
|
|
71
|
+
return (0, types_1.asErrorResult)({
|
|
72
|
+
type: 'validation_error',
|
|
73
|
+
message: 'No directory provided and no default workdir configured.',
|
|
74
|
+
code: 'MISSING_DIRECTORY',
|
|
75
|
+
recoverable: false,
|
|
76
|
+
suggestion: 'Provide a directory parameter or start the MCP server with a workdir argument.',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
let graph;
|
|
80
|
+
try {
|
|
81
|
+
graph = await (0, graph_cache_1.resolveOrFetchGraph)(client, directory);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
return (0, types_1.asErrorResult)((0, api_helpers_1.classifyApiError)(error));
|
|
85
|
+
}
|
|
86
|
+
const aNodes = collectDomainMembers(graph, domainA);
|
|
87
|
+
const bNodes = collectDomainMembers(graph, domainB);
|
|
88
|
+
if (aNodes.size === 0) {
|
|
89
|
+
return (0, types_1.asTextContentResult)(`No functions found in domain/subdomain matching "${domainA}".`);
|
|
90
|
+
}
|
|
91
|
+
if (bNodes.size === 0) {
|
|
92
|
+
return (0, types_1.asTextContentResult)(`No functions found in domain/subdomain matching "${domainB}".`);
|
|
93
|
+
}
|
|
94
|
+
// Find call edges between domain A and domain B in both directions
|
|
95
|
+
const bridges = [];
|
|
96
|
+
for (const aId of aNodes) {
|
|
97
|
+
const adj = graph.callAdj.get(aId);
|
|
98
|
+
if (!adj)
|
|
99
|
+
continue;
|
|
100
|
+
// A calls B (downstream)
|
|
101
|
+
for (const targetId of adj.out) {
|
|
102
|
+
if (!bNodes.has(targetId))
|
|
103
|
+
continue;
|
|
104
|
+
const srcNode = graph.nodeById.get(aId);
|
|
105
|
+
const tgtNode = graph.nodeById.get(targetId);
|
|
106
|
+
const srcName = srcNode?.properties?.name || '(unknown)';
|
|
107
|
+
const tgtName = tgtNode?.properties?.name || '(unknown)';
|
|
108
|
+
const srcFile = (0, graph_cache_1.normalizePath)(srcNode?.properties?.filePath || '');
|
|
109
|
+
const tgtFile = (0, graph_cache_1.normalizePath)(tgtNode?.properties?.filePath || '');
|
|
110
|
+
bridges.push(`\`${srcName}\` (${domainA}) calls \`${tgtName}\` (${domainB}) — ${srcFile} → ${tgtFile}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
for (const bId of bNodes) {
|
|
114
|
+
const adj = graph.callAdj.get(bId);
|
|
115
|
+
if (!adj)
|
|
116
|
+
continue;
|
|
117
|
+
// B calls A (reverse direction)
|
|
118
|
+
for (const targetId of adj.out) {
|
|
119
|
+
if (!aNodes.has(targetId))
|
|
120
|
+
continue;
|
|
121
|
+
const srcNode = graph.nodeById.get(bId);
|
|
122
|
+
const tgtNode = graph.nodeById.get(targetId);
|
|
123
|
+
const srcName = srcNode?.properties?.name || '(unknown)';
|
|
124
|
+
const tgtName = tgtNode?.properties?.name || '(unknown)';
|
|
125
|
+
const srcFile = (0, graph_cache_1.normalizePath)(srcNode?.properties?.filePath || '');
|
|
126
|
+
const tgtFile = (0, graph_cache_1.normalizePath)(tgtNode?.properties?.filePath || '');
|
|
127
|
+
bridges.push(`\`${srcName}\` (${domainB}) calls \`${tgtName}\` (${domainA}) — ${srcFile} → ${tgtFile}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (bridges.length === 0) {
|
|
131
|
+
return (0, types_1.asTextContentResult)(`No direct call connections found between "${domainA}" and "${domainB}".`);
|
|
132
|
+
}
|
|
133
|
+
return (0, types_1.asTextContentResult)(`Connections between ${domainA} and ${domainB}:\n\n${bridges.join('\n')}`);
|
|
134
|
+
};
|
|
135
|
+
exports.handler = handler;
|
|
136
|
+
exports.default = { tool: exports.tool, handler: exports.handler };
|
|
@@ -46,7 +46,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
46
46
|
};
|
|
47
47
|
})();
|
|
48
48
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
|
-
exports.handler = exports.tool = void 0;
|
|
49
|
+
exports.minimalTool = exports.handler = exports.tool = void 0;
|
|
50
50
|
exports.findSymbol = findSymbol;
|
|
51
51
|
exports.renderBriefSymbolContext = renderBriefSymbolContext;
|
|
52
52
|
exports.renderSymbolContext = renderSymbolContext;
|
|
@@ -484,4 +484,19 @@ function findDomain(graph, nodeId) {
|
|
|
484
484
|
}
|
|
485
485
|
return null;
|
|
486
486
|
}
|
|
487
|
+
exports.minimalTool = {
|
|
488
|
+
name: 'symbol_context',
|
|
489
|
+
description: 'Look up a function or class by name.',
|
|
490
|
+
inputSchema: {
|
|
491
|
+
type: 'object',
|
|
492
|
+
properties: {
|
|
493
|
+
symbol: { type: 'string', description: 'Symbol name.' },
|
|
494
|
+
directory: { type: 'string', description: 'Repo path.' },
|
|
495
|
+
},
|
|
496
|
+
required: [],
|
|
497
|
+
},
|
|
498
|
+
annotations: {
|
|
499
|
+
readOnlyHint: true,
|
|
500
|
+
},
|
|
501
|
+
};
|
|
487
502
|
exports.default = { tool: exports.tool, handler: exports.handler };
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Experimental tool variants to test parallel calling behavior.
|
|
4
|
+
*
|
|
5
|
+
* Each variant reuses the same underlying graph + handler logic from
|
|
6
|
+
* symbol-context.ts but changes the tool name, description, and/or
|
|
7
|
+
* schema to test whether framing affects the model's willingness to
|
|
8
|
+
* call MCP tools in parallel with built-in tools (Read, Grep, etc.).
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.annotateEndpoint = exports.annotateTool = exports.traceCallsEndpoint = exports.traceCallsTool = exports.findDefinitionEndpoint = exports.findDefinitionTool = exports.searchSymbolEndpoint = exports.searchSymbolTool = void 0;
|
|
12
|
+
const types_1 = require("../types");
|
|
13
|
+
const graph_cache_1 = require("../cache/graph-cache");
|
|
14
|
+
const symbol_context_1 = require("./symbol-context");
|
|
15
|
+
const constants_1 = require("../constants");
|
|
16
|
+
// ─── Variant D: "search_symbol" ────────────────────────────────────
|
|
17
|
+
// Hypothesis: framing as a search operation (like Grep/Glob) makes the
|
|
18
|
+
// model treat it as parallel-safe alongside other search tools.
|
|
19
|
+
exports.searchSymbolTool = {
|
|
20
|
+
name: 'search_symbol',
|
|
21
|
+
description: 'Search for a function, class, or method by name. Returns file location, callers, and callees. Like Grep but for code structure. Safe to call alongside Read, Grep, and Glob.',
|
|
22
|
+
inputSchema: {
|
|
23
|
+
type: 'object',
|
|
24
|
+
properties: {
|
|
25
|
+
query: {
|
|
26
|
+
type: 'string',
|
|
27
|
+
description: 'Function, class, or method name to search for.',
|
|
28
|
+
},
|
|
29
|
+
directory: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
description: 'Repository path. Omit to use default.',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
required: ['query'],
|
|
35
|
+
},
|
|
36
|
+
annotations: { readOnlyHint: true },
|
|
37
|
+
};
|
|
38
|
+
const searchSymbolHandler = async (client, args, defaultWorkdir) => {
|
|
39
|
+
const query = typeof args?.query === 'string' ? args.query.trim() : '';
|
|
40
|
+
if (!query) {
|
|
41
|
+
return (0, types_1.asErrorResult)({
|
|
42
|
+
type: 'validation_error',
|
|
43
|
+
message: 'Missing required "query" parameter.',
|
|
44
|
+
code: 'MISSING_QUERY',
|
|
45
|
+
recoverable: false,
|
|
46
|
+
suggestion: 'Provide the name of a function, class, or method to search for.',
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const rawDir = args?.directory;
|
|
50
|
+
const directory = (rawDir && rawDir.trim()) || defaultWorkdir || process.cwd();
|
|
51
|
+
let graph;
|
|
52
|
+
try {
|
|
53
|
+
graph = await (0, graph_cache_1.resolveOrFetchGraph)(client, directory);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
return (0, types_1.asErrorResult)({ type: 'internal_error', message: error.message, code: 'GRAPH_ERROR', recoverable: false });
|
|
57
|
+
}
|
|
58
|
+
const matches = (0, symbol_context_1.findSymbol)(graph, query);
|
|
59
|
+
if (matches.length === 0) {
|
|
60
|
+
return (0, types_1.asTextContentResult)(`No symbol matching "${query}" found.`);
|
|
61
|
+
}
|
|
62
|
+
// Always brief — keep response small so model doesn't "wait for it"
|
|
63
|
+
const parts = matches.slice(0, 3).map(node => (0, symbol_context_1.renderBriefSymbolContext)(graph, node));
|
|
64
|
+
let result = parts.join('\n---\n\n');
|
|
65
|
+
if (matches.length > 3) {
|
|
66
|
+
result += `\n\n*... and ${matches.length - 3} more matches.*`;
|
|
67
|
+
}
|
|
68
|
+
return (0, types_1.asTextContentResult)(result);
|
|
69
|
+
};
|
|
70
|
+
exports.searchSymbolEndpoint = { tool: exports.searchSymbolTool, handler: searchSymbolHandler };
|
|
71
|
+
// ─── Variant E: Split into find_definition + trace_calls ───────────
|
|
72
|
+
// Hypothesis: two small tools give the model reason to call them in
|
|
73
|
+
// parallel with each other AND with built-in tools.
|
|
74
|
+
exports.findDefinitionTool = {
|
|
75
|
+
name: 'find_definition',
|
|
76
|
+
description: 'Find where a function, class, or method is defined. Returns file path and line number. Fast, read-only.',
|
|
77
|
+
inputSchema: {
|
|
78
|
+
type: 'object',
|
|
79
|
+
properties: {
|
|
80
|
+
name: {
|
|
81
|
+
type: 'string',
|
|
82
|
+
description: 'Symbol name to find.',
|
|
83
|
+
},
|
|
84
|
+
directory: {
|
|
85
|
+
type: 'string',
|
|
86
|
+
description: 'Repository path. Omit to use default.',
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
required: ['name'],
|
|
90
|
+
},
|
|
91
|
+
annotations: { readOnlyHint: true },
|
|
92
|
+
};
|
|
93
|
+
const findDefinitionHandler = async (client, args, defaultWorkdir) => {
|
|
94
|
+
const name = typeof args?.name === 'string' ? args.name.trim() : '';
|
|
95
|
+
if (!name) {
|
|
96
|
+
return (0, types_1.asErrorResult)({
|
|
97
|
+
type: 'validation_error',
|
|
98
|
+
message: 'Missing required "name" parameter.',
|
|
99
|
+
code: 'MISSING_NAME',
|
|
100
|
+
recoverable: false,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const rawDir = args?.directory;
|
|
104
|
+
const directory = (rawDir && rawDir.trim()) || defaultWorkdir || process.cwd();
|
|
105
|
+
let graph;
|
|
106
|
+
try {
|
|
107
|
+
graph = await (0, graph_cache_1.resolveOrFetchGraph)(client, directory);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
return (0, types_1.asErrorResult)({ type: 'internal_error', message: error.message, code: 'GRAPH_ERROR', recoverable: false });
|
|
111
|
+
}
|
|
112
|
+
const matches = (0, symbol_context_1.findSymbol)(graph, name);
|
|
113
|
+
if (matches.length === 0) {
|
|
114
|
+
return (0, types_1.asTextContentResult)(`No symbol matching "${name}" found.`);
|
|
115
|
+
}
|
|
116
|
+
// Ultra-compact: just location lines
|
|
117
|
+
const lines = matches.slice(0, 5).map(node => {
|
|
118
|
+
const sym = node.properties?.name || '(unknown)';
|
|
119
|
+
const fp = (0, graph_cache_1.normalizePath)(node.properties?.filePath || '');
|
|
120
|
+
const start = node.properties?.startLine || 0;
|
|
121
|
+
const end = node.properties?.endLine || 0;
|
|
122
|
+
const kind = node.labels?.[0]?.toLowerCase() || 'symbol';
|
|
123
|
+
return `${sym} (${kind}) — ${fp}:${start}-${end}`;
|
|
124
|
+
});
|
|
125
|
+
return (0, types_1.asTextContentResult)(lines.join('\n'));
|
|
126
|
+
};
|
|
127
|
+
exports.findDefinitionEndpoint = { tool: exports.findDefinitionTool, handler: findDefinitionHandler };
|
|
128
|
+
exports.traceCallsTool = {
|
|
129
|
+
name: 'trace_calls',
|
|
130
|
+
description: 'Get the caller/callee graph for a function or method. Shows who calls it and what it calls. Fast, read-only.',
|
|
131
|
+
inputSchema: {
|
|
132
|
+
type: 'object',
|
|
133
|
+
properties: {
|
|
134
|
+
name: {
|
|
135
|
+
type: 'string',
|
|
136
|
+
description: 'Symbol name to trace.',
|
|
137
|
+
},
|
|
138
|
+
directory: {
|
|
139
|
+
type: 'string',
|
|
140
|
+
description: 'Repository path. Omit to use default.',
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
required: ['name'],
|
|
144
|
+
},
|
|
145
|
+
annotations: { readOnlyHint: true },
|
|
146
|
+
};
|
|
147
|
+
const traceCallsHandler = async (client, args, defaultWorkdir) => {
|
|
148
|
+
const name = typeof args?.name === 'string' ? args.name.trim() : '';
|
|
149
|
+
if (!name) {
|
|
150
|
+
return (0, types_1.asErrorResult)({
|
|
151
|
+
type: 'validation_error',
|
|
152
|
+
message: 'Missing required "name" parameter.',
|
|
153
|
+
code: 'MISSING_NAME',
|
|
154
|
+
recoverable: false,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
const rawDir = args?.directory;
|
|
158
|
+
const directory = (rawDir && rawDir.trim()) || defaultWorkdir || process.cwd();
|
|
159
|
+
let graph;
|
|
160
|
+
try {
|
|
161
|
+
graph = await (0, graph_cache_1.resolveOrFetchGraph)(client, directory);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
return (0, types_1.asErrorResult)({ type: 'internal_error', message: error.message, code: 'GRAPH_ERROR', recoverable: false });
|
|
165
|
+
}
|
|
166
|
+
const matches = (0, symbol_context_1.findSymbol)(graph, name);
|
|
167
|
+
if (matches.length === 0) {
|
|
168
|
+
return (0, types_1.asTextContentResult)(`No symbol matching "${name}" found.`);
|
|
169
|
+
}
|
|
170
|
+
const node = matches[0];
|
|
171
|
+
const sym = node.properties?.name || '(unknown)';
|
|
172
|
+
const adj = graph.callAdj.get(node.id);
|
|
173
|
+
const lines = [`## ${sym}`];
|
|
174
|
+
if (adj && adj.in.length > 0) {
|
|
175
|
+
lines.push(`\n**Called by (${adj.in.length}):**`);
|
|
176
|
+
adj.in
|
|
177
|
+
.map(id => graph.nodeById.get(id))
|
|
178
|
+
.filter((n) => !!n)
|
|
179
|
+
.slice(0, constants_1.MAX_SYMBOL_CALLERS)
|
|
180
|
+
.forEach(n => {
|
|
181
|
+
const cName = n.properties?.name || '?';
|
|
182
|
+
const cFile = (0, graph_cache_1.normalizePath)(n.properties?.filePath || '');
|
|
183
|
+
const cLine = n.properties?.startLine || 0;
|
|
184
|
+
lines.push(`- \`${cName}\` — ${cFile}:${cLine}`);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
if (adj && adj.out.length > 0) {
|
|
188
|
+
lines.push(`\n**Calls (${adj.out.length}):**`);
|
|
189
|
+
adj.out
|
|
190
|
+
.map(id => graph.nodeById.get(id))
|
|
191
|
+
.filter((n) => !!n)
|
|
192
|
+
.slice(0, constants_1.MAX_SYMBOL_CALLEES)
|
|
193
|
+
.forEach(n => {
|
|
194
|
+
const cName = n.properties?.name || '?';
|
|
195
|
+
const cFile = (0, graph_cache_1.normalizePath)(n.properties?.filePath || '');
|
|
196
|
+
const cLine = n.properties?.startLine || 0;
|
|
197
|
+
lines.push(`- \`${cName}\` — ${cFile}:${cLine}`);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
return (0, types_1.asTextContentResult)(lines.join('\n'));
|
|
201
|
+
};
|
|
202
|
+
exports.traceCallsEndpoint = { tool: exports.traceCallsTool, handler: traceCallsHandler };
|
|
203
|
+
// ─── Variant F: "annotate" ─────────────────────────────────────────
|
|
204
|
+
// Hypothesis: framing as supplementary enrichment ("annotate your work")
|
|
205
|
+
// makes the model fire it alongside other tools instead of waiting.
|
|
206
|
+
exports.annotateTool = {
|
|
207
|
+
name: 'annotate',
|
|
208
|
+
description: 'Enrich your understanding with structural metadata for a symbol — definition location, callers, callees. Fire alongside Read or Grep to get parallel context. Read-only, zero side effects.',
|
|
209
|
+
inputSchema: {
|
|
210
|
+
type: 'object',
|
|
211
|
+
properties: {
|
|
212
|
+
symbol: {
|
|
213
|
+
type: 'string',
|
|
214
|
+
description: 'Function, class, or method name.',
|
|
215
|
+
},
|
|
216
|
+
directory: {
|
|
217
|
+
type: 'string',
|
|
218
|
+
description: 'Repository path. Omit to use default.',
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
required: ['symbol'],
|
|
222
|
+
},
|
|
223
|
+
annotations: { readOnlyHint: true },
|
|
224
|
+
};
|
|
225
|
+
const annotateHandler = async (client, args, defaultWorkdir) => {
|
|
226
|
+
const symbol = typeof args?.symbol === 'string' ? args.symbol.trim() : '';
|
|
227
|
+
if (!symbol) {
|
|
228
|
+
return (0, types_1.asErrorResult)({
|
|
229
|
+
type: 'validation_error',
|
|
230
|
+
message: 'Missing required "symbol" parameter.',
|
|
231
|
+
code: 'MISSING_SYMBOL',
|
|
232
|
+
recoverable: false,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
const rawDir = args?.directory;
|
|
236
|
+
const directory = (rawDir && rawDir.trim()) || defaultWorkdir || process.cwd();
|
|
237
|
+
let graph;
|
|
238
|
+
try {
|
|
239
|
+
graph = await (0, graph_cache_1.resolveOrFetchGraph)(client, directory);
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
return (0, types_1.asErrorResult)({ type: 'internal_error', message: error.message, code: 'GRAPH_ERROR', recoverable: false });
|
|
243
|
+
}
|
|
244
|
+
const matches = (0, symbol_context_1.findSymbol)(graph, symbol);
|
|
245
|
+
if (matches.length === 0) {
|
|
246
|
+
return (0, types_1.asTextContentResult)(`No symbol matching "${symbol}" found.`);
|
|
247
|
+
}
|
|
248
|
+
const parts = matches.slice(0, 3).map(node => (0, symbol_context_1.renderBriefSymbolContext)(graph, node));
|
|
249
|
+
let result = parts.join('\n---\n\n');
|
|
250
|
+
if (matches.length > 3) {
|
|
251
|
+
result += `\n\n*... and ${matches.length - 3} more matches.*`;
|
|
252
|
+
}
|
|
253
|
+
return (0, types_1.asTextContentResult)(result);
|
|
254
|
+
};
|
|
255
|
+
exports.annotateEndpoint = { tool: exports.annotateTool, handler: annotateHandler };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supermodeltools/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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.10.0",
|
|
42
42
|
"archiver": "^7.0.1",
|
|
43
43
|
"ignore": "^7.0.5",
|
|
44
44
|
"jq-web": "^0.6.2",
|