@supermodeltools/mcp-server 0.4.4 → 0.4.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/server.js CHANGED
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
@@ -10,6 +43,29 @@ const sdk_1 = require("@supermodeltools/sdk");
10
43
  const create_supermodel_graph_1 = __importDefault(require("./tools/create-supermodel-graph"));
11
44
  const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
12
45
  const zip_repository_1 = require("./utils/zip-repository");
46
+ const undici_1 = require("undici");
47
+ const constants_1 = require("./constants");
48
+ const logger = __importStar(require("./utils/logger"));
49
+ // Configure HTTP timeout for API requests (default from constants)
50
+ // Some complex repos can take 10+ minutes to process
51
+ const parsedTimeout = parseInt(process.env.SUPERMODEL_TIMEOUT_MS || '', 10);
52
+ const TIMEOUT_MS = Number.isFinite(parsedTimeout) && parsedTimeout > 0
53
+ ? parsedTimeout
54
+ : constants_1.DEFAULT_API_TIMEOUT_MS;
55
+ const agent = new undici_1.Agent({
56
+ headersTimeout: TIMEOUT_MS,
57
+ bodyTimeout: TIMEOUT_MS,
58
+ connectTimeout: constants_1.CONNECTION_TIMEOUT_MS,
59
+ });
60
+ const fetchWithTimeout = (url, init) => {
61
+ return fetch(url, {
62
+ ...init,
63
+ // @ts-ignore - 'dispatcher' is a valid undici option that TypeScript's
64
+ // built-in fetch types don't recognize. This routes requests through our
65
+ // custom Agent with extended timeouts.
66
+ dispatcher: agent,
67
+ });
68
+ };
13
69
  class Server {
14
70
  server;
15
71
  client;
@@ -57,10 +113,11 @@ Example:
57
113
  const config = new sdk_1.Configuration({
58
114
  basePath: process.env.SUPERMODEL_BASE_URL || 'https://api.supermodeltools.com',
59
115
  apiKey: process.env.SUPERMODEL_API_KEY,
116
+ fetchApi: fetchWithTimeout,
60
117
  });
61
- console.error('[DEBUG] Server configuration:');
62
- console.error('[DEBUG] Base URL:', config.basePath);
63
- console.error('[DEBUG] API Key set:', !!process.env.SUPERMODEL_API_KEY);
118
+ logger.debug('Server configuration:');
119
+ logger.debug('Base URL:', config.basePath);
120
+ logger.debug('API Key set:', !!process.env.SUPERMODEL_API_KEY);
64
121
  this.client = {
65
122
  graphs: new sdk_1.DefaultApi(config),
66
123
  };
@@ -82,11 +139,10 @@ Example:
82
139
  }
83
140
  async start() {
84
141
  // Clean up any stale ZIP files from previous sessions
85
- // (older than 24 hours)
86
- await (0, zip_repository_1.cleanupOldZips)(24 * 60 * 60 * 1000);
142
+ await (0, zip_repository_1.cleanupOldZips)(constants_1.ZIP_CLEANUP_AGE_MS);
87
143
  const transport = new stdio_js_1.StdioServerTransport();
88
144
  await this.server.connect(transport);
89
- console.error('Supermodel MCP Server running on stdio');
145
+ logger.info('Supermodel MCP Server running on stdio');
90
146
  }
91
147
  }
92
148
  exports.Server = Server;
@@ -1,15 +1,48 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.handler = exports.tool = exports.metadata = void 0;
4
37
  const promises_1 = require("fs/promises");
5
38
  const child_process_1 = require("child_process");
6
39
  const crypto_1 = require("crypto");
7
- const crypto_2 = require("crypto");
8
40
  const path_1 = require("path");
9
41
  const types_1 = require("../types");
10
42
  const filtering_1 = require("../filtering");
11
43
  const queries_1 = require("../queries");
12
44
  const zip_repository_1 = require("../utils/zip-repository");
