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.
Files changed (80) hide show
  1. package/README.md +46 -31
  2. package/dist/rapidoc-min.js +1424 -213
  3. package/dist/rapidoc-static-min.js +13542 -0
  4. package/package.json +47 -35
  5. package/src/components/api-request.ts +869 -0
  6. package/src/components/api-response.ts +509 -0
  7. package/src/components/json-tree.ts +171 -0
  8. package/src/components/schema-base-element.ts +315 -0
  9. package/src/components/schema-table.ts +430 -0
  10. package/src/components/schema-tree.ts +386 -0
  11. package/src/components/tag-input.ts +184 -0
  12. package/src/index.js +6 -0
  13. package/src/oauth-receiver.ts +29 -0
  14. package/src/rapidoc-mini.ts +582 -0
  15. package/src/rapidoc.ts +1808 -0
  16. package/src/static/index.d.ts +149 -0
  17. package/src/static/index.js +62 -0
  18. package/src/static/navigation.js +497 -0
  19. package/src/static/render.js +490 -0
  20. package/src/static/serializer.js +216 -0
  21. package/src/styles/auth-styles.js +318 -0
  22. package/src/styles/border-styles.js +24 -0
  23. package/src/styles/css-bundle.js +129 -0
  24. package/src/styles/custom-styles.js +8 -0
  25. package/src/styles/dialog-box-styles.js +102 -0
  26. package/src/styles/endpoint-styles.js +559 -0
  27. package/src/styles/flex-styles.js +16 -0
  28. package/src/styles/font-styles.js +432 -0
  29. package/src/styles/info-styles.js +36 -0
  30. package/src/styles/input-styles.js +555 -0
  31. package/src/styles/layout-styles.js +396 -0
  32. package/src/styles/microlighter-styles.js +99 -0
  33. package/src/styles/nav-styles.js +609 -0
  34. package/src/styles/param-list-styles.js +261 -0
  35. package/src/styles/schema-styles.js +288 -0
  36. package/src/styles/tab-styles.js +106 -0
  37. package/src/styles/theme-tokens.js +582 -0
  38. package/src/templates/advanced-search-dialog-template.js +94 -0
  39. package/src/templates/api-response-template.js +219 -0
  40. package/src/templates/callback-template.js +86 -0
  41. package/src/templates/code-samples-template.js +52 -0
  42. package/src/templates/components-template.js +86 -0
  43. package/src/templates/downloads-template.js +85 -0
  44. package/src/templates/endpoint-template.js +386 -0
  45. package/src/templates/expanded-endpoint-template.js +266 -0
  46. package/src/templates/focused-endpoint-template.js +145 -0
  47. package/src/templates/header-template.js +92 -0
  48. package/src/templates/logo-template.js +21 -0
  49. package/src/templates/main-body-template.js +102 -0
  50. package/src/templates/navbar-template.js +489 -0
  51. package/src/templates/overview-template.js +104 -0
  52. package/src/templates/request-body-template.js +651 -0
  53. package/src/templates/request-params-template.js +474 -0
  54. package/src/templates/security-scheme-template.js +1263 -0
  55. package/src/templates/server-template.js +152 -0
  56. package/src/utils/auto-color-scheme.js +99 -0
  57. package/src/utils/bind-navbar.js +175 -0
  58. package/src/utils/color-utils.js +59 -0
  59. package/src/utils/common-utils.js +191 -0
  60. package/src/utils/curl-utils.js +123 -0
  61. package/src/utils/highlighter.js +416 -0
  62. package/src/utils/markdown-utils.js +110 -0
  63. package/src/utils/mcp/common.js +467 -0
  64. package/src/utils/mcp/generators/csharp.js +242 -0
  65. package/src/utils/mcp/generators/go.js +298 -0
  66. package/src/utils/mcp/generators/java.js +330 -0
  67. package/src/utils/mcp/generators/python.js +248 -0
  68. package/src/utils/mcp/generators/rust.js +281 -0
  69. package/src/utils/mcp/generators/typescript.js +290 -0
  70. package/src/utils/mcp-generator.js +143 -0
  71. package/src/utils/mock-interceptor.js +508 -0
  72. package/src/utils/oauth-utils.js +87 -0
  73. package/src/utils/request-utils.js +203 -0
  74. package/src/utils/response-utils.js +203 -0
  75. package/src/utils/sanitize.js +57 -0
  76. package/src/utils/schema-utils.js +1927 -0
  77. package/src/utils/slugger.js +189 -0
  78. package/src/utils/spec-parser.js +915 -0
  79. package/src/utils/url-utils.js +211 -0
  80. 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
+ }