rapidoc 11.0.1 → 12.0.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 +46 -31
- package/dist/rapidoc-min.js +1424 -213
- package/dist/rapidoc-static-min.js +13542 -0
- package/package.json +47 -35
- package/src/components/api-request.ts +869 -0
- package/src/components/api-response.ts +509 -0
- package/src/components/json-tree.ts +171 -0
- package/src/components/schema-base-element.ts +315 -0
- package/src/components/schema-table.ts +430 -0
- package/src/components/schema-tree.ts +386 -0
- package/src/components/tag-input.ts +184 -0
- package/src/index.js +6 -0
- package/src/oauth-receiver.ts +29 -0
- package/src/rapidoc-mini.ts +582 -0
- package/src/rapidoc.ts +1808 -0
- package/src/static/index.d.ts +149 -0
- package/src/static/index.js +62 -0
- package/src/static/navigation.js +497 -0
- package/src/static/render.js +490 -0
- package/src/static/serializer.js +216 -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 +559 -0
- package/src/styles/flex-styles.js +16 -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 +609 -0
- package/src/styles/param-list-styles.js +261 -0
- package/src/styles/schema-styles.js +288 -0
- package/src/styles/tab-styles.js +106 -0
- package/src/styles/theme-tokens.js +582 -0
- package/src/templates/advanced-search-dialog-template.js +94 -0
- package/src/templates/api-response-template.js +219 -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 +85 -0
- package/src/templates/endpoint-template.js +386 -0
- package/src/templates/expanded-endpoint-template.js +266 -0
- package/src/templates/focused-endpoint-template.js +145 -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 +489 -0
- package/src/templates/overview-template.js +104 -0
- package/src/templates/request-body-template.js +651 -0
- package/src/templates/request-params-template.js +474 -0
- package/src/templates/security-scheme-template.js +1263 -0
- package/src/templates/server-template.js +152 -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 +123 -0
- package/src/utils/highlighter.js +416 -0
- package/src/utils/markdown-utils.js +110 -0
- package/src/utils/mcp/common.js +467 -0
- package/src/utils/mcp/generators/csharp.js +242 -0
- package/src/utils/mcp/generators/go.js +298 -0
- package/src/utils/mcp/generators/java.js +330 -0
- package/src/utils/mcp/generators/python.js +248 -0
- package/src/utils/mcp/generators/rust.js +281 -0
- package/src/utils/mcp/generators/typescript.js +290 -0
- package/src/utils/mcp-generator.js +143 -0
- package/src/utils/mock-interceptor.js +508 -0
- package/src/utils/oauth-utils.js +87 -0
- package/src/utils/request-utils.js +203 -0
- package/src/utils/response-utils.js +203 -0
- package/src/utils/sanitize.js +57 -0
- package/src/utils/schema-utils.js +1927 -0
- package/src/utils/slugger.js +189 -0
- package/src/utils/spec-parser.js +915 -0
- package/src/utils/url-utils.js +211 -0
- package/src/utils/xml-utils.js +61 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP (Model Context Protocol) Common Utilities and Spec Extraction
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Common authentication and transport headers to strip from LLM parameter schemas
|
|
6
|
+
export const AUTH_TRANSPORT_HEADERS = new Set([
|
|
7
|
+
"authorization",
|
|
8
|
+
"cookie",
|
|
9
|
+
"set-cookie",
|
|
10
|
+
"x-api-key",
|
|
11
|
+
"api-key",
|
|
12
|
+
"apikey",
|
|
13
|
+
"user-agent",
|
|
14
|
+
"host",
|
|
15
|
+
"content-length",
|
|
16
|
+
"accept",
|
|
17
|
+
"accept-encoding",
|
|
18
|
+
"connection",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export const VALID_MCP_TARGETS = ["typescript", "python", "go", "csharp", "rust", "java"];
|
|
22
|
+
export const VALID_TARGET_SET = new Set(VALID_MCP_TARGETS);
|
|
23
|
+
|
|
24
|
+
export const MCP_TARGET_LABELS = {
|
|
25
|
+
typescript: "TypeScript",
|
|
26
|
+
python: "Python",
|
|
27
|
+
go: "Go",
|
|
28
|
+
csharp: "C#",
|
|
29
|
+
rust: "Rust",
|
|
30
|
+
java: "Java",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Normalizes and parses the `mcp` attribute/property value.
|
|
35
|
+
* Supports:
|
|
36
|
+
* - Comma-separated strings: "typescript, python"
|
|
37
|
+
* - Arrays of strings: ["typescript", "python"]
|
|
38
|
+
* - JSON string arrays: '["typescript", "python"]'
|
|
39
|
+
* If 'none' or 'false' is included anywhere in the value (or if empty/falsy),
|
|
40
|
+
* it acts as a complete kill-switch, returning [].
|
|
41
|
+
* Wildcards like '*' or 'all' are intentionally NOT supported.
|
|
42
|
+
*
|
|
43
|
+
* @param {string|string[]|boolean} val
|
|
44
|
+
* @returns {string[]} Array of normalized target names (e.g. ['typescript', 'python'])
|
|
45
|
+
*/
|
|
46
|
+
export function parseMcpTargets(val) {
|
|
47
|
+
if (!val || val === false || val === "false") return [];
|
|
48
|
+
let items = [];
|
|
49
|
+
if (Array.isArray(val)) {
|
|
50
|
+
items = val;
|
|
51
|
+
} else if (typeof val === "string") {
|
|
52
|
+
const trimmed = val.trim();
|
|
53
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
54
|
+
try {
|
|
55
|
+
items = JSON.parse(trimmed);
|
|
56
|
+
} catch {
|
|
57
|
+
items = trimmed.slice(1, -1).split(",");
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
items = trimmed.split(",");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const normalized = items.map((s) => String(s).trim().toLowerCase()).filter(Boolean);
|
|
64
|
+
if (normalized.includes("none") || normalized.includes("false")) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
const validTargets = normalized
|
|
68
|
+
.map((t) => (t === "c#" ? "csharp" : t))
|
|
69
|
+
.filter((t) => VALID_TARGET_SET.has(t));
|
|
70
|
+
return [...new Set(validTargets)];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Converts camelCase, PascalCase, kebab-case, or arbitrary text into clean snake_case.
|
|
75
|
+
*/
|
|
76
|
+
export function toSnakeCase(str) {
|
|
77
|
+
if (!str) return "tool";
|
|
78
|
+
return str
|
|
79
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
80
|
+
.replace(/[^a-zA-Z0-9_]/g, "_")
|
|
81
|
+
.replace(/_+/g, "_")
|
|
82
|
+
.replace(/^_|_$/g, "")
|
|
83
|
+
.toLowerCase();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Converts text into PascalCase (e.g. for C#, Java, Go types).
|
|
88
|
+
*/
|
|
89
|
+
export function toPascalCase(str) {
|
|
90
|
+
if (!str) return "Tool";
|
|
91
|
+
const snake = toSnakeCase(str);
|
|
92
|
+
return snake
|
|
93
|
+
.split("_")
|
|
94
|
+
.filter(Boolean)
|
|
95
|
+
.map((seg) => seg.charAt(0).toUpperCase() + seg.slice(1))
|
|
96
|
+
.join("");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Converts text into camelCase (e.g. for Go, Java, C# method arguments).
|
|
101
|
+
*/
|
|
102
|
+
export function toCamelCase(str) {
|
|
103
|
+
const pascal = toPascalCase(str);
|
|
104
|
+
if (!pascal) return "param";
|
|
105
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Sanitizes markdown/HTML descriptions into clean, concise single or multi-line agent instructions.
|
|
110
|
+
*/
|
|
111
|
+
export function sanitizeDescription(text, maxLength = 300) {
|
|
112
|
+
if (!text || typeof text !== "string") return "";
|
|
113
|
+
const cleaned = text
|
|
114
|
+
.replace(/<[^>]*>/g, "") // remove HTML tags
|
|
115
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") // [link](url) -> link
|
|
116
|
+
.replace(/[`*_~#<>]/g, "") // strip markdown markers, preserve sentence punctuation
|
|
117
|
+
.replace(/\s+/g, " ")
|
|
118
|
+
.trim();
|
|
119
|
+
if (cleaned.length <= maxLength) return cleaned;
|
|
120
|
+
return cleaned.slice(0, maxLength - 3) + "...";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Derives a clean snake_case tool name from HTTP method and path when operationId is missing.
|
|
125
|
+
* e.g., GET /customers/{customerId}/orders -> get_customers_by_customer_id_orders
|
|
126
|
+
*/
|
|
127
|
+
export function deriveToolName(method, path) {
|
|
128
|
+
const parts = path
|
|
129
|
+
.split("/")
|
|
130
|
+
.filter(Boolean)
|
|
131
|
+
.map((seg) => {
|
|
132
|
+
if (seg.startsWith("{") && seg.endsWith("}")) {
|
|
133
|
+
return `by_${seg.slice(1, -1)}`;
|
|
134
|
+
}
|
|
135
|
+
return seg;
|
|
136
|
+
});
|
|
137
|
+
return toSnakeCase(`${method}_${parts.join("_")}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Extracts and maps parameters from path parameters, query parameters, and requestBody.
|
|
142
|
+
*/
|
|
143
|
+
export function extractParameters(pathObj, xMcp = {}) {
|
|
144
|
+
const params = [];
|
|
145
|
+
const excludedNames = new Set(
|
|
146
|
+
(xMcp.parameters?.exclude || [])
|
|
147
|
+
.concat(pathObj["x-ignoredHeaderParameters"] || [])
|
|
148
|
+
.map((s) => s.toLowerCase()),
|
|
149
|
+
);
|
|
150
|
+
const includedNames = xMcp.parameters?.include
|
|
151
|
+
? new Set(xMcp.parameters.include.map((s) => s.toLowerCase()))
|
|
152
|
+
: null;
|
|
153
|
+
const customDescriptions = xMcp.parameters?.descriptions || {};
|
|
154
|
+
const defaults = xMcp.parameters?.defaults || {};
|
|
155
|
+
|
|
156
|
+
// 1. Process query, path, and header parameters
|
|
157
|
+
if (Array.isArray(pathObj.parameters)) {
|
|
158
|
+
for (const p of pathObj.parameters) {
|
|
159
|
+
if (!p || !p.name) continue;
|
|
160
|
+
const lowerName = p.name.toLowerCase();
|
|
161
|
+
|
|
162
|
+
// Automatically strip auth & transport headers
|
|
163
|
+
if (p.in === "header" && AUTH_TRANSPORT_HEADERS.has(lowerName)) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (p["x-speakeasy-param-ignore"] === true || excludedNames.has(lowerName)) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (includedNames && !includedNames.has(lowerName)) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const schema = p.schema || {};
|
|
174
|
+
const type = schema.type || (schema.enum ? "string" : "string");
|
|
175
|
+
params.push({
|
|
176
|
+
name: p.name,
|
|
177
|
+
in: p.in || "query",
|
|
178
|
+
type,
|
|
179
|
+
format: schema.format,
|
|
180
|
+
enum: schema.enum,
|
|
181
|
+
description: customDescriptions[p.name] || p.description || schema.description || "",
|
|
182
|
+
required: Boolean(p.required || p.in === "path"),
|
|
183
|
+
default: defaults[p.name] !== undefined ? defaults[p.name] : schema.default,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 2. Process requestBody schema properties
|
|
189
|
+
if (pathObj.requestBody && pathObj.requestBody.content) {
|
|
190
|
+
const jsonContent =
|
|
191
|
+
pathObj.requestBody.content["application/json"] ||
|
|
192
|
+
pathObj.requestBody.content["application/x-www-form-urlencoded"] ||
|
|
193
|
+
Object.values(pathObj.requestBody.content)[0];
|
|
194
|
+
|
|
195
|
+
const bodySchema = jsonContent?.schema;
|
|
196
|
+
if (bodySchema && bodySchema.properties) {
|
|
197
|
+
const requiredProps = new Set(bodySchema.required || []);
|
|
198
|
+
for (const [propName, propDef] of Object.entries(bodySchema.properties)) {
|
|
199
|
+
const lowerName = propName.toLowerCase();
|
|
200
|
+
if (excludedNames.has(lowerName)) continue;
|
|
201
|
+
if (includedNames && !includedNames.has(lowerName)) continue;
|
|
202
|
+
|
|
203
|
+
params.push({
|
|
204
|
+
name: propName,
|
|
205
|
+
in: "body",
|
|
206
|
+
type: propDef.type || "string",
|
|
207
|
+
format: propDef.format,
|
|
208
|
+
enum: propDef.enum,
|
|
209
|
+
description: customDescriptions[propName] || propDef.description || "",
|
|
210
|
+
required: requiredProps.has(propName),
|
|
211
|
+
default: defaults[propName] !== undefined ? defaults[propName] : propDef.default,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
} else if (bodySchema) {
|
|
215
|
+
// Single object body without top-level exploded properties
|
|
216
|
+
params.push({
|
|
217
|
+
name: "payload",
|
|
218
|
+
in: "body",
|
|
219
|
+
type: bodySchema.type || "object",
|
|
220
|
+
description: pathObj.requestBody.description || "Request payload",
|
|
221
|
+
required: Boolean(pathObj.requestBody.required),
|
|
222
|
+
default: undefined,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return params;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Inspects an operation and extracts curated MCP tool/resource metadata.
|
|
232
|
+
*/
|
|
233
|
+
export function extractMcpOperations(parsedSpec, options = {}) {
|
|
234
|
+
const tools = [];
|
|
235
|
+
const resources = [];
|
|
236
|
+
const { includeInternal = false, includeDeprecated = false } = options;
|
|
237
|
+
|
|
238
|
+
const rawSpec = parsedSpec?.spec || parsedSpec || {};
|
|
239
|
+
const tags = parsedSpec?.tags || [];
|
|
240
|
+
|
|
241
|
+
// Collect all path operations across tags or direct spec.paths
|
|
242
|
+
const operations = [];
|
|
243
|
+
if (tags.length > 0) {
|
|
244
|
+
for (const tag of tags) {
|
|
245
|
+
if (Array.isArray(tag.paths)) {
|
|
246
|
+
for (const p of tag.paths) {
|
|
247
|
+
operations.push({ ...p, tagName: tag.name });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
} else if (rawSpec.paths) {
|
|
252
|
+
for (const [pathStr, pathItem] of Object.entries(rawSpec.paths)) {
|
|
253
|
+
for (const [methodStr, opObj] of Object.entries(pathItem)) {
|
|
254
|
+
if (["get", "post", "put", "delete", "patch"].includes(methodStr.toLowerCase())) {
|
|
255
|
+
operations.push({
|
|
256
|
+
...opObj,
|
|
257
|
+
method: methodStr.toLowerCase(),
|
|
258
|
+
path: pathStr,
|
|
259
|
+
rawOperation: opObj,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
for (const op of operations) {
|
|
267
|
+
const raw = op.rawOperation || op;
|
|
268
|
+
const xMcp = raw["x-mcp"] || raw["x-mcp-tool"] || {};
|
|
269
|
+
|
|
270
|
+
// 1. Check exclusion rules
|
|
271
|
+
if (xMcp.enabled === false || xMcp.type === "skip") {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (!includeInternal && (raw["x-internal"] === true || raw["x-hidden"] === true)) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (!includeDeprecated && op.deprecated === true) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (op.isWebhook) {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const method = (op.method || "get").toLowerCase();
|
|
285
|
+
const path = op.path || "";
|
|
286
|
+
const params = extractParameters(op, xMcp);
|
|
287
|
+
|
|
288
|
+
// 2. Classify as Tool vs Resource
|
|
289
|
+
let isResource = false;
|
|
290
|
+
if (xMcp.type === "resource") {
|
|
291
|
+
isResource = true;
|
|
292
|
+
} else if (
|
|
293
|
+
xMcp.type !== "tool" &&
|
|
294
|
+
method === "get" &&
|
|
295
|
+
params.filter((p) => p.required).length === 0 &&
|
|
296
|
+
(path.includes("/reference") ||
|
|
297
|
+
path.includes("/currencies") ||
|
|
298
|
+
path.includes("/countries") ||
|
|
299
|
+
path.includes("/tiers") ||
|
|
300
|
+
path.includes("/categories") ||
|
|
301
|
+
path.includes("/status") ||
|
|
302
|
+
path.includes("/schema"))
|
|
303
|
+
) {
|
|
304
|
+
isResource = true;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 3. Resolve Tool Name
|
|
308
|
+
const toolName = toSnakeCase(
|
|
309
|
+
xMcp.name ||
|
|
310
|
+
raw["x-speakeasy-name-override"] ||
|
|
311
|
+
op.operationId ||
|
|
312
|
+
deriveToolName(method, path),
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
// 4. Resolve Description
|
|
316
|
+
const rawDesc =
|
|
317
|
+
xMcp.description || op.summary || op.description || `${method.toUpperCase()} ${path}`;
|
|
318
|
+
const description = sanitizeDescription(rawDesc);
|
|
319
|
+
|
|
320
|
+
// 5. Resolve Consequential/Safety Flag
|
|
321
|
+
const isConsequential = Boolean(
|
|
322
|
+
xMcp.requireConfirmation ??
|
|
323
|
+
raw["x-openai-isConsequential"] ??
|
|
324
|
+
["post", "put", "delete", "patch"].includes(method.toUpperCase()),
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
// 6. Response Filter
|
|
328
|
+
const responseFilter = xMcp.response?.filter || "";
|
|
329
|
+
const responseMaxItems = xMcp.response?.maxItems || 0;
|
|
330
|
+
|
|
331
|
+
const opItem = {
|
|
332
|
+
name: toolName,
|
|
333
|
+
method: method.toUpperCase(),
|
|
334
|
+
path,
|
|
335
|
+
description,
|
|
336
|
+
parameters: params,
|
|
337
|
+
isConsequential,
|
|
338
|
+
readOnly: Boolean(xMcp.readOnly ?? method === "get"),
|
|
339
|
+
responseFilter,
|
|
340
|
+
responseMaxItems,
|
|
341
|
+
tag: op.tagName || "",
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
if (isResource) {
|
|
345
|
+
resources.push({
|
|
346
|
+
...opItem,
|
|
347
|
+
uri: xMcp.uri || `reference://${toolName.replace(/^get_/, "").replace(/_/g, "-")}`,
|
|
348
|
+
mimeType: xMcp.mimeType || "application/json",
|
|
349
|
+
});
|
|
350
|
+
} else {
|
|
351
|
+
tools.push(opItem);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Extract root-level composite workflows
|
|
356
|
+
const workflows = rawSpec["x-mcp-workflows"] || [];
|
|
357
|
+
|
|
358
|
+
const metadata = {
|
|
359
|
+
title: rawSpec.info?.title || "OpenAPI MCP Server",
|
|
360
|
+
description: sanitizeDescription(rawSpec.info?.description || ""),
|
|
361
|
+
version: rawSpec.info?.version || "1.0.0",
|
|
362
|
+
servers: rawSpec.servers || [{ url: "http://localhost:3000" }],
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
return { metadata, tools, resources, workflows };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Calculates estimated token overhead for LLM tool definitions.
|
|
370
|
+
*/
|
|
371
|
+
export function estimateTokenCost(tools = [], resources = [], workflows = []) {
|
|
372
|
+
let estimatedTokens = 0;
|
|
373
|
+
let consequentialCount = 0;
|
|
374
|
+
|
|
375
|
+
for (const t of tools) {
|
|
376
|
+
// Tool name + description: ~4 chars per token
|
|
377
|
+
estimatedTokens += Math.ceil((t.name.length + t.description.length) / 4);
|
|
378
|
+
// Base JSON Schema overhead per function tool
|
|
379
|
+
estimatedTokens += 25;
|
|
380
|
+
if (t.isConsequential) consequentialCount++;
|
|
381
|
+
|
|
382
|
+
for (const p of t.parameters) {
|
|
383
|
+
estimatedTokens += Math.ceil((p.name.length + (p.description || "").length) / 4);
|
|
384
|
+
estimatedTokens += 10; // type, required, structure tokens
|
|
385
|
+
if (p.enum) estimatedTokens += p.enum.length * 3;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
for (const r of resources) {
|
|
390
|
+
estimatedTokens += Math.ceil((r.name.length + r.description.length + r.uri.length) / 4) + 20;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
for (const w of workflows) {
|
|
394
|
+
estimatedTokens += Math.ceil((w.name.length + (w.description || "").length) / 4) + 40;
|
|
395
|
+
if (w.parameters) {
|
|
396
|
+
estimatedTokens += Object.keys(w.parameters).length * 15;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const warnings = [];
|
|
401
|
+
if (estimatedTokens > 5000) {
|
|
402
|
+
warnings.push(
|
|
403
|
+
`Context overhead (~${estimatedTokens.toLocaleString()} tokens/turn) is high. Consider pruning unused tools with x-mcp.enabled: false.`,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
if (tools.length > 30) {
|
|
407
|
+
warnings.push(
|
|
408
|
+
`${tools.length} tools exceeds the recommended limit of 30 tools per agent to prevent model paralysis.`,
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
totalTools: tools.length,
|
|
414
|
+
totalResources: resources.length,
|
|
415
|
+
totalWorkflows: workflows.length,
|
|
416
|
+
estimatedTokens,
|
|
417
|
+
consequentialCount,
|
|
418
|
+
warnings,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Generates Claude Desktop and Cursor JSON configuration snippets.
|
|
424
|
+
*/
|
|
425
|
+
export function generateHostConfigs(serverName, options = {}) {
|
|
426
|
+
const safeName = toSnakeCase(serverName || "api_mcp_server");
|
|
427
|
+
const target = (options.target || "typescript").toLowerCase();
|
|
428
|
+
|
|
429
|
+
let command = "npx";
|
|
430
|
+
let args = ["-y", "tsx", "server.ts"];
|
|
431
|
+
|
|
432
|
+
if (target === "python") {
|
|
433
|
+
command = "fastmcp";
|
|
434
|
+
args = ["run", "server.py"];
|
|
435
|
+
} else if (target === "go") {
|
|
436
|
+
command = `./${safeName}`;
|
|
437
|
+
args = [];
|
|
438
|
+
} else if (target === "csharp" || target === "c#") {
|
|
439
|
+
command = "dotnet";
|
|
440
|
+
args = ["run"];
|
|
441
|
+
} else if (target === "rust") {
|
|
442
|
+
command = `./target/release/${safeName}`;
|
|
443
|
+
args = [];
|
|
444
|
+
} else if (target === "java") {
|
|
445
|
+
command = "java";
|
|
446
|
+
args = ["-jar", "target/mcp-server.jar"];
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const claudeConfig = {
|
|
450
|
+
mcpServers: {
|
|
451
|
+
[safeName]: {
|
|
452
|
+
command,
|
|
453
|
+
args,
|
|
454
|
+
env: {
|
|
455
|
+
API_KEY: "your-api-key-here",
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
const cursorConfig = claudeConfig;
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
claudeDesktopConfig: JSON.stringify(claudeConfig, null, 2),
|
|
465
|
+
cursorConfig: JSON.stringify(cursorConfig, null, 2),
|
|
466
|
+
};
|
|
467
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-file C# (.NET) MCP Server Generator
|
|
3
|
+
* Uses ModelContextProtocol.NET and Semantic Kernel conventions.
|
|
4
|
+
*/
|
|
5
|
+
import { toPascalCase, toCamelCase, toSnakeCase, estimateTokenCost } from "../common.js";
|
|
6
|
+
|
|
7
|
+
export function generateCSharpMcpServer(metadata, tools, resources, workflows, options = {}) {
|
|
8
|
+
const serverName = toPascalCase(options.serverName || metadata.title || "RapiDocMcpServer");
|
|
9
|
+
const budget = estimateTokenCost(tools, resources, workflows);
|
|
10
|
+
const defaultBaseUrl = metadata.servers?.[0]?.url || "http://localhost:3000";
|
|
11
|
+
|
|
12
|
+
const toolMethods = tools
|
|
13
|
+
.map((t) => {
|
|
14
|
+
const csharpArgs = t.parameters.map((p) => {
|
|
15
|
+
let csharpType = "string";
|
|
16
|
+
if (p.type === "integer") csharpType = "int";
|
|
17
|
+
else if (p.type === "number") csharpType = "double";
|
|
18
|
+
else if (p.type === "boolean") csharpType = "bool";
|
|
19
|
+
else if (p.type === "array") csharpType = "IEnumerable<string>";
|
|
20
|
+
else if (p.type === "object") csharpType = "Dictionary<string, object>";
|
|
21
|
+
const defaultVal = p.required ? "" : " = default";
|
|
22
|
+
return `[Description(${JSON.stringify(p.description || "")})] ${csharpType} ${toCamelCase(p.name)}${defaultVal}`;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
csharpArgs.push(
|
|
26
|
+
'[Description("Optional Bearer API key override for multi-tenant sessions")] string apiKey = default',
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const pathParams = t.parameters.filter((p) => p.in === "path");
|
|
30
|
+
const safePathCode =
|
|
31
|
+
pathParams.length > 0
|
|
32
|
+
? pathParams
|
|
33
|
+
.map(
|
|
34
|
+
(p) =>
|
|
35
|
+
` var safe${toPascalCase(p.name)} = Uri.EscapeDataString(${toCamelCase(p.name)}?.ToString() ?? "");`,
|
|
36
|
+
)
|
|
37
|
+
.join("\n") + "\n"
|
|
38
|
+
: "";
|
|
39
|
+
|
|
40
|
+
const pathSub = t.path.replace(/\{([^}]+)\}/g, (match, p1) => `{safe${toPascalCase(p1)}}`);
|
|
41
|
+
const queryParams = t.parameters.filter((p) => p.in === "query");
|
|
42
|
+
const bodyParams = t.parameters.filter((p) => p.in === "body");
|
|
43
|
+
const hasBody = ["POST", "PUT", "PATCH"].includes(t.method) && bodyParams.length > 0;
|
|
44
|
+
|
|
45
|
+
const queryCode =
|
|
46
|
+
queryParams.length > 0
|
|
47
|
+
? ` var queryParams = new List<string>();
|
|
48
|
+
${queryParams.map((p) => ` if (${toCamelCase(p.name)} != default) queryParams.Add("${p.name}=" + Uri.EscapeDataString(${toCamelCase(p.name)}.ToString()));`).join("\n")}
|
|
49
|
+
var queryString = queryParams.Count > 0 ? "?" + string.Join("&", queryParams) : "";\n`
|
|
50
|
+
: "";
|
|
51
|
+
|
|
52
|
+
const urlExpr = queryParams.length > 0 ? `$"${pathSub}" + queryString` : `$"${pathSub}"`;
|
|
53
|
+
|
|
54
|
+
const bodyCode = hasBody
|
|
55
|
+
? ` var bodyPayload = new Dictionary<string, object>();
|
|
56
|
+
${bodyParams.map((p) => ` if (${toCamelCase(p.name)} != default) bodyPayload["${p.name}"] = ${toCamelCase(p.name)};`).join("\n")}
|
|
57
|
+
if (bodyPayload.Count > 0)
|
|
58
|
+
{
|
|
59
|
+
request.Content = new StringContent(JsonSerializer.Serialize(bodyPayload), System.Text.Encoding.UTF8, "application/json");
|
|
60
|
+
}\n`
|
|
61
|
+
: "";
|
|
62
|
+
|
|
63
|
+
return `
|
|
64
|
+
[McpTool("${t.name}", ${JSON.stringify(t.description)})]
|
|
65
|
+
public static async Task<string> ${toPascalCase(t.name)}(${csharpArgs.join(", ")})
|
|
66
|
+
{
|
|
67
|
+
using var client = new HttpClient { BaseAddress = new Uri(BaseUrl.TrimEnd('/') + "/") };
|
|
68
|
+
var resolvedKey = !string.IsNullOrEmpty(apiKey) ? apiKey : ApiKey;
|
|
69
|
+
if (!string.IsNullOrEmpty(resolvedKey))
|
|
70
|
+
{
|
|
71
|
+
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", resolvedKey);
|
|
72
|
+
}
|
|
73
|
+
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
|
74
|
+
${safePathCode}${queryCode} var request = new HttpRequestMessage(HttpMethod.${toPascalCase(t.method)}, ${urlExpr});
|
|
75
|
+
${t.parameters
|
|
76
|
+
.filter((p) => p.in === "header")
|
|
77
|
+
.map(
|
|
78
|
+
(p) =>
|
|
79
|
+
` if (!string.IsNullOrEmpty(${toCamelCase(p.name)})) request.Headers.TryAddWithoutValidation("${p.name}", ${toCamelCase(p.name)});`,
|
|
80
|
+
)
|
|
81
|
+
.join("\n")}
|
|
82
|
+
${bodyCode} var response = await client.SendAsync(request);
|
|
83
|
+
return await response.Content.ReadAsStringAsync();
|
|
84
|
+
}`;
|
|
85
|
+
})
|
|
86
|
+
.join("\n");
|
|
87
|
+
|
|
88
|
+
const resourceMethods = resources
|
|
89
|
+
.map((r) => {
|
|
90
|
+
return `
|
|
91
|
+
[McpResource("${r.uri}", "${r.name}", ${JSON.stringify(r.description)})]
|
|
92
|
+
public static async Task<string> Get${toPascalCase(r.name)}(string uri = "")
|
|
93
|
+
{
|
|
94
|
+
using var client = new HttpClient { BaseAddress = new Uri(BaseUrl.TrimEnd('/') + "/") };
|
|
95
|
+
var resolvedKey = ApiKey;
|
|
96
|
+
var outboundQuery = HttpUtility.ParseQueryString(string.Empty);
|
|
97
|
+
|
|
98
|
+
if (!string.IsNullOrEmpty(uri) && uri.Contains("?"))
|
|
99
|
+
{
|
|
100
|
+
var parsedUri = new Uri(uri);
|
|
101
|
+
var incomingParams = HttpUtility.ParseQueryString(parsedUri.Query);
|
|
102
|
+
|
|
103
|
+
var dynamicKey = incomingParams["apiKey"] ?? incomingParams["api_key"];
|
|
104
|
+
if (!string.IsNullOrEmpty(dynamicKey)) resolvedKey = dynamicKey;
|
|
105
|
+
|
|
106
|
+
foreach (string key in incomingParams)
|
|
107
|
+
{
|
|
108
|
+
if (key != null && !key.Equals("apikey", StringComparison.OrdinalIgnoreCase) && !key.Equals("api_key", StringComparison.OrdinalIgnoreCase))
|
|
109
|
+
{
|
|
110
|
+
outboundQuery[key] = incomingParams[key];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!string.IsNullOrEmpty(resolvedKey)) client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", resolvedKey);
|
|
116
|
+
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("${r.mimeType || "application/json"}"));
|
|
117
|
+
|
|
118
|
+
var targetPath = "${r.path}" + (outboundQuery.Count > 0 ? "?" + outboundQuery.ToString() : "");
|
|
119
|
+
return await client.GetStringAsync(targetPath.TrimStart('/'));
|
|
120
|
+
}`;
|
|
121
|
+
})
|
|
122
|
+
.join("\n");
|
|
123
|
+
|
|
124
|
+
const workflowMethods = workflows
|
|
125
|
+
.map((w) => {
|
|
126
|
+
const paramKeys = w.parameters ? Object.keys(w.parameters) : [];
|
|
127
|
+
const args = paramKeys
|
|
128
|
+
.map(
|
|
129
|
+
(k) =>
|
|
130
|
+
`[Description(${JSON.stringify(w.parameters[k].description || "")})] string ${toCamelCase(k)}`,
|
|
131
|
+
)
|
|
132
|
+
.concat([
|
|
133
|
+
'[Description("Optional Bearer API key override for multi-tenant sessions")] string apiKey = default',
|
|
134
|
+
])
|
|
135
|
+
.join(", ");
|
|
136
|
+
const argKey = paramKeys[0] || "id";
|
|
137
|
+
|
|
138
|
+
return `
|
|
139
|
+
[McpTool("${w.name}", ${JSON.stringify(w.description || "Composite workflow")})]
|
|
140
|
+
public static async Task<string> ${toPascalCase(w.name)}(${args})
|
|
141
|
+
{
|
|
142
|
+
using var client = new HttpClient { BaseAddress = new Uri(BaseUrl.TrimEnd('/') + "/") };
|
|
143
|
+
var resolvedKey = !string.IsNullOrEmpty(apiKey) ? apiKey : ApiKey;
|
|
144
|
+
if (!string.IsNullOrEmpty(resolvedKey)) client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", resolvedKey);
|
|
145
|
+
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
|
146
|
+
|
|
147
|
+
var results = new Dictionary<string, object>();
|
|
148
|
+
${(w.steps || [])
|
|
149
|
+
.map(
|
|
150
|
+
(s, idx) => ` // Step ${idx + 1}: ${s.call}
|
|
151
|
+
var resp_${idx} = await client.GetStringAsync($"api/{Uri.EscapeDataString(${toCamelCase(argKey)} ?? "")}");
|
|
152
|
+
results["${s.call}"] = JsonSerializer.Deserialize<object>(resp_${idx});`,
|
|
153
|
+
)
|
|
154
|
+
.join("\n")}
|
|
155
|
+
|
|
156
|
+
return JsonSerializer.Serialize(results, new JsonSerializerOptions { WriteIndented = true });
|
|
157
|
+
}`;
|
|
158
|
+
})
|
|
159
|
+
.join("\n");
|
|
160
|
+
|
|
161
|
+
return `/*
|
|
162
|
+
Auto-generated by RapiDoc Curated MCP Generator
|
|
163
|
+
Spec: ${metadata.title} (v${metadata.version})
|
|
164
|
+
|
|
165
|
+
[MULTI-TENANCY & AUTHENTICATION]
|
|
166
|
+
- Local Stdio Mode: Claude Desktop, Cursor, and local agents use API_KEY from environment.
|
|
167
|
+
- Multi-Tenant / Agent Mode: When running in a shared service, the calling AI agent can pass
|
|
168
|
+
per-session credentials directly in the \`apiKey\` parameter of any tool.
|
|
169
|
+
|
|
170
|
+
[TOKEN BUDGET DIAGNOSTIC]
|
|
171
|
+
- Total Tools: ${budget.totalTools}
|
|
172
|
+
- Total Resources: ${budget.totalResources}
|
|
173
|
+
- Total Workflows: ${budget.totalWorkflows}
|
|
174
|
+
- Estimated Prompt Context Overhead: ~${budget.estimatedTokens.toLocaleString()} tokens/turn
|
|
175
|
+
${budget.warnings.map((w) => `- ${w}`).join("\n")}
|
|
176
|
+
|
|
177
|
+
[HOW TO RUN]
|
|
178
|
+
1. dotnet restore
|
|
179
|
+
2. dotnet run
|
|
180
|
+
*/
|
|
181
|
+
|
|
182
|
+
using System;
|
|
183
|
+
using System.Collections.Generic;
|
|
184
|
+
using System.ComponentModel;
|
|
185
|
+
using System.Linq;
|
|
186
|
+
using System.Net.Http;
|
|
187
|
+
using System.Net.Http.Headers;
|
|
188
|
+
using System.Text.Json;
|
|
189
|
+
using System.Threading.Tasks;
|
|
190
|
+
using System.Web;
|
|
191
|
+
using ModelContextProtocol.NET.Server;
|
|
192
|
+
|
|
193
|
+
namespace ${serverName};
|
|
194
|
+
|
|
195
|
+
public class Program
|
|
196
|
+
{
|
|
197
|
+
private static readonly string ApiKey = Environment.GetEnvironmentVariable("API_KEY") ?? "";
|
|
198
|
+
private static readonly string BaseUrl = Environment.GetEnvironmentVariable("API_BASE_URL") ?? "${defaultBaseUrl}";
|
|
199
|
+
|
|
200
|
+
public static async Task Main(string[] args)
|
|
201
|
+
{
|
|
202
|
+
var server = new McpServer(new McpServerOptions
|
|
203
|
+
{
|
|
204
|
+
ServerInfo = new() { Name = "${toSnakeCase(serverName)}", Version = "${metadata.version || "1.0.0"}" }
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
server.RegisterTools<Program>();
|
|
208
|
+
server.RegisterResources<Program>();
|
|
209
|
+
|
|
210
|
+
await server.StartAsync(Console.OpenStandardInput(), Console.OpenStandardOutput());
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
${resourceMethods}
|
|
214
|
+
${toolMethods}
|
|
215
|
+
${workflowMethods}
|
|
216
|
+
}
|
|
217
|
+
`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function generateCSharpFiles(serverCode, metadata) {
|
|
221
|
+
const csproj = `<Project Sdk="Microsoft.NET.Sdk">\n <PropertyGroup>\n <OutputType>Exe</OutputType>\n <TargetFramework>net8.0</TargetFramework>\n <ImplicitUsings>enable</ImplicitUsings>\n <Nullable>enable</Nullable>\n </PropertyGroup>\n <ItemGroup>\n <PackageReference Include="ModelContextProtocol.NET.Server" Version="1.0.0" />\n </ItemGroup>\n</Project>\n`;
|
|
222
|
+
const readme = `# ${metadata.title} - MCP Server (C# / .NET)
|
|
223
|
+
|
|
224
|
+
Auto-generated by RapiDoc Curated MCP Generator.
|
|
225
|
+
|
|
226
|
+
### Prerequisites
|
|
227
|
+
- .NET 8.0 SDK or newer
|
|
228
|
+
|
|
229
|
+
### Building & Running
|
|
230
|
+
\`\`\`bash
|
|
231
|
+
dotnet restore
|
|
232
|
+
export API_KEY="your-api-key"
|
|
233
|
+
dotnet run
|
|
234
|
+
\`\`\`
|
|
235
|
+
`;
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
"Program.cs": serverCode,
|
|
239
|
+
"Server.csproj": csproj,
|
|
240
|
+
"README.md": readme,
|
|
241
|
+
};
|
|
242
|
+
}
|