@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/README.md +0 -12
- package/dist/cache/graph-cache.js +32 -3
- package/dist/constants.js +20 -0
- package/dist/index.js +35 -1
- package/dist/queries/discovery.js +20 -5
- package/dist/queries/index.js +30 -19
- package/dist/queries/traversal.js +75 -22
- package/dist/server.js +62 -6
- package/dist/tools/create-supermodel-graph.js +263 -119
- package/dist/utils/logger.js +41 -0
- package/dist/utils/zip-repository.js +327 -41
- package/package.json +10 -2
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
//
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
statusHash = (0,
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
|
202
|
-
//
|
|
203
|
-
|
|
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
|
|
234
|
+
return `${repoName}-${pathHash}:supermodel:${hash}${statusHash}`;
|
|
209
235
|
}
|
|
210
236
|
const handler = async (client, args) => {
|
|
211
237
|
if (!args) {
|
|
212
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
282
|
+
logger.debug('Auto-zip complete:', zipResult.fileCount, 'files,', formatBytes(zipResult.sizeBytes));
|
|
283
283
|
}
|
|
284
284
|
catch (error) {
|
|
285
|
-
|
|
286
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
523
|
-
|
|
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
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
545
|
-
|
|
546
|
-
|
|
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
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
-
|
|
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)(
|
|
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
|
+
}
|