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
141 changes: 141 additions & 0 deletions src/openapi-mcp-server/client/__tests__/date-properties.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { HttpClient } from '../http-client'
import { OpenAPIV3 } from 'openapi-types'
import { describe, expect, it, vi, beforeEach } from 'vitest'

// Mock console.log and console.warn to capture debug output
const mockConsoleLog = vi.fn()
const mockConsoleWarn = vi.fn()

beforeEach(() => {
mockConsoleLog.mockClear()
mockConsoleWarn.mockClear()
vi.spyOn(console, 'log').mockImplementation(mockConsoleLog)
vi.spyOn(console, 'warn').mockImplementation(mockConsoleWarn)
})

describe('HttpClient - Date Properties Validation', () => {
const mockOpenApiSpec: OpenAPIV3.Document = {
openapi: '3.0.0',
info: { title: 'Test API', version: '1.0.0' },
servers: [{ url: 'https://api.example.com' }],
paths: {
'/v1/pages': {
post: {
operationId: 'post-page',
summary: 'Create a page',
requestBody: {
content: {
'application/json': {
schema: {
type: 'object',
required: ['parent', 'properties'],
properties: {
parent: { type: 'object' },
properties: { type: 'object' }
}
}
}
}
},
responses: { '200': { description: 'Success' } }
}
}
}
}

it('should log debug info for date properties in page operations', async () => {
const httpClient = new HttpClient(
{ baseUrl: 'https://api.example.com' },
mockOpenApiSpec
)

const operation = {
operationId: 'post-page',
method: 'post',
path: '/v1/pages',
requestBody: mockOpenApiSpec.paths['/v1/pages']!.post!.requestBody,
responses: {}
}

const params = {
parent: { page_id: 'test-id' },
'Activity': 'Test Activity',
'date:TestDate:start': '2025-10-10',
'date:TestDate:is_datetime': 0
}

// Mock the actual HTTP call to avoid network requests
const mockApi = {
'post-page': vi.fn().mockResolvedValue({
data: { success: true },
status: 200,
headers: {}
})
}

// Override the api promise in httpClient
;(httpClient as any).api = Promise.resolve(mockApi)

try {
await httpClient.executeOperation(operation, params)
} catch (error) {
// We expect this to potentially fail due to mocking, that's OK
}

// Verify that date properties were logged
expect(mockConsoleLog).toHaveBeenCalledWith(
'[post-page] Input date properties:',
expect.arrayContaining(['date:TestDate:start', 'date:TestDate:is_datetime'])
)
})

it('should not log debug info for non-page operations', async () => {
const httpClient = new HttpClient(
{ baseUrl: 'https://api.example.com' },
{
...mockOpenApiSpec,
paths: {
'/v1/users': {
get: {
operationId: 'get-users',
summary: 'Get users',
responses: { '200': { description: 'Success' } }
}
}
}
}
)

const operation = {
operationId: 'get-users',
method: 'get',
path: '/v1/users',
responses: {}
}

const params = {
'date:TestDate:start': '2025-10-10'
}

const mockApi = {
'get-users': vi.fn().mockResolvedValue({
data: { users: [] },
status: 200,
headers: {}
})
}

;(httpClient as any).api = Promise.resolve(mockApi)

try {
await httpClient.executeOperation(operation, params)
} catch (error) {
// We expect this to potentially fail due to mocking, that's OK
}

// Verify that no date property logging occurred for non-page operations
expect(mockConsoleLog).not.toHaveBeenCalledWith(
expect.stringContaining('Input date properties')
)
})
})
26 changes: 26 additions & 0 deletions src/openapi-mcp-server/client/http-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,15 @@ export class HttpClient {
throw new Error('Operation ID is required')
}

// Debug: Log date properties for page operations
const isPageOperation = operationId === 'post-page' || operationId === 'patch-page'
if (isPageOperation) {
const inputDateProps = Object.keys(params).filter(key => key.startsWith('date:'))
if (inputDateProps.length > 0) {
console.log(`[${operationId}] Input date properties:`, inputDateProps)
}
}

// Handle file uploads if present
const formData = await this.prepareFileUpload(operation, params)

Expand Down Expand Up @@ -144,6 +153,23 @@ export class HttpClient {
}
}

// Validation: Ensure date properties are preserved for page operations
if (isPageOperation) {
const inputDateProps = Object.keys(params).filter(key => key.startsWith('date:'))
const finalDateProps = [
...Object.keys(urlParameters).filter(key => key.startsWith('date:')),
...Object.keys(bodyParams).filter(key => key.startsWith('date:'))
]

const missingDateProps = inputDateProps.filter(prop => !finalDateProps.includes(prop))
if (missingDateProps.length > 0) {
console.warn(`[${operationId}] Date properties may be dropped:`, missingDateProps)
console.warn(`[${operationId}] Input params:`, Object.keys(params))
console.warn(`[${operationId}] URL params:`, Object.keys(urlParameters))
console.warn(`[${operationId}] Body params:`, Object.keys(bodyParams))
}
}

