@nordsym/apiclaw 2.5.8 → 2.5.10

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
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * APIvault - Agent-Native API Discovery MCP Server
3
+ * APIClaw - Agent-Native API Discovery MCP Server
4
4
  *
5
5
  * Tools:
6
6
  * - discover_apis: Search for APIs by capability
@@ -20,7 +20,7 @@ import { hasRealCredentials } from './credentials.js';
20
20
  import { getConnectedProviders } from './execute.js';
21
21
  import { executeMetered } from './metered.js';
22
22
  import { logAPICall } from './mcp-analytics.js';
23
- import { isOpenAPI, executeOpenAPI, listOpenAPIs, getOpenAPIBaseUrl } from './open-apis.js';
23
+ import { isOpenAPI, executeOpenAPI, listOpenAPIs, getOpenAPIBaseUrl, getAPIClawTotalStats } from './open-apis.js';
24
24
  import { getGateway, isGatewayEnabled } from './gateway-client.js';
25
25
  import { requiresConfirmationAsync, createPendingAction, consumePendingAction, generatePreview, validateParams } from './confirmation.js';
26
26
  import { executeCapability, listCapabilities, hasCapability } from './capability-router.js';
@@ -44,6 +44,128 @@ const anonymousRateLimits = new Map();
44
44
  const ANONYMOUS_HOURLY_LIMIT = 5;
45
45
  const ANONYMOUS_WEEKLY_LIMIT = 10;
46
46
  const FREE_MONTHLY_LIMIT = 50;
47
+ const MAX_MCP_TOOL_RESULT_BYTES = 900_000;
48
+ const SOFT_TRANSPORT_LIMITS = {
49
+ maxDepth: 6,
50
+ maxArrayItems: 40,
51
+ maxObjectKeys: 50,
52
+ maxStringChars: 12_000,
53
+ };
54
+ const HARD_TRANSPORT_LIMITS = {
55
+ maxDepth: 4,
56
+ maxArrayItems: 12,
57
+ maxObjectKeys: 20,
58
+ maxStringChars: 3_000,
59
+ };
60
+ function measureUtf8Bytes(text) {
61
+ return Buffer.byteLength(text, 'utf8');
62
+ }
63
+ function truncateToolString(value, maxChars) {
64
+ if (value.length <= maxChars)
65
+ return value;
66
+ const omitted = value.length - maxChars;
67
+ return `${value.slice(0, maxChars)}\n...[truncated ${omitted} chars]`;
68
+ }
69
+ function summarizeOverflowValue(value) {
70
+ if (Array.isArray(value)) {
71
+ return `[Array(${value.length})]`;
72
+ }
73
+ if (value && typeof value === 'object') {
74
+ return `[Object keys=${Object.keys(value).length}]`;
75
+ }
76
+ return String(value);
77
+ }
78
+ function compactToolPayload(value, limits, depth = 0, seen = new WeakSet()) {
79
+ if (typeof value === 'string') {
80
+ return truncateToolString(value, limits.maxStringChars);
81
+ }
82
+ if (value === null || typeof value !== 'object') {
83
+ return value;
84
+ }
85
+ if (depth >= limits.maxDepth) {
86
+ return summarizeOverflowValue(value);
87
+ }
88
+ if (seen.has(value)) {
89
+ return '[Circular]';
90
+ }
91
+ seen.add(value);
92
+ if (Array.isArray(value)) {
93
+ const items = value
94
+ .slice(0, limits.maxArrayItems)
95
+ .map((item) => compactToolPayload(item, limits, depth + 1, seen));
96
+ if (value.length > limits.maxArrayItems) {
97
+ items.push(`[${value.length - limits.maxArrayItems} more items truncated]`);
98
+ }
99
+ return items;
100
+ }
101
+ const entries = Object.entries(value);
102
+ const output = {};
103
+ for (const [key, nested] of entries.slice(0, limits.maxObjectKeys)) {
104
+ output[key] = compactToolPayload(nested, limits, depth + 1, seen);
105
+ }
106
+ if (entries.length > limits.maxObjectKeys) {
107
+ output._truncated_keys = entries.length - limits.maxObjectKeys;
108
+ }
109
+ return output;
110
+ }
111
+ function wrapToolTransportMeta(payload, meta) {
112
+ if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
113
+ return {
114
+ ...payload,
115
+ _transport: meta,
116
+ };
117
+ }
118
+ return {
119
+ data: payload,
120
+ _transport: meta,
121
+ };
122
+ }
123
+ function safeJsonStringify(payload, options = {}) {
124
+ const pretty = options.pretty ?? true;
125
+ const stringify = (value, prettyPrint = pretty) => JSON.stringify(value, null, prettyPrint ? 2 : 0);
126
+ const initial = stringify(payload);
127
+ const initialBytes = measureUtf8Bytes(initial);
128
+ if (initialBytes <= MAX_MCP_TOOL_RESULT_BYTES) {
129
+ return initial;
130
+ }
131
+ const suggestion = options.hint ||
132
+ 'Narrow the request, ask for a summary, paginate, or use compact=true when available.';
133
+ const softPayload = wrapToolTransportMeta(compactToolPayload(payload, SOFT_TRANSPORT_LIMITS), {
134
+ truncated: true,
135
+ reason: 'Tool result exceeded the MCP transport limit and was compacted automatically.',
136
+ original_bytes: initialBytes,
137
+ suggestion,
138
+ });
139
+ const softText = stringify(softPayload);
140
+ if (measureUtf8Bytes(softText) <= MAX_MCP_TOOL_RESULT_BYTES) {
141
+ return softText;
142
+ }
143
+ const hardPayload = wrapToolTransportMeta(compactToolPayload(payload, HARD_TRANSPORT_LIMITS), {
144
+ truncated: true,
145
+ reason: 'Tool result exceeded the MCP transport limit and was heavily compacted automatically.',
146
+ original_bytes: initialBytes,
147
+ suggestion,
148
+ });
149
+ const hardText = stringify(hardPayload);
150
+ if (measureUtf8Bytes(hardText) <= MAX_MCP_TOOL_RESULT_BYTES) {
151
+ return hardText;
152
+ }
153
+ return JSON.stringify({
154
+ status: 'partial',
155
+ preview: compactToolPayload(payload, {
156
+ maxDepth: 2,
157
+ maxArrayItems: 5,
158
+ maxObjectKeys: 10,
159
+ maxStringChars: 400,
160
+ }),
161
+ _transport: {
162
+ truncated: true,
163
+ reason: 'Tool result exceeded the MCP transport limit even after compaction.',
164
+ original_bytes: initialBytes,
165
+ suggestion,
166
+ },
167
+ }, null, 2);
168
+ }
47
169
  /**
48
170
  * Calculate minutes until next hour
49
171
  */
