rapidoc 11.0.1 → 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.
Files changed (96) hide show
  1. package/README.md +171 -54
  2. package/dist/rapidoc-flow-min.js +580 -0
  3. package/dist/rapidoc-flow-min.js.LICENSE.txt +171 -0
  4. package/dist/rapidoc-min.js +645 -202
  5. package/dist/rapidoc-min.js.LICENSE.txt +381 -0
  6. package/package.json +57 -42
  7. package/src/README.md +150 -0
  8. package/src/components/README.md +91 -0
  9. package/src/components/api-request.ts +912 -0
  10. package/src/components/api-response.ts +514 -0
  11. package/src/components/json-tree.ts +171 -0
  12. package/src/components/schema-base-element.ts +329 -0
  13. package/src/components/schema-table.ts +475 -0
  14. package/src/components/schema-tree.ts +393 -0
  15. package/src/components/tag-input.ts +180 -0
  16. package/src/flow.js +3 -0
  17. package/src/index.js +6 -0
  18. package/src/oauth-receiver.ts +29 -0
  19. package/src/rapi-flow.ts +1178 -0
  20. package/src/rapidoc-mini.ts +603 -0
  21. package/src/rapidoc.ts +1854 -0
  22. package/src/styles/README.md +96 -0
  23. package/src/styles/auth-styles.js +318 -0
  24. package/src/styles/border-styles.js +24 -0
  25. package/src/styles/css-bundle.js +129 -0
  26. package/src/styles/custom-styles.js +8 -0
  27. package/src/styles/dialog-box-styles.js +102 -0
  28. package/src/styles/endpoint-styles.js +564 -0
  29. package/src/styles/flex-styles.js +16 -0
  30. package/src/styles/flow-styles.js +248 -0
  31. package/src/styles/font-styles.js +432 -0
  32. package/src/styles/info-styles.js +36 -0
  33. package/src/styles/input-styles.js +555 -0
  34. package/src/styles/layout-styles.js +396 -0
  35. package/src/styles/microlighter-styles.js +99 -0
  36. package/src/styles/nav-styles.js +633 -0
  37. package/src/styles/param-list-styles.js +261 -0
  38. package/src/styles/schema-styles.js +296 -0
  39. package/src/styles/tab-styles.js +106 -0
  40. package/src/styles/theme-tokens.js +582 -0
  41. package/src/templates/README.md +89 -0
  42. package/src/templates/advanced-search-dialog-template.js +94 -0
  43. package/src/templates/api-response-template.js +223 -0
  44. package/src/templates/callback-template.js +86 -0
  45. package/src/templates/code-samples-template.js +52 -0
  46. package/src/templates/components-template.js +86 -0
  47. package/src/templates/downloads-template.js +70 -0
  48. package/src/templates/endpoint-template.js +377 -0
  49. package/src/templates/expanded-endpoint-template.js +270 -0
  50. package/src/templates/focused-endpoint-template.js +146 -0
  51. package/src/templates/header-template.js +92 -0
  52. package/src/templates/logo-template.js +21 -0
  53. package/src/templates/main-body-template.js +102 -0
  54. package/src/templates/navbar-template.js +498 -0
  55. package/src/templates/overview-template.js +104 -0
  56. package/src/templates/request-body-template.js +673 -0
  57. package/src/templates/request-params-template.js +485 -0
  58. package/src/templates/security-scheme-template.js +1272 -0
  59. package/src/templates/server-template.js +153 -0
  60. package/src/utils/README.md +146 -0
  61. package/src/utils/arazzo/README.md +119 -0
  62. package/src/utils/arazzo/components.js +15 -0
  63. package/src/utils/arazzo/graph.js +269 -0
  64. package/src/utils/arazzo/index.js +52 -0
  65. package/src/utils/arazzo/operations.js +106 -0
  66. package/src/utils/arazzo/parse.js +196 -0
  67. package/src/utils/arazzo/refs.js +40 -0
  68. package/src/utils/arazzo/svg.js +707 -0
  69. package/src/utils/auto-color-scheme.js +99 -0
  70. package/src/utils/bind-navbar.js +175 -0
  71. package/src/utils/color-utils.js +59 -0
  72. package/src/utils/common-utils.js +191 -0
  73. package/src/utils/curl-utils.js +126 -0
  74. package/src/utils/highlighter.js +419 -0
  75. package/src/utils/markdown-utils.js +110 -0
  76. package/src/utils/mcp/README.md +155 -0
  77. package/src/utils/mcp/common.js +166 -0
  78. package/src/utils/mcp/manifest.js +382 -0
  79. package/src/utils/mcp/project/server.ts +53 -0
  80. package/src/utils/mcp/project.js +88 -0
  81. package/src/utils/mcp/runtime.js +289 -0
  82. package/src/utils/mcp/schema.js +98 -0
  83. package/src/utils/mcp/workflow-runtime.js +374 -0
  84. package/src/utils/mcp/workflows.js +240 -0
  85. package/src/utils/mcp/zip.js +102 -0
  86. package/src/utils/mcp-generator.js +47 -0
  87. package/src/utils/mock-interceptor.js +557 -0
  88. package/src/utils/oauth-utils.js +87 -0
  89. package/src/utils/request-utils.js +264 -0
  90. package/src/utils/response-utils.js +203 -0
  91. package/src/utils/sanitize.js +57 -0
  92. package/src/utils/schema-utils.js +1954 -0
  93. package/src/utils/slugger.js +187 -0
  94. package/src/utils/spec-parser.js +941 -0
  95. package/src/utils/url-utils.js +246 -0
  96. package/src/utils/xml-utils.js +61 -0
