@supermodeltools/mcp-server 0.5.4 → 0.6.2

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
@@ -212,6 +212,44 @@ Verify installation:
212
212
  claude mcp list
213
213
  ```
214
214
 
215
+ ## Health Checks
216
+
217
+ This MCP server implements the [MCP ping utility](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/ping) for connection health monitoring. The ping mechanism allows clients to verify that the server is responsive and the connection remains alive.
218
+
219
+ ### How It Works
220
+
221
+ - **Request**: Client sends a `ping` JSON-RPC request with no parameters
222
+ - **Response**: Server responds promptly with an empty result object `{}`
223
+ - **Automatic**: Handled automatically by the MCP SDK - no additional configuration needed
224
+
225
+ ### Use Cases
226
+
227
+ - **Pre-flight checks**: Verify server is accessible before starting work
228
+ - **Connection monitoring**: Detect stale connections during long-running sessions
229
+ - **Periodic health checks**: Confirm server remains responsive
230
+
231
+ ### Example
232
+
233
+ ```json
234
+ // Request
235
+ {
236
+ "jsonrpc": "2.0",
237
+ "id": "123",
238
+ "method": "ping"
239
+ }
240
+
241
+ // Response
242
+ {
243
+ "jsonrpc": "2.0",
244
+ "id": "123",
245
+ "result": {}
246
+ }
247
+ ```
248
+
249
+ If the server doesn't respond within a reasonable timeout (typically 5-10 seconds), the connection should be considered stale.
250
+
251
+ For more details, see the [MCP specification for ping/health checks](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/ping).
252
+
215
253
  ## Tools
216
254
 
217
255
  ### `explore_codebase`
package/dist/server.js CHANGED
@@ -110,7 +110,20 @@ For localized bugs:
110
110
 
111
111
  Example:
112
112
  - Full repo: directory="/repo" → 180MB, 50k nodes
113
- - Subsystem: directory="/repo/django/db" → 15MB, 3k nodes`,
113
+ - Subsystem: directory="/repo/django/db" → 15MB, 3k nodes
114
+
115
+ ## Error Handling
116
+
117
+ When a tool call returns an error, check the structured error fields:
118
+ - \`error.recoverable: true\` → wait briefly and retry the request
119
+ - \`error.reportable: true\` → this is a server-side bug, not your fault
120
+
121
+ If you receive a reportable error and have GitHub access:
122
+ 1. Search the \`error.repo\` issues for the \`error.code\` to avoid duplicates
123
+ 2. If no existing issue, open one with the error code, message, and any reproduction context
124
+ 3. If you can identify and fix the root cause, fork the repo and open a PR
125
+
126
+ This helps the maintainers fix bugs faster and avoids wasting your iteration budget on issues you cannot work around locally.`,
114
127
  });