@@ -479,6 +601,76 @@ const SUGGESTED_CALL_RULES = [
479
601
  intent: 'website screenshot',
480
602
  },
481
603
  },
604
+ {
605
+ match: /(scrape|scraper|scraping|crawl|crawler|browser.*automat|extract.*page|extract.*site|browserless|firecrawl|scrapingbee)/i,
606
+ suggestion: {
607
+ provider: 'firecrawl',
608
+ action: 'scrape',
609
+ description: 'Scrape a single URL to clean markdown via Firecrawl (APIClaw owns the key).',
610
+ example_params: { url: 'https://example.com', formats: ['markdown'] },
611
+ intent: 'web scraping / page extraction',
612
+ },
613
+ },
614
+ {
615
+ match: /(web[\s-]?search|search.*web|search.*google|search.*engine|serp|google.*results|brave.*search|search.*the.*internet)/i,
616
+ suggestion: {
617
+ provider: 'brave_search',
618
+ action: 'search',
619
+ description: 'Live web search via Brave (APIClaw owns the key).',
620
+ example_params: { q: 'apiclaw nordsym', count: 10 },
621
+ intent: 'web search',
622
+ },
623
+ },
624
+ {
625
+ match: /(\bllm\b|chat[\s-]?completion|chat.*model|gpt[\s-]?\d|claude[\s-]?\d|opus|sonnet|haiku|generate.*text|language.*model|reasoning.*model)/i,
626
+ suggestion: {
627
+ provider: 'openrouter',
628
+ action: 'chat',
629
+ description: 'Route to any of 800+ LLMs via OpenRouter; or use APIClaw advisor by passing model="auto" to /v1/chat/completions.',
630
+ example_params: { model: 'auto', messages: [{ role: 'user', content: 'Hello' }] },
631
+ intent: 'LLM chat / completion',
632
+ },
633
+ },
634
+ {
635
+ match: /(text[\s-]?to[\s-]?speech|\btts\b|generate.*speech|voice[\s-]?clone|elevenlabs|speech.*synthesis)/i,
636
+ suggestion: {
637
+ provider: 'elevenlabs',
638
+ action: 'text_to_speech',
639
+ description: 'Generate speech audio from text via ElevenLabs (APIClaw owns the key).',
640
+ example_params: { text: 'Hello from APIClaw.', voice_id: 'Rachel' },
641
+ intent: 'text-to-speech',
642
+ },
643
+ },
644
+ {
645
+ match: /(transcribe|speech[\s-]?to[\s-]?text|\bstt\b|deepgram|whisper|audio.*to.*text)/i,
646
+ suggestion: {
647
+ provider: 'deepgram',
648
+ action: 'transcribe',
649
+ description: 'Transcribe audio to text via Deepgram (APIClaw owns the key).',
650
+ example_params: { url: 'https://example.com/audio.mp3' },
651
+ intent: 'audio transcription',
652
+ },
653
+ },
654
+ {
655
+ match: /(generate.*image|image[\s-]?generation|text[\s-]?to[\s-]?image|stable[\s-]?diffusion|sdxl|flux|midjourney|dall[\s-]?e|create.*picture)/i,
656
+ suggestion: {
657
+ provider: 'replicate',
658
+ action: 'run',
659
+ description: 'Run any open-source image/video model via Replicate (APIClaw owns the key).',
660
+ example_params: { model: 'black-forest-labs/flux-schnell', input: { prompt: 'a lobster wearing a tiny hat' } },
661
+ intent: 'image / video generation',
662
+ },
663
+ },
664
+ {
665
+ match: /(run.*code|sandbox|execute.*python|execute.*javascript|code.*interpreter|e2b)/i,
666
+ suggestion: {
667
+ provider: 'e2b',
668
+ action: 'run_code',
669
+ description: 'Run code in an isolated cloud sandbox via E2B (APIClaw owns the key).',
670
+ example_params: { language: 'python', code: 'print(2+2)' },
671
+ intent: 'code execution sandbox',
672
+ },
673
+ },
482
674
  ];
