anyapi-mcp-server 1.1.4 → 1.2.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 CHANGED
@@ -16,7 +16,9 @@ Instead of building a custom MCP server for every API, `anyapi-mcp-server` reads
16
16
  - **Retry with backoff** — automatic retries with exponential backoff and jitter for 429/5xx errors, honoring `Retry-After` headers
17
17
  - **Multi-format responses** — parses JSON, XML, CSV, and plain text responses automatically
18
18
  - **Built-in pagination** — API-level pagination via `params`; client-side slicing with top-level `limit`/`offset`
19
- - **Per-request headers** — override default headers on individual `call_api`/`query_api` calls
19
+ - **Spec documentation lookup** — `explain_api` returns rich endpoint docs (parameters, response codes, deprecation, request body schema) without making HTTP requests
20
+ - **Concurrent batch queries** — `batch_query` fetches data from up to 10 endpoints in parallel, returning all results in one tool call
21
+ - **Per-request headers** — override default headers on individual `call_api`/`query_api`/`batch_query` calls
20
22
  - **Environment variable interpolation** — use `${ENV_VAR}` in base URLs and headers
21
23
  - **Request logging** — optional NDJSON request/response log with sensitive header masking
22
24
 
@@ -114,10 +116,10 @@ Add to your MCP configuration (e.g. `~/.cursor/mcp.json` or Claude Desktop confi
114
116
  "anyapi-mcp-server",
115
117
  "--name", "metabase",
116
118
  "--base-url", "https://your-metabase-instance.com/api",
117
- "--header", "X-Metabase-Session: ${METABASE_SESSION_TOKEN}"
119
+ "--header", "x-api-key: ${METABASE_API_KEY}"
118
120
  ],
119
121
  "env": {
120
- "METABASE_SESSION_TOKEN": "your-metabase-session-token"
122
+ "METABASE_API_KEY": "your-metabase-api-key"
121
123
  }
122
124
  }
123
125
  }
