@ttoss/http-server-mcp-openapi 0.2.13 → 0.2.15

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 CHANGED
@@ -61,7 +61,11 @@ Each OpenAPI operation with an `operationId` and a supported HTTP method
61
61
  | operation `description` | tool description (quotes/newlines sanitised) |
62
62
 
63
63
  Path params are always required strings. Query and body params carry their
64
- declared type and `required` flag. Array params keep their `items` schema.
64
+ declared type and `required` flag. Array params keep their `items` schema. A
65
+ body property declared as a single-entry `allOf` (usually `allOf: [{ $ref }]`
66
+ beside its own `description`) takes `type`, `nullable` and `items` from the
67
+ referenced schema; a multi-entry `allOf` is forwarded verbatim, and a property
68
+ with no declared type is advertised untyped so it accepts any value.
65
69
  Parameters declared at the **path-item level** (shared by every operation on a
66
70
  path) are merged into each operation; an operation-level parameter overrides a
67
71
  path-item one with the same `name`+`in`.
@@ -69,6 +73,11 @@ path-item one with the same `name`+`in`.
69
73
  Tool arguments are **camelCase** (`agentId`, `projectId`); the generated
70
74
  request path, query string, and body use the original **snake_case** names.
71
75
 
76
+ Query params honour their declared `style` and `explode`. `form` (the default)
77
+ repeats array values, `spaceDelimited`/`pipeDelimited` join them, and
78
+ `deepObject` emits bracketed keys — including nested objects and arrays, so
79
+ `{ documentId: { $eq: 'doc_1' } }` becomes `filters[documentId][$eq]=doc_1`.
80
+
72
81
  ## `registerOpenApiTools`
73
82
 
74
83
  | Field | Description |
package/dist/index.cjs CHANGED
@@ -117,22 +117,108 @@ var buildPathFn = (pathTemplate, pathParams) => {
117
117
  return result;
118
118
  };
119
119
  };
120
+ var NON_EXPLODED_DELIMITERS = {
121
+ form: ",",
122
+ spaceDelimited: " ",
123
+ pipeDelimited: "|"
124
+ };
125
+ var isRecord = value => {
126
+ return typeof value === "object" && value !== null && !Array.isArray(value);
127
+ };
128
+ /**
129
+ * Appends a `deepObject` value as bracketed keys — `filters[documentId][$eq]`.
130
+ * OpenAPI only defines one level, but the APIs that ask for `deepObject`
131
+ * (Strapi, Directus) nest, so nested objects and arrays recurse instead of
132
+ * being stringified into `[object Object]`.
133
+ */
134
+ var appendDeepObject = args => {
135
+ if (args.value === void 0 || args.value === null) return;
136
+ if (Array.isArray(args.value)) {
137
+ for (const [index, item] of args.value.entries()) appendDeepObject({
138
+ search: args.search,
139
+ key: `${args.key}[${index}]`,
140
+ value: item
141
+ });
142
+ return;
143
+ }
144
+ if (isRecord(args.value)) {
145
+ for (const [property, propertyValue] of Object.entries(args.value)) appendDeepObject({
146
+ search: args.search,
147
+ key: `${args.key}[${property}]`,
148
+ value: propertyValue
149
+ });
150
+ return;
151
+ }
152
+ args.search.append(args.key, String(args.value));
153
+ };
154
+ var appendArrayValue = args => {
155
+ if (args.explode) {
156
+ for (const item of args.value) args.search.append(args.name, String(item));
157
+ return;
158
+ }
159
+ args.search.append(args.name, args.value.map(String).join(args.delimiter));
160
+ };
161
+ var appendObjectValue = args => {
162
+ const entries = Object.entries(args.value).filter(([, entryValue]) => {
163
+ return entryValue !== void 0 && entryValue !== null;
164
+ });
165
+ if (args.explode) {
166
+ for (const [property, entryValue] of entries) args.search.append(property, String(entryValue));
167
+ return;
168
+ }
169
+ args.search.append(args.name, entries.flatMap(([property, entryValue]) => {
170
+ return [property, String(entryValue)];
171
+ }).join(args.delimiter));
172
+ };
173
+ var appendQueryValue = args => {
174
+ const style = args.param.style ?? "form";
175
+ if (style === "deepObject") {
176
+ appendDeepObject({
177
+ search: args.search,
178
+ key: args.param.name,
179
+ value: args.value
180
+ });
181
+ return;
182
+ }
183
+ const serialization = {
184
+ search: args.search,
185
+ name: args.param.name,
186
+ explode: args.param.explode ?? style === "form",
187
+ delimiter: NON_EXPLODED_DELIMITERS[style] ?? ","
188
+ };
189
+ if (Array.isArray(args.value)) {
190
+ appendArrayValue({
191
+ ...serialization,
192
+ value: args.value
193
+ });
194
+ return;
195
+ }
196
+ if (isRecord(args.value)) {
197
+ appendObjectValue({
198
+ ...serialization,
199
+ value: args.value
200
+ });
201
+ return;
202
+ }
203
+ args.search.append(args.param.name, String(args.value));
204
+ };
120
205
  /**
121
206
  * Builds a function that serialises query params into a query string
122
- * (including the leading `?`). Returns `undefined` when the op has no query
123
- * params. Array values are appended once per element.
207
+ * (including the leading `?`), honouring each param's OpenAPI `style` and
208
+ * `explode`. Returns `undefined` when the op has no query params.
124
209
  */