483
675
  function buildSuggestedCalls(query) {
484
676
  if (!query)
@@ -528,7 +720,7 @@ const tools = [
528
720
  },
529
721
  {
530
722
  name: 'discover_apis',
531
- description: 'Search for APIs based on what you need to do. Describe your use case naturally.',
723
+ description: 'Find APIs by job-to-be-done. Use this when the user asks "what API can do X?", wants provider recommendations, or needs web search, scraping, email, SMS, speech, PDFs, browser automation, weather, finance, or other external capabilities.',
532
724
  inputSchema: {
533
725
  type: 'object',
534
726
  properties: {
@@ -538,8 +730,12 @@ const tools = [
538
730
  },
539
731
  category: {
540
732
  type: 'string',
541
- description: 'Filter by category: communication, search, ai',
542
- enum: ['communication', 'search', 'ai']
733
+ description: 'Optional category filter. Categories are case-sensitive — call list_categories first to see exact names (e.g., "Utilities", "Analytics", "Development", "AI & ML", "Cloud", "Finance", "Communication", "Location", "Entertainment", "Security", "Health").',
734
+ },
735
+ callable_only: {
736
+ type: 'boolean',
737
+ description: 'If true, only return APIs APIClaw can execute right now (managed providers + keyless open APIs). If false (default) you also see discovery-only entries — useful when scoping integrations, but agents that just want to act should set this to true.',
738
+ default: false,
543
739
  },
544
740
  max_results: {
545
741
  type: 'number',
@@ -564,7 +760,7 @@ const tools = [
564
760
  },
565
761
  {
566
762
  name: 'get_api_details',
567
- description: 'Get detailed information about a specific API provider, including endpoints, pricing, and features. Use compact=true to save ~60% tokens.',
763
+ description: 'Inspect one provider after discovery. Good when the agent needs endpoint names, params, pricing, auth, or docs. Use compact=true to avoid oversized responses in Claude/Desktop.',
568
764
  inputSchema: {
569
765
  type: 'object',
570
766
  properties: {
@@ -636,15 +832,22 @@ const tools = [
636
832
  },
637
833
  {
638
834
  name: 'list_categories',
639
- description: 'List all available API categories.',
835
+ description: 'List API categories with total + callable counts. Lightweight by design — does NOT dump every API ID. Use discover_apis(query, category) to drill into a category.',
640
836
  inputSchema: {
641
837
  type: 'object',
642
- properties: {}
643
- }
838
+ properties: {
839
+ with_api_ids: {
840
+ type: 'boolean',
841
+ description: 'If true, include the full API id list per category (large response, will auto-compact). Default: false.',
842
+ default: false,
843
+ },
844
+ },
845
+ required: [],
846
+ },
644
847
  },
645
848
  {
646
849
  name: 'call_api',
647
- description: `Execute an API call through APIClaw. Requires registration (free). If not registered, call register_owner first.
850
+ description: `Primary execution tool. Use this to actually do the job through APIClaw: live web search, scraping, email, SMS, speech, LLM calls, invoices, screenshots, currency, weather, and other external API work. Requires free registration; if not registered, call register_owner first.
648
851
 
649
852
  SINGLE CALL: Provide provider + action + params
650
853
  CHAIN: Provide chain array to execute multiple APIs in sequence/parallel with cross-step references.
@@ -757,15 +960,26 @@ Example chain:
757
960
  },
758
961
  {
759
962
  name: 'list_connected',
760
- description: 'List all APIs available for Direct Call (no API key needed).',
963
+ description: 'Summary of providers callable right now through APIClaw with no key paste. Defaults to a compact summary (managed providers + open-API counts). Pass verbose=true only if the agent explicitly needs the full open-API list. Use discover_apis(query) for narrow lookups instead of dumping the whole catalog.',
761
964
  inputSchema: {
762
965
  type: 'object',
763
- properties: {}
764
- }
966
+ properties: {
967
+ verbose: {
968
+ type: 'boolean',
969
+ description: 'If true, also include the full keyless open-API list (large response, will auto-compact). Default: false.',
970
+ default: false,
971
+ },
972
+ category: {
973
+ type: 'string',
974
+ description: 'Optional category filter for the open-API list when verbose=true.',
975
+ },
976
+ },
977
+ required: [],
978
+ },
765
979
  },
766
980
  {
767
981
  name: 'capability',
768
- description: 'Execute an action by capability, not provider. APIClaw automatically selects the best provider, handles fallback, and optimizes for cost/speed. Example: capability("sms", "send", {to: "+46...", message: "Hello"})',
982
+ description: 'Best default when you know the job but not the provider. Execute by intent such as sms, email, search, tts, invoice, or llm, and APIClaw picks the provider plus fallback automatically.',
769
983
  inputSchema: {
770
984
  type: 'object',
771
985
  properties: {
@@ -805,7 +1019,7 @@ Example chain:
805
1019
  },
806
1020
  {
807
1021
  name: 'list_capabilities',
808
- description: 'List all available capabilities and their providers.',
1022
+ description: 'List high-level jobs APIClaw can do, such as sms, email, search, tts, invoice, or llm, and which providers back them.',
809
1023
  inputSchema: {
810
1024
  type: 'object',
811
1025
  properties: {}
@@ -958,8 +1172,8 @@ Example chain:
958
1172
  ];
959
1173
  // Create server
960
1174
  const server = new Server({
961
- name: 'apivault',
962
- version: '0.1.0',
1175
+ name: 'apiclaw',
1176
+ version: process.env.npm_package_version || '0.1.0',
963
1177
  }, {
964
1178
  capabilities: {
965
1179
  tools: {},
@@ -1011,12 +1225,25 @@ Docs: https://apiclaw.cloud
1011
1225
  case 'discover_apis': {
1012
1226
  const query = args?.query;
1013
1227
  const category = args?.category;
1014
- const maxResults = args?.max_results || 5;
1228
+ const requestedMax = args?.max_results || 5;
1229
+ const callableOnly = args?.callable_only === true;
1015
1230
  const region = args?.region;
1016
1231
  const subagentId = args?.subagent_id;
1017
1232
  const aiBackend = args?.ai_backend;
1018
1233
  const startTime = Date.now();
1019
- const results = discoverAPIs(query, { category, maxResults, region });
1234
+ // When callable_only is set, over-fetch then filter so the agent still
1235
+ // gets the requested count of useful entries.
1236
+ const fetchMax = callableOnly ? Math.max(requestedMax * 6, 30) : requestedMax;
1237
+ const rawResults = discoverAPIs(query, { category, maxResults: fetchMax, region });
1238
+ const filteredResults = callableOnly
1239
+ ? rawResults.filter((r) => r.provider.callable === true)
1240
+ : rawResults;
1241
+ // Stable callable-first ordering (preserve relevance order within each bucket).
1242
+ const results = [...filteredResults].sort((a, b) => {
1243
+ const aCall = a.provider.callable === true ? 0 : 1;
1244
+ const bCall = b.provider.callable === true ? 0 : 1;
1245
+ return aCall - bCall;
1246
+ }).slice(0, requestedMax);
1020
1247
  const responseTimeMs = Date.now() - startTime;
1021
1248
  trackSearch(query, results.length, responseTimeMs);
1022
1249
  // Suggested-call enrichment.
@@ -1113,23 +1340,33 @@ Docs: https://apiclaw.cloud
1113
1340
  content: [
1114
1341
  {
1115
1342
  type: 'text',
1116
- text: JSON.stringify({
1343
+ text: safeJsonStringify({
1117
1344
  status: 'no_results',
1118
1345
  message: `No APIs found matching "${query}". Try broader terms or check available categories with list_categories.`,
1119
1346
  available_categories: getCategories()
1120
- }, null, 2)
1347
+ })
1121
1348
  }
1122
1349
  ]
1123
1350
  };
1124
1351
  }
1352
+ const callableCount = results.filter((r) => r.provider.callable === true).length;
1353
+ const discoveryOnlyCount = results.length - callableCount;
1125
1354
  return {
1126
1355
  content: [
1127
1356
  {
1128
1357
  type: 'text',
1129
- text: JSON.stringify({
1358
+ text: safeJsonStringify({
1130
1359
  status: 'success',
1131
1360
  query,
1132
1361
  results_count: results.length,
1362
+ callable_count: callableCount,
1363
+ discovery_only_count: discoveryOnlyCount,
1364
+ ...(callableCount === 0 && !callableOnly
1365
+ ? {
1366
+ no_callable_match: true,
1367
+ no_callable_match_hint: 'No directly-callable provider matched. Try call_api({provider:"generic", action:"request", params:{url, method, ...}}) for any keyless public endpoint, or refine the query, or call list_connected to see what APIClaw can execute right now.',
1368
+ }
1369
+ : {}),
1133
1370
  ...(suggestedCalls.length > 0
1134
1371
  ? {
1135
1372
  suggested_calls: suggestedCalls,
@@ -1158,7 +1395,9 @@ Docs: https://apiclaw.cloud
1158
1395
  : { tool: null, endpoint: null, hint: 'Discovery-only. See docsUrl for integration.' },
1159
1396
  };
1160
1397
  })
1161
- }, null, 2)
1398
+ }, {
1399
+ hint: 'Lower max_results or inspect one provider at a time if you need the full discovery payload.',
1400
+ })
1162
1401
  }
1163
1402
  ]
1164
1403
  };
@@ -1172,11 +1411,11 @@ Docs: https://apiclaw.cloud
1172
1411
  content: [
1173
1412
  {
1174
1413
  type: 'text',
1175
- text: JSON.stringify({
1414
+ text: safeJsonStringify({
1176
1415
  status: 'error',
1177
1416
  message: `API not found: ${apiId}`,
1178
1417
  hint: 'Try discover_apis to search, or list_connected for direct-call APIs',
1179
- }, null, 2)
1418
+ })
1180
1419
  }
1181
1420
  ]
1182
1421
  };
@@ -1187,7 +1426,10 @@ Docs: https://apiclaw.cloud
1187
1426
  content: [
1188
1427
  {
1189
1428
  type: 'text',
1190
- text: JSON.stringify({ status: 'ok', ...api })
1429
+ text: safeJsonStringify({ status: 'ok', ...api }, {
1430
+ pretty: false,
1431
+ hint: 'Retry with compact=true or inspect a single endpoint if you need less metadata.',
1432
+ })
1191
1433
  }
1192
1434
  ]
1193
1435
  };
@@ -1196,10 +1438,12 @@ Docs: https://apiclaw.cloud
1196
1438
  content: [
1197
1439
  {
1198
1440
  type: 'text',
1199
- text: JSON.stringify({
1441
+ text: safeJsonStringify({
1200
1442
  status: 'success',
1201
1443
  api
1202
- }, null, 2)
1444
+ }, {
1445
+ hint: 'Retry get_api_details({ api_id, compact: true }) for a smaller provider spec.',
1446
+ })
1203
1447
  }
1204
1448
  ]
1205
1449
  };
@@ -1293,23 +1537,50 @@ Docs: https://apiclaw.cloud
1293
1537
  };
1294
1538
  }
1295
1539
  case 'list_categories': {
1540
+ const withApiIds = args?.with_api_ids === true;
1296
1541
  const categories = getCategories();
1297
- const apisByCategory = {};
1298
- for (const cat of categories) {
1299
- apisByCategory[cat] = getAllAPIs()
1300
- .filter(a => a.category === cat)
1301
- .map(a => a.id);
1302
- }
1542
+ const allAPIs = getAllAPIs();
1543
+ const summary = categories
1544
+ .map((cat) => {
1545
+ const inCat = allAPIs.filter((a) => a.category === cat);
1546
+ const callable = inCat.filter((a) => {
1547
+ const anyA = a;
1548
+ return anyA.callable === true;
1549
+ }).length;
1550
+ const entry = {
1551
+ category: cat,
1552
+ total: inCat.length,
1553
+ callable,
1554
+ };
1555
+ if (withApiIds) {
1556
+ entry.api_ids = inCat.map((a) => a.id);
1557
+ }
1558
+ return entry;
1559
+ })
1560
+ .sort((a, b) => b.total - a.total);
1561
+ const totalAPIs = allAPIs.length;
1562
+ const totalCallable = allAPIs.filter((a) => {
1563
+ const anyA = a;
1564
+ return anyA.callable === true;
1565
+ }).length;
1303
1566
  return {
1304
1567
  content: [
1305
1568
  {
1306
1569
  type: 'text',
1307
- text: JSON.stringify({
1570
+ text: safeJsonStringify({
1308
1571
  status: 'success',
1309
- categories: apisByCategory
1310
- }, null, 2)
1311
- }
1312
- ]
1572
+ totals: {
1573
+ categories: categories.length,
1574
+ apis_indexed: totalAPIs,
1575
+ apis_callable: totalCallable,
1576
+ },
1577
+ hint: 'Use discover_apis({ query, category }) to find APIs in a specific category. Pass with_api_ids=true here only if you really need every id.',
1578
+ categories: summary,
1579
+ }, {
1580
+ hint: 'Use discover_apis({query, category}) for a narrow slice instead of every API id.',
1581
+ }),
1582
+ },
1583
+ ],
1313
1584
  };
1314
1585
  }
1315
1586
  case 'call_api': {
@@ -1390,7 +1661,7 @@ Docs: https://apiclaw.cloud
1390
1661
  return {
1391
1662
  content: [{
1392
1663
  type: 'text',
1393
- text: JSON.stringify({
1664
+ text: safeJsonStringify({
1394
1665
  status: chainResult.success ? 'success' : 'error',
1395
1666
  mode: 'chain',
1396
1667
  chainId: chainResult.chainId,
@@ -1417,7 +1688,9 @@ Docs: https://apiclaw.cloud
1417
1688
  canResume: chainResult.canResume,
1418
1689
  resumeToken: chainResult.resumeToken,
1419
1690
  } : {}),
1420
- }, null, 2)
1691
+ }, {
1692
+ hint: 'Inspect one step at a time or reduce step outputs if the chain result is too large.',
1693
+ })
1421
1694
  }],
1422
1695
  isError: !chainResult.success
1423
1696
  };
@@ -1426,11 +1699,11 @@ Docs: https://apiclaw.cloud
1426
1699
  return {
1427
1700
  content: [{
1428
1701
  type: 'text',
1429
- text: JSON.stringify({
1702
+ text: safeJsonStringify({
1430
1703
  status: 'error',
1431
1704
  mode: 'chain',
1432
1705
  error: error instanceof Error ? error.message : String(error),
1433
- }, null, 2)
1706
+ })
1434
1707
  }],
1435
1708
  isError: true
1436
1709
  };
@@ -1446,7 +1719,9 @@ Docs: https://apiclaw.cloud
1446
1719
  return {
1447
1720
  content: [{
1448
1721
  type: 'text',
1449
- text: JSON.stringify(dryRunResult, null, 2)
1722
+ text: safeJsonStringify(dryRunResult, {
1723
+ hint: 'Use smaller params or one step at a time for a shorter preview.',
1724
+ })
1450
1725
  }]
1451
1726
  };
1452
1727
  }
@@ -1533,13 +1808,15 @@ Docs: https://apiclaw.cloud
1533
1808
  return {
1534
1809
  content: [{
1535
1810
  type: 'text',
1536
- text: JSON.stringify({
1811
+ text: safeJsonStringify({
1537
1812
  status: result.success ? 'success' : 'error',
1538
1813
  provider: result.provider,
1539
1814
  action: result.action,
1540
1815
  confirmed: true,
1541
1816
  ...(result.success ? { data: result.data } : { error: result.error }),
1542
- }, null, 2)
1817
+ }, {
1818
+ hint: 'Ask for a summary or narrower params if the confirmed result is very large.',
1819
+ })
1543
1820
  }],
1544
1821
  isError: !result.success
1545
1822
  };
@@ -1719,7 +1996,7 @@ Docs: https://apiclaw.cloud
1719
1996
  content: [
1720
1997
  {
1721
1998
  type: 'text',
1722
- text: JSON.stringify({
1999
+ text: safeJsonStringify({
1723
2000
  status: 'success',
1724
2001
  message: 'Linked APIClaw to your workspace and ran the call.',
1725
2002
  provider: retry.provider,
@@ -1727,7 +2004,9 @@ Docs: https://apiclaw.cloud
1727
2004
  type: apiType,
1728
2005
  data: retry.data,
1729
2006
  ...(retry.cost !== undefined ? { cost_sek: retry.cost } : {}),
1730
- }, null, 2),
2007
+ }, {
2008
+ hint: 'Retry with narrower params or ask for a summary if the linked-call result is too large.',
2009
+ }),
1731
2010
  },
1732
2011
  ],
1733
2012
  };
