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
package/src/utils/curl-utils.js
CHANGED
|
@@ -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) {
|
package/src/utils/highlighter.js
CHANGED
|
@@ -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`.
|
package/src/utils/mcp/common.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MCP (Model Context Protocol)
|
|
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
|
-
|
|
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
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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 {
|
|
31
|
+
* @returns {boolean}
|
|
45
32
|
*/
|
|
46
|
-
export function
|
|
47
|
-
if (!val || val ===
|
|
48
|
-
|
|
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.
|
|
65
|
-
|
|
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
|
-
*
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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(
|
|
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
|
-
//
|
|
377
|
-
estimatedTokens += Math.ceil(
|
|
378
|
-
|
|
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.
|
|
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 +=
|
|
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
|
|
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
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
};
|