anyapi-mcp-server 2.0.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anyapi-mcp-server",
3
- "version": "2.0.0",
3
+ "version": "2.0.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",
@@ -1,49 +0,0 @@
1
- const DEFAULT_TTL_MS = 30_000;
2
- const cache = new Map();
3
- export function buildCacheKey(method, pathTemplate, params, body, extraHeaders) {
4
- const parts = [method, pathTemplate];
5
- if (params && Object.keys(params).length > 0) {
6
- parts.push(JSON.stringify(params, Object.keys(params).sort()));
7
- }
8
- if (body && Object.keys(body).length > 0) {
9
- parts.push(JSON.stringify(body, Object.keys(body).sort()));
10
- }
11
- if (extraHeaders && Object.keys(extraHeaders).length > 0) {
12
- parts.push(JSON.stringify(extraHeaders, Object.keys(extraHeaders).sort()));
13
- }
14
- return parts.join("|");
15
- }
16
- export function getCached(key) {
17
- const entry = cache.get(key);
18
- if (!entry)
19
- return undefined;
20
- if (Date.now() > entry.expiresAt) {
21
- cache.delete(key);
22
- return undefined;
23
- }
24
- return entry.data;
25
- }
26
- /** Read and immediately evict a cache entry (one-shot consumption). */
27
- export function consumeCached(key) {
28
- const entry = cache.get(key);
29
- if (!entry)
30
- return undefined;
31
- cache.delete(key);
32
- if (Date.now() > entry.expiresAt)
33
- return undefined;
34
- return entry.data;
35
- }
36
- export function setCache(key, data, ttlMs = DEFAULT_TTL_MS) {
37
- cache.set(key, { data, expiresAt: Date.now() + ttlMs });
38
- }
39
- export function evictExpired() {
40
- const now = Date.now();
41
- for (const [key, entry] of cache) {
42
- if (now > entry.expiresAt) {
43
- cache.delete(key);
44
- }
45
- }
46
- }
47
- export function clearCache() {
48
- cache.clear();
49
- }
@@ -1,204 +0,0 @@
1
- import { z } from "zod";
2
- import { WRITE_METHODS, formatToolError, attachRateLimit, shrinkageError, placeholderError, } from "./shared.js";
3
- import { callApi } from "../api-client.js";
4
- import { getOrBuildSchema, schemaToSDL, computeShapeHash, collectJsonFields, computeFieldCosts, collectArrayLengths, } from "../graphql-schema.js";
5
- import { generateSuggestions } from "../query-suggestions.js";
6
- import { isNonJsonResult } from "../response-parser.js";
7
- import { detectPagination, PAGINATION_PARAM_NAMES } from "../pagination.js";
8
- import { storeResponse, loadResponse } from "../data-cache.js";
9
- import { resolveBody } from "../body-file.js";
10
- import { detectPlaceholders } from "../body-validation.js";
11
- import { createBackup } from "../pre-write-backup.js";
12
- import { detectArrayShrinkage } from "../write-safety.js";
13
- export function registerCallApi({ server, config, apiIndex }) {
14
- server.tool("call_api", `Inspect a ${config.name} API endpoint. Makes a real request and returns ONLY the ` +
15
- "inferred GraphQL schema (SDL) showing all available fields and their types. " +
16
- "No response data is returned — use query_api to fetch actual data. " +
17
- "IMPORTANT: Read the returned schema carefully. The root field names in the schema " +
18
- "are what you must use in query_api — do NOT assume generic names like 'items'. " +
19
- "For example, if the schema shows 'products: [Product]', query as '{ products { id name } }', not '{ items { id name } }'. " +
20
- "Also returns accepted parameters (name, location, required) from the API spec, " +
21
- "and suggestedQueries with ready-to-use GraphQL queries. " +
22
- "Returns a dataKey — pass it to query_api to reuse the cached response (zero HTTP calls). " +
23
- "Use list_api first to discover endpoints.", {
24
- method: z
25
- .enum(["GET", "POST", "PUT", "DELETE", "PATCH"])
26
- .describe("HTTP method"),
27
- path: z
28
- .string()
29
- .describe("API path template (e.g. '/api/card/{id}'). Use list_api to discover paths."),
30
- params: z
31
- .record(z.unknown())
32
- .optional()
33
- .describe("Path and query parameters as key-value pairs. " +
34
- "Path params like {id} are interpolated; remaining become query string for GET."),
35
- body: z
36
- .record(z.unknown())
37
- .optional()
38
- .describe("Request body for POST/PUT/PATCH"),
39
- bodyFile: z
40
- .string()
41
- .optional()
42
- .describe("Absolute path to a JSON file to use as request body. " +
43
- "Mutually exclusive with 'body'. Use for large payloads that cannot be inlined."),
44
- headers: z
45
- .record(z.string())
46
- .optional()
47
- .describe("Additional HTTP headers for this request (e.g. { \"Authorization\": \"Bearer <token>\" }). " +
48
- "Overrides default --header values."),
49
- skipBackup: z
50
- .boolean()
51
- .optional()
52
- .describe("Skip the automatic pre-write backup for PUT/PATCH requests. " +
53
- "Default: false (backup is created automatically)."),
54
- }, async ({ method, path, params, body, bodyFile, headers, skipBackup }) => {
55
- try {
56
- // Resolve body from inline or file
57
- let resolvedBody;
58
- try {
59
- resolvedBody = resolveBody(body, bodyFile);
60
- }
61
- catch (err) {
62
- return formatToolError(err, apiIndex);
63
- }
64
- // Placeholder detection for write methods (except DELETE)
65
- if (resolvedBody && WRITE_METHODS.has(method) && method !== "DELETE") {
66
- const endpoint = apiIndex.getEndpoint(method, path);
67
- const warnings = detectPlaceholders(resolvedBody, endpoint?.requestBodySchema);
68
- if (warnings.length > 0)
69
- return placeholderError(warnings);
70
- }
71
- // Pre-write backup for PUT/PATCH
72
- let backupDataKey;
73
- if ((method === "PATCH" || method === "PUT") && !skipBackup) {
74
- backupDataKey = await createBackup(config, method, path, params, headers);
75
- }
76
- // Array shrinkage detection: compare body against backup
77
- if (backupDataKey && resolvedBody) {
78
- const backupEntry = loadResponse(backupDataKey);
79
- if (backupEntry) {
80
- const shrinkWarnings = detectArrayShrinkage(resolvedBody, backupEntry.data);
81
- if (shrinkWarnings.length > 0)
82
- return shrinkageError(shrinkWarnings, { backupDataKey });
83
- }
84
- }
85
- const { data, responseHeaders: respHeaders } = await callApi(config, method, path, params, resolvedBody, headers);
86
- const dataKey = storeResponse(method, path, data, respHeaders);
87
- // Non-JSON response — skip GraphQL layer, return raw parsed data
88
- if (isNonJsonResult(data)) {
89
- return {
90
- content: [
91
- { type: "text", text: JSON.stringify({
92
- rawResponse: data,
93
- responseHeaders: respHeaders,
94
- dataKey,
95
- hint: "This endpoint returned a non-JSON response. The raw parsed content is shown above. " +
96
- "GraphQL schema inference is not available for non-JSON responses — use the data directly.",
97
- }, null, 2) },
98
- ],
99
- };
100
- }
101
- const endpoint = apiIndex.getEndpoint(method, path);
102
- const bodyHash = WRITE_METHODS.has(method) && resolvedBody ? computeShapeHash(resolvedBody) : undefined;
103
- const { schema, shapeHash } = getOrBuildSchema(data, method, path, endpoint?.requestBodySchema, bodyHash);
104
- const sdl = schemaToSDL(schema);
105
- const result = { graphqlSchema: sdl, shapeHash, responseHeaders: respHeaders };
106
- if (dataKey)
107
- result.dataKey = dataKey;
108
- if (bodyHash)
109
- result.bodyHash = bodyHash;
110
- if (backupDataKey) {
111
- result.backupDataKey = backupDataKey;
112
- result.backupHint = "Pre-write snapshot stored. Use query_api with this dataKey to retrieve original data if needed.";
113
- }
114
- attachRateLimit(result, respHeaders);
115
- if (endpoint && endpoint.parameters.length > 0) {
116
- result.parameters = endpoint.parameters.map((p) => ({
117
- name: p.name,
118
- in: p.in,
119
- required: p.required,
120
- ...(p.description ? { description: p.description } : {}),
121
- ...(p.in === "query" && PAGINATION_PARAM_NAMES.has(p.name) ? { pagination: true } : {}),
122
- }));
123
- const paginationParams = endpoint.parameters
124
- .filter((p) => p.in === "query" && PAGINATION_PARAM_NAMES.has(p.name))
125
- .map((p) => p.name);
126
- if (paginationParams.length > 0) {
127
- result.paginationParams = paginationParams;
128
- }
129
- }
130
- // Smart query suggestions
131
- const suggestions = generateSuggestions(schema);
132
- if (suggestions.length > 0) {
133
- result.suggestedQueries = suggestions;
134
- }
135
- // Per-field token costs
136
- const fieldCosts = computeFieldCosts(data);
137
- result.fieldTokenCosts = fieldCosts;
138
- // Actual array lengths for truncation awareness
139
- const arrayLengths = collectArrayLengths(data);
140
- if (Object.keys(arrayLengths).length > 0) {
141
- result.fieldArrayLengths = arrayLengths;
142
- }
143
- // Budget examples
144
- const allFieldsCost = fieldCosts._total;
145
- if (Array.isArray(data) && data.length > 0 && fieldCosts._perItem) {
146
- const perItem = fieldCosts._perItem;
147
- result.budgetExamples = [
148
- `All fields: ~${perItem} tokens/item, ~${allFieldsCost} tokens total`,
149
- ];
150
- }
151
- else if (typeof data === "object" && data !== null) {
152
- result.budgetExamples = [
153
- `All fields: ~${allFieldsCost} tokens total`,
154
- ];
155
- }
156
- // Flag JSON scalar fields so the AI knows which fields are opaque
157
- const jsonFields = collectJsonFields(schema);
158
- if (jsonFields.length > 0) {
159
- result.jsonFields = jsonFields;
160
- result.jsonFieldsHint =
161
- "These fields contain heterogeneous or deeply nested data that cannot be queried " +
162
- "with GraphQL field selection. Query them as-is and parse the returned JSON directly.";
163
- }
164
- // Pagination detection from response data
165
- const pagination = detectPagination(data, endpoint?.parameters);
166
- if (pagination) {
167
- result._pagination = pagination;
168
- }
169
- const paginationParamsList = result.paginationParams ?? [];
170
- const paginationSuffix = paginationParamsList.length > 0
171
- ? ` This API supports pagination via: ${paginationParamsList.join(", ")}. Pass these inside params.`
172
- : "";
173
- if (Array.isArray(data)) {
174
- result.totalItems = data.length;
175
- result.hint =
176
- "Use query_api with field names from the schema above. " +
177
- "For raw arrays: '{ items { ... } _count }'. " +
178
- "For paginated APIs, pass limit/offset inside params (as query string parameters to the API), " +
179
- "NOT as top-level tool parameters. " +
180
- "Use fieldTokenCosts to understand per-field token costs and select fields wisely. " +
181
- "Responses over ~10k tokens require maxTokens (to truncate) or unlimited: true (for full data)." +
182
- paginationSuffix;
183
- }
184
- else {
185
- result.hint =
186
- "Use query_api with the exact root field names from the schema above (e.g. if schema shows " +
187
- "'products: [Product]', query as '{ products { id name } }' — do NOT use '{ items { ... } }'). " +
188
- "For paginated APIs, pass limit/offset inside params (as query string parameters to the API), " +
189
- "NOT as top-level tool parameters. " +
190
- "Use fieldTokenCosts to understand per-field token costs and select fields wisely. " +
191
- "Responses over ~10k tokens require maxTokens (to truncate) or unlimited: true (for full data)." +
192
- paginationSuffix;
193
- }
194
- return {
195
- content: [
196
- { type: "text", text: JSON.stringify(result, null, 2) },
197
- ],
198
- };
199
- }
200
- catch (error) {
201
- return formatToolError(error, apiIndex, method, path);
202
- }
203
- });
204
- }
@@ -1,86 +0,0 @@
1
- import { z } from "zod";
2
- export function registerExplainApi({ server, config, apiIndex }) {
3
- server.tool("explain_api", `Get detailed documentation for a ${config.name} API endpoint from the spec. ` +
4
- "Returns all available spec information — summary, description, parameters, " +
5
- "request body schema, response codes, deprecation status — without making any HTTP request. " +
6
- "Use list_api first to discover endpoints, then explain_api to understand them before calling.", {
7
- method: z
8
- .enum(["GET", "POST", "PUT", "DELETE", "PATCH"])
9
- .describe("HTTP method"),
10
- path: z
11
- .string()
12
- .describe("API path template (e.g. '/api/card/{id}')"),
13
- }, async ({ method, path }) => {
14
- try {
15
- const endpoint = apiIndex.getEndpoint(method, path);
16
- if (!endpoint) {
17
- return {
18
- content: [
19
- {
20
- type: "text",
21
- text: JSON.stringify({
22
- error: `Endpoint not found: ${method} ${path}`,
23
- hint: "Use list_api to discover available endpoints.",
24
- }),
25
- },
26
- ],
27
- isError: true,
28
- };
29
- }
30
- const result = {
31
- method: endpoint.method,
32
- path: endpoint.path,
33
- summary: endpoint.summary,
34
- };
35
- if (endpoint.description) {
36
- result.description = endpoint.description;
37
- }
38
- if (endpoint.operationId) {
39
- result.operationId = endpoint.operationId;
40
- }
41
- if (endpoint.deprecated) {
42
- result.deprecated = true;
43
- }
44
- result.tag = endpoint.tag;
45
- if (endpoint.parameters.length > 0) {
46
- result.parameters = endpoint.parameters.map((p) => ({
47
- name: p.name,
48
- in: p.in,
49
- required: p.required,
50
- ...(p.description ? { description: p.description } : {}),
51
- }));
52
- }
53
- if (endpoint.hasRequestBody) {
54
- const bodyInfo = {};
55
- if (endpoint.requestBodyDescription) {
56
- bodyInfo.description = endpoint.requestBodyDescription;
57
- }
58
- if (endpoint.requestBodySchema) {
59
- bodyInfo.contentType = endpoint.requestBodySchema.contentType;
60
- bodyInfo.properties = endpoint.requestBodySchema.properties;
61
- }
62
- result.requestBody = bodyInfo;
63
- }
64
- if (endpoint.responses && endpoint.responses.length > 0) {
65
- result.responses = endpoint.responses;
66
- }
67
- if (endpoint.externalDocs) {
68
- result.externalDocs = endpoint.externalDocs;
69
- }
70
- return {
71
- content: [
72
- { type: "text", text: JSON.stringify(result, null, 2) },
73
- ],
74
- };
75
- }
76
- catch (error) {
77
- const message = error instanceof Error ? error.message : String(error);
78
- return {
79
- content: [
80
- { type: "text", text: JSON.stringify({ error: message }) },
81
- ],
82
- isError: true,
83
- };
84
- }
85
- });
86
- }