const operationFn = (api as any)[operationId]
if (!operationFn) {
throw new Error(`Operation ${operationId} not found`)
Expand Down
193 changes: 193 additions & 0 deletions src/openapi-mcp-server/openapi/__tests__/date-properties.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import { OpenAPIToMCPConverter } from '../parser'
import { OpenAPIV3 } from 'openapi-types'
import { describe, expect, it } from 'vitest'

describe('OpenAPIToMCPConverter - Date Properties Fix', () => {
it('should allow additional properties in properties field for post-page operation', () => {
const mockSpec: OpenAPIV3.Document = {
openapi: '3.0.0',
info: { title: 'Test API', version: '1.0.0' },
paths: {
'/v1/pages': {
post: {
operationId: 'post-page',
summary: 'Create a page',
requestBody: {
content: {
'application/json': {
schema: {
type: 'object',
required: ['parent', 'properties'],
properties: {
parent: {
type: 'object',
properties: {
page_id: { type: 'string', format: 'uuid' }
},
required: ['page_id']
},
properties: {
type: 'object',
properties: {
title: {
type: 'array',
items: {
type: 'object',
properties: {
text: {
type: 'object',
properties: {
content: { type: 'string' }
},
required: ['content']
}
},
required: ['text']
}
}
},
additionalProperties: false, // This is the problem we're fixing
required: ['title']
}
}
}
}
}
},
responses: {}
}
}
}
}

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

const apiTool = tools['API']
expect(apiTool).toBeDefined()

const postPageMethod = apiTool.methods.find(m => m.name === 'post-page')
expect(postPageMethod).toBeDefined()

// Verify that the properties field allows additional properties
const propertiesSchema = postPageMethod!.inputSchema.properties!['properties']
expect(propertiesSchema).toBeDefined()
if (typeof propertiesSchema === 'object' && propertiesSchema !== null) {
expect(propertiesSchema.type).toBe('object')
expect(propertiesSchema.additionalProperties).toBe(true) // This should be true after our fix
}
})

it('should allow additional properties in properties field for patch-page operation', () => {
const mockSpec: OpenAPIV3.Document = {
openapi: '3.0.0',
info: { title: 'Test API', version: '1.0.0' },
paths: {
'/v1/pages/{page_id}': {
patch: {
operationId: 'patch-page',
summary: 'Update page properties',
parameters: [
{
name: 'page_id',
in: 'path',
required: true,
schema: { type: 'string' }
}
],
requestBody: {
content: {
'application/json': {
schema: {
type: 'object',
properties: {
properties: {
type: 'object',
properties: {
title: {
type: 'array',
items: { type: 'string' }
}
},
additionalProperties: false // This should be overridden to true
}
}
}
}
}
},
responses: {}
}
}
}
}

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

const apiTool = tools['API']
expect(apiTool).toBeDefined()

const patchPageMethod = apiTool.methods.find(m => m.name === 'patch-page')
expect(patchPageMethod).toBeDefined()

// Verify that the properties field allows additional properties
const propertiesSchema = patchPageMethod!.inputSchema.properties!['properties']
expect(propertiesSchema).toBeDefined()
if (typeof propertiesSchema === 'object' && propertiesSchema !== null) {
expect(propertiesSchema.type).toBe('object')
expect(propertiesSchema.additionalProperties).toBe(true) // This should be true after our fix
}
})

it('should not affect other operations - additionalProperties should remain as defined', () => {
const mockSpec: OpenAPIV3.Document = {
openapi: '3.0.0',
info: { title: 'Test API', version: '1.0.0' },
paths: {
'/v1/users': {
get: {
operationId: 'get-users',
summary: 'List users',
requestBody: {
content: {
'application/json': {
schema: {
type: 'object',
properties: {
properties: {
type: 'object',
properties: {
name: { type: 'string' }
},
additionalProperties: false // This should remain false for non-page operations
}
}
}
}
}
},
responses: {}
}
}
}
}

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

const apiTool = tools['API']
expect(apiTool).toBeDefined()

const getUsersMethod = apiTool.methods.find(m => m.name === 'get-users')
expect(getUsersMethod).toBeDefined()

// Verify that non-page operations are not affected by our fix
const propertiesSchema = getUsersMethod!.inputSchema.properties!['properties']
expect(propertiesSchema).toBeDefined()
if (typeof propertiesSchema === 'object' && propertiesSchema !== null) {
expect(propertiesSchema.type).toBe('object')
expect(propertiesSchema.additionalProperties).toBe(false) // Should remain false for non-page operations
}
})
})
12 changes: 11 additions & 1 deletion src/openapi-mcp-server/openapi/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,17 @@ export class OpenAPIToMCPConverter {
// Merge body schema into the inputSchema's properties
if (bodySchema.type === 'object' && bodySchema.properties) {
for (const [name, propSchema] of Object.entries(bodySchema.properties)) {
inputSchema.properties![name] = propSchema
// Special handling for "properties" field in page operations to support expanded date properties
if (name === 'properties' && (operation.operationId === 'post-page' || operation.operationId === 'patch-page')) {
const modifiedPropSchema = typeof propSchema === 'object' ? { ...propSchema as IJsonSchema } : propSchema
// Allow additional properties to support expanded date property format (e.g., date:PropertyName:start)
if (typeof modifiedPropSchema === 'object' && modifiedPropSchema.type === 'object') {
modifiedPropSchema.additionalProperties = true
}
inputSchema.properties![name] = modifiedPropSchema
} else {
inputSchema.properties![name] = propSchema
}
}
if (bodySchema.required) {
inputSchema.required!.push(...bodySchema.required!)
Expand Down