anyapi-mcp-server 1.8.1 → 2.0.0

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
@@ -117,7 +117,8 @@ mutation { post_endpoint(input: { name: "example" }) { id } }
117
117
  ```
118
118
 
119
119
  Key parameters:
120
- - **`maxTokens`** — token budget for the response (default 4000). Arrays are truncated to fit.
120
+ - **`maxTokens`** — token budget for the response. Arrays are truncated to fit. Without this or `unlimited`, responses over ~10k tokens are rejected.
121
+ - **`unlimited`** — set to `true` to return the full response with no token budget enforcement.
121
122
  - **`dataKey`** — reuse cached data from a previous `call_api` or `query_api` response.
122
123
  - **`jsonFilter`** — dot-path to extract nested values after the GraphQL query (e.g. `"data[].attributes.name"`).
123
124
  - **`bodyFile`** — absolute path to a JSON file to use as request body (mutually exclusive with `body`). Use for large payloads that can't be sent inline.
@@ -182,7 +183,7 @@ OpenAPI/Postman spec
182
183
  - **Mutation support** — write operations get typed GraphQL mutations from OpenAPI body schemas
183
184
  - **Smart suggestions** — `call_api` returns ready-to-use queries based on the inferred schema
184
185
  - **Response caching** — filesystem-based cache with 5-min TTL; `dataKey` tokens let `query_api` reuse data with zero HTTP calls
185
- - **Token budget** — `query_api` accepts `maxTokens` (default 4000) and truncates array results to fit via binary search
186
+ - **Token budget** — `query_api` enforces a ~10k token safety limit by default; use `maxTokens` to truncate the deepest largest array to fit, or `unlimited: true` for full responses
186
187
  - **Per-field token costs** — `call_api` returns a `fieldTokenCosts` tree so the LLM can make informed field selections
187
188
  - **Rate limit tracking** — parses `X-RateLimit-*` headers and warns when limits are nearly exhausted
188
189
  - **Pagination detection** — auto-detects cursor, next-page-token, and link-based pagination patterns in responses
@@ -54,12 +54,14 @@ export function cleanupExpired() {
54
54
  }
55
55
  export function storeResponse(method, path, data, responseHeaders) {
56
56
  const dataKey = randomBytes(4).toString("hex");
57
+ const now = Date.now();
57
58
  const entry = {
58
59
  method,
59
60
  path,
60
61
  data,
61
62
  responseHeaders,
62
- expiresAt: Date.now() + TTL_MS,
63
+ storedAt: now,
64
+ expiresAt: now + TTL_MS,
63
65
  };
64
66
  try {
65
67
  ensureDir();
@@ -68,6 +70,7 @@ export function storeResponse(method, path, data, responseHeaders) {
68
70
  }
69
71
  catch (err) {
70
72
  process.stderr.write(`data-cache: failed to store ${dataKey}: ${err}\n`);
73
+ return undefined;
71
74
  }
72
75
  return dataKey;
73
76
  }
@@ -88,6 +91,7 @@ export function loadResponse(dataKey) {
88
91
  path: entry.path,
89
92
  data: entry.data,
90
93
  responseHeaders: entry.responseHeaders,
94
+ storedAt: entry.storedAt ?? (entry.expiresAt - TTL_MS),
91
95
  };
92
96
  }
93
97
  catch {
@@ -134,7 +134,7 @@ function getSuggestion(status, endpoint) {
134
134
  if (status >= 500) {
135
135
  return "Server error. This is likely a temporary issue with the API. Try again later.";
136
136
  }
137
- return "Check the API documentation for this endpoint using explain_api.";
137
+ return "Check the API documentation for this endpoint using inspect_api.";
138
138
  }
139
139
  }
140
140
  const MAX_RAW_BODY = 500;
@@ -140,8 +140,8 @@ function dynamicArrayLimit(arr) {
140
140
  }
141
141
  costPerItem = totalCost / sampleCount;
142
142
  }
143
- const TOKEN_BUDGET = 200;
144
- const MIN_ITEMS = 3;
143
+ const TOKEN_BUDGET = 500;
144
+ const MIN_ITEMS = 10;
145
145
  return Math.max(MIN_ITEMS, Math.min(MAX_ARRAY_LIMIT, Math.floor(TOKEN_BUDGET / costPerItem)));
146
146
  }
147
147
  const MAX_INFER_DEPTH = 8;
@@ -428,6 +428,8 @@ function inferType(value, typeName, typeRegistry, conflicts, depth = 0) {
428
428
  });
429
429
  typeRegistry.set(typeName, placeholder);
430
430
  const usedNames = new Set();
431
+ // Pre-compute all sanitized names to detect collisions with synthetic _count fields
432
+ const allSanitizedNames = new Set(entries.map(([k]) => sanitizeFieldName(k)));
431
433
  const fieldConfigs = {};
432
434
  for (const [originalKey, fieldValue] of entries) {
433
435
  let sanitized = sanitizeFieldName(originalKey);
@@ -463,13 +465,28 @@ function inferType(value, typeName, typeRegistry, conflicts, depth = 0) {
463
465
  limit: { type: GraphQLInt, defaultValue: arrDefault },
464
466
  offset: { type: GraphQLInt, defaultValue: 0 },
465
467
  },
466
- resolve: (source, args) => {
468
+ resolve: (source, args, ctx) => {
467
469
  const val = source[key];
468
470
  if (!Array.isArray(val))
469
471
  return val;
472
+ if (ctx?.unlimited)
473
+ return val;
470
474
  return val.slice(args.offset, args.offset + args.limit);
471
475
  },
472
476
  };
477
+ // Add _count sibling so truncation is visible (skip if source data already has this field name)
478
+ const countFieldName = `${sanitized}_count`;
479
+ if (!allSanitizedNames.has(countFieldName)) {
480
+ usedNames.add(countFieldName);
481
+ fieldConfigs[countFieldName] = {
482
+ type: GraphQLInt,
483
+ description: `Total count of ${sanitized} (before any limit/offset truncation)`,
484
+ resolve: (source) => {
485
+ const val = source[key];
486
+ return Array.isArray(val) ? val.length : null;
487
+ },
488
+ };
489
+ }
473
490
  }
474
491
  else {
475
492
  fieldConfigs[sanitized] = {
@@ -602,10 +619,12 @@ export function buildSchemaFromData(data, method, pathTemplate, requestBodySchem
602
619
  limit: { type: GraphQLInt, defaultValue: topLimit },
603
620
  offset: { type: GraphQLInt, defaultValue: 0 },
604
621
  },
605
- resolve: (source, args) => {
622
+ resolve: (source, args, ctx) => {
606
623
  const arr = source;
607
624
  if (!Array.isArray(arr))
608
625
  return arr;
626
+ if (ctx?.unlimited)
627
+ return arr;
609
628
  return arr.slice(args.offset, args.offset + args.limit);
610
629
  },
611
630
  },
@@ -753,12 +772,48 @@ export function collectJsonFields(schema) {
753
772
  walk(queryType, "");
754
773
  return jsonFields;
755
774
  }
775
+ /**
776
+ * Walk response data and collect actual array lengths for each array field.
777
+ * Returns a map of sanitized dotted paths to their lengths.
778
+ * Used by inspect_api to give the AI visibility into true array sizes.
779
+ */
780
+ export function collectArrayLengths(data, depth = 0, prefix = "") {
781
+ if (depth >= MAX_INFER_DEPTH || data === null || data === undefined)
782
+ return {};
783
+ const result = {};
784
+ if (Array.isArray(data)) {
785
+ if (prefix)
786
+ result[prefix] = data.length;
787
+ // Recurse into first element to discover nested arrays
788
+ if (data.length > 0 && typeof data[0] === "object" && data[0] !== null && !Array.isArray(data[0])) {
789
+ Object.assign(result, collectArrayLengths(data[0], depth + 1, prefix));
790
+ }
791
+ return result;
792
+ }
793
+ if (typeof data === "object") {
794
+ for (const [key, value] of Object.entries(data)) {
795
+ const sanitized = sanitizeFieldName(key);
796
+ const path = prefix ? `${prefix}.${sanitized}` : sanitized;
797
+ if (Array.isArray(value)) {
798
+ result[path] = value.length;
799
+ // Recurse into first element for nested arrays
800
+ if (value.length > 0 && typeof value[0] === "object" && value[0] !== null && !Array.isArray(value[0])) {
801
+ Object.assign(result, collectArrayLengths(value[0], depth + 1, path));
802
+ }
803
+ }
804
+ else if (typeof value === "object" && value !== null) {
805
+ Object.assign(result, collectArrayLengths(value, depth + 1, path));
806
+ }
807
+ }
808
+ }
809
+ return result;
810
+ }
756
811
  /**
757
812
  * Execute a GraphQL selection query against JSON data using a schema.
758
813
  * The query should be a selection set like `{ id name collection { id } }`.
759
814
  * Also supports mutation syntax: `mutation { ... }`.
760
815
  */
761
- export async function executeQuery(schema, data, query) {
816
+ export async function executeQuery(schema, data, query, context) {
762
817
  const trimmed = query.trim();
763
818
  const fullQuery = trimmed.startsWith("query") || trimmed.startsWith("mutation") || trimmed.startsWith("{")
764
819
  ? trimmed
@@ -769,7 +824,7 @@ export async function executeQuery(schema, data, query) {
769
824
  const messages = validationErrors.map((e) => e.message).join("; ");
770
825
  throw new Error(`GraphQL query error: ${messages}`);
771
826
  }
772
- const result = await execute({ schema, document, rootValue: data });
827
+ const result = await execute({ schema, document, rootValue: data, contextValue: context });
773
828
  if (result.errors && result.errors.length > 0) {
774
829
  const messages = result.errors.map((e) => e.message).join("; ");
775
830
  throw new Error(`GraphQL query error: ${messages}`);