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.
- package/README.md +171 -54
- package/dist/rapidoc-flow-min.js +580 -0
- package/dist/rapidoc-flow-min.js.LICENSE.txt +171 -0
- package/dist/rapidoc-min.js +645 -202
- package/dist/rapidoc-min.js.LICENSE.txt +381 -0
- package/package.json +57 -42
- package/src/README.md +150 -0
- package/src/components/README.md +91 -0
- package/src/components/api-request.ts +912 -0
- package/src/components/api-response.ts +514 -0
- package/src/components/json-tree.ts +171 -0
- package/src/components/schema-base-element.ts +329 -0
- package/src/components/schema-table.ts +475 -0
- package/src/components/schema-tree.ts +393 -0
- package/src/components/tag-input.ts +180 -0
- package/src/flow.js +3 -0
- package/src/index.js +6 -0
- package/src/oauth-receiver.ts +29 -0
- package/src/rapi-flow.ts +1178 -0
- package/src/rapidoc-mini.ts +603 -0
- package/src/rapidoc.ts +1854 -0
- package/src/styles/README.md +96 -0
- package/src/styles/auth-styles.js +318 -0
- package/src/styles/border-styles.js +24 -0
- package/src/styles/css-bundle.js +129 -0
- package/src/styles/custom-styles.js +8 -0
- package/src/styles/dialog-box-styles.js +102 -0
- package/src/styles/endpoint-styles.js +564 -0
- package/src/styles/flex-styles.js +16 -0
- package/src/styles/flow-styles.js +248 -0
- package/src/styles/font-styles.js +432 -0
- package/src/styles/info-styles.js +36 -0
- package/src/styles/input-styles.js +555 -0
- package/src/styles/layout-styles.js +396 -0
- package/src/styles/microlighter-styles.js +99 -0
- package/src/styles/nav-styles.js +633 -0
- package/src/styles/param-list-styles.js +261 -0
- package/src/styles/schema-styles.js +296 -0
- package/src/styles/tab-styles.js +106 -0
- package/src/styles/theme-tokens.js +582 -0
- package/src/templates/README.md +89 -0
- package/src/templates/advanced-search-dialog-template.js +94 -0
- package/src/templates/api-response-template.js +223 -0
- package/src/templates/callback-template.js +86 -0
- package/src/templates/code-samples-template.js +52 -0
- package/src/templates/components-template.js +86 -0
- package/src/templates/downloads-template.js +70 -0
- package/src/templates/endpoint-template.js +377 -0
- package/src/templates/expanded-endpoint-template.js +270 -0
- package/src/templates/focused-endpoint-template.js +146 -0
- package/src/templates/header-template.js +92 -0
- package/src/templates/logo-template.js +21 -0
- package/src/templates/main-body-template.js +102 -0
- package/src/templates/navbar-template.js +498 -0
- package/src/templates/overview-template.js +104 -0
- package/src/templates/request-body-template.js +673 -0
- package/src/templates/request-params-template.js +485 -0
- package/src/templates/security-scheme-template.js +1272 -0
- package/src/templates/server-template.js +153 -0
- 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/auto-color-scheme.js +99 -0
- package/src/utils/bind-navbar.js +175 -0
- package/src/utils/color-utils.js +59 -0
- package/src/utils/common-utils.js +191 -0
- package/src/utils/curl-utils.js +126 -0
- package/src/utils/highlighter.js +419 -0
- package/src/utils/markdown-utils.js +110 -0
- package/src/utils/mcp/README.md +155 -0
- package/src/utils/mcp/common.js +166 -0
- 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 +47 -0
- package/src/utils/mock-interceptor.js +557 -0
- package/src/utils/oauth-utils.js +87 -0
- package/src/utils/request-utils.js +264 -0
- package/src/utils/response-utils.js +203 -0
- package/src/utils/sanitize.js +57 -0
- package/src/utils/schema-utils.js +1954 -0
- package/src/utils/slugger.js +187 -0
- package/src/utils/spec-parser.js +941 -0
- package/src/utils/url-utils.js +246 -0
- 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
|
+
}
|