45
+ const logger = __importStar(require("../utils/logger"));
13
46
  exports.metadata = {
14
47
  resource: 'graphs',
15
48
  operation: 'write',
@@ -108,10 +141,6 @@ Query types available: graph_status, summary, get_node, search, list_nodes, func
108
141
  type: 'string',
109
142
  description: 'Path to the repository directory to analyze. Can be a subdirectory for faster analysis and smaller graph size (e.g., "/repo/src/core" instead of "/repo").',
110
143
  },
111
- 'Idempotency-Key': {
112
- type: 'string',
113
- description: 'Optional cache key in format {repo}:{type}:{hash}. If not provided, will be auto-generated using git commit hash or random UUID. Provide a previously used idempotency key to fetch a cached response, for example with a different filter.',
114
- },
115
144
  query: {
116
145
  type: 'string',
117
146
  enum: [
@@ -169,70 +198,60 @@ Query types available: graph_status, summary, get_node, search, list_nodes, func
169
198
  },
170
199
  };
171
200
  /**
172
- * Generate an idempotency key in format {repo}:supermodel:{hash}
173
- * Tries to use git commit hash, falls back to UUID-based hash
201
+ * Generate an idempotency key in format {repo}-{pathHash}:supermodel:{hash}
202
+ * Includes path hash to prevent collisions between same-named repos
174
203
  */
