anyapi-mcp-server 1.2.1 → 1.4.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 +15 -15
- package/build/api-index.js +8 -5
- package/build/config.js +36 -6
- package/build/graphql-schema.js +105 -24
- package/build/index.js +9 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
# anyapi-mcp-server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
### If it has an API, you can MCP it.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
<img src="public/datadog.gif" alt="anyapi-mcp-server demo — Datadog API" width="1200" />
|
|
6
|
+
|
|
7
|
+
Traditional MCP servers hand-pick a handful of endpoints and call it a day — locking you into whatever subset someone decided was "enough." Why settle for a fraction of an API when you can have **all of it**?
|
|
8
|
+
|
|
9
|
+
`anyapi-mcp-server` is a universal [MCP](https://modelcontextprotocol.io) server that connects **any REST API** to AI assistants like Claude, Cursor, and other LLM-powered tools — just point it at an OpenAPI spec or Postman collection. Every endpoint the API provides becomes available instantly, with **GraphQL-style field selection** and automatic schema inference. No custom server code, no artificial limits.
|
|
10
|
+
|
|
11
|
+
Works with services like **Datadog**, **PostHog**, **Metabase**, **Cloudflare**, **Stripe**, **GitHub**, **Slack**, **Twilio**, **Shopify**, **HubSpot**, and anything else with a REST API — if it has an API, it just works.
|
|
6
12
|
|
|
7
13
|
## Features
|
|
8
14
|
|
|
9
|
-
- **Works with any REST API** — provide an OpenAPI (JSON/YAML) or Postman Collection v2.x spec
|
|
15
|
+
- **Works with any REST API** — provide an OpenAPI (JSON/YAML) or Postman Collection v2.x spec as a local file or HTTPS URL
|
|
16
|
+
- **Remote spec caching** — HTTPS spec URLs are fetched once and cached locally in ``~/.cache/anyapi-mcp/` (Linux/macOS) or `%LOCALAPPDATA%\anyapi-mcp\` (Windows)`
|
|
10
17
|
- **GraphQL-style queries** — select only the fields you need from API responses
|
|
11
18
|
- **Automatic schema inference** — calls an endpoint once, infers the response schema, then lets you query specific fields
|
|
12
19
|
- **Multi-sample merging** — samples up to 10 array elements to build richer schemas that capture fields missing from individual items
|
|
@@ -35,16 +42,9 @@ npm install -g anyapi-mcp-server
|
|
|
35
42
|
| Flag | Description |
|
|
36
43
|
|------|-------------|
|
|
37
44
|
| `--name` | Server name (e.g. `petstore`) |
|
|
38
|
-
|
|
39
|
-
### Either one of
|
|
40
|
-
|
|
41
|
-
| Flag | Description |
|
|
42
|
-
|------|-------------|
|
|
43
|
-
| `--spec` | Path to OpenAPI spec file (JSON or YAML) or Postman Collection |
|
|
45
|
+
| `--spec` | Path or HTTPS URL to OpenAPI spec (JSON or YAML) or Postman Collection. HTTPS URLs are cached locally in ``~/.cache/anyapi-mcp/` (Linux/macOS) or `%LOCALAPPDATA%\anyapi-mcp\` (Windows)`. Supports `${ENV_VAR}` interpolation. |
|
|
44
46
|
| `--base-url` | API base URL (e.g. `https://api.example.com`). Supports `${ENV_VAR}` interpolation. |
|
|
45
47
|
|
|
46
|
-
You can provide both `--spec` and `--base-url` together. If only `--spec` is given, the base URL is read from the spec. If only `--base-url` is given, endpoints are discovered dynamically.
|
|
47
|
-
|
|
48
48
|
### Optional arguments
|
|
49
49
|
|
|
50
50
|
| Flag | Description |
|
|
@@ -67,7 +67,7 @@ Add to your MCP configuration (e.g. `~/.cursor/mcp.json` or Claude Desktop confi
|
|
|
67
67
|
"-y",
|
|
68
68
|
"anyapi-mcp-server",
|
|
69
69
|
"--name", "cloudflare",
|
|
70
|
-
"--spec", "/
|
|
70
|
+
"--spec", "https://raw.githubusercontent.com/cloudflare/api-schemas/main/openapi.json",
|
|
71
71
|
"--base-url", "https://api.cloudflare.com/client/v4",
|
|
72
72
|
"--header", "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}"
|
|
73
73
|
],
|
|
@@ -90,8 +90,8 @@ Add to your MCP configuration (e.g. `~/.cursor/mcp.json` or Claude Desktop confi
|
|
|
90
90
|
"-y",
|
|
91
91
|
"anyapi-mcp-server",
|
|
92
92
|
"--name", "datadog",
|
|
93
|
-
"--spec", "/
|
|
94
|
-
"--base-url", "https://api.datadoghq.com
|
|
93
|
+
"--spec", "https://raw.githubusercontent.com/DataDog/datadog-api-client-typescript/master/.generator/schemas/v1/openapi.yaml",
|
|
94
|
+
"--base-url", "https://api.datadoghq.com",
|
|
95
95
|
"--header", "DD-API-KEY: ${DD_API_KEY}",
|
|
96
96
|
"--header", "DD-APPLICATION-KEY: ${DD_APP_KEY}"
|
|
97
97
|
],
|
|
@@ -259,7 +259,7 @@ OpenAPI/Postman spec
|
|
|
259
259
|
response data
|
|
260
260
|
```
|
|
261
261
|
|
|
262
|
-
1. The spec
|
|
262
|
+
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
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
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
|
|
265
265
|
4. Write operations (POST/PUT/DELETE/PATCH) with OpenAPI request body schemas get a Mutation type with typed `GraphQLInputObjectType` inputs
|
package/build/api-index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
1
|
import yaml from "js-yaml";
|
|
3
2
|
const PAGE_SIZE = 20;
|
|
4
3
|
const HTTP_METHODS = new Set(["get", "post", "put", "delete", "patch"]);
|
|
@@ -119,10 +118,14 @@ function postmanUrlToPath(url) {
|
|
|
119
118
|
export class ApiIndex {
|
|
120
119
|
byTag = new Map();
|
|
121
120
|
allEndpoints = [];
|
|
122
|
-
constructor(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
121
|
+
constructor(specContent) {
|
|
122
|
+
let parsed;
|
|
123
|
+
try {
|
|
124
|
+
parsed = JSON.parse(specContent);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
parsed = yaml.load(specContent);
|
|
128
|
+
}
|
|
126
129
|
if (isPostmanCollection(parsed)) {
|
|
127
130
|
this.parsePostman(parsed);
|
|
128
131
|
}
|
package/build/config.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
2
6
|
function getArg(flag) {
|
|
3
7
|
const idx = process.argv.indexOf(flag);
|
|
4
8
|
if (idx === -1 || !process.argv[idx + 1])
|
|
@@ -23,25 +27,51 @@ function interpolateEnv(value) {
|
|
|
23
27
|
return envValue;
|
|
24
28
|
});
|
|
25
29
|
}
|
|
26
|
-
const
|
|
30
|
+
const CACHE_DIR = process.platform === "win32"
|
|
31
|
+
? join(process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local"), "anyapi-mcp")
|
|
32
|
+
: join(homedir(), ".cache", "anyapi-mcp");
|
|
33
|
+
function isUrl(value) {
|
|
34
|
+
return /^https?:\/\//i.test(value);
|
|
35
|
+
}
|
|
36
|
+
async function loadSpec(specValue) {
|
|
37
|
+
if (!isUrl(specValue)) {
|
|
38
|
+
return readFileSync(resolve(specValue), "utf-8");
|
|
39
|
+
}
|
|
40
|
+
const hash = createHash("sha256").update(specValue).digest("hex");
|
|
41
|
+
const ext = /\.ya?ml$/i.test(specValue) ? ".yaml" : ".json";
|
|
42
|
+
const cachePath = join(CACHE_DIR, hash + ext);
|
|
43
|
+
if (existsSync(cachePath)) {
|
|
44
|
+
return readFileSync(cachePath, "utf-8");
|
|
45
|
+
}
|
|
46
|
+
const res = await fetch(specValue);
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
throw new Error(`Failed to fetch spec from ${specValue}: ${res.status} ${res.statusText}`);
|
|
49
|
+
}
|
|
50
|
+
const body = await res.text();
|
|
51
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
52
|
+
writeFileSync(cachePath, body, "utf-8");
|
|
53
|
+
return body;
|
|
54
|
+
}
|
|
55
|
+
const USAGE = `Usage: anyapi-mcp --name <name> --spec <path-or-url> --base-url <url> [--header "Key: Value"]...
|
|
27
56
|
|
|
28
57
|
Required:
|
|
29
58
|
--name Server name (e.g. "petstore")
|
|
30
|
-
--spec Path to OpenAPI spec
|
|
59
|
+
--spec Path or URL to OpenAPI spec (JSON or YAML). HTTPS URLs are cached locally.
|
|
31
60
|
--base-url API base URL (e.g. "https://api.example.com")
|
|
32
61
|
|
|
33
62
|
Optional:
|
|
34
63
|
--header HTTP header as "Key: Value" (repeatable)
|
|
35
64
|
Supports \${ENV_VAR} interpolation in values
|
|
36
65
|
--log Path to request/response log file (NDJSON format)`;
|
|
37
|
-
export function loadConfig() {
|
|
66
|
+
export async function loadConfig() {
|
|
38
67
|
const name = getArg("--name");
|
|
39
|
-
const
|
|
68
|
+
const specUrl = getArg("--spec");
|
|
40
69
|
const baseUrl = getArg("--base-url");
|
|
41
|
-
if (!name || !
|
|
70
|
+
if (!name || !specUrl || !baseUrl) {
|
|
42
71
|
console.error(USAGE);
|
|
43
72
|
process.exit(1);
|
|
44
73
|
}
|
|
74
|
+
const spec = await loadSpec(interpolateEnv(specUrl));
|
|
45
75
|
const headers = {};
|
|
46
76
|
for (const raw of getAllArgs("--header")) {
|
|
47
77
|
const colonIdx = raw.indexOf(":");
|
|
@@ -56,7 +86,7 @@ export function loadConfig() {
|
|
|
56
86
|
const logPath = getArg("--log");
|
|
57
87
|
return {
|
|
58
88
|
name,
|
|
59
|
-
spec
|
|
89
|
+
spec,
|
|
60
90
|
baseUrl: interpolateEnv(baseUrl).replace(/\/+$/, ""),
|
|
61
91
|
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
62
92
|
logPath: logPath ? resolve(logPath) : undefined,
|
package/build/graphql-schema.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
|
-
import { GraphQLSchema, GraphQLObjectType, GraphQLInputObjectType, GraphQLString, GraphQLInt, GraphQLFloat, GraphQLBoolean, GraphQLList, GraphQLNonNull, graphql as executeGraphQL, printSchema, } from "graphql";
|
|
1
|
+
import { GraphQLSchema, GraphQLObjectType, GraphQLInputObjectType, GraphQLScalarType, GraphQLString, GraphQLInt, GraphQLFloat, GraphQLBoolean, GraphQLList, GraphQLNonNull, graphql as executeGraphQL, printSchema, } from "graphql";
|
|
2
2
|
const DEFAULT_ARRAY_LIMIT = 50;
|
|
3
|
-
const MAX_SAMPLE_SIZE =
|
|
3
|
+
const MAX_SAMPLE_SIZE = 30;
|
|
4
|
+
const MAX_INFER_DEPTH = 4;
|
|
5
|
+
/**
|
|
6
|
+
* Custom scalar that passes arbitrary JSON values through as-is.
|
|
7
|
+
* Used for mixed-type arrays, type-conflicting fields, and deeply nested structures.
|
|
8
|
+
*/
|
|
9
|
+
const GraphQLJSON = new GraphQLScalarType({
|
|
10
|
+
name: "JSON",
|
|
11
|
+
description: "Arbitrary JSON value (mixed types, deep nesting, or heterogeneous structures)",
|
|
12
|
+
serialize: (value) => value,
|
|
13
|
+
parseValue: (value) => value,
|
|
14
|
+
});
|
|
4
15
|
// Schema cache keyed by "METHOD:/path/template"
|
|
5
16
|
const schemaCache = new Map();
|
|
6
17
|
/**
|
|
@@ -49,6 +60,30 @@ function deriveTypeName(method, pathTemplate) {
|
|
|
49
60
|
name = name.slice(1);
|
|
50
61
|
return name || "Unknown";
|
|
51
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Return the base type category of a value for mixed-type detection.
|
|
65
|
+
*/
|
|
66
|
+
function baseTypeOf(value) {
|
|
67
|
+
if (value === null || value === undefined)
|
|
68
|
+
return "null";
|
|
69
|
+
if (Array.isArray(value))
|
|
70
|
+
return "array";
|
|
71
|
+
return typeof value; // "string" | "number" | "boolean" | "object"
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Check if an array has mixed base types (e.g. string + number, or scalar + object).
|
|
75
|
+
* Returns true if the array should be treated as JSON scalar.
|
|
76
|
+
*/
|
|
77
|
+
function hasMixedTypes(arr) {
|
|
78
|
+
const types = new Set();
|
|
79
|
+
const sampleSize = Math.min(arr.length, MAX_SAMPLE_SIZE);
|
|
80
|
+
for (let i = 0; i < sampleSize; i++) {
|
|
81
|
+
const t = baseTypeOf(arr[i]);
|
|
82
|
+
if (t !== "null")
|
|
83
|
+
types.add(t);
|
|
84
|
+
}
|
|
85
|
+
return types.size > 1;
|
|
86
|
+
}
|
|
52
87
|
function inferScalarType(value) {
|
|
53
88
|
switch (typeof value) {
|
|
54
89
|
case "string":
|
|
@@ -65,32 +100,51 @@ function inferScalarType(value) {
|
|
|
65
100
|
* Merge multiple sample objects into a single "super-object" that contains
|
|
66
101
|
* every key seen across all samples. First non-null value wins for each key.
|
|
67
102
|
* Nested objects are merged recursively.
|
|
103
|
+
* Tracks fields where different samples have conflicting base types.
|
|
68
104
|
*/
|
|
69
105
|
function mergeSamples(items) {
|
|
70
106
|
const merged = {};
|
|
107
|
+
const conflicts = new Set();
|
|
108
|
+
const seenTypes = new Map(); // key → first base type
|
|
71
109
|
for (const item of items) {
|
|
72
110
|
for (const [key, value] of Object.entries(item)) {
|
|
111
|
+
if (value === null || value === undefined)
|
|
112
|
+
continue;
|
|
113
|
+
const valueType = baseTypeOf(value);
|
|
73
114
|
if (!(key in merged) || merged[key] === null || merged[key] === undefined) {
|
|
74
115
|
merged[key] = value;
|
|
116
|
+
if (!seenTypes.has(key))
|
|
117
|
+
seenTypes.set(key, valueType);
|
|
75
118
|
}
|
|
76
|
-
else
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
119
|
+
else {
|
|
120
|
+
const prevType = seenTypes.get(key);
|
|
121
|
+
if (prevType && prevType !== valueType) {
|
|
122
|
+
conflicts.add(key);
|
|
123
|
+
}
|
|
124
|
+
if (!conflicts.has(key) &&
|
|
125
|
+
typeof value === "object" && value !== null && !Array.isArray(value) &&
|
|
126
|
+
typeof merged[key] === "object" && merged[key] !== null && !Array.isArray(merged[key])) {
|
|
127
|
+
const sub = mergeSamples([
|
|
128
|
+
merged[key],
|
|
129
|
+
value,
|
|
130
|
+
]);
|
|
131
|
+
merged[key] = sub.merged;
|
|
132
|
+
for (const c of sub.conflicts)
|
|
133
|
+
conflicts.add(`${key}.${c}`);
|
|
134
|
+
}
|
|
135
|
+
else if (Array.isArray(value) && Array.isArray(merged[key])) {
|
|
136
|
+
if (merged[key].length === 0 && value.length > 0) {
|
|
137
|
+
merged[key] = value;
|
|
138
|
+
}
|
|
86
139
|
}
|
|
87
140
|
}
|
|
88
141
|
}
|
|
89
142
|
}
|
|
90
|
-
return merged;
|
|
143
|
+
return { merged, conflicts };
|
|
91
144
|
}
|
|
92
145
|
/**
|
|
93
146
|
* Sample and merge multiple array elements for richer type inference.
|
|
147
|
+
* Returns the merged object + conflict set, or null if no objects found.
|
|
94
148
|
*/
|
|
95
149
|
function mergeArraySamples(arr) {
|
|
96
150
|
const sampleSize = Math.min(arr.length, MAX_SAMPLE_SIZE);
|
|
@@ -105,22 +159,35 @@ function mergeArraySamples(arr) {
|
|
|
105
159
|
* Recursively infer a GraphQL type from a JSON value.
|
|
106
160
|
* For objects, creates a named GraphQLObjectType with explicit resolvers
|
|
107
161
|
* that map sanitized field names back to original JSON keys.
|
|
162
|
+
*
|
|
163
|
+
* Falls back to GraphQLJSON for:
|
|
164
|
+
* - Arrays with mixed element types (string + number + object)
|
|
165
|
+
* - Fields with conflicting types across samples
|
|
166
|
+
* - Values nested deeper than MAX_INFER_DEPTH
|
|
108
167
|
*/
|
|
109
|
-
function inferType(value, typeName, typeRegistry) {
|
|
168
|
+
function inferType(value, typeName, typeRegistry, conflicts, depth = 0) {
|
|
110
169
|
if (value === null || value === undefined) {
|
|
111
170
|
return GraphQLString;
|
|
112
171
|
}
|
|
172
|
+
// Beyond max depth, treat as opaque JSON
|
|
173
|
+
if (depth >= MAX_INFER_DEPTH) {
|
|
174
|
+
return GraphQLJSON;
|
|
175
|
+
}
|
|
113
176
|
if (Array.isArray(value)) {
|
|
114
177
|
if (value.length === 0) {
|
|
115
178
|
return new GraphQLList(GraphQLString);
|
|
116
179
|
}
|
|
180
|
+
// Mixed-type arrays → JSON scalar (e.g. ["field", 4296, { "temporal-unit": "day" }])
|
|
181
|
+
if (hasMixedTypes(value)) {
|
|
182
|
+
return GraphQLJSON;
|
|
183
|
+
}
|
|
117
184
|
// Sample multiple elements for richer type inference
|
|
118
|
-
const
|
|
119
|
-
if (
|
|
120
|
-
const elementType = inferType(merged, `${typeName}_Item`, typeRegistry);
|
|
185
|
+
const mergeResult = mergeArraySamples(value);
|
|
186
|
+
if (mergeResult) {
|
|
187
|
+
const elementType = inferType(mergeResult.merged, `${typeName}_Item`, typeRegistry, mergeResult.conflicts, depth + 1);
|
|
121
188
|
return new GraphQLList(elementType);
|
|
122
189
|
}
|
|
123
|
-
const elementType = inferType(value[0], `${typeName}_Item`, typeRegistry);
|
|
190
|
+
const elementType = inferType(value[0], `${typeName}_Item`, typeRegistry, conflicts, depth + 1);
|
|
124
191
|
return new GraphQLList(elementType);
|
|
125
192
|
}
|
|
126
193
|
if (typeof value === "object") {
|
|
@@ -154,9 +221,17 @@ function inferType(value, typeName, typeRegistry) {
|
|
|
154
221
|
sanitized = `${sanitized}_${counter}`;
|
|
155
222
|
}
|
|
156
223
|
usedNames.add(sanitized);
|
|
157
|
-
const childTypeName = `${typeName}_${sanitized}`;
|
|
158
|
-
const fieldType = inferType(fieldValue, childTypeName, typeRegistry);
|
|
159
224
|
const key = originalKey;
|
|
225
|
+
// Use JSON scalar for fields with type conflicts across samples
|
|
226
|
+
if (conflicts?.has(originalKey)) {
|
|
227
|
+
fieldConfigs[sanitized] = {
|
|
228
|
+
type: GraphQLJSON,
|
|
229
|
+
resolve: (source) => source[key],
|
|
230
|
+
};
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const childTypeName = `${typeName}_${sanitized}`;
|
|
234
|
+
const fieldType = inferType(fieldValue, childTypeName, typeRegistry, conflicts, depth + 1);
|
|
160
235
|
fieldConfigs[sanitized] = {
|
|
161
236
|
type: fieldType,
|
|
162
237
|
resolve: (source) => source[key],
|
|
@@ -205,12 +280,18 @@ export function buildSchemaFromData(data, method, pathTemplate, requestBodySchem
|
|
|
205
280
|
if (Array.isArray(data)) {
|
|
206
281
|
let itemType = GraphQLString;
|
|
207
282
|
if (data.length > 0) {
|
|
208
|
-
|
|
209
|
-
if (
|
|
210
|
-
itemType =
|
|
283
|
+
// Mixed-type top-level array → items are JSON scalars
|
|
284
|
+
if (hasMixedTypes(data)) {
|
|
285
|
+
itemType = GraphQLJSON;
|
|
211
286
|
}
|
|
212
287
|
else {
|
|
213
|
-
|
|
288
|
+
const mergeResult = mergeArraySamples(data);
|
|
289
|
+
if (mergeResult) {
|
|
290
|
+
itemType = inferType(mergeResult.merged, `${baseName}_Item`, typeRegistry, mergeResult.conflicts, 0);
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
itemType = inferScalarType(data[0]);
|
|
294
|
+
}
|
|
214
295
|
}
|
|
215
296
|
}
|
|
216
297
|
queryType = new GraphQLObjectType({
|
package/build/index.js
CHANGED
|
@@ -8,7 +8,7 @@ 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, } from "./graphql-schema.js";
|
|
11
|
-
const config = loadConfig();
|
|
11
|
+
const config = await loadConfig();
|
|
12
12
|
initLogger(config.logPath ?? null);
|
|
13
13
|
const apiIndex = new ApiIndex(config.spec);
|
|
14
14
|
const server = new McpServer({
|
|
@@ -62,6 +62,14 @@ server.tool("list_api", `List available ${config.name} API endpoints. ` +
|
|
|
62
62
|
else {
|
|
63
63
|
data = apiIndex.listAllCategories();
|
|
64
64
|
}
|
|
65
|
+
// Empty results — return directly to avoid GraphQL schema errors on empty arrays
|
|
66
|
+
if (data.length === 0) {
|
|
67
|
+
return {
|
|
68
|
+
content: [
|
|
69
|
+
{ type: "text", text: JSON.stringify({ items: [], _count: 0 }, null, 2) },
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
65
73
|
const defaultQuery = isEndpointMode
|
|
66
74
|
? "{ items { method path summary tag parameters { name in required description } } _count }"
|
|
67
75
|
: "{ items { tag endpointCount } _count }";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anyapi-mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.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",
|