115
128
  const config = new sdk_1.Configuration({
116
129
  basePath: process.env.SUPERMODEL_BASE_URL || 'https://api.supermodeltools.com',
@@ -123,8 +136,9 @@ Example:
123
136
  if (this.defaultWorkdir) {
124
137
  logger.debug('Default workdir:', this.defaultWorkdir);
125
138
  }
139
+ const api = new sdk_1.DefaultApi(config);
126
140
  this.client = {
127
- graphs: new sdk_1.DefaultApi(config),
141
+ graphs: new sdk_1.SupermodelClient(api),
128
142
  };
129
143
  this.setupHandlers();
130
144
  }
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.handler = exports.tool = exports.metadata = void 0;
37
+ exports.classifyApiError = classifyApiError;
37
38
  const promises_1 = require("fs/promises");
38
39
  const child_process_1 = require("child_process");
39
40
  const crypto_1 = require("crypto");
@@ -43,6 +44,8 @@ const filtering_1 = require("../filtering");
43
44
  const queries_1 = require("../queries");
44
45
  const zip_repository_1 = require("../utils/zip-repository");
45
46
  const logger = __importStar(require("../utils/logger"));
47
+ const REPORT_REPO = 'https://github.com/supermodeltools/mcp.git';
48
+ const REPORT_SUGGESTION = 'This may be a bug in the MCP server. You can help by opening an issue at https://github.com/supermodeltools/mcp/issues with the error details, or fork the repo and open a PR with a fix.';
46
49
  exports.metadata = {
47
50
  resource: 'graphs',
48
51
  operation: 'write',
@@ -245,12 +248,24 @@ const handler = async (client, args, defaultWorkdir) => {
245
248
  // Validate directory - check if explicitly invalid first
246
249
  if (providedDirectory !== undefined && typeof providedDirectory !== 'string') {
247
250
  logger.error('Invalid directory parameter:', providedDirectory);
248
- return (0, types_1.asErrorResult)('Invalid "directory" parameter. Provide a valid directory path as a string.');
251
+ return (0, types_1.asErrorResult)({
252
+ type: 'validation_error',
253
+ message: 'Invalid "directory" parameter. Provide a valid directory path as a string.',
254
+ code: 'INVALID_DIRECTORY',
255
+ recoverable: false,
256
+ suggestion: 'Pass directory as a string path, e.g. directory="/workspace/my-repo"',
257
+ });
249
258
  }
250
259
  // Check if we have any directory at all
251
260
  if (!directory || typeof directory !== 'string') {
252
261
  logger.error('Invalid directory parameter:', directory);
253
- return (0, types_1.asErrorResult)('No "directory" parameter provided and no default workdir configured. Please provide a directory path or start the server with a workdir argument.');
262
+ return (0, types_1.asErrorResult)({
263
+ type: 'validation_error',
264
+ message: 'No "directory" parameter provided and no default workdir configured.',
265
+ code: 'MISSING_DIRECTORY',
266
+ recoverable: false,
267
+ suggestion: 'Provide a directory path or start the MCP server with a workdir argument (e.g. npx @anthropic-ai/supermodel-mcp /path/to/repo).',
268
+ });
254
269
  }
255
270
  if (providedDirectory) {
256
271
  logger.debug('Using provided directory:', directory);
@@ -303,20 +318,58 @@ const handler = async (client, args, defaultWorkdir) => {
303
318
  if (error.stack) {
304
319
  logger.error('Stack trace:', error.stack);
305
320
  }
306
- // Return user-friendly, actionable error messages
307
- if (error.message.includes('does not exist')) {
308
- return (0, types_1.asErrorResult)(`Directory not found. Please verify the path exists: ${directory}`);
321
+ // Normalize: guard against non-Error throws (string, object, undefined)
322
+ const message = typeof error?.message === 'string' ? error.message : String(error);
323
+ // Return structured, actionable error messages
324
+ if (message.includes('does not exist')) {
325
+ return (0, types_1.asErrorResult)({
326
+ type: 'not_found_error',
327
+ message: `Directory not found: ${directory}`,
328
+ code: 'DIRECTORY_NOT_FOUND',
329
+ recoverable: false,
330
+ suggestion: 'Verify the path exists. Use an absolute path to the repository root or subdirectory.',
331
+ details: { directory },
332
+ });
309
333
  }
310
- if (error.message.includes('Permission denied')) {
311
- return (0, types_1.asErrorResult)(`Permission denied. Check that you have read access to: ${directory}`);
334
+ if (message.includes('Permission denied')) {
335
+ return (0, types_1.asErrorResult)({
336
+ type: 'resource_error',
337
+ message: `Permission denied accessing directory: ${directory}`,
338
+ code: 'PERMISSION_DENIED',
339
+ recoverable: false,
340
+ suggestion: 'Check that the MCP server process has read access to this directory.',
341
+ details: { directory },
342
+ });
312
343
  }
313
- if (error.message.includes('exceeds limit')) {
314
- return (0, types_1.asErrorResult)(error.message + '\n\nTry analyzing a subdirectory or excluding more files.');
344
+ if (message.includes('exceeds limit')) {
345
+ return (0, types_1.asErrorResult)({
346
+ type: 'resource_error',
347
+ message,
348
+ code: 'ZIP_TOO_LARGE',
349
+ recoverable: true,
350
+ suggestion: 'Analyze a subdirectory instead of the full repository (e.g. directory="/repo/src/core"). This reduces ZIP size and processing time.',
351
+ details: { directory },
352
+ });
315
353
  }
316
- if (error.message.includes('ENOSPC')) {
317
- return (0, types_1.asErrorResult)('Insufficient disk space. Free up space and try again.');
354
+ if (message.includes('ENOSPC')) {
355
+ return (0, types_1.asErrorResult)({
356
+ type: 'resource_error',
357
+ message: 'Insufficient disk space to create ZIP archive.',
358
+ code: 'DISK_FULL',
359
+ recoverable: false,
360
+ suggestion: 'Free up disk space or analyze a smaller subdirectory.',
361
+ });
318
362
  }
319
- return (0, types_1.asErrorResult)(`Failed to create ZIP archive. Check the MCP server logs for details.`);
363
+ return (0, types_1.asErrorResult)({
364
+ type: 'internal_error',
365
+ message: `Failed to create ZIP archive: ${message}`,
366
+ code: 'ZIP_CREATION_FAILED',
367
+ recoverable: false,
368
+ reportable: true,
369
+ repo: REPORT_REPO,
370
+ suggestion: REPORT_SUGGESTION,
371
+ details: { directory: (0, path_1.basename)(directory), errorType: error.name || 'Error' },
372
+ });
320
373
  }
321
374
  // Execute query with cleanup handling
322
375
  try {
@@ -431,40 +484,7 @@ async function handleQueryMode(client, params) {
431
484
  }
432
485
  catch (error) {
433
486
  // Error details are already logged by fetchFromApi and logErrorResponse
434
- // Return a user-friendly, actionable error message
435
- let errorMessage = '';
436
- if (error.response) {
437
- const status = error.response.status;
438
- switch (status) {
439
- case 401:
440
- errorMessage = 'Authentication failed. Set your SUPERMODEL_API_KEY environment variable and restart the MCP server.';
441
- break;
442
- case 403:
443
- errorMessage = 'Access forbidden. Your API key does not have permission for this operation. Contact support if this is unexpected.';
444
- break;
445
- case 404:
446
- errorMessage = 'API endpoint not found. The service URL may be incorrect. Check your SUPERMODEL_BASE_URL configuration.';
447
- break;
448
- case 429:
449
- errorMessage = 'Rate limit exceeded. Wait a few minutes and try again.';
450
- break;
451
- case 500:
452
- case 502:
453
- case 503:
454
- case 504:
455
- errorMessage = 'Server error. The Supermodel API is temporarily unavailable. Try again in a few minutes.';
456
- break;
457
- default:
458
- errorMessage = `API error (HTTP ${status}). Check the MCP server logs for details.`;
459
- }
460
- }
461
- else if (error.request) {
462
- errorMessage = 'No response from server. Check your network connection and verify the API is reachable.';
463
- }
464
- else {
465
- errorMessage = 'Request failed. Check the MCP server logs for details.';
466
- }
467
- return (0, types_1.asErrorResult)(errorMessage);
487
+ return (0, types_1.asErrorResult)(classifyApiError(error));
468
488
  }
469
489
  }
470
490
  // Handle query errors
@@ -641,10 +661,6 @@ async function fetchFromApi(client, file, idempotencyKey) {
641
661
  const apiUrl = `${baseUrl}/v1/graphs/supermodel`;
642
662
  // Log the request details
643
663
  logRequest(apiUrl, 'POST', fileSize, idempotencyKey);
644
- const requestParams = {
645
- file: fileBlob,
646
- idempotencyKey: idempotencyKey,
647
- };
648
664
  // Start progress logging
649
665
  console.error('[Supermodel] Starting codebase analysis...');
650
666
  // Set up periodic progress updates every 15 seconds
@@ -655,7 +671,8 @@ async function fetchFromApi(client, file, idempotencyKey) {
655
671
  console.error(`[Supermodel] Analysis in progress... (${elapsedSeconds}s elapsed)`);
656
672
  }, 15000);
657
673
  try {
658
- const response = await client.graphs.generateSupermodelGraph(requestParams);
674
+ // SupermodelClient handles polling automatically
675
+ const response = await client.graphs.generateSupermodelGraph(fileBlob, { idempotencyKey });
659
676
  const duration = Date.now() - startTime;
660
677
  // Clear progress interval
661
678
  if (progressInterval) {
@@ -686,19 +703,141 @@ async function fetchFromApi(client, file, idempotencyKey) {
686
703
  logger.error(`[${getTimestamp()}] [API FAILURE] Request failed after ${duration}ms`);
687
704
  // Log detailed error information
688
705
  await logErrorResponse(error);
689
- // Re-throw with enhanced error message
706
+ // Preserve error.response so classifyApiError can read the status code (#75)
690
707
  if (error.response?.status === 401) {
691
- throw new Error(`API authentication failed (401 Unauthorized). Please check your SUPERMODEL_API_KEY environment variable.`);
708
+ error.message = 'API authentication failed (401 Unauthorized). Please check your SUPERMODEL_API_KEY environment variable.';
692
709
  }
693
710
  else if (error.response?.status === 403) {
694
- throw new Error(`API access forbidden (403 Forbidden). Your API key may not have permission to access this resource.`);
711
+ error.message = 'API access forbidden (403 Forbidden). Your API key may not have permission to access this resource.';
695
712
  }
696
713
  else if (error.response?.status >= 500) {
697
- throw new Error(`Supermodel API server error (${error.response.status}). The service may be temporarily unavailable.`);
714
+ error.message = `Supermodel API server error (${error.response.status}). The service may be temporarily unavailable.`;
698
715
  }
699
716
  throw error;
700
717
  }
701
718
  }
719
+ /**
720
+ * Classify an API error into a structured error response.
721
+ * Extracts HTTP status, network conditions, and timeout signals
722
+ * to produce an agent-actionable error with recovery guidance.
723
+ */
724
+ function classifyApiError(error) {
725
+ // Guard against non-Error throws (strings, nulls, plain objects)
726
+ if (!error || typeof error !== 'object') {
727
+ return {
728
+ type: 'internal_error',
729
+ message: typeof error === 'string' ? error : 'An unexpected error occurred.',
730
+ code: 'UNKNOWN_ERROR',
731
+ recoverable: false,
732
+ reportable: true,
733
+ repo: REPORT_REPO,
734
+ suggestion: REPORT_SUGGESTION,
735
+ details: { errorType: typeof error },
736
+ };
737
+ }
738
+ if (error.response) {
739
+ const status = error.response.status;
740
+ switch (status) {
741
+ case 401:
742
+ return {
743
+ type: 'authentication_error',
744
+ message: 'Invalid or missing API key.',
745
+ code: 'INVALID_API_KEY',
746
+ recoverable: false,
747
+ suggestion: 'Set the SUPERMODEL_API_KEY environment variable and restart the MCP server.',
748
+ details: { apiKeySet: !!process.env.SUPERMODEL_API_KEY, httpStatus: 401 },
749
+ };
750
+ case 403:
751
+ return {
752
+ type: 'authorization_error',
753
+ message: 'API key does not have permission for this operation.',
754
+ code: 'FORBIDDEN',
755
+ recoverable: false,
756
+ suggestion: 'Verify your API key has the correct permissions. Contact support if unexpected.',
757
+ details: { httpStatus: 403 },
758
+ };
759
+ case 404:
760
+ return {
761
+ type: 'not_found_error',
762
+ message: 'API endpoint not found.',
763
+ code: 'ENDPOINT_NOT_FOUND',
764
+ recoverable: false,
765
+ suggestion: 'Check SUPERMODEL_BASE_URL environment variable. Default: https://api.supermodeltools.com',
766
+ details: { baseUrl: process.env.SUPERMODEL_BASE_URL || 'https://api.supermodeltools.com', httpStatus: 404 },
767
+ };
768
+ case 429:
769
+ return {
770
+ type: 'rate_limit_error',
771
+ message: 'API rate limit exceeded.',
772
+ code: 'RATE_LIMITED',
773
+ recoverable: true,
774
+ suggestion: 'Wait 30-60 seconds and retry. Consider analyzing smaller subdirectories to reduce API calls.',
775
+ details: { httpStatus: 429 },
776
+ };
777
+ case 500:
778
+ case 502:
779
+ case 503:
780
+ case 504:
781
+ return {
782
+ type: 'internal_error',
783
+ message: `Supermodel API server error (HTTP ${status}).`,
784
+ code: 'SERVER_ERROR',
785
+ recoverable: true,
786
+ reportable: true,
787
+ repo: REPORT_REPO,
788
+ suggestion: 'The API may be temporarily unavailable. Wait a few minutes and retry. If persistent, open an issue at https://github.com/supermodeltools/mcp/issues with the error details, or fork the repo and open a PR with a fix.',
789
+ details: { httpStatus: status },
790
+ };
791
+ default: {
792
+ const isServerError = status >= 500;
793
+ return {
794
+ type: isServerError ? 'internal_error' : 'validation_error',
795
+ message: `API request failed with HTTP ${status}.`,
796
+ code: 'API_ERROR',
797
+ recoverable: isServerError,
798
+ ...(isServerError && {
799
+ reportable: true,
800
+ repo: REPORT_REPO,
801
+ suggestion: 'The API may be temporarily unavailable. Wait a few minutes and retry. If persistent, open an issue at https://github.com/supermodeltools/mcp/issues with the error details, or fork the repo and open a PR with a fix.',
802
+ }),
803
+ ...(!isServerError && { suggestion: 'Check the request parameters and base URL configuration.' }),
804
+ details: { httpStatus: status },
805
+ };
806
+ }
807
+ }
808
+ }
809
+ if (error.request) {
810
+ // Distinguish timeout from general network failure
811
+ if (error.code === 'UND_ERR_HEADERS_TIMEOUT' || error.code === 'UND_ERR_BODY_TIMEOUT' || error.message?.includes('timeout')) {
812
+ return {
813
+ type: 'timeout_error',
814
+ message: 'API request timed out. The codebase may be too large for a single analysis.',
815
+ code: 'REQUEST_TIMEOUT',
816
+ recoverable: true,
817
+ suggestion: 'Analyze a smaller subdirectory (e.g. directory="/repo/src/core") or increase SUPERMODEL_TIMEOUT_MS.',
818
+ };
819
+ }
820
+ return {
821
+ type: 'network_error',
822
+ message: 'No response from Supermodel API server.',
823
+ code: 'NO_RESPONSE',
824
+ recoverable: true,
825
+ suggestion: 'Check network connectivity. Verify the API is reachable at the configured base URL.',
826
+ details: { baseUrl: process.env.SUPERMODEL_BASE_URL || 'https://api.supermodeltools.com' },
827
+ };
828
+ }
829
+ // Catch-all for unexpected errors - include the actual message
830
+ return {
831
+ type: 'internal_error',
832
+ message: error.message || 'An unexpected error occurred.',
833
+ code: 'UNKNOWN_ERROR',
834
+ recoverable: false,
835
+ reportable: true,
836
+ repo: REPORT_REPO,
837
+ suggestion: REPORT_SUGGESTION,
838
+ details: { errorType: error.name || 'Error' },
839
+ };
840
+ }
702
841
  /**
703
842
  * Legacy mode: direct jq filtering on API response
704
843
  */
@@ -710,43 +849,16 @@ async function handleLegacyMode(client, file, idempotencyKey, jq_filter) {
710
849
  catch (error) {
711
850
  if ((0, filtering_1.isJqError)(error)) {
712
851
  logger.error('jq filter error:', error.message);
713
- return (0, types_1.asErrorResult)(`Invalid jq filter syntax. Check your filter and try again.`);
852
+ return (0, types_1.asErrorResult)({
853
+ type: 'validation_error',
854
+ message: `Invalid jq filter syntax: ${error.message}`,
855
+ code: 'INVALID_JQ_FILTER',
856
+ recoverable: false,
857
+ suggestion: 'Check jq filter syntax. Use the query parameter instead for structured queries (e.g. query="summary").',
858
+ });
714
859
  }
715
860
  // Error details are already logged by fetchFromApi and logErrorResponse
716
- // Return a user-friendly, actionable error message
717
- let errorMessage = '';
718
- if (error.response) {
719
- const status = error.response.status;
720
- switch (status) {
721
- case 401:
722
- errorMessage = 'Authentication failed. Set your SUPERMODEL_API_KEY environment variable and restart the MCP server.';
723
- break;
724
- case 403:
725
- errorMessage = 'Access forbidden. Your API key does not have permission for this operation. Contact support if this is unexpected.';
726
- break;
727
- case 404:
728
- errorMessage = 'API endpoint not found. The service URL may be incorrect. Check your SUPERMODEL_BASE_URL configuration.';
729
- break;
730
- case 429:
731
- errorMessage = 'Rate limit exceeded. Wait a few minutes and try again.';
732
- break;
733
- case 500:
734
- case 502:
735
- case 503:
736
- case 504:
737
- errorMessage = 'Server error. The Supermodel API is temporarily unavailable. Try again in a few minutes.';
738
- break;
739
- default:
740
- errorMessage = `API error (HTTP ${status}). Check the MCP server logs for details.`;
741
- }
742
- }
743
- else if (error.request) {
744
- errorMessage = 'No response from server. Check your network connection and verify the API is reachable.';
745
- }
746
- else {
747
- errorMessage = 'Request failed. Check the MCP server logs for details.';
748
- }
749
- return (0, types_1.asErrorResult)(errorMessage);
861
+ return (0, types_1.asErrorResult)(classifyApiError(error));
750
862
  }
751
863
  }
752
864
  exports.default = { metadata: exports.metadata, tool: exports.tool, handler: exports.handler };
package/dist/types.js CHANGED
@@ -13,12 +13,15 @@ function asTextContentResult(result) {
13
13
  isError: false
14
14
  };
15
15
  }
16
- function asErrorResult(message) {
16
+ function asErrorResult(error) {
17
+ const text = typeof error === 'string'
18
+ ? error
19
+ : JSON.stringify({ error }, null, 2);
17
20
  return {
18
21
  content: [
19
22
  {
20
23
  type: 'text',
21
- text: message,
24
+ text,
22
25
  },
23
26
  ],
24
27
  isError: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supermodeltools/mcp-server",
3
- "version": "0.5.4",
3
+ "version": "0.6.2",
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.3.8",
41
+ "@supermodeltools/sdk": "^0.6.0",
42
42
  "archiver": "^7.0.1",
43
43
  "ignore": "^7.0.5",
44
44
  "jq-web": "^0.6.2",