@sezzlee/openapi 0.0.0-stage → 0.2.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.
@@ -0,0 +1,394 @@
1
+ import { childPointer, rootPointer } from "../ir/brand.js";
2
+ import { arrayOf, entriesOf, isObject, objectOf, stringOf, } from "../ir/json.js";
3
+ const methods = [
4
+ "get",
5
+ "put",
6
+ "post",
7
+ "delete",
8
+ "options",
9
+ "head",
10
+ "patch",
11
+ ];
12
+ const schemaKeys = [
13
+ "type",
14
+ "format",
15
+ "items",
16
+ "enum",
17
+ "default",
18
+ "minimum",
19
+ "maximum",
20
+ "exclusiveMinimum",
21
+ "exclusiveMaximum",
22
+ "minLength",
23
+ "maxLength",
24
+ "pattern",
25
+ "minItems",
26
+ "maxItems",
27
+ "uniqueItems",
28
+ "multipleOf",
29
+ ];
30
+ function located(context, node, at) {
31
+ context.origins.set(node, at);
32
+ return node;
33
+ }
34
+ /**
35
+ * Rewrites every 2.0 reference target to its 3.x home. `#/parameters` and `#/responses` point at
36
+ * 2.0 shapes that are converted in place under the same names, so their references move with them.
37
+ */
38
+ function rewriteRefs(value) {
39
+ if (Array.isArray(value)) {
40
+ return value.map(rewriteRefs);
41
+ }
42
+ if (!isObject(value)) {
43
+ return value;
44
+ }
45
+ const out = {};
46
+ for (const [key, child] of entriesOf(value)) {
47
+ if (key === "$ref" && typeof child === "string") {
48
+ out[key] = child
49
+ .replace(/^#\/definitions\//, "#/components/schemas/")
50
+ .replace(/^#\/parameters\//, "#/components/parameters/")
51
+ .replace(/^#\/responses\//, "#/components/responses/");
52
+ }
53
+ else if (key === "x-nullable" && child === true) {
54
+ out["nullable"] = true;
55
+ }
56
+ else {
57
+ out[key] = rewriteRefs(child);
58
+ }
59
+ }
60
+ if (out["type"] === "file") {
61
+ out["type"] = "string";
62
+ out["format"] = "binary";
63
+ }
64
+ return out;
65
+ }
66
+ function resolveParameter(context, parameter) {
67
+ const node = objectOf(parameter);
68
+ const ref = stringOf(node?.["$ref"]);
69
+ if (ref === undefined) {
70
+ return node;
71
+ }
72
+ const name = /^#\/parameters\/(.+)$/.exec(ref)?.[1];
73
+ return name === undefined
74
+ ? undefined
75
+ : objectOf(objectOf(context.document["parameters"])?.[name]);
76
+ }
77
+ function styleFor(location, collectionFormat, context, at) {
78
+ switch (collectionFormat ?? "csv") {
79
+ case "csv":
80
+ return location === "path" || location === "header"
81
+ ? { style: "simple", explode: false }
82
+ : { style: "form", explode: false };
83
+ case "ssv":
84
+ if (location === "query") {
85
+ return { style: "spaceDelimited", explode: false };
86
+ }
87
+ break;
88
+ case "pipes":
89
+ if (location === "query") {
90
+ return { style: "pipeDelimited", explode: false };
91
+ }
92
+ break;
93
+ case "multi":
94
+ if (location === "query") {
95
+ return { style: "form", explode: true };
96
+ }
97
+ break;
98
+ }
99
+ context.diagnostics.report("unsupported_collection_format", at, `collectionFormat '${collectionFormat ?? "csv"}' has no ${location} form.`);
100
+ return undefined;
101
+ }
102
+ function schemaOfSimple(node) {
103
+ const schema = {};
104
+ for (const key of schemaKeys) {
105
+ if (node[key] !== undefined) {
106
+ schema[key] = rewriteRefs(node[key]);
107
+ }
108
+ }
109
+ return schema;
110
+ }
111
+ function upgradeParameter(context, parameter, at) {
112
+ const location = stringOf(parameter["in"]) ?? "query";
113
+ const out = {
114
+ name: parameter["name"],
115
+ in: location,
116
+ ...(parameter["required"] === undefined
117
+ ? {}
118
+ : { required: parameter["required"] }),
119
+ ...(parameter["description"] === undefined
120
+ ? {}
121
+ : { description: parameter["description"] }),
122
+ schema: schemaOfSimple(parameter),
123
+ };
124
+ if (parameter["allowEmptyValue"] !== undefined) {
125
+ out["allowEmptyValue"] = parameter["allowEmptyValue"];
126
+ }
127
+ if (parameter["type"] === "array") {
128
+ const style = styleFor(location, stringOf(parameter["collectionFormat"]), context, at);
129
+ if (style === undefined) {
130
+ return "drop";
131
+ }
132
+ Object.assign(out, style);
133
+ }
134
+ return located(context, out, at);
135
+ }
136
+ function upgradeParameters(context, parameters, consumes, at) {
137
+ const out = [];
138
+ const form = { type: "object", properties: {} };
139
+ const formRequired = [];
140
+ let hasFormField = false;
141
+ let hasFile = false;
142
+ let requestBody;
143
+ let dropped = false;
144
+ parameters.forEach((raw, index) => {
145
+ const parameterAt = childPointer(at, index);
146
+ const parameter = resolveParameter(context, raw);
147
+ if (parameter === undefined) {
148
+ out.push(rewriteRefs(raw));
149
+ return;
150
+ }
151
+ const location = stringOf(parameter["in"]);
152
+ if (location === "body") {
153
+ const content = {};
154
+ for (const mediaType of consumes) {
155
+ content[mediaType] = {
156
+ schema: rewriteRefs(parameter["schema"] ?? {}),
157
+ };
158
+ }
159
+ requestBody = located(context, {
160
+ content,
161
+ required: parameter["required"] === true,
162
+ ...(parameter["description"] === undefined
163
+ ? {}
164
+ : { description: parameter["description"] }),
165
+ }, parameterAt);
166
+ return;
167
+ }
168
+ if (location === "formData") {
169
+ hasFormField = true;
170
+ const name = stringOf(parameter["name"]) ?? "";
171
+ const schema = schemaOfSimple(parameter);
172
+ if (schema["type"] === "file") {
173
+ hasFile = true;
174
+ schema["type"] = "string";
175
+ schema["format"] = "binary";
176
+ }
177
+ const collectionFormat = stringOf(parameter["collectionFormat"]);
178
+ if (parameter["type"] === "array" &&
179
+ collectionFormat !== undefined &&
180
+ collectionFormat !== "multi") {
181
+ context.diagnostics.report("unsupported_encoding", parameterAt, `A form field with collectionFormat '${collectionFormat}' has no form-body writer; only repeated keys are written.`);
182
+ dropped = true;
183
+ }
184
+ form["properties"][name] = located(context, schema, parameterAt);
185
+ if (parameter["required"] === true) {
186
+ formRequired.push(name);
187
+ }
188
+ return;
189
+ }
190
+ const upgraded = upgradeParameter(context, parameter, parameterAt);
191
+ if (upgraded === "drop") {
192
+ dropped = true;
193
+ return;
194
+ }
195
+ out.push(upgraded);
196
+ });
197
+ if (hasFormField) {
198
+ const mediaType = hasFile || consumes.includes("multipart/form-data")
199
+ ? "multipart/form-data"
200
+ : "application/x-www-form-urlencoded";
201
+ if (formRequired.length > 0) {
202
+ form["required"] = formRequired;
203
+ }
204
+ requestBody = {
205
+ content: { [mediaType]: { schema: form } },
206
+ required: formRequired.length > 0,
207
+ };
208
+ }
209
+ return {
210
+ parameters: out,
211
+ ...(requestBody === undefined ? {} : { requestBody }),
212
+ dropped,
213
+ };
214
+ }
215
+ function upgradeResponses(context, responses, produces, at) {
216
+ const out = {};
217
+ for (const [code, raw] of entriesOf(responses)) {
218
+ const response = objectOf(raw);
219
+ if (response === undefined) {
220
+ continue;
221
+ }
222
+ if (stringOf(response["$ref"]) !== undefined) {
223
+ out[code] = rewriteRefs(response);
224
+ continue;
225
+ }
226
+ const converted = {
227
+ description: response["description"] ?? "",
228
+ };
229
+ if (response["schema"] !== undefined) {
230
+ const content = {};
231
+ for (const mediaType of produces) {
232
+ content[mediaType] = { schema: rewriteRefs(response["schema"]) };
233
+ }
234
+ converted["content"] = content;
235
+ }
236
+ out[code] = located(context, converted, childPointer(at, code));
237
+ }
238
+ return out;
239
+ }
240
+ function upgradeSecuritySchemes(definitions) {
241
+ const out = {};
242
+ for (const [name, raw] of entriesOf(definitions)) {
243
+ const scheme = objectOf(raw);
244
+ if (scheme === undefined) {
245
+ continue;
246
+ }
247
+ switch (stringOf(scheme["type"])) {
248
+ case "basic":
249
+ out[name] = { type: "http", scheme: "basic" };
250
+ break;
251
+ case "apiKey":
252
+ out[name] = { type: "apiKey", in: scheme["in"], name: scheme["name"] };
253
+ break;
254
+ case "oauth2": {
255
+ const flow = {
256
+ implicit: "implicit",
257
+ password: "password",
258
+ application: "clientCredentials",
259
+ accessCode: "authorizationCode",
260
+ }[stringOf(scheme["flow"]) ?? ""] ?? "clientCredentials";
261
+ out[name] = {
262
+ type: "oauth2",
263
+ flows: {
264
+ [flow]: {
265
+ ...(scheme["authorizationUrl"] === undefined
266
+ ? {}
267
+ : { authorizationUrl: scheme["authorizationUrl"] }),
268
+ ...(scheme["tokenUrl"] === undefined
269
+ ? {}
270
+ : { tokenUrl: scheme["tokenUrl"] }),
271
+ scopes: scheme["scopes"] ?? {},
272
+ },
273
+ },
274
+ };
275
+ break;
276
+ }
277
+ }
278
+ }
279
+ return out;
280
+ }
281
+ function serversOf(document) {
282
+ const basePath = stringOf(document["basePath"]) ?? "";
283
+ const host = stringOf(document["host"]);
284
+ if (host === undefined) {
285
+ return [{ url: basePath === "" ? "/" : basePath }];
286
+ }
287
+ const schemes = arrayOf(document["schemes"])
288
+ .map(stringOf)
289
+ .filter((scheme) => scheme !== undefined);
290
+ const ordered = schemes.length === 0 ? ["https"] : schemes;
291
+ return [...ordered]
292
+ .sort((a, b) => Number(b === "https") - Number(a === "https"))
293
+ .map((scheme) => ({ url: `${scheme}://${host}${basePath}` }));
294
+ }
295
+ const mediaTypes = (value) => {
296
+ const list = arrayOf(value)
297
+ .map(stringOf)
298
+ .filter((item) => item !== undefined);
299
+ return list.length === 0 ? undefined : list;
300
+ };
301
+ /**
302
+ * Converts a Swagger 2.0 document to the OpenAPI 3 shape the rest of ingestion reads, following
303
+ * swagger2openapi's rules. Nodes that move are recorded in `origins` so a diagnostic still points
304
+ * into the author's document.
305
+ */
306
+ export function upgradeSwagger2(document, diagnostics, origins) {
307
+ const context = { document, diagnostics, origins };
308
+ const rootConsumes = mediaTypes(document["consumes"]) ?? ["application/json"];
309
+ const rootProduces = mediaTypes(document["produces"]) ?? ["application/json"];
310
+ const componentParameters = {};
311
+ for (const [name, raw] of entriesOf(document["parameters"])) {
312
+ const parameter = objectOf(raw);
313
+ const location = stringOf(parameter?.["in"]);
314
+ if (parameter !== undefined &&
315
+ location !== "body" &&
316
+ location !== "formData") {
317
+ const upgraded = upgradeParameter(context, parameter, childPointer(rootPointer, "parameters", name));
318
+ if (upgraded !== "drop") {
319
+ componentParameters[name] = upgraded;
320
+ }
321
+ }
322
+ }
323
+ const paths = {};
324
+ for (const [path, rawItem] of entriesOf(document["paths"])) {
325
+ const item = objectOf(rawItem);
326
+ if (item === undefined) {
327
+ continue;
328
+ }
329
+ const itemAt = childPointer(rootPointer, "paths", path);
330
+ const shared = arrayOf(item["parameters"]);
331
+ const converted = {};
332
+ for (const method of methods) {
333
+ const operation = objectOf(item[method]);
334
+ if (operation === undefined) {
335
+ continue;
336
+ }
337
+ const operationAt = childPointer(itemAt, method);
338
+ const consumes = mediaTypes(operation["consumes"]) ?? rootConsumes;
339
+ const produces = mediaTypes(operation["produces"]) ?? rootProduces;
340
+ const own = arrayOf(operation["parameters"]);
341
+ const ownKeys = new Set(own
342
+ .map((parameter) => resolveParameter(context, parameter))
343
+ .map((parameter) => parameter === undefined
344
+ ? ""
345
+ : `${stringOf(parameter["in"])}|${stringOf(parameter["name"])}`));
346
+ const inherited = shared.filter((parameter) => {
347
+ const resolved = resolveParameter(context, parameter);
348
+ return (resolved === undefined ||
349
+ !ownKeys.has(`${stringOf(resolved["in"])}|${stringOf(resolved["name"])}`));
350
+ });
351
+ const upgraded = upgradeParameters(context, [...inherited, ...own], consumes, childPointer(operationAt, "parameters"));
352
+ const out = {};
353
+ for (const [key, value] of entriesOf(operation)) {
354
+ if (key === "parameters" ||
355
+ key === "consumes" ||
356
+ key === "produces" ||
357
+ key === "responses" ||
358
+ key === "schemes") {
359
+ continue;
360
+ }
361
+ out[key] = rewriteRefs(value);
362
+ }
363
+ out["parameters"] = upgraded.parameters;
364
+ if (upgraded.requestBody !== undefined) {
365
+ out["requestBody"] = upgraded.requestBody;
366
+ }
367
+ out["responses"] = upgradeResponses(context, operation["responses"], produces, childPointer(operationAt, "responses"));
368
+ if (upgraded.dropped) {
369
+ out["x-sezzlee-dropped"] = true;
370
+ }
371
+ converted[method] = located(context, out, operationAt);
372
+ }
373
+ paths[path] = located(context, converted, itemAt);
374
+ }
375
+ const componentResponses = {};
376
+ const upgradedResponses = upgradeResponses(context, document["responses"], rootProduces, childPointer(rootPointer, "responses"));
377
+ Object.assign(componentResponses, upgradedResponses);
378
+ return {
379
+ openapi: "3.0.3",
380
+ info: document["info"] ?? {},
381
+ servers: serversOf(document),
382
+ paths,
383
+ components: {
384
+ schemas: rewriteRefs(document["definitions"] ?? {}),
385
+ parameters: componentParameters,
386
+ responses: componentResponses,
387
+ securitySchemes: upgradeSecuritySchemes(document["securityDefinitions"]),
388
+ },
389
+ ...(document["security"] === undefined
390
+ ? {}
391
+ : { security: document["security"] }),
392
+ ...(document["tags"] === undefined ? {} : { tags: document["tags"] }),
393
+ };
394
+ }
package/package.json CHANGED
@@ -1,6 +1,61 @@
1
1
  {
2
2
  "name": "@sezzlee/openapi",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "description": "Ingests a Swagger 2.0 or OpenAPI 3.0–3.2 document into the endpoint descriptors the sezzlee catalog is built from.",
5
+ "keywords": [
6
+ "mcp",
7
+ "model-context-protocol",
8
+ "sezzlee",
9
+ "openapi",
10
+ "swagger",
11
+ "agents",
12
+ "typescript"
13
+ ],
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/sezzlee/mcp.git",
18
+ "directory": "packages/http/openapi"
19
+ },
20
+ "homepage": "https://github.com/sezzlee/mcp/tree/main/packages/http/openapi#readme",
21
+ "bugs": {
22
+ "url": "https://github.com/sezzlee/mcp/issues"
23
+ },
24
+ "type": "module",
25
+ "engines": {
26
+ "node": ">=22"
27
+ },
28
+ "main": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "default": "./dist/index.js"
34
+ }
35
+ },
36
+ "files": [
37
+ "dist"
38
+ ],
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "dependencies": {
43
+ "@sezzlee/core": "^0.2.0",
44
+ "yaml": "^2.9.1"
45
+ },
46
+ "devDependencies": {
47
+ "@sezzlee/oxlint-config": "0.0.0",
48
+ "@sezzlee/typescript-config": "0.0.0",
49
+ "@types/node": "^24.3.0",
50
+ "oxlint": "1.85.0",
51
+ "typescript": "7.0.2",
52
+ "vitest": "^3.2.4"
53
+ },
54
+ "scripts": {
55
+ "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
56
+ "dev": "tsc --watch",
57
+ "lint": "oxlint --deny-warnings",
58
+ "check-types": "tsc -p tsconfig.test.json",
59
+ "test": "vitest run"
60
+ }
6
61
  }