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.
Files changed (77) hide show
  1. package/README.md +127 -25
  2. package/dist/rapidoc-flow-min.js +580 -0
  3. package/dist/rapidoc-flow-min.js.LICENSE.txt +171 -0
  4. package/dist/rapidoc-min.js +541 -1309
  5. package/dist/rapidoc-min.js.LICENSE.txt +381 -0
  6. package/package.json +21 -18
  7. package/src/README.md +150 -0
  8. package/src/components/README.md +91 -0
  9. package/src/components/api-request.ts +43 -0
  10. package/src/components/api-response.ts +9 -4
  11. package/src/components/schema-base-element.ts +18 -4
  12. package/src/components/schema-table.ts +58 -13
  13. package/src/components/schema-tree.ts +7 -0
  14. package/src/components/tag-input.ts +5 -9
  15. package/src/flow.js +3 -0
  16. package/src/rapi-flow.ts +1178 -0
  17. package/src/rapidoc-mini.ts +27 -6
  18. package/src/rapidoc.ts +108 -62
  19. package/src/styles/README.md +96 -0
  20. package/src/styles/css-bundle.js +2 -2
  21. package/src/styles/endpoint-styles.js +6 -1
  22. package/src/styles/flow-styles.js +248 -0
  23. package/src/styles/nav-styles.js +39 -15
  24. package/src/styles/schema-styles.js +13 -5
  25. package/src/styles/theme-tokens.js +2 -2
  26. package/src/templates/README.md +89 -0
  27. package/src/templates/api-response-template.js +14 -10
  28. package/src/templates/callback-template.js +1 -1
  29. package/src/templates/downloads-template.js +12 -27
  30. package/src/templates/endpoint-template.js +15 -24
  31. package/src/templates/expanded-endpoint-template.js +8 -4
  32. package/src/templates/focused-endpoint-template.js +4 -3
  33. package/src/templates/navbar-template.js +20 -11
  34. package/src/templates/request-body-template.js +29 -7
  35. package/src/templates/request-params-template.js +15 -4
  36. package/src/templates/security-scheme-template.js +11 -2
  37. package/src/templates/server-template.js +3 -2
  38. package/src/utils/README.md +146 -0
  39. package/src/utils/arazzo/README.md +119 -0
  40. package/src/utils/arazzo/components.js +15 -0
  41. package/src/utils/arazzo/graph.js +269 -0
  42. package/src/utils/arazzo/index.js +52 -0
  43. package/src/utils/arazzo/operations.js +106 -0
  44. package/src/utils/arazzo/parse.js +196 -0
  45. package/src/utils/arazzo/refs.js +40 -0
  46. package/src/utils/arazzo/svg.js +707 -0
  47. package/src/utils/curl-utils.js +5 -2
  48. package/src/utils/highlighter.js +3 -0
  49. package/src/utils/mcp/README.md +155 -0
  50. package/src/utils/mcp/common.js +45 -346
  51. package/src/utils/mcp/manifest.js +382 -0
  52. package/src/utils/mcp/project/server.ts +53 -0
  53. package/src/utils/mcp/project.js +88 -0
  54. package/src/utils/mcp/runtime.js +289 -0
  55. package/src/utils/mcp/schema.js +98 -0
  56. package/src/utils/mcp/workflow-runtime.js +374 -0
  57. package/src/utils/mcp/workflows.js +240 -0
  58. package/src/utils/mcp/zip.js +102 -0
  59. package/src/utils/mcp-generator.js +29 -125
  60. package/src/utils/mock-interceptor.js +134 -85
  61. package/src/utils/request-utils.js +61 -0
  62. package/src/utils/schema-utils.js +33 -6
  63. package/src/utils/slugger.js +5 -7
  64. package/src/utils/spec-parser.js +190 -164
  65. package/src/utils/url-utils.js +36 -1
  66. package/dist/rapidoc-static-min.js +0 -13542
  67. package/src/static/index.d.ts +0 -149
  68. package/src/static/index.js +0 -62
  69. package/src/static/navigation.js +0 -497
  70. package/src/static/render.js +0 -490
  71. package/src/static/serializer.js +0 -216
  72. package/src/utils/mcp/generators/csharp.js +0 -242
  73. package/src/utils/mcp/generators/go.js +0 -298
  74. package/src/utils/mcp/generators/java.js +0 -330
  75. package/src/utils/mcp/generators/python.js +0 -248
  76. package/src/utils/mcp/generators/rust.js +0 -281
  77. package/src/utils/mcp/generators/typescript.js +0 -290
