rapidoc 12.0.0 → 12.1.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.
- package/README.md +127 -25
- package/dist/rapidoc-flow-min.js +580 -0
- package/dist/rapidoc-flow-min.js.LICENSE.txt +171 -0
- package/dist/rapidoc-min.js +541 -1309
- package/dist/rapidoc-min.js.LICENSE.txt +381 -0
- package/package.json +21 -18
- package/src/README.md +150 -0
- package/src/components/README.md +91 -0
- package/src/components/api-request.ts +43 -0
- package/src/components/api-response.ts +9 -4
- package/src/components/schema-base-element.ts +18 -4
- package/src/components/schema-table.ts +58 -13
- package/src/components/schema-tree.ts +7 -0
- package/src/components/tag-input.ts +5 -9
- package/src/flow.js +3 -0
- package/src/rapi-flow.ts +1178 -0
- package/src/rapidoc-mini.ts +27 -6
- package/src/rapidoc.ts +108 -62
- package/src/styles/README.md +96 -0
- package/src/styles/css-bundle.js +2 -2
- package/src/styles/endpoint-styles.js +6 -1
- package/src/styles/flow-styles.js +248 -0
- package/src/styles/nav-styles.js +39 -15
- package/src/styles/schema-styles.js +13 -5
- package/src/styles/theme-tokens.js +2 -2
- package/src/templates/README.md +89 -0
- package/src/templates/api-response-template.js +14 -10
- package/src/templates/callback-template.js +1 -1
- package/src/templates/downloads-template.js +12 -27
- package/src/templates/endpoint-template.js +15 -24
- package/src/templates/expanded-endpoint-template.js +8 -4
- package/src/templates/focused-endpoint-template.js +4 -3
- package/src/templates/navbar-template.js +20 -11
- package/src/templates/request-body-template.js +29 -7
- package/src/templates/request-params-template.js +15 -4
- package/src/templates/security-scheme-template.js +11 -2
- package/src/templates/server-template.js +3 -2
- package/src/utils/README.md +146 -0
- package/src/utils/arazzo/README.md +119 -0
- package/src/utils/arazzo/components.js +15 -0
- package/src/utils/arazzo/graph.js +269 -0
- package/src/utils/arazzo/index.js +52 -0
- package/src/utils/arazzo/operations.js +106 -0
- package/src/utils/arazzo/parse.js +196 -0
- package/src/utils/arazzo/refs.js +40 -0
- package/src/utils/arazzo/svg.js +707 -0
- package/src/utils/curl-utils.js +5 -2
- package/src/utils/highlighter.js +3 -0
- package/src/utils/mcp/README.md +155 -0
- package/src/utils/mcp/common.js +45 -346
- package/src/utils/mcp/manifest.js +382 -0
- package/src/utils/mcp/project/server.ts +53 -0
- package/src/utils/mcp/project.js +88 -0
- package/src/utils/mcp/runtime.js +289 -0
- package/src/utils/mcp/schema.js +98 -0
- package/src/utils/mcp/workflow-runtime.js +374 -0
- package/src/utils/mcp/workflows.js +240 -0
- package/src/utils/mcp/zip.js +102 -0
- package/src/utils/mcp-generator.js +29 -125
- package/src/utils/mock-interceptor.js +134 -85
- package/src/utils/request-utils.js +61 -0
- package/src/utils/schema-utils.js +33 -6
- package/src/utils/slugger.js +5 -7
- package/src/utils/spec-parser.js +190 -164
- package/src/utils/url-utils.js +36 -1
- package/dist/rapidoc-static-min.js +0 -13542
- package/src/static/index.d.ts +0 -149
- package/src/static/index.js +0 -62
- package/src/static/navigation.js +0 -497
- package/src/static/render.js +0 -490
- package/src/static/serializer.js +0 -216
- package/src/utils/mcp/generators/csharp.js +0 -242
- package/src/utils/mcp/generators/go.js +0 -298
- package/src/utils/mcp/generators/java.js +0 -330
- package/src/utils/mcp/generators/python.js +0 -248
- package/src/utils/mcp/generators/rust.js +0 -281
- package/src/utils/mcp/generators/typescript.js +0 -290
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the MCP manifest: a plain JSON description of an API's tools that a runtime executes.
|
|
3
|
+
* Pure, no dependencies; takes a raw OpenAPI 3.x / Swagger 2.0 object or the result of
|
|
4
|
+
* `ProcessSpec` (`tags[].paths[]`).
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
AUTH_TRANSPORT_HEADERS,
|
|
8
|
+
deriveToolName,
|
|
9
|
+
sanitizeDescription,
|
|
10
|
+
toSnakeCase,
|
|
11
|
+
} from "./common.js";
|
|
12
|
+
import { resolvePointer, toJsonSchema } from "./schema.js";
|
|
13
|
+
import { compileWorkflows } from "./workflows.js";
|
|
14
|
+
import { indexOperations, indexResolvedSpec } from "../arazzo/operations.js";
|
|
15
|
+
|
|
16
|
+
export { toJsonSchema };
|
|
17
|
+
|
|
18
|
+
export const MANIFEST_VERSION = 1;
|
|
19
|
+
|
|
20
|
+
const METHODS = ["get", "put", "post", "delete", "options", "head", "patch", "trace", "query"];
|
|
21
|
+
const RESOURCE_PATH_HINTS = [
|
|
22
|
+
"/reference",
|
|
23
|
+
"/currencies",
|
|
24
|
+
"/countries",
|
|
25
|
+
"/tiers",
|
|
26
|
+
"/categories",
|
|
27
|
+
"/status",
|
|
28
|
+
"/schema",
|
|
29
|
+
];
|
|
30
|
+
const SECURITY_KEYS = [
|
|
31
|
+
"type",
|
|
32
|
+
"description",
|
|
33
|
+
"in",
|
|
34
|
+
"name",
|
|
35
|
+
"scheme",
|
|
36
|
+
"bearerFormat",
|
|
37
|
+
"openIdConnectUrl",
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
function isProcessedSpec(spec) {
|
|
41
|
+
return (
|
|
42
|
+
"specLoadError" in spec ||
|
|
43
|
+
(Array.isArray(spec.tags) && spec.tags.some((t) => Array.isArray(t?.paths)))
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Follows a `#/...` $ref within `spec` (a few hops, for refs to refs); anything else is returned as is.
|
|
49
|
+
* A raw spec keeps its parameters, request bodies and path items as $refs to components; a processed
|
|
50
|
+
* one (`<rapi-doc>`'s resolvedSpec) is already dereferenced.
|
|
51
|
+
*/
|
|
52
|
+
function follow(node, spec) {
|
|
53
|
+
for (
|
|
54
|
+
let hops = 0;
|
|
55
|
+
hops < 10 && typeof node?.$ref === "string" && node.$ref.startsWith("#/");
|
|
56
|
+
hops++
|
|
57
|
+
) {
|
|
58
|
+
let target = spec;
|
|
59
|
+
for (const segment of node.$ref.slice(2).split("/")) {
|
|
60
|
+
const key = decodeURIComponent(segment).replace(/~1/g, "/").replace(/~0/g, "~");
|
|
61
|
+
target = target?.[key];
|
|
62
|
+
}
|
|
63
|
+
if (!target || typeof target !== "object") return node;
|
|
64
|
+
node = target;
|
|
65
|
+
}
|
|
66
|
+
return node;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Every operation with the fields the manifest needs, from either spec shape. */
|
|
70
|
+
function collectOperations(spec, processed) {
|
|
71
|
+
const operations = [];
|
|
72
|
+
if (processed) {
|
|
73
|
+
for (const tag of spec.tags || []) {
|
|
74
|
+
for (const p of tag.paths || []) {
|
|
75
|
+
operations.push({
|
|
76
|
+
method: String(p.method || "get").toLowerCase(),
|
|
77
|
+
path: p.path || "",
|
|
78
|
+
operationId: p.rawOperation?.operationId || "",
|
|
79
|
+
summary: p.summary || "",
|
|
80
|
+
description: p.description || "",
|
|
81
|
+
deprecated: p.deprecated === true,
|
|
82
|
+
isWebhook: p.isWebhook === true,
|
|
83
|
+
internal: p.xInternal === true || p.xHidden === true,
|
|
84
|
+
parameters: Array.isArray(p.parameters) ? p.parameters : [],
|
|
85
|
+
requestBody: p.requestBody || null,
|
|
86
|
+
security: p.security,
|
|
87
|
+
tag: tag.name || "",
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return operations;
|
|
92
|
+
}
|
|
93
|
+
const params = (list) => (Array.isArray(list) ? list.map((p) => follow(p, spec)) : []);
|
|
94
|
+
for (const [path, rawItem] of Object.entries(spec.paths || {})) {
|
|
95
|
+
const item = follow(rawItem, spec);
|
|
96
|
+
if (!item || typeof item !== "object") continue;
|
|
97
|
+
const common = params(item.parameters);
|
|
98
|
+
for (const method of METHODS) {
|
|
99
|
+
const op = item[method];
|
|
100
|
+
if (!op || typeof op !== "object") continue;
|
|
101
|
+
const own = params(op.parameters);
|
|
102
|
+
// Path-level parameters apply unless the operation redefines them (same name and location)
|
|
103
|
+
const parameters = common
|
|
104
|
+
.filter((c) => !own.some((o) => o?.name === c?.name && o?.in === c?.in))
|
|
105
|
+
.concat(own);
|
|
106
|
+
operations.push({
|
|
107
|
+
method,
|
|
108
|
+
path,
|
|
109
|
+
operationId: op.operationId || "",
|
|
110
|
+
summary: op.summary || "",
|
|
111
|
+
description: op.description || "",
|
|
112
|
+
deprecated: op.deprecated === true,
|
|
113
|
+
isWebhook: false,
|
|
114
|
+
internal:
|
|
115
|
+
op["x-internal"] === true ||
|
|
116
|
+
op["x-hidden"] === true ||
|
|
117
|
+
item["x-internal"] === true ||
|
|
118
|
+
item["x-hidden"] === true,
|
|
119
|
+
parameters,
|
|
120
|
+
requestBody: follow(op.requestBody, spec) || null,
|
|
121
|
+
security: op.security ?? spec.security,
|
|
122
|
+
tag: Array.isArray(op.tags) ? op.tags[0] || "" : "",
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return operations;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The spec's security schemes as `{ id: { type, in, name, scheme, ... } }`, from either shape. */
|
|
130
|
+
function collectSecuritySchemes(spec, processed) {
|
|
131
|
+
const out = {};
|
|
132
|
+
const add = (id, scheme) => {
|
|
133
|
+
if (!scheme || typeof scheme !== "object" || !scheme.type) return;
|
|
134
|
+
const entry = {};
|
|
135
|
+
for (const key of SECURITY_KEYS) if (scheme[key] !== undefined) entry[key] = scheme[key];
|
|
136
|
+
if (scheme.type === "oauth2" && scheme.flows && typeof scheme.flows === "object") {
|
|
137
|
+
entry.flows = scheme.flows;
|
|
138
|
+
}
|
|
139
|
+
out[id] = entry;
|
|
140
|
+
};
|
|
141
|
+
if (processed) {
|
|
142
|
+
for (const s of spec.securitySchemes || []) {
|
|
143
|
+
// The scheme <rapi-doc>'s api-key attributes inject is the page's, not the API's
|
|
144
|
+
if (s?.securitySchemeId && s.securitySchemeId !== "_rapidoc_api_key") {
|
|
145
|
+
add(s.securitySchemeId, s.type === "apiKey" ? s : { ...s, in: undefined, name: undefined });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
const schemes = spec.components?.securitySchemes || spec.securityDefinitions || {};
|
|
150
|
+
for (const [id, scheme] of Object.entries(schemes)) {
|
|
151
|
+
add(id, scheme?.$ref ? resolvePointer(spec, scheme.$ref) : scheme);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function baseUrlOf(spec, processed) {
|
|
158
|
+
const server = spec.servers?.[0];
|
|
159
|
+
if (server?.url) {
|
|
160
|
+
let url = String(server.url);
|
|
161
|
+
for (const [name, v] of Object.entries(server.variables || {})) {
|
|
162
|
+
url = url.split(`{${name}}`).join(String(v?.default ?? v?.enum?.[0] ?? ""));
|
|
163
|
+
}
|
|
164
|
+
return url;
|
|
165
|
+
}
|
|
166
|
+
if (!processed && spec.host) {
|
|
167
|
+
return `${spec.schemes?.[0] || "https"}://${spec.host}${spec.basePath || ""}`;
|
|
168
|
+
}
|
|
169
|
+
return "http://localhost";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** A parameter's schema, from `schema`, `content` or Swagger 2.0's inline type keywords. */
|
|
173
|
+
function parameterSchema(param, root) {
|
|
174
|
+
if (param.schema) return toJsonSchema(param.schema, root);
|
|
175
|
+
if (param.content && typeof param.content === "object") {
|
|
176
|
+
const first = Object.values(param.content)[0];
|
|
177
|
+
if (first?.schema) return toJsonSchema(first.schema, root);
|
|
178
|
+
}
|
|
179
|
+
const inline = {};
|
|
180
|
+
for (const key of [
|
|
181
|
+
"type",
|
|
182
|
+
"format",
|
|
183
|
+
"enum",
|
|
184
|
+
"items",
|
|
185
|
+
"default",
|
|
186
|
+
"minimum",
|
|
187
|
+
"maximum",
|
|
188
|
+
"pattern",
|
|
189
|
+
]) {
|
|
190
|
+
if (param[key] !== undefined) inline[key] = param[key];
|
|
191
|
+
}
|
|
192
|
+
return inline.type ? toJsonSchema(inline, root) : { type: "string" };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function bodyContent(requestBody) {
|
|
196
|
+
const content = requestBody?.content;
|
|
197
|
+
if (!content || typeof content !== "object") return null;
|
|
198
|
+
const types = Object.keys(content);
|
|
199
|
+
const pick =
|
|
200
|
+
types.find((t) => t === "application/json") ||
|
|
201
|
+
types.find((t) => t.endsWith("+json")) ||
|
|
202
|
+
types.find((t) => t === "application/x-www-form-urlencoded") ||
|
|
203
|
+
types.find((t) => t === "multipart/form-data") ||
|
|
204
|
+
types[0];
|
|
205
|
+
return pick ? { contentType: pick, schema: content[pick]?.schema } : null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* @param {object} spec Raw OpenAPI / Swagger object, or `ProcessSpec`'s result
|
|
210
|
+
* @param {object} [options]
|
|
211
|
+
* @param {string} [options.serverName] the MCP server's name; defaults to the API title
|
|
212
|
+
* @param {object|string} [options.arazzo] a parsed Arazzo document (or JSON text) whose workflows
|
|
213
|
+
* become tools; its first OpenAPI source description is this API
|
|
214
|
+
* @param {(problem: { level: string, path: string, message: string }) => void} [options.onProblem]
|
|
215
|
+
* receives one entry per workflow the runtime cannot run, which is left out
|
|
216
|
+
* @param {string} [options.sourceName] the key of the API in `sources`: the Arazzo document's
|
|
217
|
+
* first OpenAPI source name, else `api`
|
|
218
|
+
* @param {Record<string, object>} [options.sources] the other OpenAPI documents the Arazzo
|
|
219
|
+
* document's workflows use, by source description name; each becomes a manifest source and its
|
|
220
|
+
* operations resolve for workflow steps
|
|
221
|
+
* @param {boolean} [options.includeInternal] keep `x-internal` / `x-hidden` operations
|
|
222
|
+
* @param {boolean} [options.includeDeprecated] keep deprecated operations
|
|
223
|
+
* @param {boolean} [options.includeOperations] `false` leaves the operations out: a manifest of the
|
|
224
|
+
* Arazzo document's workflow tools alone (the sources stay, the runtime needs them)
|
|
225
|
+
* @returns {object} the manifest: `{ manifestVersion, server, sources, tools, resources,
|
|
226
|
+
* workflows }`. A workflow tool has `workflow` (its id in `workflows`) instead of `operation`. Each tool has `name`, `description`, `inputSchema` (JSON Schema; parameters by
|
|
227
|
+
* name plus `body`), `annotations` (`readOnlyHint`, `destructiveHint`, `idempotentHint`) and
|
|
228
|
+
* `operation` (`source`, `method`, `path`, `params` [{ name, in, arg?, contentType? }],
|
|
229
|
+
* `security`).
|
|
230
|
+
*/
|
|
231
|
+
export function buildMcpManifest(spec, options = {}) {
|
|
232
|
+
const src = spec && typeof spec === "object" ? spec : {};
|
|
233
|
+
const processed = isProcessedSpec(src);
|
|
234
|
+
const { includeInternal = false, includeDeprecated = false, includeOperations = true } = options;
|
|
235
|
+
const info = src.info || {};
|
|
236
|
+
const title = info.title || "OpenAPI MCP Server";
|
|
237
|
+
const arazzo = typeof options.arazzo === "string" ? JSON.parse(options.arazzo) : options.arazzo;
|
|
238
|
+
const firstSource = (arazzo?.sourceDescriptions || []).find(
|
|
239
|
+
(s) => s?.name && (!s.type || s.type === "openapi"),
|
|
240
|
+
);
|
|
241
|
+
const sourceName = options.sourceName || firstSource?.name || "api";
|
|
242
|
+
const securitySchemes = collectSecuritySchemes(src, processed);
|
|
243
|
+
// Header, query and cookie parameters that carry a credential are the runtime's business
|
|
244
|
+
const credentialParams = new Set(
|
|
245
|
+
Object.values(securitySchemes)
|
|
246
|
+
.filter((s) => s.type === "apiKey" && s.name)
|
|
247
|
+
.map((s) => `${s.in || "header"}:${String(s.name).toLowerCase()}`),
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const tools = [];
|
|
251
|
+
const resources = [];
|
|
252
|
+
const usedNames = new Map();
|
|
253
|
+
|
|
254
|
+
for (const op of includeOperations ? collectOperations(src, processed) : []) {
|
|
255
|
+
if (op.isWebhook) continue;
|
|
256
|
+
if (op.internal && !includeInternal) continue;
|
|
257
|
+
if (op.deprecated && !includeDeprecated) continue;
|
|
258
|
+
|
|
259
|
+
const properties = {};
|
|
260
|
+
const required = [];
|
|
261
|
+
const params = [];
|
|
262
|
+
for (const p of op.parameters) {
|
|
263
|
+
if (!p?.name) continue;
|
|
264
|
+
const where = p.in || "query";
|
|
265
|
+
const lower = String(p.name).toLowerCase();
|
|
266
|
+
if (where === "header" && AUTH_TRANSPORT_HEADERS.has(lower)) continue;
|
|
267
|
+
if (credentialParams.has(`${where}:${lower}`)) continue;
|
|
268
|
+
if (where === "body") {
|
|
269
|
+
// Swagger 2.0 body parameter
|
|
270
|
+
const schema = parameterSchema(p, src);
|
|
271
|
+
if (p.description) schema.description = sanitizeDescription(p.description);
|
|
272
|
+
properties.body = schema;
|
|
273
|
+
if (p.required) required.push("body");
|
|
274
|
+
params.push({ name: "body", in: "body", contentType: "application/json" });
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (where === "formData") continue;
|
|
278
|
+
let arg = p.name;
|
|
279
|
+
if (arg in properties) arg = `${p.name}_${where}`;
|
|
280
|
+
const schema = parameterSchema(p, src);
|
|
281
|
+
if (p.description) schema.description = sanitizeDescription(p.description);
|
|
282
|
+
properties[arg] = schema;
|
|
283
|
+
if (p.required || where === "path") required.push(arg);
|
|
284
|
+
params.push(arg === p.name ? { name: p.name, in: where } : { name: p.name, in: where, arg });
|
|
285
|
+
}
|
|
286
|
+
const body = bodyContent(op.requestBody);
|
|
287
|
+
if (body) {
|
|
288
|
+
const schema = body.schema ? toJsonSchema(body.schema, src) : { type: "object" };
|
|
289
|
+
if (op.requestBody.description) {
|
|
290
|
+
schema.description = sanitizeDescription(op.requestBody.description);
|
|
291
|
+
}
|
|
292
|
+
properties.body = schema;
|
|
293
|
+
if (op.requestBody.required) required.push("body");
|
|
294
|
+
params.push({ name: "body", in: "body", contentType: body.contentType });
|
|
295
|
+
}
|
|
296
|
+
const inputSchema = { type: "object", properties };
|
|
297
|
+
if (required.length) inputSchema.required = required;
|
|
298
|
+
|
|
299
|
+
let name = toSnakeCase(op.operationId || deriveToolName(op.method, op.path));
|
|
300
|
+
const seen = usedNames.get(name) || 0;
|
|
301
|
+
usedNames.set(name, seen + 1);
|
|
302
|
+
if (seen) name = `${name}_${seen + 1}`;
|
|
303
|
+
|
|
304
|
+
const description = sanitizeDescription(
|
|
305
|
+
op.summary || op.description || `${op.method.toUpperCase()} ${op.path}`,
|
|
306
|
+
);
|
|
307
|
+
const operation = {
|
|
308
|
+
source: sourceName,
|
|
309
|
+
method: op.method.toUpperCase(),
|
|
310
|
+
path: op.path,
|
|
311
|
+
params,
|
|
312
|
+
security: Array.isArray(op.security)
|
|
313
|
+
? op.security.filter((s) => s && typeof s === "object")
|
|
314
|
+
: [],
|
|
315
|
+
};
|
|
316
|
+
const isGet = op.method === "get";
|
|
317
|
+
const isResource =
|
|
318
|
+
isGet && !required.length && RESOURCE_PATH_HINTS.some((hint) => op.path.includes(hint));
|
|
319
|
+
if (isResource) {
|
|
320
|
+
resources.push({
|
|
321
|
+
name,
|
|
322
|
+
uri: `reference://${name.replace(/^get_/, "").replace(/_/g, "-")}`,
|
|
323
|
+
description,
|
|
324
|
+
mimeType: "application/json",
|
|
325
|
+
operation,
|
|
326
|
+
});
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
tools.push({
|
|
330
|
+
name,
|
|
331
|
+
description,
|
|
332
|
+
inputSchema,
|
|
333
|
+
annotations: {
|
|
334
|
+
readOnlyHint: isGet,
|
|
335
|
+
// POST adds; PUT, PATCH and DELETE change or remove what is there
|
|
336
|
+
destructiveHint: ["put", "patch", "delete"].includes(op.method),
|
|
337
|
+
idempotentHint: ["get", "put", "delete"].includes(op.method),
|
|
338
|
+
},
|
|
339
|
+
operation,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const workflows = {};
|
|
344
|
+
const sources = { [sourceName]: { baseUrl: baseUrlOf(src, processed), securitySchemes } };
|
|
345
|
+
if (arazzo && typeof arazzo === "object") {
|
|
346
|
+
const indexes = new Map([
|
|
347
|
+
[sourceName, processed ? indexResolvedSpec(src) : indexOperations(src)],
|
|
348
|
+
]);
|
|
349
|
+
for (const [name, doc] of Object.entries(options.sources || {})) {
|
|
350
|
+
if (name === sourceName || !doc || typeof doc !== "object") continue;
|
|
351
|
+
const docProcessed = isProcessedSpec(doc);
|
|
352
|
+
indexes.set(name, docProcessed ? indexResolvedSpec(doc) : indexOperations(doc));
|
|
353
|
+
sources[name] = {
|
|
354
|
+
baseUrl: baseUrlOf(doc, docProcessed),
|
|
355
|
+
securitySchemes: collectSecuritySchemes(doc, docProcessed),
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
const compiled = compileWorkflows(arazzo, {
|
|
359
|
+
sources: indexes,
|
|
360
|
+
onProblem: options.onProblem,
|
|
361
|
+
});
|
|
362
|
+
for (const tool of compiled.tools) {
|
|
363
|
+
const seen = usedNames.get(tool.name) || 0;
|
|
364
|
+
usedNames.set(tool.name, seen + 1);
|
|
365
|
+
tools.push(seen ? { ...tool, name: `${tool.name}_${seen + 1}` } : tool);
|
|
366
|
+
}
|
|
367
|
+
Object.assign(workflows, compiled.workflows);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
manifestVersion: MANIFEST_VERSION,
|
|
372
|
+
server: {
|
|
373
|
+
name: toSnakeCase(options.serverName || title),
|
|
374
|
+
version: info.version || "1.0.0",
|
|
375
|
+
description: sanitizeDescription(info.description || ""),
|
|
376
|
+
},
|
|
377
|
+
sources,
|
|
378
|
+
tools,
|
|
379
|
+
resources,
|
|
380
|
+
workflows,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server over stdio for the API described in manifest.json. Exported by RapiDoc.
|
|
3
|
+
*
|
|
4
|
+
* The tools, resources and the API's base URL come from the manifest and the requests are made
|
|
5
|
+
* by runtime.js, so this file is the same for every API. Credentials come from environment
|
|
6
|
+
* variables named after the API's security schemes (see claude_desktop_config.json);
|
|
7
|
+
* API_BASE_URL overrides the base URL.
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
11
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
12
|
+
import {
|
|
13
|
+
CallToolRequestSchema,
|
|
14
|
+
ListResourcesRequestSchema,
|
|
15
|
+
ListToolsRequestSchema,
|
|
16
|
+
ReadResourceRequestSchema,
|
|
17
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
18
|
+
import { createMcpRuntime, credentialsFromEnv } from "./runtime.js";
|
|
19
|
+
|
|
20
|
+
// workflow-runtime.js is part of the export only when the manifest has workflow tools
|
|
21
|
+
const { runWorkflow } = await import("./workflow-runtime.js").catch(() => ({
|
|
22
|
+
runWorkflow: undefined,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
const manifest = JSON.parse(readFileSync(new URL("./manifest.json", import.meta.url), "utf8"));
|
|
26
|
+
|
|
27
|
+
const baseUrls: Record<string, string> = {};
|
|
28
|
+
if (process.env.API_BASE_URL) {
|
|
29
|
+
for (const name of Object.keys(manifest.sources)) baseUrls[name] = process.env.API_BASE_URL;
|
|
30
|
+
}
|
|
31
|
+
const runtime = createMcpRuntime(manifest, {
|
|
32
|
+
credentials: credentialsFromEnv(manifest, process.env),
|
|
33
|
+
baseUrls,
|
|
34
|
+
runWorkflow,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const server = new Server(
|
|
38
|
+
{ name: manifest.server.name, version: manifest.server.version },
|
|
39
|
+
{ capabilities: { tools: {}, resources: {} } },
|
|
40
|
+
);
|
|
41
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: runtime.listTools() }));
|
|
42
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) =>
|
|
43
|
+
runtime.callTool(request.params.name, request.params.arguments ?? {}),
|
|
44
|
+
);
|
|
45
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
46
|
+
resources: runtime.listResources(),
|
|
47
|
+
}));
|
|
48
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) =>
|
|
49
|
+
runtime.readResource(request.params.uri),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
await server.connect(new StdioServerTransport());
|
|
53
|
+
console.error(`[MCP] ${manifest.server.name}: ${manifest.tools.length} tools on stdio`);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writes the exported MCP server project: the manifest as data, plus fixed files that are the
|
|
3
|
+
* same for every API (the runtime and a stdio server that reads the manifest).
|
|
4
|
+
*/
|
|
5
|
+
import runtimeSource from "./runtime.js?raw";
|
|
6
|
+
import serverSource from "./project/server.ts?raw";
|
|
7
|
+
import { envNameFor, generateHostConfigs, toSnakeCase } from "./common.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {object} manifest from buildMcpManifest()
|
|
11
|
+
* @param {object} [options]
|
|
12
|
+
* @param {string} [options.serverName] the package name; defaults to the manifest's server name
|
|
13
|
+
* @param {string} [options.workflowRuntime] the text of workflow-runtime.js, written as
|
|
14
|
+
* `workflow-runtime.js` when the manifest has workflows (@rapidoc/mcp and <rapi-flow> supply it;
|
|
15
|
+
* dist/rapidoc-min.js does not carry it)
|
|
16
|
+
* @returns {Record<string, string>} file path → content
|
|
17
|
+
*/
|
|
18
|
+
export function writeMcpProject(manifest, options = {}) {
|
|
19
|
+
const name = manifest.server.name || "api_mcp_server";
|
|
20
|
+
const hasWorkflows = Object.keys(manifest.workflows || {}).length > 0;
|
|
21
|
+
const schemeIds = Object.values(manifest.sources || {}).flatMap((s) =>
|
|
22
|
+
Object.keys(s.securitySchemes || {}),
|
|
23
|
+
);
|
|
24
|
+
const hostConfigs = generateHostConfigs(options.serverName || name, { schemes: schemeIds });
|
|
25
|
+
const packageJson = {
|
|
26
|
+
name: toSnakeCase(options.serverName || name).replace(/_/g, "-"),
|
|
27
|
+
version: manifest.server.version || "1.0.0",
|
|
28
|
+
private: true,
|
|
29
|
+
type: "module",
|
|
30
|
+
scripts: { start: "tsx server.ts" },
|
|
31
|
+
dependencies: { "@modelcontextprotocol/sdk": "^1.12.0" },
|
|
32
|
+
devDependencies: { tsx: "^4.19.0" },
|
|
33
|
+
};
|
|
34
|
+
const envLines = schemeIds.length
|
|
35
|
+
? schemeIds.map((id) => {
|
|
36
|
+
const scheme = Object.values(manifest.sources)
|
|
37
|
+
.map((s) => s.securitySchemes?.[id])
|
|
38
|
+
.find(Boolean);
|
|
39
|
+
const what =
|
|
40
|
+
scheme?.type === "apiKey"
|
|
41
|
+
? `the API key sent as ${scheme.in || "header"} \`${scheme.name}\``
|
|
42
|
+
: scheme?.type === "http" && String(scheme.scheme).toLowerCase() === "basic"
|
|
43
|
+
? "`user:password` for HTTP Basic"
|
|
44
|
+
: "the bearer token";
|
|
45
|
+
return `- \`${envNameFor(id)}\`: ${what} (security scheme \`${id}\`)`;
|
|
46
|
+
})
|
|
47
|
+
: ["- `API_KEY`: not needed; this API declares no security scheme"];
|
|
48
|
+
const readme = `# ${manifest.server.name} MCP server
|
|
49
|
+
|
|
50
|
+
Exported by RapiDoc from the API's OpenAPI document. \`manifest.json\` lists ${manifest.tools.length} tools${
|
|
51
|
+
hasWorkflows ? ` (${Object.keys(manifest.workflows).length} of them workflows)` : ""
|
|
52
|
+
}${manifest.resources.length ? ` and ${manifest.resources.length} resources` : ""}; \`server.ts\` serves them over stdio and \`runtime.js\` makes the requests${
|
|
53
|
+
hasWorkflows && options.workflowRuntime
|
|
54
|
+
? ", with workflow-runtime.js running the workflows"
|
|
55
|
+
: ""
|
|
56
|
+
}.
|
|
57
|
+
|
|
58
|
+
## Run
|
|
59
|
+
|
|
60
|
+
\`\`\`bash
|
|
61
|
+
npm install
|
|
62
|
+
${schemeIds.map((id) => `${envNameFor(id)}="..." `).join("")}npm start
|
|
63
|
+
\`\`\`
|
|
64
|
+
|
|
65
|
+
Environment variables:
|
|
66
|
+
|
|
67
|
+
${envLines.join("\n")}
|
|
68
|
+
- \`API_BASE_URL\`: overrides the base URL in the manifest (${
|
|
69
|
+
Object.values(manifest.sources)[0]?.baseUrl || ""
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
## Use with Claude Desktop or Cursor
|
|
73
|
+
|
|
74
|
+
Copy the \`mcpServers\` entry from \`claude_desktop_config.json\` into your host's configuration
|
|
75
|
+
and fill in the credentials.
|
|
76
|
+
`;
|
|
77
|
+
return {
|
|
78
|
+
"manifest.json": JSON.stringify(manifest, null, 2),
|
|
79
|
+
"server.ts": serverSource,
|
|
80
|
+
"runtime.js": runtimeSource,
|
|
81
|
+
...(hasWorkflows && options.workflowRuntime
|
|
82
|
+
? { "workflow-runtime.js": options.workflowRuntime }
|
|
83
|
+
: {}),
|
|
84
|
+
"package.json": JSON.stringify(packageJson, null, 2),
|
|
85
|
+
"README.md": readme,
|
|
86
|
+
"claude_desktop_config.json": hostConfigs.claudeDesktopConfig,
|
|
87
|
+
};
|
|
88
|
+
}
|