@@ -126,7 +128,7 @@ Add to your MCP configuration (e.g. `~/.cursor/mcp.json` or Claude Desktop confi
126
128
 
127
129
  ## Tools
128
130
 
129
- The server exposes three MCP tools:
131
+ The server exposes five MCP tools:
130
132
 
131
133
  ### `list_api`
132
134
 
@@ -206,11 +208,33 @@ Fetch data from an API endpoint, returning only the fields you select via GraphQ
206
208
  - For API-level pagination, pass limit/offset inside `params` instead
207
209
  - Accepts optional `headers` to override defaults for this request
208
210
 
211
+ ### `explain_api`
212
+
213
+ Get detailed documentation for an endpoint directly from the spec — no HTTP request is made.
214
+
215
+ - Returns summary, description, operationId, deprecation status, tag
216
+ - Lists all parameters with name, location (`path`/`query`/`header`), required flag, and description
217
+ - Shows request body schema with property types, required fields, and descriptions
218
+ - Lists response status codes with descriptions (e.g. `200 OK`, `404 Not Found`)
219
+ - Includes external docs link when available
220
+
221
+ ### `batch_query`
222
+
223
+ Fetch data from multiple endpoints concurrently in a single tool call.
224
+
225
+ - Accepts an array of 1–10 requests, each with `method`, `path`, `params`, `body`, `query`, and optional `headers`
226
+ - All requests execute in parallel via `Promise.allSettled` — one failure does not affect the others
227
+ - 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
229
+ - Run `call_api` first on each endpoint to discover the schema field names
230
+
209
231
  ## Workflow
210
232
 
211
233
  1. **Discover** endpoints with `list_api`
212
- 2. **Inspect** a specific endpoint with `call_api` to see its schema and suggested queries
213
- 3. **Query** the endpoint with `query_api` to fetch exactly the fields you need
234
+ 2. **Understand** an endpoint with `explain_api` to see its parameters, request body, and response codes
235
+ 3. **Inspect** a specific endpoint with `call_api` to see the inferred response schema and suggested queries
236
+ 4. **Query** the endpoint with `query_api` to fetch exactly the fields you need
237
+ 5. **Batch** multiple queries with `batch_query` when you need data from several endpoints at once
214
238
 
215
239
  ## How It Works
216
240
 
@@ -218,20 +242,21 @@ Fetch data from an API endpoint, returning only the fields you select via GraphQ
218
242
  OpenAPI/Postman spec
219
243
 
220
244
 
221
- ┌─────────┐ ┌──────────┐ ┌────────────┐
222
- list_api │ │ call_api │────▶│ query_api │
223
- (browse) │ │ (schema) │ (data)
224
- └─────────┘ └──────────┘ └────────────┘
225
-
226
-
227
- REST API call REST API call
228
- (with retry (cached if same
229
- + caching) as call_api)
230
-
231
-
232
- Infer GraphQL Execute GraphQL
233
- schema from query against
234
- JSON response response data
245
+ ┌─────────┐ ┌─────────────┐ ┌──────────┐ ┌───────────┐ ┌─────────────┐
246
+ │list_api │ explain_api │ │ call_api │ │ query_api batch_query │
247
+ │(browse) │ (docs) │ │ (schema) │ (data) │ (parallel) │
248
+ └─────────┘ └─────────────┘ └──────────┘ └───────────┘ └─────────────┘
249
+ no HTTP │ │ │
250
+ request ▼ ▼ ▼
251
+ Spec index Spec index REST API call REST API call N concurrent
252
+ (tags, (params, (with retry (cached if REST API calls
253
+ paths) responses, + caching) same as + GraphQL
254
+ body schema) call_api) execution
255
+
256
+ Infer GraphQL
257
+ schema from Execute GraphQL
258
+ JSON response query against
259
+ response data
235
260
  ```
236
261
 
237
262
  1. The spec file is parsed at startup into an endpoint index with tags, paths, parameters, and request body schemas
@@ -144,6 +144,22 @@ export class ApiIndex {
144
144
  description: p.description,
145
145
  }));
146
146
  const requestBodySchema = extractRequestBodySchema(op.requestBody, rawSpec);
147
+ // Extract response descriptions
148
+ let responses;
149
+ if (op.responses) {
150
+ responses = Object.entries(op.responses).map(([code, resp]) => ({
151
+ statusCode: code,
152
+ description: resp.description ?? "",
153
+ }));
154
+ }
155
+ // Extract request body description
156
+ let requestBodyDescription;
157
+ if (op.requestBody && typeof op.requestBody === "object") {
158
+ const rb = op.requestBody;
159
+ if (typeof rb.description === "string") {
160
+ requestBodyDescription = rb.description;
161
+ }
162
+ }
147
163
  const endpoint = {
148
164
  method: method.toUpperCase(),
149
165
  path,
@@ -153,6 +169,13 @@ export class ApiIndex {
153
169
  parameters,
154
170
  hasRequestBody: !!op.requestBody,
155
171
  requestBodySchema,
172
+ operationId: op.operationId,
173
+ deprecated: op.deprecated ?? undefined,
174
+ responses,
175
+ requestBodyDescription,
176
+ externalDocs: op.externalDocs?.url
177
+ ? { url: op.externalDocs.url, description: op.externalDocs.description }
178
+ : undefined,
156
179
  };
157
180
  this.addEndpoint(endpoint);
158
181
  }
package/build/index.js CHANGED
@@ -13,7 +13,7 @@ initLogger(config.logPath ?? null);
13
13
  const apiIndex = new ApiIndex(config.spec);
14
14
  const server = new McpServer({
15
15
  name: config.name,
16
- version: "1.1.4",
16
+ version: "1.2.1",
17
17
  });
18
18
  // --- Tool 1: list_api ---
19
19
  server.tool("list_api", `List available ${config.name} API endpoints. ` +
@@ -252,6 +252,159 @@ server.tool("query_api", `Fetch data from a ${config.name} API endpoint, returni
252
252
  };
253
253
  }
254
254
  });