125
210
  var buildQueryFn = queryParams => {
126
211
  if (queryParams.length === 0) return void 0;
127
212
  return args => {
128
213
  const search = new URLSearchParams();
129
- for (const {
130
- name,
131
- camelName
132
- } of queryParams) {
133
- const value = args[camelName];
214
+ for (const param of queryParams) {
215
+ const value = args[param.camelName];
134
216
  if (value === void 0 || value === null) continue;
135
- if (Array.isArray(value)) for (const item of value) search.append(name, String(item));else search.append(name, String(value));
217
+ appendQueryValue({
218
+ search,
219
+ param,
220
+ value
221
+ });
136
222
  }
137
223
  const qs = search.toString();
138
224
  return qs ? `?${qs}` : "";
@@ -186,11 +272,13 @@ var sanitizeDescription = description => {
186
272
  return (description || "").replace(/'/g, "\\'").replace(/\n/g, " ").trim();
187
273
  };
188
274
  /**
189
- * Builds a single body/query property's `JsonSchemaProperty`. Split out of
190
- * {@link buildInputSchema} to keep that function's size and branching down.
275
+ * Forwards a property's `oneOf` / `anyOf` / multi-entry `allOf` verbatim, or
276
+ * returns `undefined` when it declares none.
191
277
  */
192
- var buildTypedProperty = param => {
193
- const description = sanitizeDescription(param.description);
278
+ var buildComposedProperty = param => {
279
+ const {
280
+ description
281
+ } = param;
194
282
  if (param.oneOf && param.oneOf.length > 0) return {
195
283
  oneOf: param.oneOf,
196
284
  description
@@ -199,6 +287,25 @@ var buildTypedProperty = param => {
199
287
  anyOf: param.anyOf,
200
288
  description
201
289
  };
290
+ if (param.allOf && param.allOf.length > 0) return {
291
+ allOf: param.allOf,
292
+ description
293
+ };
294
+ };
295
+ /**
296
+ * Builds a single body/query property's `JsonSchemaProperty`. Split out of
297
+ * {@link buildInputSchema} to keep that function's size and branching down.
298
+ */
299
+ var buildTypedProperty = param => {
300
+ const description = sanitizeDescription(param.description);
301
+ const composed = buildComposedProperty({
302
+ ...param,
303
+ description
304
+ });
305
+ if (composed) return composed;
306
+ if (param.type === void 0) return {
307
+ description
308
+ };
202
309
  const jsonType = getJsonSchemaType(param.type);
203
310
  const finalType = param.nullable === true ? [jsonType, "null"] : jsonType;
204
311
  if (param.type === "array") return {
@@ -230,7 +337,7 @@ var buildInputSchema = (pathParams, queryParams, bodyProps) => {
230
337
  return p.camelName;
231
338
  })];
232
339
  const properties = {};
233
- for (const param of allParams) properties[param.camelName] = "type" in param ? buildTypedProperty(param) : {
340
+ for (const param of allParams) properties[param.camelName] = "description" in param ? buildTypedProperty(param) : {
234
341
  type: "string",
235
342
  description: ""
236
343
  };
@@ -275,7 +382,9 @@ var extractQueryParams = args => {
275
382
  camelName: snakeToCamel(p.name || ""),
276
383
  description: p.description || "",
277
384
  required: p.required || false,
278
- type: p.schema?.type || "string"
385
+ type: p.schema?.type || "string",
386
+ style: p.style,
387
+ explode: p.explode
279
388
  };
280
389
  }));
281
390
  };
@@ -291,23 +400,47 @@ var extractAcceptedBodyFields = args => {
291
400
  const bodySchema = resolveBodySchema(args);
292
401
  return Object.keys(bodySchema?.properties ?? {});
293
402
  };
403
+ var isPlainObject = value => {
404
+ return typeof value === "object" && value !== null && !Array.isArray(value);
405
+ };
406
+ /**
407
+ * Folds a single-entry `allOf` into the property that wraps it. OpenAPI
408
+ * declares `allOf: [{ $ref }]` so a property can carry its own `description`
409
+ * next to a referenced schema; without folding, the referenced `type`,
410
+ * `nullable` and `items` would be lost. Keys on the wrapper win over the
411
+ * referenced schema's. Multi-entry `allOf` is left intact.
412
+ */
413
+ var flattenSingleAllOf = schema => {
414
+ const {
415
+ allOf,
416
+ ...rest
417
+ } = schema;
418
+ if (!Array.isArray(allOf) || allOf.length !== 1) return schema;
419
+ const [entry] = allOf;
420
+ if (!isPlainObject(entry)) return rest;
421
+ return {
422
+ ...flattenSingleAllOf(entry),
423
+ ...rest
424
+ };
425
+ };
294
426
  var extractBodyProps = args => {
295
427
  const bodySchema = resolveBodySchema(args);
296
428
  if (!bodySchema?.properties) return [];
297
429
  return Object.entries(bodySchema.properties).filter(([, value]) => {
298
430
  return !value[args.serverManagedExtension];
299
431
  }).map(([key, value]) => {
300
- const val = value;
432
+ const val = flattenSingleAllOf(value);
301
433
  return {
302
434
  snakeName: key,
303
435
  camelName: snakeToCamel(key),
304
436
  description: typeof val.description === "string" ? val.description : "",
305
437
  required: (bodySchema.required || []).includes(key),
306
- type: typeof val.type === "string" ? val.type : "string",
438
+ type: typeof val.type === "string" ? val.type : void 0,
307
439
  items: val.items,
308
440
  nullable: val.nullable === true,
309
441
  oneOf: Array.isArray(val.oneOf) ? val.oneOf : void 0,
310
- anyOf: Array.isArray(val.anyOf) ? val.anyOf : void 0
442
+ anyOf: Array.isArray(val.anyOf) ? val.anyOf : void 0,
443
+ allOf: Array.isArray(val.allOf) ? val.allOf : void 0
311
444
  };
312
445
  });
313
446
  };
package/dist/index.d.cts CHANGED
@@ -13,7 +13,9 @@ type JsonSchemaPrimitiveType = 'string' | 'number' | 'boolean' | 'array' | 'inte
13
13
  * client-side validator enforces the same alternatives the REST API does,
14
14
  * rather than collapsing to a single guessed primitive. `type` may be an
15
15
  * array of two entries (e.g. `['string', 'null']`) to represent an OpenAPI
16
- * `nullable: true` property without losing its declared type.
16
+ * `nullable: true` property without losing its declared type. A property with
17
+ * no declared type is emitted untyped (only `description`), so it accepts any
18
+ * value instead of a guessed `string`.
17
19
  */
18
20
  type JsonSchemaProperty = {
19
21
  type: JsonSchemaPrimitiveType | JsonSchemaPrimitiveType[];
@@ -22,6 +24,7 @@ type JsonSchemaProperty = {
22
24
  } | {
23
25
  oneOf?: unknown[];
24
26
  anyOf?: unknown[];
27
+ allOf?: unknown[];
25
28
  description?: string;
26
29
  };
27
30
  /**
@@ -93,7 +96,9 @@ interface OperationSpec {
93
96
  name?: string;
94
97
  in?: string;
95
98
  required?: boolean;
96
- description?: string;
99
+ description?: string; /** Query serialisation style, e.g. `form` (default) or `deepObject`. */
100
+ style?: string; /** Whether each value gets its own key. Defaults to `true` for `form`. */
101
+ explode?: boolean;
97
102
  schema?: {
98
103
  type?: string;
99
104
  items?: {
@@ -212,6 +217,8 @@ declare const resolveParameter: (param: Record<string, unknown> | undefined, spe
212
217
  in?: string;
213
218
  required?: boolean;
214
219
  description?: string;
220
+ style?: string;
221
+ explode?: boolean;
215
222
  schema?: {
216
223
  type?: string;
217
224
  items?: {
@@ -224,15 +231,19 @@ declare const buildPathFn: (pathTemplate: string, pathParams: Array<{
224
231
  name: string;
225
232
  camelName: string;
226
233
  }>) => ((args: Record<string, unknown>) => string);
234
+ /** Query parameter with the OpenAPI serialisation rules declared for it. */
235
+ type QueryParamSerialization = {
236
+ name: string;
237
+ camelName: string; /** OpenAPI `style` (`form`, `deepObject`, `spaceDelimited`, `pipeDelimited`). */
238
+ style?: string; /** OpenAPI `explode`. Defaults to `true` for `form`, `false` otherwise. */
239
+ explode?: boolean;
240
+ };
227
241
  /**
228
242
  * Builds a function that serialises query params into a query string
229
- * (including the leading `?`). Returns `undefined` when the op has no query
230
- * params. Array values are appended once per element.
243
+ * (including the leading `?`), honouring each param's OpenAPI `style` and
244
+ * `explode`. Returns `undefined` when the op has no query params.
231
245
  */
232
- declare const buildQueryFn: (queryParams: Array<{
233
- name: string;
234
- camelName: string;
235
- }>) => ((args: Record<string, unknown>) => string) | undefined;
246
+ declare const buildQueryFn: (queryParams: QueryParamSerialization[]) => ((args: Record<string, unknown>) => string) | undefined;
236
247
  /**
237
248
  * Builds a function that maps camelCase args back to a snake_case request
238
249
  * body, skipping `undefined` args. Returns `undefined` when the op has no body.
@@ -266,11 +277,12 @@ declare const buildInputSchema: (pathParams: Array<{
266
277
  camelName: string;
267
278
  description: string;
268
279
  required: boolean;
269
- type: string;
280
+ type?: string;
270
281
  items?: unknown;
271
282
  nullable?: boolean;
272
283
  oneOf?: unknown[];
273
284
  anyOf?: unknown[];
285
+ allOf?: unknown[];
274
286
  }>) => JsonObjectSchema;
275
287
  declare const extractPathParams: (args: {
276
288
  parameters?: Array<{
@@ -296,6 +308,8 @@ declare const extractQueryParams: (args: {
296
308
  description: string;
297
309
  required: boolean;
298
310
  type: string;
311
+ style?: string;
312
+ explode?: boolean;
299
313
  }>;
300
314
  /**
301
315
  * snake_case names of every top-level property an operation's request schema
@@ -314,11 +328,12 @@ declare const extractBodyProps: (args: {
314
328
  camelName: string;
315
329
  description: string;
316
330
  required: boolean;
317
- type: string;
331
+ type?: string;
318
332
  items?: unknown;
319
333
  nullable: boolean;
320
334
  oneOf?: unknown[];
321
335
  anyOf?: unknown[];
336
+ allOf?: unknown[];
322
337
  }>;
323
338
  declare const processOperation: (args: {
324
339
  pathTemplate: string;
@@ -360,4 +375,4 @@ declare const openApiToToolDefinitions: (args: {
360
375
  options?: OpenApiToToolsOptions;
361
376
  }) => ToolDefinition[];
362
377
  //#endregion
363
- export { DEFAULT_EXCLUDE_EXTENSION, DEFAULT_SERVER_MANAGED_EXTENSION, type JsonSchemaProperty, type OpenApiSpec, type OpenApiToToolsOptions, type OperationSpec, type RegisterOpenApiToolsArgs, type RequestBodySpec, type ResolvedRequest, type ToolDefinition, buildBodyFn, buildInputSchema, buildPathFn, buildQueryFn, dereferenceSchema, extractAcceptedBodyFields, extractBodyProps, extractPathParams, extractQueryParams, getJsonSchemaType, openApiToToolDefinitions, operationIdToToolName, processOperation, processPath, registerOpenApiTools, resolveParameter, resolveSchema, snakeToCamel };
378
+ export { DEFAULT_EXCLUDE_EXTENSION, DEFAULT_SERVER_MANAGED_EXTENSION, type JsonSchemaProperty, type OpenApiSpec, type OpenApiToToolsOptions, type OperationSpec, type QueryParamSerialization, type RegisterOpenApiToolsArgs, type RequestBodySpec, type ResolvedRequest, type ToolDefinition, buildBodyFn, buildInputSchema, buildPathFn, buildQueryFn, dereferenceSchema, extractAcceptedBodyFields, extractBodyProps, extractPathParams, extractQueryParams, getJsonSchemaType, openApiToToolDefinitions, operationIdToToolName, processOperation, processPath, registerOpenApiTools, resolveParameter, resolveSchema, snakeToCamel };
package/dist/index.d.mts CHANGED
@@ -13,7 +13,9 @@ type JsonSchemaPrimitiveType = 'string' | 'number' | 'boolean' | 'array' | 'inte
13
13
  * client-side validator enforces the same alternatives the REST API does,
14
14
  * rather than collapsing to a single guessed primitive. `type` may be an
15
15
  * array of two entries (e.g. `['string', 'null']`) to represent an OpenAPI
16
- * `nullable: true` property without losing its declared type.
16
+ * `nullable: true` property without losing its declared type. A property with
17
+ * no declared type is emitted untyped (only `description`), so it accepts any
18
+ * value instead of a guessed `string`.
17
19
  */
18
20
  type JsonSchemaProperty = {
19
21
  type: JsonSchemaPrimitiveType | JsonSchemaPrimitiveType[];
@@ -22,6 +24,7 @@ type JsonSchemaProperty = {
22
24
  } | {
23
25
  oneOf?: unknown[];
24
26
  anyOf?: unknown[];
27
+ allOf?: unknown[];
25
28
  description?: string;
26
29
  };
27
30
  /**
@@ -93,7 +96,9 @@ interface OperationSpec {
93
96
  name?: string;
94
97
  in?: string;
95
98
  required?: boolean;
96
- description?: string;
99
+ description?: string; /** Query serialisation style, e.g. `form` (default) or `deepObject`. */
100
+ style?: string; /** Whether each value gets its own key. Defaults to `true` for `form`. */
101
+ explode?: boolean;
97
102
  schema?: {
98
103
  type?: string;
99
104
  items?: {
@@ -212,6 +217,8 @@ declare const resolveParameter: (param: Record<string, unknown> | undefined, spe
212
217
  in?: string;
213
218
  required?: boolean;
214
219
  description?: string;
220
+ style?: string;
221
+ explode?: boolean;
215
222
  schema?: {
216
223
  type?: string;
217
224
  items?: {
@@ -224,15 +231,19 @@ declare const buildPathFn: (pathTemplate: string, pathParams: Array<{
224
231
  name: string;
225
232
  camelName: string;
226
233
  }>) => ((args: Record<string, unknown>) => string);
234
+ /** Query parameter with the OpenAPI serialisation rules declared for it. */
235
+ type QueryParamSerialization = {
236
+ name: string;
237
+ camelName: string; /** OpenAPI `style` (`form`, `deepObject`, `spaceDelimited`, `pipeDelimited`). */
238
+ style?: string; /** OpenAPI `explode`. Defaults to `true` for `form`, `false` otherwise. */
239
+ explode?: boolean;
240
+ };
227
241
  /**
228
242
  * Builds a function that serialises query params into a query string
229
- * (including the leading `?`). Returns `undefined` when the op has no query
230
- * params. Array values are appended once per element.
243
+ * (including the leading `?`), honouring each param's OpenAPI `style` and
244
+ * `explode`. Returns `undefined` when the op has no query params.
231
245
  */
232
- declare const buildQueryFn: (queryParams: Array<{
233
- name: string;
234
- camelName: string;
235
- }>) => ((args: Record<string, unknown>) => string) | undefined;
246
+ declare const buildQueryFn: (queryParams: QueryParamSerialization[]) => ((args: Record<string, unknown>) => string) | undefined;
236
247
  /**
237
248
  * Builds a function that maps camelCase args back to a snake_case request
238
249
  * body, skipping `undefined` args. Returns `undefined` when the op has no body.
@@ -266,11 +277,12 @@ declare const buildInputSchema: (pathParams: Array<{
266
277
  camelName: string;
267
278
  description: string;
268
279
  required: boolean;
269
- type: string;
280
+ type?: string;
270
281
  items?: unknown;
271
282
  nullable?: boolean;
272
283
  oneOf?: unknown[];
273
284
  anyOf?: unknown[];
285
+ allOf?: unknown[];
274
286
  }>) => JsonObjectSchema;
275
287
  declare const extractPathParams: (args: {
276
288
  parameters?: Array<{
@@ -296,6 +308,8 @@ declare const extractQueryParams: (args: {
296
308
  description: string;
297
309
  required: boolean;
298
310
  type: string;
311
+ style?: string;
312
+ explode?: boolean;
299
313
  }>;
300
314
  /**
301
315
  * snake_case names of every top-level property an operation's request schema
@@ -314,11 +328,12 @@ declare const extractBodyProps: (args: {
314
328
  camelName: string;
315
329
  description: string;
316
330
  required: boolean;
317
- type: string;
331
+ type?: string;
318
332
  items?: unknown;
319
333
  nullable: boolean;
320
334
  oneOf?: unknown[];
321
335
  anyOf?: unknown[];
336
+ allOf?: unknown[];
322
337
  }>;
323
338
  declare const processOperation: (args: {
324
339
  pathTemplate: string;
@@ -360,4 +375,4 @@ declare const openApiToToolDefinitions: (args: {
360
375
  options?: OpenApiToToolsOptions;
361
376
  }) => ToolDefinition[];
362
377
  //#endregion
363
- export { DEFAULT_EXCLUDE_EXTENSION, DEFAULT_SERVER_MANAGED_EXTENSION, type JsonSchemaProperty, type OpenApiSpec, type OpenApiToToolsOptions, type OperationSpec, type RegisterOpenApiToolsArgs, type RequestBodySpec, type ResolvedRequest, type ToolDefinition, buildBodyFn, buildInputSchema, buildPathFn, buildQueryFn, dereferenceSchema, extractAcceptedBodyFields, extractBodyProps, extractPathParams, extractQueryParams, getJsonSchemaType, openApiToToolDefinitions, operationIdToToolName, processOperation, processPath, registerOpenApiTools, resolveParameter, resolveSchema, snakeToCamel };
378
+ export { DEFAULT_EXCLUDE_EXTENSION, DEFAULT_SERVER_MANAGED_EXTENSION, type JsonSchemaProperty, type OpenApiSpec, type OpenApiToToolsOptions, type OperationSpec, type QueryParamSerialization, type RegisterOpenApiToolsArgs, type RequestBodySpec, type ResolvedRequest, type ToolDefinition, buildBodyFn, buildInputSchema, buildPathFn, buildQueryFn, dereferenceSchema, extractAcceptedBodyFields, extractBodyProps, extractPathParams, extractQueryParams, getJsonSchemaType, openApiToToolDefinitions, operationIdToToolName, processOperation, processPath, registerOpenApiTools, resolveParameter, resolveSchema, snakeToCamel };
package/dist/index.mjs CHANGED
@@ -114,22 +114,108 @@ var buildPathFn = (pathTemplate, pathParams) => {
114
114
  return result;
115
115
  };
116
116
  };
117
+ var NON_EXPLODED_DELIMITERS = {
118
+ form: ",",
119
+ spaceDelimited: " ",
120
+ pipeDelimited: "|"
121
+ };
122
+ var isRecord = value => {
123
+ return typeof value === "object" && value !== null && !Array.isArray(value);
124
+ };
125
+ /**
126
+ * Appends a `deepObject` value as bracketed keys — `filters[documentId][$eq]`.
127
+ * OpenAPI only defines one level, but the APIs that ask for `deepObject`
128
+ * (Strapi, Directus) nest, so nested objects and arrays recurse instead of
129
+ * being stringified into `[object Object]`.
130
+ */
131
+ var appendDeepObject = args => {
132
+ if (args.value === void 0 || args.value === null) return;
133
+ if (Array.isArray(args.value)) {
134
+ for (const [index, item] of args.value.entries()) appendDeepObject({
135
+ search: args.search,
136
+ key: `${args.key}[${index}]`,
137
+ value: item
138
+ });
139
+ return;
140
+ }
141
+ if (isRecord(args.value)) {
142
+ for (const [property, propertyValue] of Object.entries(args.value)) appendDeepObject({
143
+ search: args.search,
144
+ key: `${args.key}[${property}]`,
145
+ value: propertyValue
146
+ });
147
+ return;
148
+ }
149
+ args.search.append(args.key, String(args.value));
150
+ };
151
+ var appendArrayValue = args => {
152
+ if (args.explode) {
153
+ for (const item of args.value) args.search.append(args.name, String(item));
154
+ return;
155
+ }
156
+ args.search.append(args.name, args.value.map(String).join(args.delimiter));
157
+ };
158
+ var appendObjectValue = args => {
159
+ const entries = Object.entries(args.value).filter(([, entryValue]) => {
160
+ return entryValue !== void 0 && entryValue !== null;
161
+ });
162
+ if (args.explode) {
163
+ for (const [property, entryValue] of entries) args.search.append(property, String(entryValue));
164
+ return;
165
+ }
166
+ args.search.append(args.name, entries.flatMap(([property, entryValue]) => {
167
+ return [property, String(entryValue)];
168
+ }).join(args.delimiter));
169
+ };
170
+ var appendQueryValue = args => {
171
+ const style = args.param.style ?? "form";
172
+ if (style === "deepObject") {
173
+ appendDeepObject({
174
+ search: args.search,
175
+ key: args.param.name,
176
+ value: args.value
177
+ });
178
+ return;
179
+ }
180
+ const serialization = {
181
+ search: args.search,
182
+ name: args.param.name,
183
+ explode: args.param.explode ?? style === "form",
184
+ delimiter: NON_EXPLODED_DELIMITERS[style] ?? ","
185
+ };
186
+ if (Array.isArray(args.value)) {
187
+ appendArrayValue({
188
+ ...serialization,
189
+ value: args.value
190
+ });
191
+ return;
192
+ }
193
+ if (isRecord(args.value)) {
194
+ appendObjectValue({
195
+ ...serialization,
196
+ value: args.value
197
+ });
198
+ return;
199
+ }
200
+ args.search.append(args.param.name, String(args.value));
201
+ };
117
202
  /**
118
203
  * Builds a function that serialises query params into a query string
119
- * (including the leading `?`). Returns `undefined` when the op has no query
120
- * params. Array values are appended once per element.
204
+ * (including the leading `?`), honouring each param's OpenAPI `style` and
205
+ * `explode`. Returns `undefined` when the op has no query params.
121
206
  */
122
207
  var buildQueryFn = queryParams => {
123
208
  if (queryParams.length === 0) return void 0;
124
209
  return args => {
125
210
  const search = new URLSearchParams();
126
- for (const {
127
- name,
128
- camelName
129
- } of queryParams) {
130
- const value = args[camelName];
211
+ for (const param of queryParams) {
212
+ const value = args[param.camelName];
131
213
  if (value === void 0 || value === null) continue;
132
- if (Array.isArray(value)) for (const item of value) search.append(name, String(item));else search.append(name, String(value));
214
+ appendQueryValue({
215
+ search,
216
+ param,
217
+ value
218
+ });
133
219
  }
134
220
  const qs = search.toString();
135
221
  return qs ? `?${qs}` : "";
@@ -183,11 +269,13 @@ var sanitizeDescription = description => {
183
269
  return (description || "").replace(/'/g, "\\'").replace(/\n/g, " ").trim();
184
270
  };
185
271
  /**
186
- * Builds a single body/query property's `JsonSchemaProperty`. Split out of
187
- * {@link buildInputSchema} to keep that function's size and branching down.
272
+ * Forwards a property's `oneOf` / `anyOf` / multi-entry `allOf` verbatim, or
273
+ * returns `undefined` when it declares none.
188
274
  */
189
- var buildTypedProperty = param => {
190
- const description = sanitizeDescription(param.description);
275
+ var buildComposedProperty = param => {
276
+ const {
277
+ description
278
+ } = param;
191
279
  if (param.oneOf && param.oneOf.length > 0) return {
192
280
  oneOf: param.oneOf,
193
281
  description
@@ -196,6 +284,25 @@ var buildTypedProperty = param => {
196
284
  anyOf: param.anyOf,
197
285
  description
198
286
  };
287
+ if (param.allOf && param.allOf.length > 0) return {
288
+ allOf: param.allOf,
289
+ description
290
+ };
291
+ };
292
+ /**
293
+ * Builds a single body/query property's `JsonSchemaProperty`. Split out of
294
+ * {@link buildInputSchema} to keep that function's size and branching down.
295
+ */
296
+ var buildTypedProperty = param => {
297
+ const description = sanitizeDescription(param.description);
298
+ const composed = buildComposedProperty({
299
+ ...param,
300
+ description
301
+ });
302
+ if (composed) return composed;
303
+ if (param.type === void 0) return {
304
+ description
305
+ };
199
306
  const jsonType = getJsonSchemaType(param.type);
200
307
  const finalType = param.nullable === true ? [jsonType, "null"] : jsonType;
201
308
  if (param.type === "array") return {
@@ -227,7 +334,7 @@ var buildInputSchema = (pathParams, queryParams, bodyProps) => {
227
334
  return p.camelName;
228
335
  })];
229
336
  const properties = {};
230
- for (const param of allParams) properties[param.camelName] = "type" in param ? buildTypedProperty(param) : {
337
+ for (const param of allParams) properties[param.camelName] = "description" in param ? buildTypedProperty(param) : {
231
338
  type: "string",
232
339
  description: ""
233
340
  };
@@ -272,7 +379,9 @@ var extractQueryParams = args => {
272
379
  camelName: snakeToCamel(p.name || ""),
273
380
  description: p.description || "",
274
381
  required: p.required || false,
275
- type: p.schema?.type || "string"
382
+ type: p.schema?.type || "string",
383
+ style: p.style,
384
+ explode: p.explode
276
385
  };
277
386
  }));
278
387
  };
@@ -288,23 +397,47 @@ var extractAcceptedBodyFields = args => {
288
397
  const bodySchema = resolveBodySchema(args);
289
398
  return Object.keys(bodySchema?.properties ?? {});
290
399
  };
400
+ var isPlainObject = value => {
401
+ return typeof value === "object" && value !== null && !Array.isArray(value);
402
+ };
403
+ /**
404
+ * Folds a single-entry `allOf` into the property that wraps it. OpenAPI
405
+ * declares `allOf: [{ $ref }]` so a property can carry its own `description`
406
+ * next to a referenced schema; without folding, the referenced `type`,
407
+ * `nullable` and `items` would be lost. Keys on the wrapper win over the
408
+ * referenced schema's. Multi-entry `allOf` is left intact.
409
+ */
410
+ var flattenSingleAllOf = schema => {
411
+ const {
412
+ allOf,
413
+ ...rest
414
+ } = schema;
415
+ if (!Array.isArray(allOf) || allOf.length !== 1) return schema;
416
+ const [entry] = allOf;
417
+ if (!isPlainObject(entry)) return rest;
418
+ return {
419
+ ...flattenSingleAllOf(entry),
420
+ ...rest
421
+ };
422
+ };
291
423
  var extractBodyProps = args => {
292
424
  const bodySchema = resolveBodySchema(args);
293
425
  if (!bodySchema?.properties) return [];
294
426
  return Object.entries(bodySchema.properties).filter(([, value]) => {
295
427
  return !value[args.serverManagedExtension];
296
428
  }).map(([key, value]) => {
297
- const val = value;
429
+ const val = flattenSingleAllOf(value);
298
430
  return {
299
431
  snakeName: key,
300
432
  camelName: snakeToCamel(key),
301
433
  description: typeof val.description === "string" ? val.description : "",
302
434
  required: (bodySchema.required || []).includes(key),
303
- type: typeof val.type === "string" ? val.type : "string",
435
+ type: typeof val.type === "string" ? val.type : void 0,
304
436
  items: val.items,
305
437
  nullable: val.nullable === true,
306
438
  oneOf: Array.isArray(val.oneOf) ? val.oneOf : void 0,
307
- anyOf: Array.isArray(val.anyOf) ? val.anyOf : void 0
439
+ anyOf: Array.isArray(val.anyOf) ? val.anyOf : void 0,
440
+ allOf: Array.isArray(val.allOf) ? val.allOf : void 0
308
441
  };
309
442
  });
310
443
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttoss/http-server-mcp-openapi",
3
- "version": "0.2.13",
3
+ "version": "0.2.15",
4
4
  "description": "Generate Model Context Protocol (MCP) tools from an OpenAPI specification for @ttoss/http-server-mcp",
5
5
  "keywords": [
6
6
  "ai",
@@ -42,8 +42,8 @@
42
42
  "jest": "^30.4.2",
43
43
  "supertest": "^7.2.2",
44
44
  "tsdown": "^0.22.2",
45
- "@ttoss/http-server": "^0.8.1",
46
- "@ttoss/config": "^1.38.0"
45
+ "@ttoss/config": "^1.38.0",
46
+ "@ttoss/http-server": "^0.8.1"
47
47
  },
48
48
  "publishConfig": {
49
49
  "access": "public",