anyapi-mcp-server 1.4.0 → 1.5.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 +7 -3
- package/build/api-client.js +1 -1
- package/build/api-index.js +38 -18
- package/build/config.js +5 -5
- package/build/graphql-schema.js +59 -6
- package/build/index.js +43 -30
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ Works with services like **Datadog**, **PostHog**, **Metabase**, **Cloudflare**,
|
|
|
19
19
|
- **Multi-sample merging** — samples up to 10 array elements to build richer schemas that capture fields missing from individual items
|
|
20
20
|
- **Mutation support** — POST/PUT/DELETE/PATCH endpoints with OpenAPI request body schemas get GraphQL mutation types with typed inputs
|
|
21
21
|
- **Smart query suggestions** — `call_api` returns ready-to-use GraphQL queries based on the inferred schema
|
|
22
|
+
- **Shape-aware schema caching** — schemas are cached by response structure (not just endpoint), so the same path returning different shapes gets distinct schemas; `shapeHash` is returned for cache-aware workflows
|
|
22
23
|
- **Response caching** — 30-second TTL cache prevents duplicate HTTP calls across consecutive `call_api` → `query_api` flows
|
|
23
24
|
- **Retry with backoff** — automatic retries with exponential backoff and jitter for 429/5xx errors, honoring `Retry-After` headers
|
|
24
25
|
- **Multi-format responses** — parses JSON, XML, CSV, and plain text responses automatically
|
|
@@ -167,6 +168,8 @@ Inspect an API endpoint by making a real request and returning the inferred Grap
|
|
|
167
168
|
- Returns the full schema SDL showing all available fields and types
|
|
168
169
|
- Returns accepted parameters (name, location, required) from the API spec
|
|
169
170
|
- Returns `suggestedQueries` — ready-to-use GraphQL queries generated from the schema
|
|
171
|
+
- Returns `shapeHash` — a structural fingerprint of the response for cache-aware workflows
|
|
172
|
+
- Returns `bodyHash` for write operations when a request body is provided
|
|
170
173
|
- Accepts optional `headers` to override defaults for this request
|
|
171
174
|
- For write operations (POST/PUT/DELETE/PATCH) with request body schemas, the schema includes a Mutation type
|
|
172
175
|
|
|
@@ -207,6 +210,7 @@ Fetch data from an API endpoint, returning only the fields you select via GraphQ
|
|
|
207
210
|
- Supports `limit` and `offset` for client-side slicing of already-fetched data
|
|
208
211
|
- For API-level pagination, pass limit/offset inside `params` instead
|
|
209
212
|
- Accepts optional `headers` to override defaults for this request
|
|
213
|
+
- Response includes `_shapeHash` (and `_bodyHash` for writes) for tracking schema identity
|
|
210
214
|
|
|
211
215
|
### `explain_api`
|
|
212
216
|
|
|
@@ -225,7 +229,7 @@ Fetch data from multiple endpoints concurrently in a single tool call.
|
|
|
225
229
|
- Accepts an array of 1–10 requests, each with `method`, `path`, `params`, `body`, `query`, and optional `headers`
|
|
226
230
|
- All requests execute in parallel via `Promise.allSettled` — one failure does not affect the others
|
|
227
231
|
- Each request follows the `query_api` flow: HTTP fetch → schema inference → GraphQL field selection
|
|
228
|
-
- Returns an array of results: `{ method, path, data }` on success or `{ method, path, error }` on failure
|
|
232
|
+
- Returns an array of results: `{ method, path, data, shapeHash }` on success or `{ method, path, error }` on failure
|
|
229
233
|
- Run `call_api` first on each endpoint to discover the schema field names
|
|
230
234
|
|
|
231
235
|
## Workflow
|
|
@@ -260,8 +264,8 @@ OpenAPI/Postman spec
|
|
|
260
264
|
```
|
|
261
265
|
|
|
262
266
|
1. The spec is loaded at startup (from a local file or fetched from an HTTPS URL with filesystem caching) and parsed into an endpoint index with tags, paths, parameters, and request body schemas
|
|
263
|
-
2. `call_api` makes a real HTTP request, infers a GraphQL schema from the JSON response, and caches both the response (30s TTL) and the schema
|
|
264
|
-
3. `query_api` re-uses the cached response if called within 30s, executes your GraphQL field selection against the data, and returns only the fields you asked for
|
|
267
|
+
2. `call_api` makes a real HTTP request, infers a GraphQL schema from the JSON response, and caches both the response (30s TTL) and the schema. Schemas are keyed by endpoint + response shape, so the same path returning different structures gets distinct schemas
|
|
268
|
+
3. `query_api` re-uses the cached response if called within 30s, executes your GraphQL field selection against the data, and returns only the fields you asked for. Includes `_shapeHash` in the response for tracking schema identity
|
|
265
269
|
4. Write operations (POST/PUT/DELETE/PATCH) with OpenAPI request body schemas get a Mutation type with typed `GraphQLInputObjectType` inputs
|
|
266
270
|
|
|
267
271
|
## Supported Spec Formats
|
package/build/api-client.js
CHANGED
|
@@ -34,7 +34,7 @@ export async function callApi(config, method, pathTemplate, params, body, extraH
|
|
|
34
34
|
// --- URL construction ---
|
|
35
35
|
const { url: interpolatedPath, remainingParams } = interpolatePath(pathTemplate, params ?? {});
|
|
36
36
|
let fullUrl = `${config.baseUrl}${interpolatedPath}`;
|
|
37
|
-
if (
|
|
37
|
+
if (Object.keys(remainingParams).length > 0) {
|
|
38
38
|
const qs = new URLSearchParams();
|
|
39
39
|
for (const [k, v] of Object.entries(remainingParams)) {
|
|
40
40
|
if (v !== undefined && v !== null) {
|
package/build/api-index.js
CHANGED
|
@@ -118,19 +118,21 @@ function postmanUrlToPath(url) {
|
|
|
118
118
|
export class ApiIndex {
|
|
119
119
|
byTag = new Map();
|
|
120
120
|
allEndpoints = [];
|
|
121
|
-
constructor(
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
121
|
+
constructor(specContents) {
|
|
122
|
+
for (const specContent of specContents) {
|
|
123
|
+
let parsed;
|
|
124
|
+
try {
|
|
125
|
+
parsed = JSON.parse(specContent);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
parsed = yaml.load(specContent);
|
|
129
|
+
}
|
|
130
|
+
if (isPostmanCollection(parsed)) {
|
|
131
|
+
this.parsePostman(parsed);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
this.parseOpenApi(parsed, parsed);
|
|
135
|
+
}
|
|
134
136
|
}
|
|
135
137
|
}
|
|
136
138
|
parseOpenApi(spec, rawSpec) {
|
|
@@ -251,6 +253,15 @@ export class ApiIndex {
|
|
|
251
253
|
}
|
|
252
254
|
tagList.push(endpoint);
|
|
253
255
|
}
|
|
256
|
+
listAll() {
|
|
257
|
+
return this.allEndpoints.map((ep) => ({
|
|
258
|
+
method: ep.method,
|
|
259
|
+
path: ep.path,
|
|
260
|
+
summary: ep.summary,
|
|
261
|
+
tag: ep.tag,
|
|
262
|
+
parameters: ep.parameters,
|
|
263
|
+
}));
|
|
264
|
+
}
|
|
254
265
|
listAllCategories() {
|
|
255
266
|
const categories = [];
|
|
256
267
|
for (const [tag, endpoints] of this.byTag) {
|
|
@@ -260,7 +271,9 @@ export class ApiIndex {
|
|
|
260
271
|
return categories;
|
|
261
272
|
}
|
|
262
273
|
listAllByCategory(category) {
|
|
263
|
-
const
|
|
274
|
+
const lower = category.toLowerCase();
|
|
275
|
+
const key = [...this.byTag.keys()].find((k) => k.toLowerCase() === lower);
|
|
276
|
+
const endpoints = key ? this.byTag.get(key) : [];
|
|
264
277
|
return endpoints.map((ep) => ({
|
|
265
278
|
method: ep.method,
|
|
266
279
|
path: ep.path,
|
|
@@ -270,11 +283,18 @@ export class ApiIndex {
|
|
|
270
283
|
}));
|
|
271
284
|
}
|
|
272
285
|
searchAll(keyword) {
|
|
273
|
-
|
|
286
|
+
let matcher;
|
|
287
|
+
try {
|
|
288
|
+
const re = new RegExp(keyword, "i");
|
|
289
|
+
matcher = (text) => re.test(text);
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
const lower = keyword.toLowerCase();
|
|
293
|
+
matcher = (text) => text.toLowerCase().includes(lower);
|
|
294
|
+
}
|
|
274
295
|
return this.allEndpoints
|
|
275
|
-
.filter((ep) => ep.path
|
|
276
|
-
ep.summary
|
|
277
|
-
ep.description.toLowerCase().includes(lower))
|
|
296
|
+
.filter((ep) => matcher(ep.path) ||
|
|
297
|
+
matcher(ep.summary))
|
|
278
298
|
.map((ep) => ({
|
|
279
299
|
method: ep.method,
|
|
280
300
|
path: ep.path,
|
package/build/config.js
CHANGED
|
@@ -56,7 +56,7 @@ const USAGE = `Usage: anyapi-mcp --name <name> --spec <path-or-url> --base-url <
|
|
|
56
56
|
|
|
57
57
|
Required:
|
|
58
58
|
--name Server name (e.g. "petstore")
|
|
59
|
-
--spec Path or URL to OpenAPI spec (JSON or YAML)
|
|
59
|
+
--spec Path or URL to OpenAPI spec (JSON or YAML) (repeatable for multiple specs)
|
|
60
60
|
--base-url API base URL (e.g. "https://api.example.com")
|
|
61
61
|
|
|
62
62
|
Optional:
|
|
@@ -65,13 +65,13 @@ Optional:
|
|
|
65
65
|
--log Path to request/response log file (NDJSON format)`;
|
|
66
66
|
export async function loadConfig() {
|
|
67
67
|
const name = getArg("--name");
|
|
68
|
-
const
|
|
68
|
+
const specUrls = getAllArgs("--spec");
|
|
69
69
|
const baseUrl = getArg("--base-url");
|
|
70
|
-
if (!name ||
|
|
70
|
+
if (!name || specUrls.length === 0 || !baseUrl) {
|
|
71
71
|
console.error(USAGE);
|
|
72
72
|
process.exit(1);
|
|
73
73
|
}
|
|
74
|
-
const
|
|
74
|
+
const specs = await Promise.all(specUrls.map((url) => loadSpec(interpolateEnv(url))));
|
|
75
75
|
const headers = {};
|
|
76
76
|
for (const raw of getAllArgs("--header")) {
|
|
77
77
|
const colonIdx = raw.indexOf(":");
|
|
@@ -86,7 +86,7 @@ export async function loadConfig() {
|
|
|
86
86
|
const logPath = getArg("--log");
|
|
87
87
|
return {
|
|
88
88
|
name,
|
|
89
|
-
|
|
89
|
+
specs,
|
|
90
90
|
baseUrl: interpolateEnv(baseUrl).replace(/\/+$/, ""),
|
|
91
91
|
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
92
92
|
logPath: logPath ? resolve(logPath) : undefined,
|
package/build/graphql-schema.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { GraphQLSchema, GraphQLObjectType, GraphQLInputObjectType, GraphQLScalarType, GraphQLString, GraphQLInt, GraphQLFloat, GraphQLBoolean, GraphQLList, GraphQLNonNull, graphql as executeGraphQL, printSchema, } from "graphql";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
const DEFAULT_ARRAY_LIMIT = 50;
|
|
3
4
|
const MAX_SAMPLE_SIZE = 30;
|
|
4
5
|
const MAX_INFER_DEPTH = 4;
|
|
@@ -28,8 +29,8 @@ export function truncateIfArray(data, limit, offset) {
|
|
|
28
29
|
const sliced = data.slice(off, off + lim);
|
|
29
30
|
return { data: sliced, truncated: sliced.length < total, total };
|
|
30
31
|
}
|
|
31
|
-
function cacheKey(method, pathTemplate) {
|
|
32
|
-
return `${method}:${pathTemplate}`;
|
|
32
|
+
function cacheKey(method, pathTemplate, hash) {
|
|
33
|
+
return `${method}:${pathTemplate}:${hash}`;
|
|
33
34
|
}
|
|
34
35
|
/**
|
|
35
36
|
* Sanitize a JSON key into a valid GraphQL field name.
|
|
@@ -155,6 +156,51 @@ function mergeArraySamples(arr) {
|
|
|
155
156
|
return null;
|
|
156
157
|
return mergeSamples(objectSamples);
|
|
157
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* Produce a structural fingerprint string from JSON data.
|
|
161
|
+
* Captures keys + recursive type structure but NOT values.
|
|
162
|
+
* Sorted keys ensure determinism regardless of object key order.
|
|
163
|
+
* Bounded by MAX_INFER_DEPTH to match inference behavior.
|
|
164
|
+
*/
|
|
165
|
+
function shapeFingerprint(data, depth = 0) {
|
|
166
|
+
if (data === null || data === undefined)
|
|
167
|
+
return "n";
|
|
168
|
+
if (depth >= MAX_INFER_DEPTH)
|
|
169
|
+
return "J";
|
|
170
|
+
if (Array.isArray(data)) {
|
|
171
|
+
if (data.length === 0)
|
|
172
|
+
return "[]";
|
|
173
|
+
if (hasMixedTypes(data))
|
|
174
|
+
return "[J]";
|
|
175
|
+
const mergeResult = mergeArraySamples(data);
|
|
176
|
+
if (mergeResult)
|
|
177
|
+
return `[${shapeFingerprint(mergeResult.merged, depth + 1)}]`;
|
|
178
|
+
return `[${shapeFingerprint(data[0], depth + 1)}]`;
|
|
179
|
+
}
|
|
180
|
+
if (typeof data === "object") {
|
|
181
|
+
const obj = data;
|
|
182
|
+
const keys = Object.keys(obj).sort();
|
|
183
|
+
if (keys.length === 0)
|
|
184
|
+
return "{}";
|
|
185
|
+
return `{${keys.map((k) => `${k}:${shapeFingerprint(obj[k], depth + 1)}`).join(",")}}`;
|
|
186
|
+
}
|
|
187
|
+
switch (typeof data) {
|
|
188
|
+
case "number":
|
|
189
|
+
return Number.isInteger(data) ? "i" : "f";
|
|
190
|
+
case "boolean":
|
|
191
|
+
return "b";
|
|
192
|
+
default:
|
|
193
|
+
return "s";
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Compute a truncated SHA-256 hash of the structural fingerprint of JSON data.
|
|
198
|
+
* Returns a 12-character hex string.
|
|
199
|
+
*/
|
|
200
|
+
export function computeShapeHash(data) {
|
|
201
|
+
const fp = shapeFingerprint(data);
|
|
202
|
+
return createHash("sha256").update(fp).digest("hex").slice(0, 12);
|
|
203
|
+
}
|
|
158
204
|
/**
|
|
159
205
|
* Recursively infer a GraphQL type from a JSON value.
|
|
160
206
|
* For objects, creates a named GraphQLObjectType with explicit resolvers
|
|
@@ -376,15 +422,22 @@ export function buildSchemaFromData(data, method, pathTemplate, requestBodySchem
|
|
|
376
422
|
}
|
|
377
423
|
/**
|
|
378
424
|
* Get a cached schema or build + cache a new one from the response data.
|
|
425
|
+
*
|
|
426
|
+
* @param cacheHash Optional hash to use as the cache key discriminator.
|
|
427
|
+
* If provided (e.g. body hash for mutations), it is used instead of the
|
|
428
|
+
* response shape hash for cache lookup. The shapeHash is always computed
|
|
429
|
+
* from the response data and returned regardless.
|
|
379
430
|
*/
|
|
380
|
-
export function getOrBuildSchema(data, method, pathTemplate, requestBodySchema) {
|
|
381
|
-
const
|
|
431
|
+
export function getOrBuildSchema(data, method, pathTemplate, requestBodySchema, cacheHash) {
|
|
432
|
+
const shapeHash = computeShapeHash(data);
|
|
433
|
+
const effectiveHash = cacheHash ?? shapeHash;
|
|
434
|
+
const key = cacheKey(method, pathTemplate, effectiveHash);
|
|
382
435
|
const cached = schemaCache.get(key);
|
|
383
436
|
if (cached)
|
|
384
|
-
return cached;
|
|
437
|
+
return { schema: cached, shapeHash };
|
|
385
438
|
const schema = buildSchemaFromData(data, method, pathTemplate, requestBodySchema);
|
|
386
439
|
schemaCache.set(key, schema);
|
|
387
|
-
return schema;
|
|
440
|
+
return { schema, shapeHash };
|
|
388
441
|
}
|
|
389
442
|
/**
|
|
390
443
|
* Convert a GraphQL schema to SDL string for display.
|
package/build/index.js
CHANGED
|
@@ -7,36 +7,34 @@ import { ApiIndex } from "./api-index.js";
|
|
|
7
7
|
import { callApi } from "./api-client.js";
|
|
8
8
|
import { initLogger } from "./logger.js";
|
|
9
9
|
import { generateSuggestions } from "./query-suggestions.js";
|
|
10
|
-
import { getOrBuildSchema, executeQuery, schemaToSDL, truncateIfArray, } from "./graphql-schema.js";
|
|
10
|
+
import { getOrBuildSchema, executeQuery, schemaToSDL, truncateIfArray, computeShapeHash, } from "./graphql-schema.js";
|
|
11
|
+
const WRITE_METHODS = new Set(["POST", "PUT", "DELETE", "PATCH"]);
|
|
11
12
|
const config = await loadConfig();
|
|
12
13
|
initLogger(config.logPath ?? null);
|
|
13
|
-
const apiIndex = new ApiIndex(config.
|
|
14
|
+
const apiIndex = new ApiIndex(config.specs);
|
|
14
15
|
const server = new McpServer({
|
|
15
16
|
name: config.name,
|
|
16
17
|
version: "1.2.1",
|
|
17
18
|
});
|
|
18
19
|
// --- Tool 1: list_api ---
|
|
19
20
|
server.tool("list_api", `List available ${config.name} API endpoints. ` +
|
|
20
|
-
"Call with no arguments to see all
|
|
21
|
-
"Provide 'category' to
|
|
22
|
-
"Provide 'search' to search across paths and
|
|
23
|
-
"The correct query format is auto-selected based on mode. " +
|
|
24
|
-
"You can optionally override with a custom 'query' parameter. " +
|
|
21
|
+
"Call with no arguments to see all endpoints. " +
|
|
22
|
+
"Provide 'category' to filter by tag. " +
|
|
23
|
+
"Provide 'search' to search across paths and summaries (supports regex). " +
|
|
25
24
|
"Results are paginated with limit (default 20) and offset.", {
|
|
26
25
|
category: z
|
|
27
26
|
.string()
|
|
28
27
|
.optional()
|
|
29
|
-
.describe("Tag/category to filter by.
|
|
28
|
+
.describe("Tag/category to filter by. Case-insensitive."),
|
|
30
29
|
search: z
|
|
31
30
|
.string()
|
|
32
31
|
.optional()
|
|
33
|
-
.describe("Search keyword across endpoint paths and
|
|
32
|
+
.describe("Search keyword or regex pattern across endpoint paths and summaries"),
|
|
34
33
|
query: z
|
|
35
34
|
.string()
|
|
36
35
|
.optional()
|
|
37
|
-
.describe("
|
|
38
|
-
"
|
|
39
|
-
"Endpoints (with category/search): '{ items { method path summary tag parameters { name in required description } } _count }'"),
|
|
36
|
+
.describe("GraphQL selection query. Default: '{ items { method path summary } _count }'. " +
|
|
37
|
+
"Available fields: method, path, summary, tag, parameters { name in required description }"),
|
|
40
38
|
limit: z
|
|
41
39
|
.number()
|
|
42
40
|
.int()
|
|
@@ -51,7 +49,6 @@ server.tool("list_api", `List available ${config.name} API endpoints. ` +
|
|
|
51
49
|
.describe("Items to skip (default: 0)"),
|
|
52
50
|
}, async ({ category, search, query, limit, offset }) => {
|
|
53
51
|
try {
|
|
54
|
-
const isEndpointMode = !!(search || category);
|
|
55
52
|
let data;
|
|
56
53
|
if (search) {
|
|
57
54
|
data = apiIndex.searchAll(search);
|
|
@@ -60,7 +57,7 @@ server.tool("list_api", `List available ${config.name} API endpoints. ` +
|
|
|
60
57
|
data = apiIndex.listAllByCategory(category);
|
|
61
58
|
}
|
|
62
59
|
else {
|
|
63
|
-
data = apiIndex.
|
|
60
|
+
data = apiIndex.listAll();
|
|
64
61
|
}
|
|
65
62
|
// Empty results — return directly to avoid GraphQL schema errors on empty arrays
|
|
66
63
|
if (data.length === 0) {
|
|
@@ -70,11 +67,9 @@ server.tool("list_api", `List available ${config.name} API endpoints. ` +
|
|
|
70
67
|
],
|
|
71
68
|
};
|
|
72
69
|
}
|
|
73
|
-
const defaultQuery =
|
|
74
|
-
? "{ items { method path summary tag parameters { name in required description } } _count }"
|
|
75
|
-
: "{ items { tag endpointCount } _count }";
|
|
70
|
+
const defaultQuery = "{ items { method path summary } _count }";
|
|
76
71
|
const effectiveQuery = query ?? defaultQuery;
|
|
77
|
-
const schema = getOrBuildSchema(data, "LIST", category ?? search ?? "
|
|
72
|
+
const { schema } = getOrBuildSchema(data, "LIST", category ?? search ?? "_all");
|
|
78
73
|
const { data: sliced, truncated, total } = truncateIfArray(data, limit ?? 20, offset);
|
|
79
74
|
const queryResult = await executeQuery(schema, sliced, effectiveQuery);
|
|
80
75
|
if (truncated && typeof queryResult === "object" && queryResult !== null) {
|
|
@@ -135,9 +130,12 @@ server.tool("call_api", `Inspect a ${config.name} API endpoint. Makes a real req
|
|
|
135
130
|
try {
|
|
136
131
|
const data = await callApi(config, method, path, params, body, headers, "populate");
|
|
137
132
|
const endpoint = apiIndex.getEndpoint(method, path);
|
|
138
|
-
const
|
|
133
|
+
const bodyHash = WRITE_METHODS.has(method) && body ? computeShapeHash(body) : undefined;
|
|
134
|
+
const { schema, shapeHash } = getOrBuildSchema(data, method, path, endpoint?.requestBodySchema, bodyHash);
|
|
139
135
|
const sdl = schemaToSDL(schema);
|
|
140
|
-
const result = { graphqlSchema: sdl };
|
|
136
|
+
const result = { graphqlSchema: sdl, shapeHash };
|
|
137
|
+
if (bodyHash)
|
|
138
|
+
result.bodyHash = bodyHash;
|
|
141
139
|
if (endpoint && endpoint.parameters.length > 0) {
|
|
142
140
|
result.parameters = endpoint.parameters.map((p) => ({
|
|
143
141
|
name: p.name,
|
|
@@ -233,16 +231,22 @@ server.tool("query_api", `Fetch data from a ${config.name} API endpoint, returni
|
|
|
233
231
|
try {
|
|
234
232
|
const rawData = await callApi(config, method, path, params, body, headers, "consume");
|
|
235
233
|
const endpoint = apiIndex.getEndpoint(method, path);
|
|
236
|
-
const
|
|
234
|
+
const bodyHash = WRITE_METHODS.has(method) && body ? computeShapeHash(body) : undefined;
|
|
235
|
+
const { schema, shapeHash } = getOrBuildSchema(rawData, method, path, endpoint?.requestBodySchema, bodyHash);
|
|
237
236
|
const { data, truncated, total } = truncateIfArray(rawData, limit, offset);
|
|
238
237
|
const queryResult = await executeQuery(schema, data, query);
|
|
239
|
-
if (
|
|
240
|
-
queryResult.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
238
|
+
if (typeof queryResult === "object" && queryResult !== null) {
|
|
239
|
+
queryResult._shapeHash = shapeHash;
|
|
240
|
+
if (bodyHash)
|
|
241
|
+
queryResult._bodyHash = bodyHash;
|
|
242
|
+
if (truncated) {
|
|
243
|
+
queryResult._meta = {
|
|
244
|
+
total,
|
|
245
|
+
offset: offset ?? 0,
|
|
246
|
+
limit: limit ?? 50,
|
|
247
|
+
hasMore: true,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
246
250
|
}
|
|
247
251
|
return {
|
|
248
252
|
content: [
|
|
@@ -380,9 +384,18 @@ server.tool("batch_query", `Fetch data from multiple ${config.name} API endpoint
|
|
|
380
384
|
const settled = await Promise.allSettled(requests.map(async (req) => {
|
|
381
385
|
const rawData = await callApi(config, req.method, req.path, req.params, req.body, req.headers, "none");
|
|
382
386
|
const endpoint = apiIndex.getEndpoint(req.method, req.path);
|
|
383
|
-
const
|
|
387
|
+
const bodyHash = WRITE_METHODS.has(req.method) && req.body
|
|
388
|
+
? computeShapeHash(req.body)
|
|
389
|
+
: undefined;
|
|
390
|
+
const { schema, shapeHash } = getOrBuildSchema(rawData, req.method, req.path, endpoint?.requestBodySchema, bodyHash);
|
|
384
391
|
const queryResult = await executeQuery(schema, rawData, req.query);
|
|
385
|
-
return {
|
|
392
|
+
return {
|
|
393
|
+
method: req.method,
|
|
394
|
+
path: req.path,
|
|
395
|
+
data: queryResult,
|
|
396
|
+
shapeHash,
|
|
397
|
+
...(bodyHash ? { bodyHash } : {}),
|
|
398
|
+
};
|
|
386
399
|
}));
|
|
387
400
|
const results = settled.map((outcome, i) => {
|
|
388
401
|
if (outcome.status === "fulfilled") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anyapi-mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "A universal MCP server that connects any REST API (via OpenAPI spec) to AI assistants, with GraphQL-style field selection and automatic schema inference.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "tsc",
|
|
33
33
|
"start": "node build/index.js",
|
|
34
|
+
"test": "vitest run",
|
|
34
35
|
"prepublishOnly": "npm run build"
|
|
35
36
|
},
|
|
36
37
|
"engines": {
|
|
@@ -46,6 +47,7 @@
|
|
|
46
47
|
"devDependencies": {
|
|
47
48
|
"@types/js-yaml": "^4.0.9",
|
|
48
49
|
"@types/node": "^22.0.0",
|
|
49
|
-
"typescript": "^5.7.0"
|
|
50
|
+
"typescript": "^5.7.0",
|
|
51
|
+
"vitest": "^4.0.18"
|
|
50
52
|
}
|
|
51
53
|
}
|