175
204
  function generateIdempotencyKey(directory) {
176
205
  const repoName = (0, path_1.basename)(directory);
206
+ const absolutePath = (0, path_1.resolve)(directory);
207
+ // Always include path hash to prevent collisions
208
+ const pathHash = (0, crypto_1.createHash)('sha1').update(absolutePath).digest('hex').substring(0, 7);
177
209
  let hash;
178
210
  let statusHash = '';
179
211
  try {
180
- // Try to get git commit hash
212
+ // Get git commit hash
181
213
  hash = (0, child_process_1.execSync)('git rev-parse --short HEAD', {
182
214
  cwd: directory,
183
215
  encoding: 'utf-8',
184
- stdio: ['pipe', 'pipe', 'pipe']
185
216
  }).trim();
186
- try {
187
- // Get git status to detect uncommitted changes
188
- const statusOutput = (0, child_process_1.execSync)('git status --porcelain', {
189
- cwd: directory,
190
- encoding: 'utf-8',
191
- stdio: ['pipe', 'pipe', 'pipe']
192
- }).trim();
193
- statusHash = (0, crypto_2.createHash)('sha1').update(statusOutput || 'clean').digest('hex').substring(0, 7);
194
- console.error('[DEBUG] Generated idempotency key using git hash:', hash, 'and status hash:', statusHash);
195
- }
196
- catch (statusError) {
197
- // If git status fails, just use commit hash
198
- console.error('[DEBUG] Generated idempotency key using git hash:', hash, '(git status unavailable)');
217
+ // Include working tree status in hash to detect uncommitted changes
218
+ const statusOutput = (0, child_process_1.execSync)('git status --porcelain', {
219
+ cwd: directory,
220
+ encoding: 'utf-8',
221
+ }).toString();
222
+ if (statusOutput) {
223
+ // Create hash of status output
224
+ statusHash = '-' + (0, crypto_1.createHash)('sha1')
225
+ .update(statusOutput)
226
+ .digest('hex')
227
+ .substring(0, 7);
199
228
  }
200
229
  }
201
- catch (error) {
202
- // Git not available or not a git repo, use UUID-based hash
203
- const uuid = (0, crypto_1.randomUUID)();
204
- // Hash like git does (SHA-1) and take first 7 characters
205
- hash = (0, crypto_2.createHash)('sha1').update(uuid).digest('hex').substring(0, 7);
206
- console.error('[DEBUG] Generated idempotency key using random UUID hash:', hash);
230
+ catch {
231
+ // Fallback for non-git directories: use path hash as main identifier
232
+ hash = pathHash;
207
233
  }
208
- return statusHash ? `${repoName}:supermodel:${hash}-${statusHash}` : `${repoName}:supermodel:${hash}`;
234
+ return `${repoName}-${pathHash}:supermodel:${hash}${statusHash}`;
209
235
  }
210
236
  const handler = async (client, args) => {
211
237
  if (!args) {
212
- return (0, types_1.asErrorResult)('No arguments provided');
238
+ logger.error('No arguments provided to handler');
239
+ return (0, types_1.asErrorResult)('Missing required arguments. Provide a "directory" parameter.');
213
240
  }
214
- const { jq_filter, directory, 'Idempotency-Key': providedIdempotencyKey, query, targetId, searchText, namePattern, filePathPrefix, labels, depth, relationshipTypes, limit, includeRaw, } = args;
241
+ const { jq_filter, directory, query, targetId, searchText, namePattern, filePathPrefix, labels, depth, relationshipTypes, limit, includeRaw, } = args;
215
242
  // Validate directory
216
243
  if (!directory || typeof directory !== 'string') {
217
- return (0, types_1.asErrorResult)('Directory argument is required and must be a string path');
218
- }
219
- // Generate or validate idempotency key
220
- let idempotencyKey;
221
- let keyGenerated = false;
222
- if (!providedIdempotencyKey || typeof providedIdempotencyKey !== 'string') {
223
- idempotencyKey = generateIdempotencyKey(directory);
224
- keyGenerated = true;
225
- console.error('[DEBUG] Auto-generated idempotency key:', idempotencyKey);
226
- }
227
- else {
228
- idempotencyKey = providedIdempotencyKey;
229
- console.error('[DEBUG] Using provided idempotency key:', idempotencyKey);
244
+ logger.error('Invalid directory parameter:', directory);
245
+ return (0, types_1.asErrorResult)('Invalid "directory" parameter. Provide a valid directory path as a string.');
230
246
  }
247
+ // Generate idempotency key for API request
248
+ const idempotencyKey = generateIdempotencyKey(directory);
249
+ logger.debug('Auto-generated idempotency key:', idempotencyKey);
231
250
  // Check if we can skip zipping (graph already cached)
232
251
  // Use get() atomically to avoid TOCTOU race condition
233
252
  const cachedGraph = queries_1.graphCache.get(idempotencyKey);
234
253
  if (cachedGraph && query) {
235
- console.error('[DEBUG] Graph cached, skipping ZIP creation');
254
+ logger.debug('Graph cached, skipping ZIP creation');
236
255
  // Execute query directly from cache using the cached graph
237
256
  // We pass the cached graph to executeQuery so it doesn't need to look it up again
238
257
  const result = await handleQueryModeWithCache(client, {
@@ -250,28 +269,9 @@ const handler = async (client, args) => {
250
269
  includeRaw,
251
270
  jq_filter,
252
271
  });
253
- // Add metadata about cache hit
254
- if (keyGenerated && result.content?.[0]?.type === 'text') {
255
- const originalText = result.content[0].text;
256
- let responseData;
257
- try {
258
- responseData = JSON.parse(originalText);
259
- // Add metadata about auto-generated key
260
- responseData._metadata = {
261
- ...responseData._metadata,
262
- idempotencyKey,
263
- idempotencyKeyGenerated: true
264
- };
265
- result.content[0].text = JSON.stringify(responseData, null, 2);
266
- }
267
- catch {
268
- // Not JSON, prepend key info as text
269
- result.content[0].text = `[Auto-generated Idempotency-Key: ${idempotencyKey}]\n\n${originalText}`;
270
- }
271
- }
272
272
  return result;
273
273
  }
274
- console.error('[DEBUG] Auto-zipping directory:', directory);
274
+ logger.debug('Auto-zipping directory:', directory);
275
275
  // Handle auto-zipping
276
276
  let zipPath;
277
277
  let cleanup = null;
@@ -279,24 +279,30 @@ const handler = async (client, args) => {
279
279
  const zipResult = await (0, zip_repository_1.zipRepository)(directory);
280
280
  zipPath = zipResult.path;
281
281
  cleanup = zipResult.cleanup;
282
- console.error('[DEBUG] Auto-zip complete:', zipResult.fileCount, 'files,', formatBytes(zipResult.sizeBytes));
282
+ logger.debug('Auto-zip complete:', zipResult.fileCount, 'files,', formatBytes(zipResult.sizeBytes));
283
283
  }
284
284
  catch (error) {
285
- console.error('[ERROR] Auto-zip failed:', error.message);
286
- // Provide helpful error messages
285
+ // Log full error details for debugging
286
+ logger.error('Auto-zip failed');
287
+ logger.error('Error type:', error.name || 'Error');
288
+ logger.error('Error message:', error.message);
289
+ if (error.stack) {
290
+ logger.error('Stack trace:', error.stack);
291
+ }
292
+ // Return user-friendly, actionable error messages
287
293
  if (error.message.includes('does not exist')) {
288
- return (0, types_1.asErrorResult)(`Directory does not exist: ${directory}`);
294
+ return (0, types_1.asErrorResult)(`Directory not found. Please verify the path exists: ${directory}`);
289
295
  }
290
296
  if (error.message.includes('Permission denied')) {
291
- return (0, types_1.asErrorResult)(`Permission denied accessing directory: ${directory}`);
297
+ return (0, types_1.asErrorResult)(`Permission denied. Check that you have read access to: ${directory}`);
292
298
  }
293
299
  if (error.message.includes('exceeds limit')) {
294
- return (0, types_1.asErrorResult)(error.message);
300
+ return (0, types_1.asErrorResult)(error.message + '\n\nTry analyzing a subdirectory or excluding more files.');
295
301
  }
296
302
  if (error.message.includes('ENOSPC')) {
297
- return (0, types_1.asErrorResult)('Insufficient disk space to create ZIP archive');
303
+ return (0, types_1.asErrorResult)('Insufficient disk space. Free up space and try again.');
298
304
  }
299
- return (0, types_1.asErrorResult)(`Failed to create ZIP archive: ${error.message}`);
305
+ return (0, types_1.asErrorResult)(`Failed to create ZIP archive. Check the MCP server logs for details.`);
300
306
  }
301
307
  // Execute query with cleanup handling
302
308
  try {
@@ -323,25 +329,6 @@ const handler = async (client, args) => {
323
329
  // Legacy mode: use jq_filter directly on API response
324
330
  result = await handleLegacyMode(client, zipPath, idempotencyKey, jq_filter);
325
331
  }
326
- // If key was auto-generated, add it to the response
327
- if (keyGenerated && result.content && result.content[0]?.type === 'text') {
328
- const originalText = result.content[0].text;
329
- let responseData;
330
- try {
331
- responseData = JSON.parse(originalText);
332
- // Add metadata about auto-generated key
333
- responseData._metadata = {
334
- ...responseData._metadata,
335
- idempotencyKey,
336
- idempotencyKeyGenerated: true
337
- };
338
- result.content[0].text = JSON.stringify(responseData, null, 2);
339
- }
340
- catch {
341
- // Not JSON, prepend key info as text
342
- result.content[0].text = `[Auto-generated Idempotency-Key: ${idempotencyKey}]\n\n${originalText}`;
343
- }
344
- }
345
332
  return result;
346
333
  }
347
334
  finally {
@@ -423,13 +410,47 @@ async function handleQueryMode(client, params) {
423
410
  let result = await (0, queries_1.executeQuery)(queryParams);
424
411
  // If cache miss, fetch from API and retry
425
412
  if ((0, queries_1.isQueryError)(result) && result.error.code === 'CACHE_MISS') {
426
- console.error('[DEBUG] Cache miss, fetching from API...');
413
+ logger.debug('Cache miss, fetching from API...');
427
414
  try {
428
415
  const apiResponse = await fetchFromApi(client, params.file, params.idempotencyKey);
429
416
  result = await (0, queries_1.executeQuery)(queryParams, apiResponse);
430
417
  }
431
418
  catch (error) {
432
- return (0, types_1.asErrorResult)(`API call failed: ${error.message || String(error)}`);
419
+ // Error details are already logged by fetchFromApi and logErrorResponse
420
+ // Return a user-friendly, actionable error message
421
+ let errorMessage = '';
422
+ if (error.response) {
423
+ const status = error.response.status;
424
+ switch (status) {
425
+ case 401:
426
+ errorMessage = 'Authentication failed. Set your SUPERMODEL_API_KEY environment variable and restart the MCP server.';
427
+ break;
428
+ case 403:
429
+ errorMessage = 'Access forbidden. Your API key does not have permission for this operation. Contact support if this is unexpected.';
430
+ break;
431
+ case 404:
432
+ errorMessage = 'API endpoint not found. The service URL may be incorrect. Check your SUPERMODEL_BASE_URL configuration.';
433
+ break;
434
+ case 429:
435
+ errorMessage = 'Rate limit exceeded. Wait a few minutes and try again.';
436
+ break;
437
+ case 500:
438
+ case 502:
439
+ case 503:
440
+ case 504:
441
+ errorMessage = 'Server error. The Supermodel API is temporarily unavailable. Try again in a few minutes.';
442
+ break;
443
+ default:
444
+ errorMessage = `API error (HTTP ${status}). Check the MCP server logs for details.`;
445
+ }
446
+ }
447
+ else if (error.request) {
448
+ errorMessage = 'No response from server. Check your network connection and verify the API is reachable.';
449
+ }
450
+ else {
451
+ errorMessage = 'Request failed. Check the MCP server logs for details.';
452
+ }
453
+ return (0, types_1.asErrorResult)(errorMessage);
433
454
  }
434
455
  }
435
456
  // Handle query errors
@@ -513,21 +534,129 @@ function getErrorHints(errorCode, queryType) {
513
534
  }
514
535
  }
515
536
  /**
516
- * Fetch graph from API
537
+ * Get current timestamp in ISO format
538
+ */
539
+ function getTimestamp() {
540
+ return new Date().toISOString();
541
+ }
542
+ /**
543
+ * Log HTTP request details
544
+ */
545
+ function logRequest(url, method, bodySize, idempotencyKey) {
546
+ logger.debug(`[${getTimestamp()}] [API REQUEST]`);
547
+ logger.debug(` Method: ${method}`);
548
+ logger.debug(` URL: ${url}`);
549
+ logger.debug(` Idempotency-Key: ${idempotencyKey}`);
550
+ logger.debug(` Body size: ${formatBytes(bodySize)}`);
551
+ logger.debug(` Content-Type: multipart/form-data`);
552
+ }
553
+ /**
554
+ * Log HTTP response details
555
+ */
556
+ function logResponse(status, statusText, responseSize, duration) {
557
+ logger.debug(`[${getTimestamp()}] [API RESPONSE]`);
558
+ logger.debug(` Status: ${status} ${statusText}`);
559
+ logger.debug(` Response size: ${formatBytes(responseSize)}`);
560
+ logger.debug(` Duration: ${duration}ms`);
561
+ }
562
+ /**
563
+ * Log HTTP error with full details
564
+ */
565
+ async function logErrorResponse(error) {
566
+ logger.error(`[${getTimestamp()}] [API ERROR]`);
567
+ logger.error(` Error type: ${error.name || 'Unknown'}`);
568
+ logger.error(` Error message: ${error.message || 'No message'}`);
569
+ if (error.response) {
570
+ const status = error.response.status;
571
+ const statusText = error.response.statusText || '';
572
+ logger.error(` HTTP Status: ${status} ${statusText}`);
573
+ // Log specific error messages for common status codes
574
+ switch (status) {
575
+ case 401:
576
+ logger.error(` Unauthorized: Invalid or missing API key`);
577
+ logger.error(` Check SUPERMODEL_API_KEY environment variable`);
578
+ break;
579
+ case 403:
580
+ logger.error(` Forbidden: API key valid but lacks permission`);
581
+ break;
582
+ case 404:
583
+ logger.error(` Not Found: API endpoint does not exist`);
584
+ break;
585
+ case 429:
586
+ logger.error(` Rate Limited: Too many requests`);
587
+ break;
588
+ case 500:
589
+ case 502:
590
+ case 503:
591
+ case 504:
592
+ logger.error(` Server Error: Supermodel API is experiencing issues`);
593
+ break;
594
+ }
595
+ // Try to read and log the full error response body
596
+ try {
597
+ const responseText = await error.response.text();
598
+ logger.error(` Response body: ${responseText}`);
599
+ }
600
+ catch (e) {
601
+ logger.warn(` Could not read response body: ${e}`);
602
+ }
603
+ }
604
+ else if (error.request) {
605
+ logger.error(` No response received from server`);
606
+ logger.error(` Possible network issue or timeout`);
607
+ }
608
+ else {
609
+ logger.error(` Request setup failed`);
610
+ }
611
+ if (error.stack) {
612
+ logger.error(` Stack trace: ${error.stack}`);
613
+ }
614
+ }
615
+ /**
616
+ * Fetch graph from API with comprehensive logging
517
617
  */
518
618
  async function fetchFromApi(client, file, idempotencyKey) {
519
- console.error('[DEBUG] Reading file:', file);
619
+ const startTime = Date.now();
620
+ logger.debug('Reading file:', file);
520
621
  const fileBuffer = await (0, promises_1.readFile)(file);
521
622
  const fileBlob = new Blob([fileBuffer], { type: 'application/zip' });
522
- console.error('[DEBUG] File size:', fileBuffer.length, 'bytes');
523
- console.error('[DEBUG] Making API request with idempotency key:', idempotencyKey);
623
+ const fileSize = fileBuffer.length;
624
+ logger.debug('File size:', formatBytes(fileSize));
625
+ // Get the base URL from environment or use default
626
+ const baseUrl = process.env.SUPERMODEL_BASE_URL || 'https://api.supermodeltools.com';
627
+ const apiUrl = `${baseUrl}/v1/graphs/supermodel`;
628
+ // Log the request details
629
+ logRequest(apiUrl, 'POST', fileSize, idempotencyKey);
524
630
  const requestParams = {
525
631
  file: fileBlob,
526
632
  idempotencyKey: idempotencyKey,
527
633
  };
528
- const response = await client.graphs.generateSupermodelGraph(requestParams);
529
- console.error('[DEBUG] API request successful');
530
- return response;
634
+ try {
635
+ const response = await client.graphs.generateSupermodelGraph(requestParams);
636
+ const duration = Date.now() - startTime;
637
+ // Calculate approximate response size
638
+ const responseSize = JSON.stringify(response).length;
639
+ logResponse(200, 'OK', responseSize, duration);
640
+ logger.debug(`[${getTimestamp()}] [API SUCCESS] Request completed successfully`);
641
+ return response;
642
+ }
643
+ catch (error) {
644
+ const duration = Date.now() - startTime;
645
+ logger.error(`[${getTimestamp()}] [API FAILURE] Request failed after ${duration}ms`);
646
+ // Log detailed error information
647
+ await logErrorResponse(error);
648
+ // Re-throw with enhanced error message
649
+ if (error.response?.status === 401) {
650
+ throw new Error(`API authentication failed (401 Unauthorized). Please check your SUPERMODEL_API_KEY environment variable.`);
651
+ }
652
+ else if (error.response?.status === 403) {
653
+ throw new Error(`API access forbidden (403 Forbidden). Your API key may not have permission to access this resource.`);
654
+ }
655
+ else if (error.response?.status >= 500) {
656
+ throw new Error(`Supermodel API server error (${error.response.status}). The service may be temporarily unavailable.`);
657
+ }
658
+ throw error;
659
+ }
531
660
  }
532
661
  /**
533
662
  * Legacy mode: direct jq filtering on API response
@@ -539,29 +668,44 @@ async function handleLegacyMode(client, file, idempotencyKey, jq_filter) {
539
668
  }
540
669
  catch (error) {
541
670
  if ((0, filtering_1.isJqError)(error)) {
542
- return (0, types_1.asErrorResult)(error.message);
671
+ logger.error('jq filter error:', error.message);
672
+ return (0, types_1.asErrorResult)(`Invalid jq filter syntax. Check your filter and try again.`);
543
673
  }
544
- // Enhanced error logging
545
- console.error('[ERROR] API call failed:', error);
546
- console.error('[ERROR] Error name:', error.name);
547
- console.error('[ERROR] Error message:', error.message);
548
- console.error('[ERROR] Error stack:', error.stack);
674
+ // Error details are already logged by fetchFromApi and logErrorResponse
675
+ // Return a user-friendly, actionable error message
676
+ let errorMessage = '';
549
677
  if (error.response) {
550
- console.error('[ERROR] Response status:', error.response.status);
551
- console.error('[ERROR] Response statusText:', error.response.statusText);
552
- console.error('[ERROR] Response headers:', error.response.headers);
553
- try {
554
- const responseText = await error.response.text();
555
- console.error('[ERROR] Response body:', responseText);
556
- }
557
- catch (e) {
558
- console.error('[ERROR] Could not read response body');
678
+ const status = error.response.status;
679
+ switch (status) {
680
+ case 401:
681
+ errorMessage = 'Authentication failed. Set your SUPERMODEL_API_KEY environment variable and restart the MCP server.';
682
+ break;
683
+ case 403:
684
+ errorMessage = 'Access forbidden. Your API key does not have permission for this operation. Contact support if this is unexpected.';
685
+ break;
686
+ case 404:
687
+ errorMessage = 'API endpoint not found. The service URL may be incorrect. Check your SUPERMODEL_BASE_URL configuration.';
688
+ break;
689
+ case 429:
690
+ errorMessage = 'Rate limit exceeded. Wait a few minutes and try again.';
691
+ break;
692
+ case 500:
693
+ case 502:
694
+ case 503:
695
+ case 504:
696
+ errorMessage = 'Server error. The Supermodel API is temporarily unavailable. Try again in a few minutes.';
697
+ break;
698
+ default:
699
+ errorMessage = `API error (HTTP ${status}). Check the MCP server logs for details.`;
559
700
  }
560
701
  }
561
- if (error.request) {
562
- console.error('[ERROR] Request was made but no response received');
702
+ else if (error.request) {
703
+ errorMessage = 'No response from server. Check your network connection and verify the API is reachable.';
704
+ }
705
+ else {
706
+ errorMessage = 'Request failed. Check the MCP server logs for details.';
563
707
  }
564
- return (0, types_1.asErrorResult)(`API call failed: ${error.message || String(error)}. Check server logs for details.`);
708
+ return (0, types_1.asErrorResult)(errorMessage);
565
709
  }
566
710
  }
567
711
  exports.default = { metadata: exports.metadata, tool: exports.tool, handler: exports.handler };
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ /**
3
+ * Simple logging utility with environment variable control
4
+ *
5
+ * Usage:
6
+ * DEBUG=true node dist/index.js # Enable debug logging
7
+ * node dist/index.js # Disable debug logging
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.debug = debug;
11
+ exports.info = info;
12
+ exports.warn = warn;
13
+ exports.error = error;
14
+ const DEBUG = process.env.DEBUG === 'true';
15
+ /**
16
+ * Debug log - only shown when DEBUG=true
17
+ * Uses stderr to keep separate from application output
18
+ */
19
+ function debug(msg, ...args) {
20
+ if (DEBUG) {
21
+ console.error('[DEBUG]', msg, ...args);
22
+ }
23
+ }
24
+ /**
25
+ * Info log - informational messages to stdout
26
+ */
27
+ function info(msg, ...args) {
28
+ console.log('[INFO]', msg, ...args);
29
+ }
30
+ /**
31
+ * Warning log - warnings to stderr
32
+ */
33
+ function warn(msg, ...args) {
34
+ console.error('[WARN]', msg, ...args);
35
+ }
36
+ /**
37
+ * Error log - errors to stderr
38
+ */
39
+ function error(msg, ...args) {
40
+ console.error('[ERROR]', msg, ...args);
41
+ }