@papicandela/mcx-core 0.2.5 → 0.2.7

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/index.js CHANGED
@@ -6488,13 +6488,62 @@ class BunWorkerSandbox {
6488
6488
  return arr.slice(0, n);
6489
6489
  };
6490
6490
 
6491
+ /**
6492
+ * Poll a function until condition is met or max iterations reached.
6493
+ * @param fn - Async function to call. Return { done: true, value } to stop.
6494
+ * @param opts - { interval: ms (default 1000), maxIterations: n (default 10) }
6495
+ * @returns Array of all results, or last result if done:true was returned
6496
+ */
6497
+ globalThis.poll = async (fn, opts = {}) => {
6498
+ const interval = opts.interval ?? 1000;
6499
+ const maxIterations = opts.maxIterations ?? 10;
6500
+ const results = [];
6501
+
6502
+ for (let i = 0; i < maxIterations; i++) {
6503
+ const result = await fn(i);
6504
+ results.push(result);
6505
+
6506
+ // Check for done signal
6507
+ if (result && typeof result === 'object' && result.done) {
6508
+ return result.value !== undefined ? result.value : results;
6509
+ }
6510
+
6511
+ // Wait before next iteration (skip on last)
6512
+ if (i < maxIterations - 1) {
6513
+ await new Promise(r => setTimeout(r, interval));
6514
+ }
6515
+ }
6516
+
6517
+ return results;
6518
+ };
6519
+
6520
+ /**
6521
+ * Wait for a condition to become true.
6522
+ * @param fn - Async function that returns truthy when done
6523
+ * @param opts - { interval: ms (default 500), timeout: ms (default 10000) }
6524
+ * @returns The truthy value returned by fn
6525
+ */
6526
+ globalThis.waitFor = async (fn, opts = {}) => {
6527
+ const interval = opts.interval ?? 500;
6528
+ const timeout = opts.timeout ?? 10000;
6529
+ const start = Date.now();
6530
+
6531
+ while (Date.now() - start < timeout) {
6532
+ const result = await fn();
6533
+ if (result) return result;
6534
+ await new Promise(r => setTimeout(r, interval));
6535
+ }
6536
+
6537
+ throw new Error('waitFor timeout after ' + timeout + 'ms');
6538
+ };
6539
+
6491
6540
  // SECURITY: Reserved keys that must not be overwritten by user-provided variables/globals
6492
6541
  const RESERVED_KEYS = new Set([
6493
6542
  'onmessage', 'postMessage', 'close', 'terminate', 'self',
6494
6543
  'constructor', 'prototype', '__proto__',
6495
6544
  'pendingCalls', 'callId', 'logs', 'console', 'adapters',
6496
6545
  'fetch', 'XMLHttpRequest', 'WebSocket', 'EventSource',
6497
- 'pick', 'table', 'count', 'sum', 'first', 'safeStr'
6546
+ 'pick', 'table', 'count', 'sum', 'first', 'safeStr', 'poll', 'waitFor'
6498
6547
  ]);
6499
6548
 
