api-spec-cli 0.2.3 → 0.2.5

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,207 +1,224 @@
1
- import { readFileSync } from "fs";
2
- import { out } from "../output.js";
3
- import { parseArgs, parseKV } from "../args.js";
4
- import { createMcpClient } from "../mcp-client.js";
5
- import { resolveSpec, resolveConfig } from "../resolve.js";
6
-
7
- const HTTP_TIMEOUT = parseInt(process.env.SPEC_HTTP_TIMEOUT ?? "30000");
8
-
9
- export async function callOperation(args) {
10
- const { flags, positional } = parseArgs(args);
11
- const target = positional[0];
12
- if (!target) throw new Error(
13
- "Usage: spec call <operation> [--spec <name> | --openapi <url> | ...] [--data '{}' | --data -] [--var k=v] [--header k=v]"
14
- );
15
-
16
- if (flags["data-file"] && !flags.data) {
17
- flags.data = readFileSync(flags["data-file"], "utf-8").trim();
18
- }
19
-
20
- // Read from stdin only when --data - is explicitly passed.
21
- // Agents run in non-TTY environments — auto-detecting stdin would add latency on every call.
22
- if (flags.data === "-") {
23
- const chunks = [];
24
- for await (const chunk of process.stdin) chunks.push(chunk);
25
- flags.data = Buffer.concat(chunks).toString("utf-8").trim();
26
- }
27
-
28
- const { spec, entry } = await resolveSpec(flags);
29
- const config = resolveConfig(flags, entry);
30
-
31
- if (spec.type === "openapi") {
32
- await callOpenAPI(spec, config, target, flags);
33
- } else if (spec.type === "mcp") {
34
- await callMCP(spec, entry, target, flags);
35
- } else {
36
- await callGraphQL(spec, config, target, flags);
37
- }
38
- }
39
-
40
- async function callMCP(spec, entry, target, flags) {
41
- const tool = spec.tools.find((t) => t.name.toLowerCase() === target.toLowerCase());
42
- if (!tool) throw new Error(`Tool not found: ${target}. Run 'spec list' to see available tools.`);
43
-
44
- let toolArgs = {};
45
- if (flags.data) {
46
- try {
47
- toolArgs = JSON.parse(flags.data);
48
- } catch {
49
- throw new Error("--data must be valid JSON when calling an MCP tool");
50
- }
51
- }
52
- const varOverrides = parseKV(flags.var);
53
- toolArgs = { ...toolArgs, ...varOverrides };
54
-
55
- // Re-connect using the original entry (which holds transport config + headers/env)
56
- const client = await createMcpClient(entry);
57
- try {
58
- const result = await client.callTool({ name: tool.name, arguments: toolArgs });
59
- // Normalize MCP result: expose isError and content at the top level
60
- const isError = result.isError === true;
61
- out({ tool: tool.name, arguments: toolArgs, isError, content: result.content, result });
62
- if (isError) process.exit(1);
63
- } finally {
64
- await client.close();
65
- }
66
- }
67
-
68
- async function callOpenAPI(spec, config, target, flags) {
69
- const lower = target.toLowerCase();
70
-
71
- const op = spec.operations.find((o) =>
72
- o.id.toLowerCase() === lower ||
73
- o.path.toLowerCase() === lower ||
74
- `${o.method.toLowerCase()} ${o.path.toLowerCase()}` === lower
75
- );
76
-
77
- if (!op) throw new Error(`Operation not found: ${target}`);
78
-
79
- const baseUrl = config.baseUrl || spec.servers?.[0]?.url || "";
80
- let path = op.path;
81
-
82
- const vars = parseKV(flags.var);
83
- for (const [key, val] of Object.entries(vars)) {
84
- path = path.replaceAll(`{${key}}`, encodeURIComponent(val));
85
- }
86
-
87
- // Detect unreplaced path parameters and error clearly
88
- const missing = [...path.matchAll(/\{([^}]+)\}/g)].map((m) => m[1]);
89
- if (missing.length > 0) {
90
- throw new Error(`Missing required path parameters: ${missing.join(", ")}. Pass --var ${missing[0]}=<value>`);
91
- }
92
-
93
- const queryParams = parseKV(flags.query);
94
- const qs = new URLSearchParams(queryParams).toString();
95
- const url = `${baseUrl}${path}${qs ? "?" + qs : ""}`;
96
-
97
- const method = (flags.method || op.method).toUpperCase();
98
- const headers = { ...config.headers };
99
-
100
- let body = undefined;
101
- if (flags.data) {
102
- body = flags.data;
103
- if (!headers["Content-Type"]) headers["Content-Type"] = "application/json";
104
- }
105
-
106
- const res = await fetch(url, { method, headers, body, signal: AbortSignal.timeout(HTTP_TIMEOUT) });
107
- const contentType = res.headers.get("content-type") || "";
108
- const responseBody = contentType.includes("json") ? await res.json() : await res.text();
109
-
110
- out({
111
- status: res.status,
112
- statusText: res.statusText,
113
- headers: Object.fromEntries(res.headers.entries()),
114
- body: responseBody,
115
- });
116
- }
117
-
118
- async function callGraphQL(spec, config, target, flags) {
119
- const lower = target.toLowerCase();
120
-
121
- const op = spec.operations.find((o) => o.name.toLowerCase() === lower);
122
- if (!op) throw new Error(`Operation not found: ${target}`);
123
-
124
- const endpoint = config.baseUrl || spec.endpoint;
125
- if (!endpoint) throw new Error("No GraphQL endpoint. Set --base-url or register with --graphql <url>.");
126
-
127
- let query;
128
- let dataVariables;
129
- if (flags.data) {
130
- try {
131
- const parsed = JSON.parse(flags.data);
132
- query = parsed.query || flags.data;
133
- dataVariables = parsed.variables;
134
- } catch {
135
- query = flags.data;
136
- }
137
- } else {
138
- query = buildGraphQLQuery(op, spec.types);
139
- }
140
-
141
- const varOverrides = parseKV(flags.var);
142
- const variables = { ...dataVariables, ...varOverrides };
143
-
144
- const headers = {
145
- "Content-Type": "application/json",
146
- ...config.headers,
147
- };
148
-
149
- const body = JSON.stringify({
150
- query,
151
- variables: Object.keys(variables).length > 0 ? variables : undefined,
152
- });
153
-
154
- const res = await fetch(endpoint, { method: "POST", headers, body, signal: AbortSignal.timeout(HTTP_TIMEOUT) });
155
- const contentType = res.headers.get("content-type") || "";
156
- const responseBody = contentType.includes("json") ? await res.json() : await res.text();
157
-
158
- out({
159
- status: res.status,
160
- query,
161
- variables: Object.keys(variables).length > 0 ? variables : undefined,
162
- data: responseBody?.data || null,
163
- errors: responseBody?.errors || null,
164
- });
165
- }
166
-
167
- function buildGraphQLQuery(op, types) {
168
- const args = op.args || [];
169
- const argsStr = args.length > 0
170
- ? `(${args.map((a) => `$${a.name}: ${flattenType(a.type)}`).join(", ")})`
171
- : "";
172
- const passArgs = args.length > 0
173
- ? `(${args.map((a) => `${a.name}: $${a.name}`).join(", ")})`
174
- : "";
175
-
176
- const returnTypeName = op.returnType?.replace(/[[\]!]/g, "");
177
- const returnType = types?.find((t) => t.name === returnTypeName);
178
- let fields = "";
179
-
180
- if (returnType?.fields) {
181
- const scalarFields = returnType.fields
182
- .filter((f) => {
183
- const typeName = flattenType(f.type)?.replace(/[[\]!]/g, "");
184
- const t = types?.find((tt) => tt.name === typeName);
185
- return !t || t.kind === "SCALAR" || t.kind === "ENUM";
186
- })
187
- .map((f) => f.name);
188
-
189
- if (scalarFields.length > 0) fields = ` { ${scalarFields.join(" ")} }`;
190
- }
191
-
192
- const keyword = op.kind === "mutation" ? "mutation" : "query";
193
- return `${keyword}${argsStr} { ${op.name}${passArgs}${fields} }`;
194
- }
195
-
196
- function flattenType(t) {
197
- if (!t) return null;
198
- if (typeof t === "string") return t;
199
- if (t.name) return t.kind === "NON_NULL" ? `${t.name}!` : t.name;
200
- if (t.ofType) {
201
- const inner = flattenType(t.ofType);
202
- if (t.kind === "LIST") return `[${inner}]`;
203
- if (t.kind === "NON_NULL") return `${inner}!`;
204
- return inner;
205
- }
206
- return t.kind;
207
- }
1
+ import { readFileSync } from "fs";
2
+ import { out } from "../output.js";
3
+ import { parseArgs, parseKV } from "../args.js";
4
+ import { createMcpClient } from "../mcp-client.js";
5
+ import { resolveSpec, resolveConfig } from "../resolve.js";
6
+ import { recordUsage } from "../usage.js";
7
+
8
+ const HTTP_TIMEOUT = parseInt(process.env.SPEC_HTTP_TIMEOUT ?? "30000");
9
+
10
+ export async function callOperation(args) {
11
+ const { flags, positional } = parseArgs(args);
12
+ const target = positional[0];
13
+ if (!target)
14
+ throw new Error(
15
+ "Usage: spec call <operation> [--spec <name> | --openapi <url> | ...] [--data '{}' | --data -] [--var k=v] [--header k=v]"
16
+ );
17
+
18
+ if (flags["data-file"] && !flags.data) {
19
+ flags.data = readFileSync(flags["data-file"], "utf-8").trim();
20
+ }
21
+
22
+ // Read from stdin only when --data - is explicitly passed.
23
+ // Agents run in non-TTY environments — auto-detecting stdin would add latency on every call.
24
+ if (flags.data === "-") {
25
+ const chunks = [];
26
+ for await (const chunk of process.stdin) chunks.push(chunk);
27
+ flags.data = Buffer.concat(chunks).toString("utf-8").trim();
28
+ }
29
+
30
+ const { spec, entry } = await resolveSpec(flags);
31
+ const config = resolveConfig(flags, entry);
32
+
33
+ if (spec.type === "openapi") {
34
+ await callOpenAPI(spec, config, target, flags);
35
+ } else if (spec.type === "mcp") {
36
+ await callMCP(spec, entry, target, flags);
37
+ } else {
38
+ await callGraphQL(spec, config, target, flags);
39
+ }
40
+ }
41
+
42
+ async function callMCP(spec, entry, target, flags) {
43
+ const tool = spec.tools.find((t) => t.name.toLowerCase() === target.toLowerCase());
44
+ if (!tool) throw new Error(`Tool not found: ${target}. Run 'spec list' to see available tools.`);
45
+
46
+ let toolArgs = {};
47
+ if (flags.data) {
48
+ try {
49
+ toolArgs = JSON.parse(flags.data);
50
+ } catch {
51
+ throw new Error("--data must be valid JSON when calling an MCP tool");
52
+ }
53
+ }
54
+ const varOverrides = parseKV(flags.var);
55
+ toolArgs = { ...toolArgs, ...varOverrides };
56
+
57
+ // Re-connect using the original entry (which holds transport config + headers/env)
58
+ const client = await createMcpClient(entry);
59
+ try {
60
+ const result = await client.callTool({ name: tool.name, arguments: toolArgs });
61
+ // Normalize MCP result: expose isError and content at the top level
62
+ const isError = result.isError === true;
63
+ out({ tool: tool.name, arguments: toolArgs, isError, content: result.content, result });
64
+ recordUsage(flags.spec, tool.name);
65
+ if (isError) process.exit(1);
66
+ } finally {
67
+ await client.close();
68
+ }
69
+ }
70
+
71
+ async function callOpenAPI(spec, config, target, flags) {
72
+ const lower = target.toLowerCase();
73
+
74
+ const op = spec.operations.find(
75
+ (o) =>
76
+ o.id.toLowerCase() === lower ||
77
+ o.path.toLowerCase() === lower ||
78
+ `${o.method.toLowerCase()} ${o.path.toLowerCase()}` === lower
79
+ );
80
+
81
+ if (!op) throw new Error(`Operation not found: ${target}`);
82
+
83
+ const baseUrl = config.baseUrl || spec.servers?.[0]?.url || "";
84
+ let path = op.path;
85
+
86
+ const vars = parseKV(flags.var);
87
+ for (const [key, val] of Object.entries(vars)) {
88
+ path = path.replaceAll(`{${key}}`, encodeURIComponent(val));
89
+ }
90
+
91
+ // Detect unreplaced path parameters and error clearly
92
+ const missing = [...path.matchAll(/\{([^}]+)\}/g)].map((m) => m[1]);
93
+ if (missing.length > 0) {
94
+ throw new Error(
95
+ `Missing required path parameters: ${missing.join(", ")}. Pass --var ${missing[0]}=<value>`
96
+ );
97
+ }
98
+
99
+ const queryParams = parseKV(flags.query);
100
+ const qs = new URLSearchParams(queryParams).toString();
101
+ const url = `${baseUrl}${path}${qs ? "?" + qs : ""}`;
102
+
103
+ const method = (flags.method || op.method).toUpperCase();
104
+ const headers = { ...config.headers };
105
+
106
+ let body = undefined;
107
+ if (flags.data) {
108
+ body = flags.data;
109
+ if (!headers["Content-Type"]) headers["Content-Type"] = "application/json";
110
+ }
111
+
112
+ const res = await fetch(url, {
113
+ method,
114
+ headers,
115
+ body,
116
+ signal: AbortSignal.timeout(HTTP_TIMEOUT),
117
+ });
118
+ const contentType = res.headers.get("content-type") || "";
119
+ const responseBody = contentType.includes("json") ? await res.json() : await res.text();
120
+
121
+ out({
122
+ status: res.status,
123
+ statusText: res.statusText,
124
+ headers: Object.fromEntries(res.headers.entries()),
125
+ body: responseBody,
126
+ });
127
+ recordUsage(flags.spec, op.id);
128
+ }
129
+
130
+ async function callGraphQL(spec, config, target, flags) {
131
+ const lower = target.toLowerCase();
132
+
133
+ const op = spec.operations.find((o) => o.name.toLowerCase() === lower);
134
+ if (!op) throw new Error(`Operation not found: ${target}`);
135
+
136
+ const endpoint = config.baseUrl || spec.endpoint;
137
+ if (!endpoint)
138
+ throw new Error("No GraphQL endpoint. Set --base-url or register with --graphql <url>.");
139
+
140
+ let query;
141
+ let dataVariables;
142
+ if (flags.data) {
143
+ try {
144
+ const parsed = JSON.parse(flags.data);
145
+ query = parsed.query || flags.data;
146
+ dataVariables = parsed.variables;
147
+ } catch {
148
+ query = flags.data;
149
+ }
150
+ } else {
151
+ query = buildGraphQLQuery(op, spec.types);
152
+ }
153
+
154
+ const varOverrides = parseKV(flags.var);
155
+ const variables = { ...dataVariables, ...varOverrides };
156
+
157
+ const headers = {
158
+ "Content-Type": "application/json",
159
+ ...config.headers,
160
+ };
161
+
162
+ const body = JSON.stringify({
163
+ query,
164
+ variables: Object.keys(variables).length > 0 ? variables : undefined,
165
+ });
166
+
167
+ const res = await fetch(endpoint, {
168
+ method: "POST",
169
+ headers,
170
+ body,
171
+ signal: AbortSignal.timeout(HTTP_TIMEOUT),
172
+ });
173
+ const contentType = res.headers.get("content-type") || "";
174
+ const responseBody = contentType.includes("json") ? await res.json() : await res.text();
175
+
176
+ out({
177
+ status: res.status,
178
+ query,
179
+ variables: Object.keys(variables).length > 0 ? variables : undefined,
180
+ data: responseBody?.data || null,
181
+ errors: responseBody?.errors || null,
182
+ });
183
+ recordUsage(flags.spec, op.name);
184
+ }
185
+
186
+ function buildGraphQLQuery(op, types) {
187
+ const args = op.args || [];
188
+ const argsStr =
189
+ args.length > 0 ? `(${args.map((a) => `$${a.name}: ${flattenType(a.type)}`).join(", ")})` : "";
190
+ const passArgs =
191
+ args.length > 0 ? `(${args.map((a) => `${a.name}: $${a.name}`).join(", ")})` : "";
192
+
193
+ const returnTypeName = op.returnType?.replace(/[[\]!]/g, "");
194
+ const returnType = types?.find((t) => t.name === returnTypeName);
195
+ let fields = "";
196
+
197
+ if (returnType?.fields) {
198
+ const scalarFields = returnType.fields
199
+ .filter((f) => {
200
+ const typeName = flattenType(f.type)?.replace(/[[\]!]/g, "");
201
+ const t = types?.find((tt) => tt.name === typeName);
202
+ return !t || t.kind === "SCALAR" || t.kind === "ENUM";
203
+ })
204
+ .map((f) => f.name);
205
+
206
+ if (scalarFields.length > 0) fields = ` { ${scalarFields.join(" ")} }`;
207
+ }
208
+
209
+ const keyword = op.kind === "mutation" ? "mutation" : "query";
210
+ return `${keyword}${argsStr} { ${op.name}${passArgs}${fields} }`;
211
+ }
212
+
213
+ function flattenType(t) {
214
+ if (!t) return null;
215
+ if (typeof t === "string") return t;
216
+ if (t.name) return t.kind === "NON_NULL" ? `${t.name}!` : t.name;
217
+ if (t.ofType) {
218
+ const inner = flattenType(t.ofType);
219
+ if (t.kind === "LIST") return `[${inner}]`;
220
+ if (t.kind === "NON_NULL") return `${inner}!`;
221
+ return inner;
222
+ }
223
+ return t.kind;
224
+ }
@@ -62,8 +62,10 @@ fragment TypeRef on __Type {
62
62
 
63
63
  function applyFilter(items, nameFn, allowed, disabled) {
64
64
  let result = items;
65
- if (allowed?.length) result = result.filter((item) => allowed.some((p) => matchFilter(p, nameFn(item))));
66
- if (disabled?.length) result = result.filter((item) => !disabled.some((p) => matchFilter(p, nameFn(item))));
65
+ if (allowed?.length)
66
+ result = result.filter((item) => allowed.some((p) => matchFilter(p, nameFn(item))));
67
+ if (disabled?.length)
68
+ result = result.filter((item) => !disabled.some((p) => matchFilter(p, nameFn(item))));
67
69
  return result;
68
70
  }
69
71
 
@@ -72,23 +74,33 @@ function applyFilter(items, nameFn, allowed, disabled) {
72
74
  * Entry shape:
73
75
  * { type: "openapi", source: "<url-or-file>", config: { headers, auth } }
74
76
  * { type: "graphql", source: "<url>", config: { headers, auth } }
75
- * { type: "mcp", transport: "stdio|sse|streamable-http", url?, command?, args?, config: { headers, env } }
77
+ * { _section: "mcp", type: "stdio"|"sse"|"http", url?, command?, args?, env?, headers? }
76
78
  */
77
79
  export async function fetchSpec(entry) {
78
- if (entry.type === "mcp") return await loadMCPFromEntry(entry);
79
- if (entry.type === "graphql") {
80
+ if (entry._section === "mcp") return await loadMCPFromEntry(entry);
81
+ if (entry._section === "graphql") {
80
82
  const spec = await loadGraphQL(entry.source, entry.config?.headers);
81
83
  return {
82
84
  ...spec,
83
- operations: applyFilter(spec.operations, (op) => op.name, entry.config?.allowedTools, entry.config?.disabledTools),
85
+ operations: applyFilter(
86
+ spec.operations,
87
+ (op) => op.name,
88
+ entry.config?.allowedTools,
89
+ entry.config?.disabledTools
90
+ ),
84
91
  };
85
92
  }
86
- // openapi — url or file; skip GraphQL probe since type is explicitly declared
93
+ // openapi
87
94
  const isUrl = entry.source?.startsWith("http://") || entry.source?.startsWith("https://");
88
95
  const spec = isUrl ? await loadFromUrl(entry.source, true) : loadFromFile(entry.source);
89
96
  return {
90
97
  ...spec,
91
- operations: applyFilter(spec.operations, (op) => op.id, entry.config?.allowedTools, entry.config?.disabledTools),
98
+ operations: applyFilter(
99
+ spec.operations,
100
+ (op) => op.id,
101
+ entry.config?.allowedTools,
102
+ entry.config?.disabledTools
103
+ ),
92
104
  };
93
105
  }
94
106
 
@@ -106,38 +118,61 @@ export function inlineEntryFromFlags(flags) {
106
118
 
107
119
  if (flags["mcp-stdio"]) {
108
120
  const raw = flags["mcp-stdio"];
109
- const parts = (raw.trim() ? raw.match(/(?:[^\s"]+|"[^"]*")+/g) : null)?.map((p) => p.replace(/^"|"$/g, ""));
121
+ const parts = (raw.trim() ? raw.match(/(?:[^\s"]+|"[^"]*")+/g) : null)?.map((p) =>
122
+ p.replace(/^"|"$/g, "")
123
+ );
110
124
  if (!parts?.length) throw new Error("--mcp-stdio requires a non-empty command string");
111
125
  return {
112
- type: "mcp",
113
- transport: "stdio",
126
+ _section: "mcp",
127
+ type: "stdio",
114
128
  command: parts[0],
115
129
  args: parts.slice(1),
116
130
  cwd: flags.cwd,
117
- config: { env: parseKV(flags.env), ...filterConfig },
131
+ env: parseKV(flags.env),
132
+ ...filterConfig,
118
133
  };
119
134
  }
120
135
  if (flags["mcp-sse"]) {
136
+ const headers = parseKV(flags.header);
137
+ if (flags.auth && !headers["Authorization"]) headers["Authorization"] = `Bearer ${flags.auth}`;
121
138
  return {
122
- type: "mcp",
123
- transport: "sse",
139
+ _section: "mcp",
140
+ type: "sse",
124
141
  url: flags["mcp-sse"],
125
- config: { headers: parseKV(flags.header), ...filterConfig },
142
+ ...(Object.keys(headers).length ? { headers } : {}),
143
+ ...filterConfig,
126
144
  };
127
145
  }
128
146
  if (flags["mcp-http"]) {
147
+ const headers = parseKV(flags.header);
148
+ if (flags.auth && !headers["Authorization"]) headers["Authorization"] = `Bearer ${flags.auth}`;
129
149
  return {
130
- type: "mcp",
131
- transport: "streamable-http",
150
+ _section: "mcp",
151
+ type: "http",
132
152
  url: flags["mcp-http"],
133
- config: { headers: parseKV(flags.header), ...filterConfig },
153
+ ...(Object.keys(headers).length ? { headers } : {}),
154
+ ...filterConfig,
134
155
  };
135
156
  }
136
157
  if (flags.graphql) {
137
- return { type: "graphql", source: flags.graphql, config: { headers: parseKV(flags.header), ...filterConfig } };
158
+ return {
159
+ _section: "graphql",
160
+ type: "graphql",
161
+ source: flags.graphql,
162
+ config: { headers: parseKV(flags.header), ...filterConfig },
163
+ };
138
164
  }
139
165
  if (flags.openapi) {
140
- return { type: "openapi", source: flags.openapi, config: { headers: parseKV(flags.header), baseUrl: flags["base-url"] || null, ...filterConfig } };
166
+ return {
167
+ _section: "openapi",
168
+ type: "openapi",
169
+ source: flags.openapi,
170
+ config: {
171
+ headers: parseKV(flags.header),
172
+ baseUrl: flags["base-url"] || null,
173
+ ...filterConfig,
174
+ },
175
+ };
141
176
  }
142
177
  return null;
143
178
  }
@@ -154,17 +189,16 @@ async function loadMCPFromEntry(entry) {
154
189
  inputSchema: t.inputSchema || null,
155
190
  }));
156
191
 
157
- mapped = applyFilter(mapped, (t) => t.name, entry.config?.allowedTools, entry.config?.disabledTools);
192
+ mapped = applyFilter(mapped, (t) => t.name, entry.allowedTools, entry.disabledTools);
158
193
 
159
194
  return {
160
195
  type: "mcp",
161
196
  title: entry.name || "MCP Server",
162
- transport: entry.transport,
197
+ transport: entry.type,
163
198
  url: entry.url,
164
199
  command: entry.command,
165
200
  args: entry.args,
166
201
  cwd: entry.cwd,
167
- config: entry.config,
168
202
  tools: mapped,
169
203
  };
170
204
  } finally {
@@ -175,9 +209,7 @@ async function loadMCPFromEntry(entry) {
175
209
  async function loadFromUrl(url, skipGraphQLProbe = false) {
176
210
  const lowerUrl = url.toLowerCase();
177
211
  const isLikelyFile =
178
- lowerUrl.endsWith(".json") ||
179
- lowerUrl.endsWith(".yaml") ||
180
- lowerUrl.endsWith(".yml");
212
+ lowerUrl.endsWith(".json") || lowerUrl.endsWith(".yaml") || lowerUrl.endsWith(".yml");
181
213
 
182
214
  if (!isLikelyFile && !skipGraphQLProbe) {
183
215
  try {
@@ -288,7 +320,11 @@ function parseOpenAPI(text, source) {
288
320
  version,
289
321
  title: doc.info?.title || null,
290
322
  description: doc.info?.description || null,
291
- servers: doc.servers || (doc.host ? [{ url: `${doc.schemes?.[0] || "https"}://${doc.host}${doc.basePath || ""}` }] : []),
323
+ servers:
324
+ doc.servers ||
325
+ (doc.host
326
+ ? [{ url: `${doc.schemes?.[0] || "https"}://${doc.host}${doc.basePath || ""}` }]
327
+ : []),
292
328
  operations,
293
329
  components: doc.components || doc.definitions || {},
294
330
  raw: doc,