@supermodeltools/mcp-server 0.9.1 → 0.9.3

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_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_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,3 +22,4 @@ 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
package/dist/index.js CHANGED
@@ -214,6 +214,13 @@ async function handlePrecache(args) {
214
214
  console.error('');
215
215
  console.error('Done! To use this cache, set SUPERMODEL_CACHE_DIR=' + outputDir);
216
216
  }
217
+ // Graceful shutdown on signals (e.g., container stop, SSH drop)
218
+ for (const signal of ['SIGTERM', 'SIGINT']) {
219
+ process.on(signal, () => {
220
+ logger.debug(`Received ${signal}, shutting down`);
221
+ process.exit(0);
222
+ });
223
+ }
217
224
  main().catch((error) => {
218
225
  logger.error('Fatal error:', error);
219
226
  process.exit(1);
package/dist/server.js CHANGED
@@ -91,21 +91,21 @@ class Server {
91
91
 
92
92
  Two tools for instant codebase understanding. Pre-computed graphs enable sub-second responses.
93
93
 
94
- ## \`overview\` — Start here
95
- Get the architecture map: domains, key files, hub functions, file/class/function counts.
96
- Call this first on any new task to understand WHERE in the codebase to look.
94
+ ## 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
98
 
98
- ## \`symbol_context\` — Deep dive on a symbol
99
- Given a function, class, or method name, get its definition location, callers, callees, domain membership, and related symbols in the same file.
100
- Use this when you know WHAT to investigate (e.g. from an issue description, stack trace, or grep result).
101
- Supports partial matching and "ClassName.method" syntax.
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.
102
102
 
103
- ## Recommended workflow
104
- 1. Call \`overview\` to understand the codebase architecture
105
- 2. Read the issue/bug description and identify relevant domains and symbols
106
- 3. Call \`symbol_context\` on key symbols to understand their structural context
107
- 4. Use Read/Grep to examine the actual source code at the identified locations
108
- 5. Make your fix and verify with tests`,
103
+ ## After fixing
104
+ Run the project's existing test suite (e.g. pytest). Do NOT write standalone test scripts.
105
+
106
+ ## 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.`,
109
109
  });
110
110
  const config = new sdk_1.Configuration({
111
111
  basePath: process.env.SUPERMODEL_BASE_URL || 'https://api.supermodeltools.com',
@@ -167,26 +167,27 @@ Supports partial matching and "ClassName.method" syntax.
167
167
  logger.warn('Failed to load cache directory:', err.message || err);
168
168
  }
169
169
  }
170
+ // Connect transport FIRST so the MCP handshake completes immediately.
171
+ // This prevents Claude Code from timing out the server (MCP_TIMEOUT=60s)
172
+ // when precaching requires a slow API call.
173
+ const transport = new stdio_js_1.StdioServerTransport();
174
+ await this.server.connect(transport);
175
+ logger.info('Supermodel MCP Server running on stdio');
170
176
  // Precache the workdir's repo if --precache flag is set.
171
- // Runs BEFORE noApiFallback is set so the API is available.
172
- // On first run for a repo this calls the Supermodel API (5-15 min).
173
- // The API has server-side idempotency caching, so repeated calls
174
- // with the same repo+commit return instantly. Results are saved to
175
- // cacheDir for cross-container persistence.
177
+ // Runs AFTER connect but BEFORE noApiFallback so the API is available.
178
+ // This is fire-and-forget from the MCP client's perspective tools
179
+ // that arrive before precaching finishes will use on-demand API calls.
176
180
  if (this.options?.precache && this.defaultWorkdir) {
177
181
  try {
178
182
  await (0, graph_cache_1.precacheForDirectory)(this.client, this.defaultWorkdir, cacheDir);
179
183
  }
180
184
  catch (err) {
181
- // Non-fatal: if precaching fails, tools fall back to no-cache error
185
+ // Non-fatal: if precaching fails, tools fall back to on-demand API
182
186
  logger.warn('Startup precache failed:', err.message || err);
183
187
  }
184
188
  }
185
189
  // NOW enable no-api-fallback (after precaching had its chance)
186
190
  (0, graph_cache_1.setNoApiFallback)(!!this.options?.noApiFallback);
187
- const transport = new stdio_js_1.StdioServerTransport();
188
- await this.server.connect(transport);
189
- logger.info('Supermodel MCP Server running on stdio');
190
191
  }
191
192
  }
192
193
  exports.Server = Server;