@@ -1773,34 +2052,85 @@ Docs: https://apiclaw.cloud
1773
2052
  content: [
1774
2053
  {
1775
2054
  type: 'text',
1776
- text: JSON.stringify(responseData, null, 2)
2055
+ text: safeJsonStringify(responseData, {
2056
+ hint: 'Narrow params, paginate, or ask for a summarized result if the full dataset is too large.',
2057
+ })
1777
2058
  }
1778
2059
  ],
1779
2060
  isError: !result.success
1780
2061
  };
1781
2062
  }
1782
2063
  case 'list_connected': {
2064
+ const verbose = args?.verbose === true;
2065
+ const filterCategory = typeof args?.category === 'string' ? args.category : undefined;
1783
2066
  const directProviders = getConnectedProviders();
1784
2067
  const openProviders = listOpenAPIs();
2068
+ const stats = getAPIClawTotalStats();
2069
+ // Cheap top-N "what kind of open APIs are there" rollup so the agent
2070
+ // gets a useful narrative without 9k entries.
2071
+ const allAPIs = getAllAPIs();
2072
+ const openCategoryCounts = {};
2073
+ for (const a of allAPIs) {
2074
+ const anyA = a;
2075
+ if (anyA.callable === true && anyA.category) {
2076
+ openCategoryCounts[anyA.category] = (openCategoryCounts[anyA.category] || 0) + 1;
2077
+ }
2078
+ }
2079
+ const topOpenCategories = Object.entries(openCategoryCounts)
2080
+ .sort((a, b) => b[1] - a[1])
2081
+ .slice(0, 12)
2082
+ .map(([category, count]) => ({ category, callable: count }));
2083
+ const summary = {
2084
+ status: 'success',
2085
+ message: 'APIClaw can execute these RIGHT NOW — no key paste, no integration code.',
2086
+ counts: {
2087
+ managed_providers: directProviders.length,
2088
+ open_callable_providers: openProviders.length,
2089
+ total_callable_apis: stats.total_callable,
2090
+ indexed_for_discovery: stats.tier1_discovery_indexed,
2091
+ },
2092
+ managed_providers: {
2093
+ description: 'APIClaw owns the keys. Free tier: 25 calls/month across the whole platform, then pay-as-you-go (provider cost + 15%).',
2094
+ providers: directProviders,
2095
+ },
2096
+ open_apis_summary: {
2097
+ description: 'Keyless public APIs proxied via call_api({provider, action, params}). Free.',
2098
+ total_providers: openProviders.length,
2099
+ top_categories: topOpenCategories,
2100
+ generic_passthrough: {
2101
+ hint: 'Use call_api({provider:"generic", action:"request", params:{url, method, headers, query, body}}) to hit any keyless public endpoint not curated below.',
2102
+ },
2103
+ },
2104
+ usage: 'discover_apis(query) for narrow search. call_api(provider, action, params) to execute. Set verbose=true to also see the full open-API list.',
2105
+ };
2106
+ if (verbose) {
2107
+ const filtered = filterCategory
2108
+ ? allAPIs.filter((a) => {
2109
+ const anyA = a;
2110
+ return anyA.callable === true && anyA.category === filterCategory;
2111
+ }).map((a) => ({
2112
+ provider: a.id,
2113
+ name: a.name,
2114
+ category: a.category,
2115
+ }))
2116
+ : openProviders;
2117
+ summary.open_apis_full = {
2118
+ description: filterCategory
2119
+ ? `Open APIs in category "${filterCategory}".`
2120
+ : 'Full keyless open-API list (auto-compacted if oversized; prefer discover_apis for narrow lookups).',
2121
+ count: filtered.length,
2122
+ providers: filtered,
2123
+ };
2124
+ }
1785
2125
  return {
1786
2126
  content: [
1787
2127
  {
1788
2128
  type: 'text',
1789
- text: JSON.stringify({
1790
- status: 'success',
1791
- message: 'These APIs are available via call_api - no API key needed!',
1792
- direct_call: {
1793
- description: 'APIs where we handle authentication',
1794
- providers: directProviders,
1795
- },
1796
- open_apis: {
1797
- description: 'Free, open APIs (no auth required)',
1798
- providers: openProviders,
1799
- },
1800
- usage: 'Use call_api with provider, action, and params to execute calls.'
1801
- }, null, 2)
1802
- }
1803
- ]
2129
+ text: safeJsonStringify(summary, {
2130
+ hint: 'Use discover_apis(query) or capability() for a narrower slice instead of listing everything.',
2131
+ }),
2132
+ },
2133
+ ],
1804
2134
  };
