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.
@@ -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 properties = schema.properties;
30
- if (!properties)
105
+ const result = extractProperties(schema, spec, 0, new Set());
106
+ if (!result)
31
107
  return undefined;
32
- const requiredFields = new Set(Array.isArray(schema.required) ? schema.required : []);
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
+ }
@@ -97,7 +97,7 @@ function getSuggestion(status, endpoint) {
97
97
  return msg;
98
98
  }
99
99
  case 401:
100
- return "Authentication required. Provide credentials via --header (e.g. --header \"Authorization: Bearer <token>\") or per-request headers parameter.";
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: {