@supermodeltools/mcp-server 0.9.4 → 0.9.6

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/dist/constants.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * Single source of truth for configuration values
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.MAX_SOURCE_LINES = exports.MAX_SYMBOL_RELATED = exports.MAX_SYMBOL_CALLEES = exports.MAX_SYMBOL_CALLERS = exports.MAX_OVERVIEW_HUB_FUNCTIONS = exports.MAX_OVERVIEW_DOMAINS = exports.DEFAULT_CACHE_TTL_MS = exports.DEFAULT_MAX_NODES = exports.DEFAULT_MAX_GRAPHS = exports.MAX_ZIP_SIZE_BYTES = exports.ZIP_CLEANUP_AGE_MS = exports.CONNECTION_TIMEOUT_MS = exports.DEFAULT_API_TIMEOUT_MS = void 0;
7
+ exports.MAX_BRIEF_CALLEES = exports.MAX_BRIEF_CALLERS = exports.MAX_BATCH_SYMBOLS = exports.MAX_SOURCE_LINES = exports.MAX_SYMBOL_RELATED = exports.MAX_SYMBOL_CALLEES = exports.MAX_SYMBOL_CALLERS = exports.MAX_OVERVIEW_HUB_FUNCTIONS = exports.MAX_OVERVIEW_DOMAINS = exports.DEFAULT_CACHE_TTL_MS = exports.DEFAULT_MAX_NODES = exports.DEFAULT_MAX_GRAPHS = exports.MAX_ZIP_SIZE_BYTES = exports.ZIP_CLEANUP_AGE_MS = exports.CONNECTION_TIMEOUT_MS = exports.DEFAULT_API_TIMEOUT_MS = void 0;
8
8
  // HTTP timeout configuration
9
9
  exports.DEFAULT_API_TIMEOUT_MS = 900_000; // 15 minutes (complex repos can take 10+ min)
10
10
  exports.CONNECTION_TIMEOUT_MS = 30_000; // 30 seconds to establish connection
@@ -22,4 +22,8 @@ exports.MAX_OVERVIEW_HUB_FUNCTIONS = 10; // Top N hub functions to show
22
22
  exports.MAX_SYMBOL_CALLERS = 10; // Top N callers to show
23
23
  exports.MAX_SYMBOL_CALLEES = 10; // Top N callees to show
24
24
  exports.MAX_SYMBOL_RELATED = 8; // Related symbols in same file
