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.
- package/README.md +390 -282
- package/package.json +55 -41
- package/src/cli.js +338 -172
- package/src/commands/add.js +163 -78
- package/src/commands/auth.js +32 -0
- package/src/commands/call.js +224 -207
- package/src/commands/fetch.js +63 -27
- package/src/commands/grep.js +14 -11
- package/src/commands/list.js +89 -80
- package/src/commands/show.js +16 -7
- package/src/commands/skill.js +40 -0
- package/src/commands/specs.js +22 -9
- package/src/commands/types.js +6 -2
- package/src/commands/usage.js +15 -0
- package/src/commands/validate.js +33 -7
- package/src/dotenv.js +38 -0
- package/src/glob.js +6 -1
- package/src/mcp-client.js +91 -63
- package/src/oauth/auth-flow.js +59 -0
- package/src/oauth/provider.js +191 -0
- package/src/oauth/tokens.js +59 -0
- package/src/output.js +65 -61
- package/src/registry.js +36 -10
- package/src/resolve.js +66 -65
- package/src/secrets.js +46 -0
- package/src/skill/SKILL.md +112 -0
- package/src/usage.js +62 -0
package/src/commands/call.js
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
await
|
|
35
|
-
} else {
|
|
36
|
-
await
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
:
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
+
}
|
package/src/commands/fetch.js
CHANGED
|
@@ -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)
|
|
66
|
-
|
|
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
|
-
* {
|
|
77
|
+
* { _section: "mcp", type: "stdio"|"sse"|"http", url?, command?, args?, env?, headers? }
|
|
76
78
|
*/
|
|
77
79
|
export async function fetchSpec(entry) {
|
|
78
|
-
if (entry.
|
|
79
|
-
if (entry.
|
|
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(
|
|
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
|
|
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(
|
|
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) =>
|
|
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
|
-
|
|
113
|
-
|
|
126
|
+
_section: "mcp",
|
|
127
|
+
type: "stdio",
|
|
114
128
|
command: parts[0],
|
|
115
129
|
args: parts.slice(1),
|
|
116
130
|
cwd: flags.cwd,
|
|
117
|
-
|
|
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
|
-
|
|
123
|
-
|
|
139
|
+
_section: "mcp",
|
|
140
|
+
type: "sse",
|
|
124
141
|
url: flags["mcp-sse"],
|
|
125
|
-
|
|
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
|
-
|
|
131
|
-
|
|
150
|
+
_section: "mcp",
|
|
151
|
+
type: "http",
|
|
132
152
|
url: flags["mcp-http"],
|
|
133
|
-
|
|
153
|
+
...(Object.keys(headers).length ? { headers } : {}),
|
|
154
|
+
...filterConfig,
|
|
134
155
|
};
|
|
135
156
|
}
|
|
136
157
|
if (flags.graphql) {
|
|
137
|
-
return {
|
|
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 {
|
|
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.
|
|
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.
|
|
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:
|
|
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,
|