@@ -17,7 +17,7 @@ const api_helpers_1 = require("../utils/api-helpers");
17
17
  const constants_1 = require("../constants");
18
18
  exports.tool = {
19
19
  name: 'overview',
20
- description: `CALL THIS FIRST before grep or find. Returns a pre-computed architectural map of the entire codebase in sub-second time at zero cost. Gives you what grep/find cannot: which domains own which files, the most-called hub functions (call graph centrality), and how the codebase is structured across domains. Output is a concise summary with top architectural domains and their key files, highest-traffic functions, and file/function/class counts. Use this to know exactly where to look instead of guessing with grep.`,
20
+ description: `Returns a pre-computed architectural map of the entire codebase: which domains own which files, the most-called hub functions (call graph centrality), file/function/class counts. Sub-second, zero cost. Useful when you need to understand the overall structure before diving in. Skip this if you already know what file or symbol to investigate use symbol_context or file reading instead.`,
21
21
  inputSchema: {
22
22
  type: 'object',
23
23
  properties: {
@@ -30,7 +30,8 @@ exports.tool = {
30
30
  },
31
31
  };
32
32
  const handler = async (client, args, defaultWorkdir) => {
33
- const directory = args?.directory ?? defaultWorkdir;
33
+ const rawDir = args?.directory;
34
+ const directory = (rawDir && rawDir.trim()) || defaultWorkdir || process.cwd();
34
35
  if (!directory || typeof directory !== 'string') {
35
36
  return (0, types_1.asErrorResult)({
36
37
  type: 'validation_error',
@@ -2,8 +2,9 @@
2
2
  /**
3
3
  * `symbol_context` tool -- deep dive on a specific symbol.
4
4
  *
5
- * Given a function, class, or method name, returns (<5KB markdown):
5
+ * Given a function, class, or method name, returns (<10KB markdown):
6
6
  * - Definition location (file, line)
7
+ * - Source code (up to MAX_SOURCE_LINES)
7
8
  * - Callers (who calls this)
8
9
  * - Callees (what this calls)
9
10
  * - Domain membership
@@ -11,15 +12,53 @@
11
12
  *
12
13
  * Backed by pre-computed graphs (sub-second) with on-demand API fallback.
13
14
  */
15
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
+ desc = { enumerable: true, get: function() { return m[k]; } };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }) : (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ o[k2] = m[k];
25
+ }));
26
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
28
+ }) : function(o, v) {
29
+ o["default"] = v;
30
+ });
31
+ var __importStar = (this && this.__importStar) || (function () {
32
+ var ownKeys = function(o) {
33
+ ownKeys = Object.getOwnPropertyNames || function (o) {
34
+ var ar = [];
35
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
36
+ return ar;
37
+ };
38
+ return ownKeys(o);
39
+ };
40
+ return function (mod) {
41
+ if (mod && mod.__esModule) return mod;
42
+ var result = {};
43
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
44
+ __setModuleDefault(result, mod);
45
+ return result;
46
+ };
47
+ })();
14
48
  Object.defineProperty(exports, "__esModule", { value: true });
15
49
  exports.handler = exports.tool = void 0;
50
+ exports.findSymbol = findSymbol;
51
+ exports.renderSymbolContext = renderSymbolContext;
52
+ exports.languageFromExtension = languageFromExtension;
53
+ const fs_1 = require("fs");
54
+ const path = __importStar(require("path"));
16
55
  const types_1 = require("../types");
17
56
  const graph_cache_1 = require("../cache/graph-cache");
18
57
  const api_helpers_1 = require("../utils/api-helpers");
19
58
  const constants_1 = require("../constants");
20
59
  exports.tool = {
21
60
  name: 'symbol_context',
22
- description: `Strictly better than grep for understanding a function, class, or method. Given a symbol name, instantly returns its 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.`,
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.`,
23
62
  inputSchema: {
24
63
  type: 'object',
25
64
  properties: {
@@ -37,7 +76,8 @@ exports.tool = {
37
76
  };
38
77
  const handler = async (client, args, defaultWorkdir) => {
39
78
  const symbol = typeof args?.symbol === 'string' ? args.symbol.trim() : '';
40
- const directory = args?.directory ?? defaultWorkdir;
79
+ const rawDir = args?.directory;
80
+ const directory = (rawDir && rawDir.trim()) || defaultWorkdir || process.cwd();
41
81
  if (!symbol) {
42
82
  return (0, types_1.asErrorResult)({
43
83
  type: 'validation_error',
@@ -73,10 +113,8 @@ const handler = async (client, args, defaultWorkdir) => {
73
113
  `- Use the \`overview\` tool to see available domains and key functions`);
