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,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-file Python FastMCP Server Generator
|
|
3
|
+
* Uses fastmcp with zero external networking dependencies (urllib.request).
|
|
4
|
+
*/
|
|
5
|
+
import { toSnakeCase, estimateTokenCost } from "../common.js";
|
|
6
|
+
|
|
7
|
+
export function generatePythonFastMcpServer(metadata, tools, resources, workflows, options = {}) {
|
|
8
|
+
const serverName = toSnakeCase(options.serverName || metadata.title || "rapidoc_mcp_server");
|
|
9
|
+
const budget = estimateTokenCost(tools, resources, workflows);
|
|
10
|
+
const defaultBaseUrl = metadata.servers?.[0]?.url || "http://localhost:3000";
|
|
11
|
+
|
|
12
|
+
const toolFunctions = tools
|
|
13
|
+
.map((t) => {
|
|
14
|
+
const pyArgs = t.parameters
|
|
15
|
+
.map((p) => {
|
|
16
|
+
let pyType = "str";
|
|
17
|
+
if (p.type === "integer") pyType = "int";
|
|
18
|
+
else if (p.type === "number") pyType = "float";
|
|
19
|
+
else if (p.type === "boolean") pyType = "bool";
|
|
20
|
+
else if (p.type === "array") {
|
|
21
|
+
const itemType = p.items?.type;
|
|
22
|
+
if (itemType === "integer") pyType = "List[int]";
|
|
23
|
+
else if (itemType === "number") pyType = "List[float]";
|
|
24
|
+
else if (itemType === "boolean") pyType = "List[bool]";
|
|
25
|
+
else if (itemType === "object") pyType = "List[Dict[str, Any]]";
|
|
26
|
+
else pyType = "List[str]";
|
|
27
|
+
} else if (p.type === "object") pyType = "Dict[str, Any]";
|
|
28
|
+
|
|
29
|
+
if (!p.required) {
|
|
30
|
+
return `${toSnakeCase(p.name)}: Optional[${pyType}] = None`;
|
|
31
|
+
}
|
|
32
|
+
return `${toSnakeCase(p.name)}: ${pyType}`;
|
|
33
|
+
})
|
|
34
|
+
.join(", ");
|
|
35
|
+
|
|
36
|
+
const allPyArgs = [pyArgs, "api_key: Optional[str] = None"].filter(Boolean).join(", ");
|
|
37
|
+
const pathParams = t.parameters.filter((p) => p.in === "path");
|
|
38
|
+
const safePathCode = pathParams
|
|
39
|
+
.map(
|
|
40
|
+
(p) =>
|
|
41
|
+
` safe_${toSnakeCase(p.name)} = urllib.parse.quote(str(${toSnakeCase(p.name)}))`,
|
|
42
|
+
)
|
|
43
|
+
.join("\n");
|
|
44
|
+
const pathSub = t.path.replace(/\{([^}]+)\}/g, (match, p1) => `{safe_${toSnakeCase(p1)}}`);
|
|
45
|
+
|
|
46
|
+
return `
|
|
47
|
+
@mcp.tool(
|
|
48
|
+
name="${t.name}",
|
|
49
|
+
description="${t.description.replace(/"/g, '\\"')}"
|
|
50
|
+
)
|
|
51
|
+
def ${t.name}(${allPyArgs}) -> str:
|
|
52
|
+
"""${t.description}${t.isConsequential ? " (⚠️ Consequential action - requires confirmation)" : ""}"""
|
|
53
|
+
${safePathCode ? safePathCode + "\n" : ""} url = f"{BASE_URL.rstrip('/')}${pathSub}"
|
|
54
|
+
headers = {"Accept": "application/json"}
|
|
55
|
+
${t.parameters
|
|
56
|
+
.filter((p) => p.in === "header")
|
|
57
|
+
.map(
|
|
58
|
+
(p) =>
|
|
59
|
+
` if ${toSnakeCase(p.name)} is not None:\n headers["${p.name}"] = str(${toSnakeCase(p.name)})`,
|
|
60
|
+
)
|
|
61
|
+
.join("\n")}
|
|
62
|
+
resolved_key = api_key or API_KEY
|
|
63
|
+
if resolved_key:
|
|
64
|
+
headers["Authorization"] = f"Bearer {resolved_key}"
|
|
65
|
+
|
|
66
|
+
params = {}
|
|
67
|
+
${t.parameters
|
|
68
|
+
.filter((p) => p.in === "query")
|
|
69
|
+
.map(
|
|
70
|
+
(p) =>
|
|
71
|
+
` if ${toSnakeCase(p.name)} is not None:\n params["${p.name}"] = ${toSnakeCase(p.name)}`,
|
|
72
|
+
)
|
|
73
|
+
.join("\n")}
|
|
74
|
+
if params:
|
|
75
|
+
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(params)
|
|
76
|
+
|
|
77
|
+
body_bytes = None
|
|
78
|
+
${
|
|
79
|
+
["POST", "PUT", "PATCH"].includes(t.method)
|
|
80
|
+
? ` body_payload = {}
|
|
81
|
+
${t.parameters
|
|
82
|
+
.filter((p) => p.in === "body")
|
|
83
|
+
.map(
|
|
84
|
+
(p) =>
|
|
85
|
+
` if ${toSnakeCase(p.name)} is not None:\n body_payload["${p.name}"] = ${toSnakeCase(p.name)}`,
|
|
86
|
+
)
|
|
87
|
+
.join("\n")}
|
|
88
|
+
if body_payload:
|
|
89
|
+
headers["Content-Type"] = "application/json"
|
|
90
|
+
body_bytes = json.dumps(body_payload).encode("utf-8")`
|
|
91
|
+
: ""
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
req = urllib.request.Request(url, data=body_bytes, headers=headers, method="${t.method}")
|
|
95
|
+
try:
|
|
96
|
+
with urllib.request.urlopen(req, timeout=30.0) as resp:
|
|
97
|
+
return resp.read().decode("utf-8")
|
|
98
|
+
except urllib.error.HTTPError as e:
|
|
99
|
+
return e.read().decode("utf-8")
|
|
100
|
+
except Exception as e:
|
|
101
|
+
return f"Error invoking ${t.name}: {str(e)}"
|
|
102
|
+
`;
|
|
103
|
+
})
|
|
104
|
+
.join("\n");
|
|
105
|
+
|
|
106
|
+
const resourceFunctions = resources
|
|
107
|
+
.map((r) => {
|
|
108
|
+
return `
|
|
109
|
+
@mcp.resource("${r.uri}")
|
|
110
|
+
def ${toSnakeCase(r.name)}(uri: str = "") -> str:
|
|
111
|
+
"""${r.description}"""
|
|
112
|
+
url = f"{BASE_URL.rstrip('/')}${r.path}"
|
|
113
|
+
headers = {"Accept": "${r.mimeType || "application/json"}"}
|
|
114
|
+
|
|
115
|
+
resolved_key = API_KEY
|
|
116
|
+
if uri and "?" in uri:
|
|
117
|
+
parsed_query = urllib.parse.parse_qs(urllib.parse.urlparse(uri).query)
|
|
118
|
+
passed_key = parsed_query.get("apiKey") or parsed_query.get("api_key")
|
|
119
|
+
if passed_key and passed_key[0].strip():
|
|
120
|
+
resolved_key = passed_key[0].strip()
|
|
121
|
+
|
|
122
|
+
if resolved_key:
|
|
123
|
+
headers["Authorization"] = f"Bearer {resolved_key}"
|
|
124
|
+
|
|
125
|
+
if uri and "?" in uri:
|
|
126
|
+
parsed_query = urllib.parse.parse_qs(urllib.parse.urlparse(uri).query)
|
|
127
|
+
params = {k: v[0] for k, v in parsed_query.items() if k.lower() not in ["apikey", "api_key"]}
|
|
128
|
+
if params:
|
|
129
|
+
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(params)
|
|
130
|
+
|
|
131
|
+
req = urllib.request.Request(url, headers=headers, method="GET")
|
|
132
|
+
try:
|
|
133
|
+
with urllib.request.urlopen(req, timeout=30.0) as resp:
|
|
134
|
+
return resp.read().decode("utf-8")
|
|
135
|
+
except urllib.error.HTTPError as e:
|
|
136
|
+
return e.read().decode("utf-8")
|
|
137
|
+
except Exception as e:
|
|
138
|
+
return f"Error loading resource: {str(e)}"
|
|
139
|
+
`;
|
|
140
|
+
})
|
|
141
|
+
.join("\n");
|
|
142
|
+
|
|
143
|
+
const workflowFunctions = workflows
|
|
144
|
+
.map((w) => {
|
|
145
|
+
const paramKeys = w.parameters ? Object.keys(w.parameters) : [];
|
|
146
|
+
const pyArgs = paramKeys
|
|
147
|
+
.map((k) => `${toSnakeCase(k)}: str`)
|
|
148
|
+
.concat(["api_key: Optional[str] = None"])
|
|
149
|
+
.join(", ");
|
|
150
|
+
|
|
151
|
+
return `
|
|
152
|
+
@mcp.tool(
|
|
153
|
+
name="${w.name}",
|
|
154
|
+
description="${(w.description || "Composite workflow").replace(/"/g, '\\"')}"
|
|
155
|
+
)
|
|
156
|
+
def ${toSnakeCase(w.name)}(${pyArgs}) -> str:
|
|
157
|
+
"""Composite workflow: ${(w.description || "").replace(/"/g, '\\"')}"""
|
|
158
|
+
results = {}
|
|
159
|
+
headers = {"Accept": "application/json"}
|
|
160
|
+
resolved_key = api_key or API_KEY
|
|
161
|
+
if resolved_key:
|
|
162
|
+
headers["Authorization"] = f"Bearer {resolved_key}"
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
${(w.steps || [])
|
|
166
|
+
.map((s, idx) => {
|
|
167
|
+
const argKey = paramKeys[0] || "id";
|
|
168
|
+
return ` # Step ${idx + 1}: ${s.call}
|
|
169
|
+
step_url_${idx} = f"{BASE_URL.rstrip('/')}/api/{urllib.parse.quote(str(${toSnakeCase(argKey)}))}"
|
|
170
|
+
req_${idx} = urllib.request.Request(step_url_${idx}, headers=headers, method="GET")
|
|
171
|
+
with urllib.request.urlopen(req_${idx}, timeout=30.0) as resp_${idx}:
|
|
172
|
+
results["${s.call}"] = json.loads(resp_${idx}.read().decode("utf-8"))`;
|
|
173
|
+
})
|
|
174
|
+
.join("\n")}
|
|
175
|
+
return json.dumps(results, indent=2)
|
|
176
|
+
except Exception as e:
|
|
177
|
+
return f"Workflow ${w.name} failed: {str(e)}"
|
|
178
|
+
`;
|
|
179
|
+
})
|
|
180
|
+
.join("\n");
|
|
181
|
+
|
|
182
|
+
return `"""
|
|
183
|
+
Auto-generated by RapiDoc Curated MCP Generator
|
|
184
|
+
Spec: ${metadata.title} (v${metadata.version})
|
|
185
|
+
|
|
186
|
+
[MULTI-TENANCY & AUTHENTICATION]
|
|
187
|
+
- Local Stdio Mode: Claude Desktop, Cursor, and local agents use API_KEY from environment.
|
|
188
|
+
- Multi-Tenant / Agent Mode: When running in a shared service, the calling AI agent can pass
|
|
189
|
+
per-session credentials directly in the \`api_key\` parameter of any tool.
|
|
190
|
+
|
|
191
|
+
[TOKEN BUDGET DIAGNOSTIC]
|
|
192
|
+
- Total Tools: ${budget.totalTools}
|
|
193
|
+
- Total Resources: ${budget.totalResources}
|
|
194
|
+
- Total Workflows: ${budget.totalWorkflows}
|
|
195
|
+
- Estimated Prompt Context Overhead: ~${budget.estimatedTokens.toLocaleString()} tokens/turn
|
|
196
|
+
${budget.warnings.map((w) => `- ${w}`).join("\n")}
|
|
197
|
+
|
|
198
|
+
[HOW TO RUN]
|
|
199
|
+
1. pip install fastmcp
|
|
200
|
+
2. fastmcp run server.py
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
import os
|
|
204
|
+
import json
|
|
205
|
+
import urllib.request
|
|
206
|
+
import urllib.parse
|
|
207
|
+
import urllib.error
|
|
208
|
+
from typing import Optional, List, Dict, Any
|
|
209
|
+
from fastmcp import FastMCP
|
|
210
|
+
|
|
211
|
+
API_KEY = os.environ.get("API_KEY", "")
|
|
212
|
+
BASE_URL = os.environ.get("API_BASE_URL", "${defaultBaseUrl}")
|
|
213
|
+
|
|
214
|
+
mcp = FastMCP("${serverName}")
|
|
215
|
+
|
|
216
|
+
${resourceFunctions}
|
|
217
|
+
${toolFunctions}
|
|
218
|
+
${workflowFunctions}
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
mcp.run()
|
|
222
|
+
`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function generatePythonFiles(serverCode, metadata) {
|
|
226
|
+
const readme = `# ${metadata.title} - MCP Server (Python FastMCP)
|
|
227
|
+
|
|
228
|
+
Auto-generated by RapiDoc Curated MCP Generator.
|
|
229
|
+
|
|
230
|
+
Zero external networking dependencies: uses Python standard library (\`urllib.request\`) + FastMCP.
|
|
231
|
+
|
|
232
|
+
### Prerequisites
|
|
233
|
+
- Python 3.10+
|
|
234
|
+
- \`pip install -r requirements.txt\`
|
|
235
|
+
|
|
236
|
+
### Running the Server
|
|
237
|
+
\`\`\`bash
|
|
238
|
+
export API_KEY="your-api-key"
|
|
239
|
+
fastmcp run server.py
|
|
240
|
+
\`\`\`
|
|
241
|
+
`;
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
"server.py": serverCode,
|
|
245
|
+
"requirements.txt": "fastmcp>=0.4.0\n",
|
|
246
|
+
"README.md": readme,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-file Rust MCP Server Generator
|
|
3
|
+
* Uses mcp_sdk / rmcp conventions.
|
|
4
|
+
*/
|
|
5
|
+
import { toSnakeCase, estimateTokenCost } from "../common.js";
|
|
6
|
+
|
|
7
|
+
export function generateRustMcpServer(metadata, tools, resources, workflows, options = {}) {
|
|
8
|
+
const serverName = toSnakeCase(options.serverName || metadata.title || "rapidoc_mcp_server");
|
|
9
|
+
const budget = estimateTokenCost(tools, resources, workflows);
|
|
10
|
+
const defaultBaseUrl = metadata.servers?.[0]?.url || "http://localhost:3000";
|
|
11
|
+
|
|
12
|
+
const toolRegistrations = tools
|
|
13
|
+
.map((t) => {
|
|
14
|
+
const propCalls = t.parameters
|
|
15
|
+
.map((p) => {
|
|
16
|
+
let rustSchemaType = "string";
|
|
17
|
+
if (p.type === "integer" || p.type === "number") rustSchemaType = "number";
|
|
18
|
+
else if (p.type === "boolean") rustSchemaType = "boolean";
|
|
19
|
+
else if (p.type === "array") rustSchemaType = "array";
|
|
20
|
+
else if (p.type === "object") rustSchemaType = "object";
|
|
21
|
+
return `\n .property("${toSnakeCase(p.name)}", "${rustSchemaType}", ${Boolean(p.required)}, ${JSON.stringify(p.description || "")})`;
|
|
22
|
+
})
|
|
23
|
+
.join("");
|
|
24
|
+
|
|
25
|
+
return ` server.add_tool(
|
|
26
|
+
Tool::new("${t.name}", ${JSON.stringify(t.description)})
|
|
27
|
+
${propCalls ? propCalls + "\n" : ""} .property("api_key", "string", false, "Optional Bearer API key override for multi-tenant sessions")
|
|
28
|
+
);`;
|
|
29
|
+
})
|
|
30
|
+
.join("\n\n");
|
|
31
|
+
|
|
32
|
+
const workflowRegistrations = workflows
|
|
33
|
+
.map((w) => {
|
|
34
|
+
const paramKeys = w.parameters ? Object.keys(w.parameters) : [];
|
|
35
|
+
const propCalls = paramKeys
|
|
36
|
+
.map(
|
|
37
|
+
(k) =>
|
|
38
|
+
`\n .property("${toSnakeCase(k)}", "string", true, ${JSON.stringify(w.parameters[k].description || "")})`,
|
|
39
|
+
)
|
|
40
|
+
.join("");
|
|
41
|
+
|
|
42
|
+
return ` server.add_tool(
|
|
43
|
+
Tool::new("${w.name}", ${JSON.stringify(w.description || "Composite workflow")})
|
|
44
|
+
${propCalls ? propCalls + "\n" : ""} .property("api_key", "string", false, "Optional Bearer API key override for multi-tenant sessions")
|
|
45
|
+
);`;
|
|
46
|
+
})
|
|
47
|
+
.join("\n\n");
|
|
48
|
+
|
|
49
|
+
const toolArms = tools
|
|
50
|
+
.map((t) => {
|
|
51
|
+
const pathSub = t.path.replace(/\{[^}]+\}/g, "{}");
|
|
52
|
+
const pathParams = t.parameters.filter((p) => p.in === "path");
|
|
53
|
+
const queryParams = t.parameters.filter((p) => p.in === "query");
|
|
54
|
+
const bodyParams = t.parameters.filter((p) => p.in === "body");
|
|
55
|
+
const hasBody = ["POST", "PUT", "PATCH"].includes(t.method) && bodyParams.length > 0;
|
|
56
|
+
|
|
57
|
+
const pathArgs = pathParams
|
|
58
|
+
.map(
|
|
59
|
+
(p) =>
|
|
60
|
+
`urlencoding::encode(args.get("${toSnakeCase(p.name)}").and_then(|v| v.as_str()).unwrap_or("")).as_ref()`,
|
|
61
|
+
)
|
|
62
|
+
.join(", ");
|
|
63
|
+
|
|
64
|
+
const queryCode =
|
|
65
|
+
queryParams.length > 0
|
|
66
|
+
? ` let mut query_params: Vec<(&str, String)> = Vec::new();
|
|
67
|
+
${queryParams
|
|
68
|
+
.map(
|
|
69
|
+
(p) => ` if let Some(val) = args.get("${toSnakeCase(p.name)}") {
|
|
70
|
+
let s = val.as_str().map(String::from).unwrap_or_else(|| val.to_string());
|
|
71
|
+
query_params.push(("${p.name}", s));
|
|
72
|
+
}`,
|
|
73
|
+
)
|
|
74
|
+
.join("\n")}
|
|
75
|
+
if !query_params.is_empty() {
|
|
76
|
+
req = req.query(&query_params);
|
|
77
|
+
}\n`
|
|
78
|
+
: "";
|
|
79
|
+
|
|
80
|
+
const bodyCode = hasBody
|
|
81
|
+
? ` let mut body_map = serde_json::Map::new();
|
|
82
|
+
${bodyParams
|
|
83
|
+
.map(
|
|
84
|
+
(p) => ` if let Some(val) = args.get("${toSnakeCase(p.name)}") {
|
|
85
|
+
body_map.insert("${p.name}".to_string(), val.clone());
|
|
86
|
+
}`,
|
|
87
|
+
)
|
|
88
|
+
.join("\n")}
|
|
89
|
+
if !body_map.is_empty() {
|
|
90
|
+
req = req.json(&body_map);
|
|
91
|
+
}\n`
|
|
92
|
+
: "";
|
|
93
|
+
|
|
94
|
+
return ` "${t.name}" => {
|
|
95
|
+
let url = format!("{}${pathSub}", base_url.trim_end_matches('/')${pathArgs ? `, ${pathArgs}` : ""});
|
|
96
|
+
let mut req = client.request(reqwest::Method::from_bytes(b"${t.method}")?, &url)
|
|
97
|
+
.header("Accept", "application/json");
|
|
98
|
+
${t.parameters
|
|
99
|
+
.filter((p) => p.in === "header")
|
|
100
|
+
.map(
|
|
101
|
+
(p) =>
|
|
102
|
+
` if let Some(h) = args.get("${p.name}").or_else(|| args.get("${toSnakeCase(p.name)}")).and_then(|v| v.as_str()) {\n req = req.header("${p.name}", h);\n }`,
|
|
103
|
+
)
|
|
104
|
+
.join("\n")}
|
|
105
|
+
let resolved_key = args.get("api_key").and_then(|v| v.as_str()).map(String::from).or_else(|| api_key.clone());
|
|
106
|
+
if let Some(ref key) = resolved_key {
|
|
107
|
+
req = req.header("Authorization", format!("Bearer {}", key));
|
|
108
|
+
}
|
|
109
|
+
${queryCode}${bodyCode} let resp = req.send().await?;
|
|
110
|
+
let text = resp.text().await?;
|
|
111
|
+
Ok(ToolResponse::text(text))
|
|
112
|
+
}`;
|
|
113
|
+
})
|
|
114
|
+
.join("\n");
|
|
115
|
+
|
|
116
|
+
const workflowArms = workflows
|
|
117
|
+
.map((w) => {
|
|
118
|
+
const paramKeys = w.parameters ? Object.keys(w.parameters) : [];
|
|
119
|
+
const argKey = paramKeys[0] || "id";
|
|
120
|
+
|
|
121
|
+
return ` "${w.name}" => {
|
|
122
|
+
let resolved_key = args.get("api_key").and_then(|v| v.as_str()).map(String::from).or_else(|| api_key.clone());
|
|
123
|
+
let param_val = args.get("${toSnakeCase(argKey)}").and_then(|v| v.as_str()).unwrap_or("");
|
|
124
|
+
let mut results = serde_json::Map::new();
|
|
125
|
+
${(w.steps || [])
|
|
126
|
+
.map(
|
|
127
|
+
(s, idx) => ` // Step ${idx + 1}: ${s.call}
|
|
128
|
+
let step_url_${idx} = format!("{}/api/{}", base_url.trim_end_matches('/'), urlencoding::encode(param_val));
|
|
129
|
+
let mut step_req_${idx} = client.get(&step_url_${idx}).header("Accept", "application/json");
|
|
130
|
+
if let Some(ref key) = resolved_key {
|
|
131
|
+
step_req_${idx} = step_req_${idx}.header("Authorization", format!("Bearer {}", key));
|
|
132
|
+
}
|
|
133
|
+
if let Ok(resp) = step_req_${idx}.send().await {
|
|
134
|
+
if let Ok(json_res) = resp.json::<serde_json::Value>().await {
|
|
135
|
+
results.insert("${s.call}".to_string(), json_res);
|
|
136
|
+
}
|
|
137
|
+
}`,
|
|
138
|
+
)
|
|
139
|
+
.join("\n")}
|
|
140
|
+
Ok(ToolResponse::text(serde_json::to_string_pretty(&results)?))
|
|
141
|
+
}`;
|
|
142
|
+
})
|
|
143
|
+
.join("\n");
|
|
144
|
+
|
|
145
|
+
const resourceArms = resources
|
|
146
|
+
.map((r) => {
|
|
147
|
+
return ` "${r.uri}" => {
|
|
148
|
+
let mut req = client.get(format!("{}{}", base_url.trim_end_matches('/'), "${r.path}")).header("Accept", "${r.mimeType}");
|
|
149
|
+
if !query_params.is_empty() {
|
|
150
|
+
req = req.query(&query_params);
|
|
151
|
+
}
|
|
152
|
+
if let Some(ref key) = resolved_key {
|
|
153
|
+
req = req.header("Authorization", format!("Bearer {}", key));
|
|
154
|
+
}
|
|
155
|
+
let resp = req.send().await.map_err(|e| Box::new(e) as Box<dyn Error>)?;
|
|
156
|
+
let text = resp.text().await.map_err(|e| Box::new(e) as Box<dyn Error>)?;
|
|
157
|
+
Ok(vec![mcp_sdk::server::ResourceContents::text("${r.uri}", "${r.mimeType}", text)])
|
|
158
|
+
}`;
|
|
159
|
+
})
|
|
160
|
+
.join("\n");
|
|
161
|
+
|
|
162
|
+
const hasResources = resources.length > 0;
|
|
163
|
+
const resourceRegistration = hasResources
|
|
164
|
+
? `
|
|
165
|
+
server.on_read_resource(move |uri| {
|
|
166
|
+
let client = client.clone();
|
|
167
|
+
let api_key = api_key.clone();
|
|
168
|
+
let base_url = base_url.clone();
|
|
169
|
+
async move {
|
|
170
|
+
let (base_uri, query_str) = uri.split_once('?').unwrap_or((uri.as_str(), ""));
|
|
171
|
+
let mut resolved_key = api_key.clone();
|
|
172
|
+
let mut query_params: Vec<(&str, &str)> = Vec::new();
|
|
173
|
+
|
|
174
|
+
if !query_str.is_empty() {
|
|
175
|
+
for pair in query_str.split('&') {
|
|
176
|
+
if let Some((k, v)) = pair.split_once('=') {
|
|
177
|
+
if k.eq_ignore_ascii_case("apikey") || k.eq_ignore_ascii_case("api_key") {
|
|
178
|
+
if !v.is_empty() {
|
|
179
|
+
resolved_key = Some(v.to_string());
|
|
180
|
+
}
|
|
181
|
+
} else if !k.is_empty() {
|
|
182
|
+
query_params.push((k, v));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
match base_uri {
|
|
189
|
+
${resourceArms}
|
|
190
|
+
_ => Err(format!("Unknown resource: {}", uri).into()),
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
});`
|
|
194
|
+
: "";
|
|
195
|
+
|
|
196
|
+
return `/*
|
|
197
|
+
Auto-generated by RapiDoc Curated MCP Generator
|
|
198
|
+
Spec: ${metadata.title} (v${metadata.version})
|
|
199
|
+
|
|
200
|
+
[MULTI-TENANCY & AUTHENTICATION]
|
|
201
|
+
- Local Stdio Mode: Claude Desktop, Cursor, and local agents use API_KEY from environment.
|
|
202
|
+
- Multi-Tenant / Agent Mode: When running in a shared service, the calling AI agent can pass
|
|
203
|
+
per-session credentials directly in the \`api_key\` parameter of any tool.
|
|
204
|
+
|
|
205
|
+
[TOKEN BUDGET DIAGNOSTIC]
|
|
206
|
+
- Total Tools: ${budget.totalTools}
|
|
207
|
+
- Total Resources: ${budget.totalResources}
|
|
208
|
+
- Total Workflows: ${budget.totalWorkflows}
|
|
209
|
+
- Estimated Prompt Context Overhead: ~${budget.estimatedTokens.toLocaleString()} tokens/turn
|
|
210
|
+
${budget.warnings.map((w) => `- ${w}`).join("\n")}
|
|
211
|
+
|
|
212
|
+
[HOW TO RUN]
|
|
213
|
+
1. cargo build --release
|
|
214
|
+
2. cargo run
|
|
215
|
+
*/
|
|
216
|
+
|
|
217
|
+
use std::env;
|
|
218
|
+
use std::error::Error;
|
|
219
|
+
use mcp_sdk::server::{Server, StdioTransport, Tool, ToolResponse};
|
|
220
|
+
use serde_json::Value;
|
|
221
|
+
|
|
222
|
+
#[tokio::main]
|
|
223
|
+
async fn main() -> Result<(), Box<dyn Error>> {
|
|
224
|
+
let api_key = env::var("API_KEY").ok();
|
|
225
|
+
let base_url = env::var("API_BASE_URL").unwrap_or_else(|_| "${defaultBaseUrl}".to_string());
|
|
226
|
+
let client = reqwest::Client::new();
|
|
227
|
+
|
|
228
|
+
let mut server = Server::new("${serverName}", "${metadata.version || "1.0.0"}");
|
|
229
|
+
|
|
230
|
+
${toolRegistrations}
|
|
231
|
+
${workflowRegistrations ? "\n" + workflowRegistrations : ""}
|
|
232
|
+
|
|
233
|
+
server.on_call_tool(move |name, args: Value| {
|
|
234
|
+
let client = client.clone();
|
|
235
|
+
let api_key = api_key.clone();
|
|
236
|
+
let base_url = base_url.clone();
|
|
237
|
+
async move {
|
|
238
|
+
let result: Result<ToolResponse, Box<dyn Error>> = async {
|
|
239
|
+
match name.as_str() {
|
|
240
|
+
${toolArms}
|
|
241
|
+
${workflowArms}
|
|
242
|
+
_ => Err(format!("Unknown tool or workflow: {}", name).into()),
|
|
243
|
+
}
|
|
244
|
+
}.await;
|
|
245
|
+
|
|
246
|
+
match result {
|
|
247
|
+
Ok(resp) => Ok(resp),
|
|
248
|
+
Err(e) => Ok(ToolResponse::error(format!("Execution failed: {}", e))),
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
});${resourceRegistration}
|
|
252
|
+
|
|
253
|
+
server.listen(StdioTransport::new()).await?;
|
|
254
|
+
Ok(())
|
|
255
|
+
}
|
|
256
|
+
`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function generateRustFiles(serverCode, metadata, options = {}) {
|
|
260
|
+
const cargoToml = `[package]\nname = "${toSnakeCase(options.serverName || metadata.title || "rapidoc_mcp")}"\nversion = "0.1.0"\nedition = "2021"\n\n[dependencies]\nmcp-sdk = "0.1"\ntokio = { version = "1.0", features = ["full"] }\nreqwest = { version = "0.11", features = ["json"] }\nserde_json = "1.0"\nurlencoding = "2.1"\n`;
|
|
261
|
+
const readme = `# ${metadata.title} - MCP Server (Rust)
|
|
262
|
+
|
|
263
|
+
Auto-generated by RapiDoc Curated MCP Generator.
|
|
264
|
+
|
|
265
|
+
### Prerequisites
|
|
266
|
+
- Rust & Cargo (1.75+)
|
|
267
|
+
|
|
268
|
+
### Building & Running
|
|
269
|
+
\`\`\`bash
|
|
270
|
+
cargo build --release
|
|
271
|
+
export API_KEY="your-api-key"
|
|
272
|
+
./target/release/${toSnakeCase(options.serverName || metadata.title || "rapidoc_mcp")}
|
|
273
|
+
\`\`\`
|
|
274
|
+
`;
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
"src/main.rs": serverCode,
|
|
278
|
+
"Cargo.toml": cargoToml,
|
|
279
|
+
"README.md": readme,
|
|
280
|
+
};
|
|
281
|
+
}
|