6500
6549
  self.onmessage = async (event) => {
@@ -6521,12 +6570,62 @@ class BunWorkerSandbox {
6521
6570
  globalThis[key] = value;
6522
6571
  }
6523
6572
 
6524
- // Create adapter proxies and freeze them to prevent user code modification
6573
+ // Levenshtein distance for fuzzy matching
6574
+ const levenshtein = (a, b) => {
6575
+ if (a.length === 0) return b.length;
6576
+ if (b.length === 0) return a.length;
6577
+ const matrix = [];
6578
+ for (let i = 0; i <= b.length; i++) matrix[i] = [i];
6579
+ for (let j = 0; j <= a.length; j++) matrix[0][j] = j;
6580
+ for (let i = 1; i <= b.length; i++) {
6581
+ for (let j = 1; j <= a.length; j++) {
6582
+ matrix[i][j] = b[i-1] === a[j-1]
6583
+ ? matrix[i-1][j-1]
6584
+ : Math.min(matrix[i-1][j-1] + 1, matrix[i][j-1] + 1, matrix[i-1][j] + 1);
6585
+ }
6586
+ }
6587
+ return matrix[b.length][a.length];
6588
+ };
6589
+
6590
+ // Convert camelCase to snake_case
6591
+ const toSnakeCase = (str) => str
6592
+ .replace(/([a-z])([A-Z])/g, '$1_$2')
6593
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
6594
+ .toLowerCase();
6595
+
6596
+ // Find similar method names
6597
+ const findSimilar = (name, methods, maxDist = 3) => {
6598
+ const normalized = name.toLowerCase().replace(/[-_]/g, '');
6599
+ return methods
6600
+ .map(m => ({ method: m, dist: levenshtein(normalized, m.toLowerCase().replace(/[-_]/g, '')) }))
6601
+ .filter(x => x.dist <= maxDist)
6602
+ .sort((a, b) => a.dist - b.dist)
6603
+ .slice(0, 3)
6604
+ .map(x => x.method);
6605
+ };
6606
+
6607
+ // Find best auto-correctable match (distance ≤ 2 = safe to auto-correct)
6608
+ const findAutoCorrect = (name, methods) => {
6609
+ // Try exact snake_case conversion first
6610
+ const snake = toSnakeCase(name);
6611
+ if (methods.includes(snake)) return snake;
6612
+
6613
+ // Try normalized fuzzy match
6614
+ const normalized = name.toLowerCase().replace(/[-_]/g, '');
6615
+ const matches = methods
6616
+ .map(m => ({ method: m, dist: levenshtein(normalized, m.toLowerCase().replace(/[-_]/g, '')) }))
6617
+ .filter(x => x.dist <= 2) // Only auto-correct if very close
6618
+ .sort((a, b) => a.dist - b.dist);
6619
+
6620
+ return matches.length > 0 ? matches[0].method : null;
6621
+ };
6622
+
6623
+ // Create adapter proxies with helpful error messages
6525
6624
  const adaptersObj = {};
6526
6625
  for (const [adapterName, methods] of Object.entries(adapterMethods)) {
6527
- const adapterObj = {};
6626
+ const methodsImpl = {};
6528
6627
  for (const methodName of methods) {
6529
- adapterObj[methodName] = async (...args) => {
6628
+ methodsImpl[methodName] = async (...args) => {
6530
6629
  const id = ++callId;
6531
6630
  return new Promise((resolve, reject) => {
6532
6631
  pendingCalls.set(id, { resolve, reject });
@@ -6540,18 +6639,57 @@ class BunWorkerSandbox {
6540
6639
  });
6541
6640
  };
6542
6641
  }
6543
- // Freeze individual adapter to prevent method tampering
6544
- Object.freeze(adapterObj);
6545
- adaptersObj[adapterName] = adapterObj;
6642
+
6643
+ // Precompute auto-correct lookup (O(1) instead of O(n) per access)
6644
+ const autoCorrectMap = new Map();
6645
+ for (const m of methods) {
6646
+ // Map snake_case variants: execute_sql -> execute_sql (identity)
6647
+ autoCorrectMap.set(m, m);
6648
+ // Map normalized: executesql -> execute_sql
6649
+ autoCorrectMap.set(m.toLowerCase().replace(/[-_]/g, ''), m);
6650
+ }
6651
+
6652
+ // Use Proxy to intercept undefined method calls with auto-correction
6653
+ const adapterProxy = new Proxy(methodsImpl, {
6654
+ get(target, prop) {
6655
+ if (prop in target) return target[prop];
6656
+ if (typeof prop === 'symbol') return undefined;
6657
+
6658
+ const propStr = String(prop);
6659
+
6660
+ // Fast path: check precomputed map (O(1))
6661
+ const snake = toSnakeCase(propStr);
6662
+ if (autoCorrectMap.has(snake)) {
6663
+ return target[autoCorrectMap.get(snake)];
6664
+ }
6665
+ const normalized = propStr.toLowerCase().replace(/[-_]/g, '');
6666
+ if (autoCorrectMap.has(normalized)) {
6667
+ return target[autoCorrectMap.get(normalized)];
6668
+ }
6669
+
6670
+ // Slow path: fuzzy match for typos (only if fast path failed)
6671
+ const corrected = findAutoCorrect(propStr, methods);
6672
+ if (corrected && corrected in target) {
6673
+ return target[corrected];
6674
+ }
6675
+
6676
+ // No auto-correct possible, throw with suggestions
6677
+ const similar = findSimilar(propStr, methods);
6678
+ const suggestion = similar.length > 0
6679
+ ? '. Did you mean: ' + similar.join(', ') + '?'
6680
+ : '. Available: ' + methods.slice(0, 5).join(', ') + (methods.length > 5 ? '...' : '');
6681
+ throw new Error(adapterName + '.' + propStr + ' is not a function' + suggestion);
6682
+ }
6683
+ });
6684
+
6685
+ adaptersObj[adapterName] = adapterProxy;
6546
6686
  // Also expose at top level but as non-writable
6547
6687
  Object.defineProperty(globalThis, adapterName, {
6548
- value: adapterObj,
6688
+ value: adapterProxy,
6549
6689
  writable: false,
6550
6690
  configurable: false
6551
6691
  });
6552
6692
  }
6553
- // Freeze the adapters namespace and make it non-writable
6554
- Object.freeze(adaptersObj);
6555
6693
  Object.defineProperty(globalThis, 'adapters', {
6556
6694
  value: adaptersObj,
6557
6695
  writable: false,
@@ -6577,7 +6715,23 @@ class BunWorkerSandbox {
6577
6715
  const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
6578
6716
  const fn = new AsyncFunction(data.code);
6579
6717
  const result = await fn();
6580
- self.postMessage({ type: 'result', success: true, value: result, logs });
6718
+
6719
+ // SECURITY: Hard cap on output size to prevent OOM
6720
+ const HARD_CAP_BYTES = 100 * 1024 * 1024; // 100MB
6721
+ const serialized = safeStr(result);
6722
+ if (serialized.length > HARD_CAP_BYTES) {
6723
+ self.postMessage({
6724
+ type: 'result',
6725
+ success: false,
6726
+ error: {
6727
+ name: 'OutputSizeError',
6728
+ message: 'Output exceeds 100MB limit. Use pagination or filtering.'
6729
+ },
6730
+ logs
6731
+ });
6732
+ } else {
6733
+ self.postMessage({ type: 'result', success: true, value: result, logs });
6734
+ }
6581
6735
  } catch (err) {
6582
6736
  // Truncate stack to 5 lines to prevent context bloat
6583
6737
  const stack = err.stack ? err.stack.split('\\n').slice(0, 5).join('\\n') : undefined;
@@ -10867,12 +11021,54 @@ function generateTypes(adapters, options = {}) {
10867
11021
  `).trim();
10868
11022
  }
10869
11023
  function generateTypesSummary(adapters) {
10870
- return adapters.map((adapter) => {
10871
- const count = Object.keys(adapter.tools).length;
10872
- return `- ${adapter.name} (${count} methods)`;
10873
- }).join(`
11024
+ const byDomain = new Map;
11025
+ for (const adapter of adapters) {
11026
+ const domain = inferDomain(adapter);
11027
+ if (!byDomain.has(domain)) {
11028
+ byDomain.set(domain, []);
11029
+ }
11030
+ byDomain.get(domain).push(adapter);
11031
+ }
11032
+ if (byDomain.size <= 1 || adapters.length < 4) {
11033
+ return adapters.map((adapter) => {
11034
+ const count = Object.keys(adapter.tools).length;
11035
+ return `- ${adapter.name} (${count} methods)`;
11036
+ }).join(`
11037
+ `);
11038
+ }
11039
+ const lines = [];
11040
+ for (const [domain, domainAdapters] of byDomain) {
11041
+ const adapterList = domainAdapters.map((a) => `${a.name}(${Object.keys(a.tools).length})`).join(", ");
11042
+ lines.push(`[${domain}] ${adapterList}`);
11043
+ }
11044
+ return lines.join(`
10874
11045
  `);
10875
11046
  }
11047
+ function inferDomain(adapter) {
11048
+ if (adapter.domain)
11049
+ return adapter.domain;
11050
+ const name = adapter.name.toLowerCase();
11051
+ const desc = (adapter.description || "").toLowerCase();
11052
+ const combined = `${name} ${desc}`;
11053
+ const domains = {
11054
+ payments: ["stripe", "paypal", "square", "payment", "checkout", "billing", "invoice"],
11055
+ database: ["supabase", "postgres", "mysql", "mongodb", "redis", "database", "sql", "query"],
11056
+ email: ["sendgrid", "mailgun", "postmark", "email", "smtp", "mail"],
11057
+ storage: ["s3", "cloudflare", "storage", "blob", "file", "upload"],
11058
+ auth: ["auth", "oauth", "login", "jwt", "clerk", "auth0"],
11059
+ ai: ["openai", "anthropic", "claude", "gpt", "llm", "ai", "ml"],
11060
+ messaging: ["slack", "discord", "telegram", "twilio", "sms", "chat"],
11061
+ crm: ["hubspot", "salesforce", "crm", "customer"],
11062
+ analytics: ["analytics", "metrics", "tracking", "mixpanel", "amplitude"],
11063
+ devtools: ["github", "gitlab", "jira", "linear", "chrome", "devtools", "ci", "cd"]
11064
+ };
11065
+ for (const [domain, keywords2] of Object.entries(domains)) {
11066
+ if (keywords2.some((k) => combined.includes(k))) {
11067
+ return domain;
11068
+ }
11069
+ }
11070
+ return "general";
11071
+ }
10876
11072
  function generateInputInterface(typeName, parameters, includeDescriptions) {
10877
11073
  const lines = [`interface ${typeName} {`];
10878
11074
  for (const [paramName, param] of Object.entries(parameters)) {
@@ -10881,7 +11077,7 @@ function generateInputInterface(typeName, parameters, includeDescriptions) {
10881
11077
  lines.push(` /** ${sanitizeJSDoc(param.description)} */`);
10882
11078
  }
10883
11079
  const tsType = paramTypeToTS(param.type);
10884
- const optional = param.required === false ? "?" : "";
11080
+ const optional = param.required === true ? "" : "?";
10885
11081
  lines.push(` ${safeParamName}${optional}: ${tsType};`);
10886
11082
  }
10887
11083
  lines.push("}");
@@ -11170,6 +11366,7 @@ export {
11170
11366
  normalizeCode,
11171
11367
  mergeConfigs,
11172
11368
  isUrlAllowed,
11369
+ inferDomain,
11173
11370
  generateTypesSummary,
11174
11371
  generateTypes,
11175
11372
  formatFindings,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papicandela/mcx-core",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "MCX Core - MCP Code Execution Framework",
5
5
  "author": "papicandela",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -78,6 +78,7 @@ export { configBuilder, defineConfig, mergeConfigs } from "./config.js";
78
78
  export {
79
79
  generateTypes,
80
80
  generateTypesSummary,
81
+ inferDomain,
81
82
  sanitizeIdentifier,
82
83
  type TypeGeneratorOptions,
83
84
  } from "./type-generator.js";
@@ -285,13 +285,62 @@ export class BunWorkerSandbox implements ISandbox {
285
285
  return arr.slice(0, n);
286
286
  };
287
287
 
288
+ /**
289
+ * Poll a function until condition is met or max iterations reached.
290
+ * @param fn - Async function to call. Return { done: true, value } to stop.
291
+ * @param opts - { interval: ms (default 1000), maxIterations: n (default 10) }
292
+ * @returns Array of all results, or last result if done:true was returned
293
+ */
294
+ globalThis.poll = async (fn, opts = {}) => {
295
+ const interval = opts.interval ?? 1000;
296
+ const maxIterations = opts.maxIterations ?? 10;
297
+ const results = [];
298
+
299
+ for (let i = 0; i < maxIterations; i++) {
300
+ const result = await fn(i);
301
+ results.push(result);
302
+
303
+ // Check for done signal
304
+ if (result && typeof result === 'object' && result.done) {
305
+ return result.value !== undefined ? result.value : results;
306
+ }
307
+
308
+ // Wait before next iteration (skip on last)
309
+ if (i < maxIterations - 1) {
310
+ await new Promise(r => setTimeout(r, interval));
311
+ }
312
+ }
313
+
314
+ return results;
315
+ };
316
+
317
+ /**
318
+ * Wait for a condition to become true.
319
+ * @param fn - Async function that returns truthy when done
320
+ * @param opts - { interval: ms (default 500), timeout: ms (default 10000) }
321
+ * @returns The truthy value returned by fn
322
+ */
323
+ globalThis.waitFor = async (fn, opts = {}) => {
324
+ const interval = opts.interval ?? 500;
325
+ const timeout = opts.timeout ?? 10000;
326
+ const start = Date.now();
327
+
328
+ while (Date.now() - start < timeout) {
329
+ const result = await fn();
330
+ if (result) return result;
331
+ await new Promise(r => setTimeout(r, interval));
332
+ }
333
+
334
+ throw new Error('waitFor timeout after ' + timeout + 'ms');
335
+ };
336
+
288
337
  // SECURITY: Reserved keys that must not be overwritten by user-provided variables/globals
289
338
  const RESERVED_KEYS = new Set([
290
339
  'onmessage', 'postMessage', 'close', 'terminate', 'self',
291
340
  'constructor', 'prototype', '__proto__',
292
341
  'pendingCalls', 'callId', 'logs', 'console', 'adapters',
293
342
  'fetch', 'XMLHttpRequest', 'WebSocket', 'EventSource',
294
- 'pick', 'table', 'count', 'sum', 'first', 'safeStr'
343
+ 'pick', 'table', 'count', 'sum', 'first', 'safeStr', 'poll', 'waitFor'
295
344
  ]);
296
345
 
297
346
  self.onmessage = async (event) => {
@@ -318,12 +367,62 @@ export class BunWorkerSandbox implements ISandbox {
318
367
  globalThis[key] = value;
319
368
  }
320
369
 
321
- // Create adapter proxies and freeze them to prevent user code modification
370
+ // Levenshtein distance for fuzzy matching
371
+ const levenshtein = (a, b) => {
372
+ if (a.length === 0) return b.length;
373
+ if (b.length === 0) return a.length;
374
+ const matrix = [];
375
+ for (let i = 0; i <= b.length; i++) matrix[i] = [i];
376
+ for (let j = 0; j <= a.length; j++) matrix[0][j] = j;
377
+ for (let i = 1; i <= b.length; i++) {
378
+ for (let j = 1; j <= a.length; j++) {
379
+ matrix[i][j] = b[i-1] === a[j-1]
380
+ ? matrix[i-1][j-1]
381
+ : Math.min(matrix[i-1][j-1] + 1, matrix[i][j-1] + 1, matrix[i-1][j] + 1);
382
+ }
383
+ }
384
+ return matrix[b.length][a.length];
385
+ };
386
+
387
+ // Convert camelCase to snake_case
388
+ const toSnakeCase = (str) => str
389
+ .replace(/([a-z])([A-Z])/g, '$1_$2')
390
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
391
+ .toLowerCase();
392
+
393
+ // Find similar method names
394
+ const findSimilar = (name, methods, maxDist = 3) => {
395
+ const normalized = name.toLowerCase().replace(/[-_]/g, '');
396
+ return methods
397
+ .map(m => ({ method: m, dist: levenshtein(normalized, m.toLowerCase().replace(/[-_]/g, '')) }))
398
+ .filter(x => x.dist <= maxDist)
399
+ .sort((a, b) => a.dist - b.dist)
400
+ .slice(0, 3)
401
+ .map(x => x.method);
402
+ };
403
+
404
+ // Find best auto-correctable match (distance ≤ 2 = safe to auto-correct)
405
+ const findAutoCorrect = (name, methods) => {
406
+ // Try exact snake_case conversion first
407
+ const snake = toSnakeCase(name);
408
+ if (methods.includes(snake)) return snake;
409
+
410
+ // Try normalized fuzzy match
411
+ const normalized = name.toLowerCase().replace(/[-_]/g, '');
412
+ const matches = methods
413
+ .map(m => ({ method: m, dist: levenshtein(normalized, m.toLowerCase().replace(/[-_]/g, '')) }))
414
+ .filter(x => x.dist <= 2) // Only auto-correct if very close
415
+ .sort((a, b) => a.dist - b.dist);
416
+
417
+ return matches.length > 0 ? matches[0].method : null;
418
+ };
419
+
420
+ // Create adapter proxies with helpful error messages
322
421
  const adaptersObj = {};
323
422
  for (const [adapterName, methods] of Object.entries(adapterMethods)) {
324
- const adapterObj = {};
423
+ const methodsImpl = {};
325
424
  for (const methodName of methods) {
326
- adapterObj[methodName] = async (...args) => {
425
+ methodsImpl[methodName] = async (...args) => {
327
426
  const id = ++callId;
328
427
  return new Promise((resolve, reject) => {
329
428
  pendingCalls.set(id, { resolve, reject });
@@ -337,18 +436,57 @@ export class BunWorkerSandbox implements ISandbox {
337
436
  });
338
437
  };
339
438
  }
340
- // Freeze individual adapter to prevent method tampering
341
- Object.freeze(adapterObj);
342
- adaptersObj[adapterName] = adapterObj;
439
+
440
+ // Precompute auto-correct lookup (O(1) instead of O(n) per access)
441
+ const autoCorrectMap = new Map();
442
+ for (const m of methods) {
443
+ // Map snake_case variants: execute_sql -> execute_sql (identity)
444
+ autoCorrectMap.set(m, m);
445
+ // Map normalized: executesql -> execute_sql
446
+ autoCorrectMap.set(m.toLowerCase().replace(/[-_]/g, ''), m);
447
+ }
448
+
449
+ // Use Proxy to intercept undefined method calls with auto-correction
450
+ const adapterProxy = new Proxy(methodsImpl, {
451
+ get(target, prop) {
452
+ if (prop in target) return target[prop];
453
+ if (typeof prop === 'symbol') return undefined;
454
+
455
+ const propStr = String(prop);
456
+
457
+ // Fast path: check precomputed map (O(1))
458
+ const snake = toSnakeCase(propStr);
459
+ if (autoCorrectMap.has(snake)) {
460
+ return target[autoCorrectMap.get(snake)];
461
+ }
462
+ const normalized = propStr.toLowerCase().replace(/[-_]/g, '');
463
+ if (autoCorrectMap.has(normalized)) {
464
+ return target[autoCorrectMap.get(normalized)];
465
+ }
466
+
467
+ // Slow path: fuzzy match for typos (only if fast path failed)
468
+ const corrected = findAutoCorrect(propStr, methods);
469
+ if (corrected && corrected in target) {
470
+ return target[corrected];
471
+ }
472
+
473
+ // No auto-correct possible, throw with suggestions
474
+ const similar = findSimilar(propStr, methods);
475
+ const suggestion = similar.length > 0
476
+ ? '. Did you mean: ' + similar.join(', ') + '?'
477
+ : '. Available: ' + methods.slice(0, 5).join(', ') + (methods.length > 5 ? '...' : '');
478
+ throw new Error(adapterName + '.' + propStr + ' is not a function' + suggestion);
479
+ }
480
+ });
481
+
482
+ adaptersObj[adapterName] = adapterProxy;
343
483
  // Also expose at top level but as non-writable
344
484
  Object.defineProperty(globalThis, adapterName, {
345
- value: adapterObj,
485
+ value: adapterProxy,
346
486
  writable: false,
347
487
  configurable: false
348
488
  });
349
489
  }
350
- // Freeze the adapters namespace and make it non-writable
351
- Object.freeze(adaptersObj);
352
490
  Object.defineProperty(globalThis, 'adapters', {
353
491
  value: adaptersObj,
354
492
  writable: false,
@@ -374,7 +512,23 @@ export class BunWorkerSandbox implements ISandbox {
374
512
  const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
375
513
  const fn = new AsyncFunction(data.code);
376
514
  const result = await fn();
377
- self.postMessage({ type: 'result', success: true, value: result, logs });
515
+
516
+ // SECURITY: Hard cap on output size to prevent OOM
517
+ const HARD_CAP_BYTES = 100 * 1024 * 1024; // 100MB
518
+ const serialized = safeStr(result);
519
+ if (serialized.length > HARD_CAP_BYTES) {
520
+ self.postMessage({
521
+ type: 'result',
522
+ success: false,
523
+ error: {
524
+ name: 'OutputSizeError',
525
+ message: 'Output exceeds 100MB limit. Use pagination or filtering.'
526
+ },
527
+ logs
528
+ });
529
+ } else {
530
+ self.postMessage({ type: 'result', success: true, value: result, logs });
531
+ }
378
532
  } catch (err) {
379
533
  // Truncate stack to 5 lines to prevent context bloat
380
534
  const stack = err.stack ? err.stack.split('\\n').slice(0, 5).join('\\n') : undefined;
@@ -85,19 +85,76 @@ export function generateTypes(
85
85
 
86
86
  /**
87
87
  * Generate a compact type summary for token-constrained contexts.
88
- * Only shows adapter names and method count to minimize context usage.
88
+ * Groups adapters by domain and shows method count.
89
89
  * Use mcx_search to discover specific methods.
90
90
  *
91
91
  * @param adapters - Array of adapters
92
- * @returns Compact summary string
92
+ * @returns Compact summary string with domain hints
93
93
  */
94
94
  export function generateTypesSummary(adapters: Adapter[]): string {
95
- return adapters
96
- .map((adapter) => {
97
- const count = Object.keys(adapter.tools).length;
98
- return `- ${adapter.name} (${count} methods)`;
99
- })
100
- .join("\n");
95
+ // Group adapters by domain
96
+ const byDomain = new Map<string, Adapter[]>();
97
+
98
+ for (const adapter of adapters) {
99
+ const domain = inferDomain(adapter);
100
+ if (!byDomain.has(domain)) {
101
+ byDomain.set(domain, []);
102
+ }
103
+ byDomain.get(domain)!.push(adapter);
104
+ }
105
+
106
+ // If only one domain or fewer than 4 adapters, simple list
107
+ if (byDomain.size <= 1 || adapters.length < 4) {
108
+ return adapters
109
+ .map((adapter) => {
110
+ const count = Object.keys(adapter.tools).length;
111
+ return `- ${adapter.name} (${count} methods)`;
112
+ })
113
+ .join("\n");
114
+ }
115
+
116
+ // Group by domain for better discoverability
117
+ const lines: string[] = [];
118
+ for (const [domain, domainAdapters] of byDomain) {
119
+ const adapterList = domainAdapters
120
+ .map((a) => `${a.name}(${Object.keys(a.tools).length})`)
121
+ .join(", ");
122
+ lines.push(`[${domain}] ${adapterList}`);
123
+ }
124
+ return lines.join("\n");
125
+ }
126
+
127
+ /**
128
+ * Infer domain from adapter name/description if not explicitly set.
129
+ * Returns the adapter's explicit domain if set, otherwise infers from name/description.
130
+ */
131
+ export function inferDomain(adapter: Adapter): string {
132
+ if (adapter.domain) return adapter.domain;
133
+
134
+ const name = adapter.name.toLowerCase();
135
+ const desc = (adapter.description || "").toLowerCase();
136
+ const combined = `${name} ${desc}`;
137
+
138
+ const domains: Record<string, string[]> = {
139
+ payments: ["stripe", "paypal", "square", "payment", "checkout", "billing", "invoice"],
140
+ database: ["supabase", "postgres", "mysql", "mongodb", "redis", "database", "sql", "query"],
141
+ email: ["sendgrid", "mailgun", "postmark", "email", "smtp", "mail"],
142
+ storage: ["s3", "cloudflare", "storage", "blob", "file", "upload"],
143
+ auth: ["auth", "oauth", "login", "jwt", "clerk", "auth0"],
144
+ ai: ["openai", "anthropic", "claude", "gpt", "llm", "ai", "ml"],
145
+ messaging: ["slack", "discord", "telegram", "twilio", "sms", "chat"],
146
+ crm: ["hubspot", "salesforce", "crm", "customer"],
147
+ analytics: ["analytics", "metrics", "tracking", "mixpanel", "amplitude"],
148
+ devtools: ["github", "gitlab", "jira", "linear", "chrome", "devtools", "ci", "cd"],
149
+ };
150
+
151
+ for (const [domain, keywords] of Object.entries(domains)) {
152
+ if (keywords.some((k) => combined.includes(k))) {
153
+ return domain;
154
+ }
155
+ }
156
+
157
+ return "general";
101
158
  }
102
159
 
103
160
  /**
@@ -119,7 +176,7 @@ function generateInputInterface(
119
176
  }
120
177
 
121
178
  const tsType = paramTypeToTS(param.type);
122
- const optional = param.required === false ? "?" : "";
179
+ const optional = param.required === true ? "" : "?";
123
180
  lines.push(` ${safeParamName}${optional}: ${tsType};`);
124
181
  }
125
182
 
package/src/types.ts CHANGED
@@ -12,6 +12,8 @@ export interface ParameterDefinition {
12
12
  description?: string;
13
13
  required?: boolean;
14
14
  default?: unknown;
15
+ /** Example value for this parameter (helps LLMs understand expected format) */
16
+ example?: unknown;
15
17
  }
16
18
 
17
19
  /**
@@ -33,6 +35,8 @@ export interface Adapter<TTools extends Record<string, AdapterTool> = Record<str
33
35
  name: string;
34
36
  description?: string;
35
37
  version?: string;
38
+ /** Domain/category for tool discovery (e.g., 'payments', 'database', 'email') */
39
+ domain?: string;
36
40
  tools: TTools;
37
41
  dispose?: () => Promise<void> | void;
38
42
  }