rapidoc 12.0.0 → 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 +127 -25
- package/dist/rapidoc-flow-min.js +580 -0
- package/dist/rapidoc-flow-min.js.LICENSE.txt +171 -0
- package/dist/rapidoc-min.js +541 -1309
- package/dist/rapidoc-min.js.LICENSE.txt +381 -0
- package/package.json +21 -18
- package/src/README.md +150 -0
- package/src/components/README.md +91 -0
- package/src/components/api-request.ts +43 -0
- package/src/components/api-response.ts +9 -4
- package/src/components/schema-base-element.ts +18 -4
- package/src/components/schema-table.ts +58 -13
- package/src/components/schema-tree.ts +7 -0
- package/src/components/tag-input.ts +5 -9
- package/src/flow.js +3 -0
- package/src/rapi-flow.ts +1178 -0
- package/src/rapidoc-mini.ts +27 -6
- package/src/rapidoc.ts +108 -62
- package/src/styles/README.md +96 -0
- package/src/styles/css-bundle.js +2 -2
- package/src/styles/endpoint-styles.js +6 -1
- package/src/styles/flow-styles.js +248 -0
- package/src/styles/nav-styles.js +39 -15
- package/src/styles/schema-styles.js +13 -5
- package/src/styles/theme-tokens.js +2 -2
- package/src/templates/README.md +89 -0
- package/src/templates/api-response-template.js +14 -10
- package/src/templates/callback-template.js +1 -1
- package/src/templates/downloads-template.js +12 -27
- package/src/templates/endpoint-template.js +15 -24
- package/src/templates/expanded-endpoint-template.js +8 -4
- package/src/templates/focused-endpoint-template.js +4 -3
- package/src/templates/navbar-template.js +20 -11
- package/src/templates/request-body-template.js +29 -7
- package/src/templates/request-params-template.js +15 -4
- package/src/templates/security-scheme-template.js +11 -2
- package/src/templates/server-template.js +3 -2
- 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/curl-utils.js +5 -2
- package/src/utils/highlighter.js +3 -0
- package/src/utils/mcp/README.md +155 -0
- package/src/utils/mcp/common.js +45 -346
- 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 +29 -125
- package/src/utils/mock-interceptor.js +134 -85
- package/src/utils/request-utils.js +61 -0
- package/src/utils/schema-utils.js +33 -6
- package/src/utils/slugger.js +5 -7
- package/src/utils/spec-parser.js +190 -164
- package/src/utils/url-utils.js +36 -1
- package/dist/rapidoc-static-min.js +0 -13542
- package/src/static/index.d.ts +0 -149
- package/src/static/index.js +0 -62
- package/src/static/navigation.js +0 -497
- package/src/static/render.js +0 -490
- package/src/static/serializer.js +0 -216
- package/src/utils/mcp/generators/csharp.js +0 -242
- package/src/utils/mcp/generators/go.js +0 -298
- package/src/utils/mcp/generators/java.js +0 -330
- package/src/utils/mcp/generators/python.js +0 -248
- package/src/utils/mcp/generators/rust.js +0 -281
- package/src/utils/mcp/generators/typescript.js +0 -290
|
@@ -1,298 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Single-file Go MCP Server Generator
|
|
3
|
-
* Uses mark3labs/mcp-go framework.
|
|
4
|
-
*/
|
|
5
|
-
import { toSnakeCase, estimateTokenCost } from "../common.js";
|
|
6
|
-
|
|
7
|
-
export function generateGoMcpServer(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 paramModifiers = t.parameters
|
|
15
|
-
.map((p) => {
|
|
16
|
-
const isReq = p.required ? "mcp.Required()" : "";
|
|
17
|
-
const desc = p.description ? `mcp.Description(${JSON.stringify(p.description)})` : "";
|
|
18
|
-
const mods = [isReq, desc].filter(Boolean).join(", ");
|
|
19
|
-
if (p.type === "integer" || p.type === "number") {
|
|
20
|
-
return `\t\tmcp.WithNumber("${toSnakeCase(p.name)}"${mods ? `, ${mods}` : ""}),`;
|
|
21
|
-
}
|
|
22
|
-
if (p.type === "boolean") {
|
|
23
|
-
return `\t\tmcp.WithBoolean("${toSnakeCase(p.name)}"${mods ? `, ${mods}` : ""}),`;
|
|
24
|
-
}
|
|
25
|
-
if (p.type === "array") {
|
|
26
|
-
return `\t\tmcp.WithList("${toSnakeCase(p.name)}"${mods ? `, ${mods}` : ""}),`;
|
|
27
|
-
}
|
|
28
|
-
if (p.type === "object") {
|
|
29
|
-
return `\t\tmcp.WithObject("${toSnakeCase(p.name)}"${mods ? `, ${mods}` : ""}),`;
|
|
30
|
-
}
|
|
31
|
-
return `\t\tmcp.WithString("${toSnakeCase(p.name)}"${mods ? `, ${mods}` : ""}),`;
|
|
32
|
-
})
|
|
33
|
-
.join("\n");
|
|
34
|
-
|
|
35
|
-
const queryParams = t.parameters.filter((p) => p.in === "query");
|
|
36
|
-
const bodyParams = t.parameters.filter((p) => p.in === "body");
|
|
37
|
-
const hasBody = ["POST", "PUT", "PATCH"].includes(t.method) && bodyParams.length > 0;
|
|
38
|
-
|
|
39
|
-
const allParamModifiers = paramModifiers
|
|
40
|
-
? `${paramModifiers}\n\t\tmcp.WithString("api_key", mcp.Description("Optional Bearer API key override for multi-tenant sessions")),`
|
|
41
|
-
: `\t\tmcp.WithString("api_key", mcp.Description("Optional Bearer API key override for multi-tenant sessions")),`;
|
|
42
|
-
|
|
43
|
-
const pathSub = t.path.replace(/\{[^}]+\}/g, "%s");
|
|
44
|
-
const pathParams = t.parameters
|
|
45
|
-
.filter((p) => p.in === "path")
|
|
46
|
-
.map((p) => `url.PathEscape(fmt.Sprintf("%v", args["${toSnakeCase(p.name)}"]))`)
|
|
47
|
-
.join(", ");
|
|
48
|
-
|
|
49
|
-
const queryCode =
|
|
50
|
-
queryParams.length > 0
|
|
51
|
-
? `\t\tqueryParams := url.Values{}
|
|
52
|
-
${queryParams.map((p) => `\t\tif v, ok := args["${toSnakeCase(p.name)}"]; ok && v != nil {\n\t\t\tqueryParams.Set("${p.name}", fmt.Sprintf("%v", v))\n\t\t}`).join("\n")}
|
|
53
|
-
\t\tif len(queryParams) > 0 {
|
|
54
|
-
\t\t\tif strings.Contains(urlStr, "?") {
|
|
55
|
-
\t\t\t\turlStr += "&" + queryParams.Encode()
|
|
56
|
-
\t\t\t} else {
|
|
57
|
-
\t\t\t\turlStr += "?" + queryParams.Encode()
|
|
58
|
-
\t\t\t}
|
|
59
|
-
\t\t}\n`
|
|
60
|
-
: "";
|
|
61
|
-
|
|
62
|
-
const bodyCode = hasBody
|
|
63
|
-
? `\t\tvar bodyReader io.Reader
|
|
64
|
-
\t\tbodyPayload := make(map[string]any)
|
|
65
|
-
${bodyParams.map((p) => `\t\tif v, ok := args["${toSnakeCase(p.name)}"]; ok && v != nil {\n\t\t\tbodyPayload["${p.name}"] = v\n\t\t}`).join("\n")}
|
|
66
|
-
\t\tif len(bodyPayload) > 0 {
|
|
67
|
-
\t\t\tb, _ := json.Marshal(bodyPayload)
|
|
68
|
-
\t\t\tbodyReader = bytes.NewBuffer(b)
|
|
69
|
-
\t\t}
|
|
70
|
-
\t\treq, err := http.NewRequestWithContext(ctx, "${t.method}", urlStr, bodyReader)
|
|
71
|
-
\t\tif err != nil {
|
|
72
|
-
\t\t\treturn mcp.NewToolResultError(fmt.Sprintf("Failed to create request: %v", err)), nil
|
|
73
|
-
\t\t}
|
|
74
|
-
\t\tif bodyReader != nil {
|
|
75
|
-
\t\t\treq.Header.Set("Content-Type", "application/json")
|
|
76
|
-
\t\t}\n`
|
|
77
|
-
: `\t\treq, err := http.NewRequestWithContext(ctx, "${t.method}", urlStr, nil)
|
|
78
|
-
\t\tif err != nil {
|
|
79
|
-
\t\t\treturn mcp.NewToolResultError(fmt.Sprintf("Failed to create request: %v", err)), nil
|
|
80
|
-
\t\t}\n`;
|
|
81
|
-
|
|
82
|
-
return `
|
|
83
|
-
\t// Tool: ${t.name} (${t.method} ${t.path})
|
|
84
|
-
\ts.AddTool(mcp.NewTool("${t.name}",
|
|
85
|
-
\t\tmcp.WithDescription(${JSON.stringify(t.description)}),
|
|
86
|
-
${allParamModifiers}
|
|
87
|
-
\t), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
88
|
-
\t\targs := request.Params.Arguments
|
|
89
|
-
\t\turlStr := fmt.Sprintf("%s${pathSub}", strings.TrimRight(baseURL, "/")${pathParams ? `, ${pathParams}` : ""})
|
|
90
|
-
${queryCode}${bodyCode}\t\treq.Header.Set("Accept", "application/json")
|
|
91
|
-
${t.parameters
|
|
92
|
-
.filter((p) => p.in === "header")
|
|
93
|
-
.map(
|
|
94
|
-
(p) =>
|
|
95
|
-
`\t\tif v, ok := args["${toSnakeCase(p.name)}"]; ok && v != nil {\n\t\t\treq.Header.Set("${p.name}", fmt.Sprintf("%v", v))\n\t\t}`,
|
|
96
|
-
)
|
|
97
|
-
.join("\n")}
|
|
98
|
-
\t\tresolvedKey := apiKey
|
|
99
|
-
\t\tif keyVal, ok := args["api_key"].(string); ok && keyVal != "" {
|
|
100
|
-
\t\t\tresolvedKey = keyVal
|
|
101
|
-
\t\t}
|
|
102
|
-
\t\tif resolvedKey != "" {
|
|
103
|
-
\t\t\treq.Header.Set("Authorization", "Bearer "+resolvedKey)
|
|
104
|
-
\t\t}
|
|
105
|
-
\t\tresp, err := client.Do(req)
|
|
106
|
-
\t\tif err != nil {
|
|
107
|
-
\t\t\treturn mcp.NewToolResultError(fmt.Sprintf("API request failed: %v", err)), nil
|
|
108
|
-
\t\t}
|
|
109
|
-
\t\tdefer resp.Body.Close()
|
|
110
|
-
\t\tbody, _ := io.ReadAll(resp.Body)
|
|
111
|
-
\t\treturn mcp.NewToolResultText(string(body)), nil
|
|
112
|
-
\t})
|
|
113
|
-
`;
|
|
114
|
-
})
|
|
115
|
-
.join("\n");
|
|
116
|
-
|
|
117
|
-
const resourceRegistrations = resources
|
|
118
|
-
.map((r) => {
|
|
119
|
-
return `
|
|
120
|
-
\t// Resource: ${r.name} (${r.uri})
|
|
121
|
-
\ts.AddResource(mcp.NewResource("${r.uri}", "${toSnakeCase(r.name)}", mcp.WithResourceDescription(${JSON.stringify(r.description)})),
|
|
122
|
-
\t\tfunc(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
|
|
123
|
-
\t\t\ttargetURL, err := url.Parse(strings.TrimRight(baseURL, "/") + "${r.path}")
|
|
124
|
-
\t\t\tif err != nil {
|
|
125
|
-
\t\t\t\treturn nil, err
|
|
126
|
-
\t\t\t}
|
|
127
|
-
|
|
128
|
-
\t\t\tresolvedKey := apiKey
|
|
129
|
-
\t\t\tif incomingURL, err := url.Parse(request.Params.URI); err == nil {
|
|
130
|
-
\t\t\t\tq := incomingURL.Query()
|
|
131
|
-
\t\t\t\tif passedKey := q.Get("apiKey"); passedKey != "" {
|
|
132
|
-
\t\t\t\t\tresolvedKey = passedKey
|
|
133
|
-
\t\t\t\t} else if passedKey := q.Get("api_key"); passedKey != "" {
|
|
134
|
-
\t\t\t\t\tresolvedKey = passedKey
|
|
135
|
-
\t\t\t\t}
|
|
136
|
-
|
|
137
|
-
\t\t\t\ttargetQuery := targetURL.Query()
|
|
138
|
-
\t\t\t\tfor k, v := range q {
|
|
139
|
-
\t\t\t\t\tif strings.ToLower(k) != "apikey" && strings.ToLower(k) != "api_key" && len(v) > 0 {
|
|
140
|
-
\t\t\t\t\t\ttargetQuery.Set(k, v[0])
|
|
141
|
-
\t\t\t\t\t}
|
|
142
|
-
\t\t\t\t}
|
|
143
|
-
\t\t\t\ttargetURL.RawQuery = targetQuery.Encode()
|
|
144
|
-
\t\t\t}
|
|
145
|
-
|
|
146
|
-
\t\t\treq, err := http.NewRequestWithContext(ctx, "GET", targetURL.String(), nil)
|
|
147
|
-
\t\t\tif err != nil {
|
|
148
|
-
\t\t\t\treturn nil, err
|
|
149
|
-
\t\t\t}
|
|
150
|
-
\t\t\treq.Header.Set("Accept", "${r.mimeType || "application/json"}")
|
|
151
|
-
\t\t\tif resolvedKey != "" {
|
|
152
|
-
\t\t\t\treq.Header.Set("Authorization", "Bearer "+resolvedKey)
|
|
153
|
-
\t\t\t}
|
|
154
|
-
\t\t\tresp, err := client.Do(req)
|
|
155
|
-
\t\t\tif err != nil {
|
|
156
|
-
\t\t\t\treturn nil, err
|
|
157
|
-
\t\t\t}
|
|
158
|
-
\t\t\tdefer resp.Body.Close()
|
|
159
|
-
\t\tbody, _ := io.ReadAll(resp.Body)
|
|
160
|
-
\t\treturn []mcp.ResourceContents{mcp.TextResourceContents{URI: request.Params.URI, MIMEType: "${r.mimeType || "application/json"}", Text: string(body)}}, nil
|
|
161
|
-
\t\t})`;
|
|
162
|
-
})
|
|
163
|
-
.join("\n");
|
|
164
|
-
|
|
165
|
-
const workflowRegistrations = workflows
|
|
166
|
-
.map((w) => {
|
|
167
|
-
const paramKeys = w.parameters ? Object.keys(w.parameters) : [];
|
|
168
|
-
const paramMods = paramKeys
|
|
169
|
-
.map(
|
|
170
|
-
(k) =>
|
|
171
|
-
`\t\tmcp.WithString("${toSnakeCase(k)}", mcp.Required(), mcp.Description(${JSON.stringify(w.parameters[k].description || "")})),`,
|
|
172
|
-
)
|
|
173
|
-
.concat([
|
|
174
|
-
`\t\tmcp.WithString("api_key", mcp.Description("Optional Bearer API key override for multi-tenant sessions")),`,
|
|
175
|
-
])
|
|
176
|
-
.join("\n");
|
|
177
|
-
|
|
178
|
-
return `
|
|
179
|
-
\t// Composite Workflow: ${w.name}
|
|
180
|
-
\ts.AddTool(mcp.NewTool("${w.name}",
|
|
181
|
-
\t\tmcp.WithDescription(${JSON.stringify(w.description || "Composite workflow")}),
|
|
182
|
-
${paramMods}
|
|
183
|
-
\t), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
184
|
-
\t\tresults := make(map[string]any)
|
|
185
|
-
\t\tparamVal, _ := request.Params.Arguments["${toSnakeCase(paramKeys[0] || "id")}"].(string)
|
|
186
|
-
\t\tresolvedKey := apiKey
|
|
187
|
-
\t\tif keyVal, ok := request.Params.Arguments["api_key"].(string); ok && keyVal != "" {
|
|
188
|
-
\t\t\tresolvedKey = keyVal
|
|
189
|
-
\t\t}
|
|
190
|
-
${(w.steps || [])
|
|
191
|
-
.map((s, idx) => {
|
|
192
|
-
return `\t\t// Step ${idx + 1}: ${s.call}
|
|
193
|
-
\t\tstepReq_${idx}, _ := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/api/%s", strings.TrimRight(baseURL, "/"), url.QueryEscape(paramVal)), nil)
|
|
194
|
-
\t\tstepReq_${idx}.Header.Set("Accept", "application/json")
|
|
195
|
-
\t\tif resolvedKey != "" { stepReq_${idx}.Header.Set("Authorization", "Bearer "+resolvedKey) }
|
|
196
|
-
\t\tif resp_${idx}, err := client.Do(stepReq_${idx}); err == nil {
|
|
197
|
-
\t\t\tvar jsonRes any
|
|
198
|
-
\t\t\tjson.NewDecoder(resp_${idx}.Body).Decode(&jsonRes)
|
|
199
|
-
\t\t\tresults["${s.call}"] = jsonRes
|
|
200
|
-
\t\t\tresp_${idx}.Body.Close()
|
|
201
|
-
\t\t}`;
|
|
202
|
-
})
|
|
203
|
-
.join("\n")}
|
|
204
|
-
\t\toutBytes, _ := json.MarshalIndent(results, "", " ")
|
|
205
|
-
\t\treturn mcp.NewToolResultText(string(outBytes)), nil
|
|
206
|
-
\t})`;
|
|
207
|
-
})
|
|
208
|
-
.join("\n");
|
|
209
|
-
|
|
210
|
-
return `/*
|
|
211
|
-
Auto-generated by RapiDoc Curated MCP Generator
|
|
212
|
-
Spec: ${metadata.title} (v${metadata.version})
|
|
213
|
-
|
|
214
|
-
[MULTI-TENANCY & AUTHENTICATION]
|
|
215
|
-
- Local Stdio Mode: Claude Desktop, Cursor, and local agents use API_KEY from environment.
|
|
216
|
-
- Multi-Tenant / Agent Mode: When running in a shared service, the calling AI agent can pass
|
|
217
|
-
per-session credentials directly in the \`api_key\` parameter of any tool.
|
|
218
|
-
|
|
219
|
-
[TOKEN BUDGET DIAGNOSTIC]
|
|
220
|
-
- Total Tools: ${budget.totalTools}
|
|
221
|
-
- Total Resources: ${budget.totalResources}
|
|
222
|
-
- Total Workflows: ${budget.totalWorkflows}
|
|
223
|
-
- Estimated Prompt Context Overhead: ~${budget.estimatedTokens.toLocaleString()} tokens/turn
|
|
224
|
-
${budget.warnings.map((w) => `- ${w}`).join("\n")}
|
|
225
|
-
|
|
226
|
-
[HOW TO RUN]
|
|
227
|
-
1. go mod tidy
|
|
228
|
-
2. go run main.go
|
|
229
|
-
*/
|
|
230
|
-
|
|
231
|
-
package main
|
|
232
|
-
|
|
233
|
-
import (
|
|
234
|
-
\t"bytes"
|
|
235
|
-
\t"context"
|
|
236
|
-
\t"encoding/json"
|
|
237
|
-
\t"fmt"
|
|
238
|
-
\t"io"
|
|
239
|
-
\t"net/http"
|
|
240
|
-
\t"net/url"
|
|
241
|
-
\t"os"
|
|
242
|
-
\t"strings"
|
|
243
|
-
\t"time"
|
|
244
|
-
|
|
245
|
-
\t"github.com/mark3labs/mcp-go/mcp"
|
|
246
|
-
\t"github.com/mark3labs/mcp-go/server"
|
|
247
|
-
)
|
|
248
|
-
|
|
249
|
-
var (
|
|
250
|
-
\tapiKey = os.Getenv("API_KEY")
|
|
251
|
-
\tbaseURL = strings.TrimRight("${defaultBaseUrl}", "/")
|
|
252
|
-
\tclient = &http.Client{Timeout: 30 * time.Second}
|
|
253
|
-
)
|
|
254
|
-
|
|
255
|
-
func main() {
|
|
256
|
-
\tif envURL := os.Getenv("API_BASE_URL"); envURL != "" {
|
|
257
|
-
\t\tbaseURL = strings.TrimRight(envURL, "/")
|
|
258
|
-
\t}
|
|
259
|
-
\tbaseURL = strings.TrimRight(baseURL, "/")
|
|
260
|
-
|
|
261
|
-
\ts := server.NewMCPServer("${serverName}", "${metadata.version || "1.0.0"}", server.WithResourceCapabilities(true, true))
|
|
262
|
-
|
|
263
|
-
${resourceRegistrations}
|
|
264
|
-
${toolRegistrations}
|
|
265
|
-
${workflowRegistrations}
|
|
266
|
-
|
|
267
|
-
\tif err := server.ServeStdio(s); err != nil {
|
|
268
|
-
\t\tfmt.Fprintf(os.Stderr, "Server error: %v\\n", err)
|
|
269
|
-
\t\tos.Exit(1)
|
|
270
|
-
\t}
|
|
271
|
-
}
|
|
272
|
-
`;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
export function generateGoFiles(serverCode, metadata, options = {}) {
|
|
276
|
-
const modFile = `module ${toSnakeCase(options.serverName || metadata.title || "rapidoc_mcp")}\n\ngo 1.22\n\nrequire github.com/mark3labs/mcp-go v0.8.4\n`;
|
|
277
|
-
const readme = `# ${metadata.title} - MCP Server (Go)
|
|
278
|
-
|
|
279
|
-
Auto-generated by RapiDoc Curated MCP Generator using mark3labs/mcp-go.
|
|
280
|
-
|
|
281
|
-
### Prerequisites
|
|
282
|
-
- Go 1.22+
|
|
283
|
-
|
|
284
|
-
### Building & Running
|
|
285
|
-
\`\`\`bash
|
|
286
|
-
go mod tidy
|
|
287
|
-
go build -o server main.go
|
|
288
|
-
export API_KEY="your-api-key"
|
|
289
|
-
./server
|
|
290
|
-
\`\`\`
|
|
291
|
-
`;
|
|
292
|
-
|
|
293
|
-
return {
|
|
294
|
-
"main.go": serverCode,
|
|
295
|
-
"go.mod": modFile,
|
|
296
|
-
"README.md": readme,
|
|
297
|
-
};
|
|
298
|
-
}
|
|
@@ -1,330 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Single-file Java / Spring AI MCP Server Generator
|
|
3
|
-
* Uses Spring AI MCP annotations, strongly typed records, and RestClient.
|
|
4
|
-
*/
|
|
5
|
-
import { toPascalCase, toCamelCase, estimateTokenCost } from "../common.js";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Helper to map OpenAPI / JSON schema parameter type to idiomatic Java type.
|
|
9
|
-
*/
|
|
10
|
-
function toJavaType(p) {
|
|
11
|
-
if (p.type === "integer") return "Integer";
|
|
12
|
-
if (p.type === "number") return "Double";
|
|
13
|
-
if (p.type === "boolean") return "Boolean";
|
|
14
|
-
if (p.type === "array") return "java.util.List<String>";
|
|
15
|
-
if (p.type === "object") return "java.util.Map<String, Object>";
|
|
16
|
-
return "String";
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function generateJavaMcpServer(metadata, tools, resources, workflows, options = {}) {
|
|
20
|
-
const className = toPascalCase(options.serverName || "RapiDocMcpServer");
|
|
21
|
-
const budget = estimateTokenCost(tools, resources, workflows);
|
|
22
|
-
const defaultBaseUrl = metadata.servers?.[0]?.url || "http://localhost:3000";
|
|
23
|
-
|
|
24
|
-
const toolRecords = [];
|
|
25
|
-
const toolBeans = tools
|
|
26
|
-
.map((t) => {
|
|
27
|
-
const recordName = `${toPascalCase(t.name)}Request`;
|
|
28
|
-
const pathParams = t.parameters.filter((p) => p.in === "path");
|
|
29
|
-
const queryParams = t.parameters.filter((p) => p.in === "query");
|
|
30
|
-
const headerParams = t.parameters.filter((p) => p.in === "header");
|
|
31
|
-
const bodyParams = t.parameters.filter((p) => p.in === "body");
|
|
32
|
-
|
|
33
|
-
// 1. Build typed request record
|
|
34
|
-
const recordFields = t.parameters.map((p) => {
|
|
35
|
-
const desc = ` @JsonPropertyDescription(${JSON.stringify(p.description || p.name)})\n`;
|
|
36
|
-
const propAnno = p.required
|
|
37
|
-
? ` @JsonProperty(value = ${JSON.stringify(p.name)}, required = true)\n`
|
|
38
|
-
: ` @JsonProperty(${JSON.stringify(p.name)})\n`;
|
|
39
|
-
return `${desc}${propAnno} ${toJavaType(p)} ${toCamelCase(p.name)}`;
|
|
40
|
-
});
|
|
41
|
-
// Always provide per-session dynamic auth override field for multi-tenancy
|
|
42
|
-
recordFields.push(
|
|
43
|
-
' @JsonPropertyDescription("Optional Bearer API key override for multi-tenant sessions")\n @JsonProperty("apiKey")\n String apiKey',
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
toolRecords.push(` @JsonClassDescription(${JSON.stringify(t.description || t.name)})
|
|
47
|
-
public record ${recordName}(
|
|
48
|
-
${recordFields.join(",\n")}
|
|
49
|
-
) {}`);
|
|
50
|
-
|
|
51
|
-
// 2. Build Bean function
|
|
52
|
-
const pathReplacements = pathParams
|
|
53
|
-
.map(
|
|
54
|
-
(p) =>
|
|
55
|
-
` .replace("{${p.name}}", request.${toCamelCase(p.name)}() != null ? UriUtils.encodePath(String.valueOf(request.${toCamelCase(p.name)}()), StandardCharsets.UTF_8) : "")`,
|
|
56
|
-
)
|
|
57
|
-
.join("\n");
|
|
58
|
-
|
|
59
|
-
const queryAppends = queryParams
|
|
60
|
-
.map(
|
|
61
|
-
(p) =>
|
|
62
|
-
` if (request.${toCamelCase(p.name)}() != null) uriBuilder.queryParam("${p.name}", request.${toCamelCase(p.name)}());`,
|
|
63
|
-
)
|
|
64
|
-
.join("\n");
|
|
65
|
-
|
|
66
|
-
const headerAppends = headerParams
|
|
67
|
-
.map(
|
|
68
|
-
(p) =>
|
|
69
|
-
` if (request.${toCamelCase(p.name)}() != null && !String.valueOf(request.${toCamelCase(p.name)}()).isBlank()) {\n spec.header("${p.name}", String.valueOf(request.${toCamelCase(p.name)}()));\n }`,
|
|
70
|
-
)
|
|
71
|
-
.join("\n");
|
|
72
|
-
|
|
73
|
-
const hasBody = ["POST", "PUT", "PATCH"].includes(t.method) && bodyParams.length > 0;
|
|
74
|
-
const bodyAppends = hasBody
|
|
75
|
-
? ` Map<String, Object> bodyPayload = new java.util.LinkedHashMap<>();
|
|
76
|
-
${bodyParams
|
|
77
|
-
.map(
|
|
78
|
-
(p) =>
|
|
79
|
-
` if (request.${toCamelCase(p.name)}() != null) bodyPayload.put("${p.name}", request.${toCamelCase(p.name)}());`,
|
|
80
|
-
)
|
|
81
|
-
.join("\n")}`
|
|
82
|
-
: "";
|
|
83
|
-
|
|
84
|
-
return ` // Tool: ${t.name} (${t.method} ${t.path})
|
|
85
|
-
// ${t.isConsequential ? "⚠️ Consequential/Destructive action" : "Safe/Read-only action"}
|
|
86
|
-
@Bean
|
|
87
|
-
@Description(${JSON.stringify(t.description + (t.isConsequential ? " (Requires confirmation)" : ""))})
|
|
88
|
-
public Function<${recordName}, String> ${toCamelCase(t.name)}() {
|
|
89
|
-
return request -> {
|
|
90
|
-
String resolvedKey = request.apiKey() != null && !request.apiKey().isBlank() ? request.apiKey() : this.apiKey;
|
|
91
|
-
String resolvedPath = "${t.path}"${pathReplacements ? "\n" + pathReplacements : ""};
|
|
92
|
-
// Spring's builder automatically normalizes matching slashes between baseUrl and paths
|
|
93
|
-
UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(this.baseUrl);
|
|
94
|
-
uriBuilder.path(resolvedPath);
|
|
95
|
-
${queryAppends ? queryAppends + "\n" : ""}${bodyAppends ? bodyAppends + "\n" : ""} try {
|
|
96
|
-
RestClient.RequestBodySpec spec = restClient.method(HttpMethod.${t.method})
|
|
97
|
-
.uri(uriBuilder.build().toUri())
|
|
98
|
-
.accept(MediaType.APPLICATION_JSON);
|
|
99
|
-
if (resolvedKey != null && !resolvedKey.isBlank()) {
|
|
100
|
-
spec.header("Authorization", "Bearer " + resolvedKey);
|
|
101
|
-
}
|
|
102
|
-
${headerAppends ? headerAppends + "\n" : ""}${
|
|
103
|
-
hasBody
|
|
104
|
-
? ` if (!bodyPayload.isEmpty()) {
|
|
105
|
-
spec.contentType(MediaType.APPLICATION_JSON).body(bodyPayload);
|
|
106
|
-
}\n`
|
|
107
|
-
: ""
|
|
108
|
-
} return spec.retrieve().body(String.class);
|
|
109
|
-
} catch (Exception e) {
|
|
110
|
-
return "Error invoking ${t.name}: " + e.getMessage();
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
}`;
|
|
114
|
-
})
|
|
115
|
-
.join("\n\n");
|
|
116
|
-
|
|
117
|
-
const hasWorkflows = workflows.length > 0;
|
|
118
|
-
|
|
119
|
-
const resourceBeans = resources
|
|
120
|
-
.map((r) => {
|
|
121
|
-
const uriTemplate = `${r.uri}{?apiKey,api_key}`;
|
|
122
|
-
return ` // Resource: ${r.name} (${uriTemplate})
|
|
123
|
-
@Bean
|
|
124
|
-
@Description(${JSON.stringify(`Resource: ${r.name} (${uriTemplate})`)})
|
|
125
|
-
public Function<Map<String, Object>, String> ${toCamelCase(r.name)}() {
|
|
126
|
-
return args -> {
|
|
127
|
-
String resolvedKey = this.apiKey;
|
|
128
|
-
if (args != null && args.containsKey("apiKey") && args.get("apiKey") != null) {
|
|
129
|
-
String passedKey = String.valueOf(args.get("apiKey"));
|
|
130
|
-
if (!passedKey.isBlank()) {
|
|
131
|
-
resolvedKey = passedKey;
|
|
132
|
-
}
|
|
133
|
-
} else if (args != null && args.containsKey("api_key") && args.get("api_key") != null) {
|
|
134
|
-
String passedKey = String.valueOf(args.get("api_key"));
|
|
135
|
-
if (!passedKey.isBlank()) {
|
|
136
|
-
resolvedKey = passedKey;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Spring's builder automatically normalizes matching slashes between baseUrl and paths
|
|
141
|
-
UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(this.baseUrl);
|
|
142
|
-
uriBuilder.path("${r.path}");
|
|
143
|
-
if (args != null) {
|
|
144
|
-
args.forEach((key, value) -> {
|
|
145
|
-
if (!"apiKey".equalsIgnoreCase(key) && !"api_key".equalsIgnoreCase(key) && value != null) {
|
|
146
|
-
uriBuilder.queryParam(key, value);
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
try {
|
|
151
|
-
RestClient.RequestHeadersSpec<?> spec = restClient.get()
|
|
152
|
-
.uri(uriBuilder.build().toUri())
|
|
153
|
-
.accept(MediaType.parseMediaType("${r.mimeType || "application/json"}"));
|
|
154
|
-
if (resolvedKey != null && !resolvedKey.isBlank()) {
|
|
155
|
-
spec.header("Authorization", "Bearer " + resolvedKey);
|
|
156
|
-
}
|
|
157
|
-
return spec.retrieve().body(String.class);
|
|
158
|
-
} catch (Exception e) {
|
|
159
|
-
return "Error loading resource: " + e.getMessage();
|
|
160
|
-
}
|
|
161
|
-
};
|
|
162
|
-
}`;
|
|
163
|
-
})
|
|
164
|
-
.join("\n\n");
|
|
165
|
-
|
|
166
|
-
const workflowBeans = workflows
|
|
167
|
-
.map((w) => {
|
|
168
|
-
const recordName = `${toPascalCase(w.name)}Request`;
|
|
169
|
-
const paramKeys = w.parameters ? Object.keys(w.parameters) : [];
|
|
170
|
-
const recordFields = paramKeys.map(
|
|
171
|
-
(k) =>
|
|
172
|
-
` @JsonPropertyDescription(${JSON.stringify(w.parameters[k].description || "")})\n @JsonProperty(required = true)\n String ${toCamelCase(k)}`,
|
|
173
|
-
);
|
|
174
|
-
recordFields.push(
|
|
175
|
-
' @JsonPropertyDescription("Optional Bearer API key override for multi-tenant sessions")\n String apiKey',
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
toolRecords.push(` @JsonClassDescription(${JSON.stringify(w.description || "Composite workflow")})
|
|
179
|
-
public record ${recordName}(
|
|
180
|
-
${recordFields.join(",\n")}
|
|
181
|
-
) {}`);
|
|
182
|
-
|
|
183
|
-
const firstParam = paramKeys[0] || "id";
|
|
184
|
-
|
|
185
|
-
return ` // Composite Workflow: ${w.name}
|
|
186
|
-
@Bean
|
|
187
|
-
@Description(${JSON.stringify(w.description || "Composite workflow")})
|
|
188
|
-
public Function<${recordName}, String> ${toCamelCase(w.name)}() {
|
|
189
|
-
return request -> {
|
|
190
|
-
String resolvedKey = request.apiKey() != null && !request.apiKey().isBlank() ? request.apiKey() : this.apiKey;
|
|
191
|
-
Map<String, Object> results = new java.util.LinkedHashMap<>();
|
|
192
|
-
ObjectMapper mapper = new ObjectMapper();
|
|
193
|
-
try {
|
|
194
|
-
${(w.steps || [])
|
|
195
|
-
.map((s, idx) => {
|
|
196
|
-
return ` // Step ${idx + 1}: ${s.call}
|
|
197
|
-
UriComponentsBuilder stepUri_${idx} = UriComponentsBuilder.fromUriString(this.baseUrl);
|
|
198
|
-
stepUri_${idx}.path("/api/" + UriUtils.encodePath(String.valueOf(request.${toCamelCase(firstParam)}()), StandardCharsets.UTF_8));
|
|
199
|
-
RestClient.RequestHeadersSpec<?> stepSpec_${idx} = restClient.get().uri(stepUri_${idx}.build().toUri()).accept(MediaType.APPLICATION_JSON);
|
|
200
|
-
if (resolvedKey != null && !resolvedKey.isBlank()) { stepSpec_${idx}.header("Authorization", "Bearer " + resolvedKey); }
|
|
201
|
-
String stepBody_${idx} = stepSpec_${idx}.retrieve().body(String.class);
|
|
202
|
-
try {
|
|
203
|
-
results.put("${s.call}", mapper.readValue(stepBody_${idx}, Object.class));
|
|
204
|
-
} catch (Exception parseEx) {
|
|
205
|
-
results.put("${s.call}", stepBody_${idx});
|
|
206
|
-
}`;
|
|
207
|
-
})
|
|
208
|
-
.join("\n")}
|
|
209
|
-
return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(results);
|
|
210
|
-
} catch (Exception e) {
|
|
211
|
-
return "Workflow ${w.name} failed: " + e.getMessage();
|
|
212
|
-
}
|
|
213
|
-
};
|
|
214
|
-
}`;
|
|
215
|
-
})
|
|
216
|
-
.join("\n\n");
|
|
217
|
-
|
|
218
|
-
return `/*
|
|
219
|
-
Auto-generated by RapiDoc Curated MCP Generator
|
|
220
|
-
Spec: ${metadata.title} (v${metadata.version})
|
|
221
|
-
|
|
222
|
-
[MULTI-TENANCY & AUTHENTICATION]
|
|
223
|
-
- Local Stdio Mode: Runs locally (e.g. Claude Desktop, Cursor). Uses \`API_KEY\` from environment.
|
|
224
|
-
- Multi-Tenant / Agent Mode: When running in a shared service, the calling AI agent can pass
|
|
225
|
-
per-session credentials directly in the \`apiKey\` request field of any tool record.
|
|
226
|
-
|
|
227
|
-
[TOKEN BUDGET DIAGNOSTIC]
|
|
228
|
-
- Total Tools: ${budget.totalTools}
|
|
229
|
-
- Total Resources: ${budget.totalResources}
|
|
230
|
-
- Total Workflows: ${budget.totalWorkflows}
|
|
231
|
-
- Estimated Prompt Context Overhead: ~${budget.estimatedTokens.toLocaleString()} tokens/turn
|
|
232
|
-
${budget.warnings.map((w) => `- ${w}`).join("\n")}
|
|
233
|
-
|
|
234
|
-
[HOW TO RUN]
|
|
235
|
-
1. export API_KEY="your-api-key"
|
|
236
|
-
2. ./mvnw spring-boot:run
|
|
237
|
-
*/
|
|
238
|
-
|
|
239
|
-
package com.rapidoc.mcp;
|
|
240
|
-
|
|
241
|
-
import org.springframework.boot.SpringApplication;
|
|
242
|
-
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
|
243
|
-
import org.springframework.context.annotation.Bean;
|
|
244
|
-
import org.springframework.context.annotation.Description;
|
|
245
|
-
import org.springframework.http.HttpMethod;
|
|
246
|
-
import org.springframework.http.MediaType;
|
|
247
|
-
import org.springframework.web.client.RestClient;
|
|
248
|
-
import org.springframework.web.util.UriComponentsBuilder;
|
|
249
|
-
import org.springframework.web.util.UriUtils;
|
|
250
|
-
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
251
|
-
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
|
|
252
|
-
import com.fasterxml.jackson.annotation.JsonClassDescription;
|
|
253
|
-
${hasWorkflows ? "import com.fasterxml.jackson.databind.ObjectMapper;\n" : ""}
|
|
254
|
-
import java.nio.charset.StandardCharsets;
|
|
255
|
-
import java.util.Map;
|
|
256
|
-
import java.util.function.Function;
|
|
257
|
-
|
|
258
|
-
@SpringBootApplication
|
|
259
|
-
public class ${className} {
|
|
260
|
-
|
|
261
|
-
private final String baseUrl = System.getenv().getOrDefault("API_BASE_URL", "${defaultBaseUrl}");
|
|
262
|
-
private final String apiKey = System.getenv().getOrDefault("API_KEY", "");
|
|
263
|
-
private final RestClient restClient = RestClient.builder().build();
|
|
264
|
-
|
|
265
|
-
public static void main(String[] args) {
|
|
266
|
-
SpringApplication.run(${className}.class, args);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// ==========================================
|
|
270
|
-
// TOOL REQUEST RECORDS (Strongly Typed for Spring AI MCP)
|
|
271
|
-
// ==========================================
|
|
272
|
-
${toolRecords.join("\n\n")}
|
|
273
|
-
|
|
274
|
-
// ==========================================
|
|
275
|
-
// MCP TOOLS (@Bean Function Callbacks)
|
|
276
|
-
// ==========================================
|
|
277
|
-
${toolBeans}
|
|
278
|
-
${resourceBeans ? "\n // ==========================================\n // MCP RESOURCES\n // ==========================================\n" + resourceBeans : ""}
|
|
279
|
-
${workflowBeans ? "\n // ==========================================\n // COMPOSITE WORKFLOWS\n // ==========================================\n" + workflowBeans : ""}
|
|
280
|
-
}
|
|
281
|
-
`;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
export function generateJavaFiles(serverCode, metadata) {
|
|
285
|
-
const pomXml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
286
|
-
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
287
|
-
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
288
|
-
<modelVersion>4.0.0</modelVersion>
|
|
289
|
-
<parent>
|
|
290
|
-
<groupId>org.springframework.boot</groupId>
|
|
291
|
-
<artifactId>spring-boot-starter-parent</artifactId>
|
|
292
|
-
<version>3.3.0</version>
|
|
293
|
-
<relativePath/>
|
|
294
|
-
</parent>
|
|
295
|
-
<groupId>com.rapidoc</groupId>
|
|
296
|
-
<artifactId>mcp-server</artifactId>
|
|
297
|
-
<version>1.0.0</version>
|
|
298
|
-
<dependencies>
|
|
299
|
-
<dependency>
|
|
300
|
-
<groupId>org.springframework.boot</groupId>
|
|
301
|
-
<artifactId>spring-boot-starter</artifactId>
|
|
302
|
-
</dependency>
|
|
303
|
-
<dependency>
|
|
304
|
-
<groupId>org.springframework.boot</groupId>
|
|
305
|
-
<artifactId>spring-boot-starter-web</artifactId>
|
|
306
|
-
</dependency>
|
|
307
|
-
</dependencies>
|
|
308
|
-
</project>\n`;
|
|
309
|
-
|
|
310
|
-
const readme = `# ${metadata.title} - MCP Server (Java Spring AI)
|
|
311
|
-
|
|
312
|
-
Auto-generated by RapiDoc Curated MCP Generator.
|
|
313
|
-
|
|
314
|
-
### Prerequisites
|
|
315
|
-
- Java 17+
|
|
316
|
-
- Maven 3.8+
|
|
317
|
-
|
|
318
|
-
### Building & Running
|
|
319
|
-
\`\`\`bash
|
|
320
|
-
export API_KEY="your-api-key"
|
|
321
|
-
./mvnw spring-boot:run
|
|
322
|
-
\`\`\`
|
|
323
|
-
`;
|
|
324
|
-
|
|
325
|
-
return {
|
|
326
|
-
"src/main/java/com/rapidoc/mcp/RapiDocMcpServer.java": serverCode,
|
|
327
|
-
"pom.xml": pomXml,
|
|
328
|
-
"README.md": readme,
|
|
329
|
-
};
|
|
330
|
-
}
|