anyapi-mcp-server 1.5.0 → 1.5.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.
@@ -34,7 +34,7 @@ export async function callApi(config, method, pathTemplate, params, body, extraH
34
34
  // --- URL construction ---
35
35
  const { url: interpolatedPath, remainingParams } = interpolatePath(pathTemplate, params ?? {});
36
36
  let fullUrl = `${config.baseUrl}${interpolatedPath}`;
37
- if (method === "GET" && Object.keys(remainingParams).length > 0) {
37
+ if (Object.keys(remainingParams).length > 0) {
38
38
  const qs = new URLSearchParams();
39
39
  for (const [k, v] of Object.entries(remainingParams)) {
40
40
  if (v !== undefined && v !== null) {
@@ -118,19 +118,21 @@ function postmanUrlToPath(url) {
118
118
  export class ApiIndex {
119
119
  byTag = new Map();
120
120
  allEndpoints = [];
121
- constructor(specContent) {
122
- let parsed;
123
- try {
124
- parsed = JSON.parse(specContent);
125
- }
126
- catch {
127
- parsed = yaml.load(specContent);
128
- }
129
- if (isPostmanCollection(parsed)) {
130
- this.parsePostman(parsed);
131
- }
132
- else {
133
- this.parseOpenApi(parsed, parsed);
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 endpoints = this.byTag.get(category) ?? [];
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
- const lower = keyword.toLowerCase();
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.toLowerCase().includes(lower) ||
276
- ep.summary.toLowerCase().includes(lower) ||
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). HTTPS URLs are cached locally.
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 specUrl = getArg("--spec");
68
+ const specUrls = getAllArgs("--spec");
69
69
  const baseUrl = getArg("--base-url");
70
- if (!name || !specUrl || !baseUrl) {
70
+ if (!name || specUrls.length === 0 || !baseUrl) {
71
71
  console.error(USAGE);
72
72
  process.exit(1);
73
73
  }
74
- const spec = await loadSpec(interpolateEnv(specUrl));
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
- spec,
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,
package/build/index.js CHANGED
@@ -11,33 +11,30 @@ import { getOrBuildSchema, executeQuery, schemaToSDL, truncateIfArray, computeSh
11
11
  const WRITE_METHODS = new Set(["POST", "PUT", "DELETE", "PATCH"]);
12
12
  const config = await loadConfig();
13
13
  initLogger(config.logPath ?? null);
14
- const apiIndex = new ApiIndex(config.spec);
14
+ const apiIndex = new ApiIndex(config.specs);
15
15
  const server = new McpServer({
16
16
  name: config.name,
17
17
  version: "1.2.1",
18
18
  });
19
19
  // --- Tool 1: list_api ---
20
20
  server.tool("list_api", `List available ${config.name} API endpoints. ` +
21
- "Call with no arguments to see all categories. " +
22
- "Provide 'category' to list endpoints in a tag. " +
23
- "Provide 'search' to search across paths and descriptions. " +
24
- "The correct query format is auto-selected based on mode. " +
25
- "You can optionally override with a custom 'query' parameter. " +
21
+ "Call with no arguments to see all endpoints. " +
22
+ "Provide 'category' to filter by tag. " +
23
+ "Provide 'search' to search across paths and summaries (supports regex). " +
26
24
  "Results are paginated with limit (default 20) and offset.", {
27
25
  category: z
28
26
  .string()
29
27
  .optional()
30
- .describe("Tag/category to filter by. Omit to see all categories."),
28
+ .describe("Tag/category to filter by. Case-insensitive."),
31
29
  search: z
32
30
  .string()
33
31
  .optional()
34
- .describe("Search keyword across endpoint paths and descriptions"),
32
+ .describe("Search keyword or regex pattern across endpoint paths and summaries"),
35
33
  query: z
36
34
  .string()
37
35
  .optional()
38
- .describe("Optional GraphQL selection query override. If omitted, a sensible default is used automatically:\n" +
39
- "Categories (no args): '{ items { tag endpointCount } _count }'\n" +
40
- "Endpoints (with category/search): '{ items { method path summary tag parameters { name in required description } } _count }'"),
36
+ .describe("GraphQL selection query. Default: '{ items { method path summary } _count }'. " +
37
+ "Available fields: method, path, summary, tag, parameters { name in required description }"),
41
38
  limit: z
42
39
  .number()
43
40
  .int()
@@ -52,7 +49,6 @@ server.tool("list_api", `List available ${config.name} API endpoints. ` +
52
49
  .describe("Items to skip (default: 0)"),
53
50
  }, async ({ category, search, query, limit, offset }) => {
54
51
  try {
55
- const isEndpointMode = !!(search || category);
56
52
  let data;
57
53
  if (search) {
58
54
  data = apiIndex.searchAll(search);
@@ -61,7 +57,7 @@ server.tool("list_api", `List available ${config.name} API endpoints. ` +
61
57
  data = apiIndex.listAllByCategory(category);
62
58
  }
63
59
  else {
64
- data = apiIndex.listAllCategories();
60
+ data = apiIndex.listAll();
65
61
  }
66
62
  // Empty results — return directly to avoid GraphQL schema errors on empty arrays
67
63
  if (data.length === 0) {
@@ -71,11 +67,9 @@ server.tool("list_api", `List available ${config.name} API endpoints. ` +
71
67
  ],
72
68
  };
73
69
  }
74
- const defaultQuery = isEndpointMode
75
- ? "{ items { method path summary tag parameters { name in required description } } _count }"
76
- : "{ items { tag endpointCount } _count }";
70
+ const defaultQuery = "{ items { method path summary } _count }";
77
71
  const effectiveQuery = query ?? defaultQuery;
78
- const { schema } = getOrBuildSchema(data, "LIST", category ?? search ?? "_categories");
72
+ const { schema } = getOrBuildSchema(data, "LIST", category ?? search ?? "_all");
79
73
  const { data: sliced, truncated, total } = truncateIfArray(data, limit ?? 20, offset);
80
74
  const queryResult = await executeQuery(schema, sliced, effectiveQuery);
81
75
  if (truncated && typeof queryResult === "object" && queryResult !== null) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anyapi-mcp-server",
3
- "version": "1.5.0",
3
+ "version": "1.5.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",