@@ -0,0 +1,166 @@
1
+ /**
2
+ * MCP (Model Context Protocol) helpers shared by the manifest builder and the project writer.
3
+ */
4
+ import { envNameFor } from "./runtime.js";
5
+
6
+ export { envNameFor };
7
+
8
+ // Auth and transport headers never exposed as tool arguments
9
+ export const AUTH_TRANSPORT_HEADERS = new Set([
10
+ "authorization",
11
+ "cookie",
12
+ "set-cookie",
13
+ "x-api-key",
14
+ "api-key",
15
+ "apikey",
16
+ "api_key",
17
+ "user-agent",
18
+ "host",
19
+ "content-length",
20
+ "accept",
21
+ "accept-encoding",
22
+ "connection",
23
+ ]);
24
+
25
+ /**
26
+ * Reads the `mcp` attribute: export is off for an empty value, `false`, `none` or a list that
27
+ * contains one of them (the old kill-switch), and on for anything else, including the old
28
+ * language names.
29
+ *
30
+ * @param {string|string[]|boolean} val
31
+ * @returns {boolean}
32
+ */
33
+ export function parseMcpEnabled(val) {
34
+ if (!val || val === "false") return false;
35
+ const items = Array.isArray(val) ? val : String(val).split(",");
36
+ const normalized = items.map((s) => String(s).trim().toLowerCase()).filter(Boolean);
37
+ if (!normalized.length) return false;
38
+ return !normalized.some((s) => s === "none" || s === "false");
39
+ }
40
+
41
+ /**
42
+ * Converts camelCase, PascalCase, kebab-case, or arbitrary text into clean snake_case.
43
+ */
44
+ export function toSnakeCase(str) {
45
+ if (!str) return "tool";
46
+ return str
47
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
48
+ .replace(/[^a-zA-Z0-9_]/g, "_")
49
+ .replace(/_+/g, "_")
50
+ .replace(/^_|_$/g, "")
51
+ .toLowerCase();
52
+ }
53
+
54
+ /**
55
+ * Sanitizes markdown/HTML descriptions into clean, concise single or multi-line agent instructions.
56
+ */
57
+ export function sanitizeDescription(text, maxLength = 300) {
58
+ if (!text || typeof text !== "string") return "";
59
+ const cleaned = text
60
+ .replace(/<[^>]*>/g, "") // remove HTML tags
61
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") // [link](url) -> link
62
+ .replace(/[`*_~#<>]/g, "") // strip markdown markers, preserve sentence punctuation
63
+ .replace(/\s+/g, " ")
64
+ .trim();
65
+ if (cleaned.length <= maxLength) return cleaned;
66
+ return cleaned.slice(0, maxLength - 3) + "...";
67
+ }
68
+
69
+ /**
70
+ * Derives a clean snake_case tool name from HTTP method and path when operationId is missing.
71
+ * e.g., GET /customers/{customerId}/orders -> get_customers_by_customer_id_orders
72
+ */
73
+ export function deriveToolName(method, path) {
74
+ const parts = path
75
+ .split("/")
76
+ .filter(Boolean)
77
+ .map((seg) => {
78
+ if (seg.startsWith("{") && seg.endsWith("}")) {
79
+ return `by_${seg.slice(1, -1)}`;
80
+ }
81
+ return seg;
82
+ });
83
+ return toSnakeCase(`${method}_${parts.join("_")}`);
84
+ }
85
+
86
+ /**
87
+ * Estimates the prompt context a manifest's tool definitions cost an agent on every turn.
88
+ *
89
+ * @param {object} manifest from buildMcpManifest()
90
+ * @returns {{ totalTools: number, totalResources: number, estimatedTokens: number,
91
+ * consequentialCount: number, warnings: string[] }} `consequentialCount` is the number of
92
+ * tools that are not read-only
93
+ */
94
+ export function estimateTokenCost(manifest) {
95
+ const tools = manifest?.tools || [];
96
+ const resources = manifest?.resources || [];
97
+ let estimatedTokens = 0;
98
+ let consequentialCount = 0;
99
+
100
+ for (const t of tools) {
101
+ // Name, description and the JSON Schema: ~4 chars per token, plus the envelope
102
+ estimatedTokens += Math.ceil(
103
+ (t.name.length + (t.description || "").length + JSON.stringify(t.inputSchema || {}).length) /
104
+ 4,
105
+ );
106
+ estimatedTokens += 25;
107
+ if (!t.annotations?.readOnlyHint) consequentialCount++;
108
+ }
109
+
110
+ for (const r of resources) {
111
+ estimatedTokens +=
112
+ Math.ceil((r.name.length + (r.description || "").length + r.uri.length) / 4) + 20;
113
+ }
114
+
115
+ const warnings = [];
116
+ if (estimatedTokens > 5000) {
117
+ warnings.push(
118
+ `Context overhead (~${estimatedTokens.toLocaleString()} tokens/turn) is high. Consider hiding unused operations with x-internal: true.`,
119
+ );
120
+ }
121
+ if (tools.length > 30) {
122
+ warnings.push(
123
+ `${tools.length} tools exceeds the recommended limit of 30 tools per agent to prevent model paralysis.`,
124
+ );
125
+ }
126
+
127
+ return {
128
+ totalTools: tools.length,
129
+ totalResources: resources.length,
130
+ estimatedTokens,
131
+ consequentialCount,
132
+ warnings,
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Generates Claude Desktop and Cursor JSON configuration snippets for the exported project.
138
+ *
139
+ * @param {string} serverName
140
+ * @param {object} [options]
141
+ * @param {string[]} [options.schemes] security scheme ids; one `env` entry each (`API_KEY` when none)
142
+ */
143
+ export function generateHostConfigs(serverName, options = {}) {
144
+ const safeName = toSnakeCase(serverName || "api_mcp_server");
145
+ const schemes = options.schemes?.length ? options.schemes : [];
146
+ const env = {};
147
+ for (const id of schemes) env[envNameFor(id)] = `your-${toSnakeCase(id).replace(/_/g, "-")}`;
148
+ if (!schemes.length) env.API_KEY = "your-api-key-here";
149
+
150
+ const claudeConfig = {
151
+ mcpServers: {
152
+ [safeName]: {
153
+ command: "npx",
154
+ args: ["-y", "tsx", "server.ts"],
155
+ env,
156
+ },
157
+ },
158
+ };
159
+
160
+ const cursorConfig = claudeConfig;
161
+
162
+ return {
163
+ claudeDesktopConfig: JSON.stringify(claudeConfig, null, 2),
164
+ cursorConfig: JSON.stringify(cursorConfig, null, 2),
165
+ };
166
+ }
@@ -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
+ }