74
114
  }
75
115
  // Render results for top matches (usually 1, sometimes a few)
76
- const rendered = matches
77
- .slice(0, 3)
78
- .map(node => renderSymbolContext(graph, node))
79
- .join('\n---\n\n');
116
+ const renderedParts = await Promise.all(matches.slice(0, 3).map(node => renderSymbolContext(graph, node, directory)));
117
+ const rendered = renderedParts.join('\n---\n\n');
80
118
  if (matches.length > 3) {
81
119
  return (0, types_1.asTextContentResult)(rendered + `\n\n*... and ${matches.length - 3} more matches. Use a more specific name to narrow results.*`);
82
120
  }
@@ -100,7 +138,12 @@ function findSymbol(graph, query) {
100
138
  return exactIds
101
139
  .map(id => graph.nodeById.get(id))
102
140
  .filter(n => n && isCodeSymbol(n))
103
- .sort((a, b) => symbolPriority(a) - symbolPriority(b));
141
+ .sort((a, b) => {
142
+ const pDiff = symbolPriority(a) - symbolPriority(b);
143
+ if (pDiff !== 0)
144
+ return pDiff;
145
+ return callerCount(graph, b) - callerCount(graph, a);
146
+ });
104
147
  }
105
148
  // Strategy 2: ClassName.method match
106
149
  if (className && methodName) {
@@ -116,13 +159,21 @@ function findSymbol(graph, query) {
116
159
  return fp && classFilePaths.has(fp);
117
160
  });
118
161
  if (matched.length > 0) {
119
- return matched.sort((a, b) => symbolPriority(a) - symbolPriority(b));
162
+ return matched.sort((a, b) => {
163
+ const pDiff = symbolPriority(a) - symbolPriority(b);
164
+ if (pDiff !== 0)
165
+ return pDiff;
166
+ return callerCount(graph, b) - callerCount(graph, a);
167
+ });
120
168
  }
121
169
  }
122
170
  // Strategy 3: Substring match (for partial names)
171
+ if (lowerQuery.length < 2) {
172
+ return [];
173
+ }
123
174
  const substringMatches = [];