1805
2135
  }
1806
2136
  case 'capability': {
@@ -1837,12 +2167,12 @@ Docs: https://apiclaw.cloud
1837
2167
  return {
1838
2168
  content: [{
1839
2169
  type: 'text',
1840
- text: JSON.stringify({
2170
+ text: safeJsonStringify({
1841
2171
  status: 'error',
1842
2172
  error: `Unknown capability: ${capabilityId}`,
1843
2173
  available_capabilities: available.map(c => c.id),
1844
2174
  hint: 'Use list_capabilities to see all available capabilities.'
1845
- }, null, 2)
2175
+ })
1846
2176
  }],
1847
2177
  isError: true
1848
2178
  };
@@ -1852,7 +2182,7 @@ Docs: https://apiclaw.cloud
1852
2182
  return {
1853
2183
  content: [{
1854
2184
  type: 'text',
1855
- text: JSON.stringify({
2185
+ text: safeJsonStringify({
1856
2186
  status: result.success ? 'success' : 'error',
1857
2187
  capability: result.capability,
1858
2188
  action: result.action,
@@ -1862,7 +2192,9 @@ Docs: https://apiclaw.cloud
1862
2192
  ...(result.success ? { data: result.data } : { error: result.error }),
1863
2193
  ...(result.cost !== undefined ? { cost: result.cost, currency: result.currency } : {}),
1864
2194
  latency_ms: result.latencyMs,
1865
- }, null, 2)
2195
+ }, {
2196
+ hint: 'Use more specific params or one capability call at a time for a smaller payload.',
2197
+ })
1866
2198
  }],
1867
2199
  isError: !result.success
1868
2200
  };
@@ -1872,12 +2204,14 @@ Docs: https://apiclaw.cloud
1872
2204
  return {
1873
2205
  content: [{
1874
2206
  type: 'text',
1875
- text: JSON.stringify({
2207
+ text: safeJsonStringify({
1876
2208
  status: 'success',
1877
2209
  message: 'Available capabilities - use capability() to execute',
1878
2210
  capabilities,
1879
2211
  usage: 'capability("sms", "send", {to: "+46...", message: "Hello"})'
1880
- }, null, 2)
2212
+ }, {
2213
+ hint: 'If this list is too broad, ask for a specific capability like search, sms, email, tts, invoice, or llm.',
2214
+ })
1881
2215
  }]
1882
2216
  };
1883
2217
  }