25
- exports.MAX_SOURCE_LINES = 50; // Max lines of source code to include inline
25
+ exports.MAX_SOURCE_LINES = 80; // Max lines of source code to include inline
26
+ // Batch symbol_context limits
27
+ exports.MAX_BATCH_SYMBOLS = 10; // Max symbols per batch call
28
+ exports.MAX_BRIEF_CALLERS = 5; // Callers shown in brief mode
29
+ exports.MAX_BRIEF_CALLEES = 5; // Callees shown in brief mode
package/dist/server.js CHANGED
@@ -40,7 +40,7 @@ exports.Server = void 0;
40
40
  /**
41
41
  * MCP Server implementation for the Supermodel codebase analysis tools.
42
42
  * Redesigned for maximum SWE-bench performance:
43
- * - 2 tools (overview, symbol_context) instead of 10
43
+ * - 1 tool (symbol_context) with batch support; overview injected into instructions
44
44
  * - Pre-computed graph support for sub-second response times
45
45
  * - On-demand API fallback when no cache exists
46
46
  * @module server
@@ -48,7 +48,7 @@ exports.Server = void 0;
48
48
  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
- const overview_1 = __importDefault(require("./tools/overview"));
51
+ const overview_1 = require("./tools/overview");
52
52
  const symbol_context_1 = __importDefault(require("./tools/symbol-context"));
53
53
  const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
54
54
  const zip_repository_1 = require("./utils/zip-repository");
@@ -89,23 +89,28 @@ class Server {
89
89
  capabilities: { tools: {}, logging: {} },
90
90
  instructions: `# Supermodel: Codebase Intelligence
91
91
 
92
- Two tools for instant codebase understanding. Pre-computed graphs enable sub-second responses.
92
+ One read-only tool for instant codebase understanding. Pre-computed graphs enable sub-second responses.
93
+
94
+ The codebase overview is included below in these instructions — you already have the architecture map.
93
95
 
94
96
  ## Recommended workflow
95
- 1. \`overview\` first to learn architecture and key symbols (1 call)
96
- 2. \`symbol_context\` on 1-2 symbols from the issue to see source, callers, callees (1-2 calls)
97
- 3. Stop calling MCP tools. Use the results to make your fix.
97
+ 1. Identify symbols from the issue/overview and call \`symbol_context\` to explore them.
98
+ Batch via \`symbols\` array or issue multiple calls in parallel (read-only, safe).
99
+ 2. Stop calling MCP tools. Start editing by turn 3. Max 3 MCP calls total.
98
100
 
99
- ## Anti-patterns
100
- - >3 MCP calls total = diminishing returns. Aim for 1 overview + 1-2 symbol lookups.
101
- - Chasing callers-of-callers burns iterations without helping.
101
+ ## Rules
102
+ - Do NOT use TodoWrite. Act directly.
103
+ - Use the Task tool to delegate subtasks (e.g. running tests, exploring tangential code).
104
+ - >2 MCP turns = diminishing returns. Explore everything you need in one turn.
102
105
 
103
106
  ## After fixing
104
- Run the project's existing test suite (e.g. pytest). Do NOT write standalone test scripts.
107
+ Run the full related test suite to catch regressions. Do NOT write standalone test scripts.
105
108
 
106
109
  ## Tool reference
107
- - \`overview\`: Architecture map, domains, hub functions, file counts. Zero-arg, sub-second.
108
- - \`symbol_context\`: Source, callers, callees, domain for any function/class/method. Supports "Class.method" and partial matching.`,
110
+ - \`symbol_context\`: Source, callers, callees, domain for any function/class/method.
111
+ Supports "Class.method", partial matching, and batch lookups via \`symbols\` array.
112
+ Use \`brief: true\` for compact output when looking up 3+ symbols.
113
+ Read-only — safe to call in parallel.`,
109
114
  });
110
115
  const config = new sdk_1.Configuration({
111
116
  basePath: process.env.SUPERMODEL_BASE_URL || 'https://api.supermodeltools.com',
@@ -126,7 +131,6 @@ Run the project's existing test suite (e.g. pytest). Do NOT write standalone tes
126
131
  }
127
132
  setupHandlers() {
128
133
  const allTools = [
129
- overview_1.default,
130
134
  symbol_context_1.default,
131
135
  ];
132
136
  // Create a map for quick handler lookup
@@ -151,6 +155,37 @@ Run the project's existing test suite (e.g. pytest). Do NOT write standalone tes
151
155
  throw new Error(`Unknown tool: ${name}`);
152
156
  });
153
157
  }
158
+ getTestHint(primaryLanguage) {
159
+ switch (primaryLanguage.toLowerCase()) {
160
+ case 'python': return '\n\n**Test with:** `python -m pytest <test_file> -x`';
161
+ case 'javascript':
162
+ case 'typescript': return '\n\n**Test with:** `npm test`';
163
+ case 'go': return '\n\n**Test with:** `go test ./...`';
164
+ case 'rust': return '\n\n**Test with:** `cargo test`';
165
+ case 'java': return '\n\n**Test with:** `mvn test` or `gradle test`';
166
+ case 'ruby': return '\n\n**Test with:** `bundle exec rake test`';
167
+ default: return '';
168
+ }
169
+ }
170
+ injectOverviewInstructions(repoMap) {
171
+ if (repoMap.size === 0)
172
+ return;
173
+ // Only inject if there's exactly 1 unique graph (SWE-bench always has exactly 1 repo)
174
+ const uniqueGraphs = new Set([...repoMap.values()]);
175
+ if (uniqueGraphs.size !== 1)
176
+ return;
177
+ const graph = [...uniqueGraphs][0];
178
+ try {
179
+ const overview = (0, overview_1.renderOverview)(graph);
180
+ const testHint = this.getTestHint(graph.summary.primaryLanguage);
181
+ const current = this.server.server._instructions;
182
+ this.server.server._instructions = (current || '') + '\n\n' + overview + testHint;
183
+ logger.debug('Injected overview into server instructions');
184
+ }
185
+ catch (err) {
186
+ logger.warn('Failed to render overview for instructions:', err.message || err);
187
+ }
188
+ }
154
189
  async start() {
155
190
  // Clean up any stale ZIP files from previous sessions
156
191
  await (0, zip_repository_1.cleanupOldZips)(constants_1.ZIP_CLEANUP_AGE_MS);
@@ -162,6 +197,7 @@ Run the project's existing test suite (e.g. pytest). Do NOT write standalone tes
162
197
  const repoMap = await (0, graph_cache_1.loadCacheFromDisk)(cacheDir, graph_cache_1.graphCache);
163
198
  (0, graph_cache_1.setRepoMap)(repoMap);
164
199
  logger.debug(`Loaded ${repoMap.size} repo mappings`);
200
+ this.injectOverviewInstructions(repoMap);
165
201
  }
166
202
  catch (err) {
167
203
  logger.warn('Failed to load cache directory:', err.message || err);
@@ -11,6 +11,7 @@
11
11
  */
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.handler = exports.tool = void 0;
14
+ exports.renderOverview = renderOverview;
14
15
  const types_1 = require("../types");