@@ -34,6 +34,7 @@ export function deduplicateHeaderValues(value) {
34
34
  * @param {Headers|Map|Array<[string, string]>|object} [options.headers] - Request headers
35
35
  * @param {URLSearchParams|FormData|File|string|object} [options.body] - Request body payload
36
36
  * @param {string} [options.contentType=''] - Content type of the payload
37
+ * @param {Object<string, string>} [options.partContentTypes] - Content-Type of multipart parts, by field name
37
38
  * @param {string} [options.baseLocation] - Optional base location to resolve relative URLs
38
39
  * @returns {string} Formatted cURL snippet
39
40
  */
@@ -43,6 +44,7 @@ export function generateCurlCommand({
43
44
  headers,
44
45
  body,
45
46
  contentType = "",
47
+ partContentTypes = {},
46
48
  baseLocation = typeof location !== "undefined" ? location.href : "",
47
49
  } = {}) {
48
50
  let curlUrl = url;
@@ -87,12 +89,13 @@ export function generateCurlCommand({
87
89
  } else if (typeof FormData !== "undefined" && body instanceof FormData) {
88
90
  curlForm = Array.from(body)
89
91
  .map(([key, value]) => {
92
+ const type = partContentTypes?.[key] ? `;type=${partContentTypes[key]}` : "";
90
93
  if (typeof File !== "undefined" && value instanceof File) {
91
- return ` -F "${key}=@${value.name}"`;
94
+ return ` -F "${key}=@${value.name}${value.type ? "" : type}"`;
92
95
  }
93
96
  const sanitizedVal =
94
97
  typeof value === "string" ? value.replace(/\\/g, "\\\\").replace(/"/g, '\\"') : value;
95
- return ` -F "${key}=${sanitizedVal}"`;
98
+ return ` -F "${key}=${sanitizedVal}${type}"`;
96
99
  })
97
100
  .join("\\\n");
98
101
  if (curlForm) {
@@ -407,6 +407,9 @@ export const highlightAllCode = (rootNode = document) => {
407
407
  };
408
408
 
409
409
  export const scheduleHighlight = (rootNode = document) => {
410
+ if (typeof window === "undefined" || typeof CSS === "undefined" || !CSS.highlights) {
411
+ return;
412
+ }
410
413
  if (highlightScheduled) return;
411
414
  highlightScheduled = true;
412
415
  requestAnimationFrame(() => {
@@ -0,0 +1,155 @@
1
+ # MCP Manifest, Runtime and Export
2
+
3
+ ## Overview
4
+
5
+ Turns an OpenAPI document into an **MCP manifest** (plain JSON: one tool per operation, with
6
+ JSON Schema arguments and MCP safety annotations) and executes that manifest with `fetch`. The
7
+ exported MCP server is the manifest plus two fixed files: `runtime.js` (this folder's runtime,
8
+ copied verbatim) and `project/server.ts` (a stdio server on `@modelcontextprotocol/sdk` that
9
+ registers the manifest's tools and hands calls to the runtime). The entry point is
10
+ `src/utils/mcp-generator.js`; `<rapi-doc>` (`src/rapidoc.ts`, `src/templates/downloads-template.js`)
11
+ uses it for its Export MCP Server button, and `packages/rapidoc-mcp/src/index.js` re-exports it as `@rapidoc/mcp`
12
+ (a separate package, Node only, no Lit) for the portal build, `@rapidoc/server` and the CLI.
13
+
14
+ Arazzo workflows become tools too: `workflows.js` compiles a parsed Arazzo document into
15
+ `manifest.workflows` (steps resolved to operations) and one tool per workflow, and
16
+ `workflow-runtime.js` runs them on top of `runtime.js`. The two runtimes are separate files so
17
+ `dist/rapidoc-min.js` (operation tools only) carries no Arazzo runtime, while
18
+ `dist/rapidoc-flow-min.js` and `@rapidoc/mcp` include both. Not owned here: Arazzo parsing and validation (`../arazzo/`), the `<rapi-doc>` attribute and
19
+ methods, and the network calls themselves (the runtime only builds and sends requests).
20
+
21
+ ## Interface
22
+
23
+ - `compileWorkflows(arazzo, { sources, onProblem }) -> { tools, workflows }` [workflows.js]:
24
+ `sources` are operation indexes by Arazzo source name (`../arazzo/operations.js`). A workflow the
25
+ runtime cannot run is reported once through `onProblem` and left out. `buildMcpManifest` calls
26
+ it for `options.arazzo`, naming the manifest's source after the Arazzo document's first OpenAPI
27
+ source so steps resolve; `generateMcpServer` collects the problems into its result.
28
+
29
+ - `buildMcpManifest(spec, options) -> manifest` [manifest.js]: `spec` is a raw OpenAPI 3.x /
30
+ Swagger 2.0 object or `ProcessSpec`'s result (`tags[].paths[]`). Options: `serverName`,
31
+ `sourceName` (default `api`), `includeInternal`, `includeDeprecated`, `includeOperations` (`false`:
32
+ only the Arazzo workflows' tools, for a manifest of workflows). `toJsonSchema(schema,
33
+ root)` is the schema converter on its own; `MANIFEST_VERSION` is `1`.
34
+ - `createMcpRuntime(manifest, { fetch, credentials, baseUrls, runWorkflow })` [runtime.js]:
35
+ `listTools()`, `listResources()`, `callTool(name, args, hooks) -> { content, structuredContent?,
36
+ isError? }`, `execute(operation, args, { headers? })` (one request, for workflow runners; the
37
+ headers are added to the request). A workflow tool is handed to `runWorkflow`; without it the
38
+ call is an error result. `callTool`'s hooks reach the workflow runner: `onStepStart`, `onStep`,
39
+ `sleep`, and `requestHeaders({ workflowId, stepId, attempt, operation })`, whose result is sent
40
+ with that step's request (how `<rapi-flow>` asks `rapidoc mock` for a status with
41
+ `Prefer: code=`).
42
+ - `runWorkflow(runtime, tool, args, { onStepStart, onStep, sleep })` and
43
+ `createMcpWorkflowRuntime(manifest, options)` (createMcpRuntime with `runWorkflow` plugged in;
44
+ `options.sleep` waits between retries) [workflow-runtime.js], plus `evaluateExpression`,
45
+ `resolveValue` and `evaluateCondition`. `@rapidoc/mcp` exports `createMcpWorkflowRuntime` as its
46
+ `createMcpRuntime`.
47
+ `readResource(uri) -> { contents }`. `buildRequest(manifest, operation, args, options)` returns
48
+ the request without sending it; `credentialsFromEnv(manifest, env)` and `envNameFor(id)` map
49
+ security schemes to environment variables.
50
+ - `writeMcpProject(manifest, { serverName, workflowRuntime }) -> { [path]: text }` [project.js]:
51
+ `manifest.json`, `server.ts`, `runtime.js`, `package.json`, `README.md`,
52
+ `claude_desktop_config.json`, and `workflow-runtime.js` when the manifest has workflows and
53
+ `workflowRuntime` (its text) is given: `@rapidoc/mcp` and `<rapi-flow>` supply it,
54
+ `dist/rapidoc-min.js` does not carry it. `server.ts` imports it dynamically when present.
55
+ - `createZip(files) -> Uint8Array` [zip.js]: a stored (uncompressed) ZIP of text files.
56
+ - `generateMcpServer(spec, options) -> { manifest, files, budget, hostConfig }`
57
+ [../mcp-generator.js]: the three above in one call.
58
+ - `common.js`: `parseMcpEnabled(val)` (the `mcp` attribute), `toSnakeCase`, `sanitizeDescription`,
59
+ `deriveToolName`, `AUTH_TRANSPORT_HEADERS`, `estimateTokenCost(manifest)` and
60
+ `generateHostConfigs(serverName, { schemes })`.
61
+
62
+ ## Invariants
63
+
64
+ - **Manifest shape.** `{ manifestVersion, server: { name, version, description }, sources:
65
+ { [name]: { baseUrl, securitySchemes } }, tools, resources, workflows }`. A workflow tool has
66
+ `workflow` (its id) instead of `operation`; `workflows[id]` is `{ workflowId, summary,
67
+ description, inputs, steps, outputs }`, each step `{ stepId, operation, parameters, requestBody,
68
+ successCriteria, onSuccess, onFailure, outputs }` with the operation resolved (source, method,
69
+ path, params from the step's parameters and body, security from the OpenAPI operation) and
70
+ values kept as Arazzo expressions. A tool is
71
+ `{ name, description, inputSchema, annotations, operation }` with `operation = { source, method,
72
+ path, params: [{ name, in, arg?, contentType? }], security }`; `arg` names the argument when it
73
+ differs from the wire name, `contentType` is set for `in: "body"`. A resource has `uri`
74
+ (`reference://...`) and `mimeType` instead of `inputSchema` and `annotations`.
75
+ - **Exclusion** (`[manifest.js:buildMcpManifest]`): an operation is skipped when `x-internal` or
76
+ `x-hidden` is `true` on it or its path item (unless `includeInternal`), when `deprecated` (unless
77
+ `includeDeprecated`), or when it is a webhook.
78
+ - **Arguments.** Path, query, header and cookie parameters become properties by name (a clash
79
+ across locations gets `_<in>`); path parameters are required. The request body is one `body`
80
+ property with the first JSON content type's schema (else form-urlencoded, multipart, or the first
81
+ listed). Path-level parameters are merged unless the operation redefines them. Headers in
82
+ `AUTH_TRANSPORT_HEADERS` and any parameter that matches an `apiKey` scheme's `in` and `name`
83
+ are never arguments: the runtime sends them.
84
+ - **JSON Schema** (`[manifest.js:toJsonSchema]`): `$ref`s are inlined from the raw document;
85
+ a schema met again on the same branch is cut to `{ type }`; `nullable` folds into `type`;
86
+ `readOnly` properties (and their `required` entries) are dropped; OpenAPI 3.0 boolean
87
+ `exclusiveMinimum/Maximum` become numbers; `example`, `xml`, `x-*` and other non-schema keys
88
+ are dropped.
89
+ - **Naming**: `toSnakeCase(operationId)`, else `deriveToolName(method, path)`; workflow tools are
90
+ `toSnakeCase(workflowId)`, after the operations; a repeated name gets `_2`, `_3`, ... in that
91
+ order. `sanitizeDescription` deletes every `_`.
92
+ - **Annotations**: `readOnlyHint` for GET; `destructiveHint` for PUT, PATCH and DELETE;
93
+ `idempotentHint` for GET, PUT and DELETE. A workflow tool is read-only when every step is a
94
+ GET, destructive when any step is PUT, PATCH or DELETE, never idempotent. `estimateTokenCost.consequentialCount` counts tools
95
+ without `readOnlyHint`.
96
+ - **Resources**: a GET with no required argument whose path contains one of `/reference`,
97
+ `/currencies`, `/countries`, `/tiers`, `/categories`, `/status`, `/schema`.
98
+ - **Security.** `securitySchemes` keeps `type, description, in, name, scheme, bearerFormat,
99
+ openIdConnectUrl` and OAuth 2 `flows`; `<rapi-doc>`'s injected `_rapidoc_api_key` scheme is
100
+ left out. A tool's `security` is the operation's, else the document's. The runtime uses the
101
+ first alternative all of whose credentials are known; with none, nothing is sent when an
102
+ alternative is empty, else the first alternative as far as credentials go.
103
+ - **Runtime.** `runtime.js` imports nothing and uses only `fetch`, `URLSearchParams`,
104
+ `TextEncoder` and `btoa`, and `workflow-runtime.js` imports only `./runtime.js`, because the
105
+ project writer embeds their text (`?raw` imports, supported by Vite and
106
+ `scripts/test-loader.mjs`). Path parameters are percent-encoded; array query values
107
+ repeat the key; objects elsewhere are JSON; `Accept: application/json` unless set; a missing path
108
+ parameter is an error result, as is a transport failure or a non-2xx status (text `HTTP <status>`
109
+ plus the body). JSON responses are pretty-printed and, for objects, also `structuredContent`.
110
+ - **Workflows the runtime runs** (`[workflows.js:compileWorkflows]`): steps that name an
111
+ operation found in the sources, parameters in path, query, header or cookie, a request body
112
+ with `payload` and `replacements`, simple criteria only (`regex`, `jsonpath` and `xpath` are
113
+ reported), actions `end`, `goto` to a step and `retry` (`retryLimit` default 1, `retryAfter`
114
+ seconds) with simple criteria, and `$inputs` / `$steps` expressions. Steps that call another
115
+ workflow, actions that go to one, and `$workflows` expressions are reported. A step's actions
116
+ are its own `onSuccess` / `onFailure` followed by the workflow's it does not override by name.
117
+ - **Running a workflow** (`[workflow-runtime.js:runWorkflow]`): required inputs are checked first.
118
+ Each step's parameters and payload are evaluated against `$inputs` and earlier steps' outputs
119
+ (`resolveValue`: a whole-string expression keeps its type, `{$expr}` inside text is
120
+ interpolated), the request is sent, and every criterion must hold (`evaluateCondition`: `==`,
121
+ `!=`, `<`, `>`, `<=`, `>=`, `&&`, `||`, `!`, parentheses, quoted strings, numbers, `true`,
122
+ `false`, `null`; `==` compares numbers and strings as text and treats a missing value as
123
+ `null`). A step without criteria succeeds on a 2xx response. The first action whose criteria all hold runs: on success
124
+ `end` finishes, `goto` jumps, none falls through; on failure none or `end` fails the workflow
125
+ (the result names the step, the criterion and the response), `goto` jumps, `retry` re-runs the
126
+ step (or `stepId`) after `retryAfter` seconds up to `retryLimit` times. A transport error is a
127
+ failure with no response. More than 1000 step runs is an error. The result is the workflow's
128
+ `outputs` as `structuredContent`; `onStepStart` gets `{ workflowId, stepId, attempt }` before
129
+ each request and `onStep` gets `{ workflowId, stepId, attempt, outcome, request, response,
130
+ outputs, failedCriterion, action }` after it.
131
+ - **Environment.** `envNameFor("petstore_auth")` is `PETSTORE_AUTH`; `API_KEY` is the fallback
132
+ for every scheme; HTTP Basic reads `<NAME>` as `user:password` or `<NAME>_USERNAME` /
133
+ `<NAME>_PASSWORD`. `API_BASE_URL` in `server.ts` overrides every source's base URL.
134
+ - **Project.** `server.ts` and `runtime.js` are identical for every API; only `manifest.json`,
135
+ `package.json`, `README.md` and `claude_desktop_config.json` vary. The zip stores entries
136
+ uncompressed with a fixed 1980-01-01 timestamp and UTF-8 names.
137
+
138
+ ## Side Effects
139
+
140
+ - None in this folder: no network, DOM, storage or console. The exported `server.ts` reads
141
+ `manifest.json`, the environment and stdio, and the runtime it calls makes HTTP requests.
142
+
143
+ ## Verification
144
+
145
+ - `npm run test:unit` from the repo root. `tests/unit/mcp-generator.test.js` covers the manifest
146
+ (from `docs/public/specs/petstore.yaml` and small documents), the schema converter, the runtime
147
+ with a fake `fetch`, credentials from the environment, the project files and the zip.
148
+ `tests/unit/mcp-workflows.test.js` covers compiling `docs/public/specs/petstore.arazzo.yaml`,
149
+ expressions, conditions, and runs (outputs between steps, retry, goto, failure) with a fake
150
+ `fetch`. `tests/unit/mcp-project.test.js` runs the exported project for real: it starts `server.ts`
151
+ (through `scripts/test-loader.mjs` instead of `tsx`) under `.test-tmp/`, drives it with the MCP
152
+ SDK's stdio client (`@modelcontextprotocol/sdk`, a root dev dependency) and points
153
+ `API_BASE_URL` at `@rapidoc/mock` serving the same Petstore spec, workflow tools included.
154
+ - `/examples/mcp-server` on the docs site shows the manifest, `server.ts` and `runtime.js` and
155
+ downloads the project. After editing, `npm run lint` and `npm run format-fix`.
@@ -1,8 +1,11 @@
1
1
  /**
2
- * MCP (Model Context Protocol) Common Utilities and Spec Extraction
2
+ * MCP (Model Context Protocol) helpers shared by the manifest builder and the project writer.
3
3
  */
4
+ import { envNameFor } from "./runtime.js";
4
5
 
5
- // Common authentication and transport headers to strip from LLM parameter schemas
6
+ export { envNameFor };
7
+
8
+ // Auth and transport headers never exposed as tool arguments
6
9
  export const AUTH_TRANSPORT_HEADERS = new Set([
7
10
  "authorization",
8
11
  "cookie",
@@ -10,6 +13,7 @@ export const AUTH_TRANSPORT_HEADERS = new Set([
10
13
  "x-api-key",
11
14
  "api-key",
12
15
  "apikey",
16
+ "api_key",
13
17
  "user-agent",
14
18
  "host",
15
19
  "content-length",
@@ -18,56 +22,20 @@ export const AUTH_TRANSPORT_HEADERS = new Set([
18
22
  "connection",
19
23
  ]);
20
24
 
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
25
  /**
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.
26
+ * Reads the `mcp` attribute: export is off for an empty value, `false`, `none` or a list that
27
+ * contains one of them (the old kill-switch), and on for anything else, including the old
28
+ * language names.
42
29
  *
43
30
  * @param {string|string[]|boolean} val
44
- * @returns {string[]} Array of normalized target names (e.g. ['typescript', 'python'])
31
+ * @returns {boolean}
45
32
  */
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
- }
33
+ export function parseMcpEnabled(val) {
34
+ if (!val || val === "false") return false;
35
+ const items = Array.isArray(val) ? val : String(val).split(",");
63
36
  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)];
37
+ if (!normalized.length) return false;
38
+ return !normalized.some((s) => s === "none" || s === "false");
71
39
  }
72
40
 
73
41
  /**
@@ -83,28 +51,6 @@ export function toSnakeCase(str) {
83
51
  .toLowerCase();
84
52
  }
85
53
 
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
54
  /**
109
55
  * Sanitizes markdown/HTML descriptions into clean, concise single or multi-line agent instructions.
110
56
  */
@@ -138,269 +84,38 @@ export function deriveToolName(method, path) {
138
84
  }
139
85
 
140
86
  /**
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.
87
+ * Estimates the prompt context a manifest's tool definitions cost an agent on every turn.
88
+ *
89
+ * @param {object} manifest from buildMcpManifest()
90
+ * @returns {{ totalTools: number, totalResources: number, estimatedTokens: number,
91
+ * consequentialCount: number, warnings: string[] }} `consequentialCount` is the number of
92
+ * tools that are not read-only
370
93
  */
371
- export function estimateTokenCost(tools = [], resources = [], workflows = []) {
94
+ export function estimateTokenCost(manifest) {
95
+ const tools = manifest?.tools || [];
96
+ const resources = manifest?.resources || [];
372
97
  let estimatedTokens = 0;
373
98
  let consequentialCount = 0;
374
99
 
375
100
  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
101
+ // Name, description and the JSON Schema: ~4 chars per token, plus the envelope
102
+ estimatedTokens += Math.ceil(
103
+ (t.name.length + (t.description || "").length + JSON.stringify(t.inputSchema || {}).length) /
104
+ 4,
105
+ );
379
106
  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
- }
107
+ if (!t.annotations?.readOnlyHint) consequentialCount++;
387
108
  }
388
109
 
389
110
  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
- }
111
+ estimatedTokens +=
112
+ Math.ceil((r.name.length + (r.description || "").length + r.uri.length) / 4) + 20;
398
113
  }
399
114
 
400
115
  const warnings = [];
401
116
  if (estimatedTokens > 5000) {
402
117
  warnings.push(
403
- `Context overhead (~${estimatedTokens.toLocaleString()} tokens/turn) is high. Consider pruning unused tools with x-mcp.enabled: false.`,
118
+ `Context overhead (~${estimatedTokens.toLocaleString()} tokens/turn) is high. Consider hiding unused operations with x-internal: true.`,
404
119
  );
405
120
  }
406
121
  if (tools.length > 30) {
@@ -412,7 +127,6 @@ export function estimateTokenCost(tools = [], resources = [], workflows = []) {
412
127
  return {
413
128
  totalTools: tools.length,
414
129
  totalResources: resources.length,
415
- totalWorkflows: workflows.length,
416
130
  estimatedTokens,
417
131
  consequentialCount,
418
132
  warnings,
@@ -420,40 +134,25 @@ export function estimateTokenCost(tools = [], resources = [], workflows = []) {
420
134
  }
421
135
 
422
136
  /**
423
- * Generates Claude Desktop and Cursor JSON configuration snippets.
137
+ * Generates Claude Desktop and Cursor JSON configuration snippets for the exported project.
138
+ *
139
+ * @param {string} serverName
140
+ * @param {object} [options]
141
+ * @param {string[]} [options.schemes] security scheme ids; one `env` entry each (`API_KEY` when none)
424
142
  */
425
143
  export function generateHostConfigs(serverName, options = {}) {
426
144
  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
- }
145
+ const schemes = options.schemes?.length ? options.schemes : [];
146
+ const env = {};
147
+ for (const id of schemes) env[envNameFor(id)] = `your-${toSnakeCase(id).replace(/_/g, "-")}`;
148
+ if (!schemes.length) env.API_KEY = "your-api-key-here";
448
149
 
449
150
  const claudeConfig = {
450
151
  mcpServers: {
451
152
  [safeName]: {
452
- command,
453
- args,
454
- env: {
455
- API_KEY: "your-api-key-here",
456
- },
153
+ command: "npx",
154
+ args: ["-y", "tsx", "server.ts"],
155
+ env,
457
156
  },
458
157
  },
459
158
  };