@@ -2457,7 +2791,7 @@ Docs: https://apiclaw.cloud
2457
2791
  return {
2458
2792
  content: [{
2459
2793
  type: 'text',
2460
- text: JSON.stringify({
2794
+ text: safeJsonStringify({
2461
2795
  status: 'success',
2462
2796
  chain: {
2463
2797
  chainId: chainStatus.chainId,
@@ -2475,7 +2809,9 @@ Docs: https://apiclaw.cloud
2475
2809
  }
2476
2810
  } : {})
2477
2811
  }
2478
- }, null, 2)
2812
+ }, {
2813
+ hint: 'Inspect one chain step or ask for a shorter status summary if needed.',
2814
+ })
2479
2815
  }]
2480
2816
  };
2481
2817
  }
@@ -2531,7 +2867,7 @@ Docs: https://apiclaw.cloud
2531
2867
  return {
2532
2868
  content: [{
2533
2869
  type: 'text',
2534
- text: JSON.stringify({
2870
+ text: safeJsonStringify({
2535
2871
  status: result.success ? 'success' : 'error',
2536
2872
  mode: 'chain_resumed',
2537
2873
  chainId: result.chainId,
@@ -2550,7 +2886,9 @@ Docs: https://apiclaw.cloud
2550
2886
  canResume: result.canResume,
2551
2887
  resumeToken: result.resumeToken,
2552
2888
  } : {}),
2553
- }, null, 2)
2889
+ }, {
2890
+ hint: 'Inspect one resumed step at a time or reduce step outputs if the trace is too large.',
2891
+ })
2554
2892
  }],
2555
2893
  isError: !result.success
2556
2894
  };