255
+ // --- Tool 4: explain_api ---
256
+ server.tool("explain_api", `Get detailed documentation for a ${config.name} API endpoint from the spec. ` +
257
+ "Returns all available spec information — summary, description, parameters, " +
258
+ "request body schema, response codes, deprecation status — without making any HTTP request. " +
259
+ "Use list_api first to discover endpoints, then explain_api to understand them before calling.", {
260
+ method: z
261
+ .enum(["GET", "POST", "PUT", "DELETE", "PATCH"])
262
+ .describe("HTTP method"),
263
+ path: z
264
+ .string()
265
+ .describe("API path template (e.g. '/api/card/{id}')"),
266
+ }, async ({ method, path }) => {
267
+ try {
268
+ const endpoint = apiIndex.getEndpoint(method, path);
269
+ if (!endpoint) {
270
+ return {
271
+ content: [
272
+ {
273
+ type: "text",
274
+ text: JSON.stringify({
275
+ error: `Endpoint not found: ${method} ${path}`,
276
+ hint: "Use list_api to discover available endpoints.",
277
+ }),
278
+ },
279
+ ],
280
+ isError: true,
281
+ };
282
+ }
283
+ const result = {
284
+ method: endpoint.method,
285
+ path: endpoint.path,
286
+ summary: endpoint.summary,
287
+ };
288
+ if (endpoint.description) {
289
+ result.description = endpoint.description;
290
+ }
291
+ if (endpoint.operationId) {
292
+ result.operationId = endpoint.operationId;
293
+ }
294
+ if (endpoint.deprecated) {
295
+ result.deprecated = true;
296
+ }
297
+ result.tag = endpoint.tag;
298
+ if (endpoint.parameters.length > 0) {
299
+ result.parameters = endpoint.parameters.map((p) => ({
300
+ name: p.name,
301
+ in: p.in,
302
+ required: p.required,
303
+ ...(p.description ? { description: p.description } : {}),
304
+ }));
305
+ }
306
+ if (endpoint.hasRequestBody) {
307
+ const bodyInfo = {};
308
+ if (endpoint.requestBodyDescription) {
309
+ bodyInfo.description = endpoint.requestBodyDescription;
310
+ }
311
+ if (endpoint.requestBodySchema) {
312
+ bodyInfo.contentType = endpoint.requestBodySchema.contentType;
313
+ bodyInfo.properties = endpoint.requestBodySchema.properties;
314
+ }
315
+ result.requestBody = bodyInfo;
316
+ }
317
+ if (endpoint.responses && endpoint.responses.length > 0) {
318
+ result.responses = endpoint.responses;
319
+ }
320
+ if (endpoint.externalDocs) {
321
+ result.externalDocs = endpoint.externalDocs;
322
+ }
323
+ return {
324
+ content: [
325
+ { type: "text", text: JSON.stringify(result, null, 2) },
326
+ ],
327
+ };
328
+ }
329
+ catch (error) {
330
+ const message = error instanceof Error ? error.message : String(error);
331
+ return {
332
+ content: [
333
+ { type: "text", text: JSON.stringify({ error: message }) },
334
+ ],
335
+ isError: true,
336
+ };
337
+ }
338
+ });
339
+ // --- Tool 5: batch_query ---
340
+ server.tool("batch_query", `Fetch data from multiple ${config.name} API endpoints concurrently. ` +
341
+ "Each request in the batch follows the query_api flow — makes a real HTTP request " +
342
+ "and returns only the fields selected via GraphQL query. " +
343
+ "All requests execute in parallel; one failure does not affect the others. " +
344
+ "IMPORTANT: Run call_api first on each endpoint to discover schema field names.", {
345
+ requests: z
346
+ .array(z.object({
347
+ method: z
348
+ .enum(["GET", "POST", "PUT", "DELETE", "PATCH"])
349
+ .describe("HTTP method"),
350
+ path: z.string().describe("API path template"),
351
+ params: z
352
+ .record(z.unknown())
353
+ .optional()
354
+ .describe("Path and query parameters"),
355
+ body: z
356
+ .record(z.unknown())
357
+ .optional()
358
+ .describe("Request body for POST/PUT/PATCH"),
359
+ query: z
360
+ .string()
361
+ .describe("GraphQL selection query (use field names from call_api schema)"),
362
+ headers: z
363
+ .record(z.string())
364
+ .optional()
365
+ .describe("Additional HTTP headers for this request"),
366
+ }))
367
+ .min(1)
368
+ .max(10)
369
+ .describe("Array of requests to execute concurrently (1-10)"),
370
+ }, async ({ requests }) => {
371
+ try {
372
+ const settled = await Promise.allSettled(requests.map(async (req) => {
373
+ const rawData = await callApi(config, req.method, req.path, req.params, req.body, req.headers, "none");
374
+ const endpoint = apiIndex.getEndpoint(req.method, req.path);
375
+ const schema = getOrBuildSchema(rawData, req.method, req.path, endpoint?.requestBodySchema);
376
+ const queryResult = await executeQuery(schema, rawData, req.query);
377
+ return { method: req.method, path: req.path, data: queryResult };
378
+ }));
379
+ const results = settled.map((outcome, i) => {
380
+ if (outcome.status === "fulfilled") {
381
+ return outcome.value;
382
+ }
383
+ const message = outcome.reason instanceof Error
384
+ ? outcome.reason.message
385
+ : String(outcome.reason);
386
+ return {
387
+ method: requests[i].method,
388
+ path: requests[i].path,
389
+ error: message,
390
+ };
391
+ });
392
+ return {
393
+ content: [
394
+ { type: "text", text: JSON.stringify(results, null, 2) },
395
+ ],
396
+ };
397
+ }
398
+ catch (error) {
399
+ const message = error instanceof Error ? error.message : String(error);
400
+ return {
401
+ content: [
402
+ { type: "text", text: JSON.stringify({ error: message }) },
403
+ ],
404
+ isError: true,
405
+ };
406
+ }
407
+ });
255
408
  async function main() {
256
409
  const transport = new StdioServerTransport();
257
410
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anyapi-mcp-server",
3
- "version": "1.1.4",
3
+ "version": "1.2.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",