diff --git a/test/required-unconstrained-body.test.ts b/test/required-unconstrained-body.test.ts new file mode 100644 index 0000000..bff60ef --- /dev/null +++ b/test/required-unconstrained-body.test.ts @@ -0,0 +1,20 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import type { OpenAPIV3 } from 'openapi-types'; +import { init } from '../vendor/forge/index.js'; +import { buildManifest } from '../src/manifest.js'; + +for (const schema of [{ type: 'object' }, {}]) { + test(`required unconstrained JSON body is not discarded: ${JSON.stringify(schema)}`, async () => { + const doc = { openapi: '3.0.3', info: { title: 'Required body', version: '1' }, components: { schemas: {} }, + paths: { '/x': { post: { operationId: 'x', requestBody: { required: true, content: { 'application/json': { schema } } }, + responses: { '204': { description: 'OK' } } } } }, + } as unknown as OpenAPIV3.Document; + await init(doc); + const tool = buildManifest(doc).tools[0]!; + assert.equal(tool.contentType, 'application/json'); + assert.deepEqual(tool.inputSchema.required, ['body']); + assert.equal(tool.args[0]?.required, true); + assert.deepEqual(tool.args[0]?.apiFieldPath, []); + }); +} diff --git a/vendor/forge/openapi-resolver.ts b/vendor/forge/openapi-resolver.ts index daf3af1..b478e37 100644 --- a/vendor/forge/openapi-resolver.ts +++ b/vendor/forge/openapi-resolver.ts @@ -41,6 +41,7 @@ interface PathItem { } interface RequestBody { + required?: boolean; /** Reference to a `#/components/requestBodies/...` entry, if applicable. */ $ref?: string; /** Human-readable description of the request body. */ @@ -647,6 +648,8 @@ export function normalizedRequestBody(value: T): T { } function isSemanticallyEmptyRequestBody(requestBody: RequestBody): boolean { + // A required body still needs a wire representation even without constraints. + if (requestBody.required === true) return false; const content = requestBody.content; if (!content) return false; const mediaEntries = Object.entries(content);