@supermodeltools/mcp-server 0.9.5 → 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 CHANGED
@@ -4,7 +4,7 @@
4
4
  [![MCP](https://img.shields.io/badge/MCP-compatible-blue)](https://modelcontextprotocol.io)
5
5
  [![CI](https://github.com/supermodeltools/mcp/actions/workflows/ci.yml/badge.svg)](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). Two tools `overview` and `symbol_context` backed by pre-computed code graphs for sub-second responses.
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
- ### `overview`
136
+ ### `symbol_context` (Default Mode)
136
137
 
137
- Returns a pre-computed architectural map of the codebase in sub-second time. Call this first on any new task.
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
- - Top architectural domains and their key files
141
- - Most-called hub functions (call graph centrality)
142
- - File, function, and class counts
143
- - Primary language and graph statistics
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
- - "Give me an overview of this codebase"
153
- - "What's the architecture of this project?"
160
+ - "Look up the symbol `filter_queryset` in this codebase"
161
+ - "What calls `QuerySet.filter` and what does it call?"
154
162
 
155
- ### `symbol_context`
163
+ ### GraphRAG Mode (Experimental)
156
164
 
157
- Deep dive on a specific function, class, or method. Given a symbol name, instantly returns its definition location, all callers, all callees, domain membership, and related symbols in the same file.
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
- **Output includes:**
160
- - Definition location (file, line range)
161
- - Callers (who calls this symbol)
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 | Name of the function, class, or method. Supports `ClassName.method` syntax and partial matching. |
172
- | `directory` | string | No | Path to repository directory. Omit if server was started with a default workdir. |
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
- **Example prompts:**
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
- 1. Call `overview` to understand the codebase architecture
181
- 2. Read the issue/bug description and identify relevant domains and symbols
182
- 3. Call `symbol_context` on key symbols to understand their structural context
183
- 4. Use Read/Grep to examine the actual source code at the identified locations
184
- 5. Make your fix and verify with tests
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 = __importDefault(require("./tools/symbol-context"));
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
- this.server = new mcp_js_1.McpServer({
86
- name: 'supermodel_api',
87
- version: '0.0.1',
88
- }, {
89
- capabilities: { tools: {}, logging: {} },
90
- instructions: `# Supermodel: Codebase Intelligence
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 allTools = [
134
- symbol_context_1.default,
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.9.5",
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.9.5",
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",