15
16
  const graph_cache_1 = require("../cache/graph-cache");
16
17
  const api_helpers_1 = require("../utils/api-helpers");
@@ -28,6 +29,9 @@ exports.tool = {
28
29
  },
29
30
  required: [],
30
31
  },
32
+ annotations: {
33
+ readOnlyHint: true,
34
+ },
31
35
  };
32
36
  const handler = async (client, args, defaultWorkdir) => {
33
37
  const rawDir = args?.directory;
@@ -48,6 +48,7 @@ var __importStar = (this && this.__importStar) || (function () {
48
48
  Object.defineProperty(exports, "__esModule", { value: true });
49
49
  exports.handler = exports.tool = void 0;
50
50
  exports.findSymbol = findSymbol;
51
+ exports.renderBriefSymbolContext = renderBriefSymbolContext;
51
52
  exports.renderSymbolContext = renderSymbolContext;
52
53
  exports.languageFromExtension = languageFromExtension;
53
54
  const fs_1 = require("fs");
@@ -58,7 +59,7 @@ const api_helpers_1 = require("../utils/api-helpers");
58
59
  const constants_1 = require("../constants");
59
60
  exports.tool = {
60
61
  name: 'symbol_context',
61
- description: `Strictly better than grep for understanding a function, class, or method. Given a symbol name, instantly returns its source code, definition location, all callers, all callees, architectural domain, and related symbols in the same file -- structural context that grep cannot reconstruct. Sub-second, zero cost. Supports partial matching ("filter" finds "QuerySet.filter", "filter_queryset", etc.) and "ClassName.method" syntax. Use this whenever you have a symbol name from a stack trace, issue, or search result and need to understand how it connects to the rest of the codebase.`,
62
+ description: `Strictly better than grep for understanding a function, class, or method. Given a symbol name, instantly returns its source code, definition location, all callers, all callees, architectural domain, and related symbols in the same file -- structural context that grep cannot reconstruct. Sub-second, zero cost, read-only. Supports partial matching ("filter" finds "QuerySet.filter", "filter_queryset", etc.) and "ClassName.method" syntax. You can issue multiple calls in parallel (read-only, no side effects) or batch symbols into one call via the "symbols" array.`,
62
63
  inputSchema: {
63
64
  type: 'object',
64
65
  properties: {
@@ -66,27 +67,54 @@ exports.tool = {
66
67
  type: 'string',
67
68
  description: 'Name of the function, class, or method to look up. Supports "ClassName.method" syntax.',
68
69
  },
70
+ symbols: {
71
+ type: 'array',
72
+ items: { type: 'string' },
73
+ description: 'Array of symbol names to look up in parallel. More efficient than multiple calls.',
74
+ maxItems: 10,
75
+ },
69
76
  directory: {
70
77
  type: 'string',
71
78
  description: 'Path to the repository directory. Omit if the MCP server was started with a default workdir.',
72
79
  },
80
+ brief: {
81
+ type: 'boolean',
82
+ description: 'Return compact output (definition + callers only, no source code). Recommended when looking up 3+ symbols.',
83
+ },
73
84
  },
74
- required: ['symbol'],
85
+ required: [],
86
+ },
87
+ annotations: {
88
+ readOnlyHint: true,
75
89
  },
76
90
  };
77
91
  const handler = async (client, args, defaultWorkdir) => {
78
- const symbol = typeof args?.symbol === 'string' ? args.symbol.trim() : '';
79
- const rawDir = args?.directory;
80
- const directory = (rawDir && rawDir.trim()) || defaultWorkdir || process.cwd();
81
- if (!symbol) {
92
+ // Extract symbol list: prefer `symbols` array, fall back to single `symbol`
93
+ let symbolList;
94
+ const symbolsArg = args?.symbols;
95
+ const symbolArg = typeof args?.symbol === 'string' ? args.symbol.trim() : '';
96
+ if (Array.isArray(symbolsArg) && symbolsArg.length > 0) {
97
+ symbolList = symbolsArg
98
+ .filter((s) => typeof s === 'string')
99
+ .map(s => s.trim())
100
+ .filter(s => s.length > 0)
101
+ .slice(0, constants_1.MAX_BATCH_SYMBOLS);
102
+ }
103
+ else if (symbolArg) {
104
+ symbolList = [symbolArg];
105
+ }
106
+ else {
82
107
  return (0, types_1.asErrorResult)({
83
108
  type: 'validation_error',
84
- message: 'Missing required "symbol" parameter.',
109
+ message: 'Missing required "symbol" or "symbols" parameter.',
85
110
  code: 'MISSING_SYMBOL',
86
111
  recoverable: false,
87
112
  suggestion: 'Provide the name of a function, class, or method to look up.',
88
113
  });
89
114
  }
115
+ const brief = !!args?.brief;
116
+ const rawDir = args?.directory;
117
+ const directory = (rawDir && rawDir.trim()) || defaultWorkdir || process.cwd();
90
118
  if (!directory || typeof directory !== 'string') {
91
119
  return (0, types_1.asErrorResult)({
92
120
  type: 'validation_error',
@@ -103,22 +131,33 @@ const handler = async (client, args, defaultWorkdir) => {
103
131
  catch (error) {
104
132
  return (0, types_1.asErrorResult)((0, api_helpers_1.classifyApiError)(error));
105
133
  }
106
- // Find the symbol
107
- const matches = findSymbol(graph, symbol);
108
- if (matches.length === 0) {
109
- return (0, types_1.asTextContentResult)(`No symbol matching "${symbol}" found in the code graph.\n\n` +
110
- `Try:\n` +
111
- `- A different spelling or casing\n` +
112
- `- Just the function name without the class prefix\n` +
113
- `- Use the \`overview\` tool to see available domains and key functions`);
114
- }
115
- // Render results for top matches (usually 1, sometimes a few)
116
- const renderedParts = await Promise.all(matches.slice(0, 3).map(node => renderSymbolContext(graph, node, directory)));
117
- const rendered = renderedParts.join('\n---\n\n');
118
- if (matches.length > 3) {
119
- return (0, types_1.asTextContentResult)(rendered + `\n\n*... and ${matches.length - 3} more matches. Use a more specific name to narrow results.*`);
134
+ const isBatch = symbolList.length > 1;
135
+ const useBrief = brief || isBatch;
136
+ const allRendered = [];
137
+ for (const sym of symbolList) {
138
+ const matches = findSymbol(graph, sym);
139
+ if (matches.length === 0) {
140
+ allRendered.push(`## ${sym}\n\nNo symbol matching "${sym}" found in the code graph.`);
141
+ continue;
142
+ }
143
+ if (useBrief) {
144
+ // Brief mode: compact output for each top match
145
+ const parts = matches.slice(0, 3).map(node => renderBriefSymbolContext(graph, node));
146
+ allRendered.push(parts.join('\n---\n\n'));
147
+ if (matches.length > 3) {
148
+ allRendered.push(`*... and ${matches.length - 3} more matches for "${sym}".*`);
149
+ }
150
+ }
151
+ else {
152
+ // Full mode: detailed output (single symbol, not brief)
153
+ const parts = await Promise.all(matches.slice(0, 3).map(node => renderSymbolContext(graph, node, directory)));
154
+ allRendered.push(parts.join('\n---\n\n'));
155
+ if (matches.length > 3) {
156
+ allRendered.push(`*... and ${matches.length - 3} more matches. Use a more specific name to narrow results.*`);
157
+ }
158
+ }
120
159
  }
121
- return (0, types_1.asTextContentResult)(rendered);
160
+ return (0, types_1.asTextContentResult)(allRendered.join('\n\n---\n\n'));
122
161
  };
123
162
  exports.handler = handler;
124
163
  // ── Symbol lookup ──
@@ -215,6 +254,45 @@ function callerCount(graph, node) {
215
254
  return graph.callAdj.get(node.id)?.in.length || 0;
216
255
  }
217
256
  // ── Rendering ──
257
+ function renderBriefSymbolContext(graph, node) {
258
+ const name = node.properties?.name || '(unknown)';
259
+ const rawFilePath = node.properties?.filePath || '';
260
+ const filePath = (0, graph_cache_1.normalizePath)(rawFilePath);
261
+ const startLine = node.properties?.startLine || 0;
262
+ const endLine = node.properties?.endLine || 0;
263
+ const kind = node.properties?.kind || node.labels?.[0]?.toLowerCase() || 'symbol';
264
+ const language = node.properties?.language || '';
265
+ const lines = [];
266
+ lines.push(`## ${name}`);
267
+ lines.push(`**Defined in:** ${filePath}${startLine ? ':' + startLine : ''}${endLine ? '-' + endLine : ''}`);
268
+ lines.push(`**Type:** ${kind}${language ? ' (' + language + ')' : ''}`);
269
+ const domain = findDomain(graph, node.id);
270
+ if (domain) {
271
+ lines.push(`**Domain:** ${domain}`);
272
+ }
273
+ // Callers (inline, max MAX_BRIEF_CALLERS)
274
+ const adj = graph.callAdj.get(node.id);
275
+ if (adj && adj.in.length > 0) {
276
+ const callerNames = adj.in
277
+ .map(id => graph.nodeById.get(id))
278
+ .filter((n) => !!n)
279
+ .map(n => `\`${n.properties?.name || '(unknown)'}\``)
280
+ .slice(0, constants_1.MAX_BRIEF_CALLERS);
281
+ const suffix = adj.in.length > constants_1.MAX_BRIEF_CALLERS ? `, ... (${adj.in.length} total)` : '';
282
+ lines.push(`**Called by:** ${callerNames.join(', ')}${suffix}`);
283
+ }
284
+ // Callees (inline, max MAX_BRIEF_CALLEES)
285
+ if (adj && adj.out.length > 0) {
286
+ const calleeNames = adj.out
287
+ .map(id => graph.nodeById.get(id))
288
+ .filter((n) => !!n)
289
+ .map(n => `\`${n.properties?.name || '(unknown)'}\``)
290
+ .slice(0, constants_1.MAX_BRIEF_CALLEES);
291
+ const suffix = adj.out.length > constants_1.MAX_BRIEF_CALLEES ? `, ... (${adj.out.length} total)` : '';
292
+ lines.push(`**Calls:** ${calleeNames.join(', ')}${suffix}`);
293
+ }
294
+ return lines.join('\n');
295
+ }
218
296
  async function renderSymbolContext(graph, node, directory) {
219
297
  const name = node.properties?.name || '(unknown)';
220
298
  const rawFilePath = node.properties?.filePath || '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supermodeltools/mcp-server",
3
- "version": "0.9.4",
3
+ "version": "0.9.6",
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.4",
41
+ "@supermodeltools/sdk": "0.9.6",
42
42
  "archiver": "^7.0.1",
43
43
  "ignore": "^7.0.5",
44
44
  "jq-web": "^0.6.2",