124
175
  for (const [name, ids] of graph.nameIndex) {
125
- if (name.includes(lowerQuery) || lowerQuery.includes(name)) {
176
+ if (name.includes(lowerQuery)) {
126
177
  for (const id of ids) {
127
178
  const node = graph.nodeById.get(id);
128
179
  if (node && isCodeSymbol(node)) {
@@ -131,7 +182,7 @@ function findSymbol(graph, query) {
131
182
  }
132
183
  }
133
184
  }
134
- // Sort by relevance: exact prefix > contains > contained-in
185
+ // Sort by relevance: exact prefix > contains, then symbol priority, then caller count
135
186
  substringMatches.sort((a, b) => {
136
187
  const aName = (a.properties?.name || '').toLowerCase();
137
188
  const bName = (b.properties?.name || '').toLowerCase();
@@ -139,7 +190,10 @@ function findSymbol(graph, query) {
139
190
  const bPrefix = bName.startsWith(lowerQuery) ? 0 : 1;
140
191
  if (aPrefix !== bPrefix)
141
192
  return aPrefix - bPrefix;
142
- return symbolPriority(a) - symbolPriority(b);
193
+ const pDiff = symbolPriority(a) - symbolPriority(b);
194
+ if (pDiff !== 0)
195
+ return pDiff;
196
+ return callerCount(graph, b) - callerCount(graph, a);
143
197
  });
144
198
  return substringMatches.slice(0, 10);
145
199
  }
@@ -157,8 +211,11 @@ function symbolPriority(node) {
157
211
  return 2;
158
212
  return 3;
159
213
  }
214
+ function callerCount(graph, node) {
215
+ return graph.callAdj.get(node.id)?.in.length || 0;
216
+ }
160
217
  // ── Rendering ──
161
- function renderSymbolContext(graph, node) {
218
+ async function renderSymbolContext(graph, node, directory) {
162
219
  const name = node.properties?.name || '(unknown)';
163
220
  const rawFilePath = node.properties?.filePath || '';
164
221
  const filePath = (0, graph_cache_1.normalizePath)(rawFilePath);
@@ -178,6 +235,31 @@ function renderSymbolContext(graph, node) {
178
235
  lines.push(`**Domain:** ${domain}`);
179
236
  }
180
237
  lines.push('');
238
+ // Source code
239
+ if (filePath && startLine > 0) {
240
+ try {
241
+ const absPath = path.resolve(directory, filePath);
242
+ const content = await fs_1.promises.readFile(absPath, 'utf-8');
243
+ const fileLines = content.split('\n');
244
+ const end = endLine > 0 ? Math.min(endLine, startLine + constants_1.MAX_SOURCE_LINES - 1) : startLine + constants_1.MAX_SOURCE_LINES - 1;
245
+ const sourceSlice = fileLines.slice(startLine - 1, end);
246
+ if (sourceSlice.length > 0) {
247
+ const lang = languageFromExtension(filePath);
248
+ lines.push(`### Source`);
249
+ lines.push('');
250
+ lines.push(`\`\`\`${lang}`);
251
+ lines.push(sourceSlice.join('\n'));
252
+ lines.push('```');
253
+ if (endLine > 0 && endLine > startLine + constants_1.MAX_SOURCE_LINES - 1) {
254
+ lines.push(`*... truncated (showing ${constants_1.MAX_SOURCE_LINES} of ${endLine - startLine + 1} lines)*`);
255
+ }
256
+ lines.push('');
257
+ }
258
+ }
259
+ catch {
260
+ // File unreadable — skip source section silently
261
+ }
262
+ }
181
263
  // Callers
182
264
  const adj = graph.callAdj.get(node.id);
183
265
  if (adj && adj.in.length > 0) {
@@ -274,6 +356,48 @@ function renderSymbolContext(graph, node) {
274
356
  }
275
357
  return lines.join('\n');
276
358
  }
359
+ function languageFromExtension(filePath) {
360
+ const ext = path.extname(filePath).toLowerCase();
361
+ const map = {
362
+ '.ts': 'typescript',
363
+ '.tsx': 'typescript',
364
+ '.js': 'javascript',
365
+ '.jsx': 'javascript',
366
+ '.py': 'python',
367
+ '.rb': 'ruby',
368
+ '.go': 'go',
369
+ '.rs': 'rust',
370
+ '.java': 'java',
371
+ '.kt': 'kotlin',
372
+ '.cs': 'csharp',
373
+ '.cpp': 'cpp',
374
+ '.c': 'c',
375
+ '.h': 'c',
376
+ '.hpp': 'cpp',
377
+ '.swift': 'swift',
378
+ '.php': 'php',
379
+ '.scala': 'scala',
380
+ '.sh': 'bash',
381
+ '.bash': 'bash',
382
+ '.yaml': 'yaml',
383
+ '.yml': 'yaml',
384
+ '.json': 'json',
385
+ '.xml': 'xml',
386
+ '.html': 'html',
387
+ '.css': 'css',
388
+ '.sql': 'sql',
389
+ '.r': 'r',
390
+ '.lua': 'lua',
391
+ '.dart': 'dart',
392
+ '.ex': 'elixir',
393
+ '.exs': 'elixir',
394
+ '.erl': 'erlang',
395
+ '.hs': 'haskell',
396
+ '.ml': 'ocaml',
397
+ '.clj': 'clojure',
398
+ };
399
+ return map[ext] || '';
400
+ }
277
401
  function findDomain(graph, nodeId) {
278
402
  for (const [domainName, data] of graph.domainIndex) {
279
403
  if (data.memberIds.includes(nodeId)) {
@@ -66,6 +66,16 @@ function classifyApiError(error) {
66
66
  details: { errorType: typeof error },
67
67
  };
68
68
  }
69
+ // Fast-fail: no pre-computed cache and API fallback disabled
70
+ if (error.code === 'NO_CACHE') {
71
+ return {
72
+ type: 'cache_error',
73
+ message: error.message || 'No pre-computed graph available for this repository.',
74
+ code: 'NO_CACHE',
75
+ recoverable: false,
76
+ suggestion: 'Use grep, find, and file reading to explore the codebase instead. Or run the precache command and set SUPERMODEL_CACHE_DIR.',
77
+ };
78
+ }
69
79
  if (error.response) {
70
80
  const status = error.response.status;
71
81
  switch (status) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supermodeltools/mcp-server",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
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.1",
41
+ "@supermodeltools/sdk": "0.9.3",
42
42
  "archiver": "^7.0.1",
43
43
  "ignore": "^7.0.5",
44
44
  "jq-web": "^0.6.2",