Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 75 additions & 50 deletions src/openapi-mcp-server/openapi/__tests__/parser-multipart.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,26 +154,36 @@ describe('OpenAPI Multipart Form Parser', () => {
type: 'integer',
},
documents: {
type: 'array',
items: {
anyOf: [
{ type: 'string', format: 'uri-reference', description: 'absolute paths to local files' },
{ type: 'string' },
{ type: 'object', additionalProperties: true },
],
},
description: expect.stringContaining('max 5 files'),
anyOf: [
{
type: 'array',
items: {
anyOf: [
{ type: 'string', format: 'uri-reference', description: 'absolute paths to local files' },
{ type: 'string' },
{ type: 'object', additionalProperties: true },
],
},
description: expect.stringContaining('max 5 files'),
},
{ type: 'string' },
],
},
tags: {
type: 'array',
items: {
anyOf: [
{ type: 'string' },
{ type: 'string' },
{ type: 'object', additionalProperties: true },
],
},
description: expect.stringContaining('Optional tags'),
anyOf: [
{
type: 'array',
items: {
anyOf: [
{ type: 'string' },
{ type: 'string' },
{ type: 'object', additionalProperties: true },
],
},
description: expect.stringContaining('Optional tags'),
},
{ type: 'string' },
],
},
})

Expand Down Expand Up @@ -274,15 +284,20 @@ describe('OpenAPI Multipart Form Parser', () => {
description: expect.stringContaining('Profile picture (absolute paths to local files)'),
},
gallery: {
type: 'array',
items: {
anyOf: [
{ type: 'string', format: 'uri-reference', description: 'absolute paths to local files' },
{ type: 'string' },
{ type: 'object', additionalProperties: true },
],
},
description: expect.stringContaining('Additional pet photos'),
anyOf: [
{
type: 'array',
items: {
anyOf: [
{ type: 'string', format: 'uri-reference', description: 'absolute paths to local files' },
{ type: 'string' },
{ type: 'object', additionalProperties: true },
],
},
description: expect.stringContaining('Additional pet photos'),
},
{ type: 'string' },
],
},
details: {
anyOf: [
Expand All @@ -299,21 +314,26 @@ describe('OpenAPI Multipart Form Parser', () => {
],
},
preferences: {
type: 'array',
items: {
anyOf: [
{
type: 'object',
properties: {
category: { type: 'string' },
value: { type: 'string' },
},
additionalProperties: true,
anyOf: [
{
type: 'array',
items: {
anyOf: [
{
type: 'object',
properties: {
category: { type: 'string' },
value: { type: 'string' },
},
additionalProperties: true,
},
{ type: 'string' },
{ type: 'object', additionalProperties: true },
],
},
{ type: 'string' },
{ type: 'object', additionalProperties: true },
],
},
},
{ type: 'string' },
],
},
})

Expand Down Expand Up @@ -420,15 +440,20 @@ describe('OpenAPI Multipart Form Parser', () => {
description: expect.stringContaining('Optional pet certificate (absolute paths to local files)'),
},
vaccinations: {
type: 'array',
items: {
anyOf: [
{ type: 'string', format: 'uri-reference', description: 'absolute paths to local files' },
{ type: 'string' },
{ type: 'object', additionalProperties: true },
],
},
description: expect.stringContaining('Optional vaccination records'),
anyOf: [
{
type: 'array',
items: {
anyOf: [
{ type: 'string', format: 'uri-reference', description: 'absolute paths to local files' },
{ type: 'string' },
{ type: 'object', additionalProperties: true },
],
},
description: expect.stringContaining('Optional vaccination records'),
},
{ type: 'string' },
],
},
})
})
Expand Down
73 changes: 73 additions & 0 deletions src/openapi-mcp-server/openapi/__tests__/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,79 @@ describe('OpenAPIToMCPConverter', () => {
const workspaceOption = parentRequest.oneOf[2]
expect(workspaceOption.properties.type.const).toBe('workspace')
})

it('wraps top-level array request body parameters in anyOf to also accept JSON-encoded strings', () => {
// Reproduces the bug where MCP clients like Claude Code double-serialize array parameters,
// sending them as JSON strings (e.g. "[{...}]") instead of actual arrays.
// The schema must accept both an array and a string so validation passes.
// At runtime, deserializeParams() in proxy.ts converts the string back to an array.
// See: https://github.com/makenotion/notion-mcp-server/issues/176
const spec: OpenAPIV3.Document = {
openapi: '3.0.0',
info: { title: 'Test API', version: '1.0.0' },
paths: {
'/pages': {
post: {
operationId: 'createPages',
summary: 'Create multiple pages',
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['pages'],
properties: {
pages: {
type: 'array',
description: 'List of pages to create',
items: {
type: 'object',
properties: {
title: { type: 'string' },
},
},
},
},
},
},
},
},
responses: {
'200': {
description: 'Pages created',
content: {
'application/json': {
schema: { type: 'object' },
},
},
},
},
},
},
},
}

const converter = new OpenAPIToMCPConverter(spec)
const { tools } = converter.convertToMCPTools()

const createPagesMethod = tools.API.methods.find((m) => m.name === 'createPages')
expect(createPagesMethod).toBeDefined()

const pagesSchema = (createPagesMethod!.inputSchema.properties as any).pages
expect(pagesSchema).toBeDefined()

// The pages property must accept both an array and a JSON-encoded string
expect(pagesSchema).toHaveProperty('anyOf')
const types = pagesSchema.anyOf.map((s: any) => s.type)
expect(types).toContain('array')
expect(types).toContain('string')

// The array variant must also accept string items (for double-serialized elements)
const arrayVariant = pagesSchema.anyOf.find((s: any) => s.type === 'array')
expect(arrayVariant).toBeDefined()
expect(arrayVariant.items).toHaveProperty('anyOf')
})
})

// Additional complex test scenarios as a table test
Expand Down
25 changes: 17 additions & 8 deletions src/openapi-mcp-server/openapi/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -500,15 +500,24 @@ export class OpenAPIToMCPConverter {
}

if (schema.type === 'array' && schema.items) {
// Also accept the entire array as a JSON-encoded string, consistent with
// the object handling above. Some MCP clients (e.g. Claude Code) double-serialize
// top-level array parameters, sending them as JSON strings instead of arrays.
// deserializeParams() in proxy.ts handles the string→array conversion at runtime.
return {
...schema,
items: {
anyOf: [
schema.items as IJsonSchema,
{ type: 'string' },
{ type: 'object', additionalProperties: true },
],
},
anyOf: [
{
...schema,
items: {
anyOf: [
schema.items as IJsonSchema,
{ type: 'string' },
{ type: 'object', additionalProperties: true },
],
},
},
{ type: 'string' },
],
}
}

Expand Down