anyapi-mcp-server 1.5.0 → 1.6.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 +3 -1
- package/build/api-client.js +8 -7
- package/build/api-index.js +38 -18
- package/build/config.js +5 -5
- package/build/error-context.js +179 -0
- package/build/index.js +36 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@ Works with services like **Datadog**, **PostHog**, **Metabase**, **Cloudflare**,
|
|
|
28
28
|
- **Concurrent batch queries** — `batch_query` fetches data from up to 10 endpoints in parallel, returning all results in one tool call
|
|
29
29
|
- **Per-request headers** — override default headers on individual `call_api`/`query_api`/`batch_query` calls
|
|
30
30
|
- **Environment variable interpolation** — use `${ENV_VAR}` in base URLs and headers
|
|
31
|
+
- **Rich error context** — API errors return structured messages (parses RFC 7807, `{ error: { message, code } }`, `{ errors: [...] }`, and more), status-specific suggestions (e.g. "Authentication required" for 401), and relevant spec info (required parameters, request body schema) for 400/422 errors so the LLM can self-correct
|
|
31
32
|
- **Request logging** — optional NDJSON request/response log with sensitive header masking
|
|
32
33
|
|
|
33
34
|
[](https://www.npmjs.com/package/anyapi-mcp-server)
|
|
@@ -229,7 +230,7 @@ Fetch data from multiple endpoints concurrently in a single tool call.
|
|
|
229
230
|
- Accepts an array of 1–10 requests, each with `method`, `path`, `params`, `body`, `query`, and optional `headers`
|
|
230
231
|
- All requests execute in parallel via `Promise.allSettled` — one failure does not affect the others
|
|
231
232
|
- Each request follows the `query_api` flow: HTTP fetch → schema inference → GraphQL field selection
|
|
232
|
-
- Returns an array of results: `{ method, path, data, shapeHash }` on success or
|
|
233
|
+
- Returns an array of results: `{ method, path, data, shapeHash }` on success or a structured error with status, suggestion, and spec context on failure
|
|
233
234
|
- Run `call_api` first on each endpoint to discover the schema field names
|
|
234
235
|
|
|
235
236
|
## Workflow
|
|
@@ -267,6 +268,7 @@ OpenAPI/Postman spec
|
|
|
267
268
|
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
269
|
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
|
|
269
270
|
4. Write operations (POST/PUT/DELETE/PATCH) with OpenAPI request body schemas get a Mutation type with typed `GraphQLInputObjectType` inputs
|
|
271
|
+
5. When an API call fails, the error response includes a parsed error message (extracted from common formats like RFC 7807 Problem Details, `{ error: { message } }`, GraphQL-style `{ errors: [...] }`), the HTTP status code, a status-specific suggestion for what to try next, and — for validation errors (400/422) — the full parameter list and request body schema from the spec
|
|
270
272
|
|
|
271
273
|
## Supported Spec Formats
|
|
272
274
|
|
package/build/api-client.js
CHANGED
|
@@ -2,6 +2,7 @@ import { withRetry, RetryableError, isRetryableStatus } from "./retry.js";
|
|
|
2
2
|
import { buildCacheKey, consumeCached, setCache } from "./response-cache.js";
|
|
3
3
|
import { logEntry, isLoggingEnabled } from "./logger.js";
|
|
4
4
|
import { parseResponse } from "./response-parser.js";
|
|
5
|
+
import { ApiError } from "./error-context.js";
|
|
5
6
|
const TIMEOUT_MS = 30_000;
|
|
6
7
|
function interpolatePath(pathTemplate, params) {
|
|
7
8
|
const remaining = { ...params };
|
|
@@ -34,7 +35,7 @@ export async function callApi(config, method, pathTemplate, params, body, extraH
|
|
|
34
35
|
// --- URL construction ---
|
|
35
36
|
const { url: interpolatedPath, remainingParams } = interpolatePath(pathTemplate, params ?? {});
|
|
36
37
|
let fullUrl = `${config.baseUrl}${interpolatedPath}`;
|
|
37
|
-
if (
|
|
38
|
+
if (Object.keys(remainingParams).length > 0) {
|
|
38
39
|
const qs = new URLSearchParams();
|
|
39
40
|
for (const [k, v] of Object.entries(remainingParams)) {
|
|
40
41
|
if (v !== undefined && v !== null) {
|
|
@@ -65,12 +66,12 @@ export async function callApi(config, method, pathTemplate, params, body, extraH
|
|
|
65
66
|
const response = await fetch(fullUrl, fetchOptions);
|
|
66
67
|
const durationMs = Date.now() - startTime;
|
|
67
68
|
const bodyText = await response.text();
|
|
69
|
+
const responseHeaders = {};
|
|
70
|
+
response.headers.forEach((v, k) => {
|
|
71
|
+
responseHeaders[k] = v;
|
|
72
|
+
});
|
|
68
73
|
// Log request/response
|
|
69
74
|
if (isLoggingEnabled()) {
|
|
70
|
-
const responseHeaders = {};
|
|
71
|
-
response.headers.forEach((v, k) => {
|
|
72
|
-
responseHeaders[k] = v;
|
|
73
|
-
});
|
|
74
75
|
await logEntry({
|
|
75
76
|
timestamp: new Date().toISOString(),
|
|
76
77
|
method,
|
|
@@ -84,8 +85,8 @@ export async function callApi(config, method, pathTemplate, params, body, extraH
|
|
|
84
85
|
});
|
|
85
86
|
}
|
|
86
87
|
if (!response.ok) {
|
|
87
|
-
const msg = `API error ${response.status} ${response.statusText}: ${bodyText}`;
|
|
88
88
|
if (isRetryableStatus(response.status)) {
|
|
89
|
+
const msg = `API error ${response.status} ${response.statusText}: ${bodyText}`;
|
|
89
90
|
let retryAfterMs;
|
|
90
91
|
const retryAfter = response.headers.get("retry-after");
|
|
91
92
|
if (retryAfter) {
|
|
@@ -95,7 +96,7 @@ export async function callApi(config, method, pathTemplate, params, body, extraH
|
|
|
95
96
|
}
|
|
96
97
|
throw new RetryableError(msg, response.status, retryAfterMs);
|
|
97
98
|
}
|
|
98
|
-
throw new
|
|
99
|
+
throw new ApiError(`API error ${response.status} ${response.statusText}`, response.status, response.statusText, bodyText, responseHeaders);
|
|
99
100
|
}
|
|
100
101
|
return parseResponse(response.headers.get("content-type"), bodyText);
|
|
101
102
|
}
|
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,
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rich API error carrying status, raw body, and response headers
|
|
3
|
+
* for structured error reporting.
|
|
4
|
+
*/
|
|
5
|
+
export class ApiError extends Error {
|
|
6
|
+
status;
|
|
7
|
+
statusText;
|
|
8
|
+
bodyText;
|
|
9
|
+
responseHeaders;
|
|
10
|
+
constructor(message, status, statusText, bodyText, responseHeaders = {}) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.status = status;
|
|
13
|
+
this.statusText = statusText;
|
|
14
|
+
this.bodyText = bodyText;
|
|
15
|
+
this.responseHeaders = responseHeaders;
|
|
16
|
+
this.name = "ApiError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Extract a human-readable error message from common API error response formats:
|
|
21
|
+
* - RFC 7807 Problem Details: { type, title, status, detail }
|
|
22
|
+
* - Stripe/generic: { error: { message, code } }
|
|
23
|
+
* - Simple: { error: "message" } or { message: "..." }
|
|
24
|
+
* - GraphQL-style: { errors: [{ message }] }
|
|
25
|
+
* - SOAP/Apigee: { fault: { faultstring } }
|
|
26
|
+
*/
|
|
27
|
+
export function extractErrorMessage(bodyText) {
|
|
28
|
+
if (!bodyText.trim())
|
|
29
|
+
return undefined;
|
|
30
|
+
let parsed;
|
|
31
|
+
try {
|
|
32
|
+
parsed = JSON.parse(bodyText);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
38
|
+
return undefined;
|
|
39
|
+
const obj = parsed;
|
|
40
|
+
// RFC 7807 Problem Details
|
|
41
|
+
if (typeof obj.detail === "string") {
|
|
42
|
+
const title = typeof obj.title === "string" ? obj.title : undefined;
|
|
43
|
+
return title ? `${title}: ${obj.detail}` : obj.detail;
|
|
44
|
+
}
|
|
45
|
+
// { error: { message, code? } }
|
|
46
|
+
if (obj.error && typeof obj.error === "object") {
|
|
47
|
+
const errObj = obj.error;
|
|
48
|
+
if (typeof errObj.message === "string") {
|
|
49
|
+
const code = typeof errObj.code === "string" ? ` (${errObj.code})` : "";
|
|
50
|
+
return `${errObj.message}${code}`;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// { error: "message" }
|
|
54
|
+
if (typeof obj.error === "string") {
|
|
55
|
+
return obj.error;
|
|
56
|
+
}
|
|
57
|
+
// { message: "..." }
|
|
58
|
+
if (typeof obj.message === "string") {
|
|
59
|
+
return obj.message;
|
|
60
|
+
}
|
|
61
|
+
// { errors: [{ message }] }
|
|
62
|
+
if (Array.isArray(obj.errors)) {
|
|
63
|
+
const messages = obj.errors
|
|
64
|
+
.filter((e) => typeof e === "object" &&
|
|
65
|
+
e !== null &&
|
|
66
|
+
typeof e.message === "string")
|
|
67
|
+
.map((e) => e.message);
|
|
68
|
+
if (messages.length > 0)
|
|
69
|
+
return messages.join("; ");
|
|
70
|
+
}
|
|
71
|
+
// { fault: { faultstring } }
|
|
72
|
+
if (obj.fault && typeof obj.fault === "object") {
|
|
73
|
+
const fault = obj.fault;
|
|
74
|
+
if (typeof fault.faultstring === "string")
|
|
75
|
+
return fault.faultstring;
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
function getSuggestion(status, endpoint) {
|
|
80
|
+
switch (status) {
|
|
81
|
+
case 400: {
|
|
82
|
+
let msg = "Bad request. Check that all required parameters are provided and the request body matches the expected schema.";
|
|
83
|
+
if (endpoint) {
|
|
84
|
+
const required = endpoint.parameters.filter((p) => p.required);
|
|
85
|
+
if (required.length > 0) {
|
|
86
|
+
msg += ` Required parameters: ${required.map((p) => `${p.name} (${p.in})`).join(", ")}.`;
|
|
87
|
+
}
|
|
88
|
+
if (endpoint.hasRequestBody && endpoint.requestBodySchema) {
|
|
89
|
+
const requiredFields = Object.entries(endpoint.requestBodySchema.properties)
|
|
90
|
+
.filter(([, p]) => p.required)
|
|
91
|
+
.map(([name]) => name);
|
|
92
|
+
if (requiredFields.length > 0) {
|
|
93
|
+
msg += ` Required body fields: ${requiredFields.join(", ")}.`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return msg;
|
|
98
|
+
}
|
|
99
|
+
case 401:
|
|
100
|
+
return "Authentication required. Provide credentials via --header (e.g. --header \"Authorization: Bearer <token>\") or per-request headers parameter.";
|
|
101
|
+
case 403:
|
|
102
|
+
return "Forbidden. Your credentials don't have permission for this operation. Verify your API key or token has the required scopes.";
|
|
103
|
+
case 404: {
|
|
104
|
+
let msg = "Resource not found. Verify the path and parameter values are correct.";
|
|
105
|
+
if (endpoint) {
|
|
106
|
+
const pathParams = endpoint.parameters.filter((p) => p.in === "path");
|
|
107
|
+
if (pathParams.length > 0) {
|
|
108
|
+
msg += ` Check path parameters: ${pathParams.map((p) => p.name).join(", ")}.`;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
msg += " Use list_api to confirm available endpoints.";
|
|
112
|
+
return msg;
|
|
113
|
+
}
|
|
114
|
+
case 405:
|
|
115
|
+
return "Method not allowed for this endpoint. Use list_api to check which HTTP methods are supported.";
|
|
116
|
+
case 409:
|
|
117
|
+
return "Conflict. The resource may already exist or be in a state that prevents this operation.";
|
|
118
|
+
case 415:
|
|
119
|
+
return "Unsupported media type. The API may expect a different Content-Type header.";
|
|
120
|
+
case 422: {
|
|
121
|
+
let msg = "Validation failed. Check that request body fields match the expected types and formats.";
|
|
122
|
+
if (endpoint?.requestBodySchema) {
|
|
123
|
+
const props = Object.entries(endpoint.requestBodySchema.properties)
|
|
124
|
+
.map(([name, p]) => `${name}: ${p.type}${p.required ? " (required)" : ""}`)
|
|
125
|
+
.join(", ");
|
|
126
|
+
if (props)
|
|
127
|
+
msg += ` Expected fields: ${props}.`;
|
|
128
|
+
}
|
|
129
|
+
return msg;
|
|
130
|
+
}
|
|
131
|
+
case 429:
|
|
132
|
+
return "Rate limit exceeded (retries exhausted). Wait before trying again or reduce request frequency.";
|
|
133
|
+
default:
|
|
134
|
+
if (status >= 500) {
|
|
135
|
+
return "Server error. This is likely a temporary issue with the API. Try again later.";
|
|
136
|
+
}
|
|
137
|
+
return "Check the API documentation for this endpoint using explain_api.";
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const MAX_RAW_BODY = 500;
|
|
141
|
+
/**
|
|
142
|
+
* Build a rich error response object from an API error or exhausted retry error.
|
|
143
|
+
*/
|
|
144
|
+
export function buildErrorContext(error, method, path, endpoint) {
|
|
145
|
+
const isApiError = error instanceof ApiError;
|
|
146
|
+
const status = isApiError ? error.status : error.status;
|
|
147
|
+
const statusText = isApiError ? error.statusText : "";
|
|
148
|
+
const bodyText = isApiError ? error.bodyText : "";
|
|
149
|
+
const extracted = bodyText ? extractErrorMessage(bodyText) : undefined;
|
|
150
|
+
const result = {
|
|
151
|
+
error: extracted ?? error.message,
|
|
152
|
+
status,
|
|
153
|
+
...(statusText ? { statusText } : {}),
|
|
154
|
+
endpoint: `${method} ${path}`,
|
|
155
|
+
suggestion: getSuggestion(status, endpoint),
|
|
156
|
+
};
|
|
157
|
+
// Include raw body snippet when structured extraction failed
|
|
158
|
+
if (!extracted && bodyText.length > 0) {
|
|
159
|
+
result.rawBody =
|
|
160
|
+
bodyText.length > MAX_RAW_BODY
|
|
161
|
+
? bodyText.slice(0, MAX_RAW_BODY) + "..."
|
|
162
|
+
: bodyText;
|
|
163
|
+
}
|
|
164
|
+
// Include spec info for validation-related errors
|
|
165
|
+
if ((status === 400 || status === 422) && endpoint) {
|
|
166
|
+
if (endpoint.parameters.length > 0) {
|
|
167
|
+
result.specParameters = endpoint.parameters.map((p) => ({
|
|
168
|
+
name: p.name,
|
|
169
|
+
in: p.in,
|
|
170
|
+
required: p.required,
|
|
171
|
+
...(p.description ? { description: p.description } : {}),
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
if (endpoint.hasRequestBody && endpoint.requestBodySchema) {
|
|
175
|
+
result.specRequestBody = endpoint.requestBodySchema;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
package/build/index.js
CHANGED
|
@@ -8,36 +8,50 @@ import { callApi } from "./api-client.js";
|
|
|
8
8
|
import { initLogger } from "./logger.js";
|
|
9
9
|
import { generateSuggestions } from "./query-suggestions.js";
|
|
10
10
|
import { getOrBuildSchema, executeQuery, schemaToSDL, truncateIfArray, computeShapeHash, } from "./graphql-schema.js";
|
|
11
|
+
import { ApiError, buildErrorContext } from "./error-context.js";
|
|
12
|
+
import { RetryableError } from "./retry.js";
|
|
11
13
|
const WRITE_METHODS = new Set(["POST", "PUT", "DELETE", "PATCH"]);
|
|
12
14
|
const config = await loadConfig();
|
|
13
15
|
initLogger(config.logPath ?? null);
|
|
14
|
-
const apiIndex = new ApiIndex(config.
|
|
16
|
+
const apiIndex = new ApiIndex(config.specs);
|
|
17
|
+
function formatToolError(error, method, path) {
|
|
18
|
+
if ((error instanceof ApiError || error instanceof RetryableError) && method && path) {
|
|
19
|
+
const endpoint = apiIndex.getEndpoint(method, path);
|
|
20
|
+
const context = buildErrorContext(error, method, path, endpoint);
|
|
21
|
+
return {
|
|
22
|
+
content: [{ type: "text", text: JSON.stringify(context, null, 2) }],
|
|
23
|
+
isError: true,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
27
|
+
return {
|
|
28
|
+
content: [{ type: "text", text: JSON.stringify({ error: message }) }],
|
|
29
|
+
isError: true,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
15
32
|
const server = new McpServer({
|
|
16
33
|
name: config.name,
|
|
17
34
|
version: "1.2.1",
|
|
18
35
|
});
|
|
19
36
|
// --- Tool 1: list_api ---
|
|
20
37
|
server.tool("list_api", `List available ${config.name} API endpoints. ` +
|
|
21
|
-
"Call with no arguments to see all
|
|
22
|
-
"Provide 'category' to
|
|
23
|
-
"Provide 'search' to search across paths and
|
|
24
|
-
"The correct query format is auto-selected based on mode. " +
|
|
25
|
-
"You can optionally override with a custom 'query' parameter. " +
|
|
38
|
+
"Call with no arguments to see all endpoints. " +
|
|
39
|
+
"Provide 'category' to filter by tag. " +
|
|
40
|
+
"Provide 'search' to search across paths and summaries (supports regex). " +
|
|
26
41
|
"Results are paginated with limit (default 20) and offset.", {
|
|
27
42
|
category: z
|
|
28
43
|
.string()
|
|
29
44
|
.optional()
|
|
30
|
-
.describe("Tag/category to filter by.
|
|
45
|
+
.describe("Tag/category to filter by. Case-insensitive."),
|
|
31
46
|
search: z
|
|
32
47
|
.string()
|
|
33
48
|
.optional()
|
|
34
|
-
.describe("Search keyword across endpoint paths and
|
|
49
|
+
.describe("Search keyword or regex pattern across endpoint paths and summaries"),
|
|
35
50
|
query: z
|
|
36
51
|
.string()
|
|
37
52
|
.optional()
|
|
38
|
-
.describe("
|
|
39
|
-
"
|
|
40
|
-
"Endpoints (with category/search): '{ items { method path summary tag parameters { name in required description } } _count }'"),
|
|
53
|
+
.describe("GraphQL selection query. Default: '{ items { method path summary } _count }'. " +
|
|
54
|
+
"Available fields: method, path, summary, tag, parameters { name in required description }"),
|
|
41
55
|
limit: z
|
|
42
56
|
.number()
|
|
43
57
|
.int()
|
|
@@ -52,7 +66,6 @@ server.tool("list_api", `List available ${config.name} API endpoints. ` +
|
|
|
52
66
|
.describe("Items to skip (default: 0)"),
|
|
53
67
|
}, async ({ category, search, query, limit, offset }) => {
|
|
54
68
|
try {
|
|
55
|
-
const isEndpointMode = !!(search || category);
|
|
56
69
|
let data;
|
|
57
70
|
if (search) {
|
|
58
71
|
data = apiIndex.searchAll(search);
|
|
@@ -61,7 +74,7 @@ server.tool("list_api", `List available ${config.name} API endpoints. ` +
|
|
|
61
74
|
data = apiIndex.listAllByCategory(category);
|
|
62
75
|
}
|
|
63
76
|
else {
|
|
64
|
-
data = apiIndex.
|
|
77
|
+
data = apiIndex.listAll();
|
|
65
78
|
}
|
|
66
79
|
// Empty results — return directly to avoid GraphQL schema errors on empty arrays
|
|
67
80
|
if (data.length === 0) {
|
|
@@ -71,11 +84,9 @@ server.tool("list_api", `List available ${config.name} API endpoints. ` +
|
|
|
71
84
|
],
|
|
72
85
|
};
|
|
73
86
|
}
|
|
74
|
-
const defaultQuery =
|
|
75
|
-
? "{ items { method path summary tag parameters { name in required description } } _count }"
|
|
76
|
-
: "{ items { tag endpointCount } _count }";
|
|
87
|
+
const defaultQuery = "{ items { method path summary } _count }";
|
|
77
88
|
const effectiveQuery = query ?? defaultQuery;
|
|
78
|
-
const { schema } = getOrBuildSchema(data, "LIST", category ?? search ?? "
|
|
89
|
+
const { schema } = getOrBuildSchema(data, "LIST", category ?? search ?? "_all");
|
|
79
90
|
const { data: sliced, truncated, total } = truncateIfArray(data, limit ?? 20, offset);
|
|
80
91
|
const queryResult = await executeQuery(schema, sliced, effectiveQuery);
|
|
81
92
|
if (truncated && typeof queryResult === "object" && queryResult !== null) {
|
|
@@ -177,13 +188,7 @@ server.tool("call_api", `Inspect a ${config.name} API endpoint. Makes a real req
|
|
|
177
188
|
};
|
|
178
189
|
}
|
|
179
190
|
catch (error) {
|
|
180
|
-
|
|
181
|
-
return {
|
|
182
|
-
content: [
|
|
183
|
-
{ type: "text", text: JSON.stringify({ error: message }) },
|
|
184
|
-
],
|
|
185
|
-
isError: true,
|
|
186
|
-
};
|
|
191
|
+
return formatToolError(error, method, path);
|
|
187
192
|
}
|
|
188
193
|
});
|
|
189
194
|
// --- Tool 3: query_api ---
|
|
@@ -261,13 +266,7 @@ server.tool("query_api", `Fetch data from a ${config.name} API endpoint, returni
|
|
|
261
266
|
};
|
|
262
267
|
}
|
|
263
268
|
catch (error) {
|
|
264
|
-
|
|
265
|
-
return {
|
|
266
|
-
content: [
|
|
267
|
-
{ type: "text", text: JSON.stringify({ error: message }) },
|
|
268
|
-
],
|
|
269
|
-
isError: true,
|
|
270
|
-
};
|
|
269
|
+
return formatToolError(error, method, path);
|
|
271
270
|
}
|
|
272
271
|
});
|
|
273
272
|
// --- Tool 4: explain_api ---
|
|
@@ -407,9 +406,12 @@ server.tool("batch_query", `Fetch data from multiple ${config.name} API endpoint
|
|
|
407
406
|
if (outcome.status === "fulfilled") {
|
|
408
407
|
return outcome.value;
|
|
409
408
|
}
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
409
|
+
const reason = outcome.reason;
|
|
410
|
+
if (reason instanceof ApiError || reason instanceof RetryableError) {
|
|
411
|
+
const endpoint = apiIndex.getEndpoint(requests[i].method, requests[i].path);
|
|
412
|
+
return buildErrorContext(reason, requests[i].method, requests[i].path, endpoint);
|
|
413
|
+
}
|
|
414
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
413
415
|
return {
|
|
414
416
|
method: requests[i].method,
|
|
415
417
|
path: requests[i].path,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anyapi-mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
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",
|