anyapi-mcp-server 1.1.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/LICENSE ADDED
@@ -0,0 +1,32 @@
1
+ anyapi-mcp-server - Proprietary Non-Commercial License
2
+
3
+ Copyright (c) 2026 quiloos39
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to use,
7
+ copy, modify, and distribute the Software for personal, educational, or
8
+ non-commercial purposes only, subject to the following conditions:
9
+
10
+ 1. COMMERCIAL USE PROHIBITED. The Software may not be used, in whole or in
11
+ part, for any commercial purpose without prior written permission from the
12
+ copyright holder. "Commercial purpose" includes, but is not limited to:
13
+ - Selling, licensing, or sublicensing the Software or derivative works
14
+ - Using the Software in a product or service sold to customers
15
+ - Using the Software to generate revenue, directly or indirectly
16
+ - Using the Software in a commercial organization's operations
17
+
18
+ 2. The above copyright notice and this permission notice shall be included in
19
+ all copies or substantial portions of the Software.
20
+
21
+ 3. Derivative works must include this same license and may not be released
22
+ under a different license.
23
+
24
+ 4. For commercial licensing inquiries, contact the copyright holder.
25
+
26
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
27
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
28
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
29
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
30
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
31
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
32
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,178 @@
1
+ # anyapi-mcp-server
2
+
3
+ A universal [Model Context Protocol (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.
4
+
5
+ Instead of building a custom MCP server for every API, `anyapi-mcp-server` reads your spec file and dynamically creates tools with **GraphQL-style field selection** and automatic schema inference.
6
+
7
+ ## Features
8
+
9
+ - **Works with any REST API** — provide an OpenAPI (JSON/YAML) or Postman Collection v2.x spec
10
+ - **GraphQL-style queries** — select only the fields you need from API responses
11
+ - **Automatic schema inference** — calls an endpoint once, infers the response schema, then lets you query specific fields
12
+ - **Multi-sample merging** — samples up to 10 array elements to build richer schemas that capture fields missing from individual items
13
+ - **Mutation support** — POST/PUT/DELETE/PATCH endpoints with OpenAPI request body schemas get GraphQL mutation types with typed inputs
14
+ - **Smart query suggestions** — `call_api` returns ready-to-use GraphQL queries based on the inferred schema
15
+ - **Response caching** — 30-second TTL cache prevents duplicate HTTP calls across consecutive `call_api` → `query_api` flows
16
+ - **Retry with backoff** — automatic retries with exponential backoff and jitter for 429/5xx errors, honoring `Retry-After` headers
17
+ - **Multi-format responses** — parses JSON, XML, CSV, and plain text responses automatically
18
+ - **Built-in pagination** — API-level pagination via `params`; client-side slicing with top-level `limit`/`offset`
19
+ - **Per-request headers** — override default headers on individual `call_api`/`query_api` calls
20
+ - **Environment variable interpolation** — use `${ENV_VAR}` in base URLs and headers
21
+ - **Request logging** — optional NDJSON request/response log with sensitive header masking
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ npm install -g anyapi-mcp-server
27
+ ```
28
+
29
+ Or use directly with `npx`:
30
+
31
+ ```bash
32
+ npx anyapi-mcp-server --name petstore --spec ./petstore.yaml --base-url https://petstore.swagger.io/v2
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ ```
38
+ anyapi-mcp --name <name> --spec <path> --base-url <url> [--header "Key: Value"]... [--log <path>]
39
+ ```
40
+
41
+ ### Required arguments
42
+
43
+ | Flag | Description |
44
+ |------|-------------|
45
+ | `--name` | Server name (e.g. `petstore`) |
46
+ | `--spec` | Path to OpenAPI spec file (JSON or YAML) or Postman Collection |
47
+ | `--base-url` | API base URL (e.g. `https://api.example.com`). Supports `${ENV_VAR}` interpolation. |
48
+
49
+ ### Optional arguments
50
+
51
+ | Flag | Description |
52
+ |------|-------------|
53
+ | `--header` | HTTP header as `"Key: Value"` (repeatable). Supports `${ENV_VAR}` interpolation in values. |
54
+ | `--log` | Path to request/response log file (NDJSON format). Sensitive headers are masked automatically. |
55
+
56
+ ### Example: Cursor / Claude Desktop configuration
57
+
58
+ Add to your MCP configuration (e.g. `~/.cursor/mcp.json` or Claude Desktop config):
59
+
60
+ ```json
61
+ {
62
+ "mcpServers": {
63
+ "petstore": {
64
+ "command": "npx",
65
+ "args": [
66
+ "-y",
67
+ "anyapi-mcp-server",
68
+ "--name", "petstore",
69
+ "--spec", "/path/to/petstore.yaml",
70
+ "--base-url", "https://petstore.swagger.io/v2"
71
+ ]
72
+ }
73
+ }
74
+ }
75
+ ```
76
+
77
+ With authentication headers:
78
+
79
+ ```json
80
+ {
81
+ "mcpServers": {
82
+ "my-api": {
83
+ "command": "npx",
84
+ "args": [
85
+ "-y",
86
+ "anyapi-mcp-server",
87
+ "--name", "my-api",
88
+ "--spec", "/path/to/openapi.json",
89
+ "--base-url", "https://api.example.com",
90
+ "--header", "Authorization: Bearer ${API_TOKEN}"
91
+ ],
92
+ "env": {
93
+ "API_TOKEN": "your-token-here"
94
+ }
95
+ }
96
+ }
97
+ }
98
+ ```
99
+
100
+ ## Tools
101
+
102
+ The server exposes three MCP tools:
103
+
104
+ ### `list_api`
105
+
106
+ Browse and search available API endpoints from the spec.
107
+
108
+ - Call with no arguments to see all categories/tags
109
+ - Provide `category` to list endpoints in a tag
110
+ - Provide `search` to search across paths and descriptions
111
+ - Results are paginated with `limit` (default 20) and `offset`
112
+ - Uses GraphQL selection: `{ items { tag endpointCount } _count }` for categories, `{ items { method path summary } _count }` for endpoints
113
+
114
+ ### `call_api`
115
+
116
+ Inspect an API endpoint by making a real request and returning the inferred GraphQL schema (SDL). No response data is returned — use `query_api` to fetch actual data.
117
+
118
+ - Returns the full schema SDL showing all available fields and types
119
+ - Returns accepted parameters (name, location, required) from the API spec
120
+ - Returns `suggestedQueries` — ready-to-use GraphQL queries generated from the schema
121
+ - Accepts optional `headers` to override defaults for this request
122
+ - For write operations (POST/PUT/DELETE/PATCH) with request body schemas, the schema includes a Mutation type
123
+
124
+ ### `query_api`
125
+
126
+ Fetch data from an API endpoint, returning only the fields you select via GraphQL.
127
+
128
+ - Object responses: `{ id name collection { id name } }`
129
+ - Array responses: `{ items { id name } _count }`
130
+ - Mutation syntax for writes: `mutation { post_endpoint(input: { ... }) { id name } }`
131
+ - Supports `limit` and `offset` for client-side slicing of already-fetched data
132
+ - For API-level pagination, pass limit/offset inside `params` instead
133
+ - Accepts optional `headers` to override defaults for this request
134
+
135
+ ## Workflow
136
+
137
+ 1. **Discover** endpoints with `list_api`
138
+ 2. **Inspect** a specific endpoint with `call_api` to see its schema and suggested queries
139
+ 3. **Query** the endpoint with `query_api` to fetch exactly the fields you need
140
+
141
+ ## How It Works
142
+
143
+ ```
144
+ OpenAPI/Postman spec
145
+
146
+
147
+ ┌─────────┐ ┌──────────┐ ┌────────────┐
148
+ │ list_api │ │ call_api │────▶│ query_api │
149
+ │ (browse) │ │ (schema) │ │ (data) │
150
+ └─────────┘ └──────────┘ └────────────┘
151
+ │ │
152
+ ▼ ▼
153
+ REST API call REST API call
154
+ (with retry (cached if same
155
+ + caching) as call_api)
156
+ │ │
157
+ ▼ ▼
158
+ Infer GraphQL Execute GraphQL
159
+ schema from query against
160
+ JSON response response data
161
+ ```
162
+
163
+ 1. The spec file is parsed at startup into an endpoint index with tags, paths, parameters, and request body schemas
164
+ 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
165
+ 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
166
+ 4. Write operations (POST/PUT/DELETE/PATCH) with OpenAPI request body schemas get a Mutation type with typed `GraphQLInputObjectType` inputs
167
+
168
+ ## Supported Spec Formats
169
+
170
+ - **OpenAPI 3.x** (JSON or YAML)
171
+ - **OpenAPI 2.0 / Swagger** (JSON or YAML)
172
+ - **Postman Collection v2.x** (JSON)
173
+
174
+ `$ref` resolution is supported for OpenAPI request body schemas. Postman `:param` path variables are converted to OpenAPI-style `{param}` automatically.
175
+
176
+ ## License
177
+
178
+ Proprietary Non-Commercial. Free for personal and educational use. Commercial use requires written permission. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,105 @@
1
+ import { withRetry, RetryableError, isRetryableStatus } from "./retry.js";
2
+ import { buildCacheKey, getCached, setCache } from "./response-cache.js";
3
+ import { logEntry, isLoggingEnabled } from "./logger.js";
4
+ import { parseResponse } from "./response-parser.js";
5
+ const TIMEOUT_MS = 30_000;
6
+ function interpolatePath(pathTemplate, params) {
7
+ const remaining = { ...params };
8
+ const url = pathTemplate.replace(/\{([^}]+)\}/g, (_, paramName) => {
9
+ const value = remaining[paramName];
10
+ if (value === undefined) {
11
+ throw new Error(`Missing required path parameter: ${paramName}`);
12
+ }
13
+ delete remaining[paramName];
14
+ return encodeURIComponent(String(value));
15
+ });
16
+ return { url, remainingParams: remaining };
17
+ }
18
+ export async function callApi(config, method, pathTemplate, params, body, extraHeaders) {
19
+ // --- Cache check ---
20
+ const cacheKey = buildCacheKey(method, pathTemplate, params, body, extraHeaders);
21
+ const cached = getCached(cacheKey);
22
+ if (cached !== undefined)
23
+ return cached;
24
+ // --- URL construction ---
25
+ const { url: interpolatedPath, remainingParams } = interpolatePath(pathTemplate, params ?? {});
26
+ let fullUrl = `${config.baseUrl}${interpolatedPath}`;
27
+ if (method === "GET" && Object.keys(remainingParams).length > 0) {
28
+ const qs = new URLSearchParams();
29
+ for (const [k, v] of Object.entries(remainingParams)) {
30
+ if (v !== undefined && v !== null) {
31
+ qs.append(k, String(v));
32
+ }
33
+ }
34
+ fullUrl += `?${qs.toString()}`;
35
+ }
36
+ const mergedHeaders = {
37
+ "Content-Type": "application/json",
38
+ ...config.headers,
39
+ ...extraHeaders,
40
+ };
41
+ // --- Retry-wrapped fetch ---
42
+ const result = await withRetry(async () => {
43
+ const controller = new AbortController();
44
+ const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
45
+ const startTime = Date.now();
46
+ try {
47
+ const fetchOptions = {
48
+ method,
49
+ headers: mergedHeaders,
50
+ signal: controller.signal,
51
+ };
52
+ if (body && method !== "GET") {
53
+ fetchOptions.body = JSON.stringify(body);
54
+ }
55
+ const response = await fetch(fullUrl, fetchOptions);
56
+ const durationMs = Date.now() - startTime;
57
+ const bodyText = await response.text();
58
+ // Log request/response
59
+ if (isLoggingEnabled()) {
60
+ const responseHeaders = {};
61
+ response.headers.forEach((v, k) => {
62
+ responseHeaders[k] = v;
63
+ });
64
+ await logEntry({
65
+ timestamp: new Date().toISOString(),
66
+ method,
67
+ url: fullUrl,
68
+ requestHeaders: mergedHeaders,
69
+ requestBody: body,
70
+ responseStatus: response.status,
71
+ responseHeaders,
72
+ responseBody: bodyText,
73
+ durationMs,
74
+ });
75
+ }
76
+ if (!response.ok) {
77
+ const msg = `API error ${response.status} ${response.statusText}: ${bodyText}`;
78
+ if (isRetryableStatus(response.status)) {
79
+ let retryAfterMs;
80
+ const retryAfter = response.headers.get("retry-after");
81
+ if (retryAfter) {
82
+ const seconds = parseInt(retryAfter, 10);
83
+ if (!isNaN(seconds))
84
+ retryAfterMs = seconds * 1000;
85
+ }
86
+ throw new RetryableError(msg, response.status, retryAfterMs);
87
+ }
88
+ throw new Error(msg);
89
+ }
90
+ return parseResponse(response.headers.get("content-type"), bodyText);
91
+ }
92
+ catch (error) {
93
+ if (error instanceof DOMException && error.name === "AbortError") {
94
+ throw new Error(`Request to ${method} ${pathTemplate} timed out after ${TIMEOUT_MS / 1000}s`);
95
+ }
96
+ throw error;
97
+ }
98
+ finally {
99
+ clearTimeout(timeout);
100
+ }
101
+ });
102
+ // --- Cache store ---
103
+ setCache(cacheKey, result);
104
+ return result;
105
+ }
@@ -0,0 +1,263 @@
1
+ import { readFileSync } from "node:fs";
2
+ import yaml from "js-yaml";
3
+ const PAGE_SIZE = 20;
4
+ const HTTP_METHODS = new Set(["get", "post", "put", "delete", "patch"]);
5
+ function extractRequestBodySchema(requestBody, spec) {
6
+ if (!requestBody || typeof requestBody !== "object")
7
+ return undefined;
8
+ const rb = requestBody;
9
+ // Handle $ref at the requestBody level
10
+ if (rb["$ref"] && typeof rb["$ref"] === "string") {
11
+ const resolved = resolveRef(rb["$ref"], spec);
12
+ if (!resolved)
13
+ return undefined;
14
+ return extractRequestBodySchema(resolved, spec);
15
+ }
16
+ const content = rb.content;
17
+ if (!content)
18
+ return undefined;
19
+ const jsonContent = content["application/json"];
20
+ if (!jsonContent?.schema)
21
+ return undefined;
22
+ let schema = jsonContent.schema;
23
+ // Resolve top-level $ref
24
+ if (schema["$ref"] && typeof schema["$ref"] === "string") {
25
+ const resolved = resolveRef(schema["$ref"], spec);
26
+ if (!resolved)
27
+ return undefined;
28
+ schema = resolved;
29
+ }
30
+ const properties = schema.properties;
31
+ if (!properties)
32
+ return undefined;
33
+ const requiredFields = new Set(Array.isArray(schema.required) ? schema.required : []);
34
+ const result = {};
35
+ for (const [propName, propDef] of Object.entries(properties)) {
36
+ let def = propDef;
37
+ // Resolve property-level $ref
38
+ if (def["$ref"] && typeof def["$ref"] === "string") {
39
+ const resolved = resolveRef(def["$ref"], spec);
40
+ if (resolved)
41
+ def = resolved;
42
+ }
43
+ const prop = {
44
+ type: def.type ?? "string",
45
+ required: requiredFields.has(propName),
46
+ };
47
+ if (def.description)
48
+ prop.description = def.description;
49
+ if (def.items && typeof def.items === "object") {
50
+ const items = def.items;
51
+ prop.items = { type: items.type ?? "string" };
52
+ }
53
+ result[propName] = prop;
54
+ }
55
+ return Object.keys(result).length > 0
56
+ ? { contentType: "application/json", properties: result }
57
+ : undefined;
58
+ }
59
+ function resolveRef(ref, spec) {
60
+ // Handle "#/components/schemas/Foo" style refs
61
+ if (!ref.startsWith("#/"))
62
+ return undefined;
63
+ const parts = ref.slice(2).split("/");
64
+ let current = spec;
65
+ for (const part of parts) {
66
+ if (typeof current !== "object" || current === null)
67
+ return undefined;
68
+ current = current[part];
69
+ }
70
+ return current;
71
+ }
72
+ function isPostmanCollection(parsed) {
73
+ const obj = parsed;
74
+ return !!(obj.info &&
75
+ typeof obj.info === "object" &&
76
+ obj.info.schema &&
77
+ typeof obj.info.schema === "string" &&
78
+ obj.info.schema.includes("schema.getpostman.com"));
79
+ }
80
+ /**
81
+ * Extract path template from Postman URL.
82
+ * Converts Postman's :param to OpenAPI-style {param}.
83
+ */
84
+ function postmanUrlToPath(url) {
85
+ let raw;
86
+ if (typeof url === "string") {
87
+ raw = url;
88
+ }
89
+ else if (url.raw) {
90
+ raw = url.raw;
91
+ }
92
+ else if (url.path) {
93
+ raw = "/" + url.path.join("/");
94
+ }
95
+ else {
96
+ return "/";
97
+ }
98
+ // Strip protocol + host, keep path only
99
+ try {
100
+ const parsed = new URL(raw);
101
+ raw = parsed.pathname + parsed.search;
102
+ }
103
+ catch {
104
+ // Not a full URL — might be just a path or use {{baseUrl}}
105
+ raw = raw.replace(/^\{\{[^}]+\}\}/, "");
106
+ if (!raw.startsWith("/")) {
107
+ const slashIdx = raw.indexOf("/");
108
+ raw = slashIdx >= 0 ? raw.slice(slashIdx) : "/" + raw;
109
+ }
110
+ }
111
+ // Strip query string
112
+ const qIdx = raw.indexOf("?");
113
+ if (qIdx >= 0)
114
+ raw = raw.slice(0, qIdx);
115
+ // Convert :param to {param}
116
+ raw = raw.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, "{$1}");
117
+ return raw || "/";
118
+ }
119
+ export class ApiIndex {
120
+ byTag = new Map();
121
+ allEndpoints = [];
122
+ constructor(specPath) {
123
+ const raw = readFileSync(specPath, "utf-8");
124
+ const isYaml = /\.ya?ml$/i.test(specPath);
125
+ const parsed = isYaml ? yaml.load(raw) : JSON.parse(raw);
126
+ if (isPostmanCollection(parsed)) {
127
+ this.parsePostman(parsed);
128
+ }
129
+ else {
130
+ this.parseOpenApi(parsed, parsed);
131
+ }
132
+ }
133
+ parseOpenApi(spec, rawSpec) {
134
+ for (const [path, methods] of Object.entries(spec.paths)) {
135
+ for (const [method, operation] of Object.entries(methods)) {
136
+ if (!HTTP_METHODS.has(method))
137
+ continue;
138
+ const op = operation;
139
+ const tag = op.tags?.[0] ?? "untagged";
140
+ const parameters = (op.parameters ?? []).map((p) => ({
141
+ name: p.name,
142
+ in: p.in,
143
+ required: p.required ?? false,
144
+ description: p.description,
145
+ }));
146
+ const requestBodySchema = extractRequestBodySchema(op.requestBody, rawSpec);
147
+ const endpoint = {
148
+ method: method.toUpperCase(),
149
+ path,
150
+ summary: op.summary ?? `${method.toUpperCase()} ${path}`,
151
+ description: op.description ?? "",
152
+ tag,
153
+ parameters,
154
+ hasRequestBody: !!op.requestBody,
155
+ requestBodySchema,
156
+ };
157
+ this.addEndpoint(endpoint);
158
+ }
159
+ }
160
+ }
161
+ parsePostman(collection) {
162
+ this.walkPostmanItems(collection.item ?? [], []);
163
+ }
164
+ walkPostmanItems(items, folderPath) {
165
+ for (const item of items) {
166
+ if (item.item) {
167
+ // Folder — recurse with folder name as tag context
168
+ this.walkPostmanItems(item.item, [...folderPath, item.name ?? "unnamed"]);
169
+ }
170
+ else if (item.request) {
171
+ this.parsePostmanRequest(item, folderPath);
172
+ }
173
+ }
174
+ }
175
+ parsePostmanRequest(item, folderPath) {
176
+ const req = item.request;
177
+ const method = (req.method ?? "GET").toUpperCase();
178
+ if (!HTTP_METHODS.has(method.toLowerCase()))
179
+ return;
180
+ const path = req.url ? postmanUrlToPath(req.url) : "/";
181
+ const tag = folderPath.length > 0 ? folderPath.join("/") : "untagged";
182
+ const description = typeof req.description === "string" ? req.description : "";
183
+ // Extract query params
184
+ const parameters = [];
185
+ if (typeof req.url === "object" && req.url.query) {
186
+ for (const q of req.url.query) {
187
+ if (q.disabled)
188
+ continue;
189
+ parameters.push({
190
+ name: q.key,
191
+ in: "query",
192
+ required: false,
193
+ description: q.description,
194
+ });
195
+ }
196
+ }
197
+ // Extract path variables
198
+ if (typeof req.url === "object" && req.url.variable) {
199
+ for (const v of req.url.variable) {
200
+ parameters.push({
201
+ name: v.key,
202
+ in: "path",
203
+ required: true,
204
+ description: v.description,
205
+ });
206
+ }
207
+ }
208
+ const endpoint = {
209
+ method,
210
+ path,
211
+ summary: item.name ?? `${method} ${path}`,
212
+ description,
213
+ tag,
214
+ parameters,
215
+ hasRequestBody: !!req.body,
216
+ };
217
+ this.addEndpoint(endpoint);
218
+ }
219
+ addEndpoint(endpoint) {
220
+ this.allEndpoints.push(endpoint);
221
+ let tagList = this.byTag.get(endpoint.tag);
222
+ if (!tagList) {
223
+ tagList = [];
224
+ this.byTag.set(endpoint.tag, tagList);
225
+ }
226
+ tagList.push(endpoint);
227
+ }
228
+ listAllCategories() {
229
+ const categories = [];
230
+ for (const [tag, endpoints] of this.byTag) {
231
+ categories.push({ tag, endpointCount: endpoints.length });
232
+ }
233
+ categories.sort((a, b) => a.tag.localeCompare(b.tag));
234
+ return categories;
235
+ }
236
+ listAllByCategory(category) {
237
+ const endpoints = this.byTag.get(category) ?? [];
238
+ return endpoints.map((ep) => ({
239
+ method: ep.method,
240
+ path: ep.path,
241
+ summary: ep.summary,
242
+ tag: ep.tag,
243
+ parameters: ep.parameters,
244
+ }));
245
+ }
246
+ searchAll(keyword) {
247
+ const lower = keyword.toLowerCase();
248
+ return this.allEndpoints
249
+ .filter((ep) => ep.path.toLowerCase().includes(lower) ||
250
+ ep.summary.toLowerCase().includes(lower) ||
251
+ ep.description.toLowerCase().includes(lower))
252
+ .map((ep) => ({
253
+ method: ep.method,
254
+ path: ep.path,
255
+ summary: ep.summary,
256
+ tag: ep.tag,
257
+ parameters: ep.parameters,
258
+ }));
259
+ }
260
+ getEndpoint(method, path) {
261
+ return this.allEndpoints.find((ep) => ep.method === method.toUpperCase() && ep.path === path);
262
+ }
263
+ }
@@ -0,0 +1,64 @@
1
+ import { resolve } from "node:path";
2
+ function getArg(flag) {
3
+ const idx = process.argv.indexOf(flag);
4
+ if (idx === -1 || !process.argv[idx + 1])
5
+ return undefined;
6
+ return process.argv[idx + 1];
7
+ }
8
+ function getAllArgs(flag) {
9
+ const values = [];
10
+ for (let i = 0; i < process.argv.length; i++) {
11
+ if (process.argv[i] === flag && process.argv[i + 1]) {
12
+ values.push(process.argv[i + 1]);
13
+ }
14
+ }
15
+ return values;
16
+ }
17
+ function interpolateEnv(value) {
18
+ return value.replace(/\$\{([^}]+)\}/g, (_, varName) => {
19
+ const envValue = process.env[varName];
20
+ if (envValue === undefined) {
21
+ throw new Error(`Environment variable ${varName} is not set`);
22
+ }
23
+ return envValue;
24
+ });
25
+ }
26
+ const USAGE = `Usage: anyapi-mcp --name <name> --spec <path> --base-url <url> [--header "Key: Value"]...
27
+
28
+ Required:
29
+ --name Server name (e.g. "petstore")
30
+ --spec Path to OpenAPI spec file (JSON or YAML)
31
+ --base-url API base URL (e.g. "https://api.example.com")
32
+
33
+ Optional:
34
+ --header HTTP header as "Key: Value" (repeatable)
35
+ Supports \${ENV_VAR} interpolation in values
36
+ --log Path to request/response log file (NDJSON format)`;
37
+ export function loadConfig() {
38
+ const name = getArg("--name");
39
+ const spec = getArg("--spec");
40
+ const baseUrl = getArg("--base-url");
41
+ if (!name || !spec || !baseUrl) {
42
+ console.error(USAGE);
43
+ process.exit(1);
44
+ }
45
+ const headers = {};
46
+ for (const raw of getAllArgs("--header")) {
47
+ const colonIdx = raw.indexOf(":");
48
+ if (colonIdx === -1) {
49
+ console.error(`ERROR: Invalid header format "${raw}". Expected "Key: Value"`);
50
+ process.exit(1);
51
+ }
52
+ const key = raw.slice(0, colonIdx).trim();
53
+ const value = raw.slice(colonIdx + 1).trim();
54
+ headers[key] = interpolateEnv(value);
55
+ }
56
+ const logPath = getArg("--log");
57
+ return {
58
+ name,
59
+ spec: resolve(spec),
60
+ baseUrl: interpolateEnv(baseUrl).replace(/\/+$/, ""),
61
+ headers: Object.keys(headers).length > 0 ? headers : undefined,
62
+ logPath: logPath ? resolve(logPath) : undefined,
63
+ };
64
+ }