anyapi-mcp-server 1.8.1 → 2.0.1
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 +3 -2
- package/build/data-cache.js +5 -1
- package/build/error-context.js +1 -1
- package/build/graphql-schema.js +61 -6
- package/build/index.js +13 -699
- package/build/json-patch.js +137 -0
- package/build/oauth.js +0 -3
- package/build/pagination.js +1 -1
- package/build/pre-write-backup.js +19 -1
- package/build/token-budget.js +88 -45
- package/build/tools/auth.js +117 -0
- package/build/tools/inspect-api.js +213 -0
- package/build/tools/list-api.js +82 -0
- package/build/tools/mutate-api.js +222 -0
- package/build/tools/query-api.js +183 -0
- package/build/tools/shared.js +65 -0
- package/build/write-safety.js +56 -0
- package/package.json +1 -1
- package/build/response-cache.js +0 -49
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
|
|
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`
|
|
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
|
package/build/data-cache.js
CHANGED
|
@@ -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
|
-
|
|
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 {
|
package/build/error-context.js
CHANGED
|
@@ -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
|
|
137
|
+
return "Check the API documentation for this endpoint using inspect_api.";
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
const MAX_RAW_BODY = 500;
|
package/build/graphql-schema.js
CHANGED
|
@@ -140,8 +140,8 @@ function dynamicArrayLimit(arr) {
|
|
|
140
140
|
}
|
|
141
141
|
costPerItem = totalCost / sampleCount;
|
|
142
142
|
}
|
|
143
|
-
const TOKEN_BUDGET =
|
|
144
|
-
const MIN_ITEMS =
|
|
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}`);
|