anyapi-mcp-server 1.6.0 → 1.7.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 +131 -211
- package/build/api-client.js +134 -75
- package/build/api-index.js +120 -27
- package/build/config.js +55 -1
- package/build/data-cache.js +96 -0
- package/build/error-context.js +1 -1
- package/build/graphql-schema.js +342 -40
- package/build/index.js +293 -109
- package/build/json-filter.js +62 -0
- package/build/oauth.js +340 -0
- package/build/pagination.js +123 -0
- package/build/query-suggestions.js +34 -9
- package/build/rate-limit-tracker.js +59 -0
- package/build/response-parser.js +47 -2
- package/build/token-budget.js +135 -0
- package/package.json +1 -1
package/build/api-index.js
CHANGED
|
@@ -1,6 +1,82 @@
|
|
|
1
1
|
import yaml from "js-yaml";
|
|
2
2
|
const PAGE_SIZE = 20;
|
|
3
3
|
const HTTP_METHODS = new Set(["get", "post", "put", "delete", "patch"]);
|
|
4
|
+
const MAX_BODY_SCHEMA_DEPTH = 6;
|
|
5
|
+
function extractProperties(schema, spec, depth, visited) {
|
|
6
|
+
const properties = schema.properties;
|
|
7
|
+
if (!properties)
|
|
8
|
+
return undefined;
|
|
9
|
+
const requiredFields = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
10
|
+
const result = {};
|
|
11
|
+
for (const [propName, propDef] of Object.entries(properties)) {
|
|
12
|
+
let def = propDef;
|
|
13
|
+
let refPath;
|
|
14
|
+
// Resolve property-level $ref
|
|
15
|
+
if (def["$ref"] && typeof def["$ref"] === "string") {
|
|
16
|
+
refPath = def["$ref"];
|
|
17
|
+
if (visited.has(refPath)) {
|
|
18
|
+
result[propName] = { type: "object", required: requiredFields.has(propName) };
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const resolved = resolveRef(refPath, spec);
|
|
22
|
+
if (resolved)
|
|
23
|
+
def = resolved;
|
|
24
|
+
}
|
|
25
|
+
const prop = {
|
|
26
|
+
type: def.type ?? "string",
|
|
27
|
+
required: requiredFields.has(propName),
|
|
28
|
+
};
|
|
29
|
+
if (def.description)
|
|
30
|
+
prop.description = def.description;
|
|
31
|
+
// Recurse into nested objects
|
|
32
|
+
if (prop.type === "object" && def.properties && depth < MAX_BODY_SCHEMA_DEPTH) {
|
|
33
|
+
const branch = new Set(visited);
|
|
34
|
+
if (refPath)
|
|
35
|
+
branch.add(refPath);
|
|
36
|
+
const nested = extractProperties(def, spec, depth + 1, branch);
|
|
37
|
+
if (nested) {
|
|
38
|
+
prop.properties = nested;
|
|
39
|
+
if (Array.isArray(def.required) && def.required.length > 0) {
|
|
40
|
+
prop.required_fields = def.required;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (def.items && typeof def.items === "object") {
|
|
45
|
+
let itemsDef = def.items;
|
|
46
|
+
let itemsRefPath;
|
|
47
|
+
if (itemsDef["$ref"] && typeof itemsDef["$ref"] === "string") {
|
|
48
|
+
itemsRefPath = itemsDef["$ref"];
|
|
49
|
+
if (!visited.has(itemsRefPath)) {
|
|
50
|
+
const resolved = resolveRef(itemsRefPath, spec);
|
|
51
|
+
if (resolved)
|
|
52
|
+
itemsDef = resolved;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const itemType = itemsDef.type ?? "string";
|
|
56
|
+
if (itemType === "object" && itemsDef.properties && depth < MAX_BODY_SCHEMA_DEPTH) {
|
|
57
|
+
const branch = new Set(visited);
|
|
58
|
+
if (itemsRefPath)
|
|
59
|
+
branch.add(itemsRefPath);
|
|
60
|
+
const nestedItems = extractProperties(itemsDef, spec, depth + 1, branch);
|
|
61
|
+
if (nestedItems) {
|
|
62
|
+
prop.items = {
|
|
63
|
+
type: itemType,
|
|
64
|
+
properties: nestedItems,
|
|
65
|
+
required: Array.isArray(itemsDef.required) ? itemsDef.required : undefined,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
prop.items = { type: itemType };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
prop.items = { type: itemType };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
result[propName] = prop;
|
|
77
|
+
}
|
|
78
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
79
|
+
}
|
|
4
80
|
function extractRequestBodySchema(requestBody, spec) {
|
|
5
81
|
if (!requestBody || typeof requestBody !== "object")
|
|
6
82
|
return undefined;
|
|
@@ -26,34 +102,10 @@ function extractRequestBodySchema(requestBody, spec) {
|
|
|
26
102
|
return undefined;
|
|
27
103
|
schema = resolved;
|
|
28
104
|
}
|
|
29
|
-
const
|
|
30
|
-
if (!
|
|
105
|
+
const result = extractProperties(schema, spec, 0, new Set());
|
|
106
|
+
if (!result)
|
|
31
107
|
return undefined;
|
|
32
|
-
|
|
33
|
-
const result = {};
|
|
34
|
-
for (const [propName, propDef] of Object.entries(properties)) {
|
|
35
|
-
let def = propDef;
|
|
36
|
-
// Resolve property-level $ref
|
|
37
|
-
if (def["$ref"] && typeof def["$ref"] === "string") {
|
|
38
|
-
const resolved = resolveRef(def["$ref"], spec);
|
|
39
|
-
if (resolved)
|
|
40
|
-
def = resolved;
|
|
41
|
-
}
|
|
42
|
-
const prop = {
|
|
43
|
-
type: def.type ?? "string",
|
|
44
|
-
required: requiredFields.has(propName),
|
|
45
|
-
};
|
|
46
|
-
if (def.description)
|
|
47
|
-
prop.description = def.description;
|
|
48
|
-
if (def.items && typeof def.items === "object") {
|
|
49
|
-
const items = def.items;
|
|
50
|
-
prop.items = { type: items.type ?? "string" };
|
|
51
|
-
}
|
|
52
|
-
result[propName] = prop;
|
|
53
|
-
}
|
|
54
|
-
return Object.keys(result).length > 0
|
|
55
|
-
? { contentType: "application/json", properties: result }
|
|
56
|
-
: undefined;
|
|
108
|
+
return { contentType: "application/json", properties: result };
|
|
57
109
|
}
|
|
58
110
|
function resolveRef(ref, spec) {
|
|
59
111
|
// Handle "#/components/schemas/Foo" style refs
|
|
@@ -118,6 +170,7 @@ function postmanUrlToPath(url) {
|
|
|
118
170
|
export class ApiIndex {
|
|
119
171
|
byTag = new Map();
|
|
120
172
|
allEndpoints = [];
|
|
173
|
+
oauthSchemes = [];
|
|
121
174
|
constructor(specContents) {
|
|
122
175
|
for (const specContent of specContents) {
|
|
123
176
|
let parsed;
|
|
@@ -185,6 +238,46 @@ export class ApiIndex {
|
|
|
185
238
|
this.addEndpoint(endpoint);
|
|
186
239
|
}
|
|
187
240
|
}
|
|
241
|
+
this.extractSecuritySchemes(rawSpec);
|
|
242
|
+
}
|
|
243
|
+
extractSecuritySchemes(spec) {
|
|
244
|
+
// OpenAPI 3.x: components.securitySchemes
|
|
245
|
+
const components = spec.components;
|
|
246
|
+
const securitySchemes = components?.securitySchemes;
|
|
247
|
+
// OpenAPI 2.x (Swagger): securityDefinitions
|
|
248
|
+
const securityDefs = spec.securityDefinitions;
|
|
249
|
+
const schemes = securitySchemes ?? securityDefs ?? {};
|
|
250
|
+
for (const schemeDef of Object.values(schemes)) {
|
|
251
|
+
if (schemeDef.type !== "oauth2")
|
|
252
|
+
continue;
|
|
253
|
+
// OpenAPI 3.x: flows.authorizationCode, flows.clientCredentials, etc.
|
|
254
|
+
const flows = schemeDef.flows;
|
|
255
|
+
if (flows) {
|
|
256
|
+
for (const flow of Object.values(flows)) {
|
|
257
|
+
const scopes = flow.scopes
|
|
258
|
+
? Object.keys(flow.scopes)
|
|
259
|
+
: [];
|
|
260
|
+
this.oauthSchemes.push({
|
|
261
|
+
authorizationUrl: flow.authorizationUrl,
|
|
262
|
+
tokenUrl: flow.tokenUrl,
|
|
263
|
+
scopes,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
// OpenAPI 2.x (Swagger): direct fields on the scheme
|
|
269
|
+
const scopes = schemeDef.scopes
|
|
270
|
+
? Object.keys(schemeDef.scopes)
|
|
271
|
+
: [];
|
|
272
|
+
this.oauthSchemes.push({
|
|
273
|
+
authorizationUrl: schemeDef.authorizationUrl,
|
|
274
|
+
tokenUrl: schemeDef.tokenUrl,
|
|
275
|
+
scopes,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
getOAuthSchemes() {
|
|
280
|
+
return this.oauthSchemes;
|
|
188
281
|
}
|
|
189
282
|
parsePostman(collection) {
|
|
190
283
|
this.walkPostmanItems(collection.item ?? [], []);
|
package/build/config.js
CHANGED
|
@@ -62,7 +62,17 @@ Required:
|
|
|
62
62
|
Optional:
|
|
63
63
|
--header HTTP header as "Key: Value" (repeatable)
|
|
64
64
|
Supports \${ENV_VAR} interpolation in values
|
|
65
|
-
--log Path to request/response log file (NDJSON format)
|
|
65
|
+
--log Path to request/response log file (NDJSON format)
|
|
66
|
+
|
|
67
|
+
OAuth 2.0 (all optional, but client-id/client-secret/token-url are required together):
|
|
68
|
+
--oauth-client-id OAuth client ID
|
|
69
|
+
--oauth-client-secret OAuth client secret
|
|
70
|
+
--oauth-token-url OAuth token endpoint URL
|
|
71
|
+
--oauth-auth-url OAuth authorization endpoint URL (authorization_code flow)
|
|
72
|
+
--oauth-scopes Comma-separated scopes (e.g. "read,write")
|
|
73
|
+
--oauth-flow "authorization_code" (default) or "client_credentials"
|
|
74
|
+
--oauth-param Extra auth URL param as "key=value" (repeatable)
|
|
75
|
+
All OAuth values support \${ENV_VAR} interpolation`;
|
|
66
76
|
export async function loadConfig() {
|
|
67
77
|
const name = getArg("--name");
|
|
68
78
|
const specUrls = getAllArgs("--spec");
|
|
@@ -84,11 +94,55 @@ export async function loadConfig() {
|
|
|
84
94
|
headers[key] = interpolateEnv(value);
|
|
85
95
|
}
|
|
86
96
|
const logPath = getArg("--log");
|
|
97
|
+
// --- OAuth CLI flags ---
|
|
98
|
+
const oauthClientId = getArg("--oauth-client-id");
|
|
99
|
+
const oauthClientSecret = getArg("--oauth-client-secret");
|
|
100
|
+
const oauthTokenUrl = getArg("--oauth-token-url");
|
|
101
|
+
const oauthAuthUrl = getArg("--oauth-auth-url");
|
|
102
|
+
const oauthScopes = getArg("--oauth-scopes");
|
|
103
|
+
const oauthFlow = getArg("--oauth-flow");
|
|
104
|
+
const oauthParams = getAllArgs("--oauth-param");
|
|
105
|
+
const hasAnyOAuth = oauthClientId || oauthClientSecret || oauthTokenUrl;
|
|
106
|
+
if (hasAnyOAuth && !(oauthClientId && oauthClientSecret && oauthTokenUrl)) {
|
|
107
|
+
console.error("ERROR: --oauth-client-id, --oauth-client-secret, and --oauth-token-url must all be provided together.");
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
let oauth;
|
|
111
|
+
if (oauthClientId && oauthClientSecret && oauthTokenUrl) {
|
|
112
|
+
const extraParams = {};
|
|
113
|
+
for (const raw of oauthParams) {
|
|
114
|
+
const eqIdx = raw.indexOf("=");
|
|
115
|
+
if (eqIdx === -1) {
|
|
116
|
+
console.error(`ERROR: Invalid --oauth-param format "${raw}". Expected "key=value"`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
extraParams[raw.slice(0, eqIdx)] = interpolateEnv(raw.slice(eqIdx + 1));
|
|
120
|
+
}
|
|
121
|
+
const flow = (oauthFlow ? interpolateEnv(oauthFlow) : "authorization_code");
|
|
122
|
+
if (flow !== "authorization_code" && flow !== "client_credentials") {
|
|
123
|
+
console.error(`ERROR: Invalid --oauth-flow "${flow}". Must be "authorization_code" or "client_credentials".`);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
oauth = {
|
|
127
|
+
clientId: interpolateEnv(oauthClientId),
|
|
128
|
+
clientSecret: interpolateEnv(oauthClientSecret),
|
|
129
|
+
tokenUrl: interpolateEnv(oauthTokenUrl),
|
|
130
|
+
authUrl: oauthAuthUrl ? interpolateEnv(oauthAuthUrl) : undefined,
|
|
131
|
+
scopes: oauthScopes
|
|
132
|
+
? interpolateEnv(oauthScopes)
|
|
133
|
+
.split(/[,\s]+/)
|
|
134
|
+
.filter(Boolean)
|
|
135
|
+
: [],
|
|
136
|
+
flow,
|
|
137
|
+
extraParams,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
87
140
|
return {
|
|
88
141
|
name,
|
|
89
142
|
specs,
|
|
90
143
|
baseUrl: interpolateEnv(baseUrl).replace(/\/+$/, ""),
|
|
91
144
|
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
92
145
|
logPath: logPath ? resolve(logPath) : undefined,
|
|
146
|
+
oauth,
|
|
93
147
|
};
|
|
94
148
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { mkdirSync, writeFileSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { platform, env } from "node:process";
|
|
5
|
+
const TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
6
|
+
function defaultResponseDir() {
|
|
7
|
+
if (platform === "win32") {
|
|
8
|
+
const base = env.LOCALAPPDATA ?? join(env.USERPROFILE ?? "", "AppData", "Local");
|
|
9
|
+
return join(base, "anyapi-mcp", "responses");
|
|
10
|
+
}
|
|
11
|
+
return join(env.HOME ?? "/tmp", ".cache", "anyapi-mcp", "responses");
|
|
12
|
+
}
|
|
13
|
+
let responseDir = defaultResponseDir();
|
|
14
|
+
export function _setResponseDirForTests(dir) {
|
|
15
|
+
responseDir = dir;
|
|
16
|
+
}
|
|
17
|
+
export function _clearAllForTests() {
|
|
18
|
+
try {
|
|
19
|
+
for (const file of readdirSync(responseDir)) {
|
|
20
|
+
if (file.endsWith(".json")) {
|
|
21
|
+
try {
|
|
22
|
+
unlinkSync(join(responseDir, file));
|
|
23
|
+
}
|
|
24
|
+
catch { /* ignore */ }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch { /* dir may not exist */ }
|
|
29
|
+
}
|
|
30
|
+
function ensureDir() {
|
|
31
|
+
mkdirSync(responseDir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
export function cleanupExpired() {
|
|
34
|
+
try {
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
for (const file of readdirSync(responseDir)) {
|
|
37
|
+
if (!file.endsWith(".json"))
|
|
38
|
+
continue;
|
|
39
|
+
try {
|
|
40
|
+
const content = readFileSync(join(responseDir, file), "utf-8");
|
|
41
|
+
const entry = JSON.parse(content);
|
|
42
|
+
if (entry.expiresAt < now) {
|
|
43
|
+
unlinkSync(join(responseDir, file));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
/* ignore individual file errors */
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
/* dir may not exist yet */
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function storeResponse(method, path, data, responseHeaders) {
|
|
56
|
+
const dataKey = randomBytes(4).toString("hex");
|
|
57
|
+
const entry = {
|
|
58
|
+
method,
|
|
59
|
+
path,
|
|
60
|
+
data,
|
|
61
|
+
responseHeaders,
|
|
62
|
+
expiresAt: Date.now() + TTL_MS,
|
|
63
|
+
};
|
|
64
|
+
try {
|
|
65
|
+
ensureDir();
|
|
66
|
+
writeFileSync(join(responseDir, `${dataKey}.json`), JSON.stringify(entry));
|
|
67
|
+
cleanupExpired();
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
process.stderr.write(`data-cache: failed to store ${dataKey}: ${err}\n`);
|
|
71
|
+
}
|
|
72
|
+
return dataKey;
|
|
73
|
+
}
|
|
74
|
+
export function loadResponse(dataKey) {
|
|
75
|
+
try {
|
|
76
|
+
const filePath = join(responseDir, `${dataKey}.json`);
|
|
77
|
+
const content = readFileSync(filePath, "utf-8");
|
|
78
|
+
const entry = JSON.parse(content);
|
|
79
|
+
if (entry.expiresAt < Date.now()) {
|
|
80
|
+
try {
|
|
81
|
+
unlinkSync(filePath);
|
|
82
|
+
}
|
|
83
|
+
catch { /* ignore */ }
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
method: entry.method,
|
|
88
|
+
path: entry.path,
|
|
89
|
+
data: entry.data,
|
|
90
|
+
responseHeaders: entry.responseHeaders,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
package/build/error-context.js
CHANGED
|
@@ -97,7 +97,7 @@ function getSuggestion(status, endpoint) {
|
|
|
97
97
|
return msg;
|
|
98
98
|
}
|
|
99
99
|
case 401:
|
|
100
|
-
return "Authentication required.
|
|
100
|
+
return "Authentication required. Use the auth tool to authenticate via OAuth, or provide credentials via --header (e.g. --header \"Authorization: Bearer <token>\") or per-request headers parameter.";
|
|
101
101
|
case 403:
|
|
102
102
|
return "Forbidden. Your credentials don't have permission for this operation. Verify your API key or token has the required scopes.";
|
|
103
103
|
case 404: {
|