@mieubrisse/notion-mcp-server 2.0.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.
- package/.devcontainer/devcontainer.json +4 -0
- package/.dockerignore +3 -0
- package/.github/pull_request_template.md +8 -0
- package/.github/workflows/ci.yml +42 -0
- package/Dockerfile +36 -0
- package/LICENSE +7 -0
- package/README.md +412 -0
- package/docker-compose.yml +6 -0
- package/docs/images/connections.png +0 -0
- package/docs/images/integration-access.png +0 -0
- package/docs/images/integrations-capabilities.png +0 -0
- package/docs/images/integrations-creation.png +0 -0
- package/docs/images/page-access-edit.png +0 -0
- package/package.json +63 -0
- package/scripts/build-cli.js +30 -0
- package/scripts/notion-openapi.json +2238 -0
- package/scripts/start-server.ts +243 -0
- package/src/init-server.ts +50 -0
- package/src/openapi-mcp-server/README.md +3 -0
- package/src/openapi-mcp-server/auth/index.ts +2 -0
- package/src/openapi-mcp-server/auth/template.ts +24 -0
- package/src/openapi-mcp-server/auth/types.ts +26 -0
- package/src/openapi-mcp-server/client/__tests__/http-client-upload.test.ts +205 -0
- package/src/openapi-mcp-server/client/__tests__/http-client.integration.test.ts +282 -0
- package/src/openapi-mcp-server/client/__tests__/http-client.test.ts +537 -0
- package/src/openapi-mcp-server/client/http-client.ts +198 -0
- package/src/openapi-mcp-server/client/polyfill-headers.ts +42 -0
- package/src/openapi-mcp-server/index.ts +3 -0
- package/src/openapi-mcp-server/mcp/__tests__/proxy.test.ts +479 -0
- package/src/openapi-mcp-server/mcp/proxy.ts +250 -0
- package/src/openapi-mcp-server/openapi/__tests__/file-upload.test.ts +100 -0
- package/src/openapi-mcp-server/openapi/__tests__/parser-multipart.test.ts +602 -0
- package/src/openapi-mcp-server/openapi/__tests__/parser.test.ts +1448 -0
- package/src/openapi-mcp-server/openapi/file-upload.ts +40 -0
- package/src/openapi-mcp-server/openapi/parser.ts +529 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import { MCPProxy } from '../proxy'
|
|
2
|
+
import { OpenAPIV3 } from 'openapi-types'
|
|
3
|
+
import { HttpClient } from '../../client/http-client'
|
|
4
|
+
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
|
|
5
|
+
import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest'
|
|
6
|
+
|
|
7
|
+
// Mock the dependencies
|
|
8
|
+
vi.mock('../../client/http-client')
|
|
9
|
+
vi.mock('@modelcontextprotocol/sdk/server/index.js')
|
|
10
|
+
|
|
11
|
+
describe('MCPProxy', () => {
|
|
12
|
+
let proxy: MCPProxy
|
|
13
|
+
let mockOpenApiSpec: OpenAPIV3.Document
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
// Reset all mocks
|
|
17
|
+
vi.clearAllMocks()
|
|
18
|
+
|
|
19
|
+
// Setup minimal OpenAPI spec for testing
|
|
20
|
+
mockOpenApiSpec = {
|
|
21
|
+
openapi: '3.0.0',
|
|
22
|
+
servers: [{ url: 'http://localhost:3000' }],
|
|
23
|
+
info: {
|
|
24
|
+
title: 'Test API',
|
|
25
|
+
version: '1.0.0',
|
|
26
|
+
},
|
|
27
|
+
paths: {
|
|
28
|
+
'/test': {
|
|
29
|
+
get: {
|
|
30
|
+
operationId: 'getTest',
|
|
31
|
+
responses: {
|
|
32
|
+
'200': {
|
|
33
|
+
description: 'Success',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
proxy = new MCPProxy('test-proxy', mockOpenApiSpec)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('listTools handler', () => {
|
|
45
|
+
it('should return converted tools from OpenAPI spec', async () => {
|
|
46
|
+
const server = (proxy as any).server
|
|
47
|
+
const listToolsHandler = server.setRequestHandler.mock.calls[0].filter((x: unknown) => typeof x === 'function')[0]
|
|
48
|
+
const result = await listToolsHandler()
|
|
49
|
+
|
|
50
|
+
expect(result).toHaveProperty('tools')
|
|
51
|
+
expect(Array.isArray(result.tools)).toBe(true)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('should truncate tool names exceeding 64 characters', async () => {
|
|
55
|
+
// Setup OpenAPI spec with long tool names
|
|
56
|
+
mockOpenApiSpec.paths = {
|
|
57
|
+
'/test': {
|
|
58
|
+
get: {
|
|
59
|
+
operationId: 'a'.repeat(65),
|
|
60
|
+
responses: {
|
|
61
|
+
'200': {
|
|
62
|
+
description: 'Success'
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
proxy = new MCPProxy('test-proxy', mockOpenApiSpec)
|
|
69
|
+
const server = (proxy as any).server
|
|
70
|
+
const listToolsHandler = server.setRequestHandler.mock.calls[0].filter((x: unknown) => typeof x === 'function')[0];
|
|
71
|
+
const result = await listToolsHandler()
|
|
72
|
+
|
|
73
|
+
expect(result.tools[0].name.length).toBeLessThanOrEqual(64)
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('callTool handler', () => {
|
|
78
|
+
it('should execute operation and return formatted response', async () => {
|
|
79
|
+
// Mock HttpClient response
|
|
80
|
+
const mockResponse = {
|
|
81
|
+
data: { message: 'success' },
|
|
82
|
+
status: 200,
|
|
83
|
+
headers: new Headers({
|
|
84
|
+
'content-type': 'application/json',
|
|
85
|
+
}),
|
|
86
|
+
}
|
|
87
|
+
;(HttpClient.prototype.executeOperation as ReturnType<typeof vi.fn>).mockResolvedValue(mockResponse)
|
|
88
|
+
|
|
89
|
+
// Set up the openApiLookup with our test operation
|
|
90
|
+
;(proxy as any).openApiLookup = {
|
|
91
|
+
'API-getTest': {
|
|
92
|
+
operationId: 'getTest',
|
|
93
|
+
responses: { '200': { description: 'Success' } },
|
|
94
|
+
method: 'get',
|
|
95
|
+
path: '/test',
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const server = (proxy as any).server
|
|
100
|
+
const handlers = server.setRequestHandler.mock.calls.flatMap((x: unknown[]) => x).filter((x: unknown) => typeof x === 'function')
|
|
101
|
+
const callToolHandler = handlers[1]
|
|
102
|
+
|
|
103
|
+
const result = await callToolHandler({
|
|
104
|
+
params: {
|
|
105
|
+
name: 'API-getTest',
|
|
106
|
+
arguments: {},
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
expect(result).toEqual({
|
|
111
|
+
content: [
|
|
112
|
+
{
|
|
113
|
+
type: 'text',
|
|
114
|
+
text: JSON.stringify({ message: 'success' }),
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('should throw error for non-existent operation', async () => {
|
|
121
|
+
const server = (proxy as any).server
|
|
122
|
+
const handlers = server.setRequestHandler.mock.calls.flatMap((x: unknown[]) => x).filter((x: unknown) => typeof x === 'function')
|
|
123
|
+
const callToolHandler = handlers[1]
|
|
124
|
+
|
|
125
|
+
await expect(
|
|
126
|
+
callToolHandler({
|
|
127
|
+
params: {
|
|
128
|
+
name: 'nonExistentMethod',
|
|
129
|
+
arguments: {},
|
|
130
|
+
},
|
|
131
|
+
}),
|
|
132
|
+
).rejects.toThrow('Method nonExistentMethod not found')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('should handle tool names exceeding 64 characters', async () => {
|
|
136
|
+
// Mock HttpClient response
|
|
137
|
+
const mockResponse = {
|
|
138
|
+
data: { message: 'success' },
|
|
139
|
+
status: 200,
|
|
140
|
+
headers: new Headers({
|
|
141
|
+
'content-type': 'application/json'
|
|
142
|
+
})
|
|
143
|
+
};
|
|
144
|
+
(HttpClient.prototype.executeOperation as ReturnType<typeof vi.fn>).mockResolvedValue(mockResponse);
|
|
145
|
+
|
|
146
|
+
// Set up the openApiLookup with a long tool name
|
|
147
|
+
const longToolName = 'a'.repeat(65)
|
|
148
|
+
const truncatedToolName = longToolName.slice(0, 64)
|
|
149
|
+
;(proxy as any).openApiLookup = {
|
|
150
|
+
[truncatedToolName]: {
|
|
151
|
+
operationId: longToolName,
|
|
152
|
+
responses: { '200': { description: 'Success' } },
|
|
153
|
+
method: 'get',
|
|
154
|
+
path: '/test'
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const server = (proxy as any).server;
|
|
159
|
+
const handlers = server.setRequestHandler.mock.calls.flatMap((x: unknown[]) => x).filter((x: unknown) => typeof x === 'function');
|
|
160
|
+
const callToolHandler = handlers[1];
|
|
161
|
+
|
|
162
|
+
const result = await callToolHandler({
|
|
163
|
+
params: {
|
|
164
|
+
name: truncatedToolName,
|
|
165
|
+
arguments: {}
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
expect(result).toEqual({
|
|
170
|
+
content: [
|
|
171
|
+
{
|
|
172
|
+
type: 'text',
|
|
173
|
+
text: JSON.stringify({ message: 'success' })
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
describe('getContentType', () => {
|
|
181
|
+
it('should return correct content type for different headers', () => {
|
|
182
|
+
const getContentType = (proxy as any).getContentType.bind(proxy)
|
|
183
|
+
|
|
184
|
+
expect(getContentType(new Headers({ 'content-type': 'text/plain' }))).toBe('text')
|
|
185
|
+
expect(getContentType(new Headers({ 'content-type': 'application/json' }))).toBe('text')
|
|
186
|
+
expect(getContentType(new Headers({ 'content-type': 'image/jpeg' }))).toBe('image')
|
|
187
|
+
expect(getContentType(new Headers({ 'content-type': 'application/octet-stream' }))).toBe('binary')
|
|
188
|
+
expect(getContentType(new Headers())).toBe('binary')
|
|
189
|
+
})
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
describe('parseHeadersFromEnv', () => {
|
|
193
|
+
const originalEnv = process.env
|
|
194
|
+
|
|
195
|
+
beforeEach(() => {
|
|
196
|
+
process.env = { ...originalEnv }
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
afterEach(() => {
|
|
200
|
+
process.env = originalEnv
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('should parse valid JSON headers from env', () => {
|
|
204
|
+
process.env.OPENAPI_MCP_HEADERS = JSON.stringify({
|
|
205
|
+
Authorization: 'Bearer token123',
|
|
206
|
+
'X-Custom-Header': 'test',
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
const proxy = new MCPProxy('test-proxy', mockOpenApiSpec)
|
|
210
|
+
expect(HttpClient).toHaveBeenCalledWith(
|
|
211
|
+
expect.objectContaining({
|
|
212
|
+
headers: {
|
|
213
|
+
Authorization: 'Bearer token123',
|
|
214
|
+
'X-Custom-Header': 'test',
|
|
215
|
+
},
|
|
216
|
+
}),
|
|
217
|
+
expect.anything(),
|
|
218
|
+
)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('should return empty object when env var is not set', () => {
|
|
222
|
+
delete process.env.OPENAPI_MCP_HEADERS
|
|
223
|
+
|
|
224
|
+
const proxy = new MCPProxy('test-proxy', mockOpenApiSpec)
|
|
225
|
+
expect(HttpClient).toHaveBeenCalledWith(
|
|
226
|
+
expect.objectContaining({
|
|
227
|
+
headers: {},
|
|
228
|
+
}),
|
|
229
|
+
expect.anything(),
|
|
230
|
+
)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('should return empty object and warn on invalid JSON', () => {
|
|
234
|
+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
235
|
+
process.env.OPENAPI_MCP_HEADERS = 'invalid json'
|
|
236
|
+
|
|
237
|
+
const proxy = new MCPProxy('test-proxy', mockOpenApiSpec)
|
|
238
|
+
expect(HttpClient).toHaveBeenCalledWith(
|
|
239
|
+
expect.objectContaining({
|
|
240
|
+
headers: {},
|
|
241
|
+
}),
|
|
242
|
+
expect.anything(),
|
|
243
|
+
)
|
|
244
|
+
expect(consoleSpy).toHaveBeenCalledWith('Failed to parse OPENAPI_MCP_HEADERS environment variable:', expect.any(Error))
|
|
245
|
+
consoleSpy.mockRestore()
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('should return empty object and warn on non-object JSON', () => {
|
|
249
|
+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
250
|
+
process.env.OPENAPI_MCP_HEADERS = '"string"'
|
|
251
|
+
|
|
252
|
+
const proxy = new MCPProxy('test-proxy', mockOpenApiSpec)
|
|
253
|
+
expect(HttpClient).toHaveBeenCalledWith(
|
|
254
|
+
expect.objectContaining({
|
|
255
|
+
headers: {},
|
|
256
|
+
}),
|
|
257
|
+
expect.anything(),
|
|
258
|
+
)
|
|
259
|
+
expect(consoleSpy).toHaveBeenCalledWith('OPENAPI_MCP_HEADERS environment variable must be a JSON object, got:', 'string')
|
|
260
|
+
consoleSpy.mockRestore()
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('should use NOTION_TOKEN when OPENAPI_MCP_HEADERS is not set', () => {
|
|
264
|
+
delete process.env.OPENAPI_MCP_HEADERS
|
|
265
|
+
process.env.NOTION_TOKEN = 'ntn_test_token_123'
|
|
266
|
+
|
|
267
|
+
const proxy = new MCPProxy('test-proxy', mockOpenApiSpec)
|
|
268
|
+
expect(HttpClient).toHaveBeenCalledWith(
|
|
269
|
+
expect.objectContaining({
|
|
270
|
+
headers: {
|
|
271
|
+
'Authorization': 'Bearer ntn_test_token_123',
|
|
272
|
+
'Notion-Version': '2025-09-03'
|
|
273
|
+
},
|
|
274
|
+
}),
|
|
275
|
+
expect.anything(),
|
|
276
|
+
)
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it('should prioritize OPENAPI_MCP_HEADERS over NOTION_TOKEN when both are set', () => {
|
|
280
|
+
process.env.OPENAPI_MCP_HEADERS = JSON.stringify({
|
|
281
|
+
Authorization: 'Bearer custom_token',
|
|
282
|
+
'Custom-Header': 'custom_value',
|
|
283
|
+
})
|
|
284
|
+
process.env.NOTION_TOKEN = 'ntn_test_token_123'
|
|
285
|
+
|
|
286
|
+
const proxy = new MCPProxy('test-proxy', mockOpenApiSpec)
|
|
287
|
+
expect(HttpClient).toHaveBeenCalledWith(
|
|
288
|
+
expect.objectContaining({
|
|
289
|
+
headers: {
|
|
290
|
+
Authorization: 'Bearer custom_token',
|
|
291
|
+
'Custom-Header': 'custom_value',
|
|
292
|
+
},
|
|
293
|
+
}),
|
|
294
|
+
expect.anything(),
|
|
295
|
+
)
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('should return empty object when neither OPENAPI_MCP_HEADERS nor NOTION_TOKEN are set', () => {
|
|
299
|
+
delete process.env.OPENAPI_MCP_HEADERS
|
|
300
|
+
delete process.env.NOTION_TOKEN
|
|
301
|
+
|
|
302
|
+
const proxy = new MCPProxy('test-proxy', mockOpenApiSpec)
|
|
303
|
+
expect(HttpClient).toHaveBeenCalledWith(
|
|
304
|
+
expect.objectContaining({
|
|
305
|
+
headers: {},
|
|
306
|
+
}),
|
|
307
|
+
expect.anything(),
|
|
308
|
+
)
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('should use NOTION_TOKEN when OPENAPI_MCP_HEADERS is empty object', () => {
|
|
312
|
+
process.env.OPENAPI_MCP_HEADERS = '{}'
|
|
313
|
+
process.env.NOTION_TOKEN = 'ntn_test_token_123'
|
|
314
|
+
|
|
315
|
+
const proxy = new MCPProxy('test-proxy', mockOpenApiSpec)
|
|
316
|
+
expect(HttpClient).toHaveBeenCalledWith(
|
|
317
|
+
expect.objectContaining({
|
|
318
|
+
headers: {
|
|
319
|
+
'Authorization': 'Bearer ntn_test_token_123',
|
|
320
|
+
'Notion-Version': '2025-09-03'
|
|
321
|
+
},
|
|
322
|
+
}),
|
|
323
|
+
expect.anything(),
|
|
324
|
+
)
|
|
325
|
+
})
|
|
326
|
+
})
|
|
327
|
+
describe('connect', () => {
|
|
328
|
+
it('should connect to transport', async () => {
|
|
329
|
+
const mockTransport = {} as Transport
|
|
330
|
+
await proxy.connect(mockTransport)
|
|
331
|
+
|
|
332
|
+
const server = (proxy as any).server
|
|
333
|
+
expect(server.connect).toHaveBeenCalledWith(mockTransport)
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
describe('double-serialization fix (issue #176)', () => {
|
|
338
|
+
it('should deserialize stringified JSON object parameters', async () => {
|
|
339
|
+
// Mock HttpClient response
|
|
340
|
+
const mockResponse = {
|
|
341
|
+
data: { message: 'success' },
|
|
342
|
+
status: 200,
|
|
343
|
+
headers: new Headers({
|
|
344
|
+
'content-type': 'application/json',
|
|
345
|
+
}),
|
|
346
|
+
}
|
|
347
|
+
;(HttpClient.prototype.executeOperation as ReturnType<typeof vi.fn>).mockResolvedValue(mockResponse)
|
|
348
|
+
|
|
349
|
+
// Set up the openApiLookup with our test operation
|
|
350
|
+
;(proxy as any).openApiLookup = {
|
|
351
|
+
'API-updatePage': {
|
|
352
|
+
operationId: 'updatePage',
|
|
353
|
+
responses: { '200': { description: 'Success' } },
|
|
354
|
+
method: 'patch',
|
|
355
|
+
path: '/pages/{page_id}',
|
|
356
|
+
},
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const server = (proxy as any).server
|
|
360
|
+
const handlers = server.setRequestHandler.mock.calls.flatMap((x: unknown[]) => x).filter((x: unknown) => typeof x === 'function')
|
|
361
|
+
const callToolHandler = handlers[1]
|
|
362
|
+
|
|
363
|
+
// Simulate double-serialized parameters (the bug from issue #176)
|
|
364
|
+
const stringifiedData = JSON.stringify({
|
|
365
|
+
page_id: 'test-page-id',
|
|
366
|
+
command: 'update_properties',
|
|
367
|
+
properties: { Status: 'Done' },
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
await callToolHandler({
|
|
371
|
+
params: {
|
|
372
|
+
name: 'API-updatePage',
|
|
373
|
+
arguments: {
|
|
374
|
+
data: stringifiedData, // This would normally fail with "Expected object, received string"
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
// Verify that the parameters were deserialized before being passed to executeOperation
|
|
380
|
+
expect(HttpClient.prototype.executeOperation).toHaveBeenCalledWith(
|
|
381
|
+
expect.anything(),
|
|
382
|
+
{
|
|
383
|
+
data: {
|
|
384
|
+
page_id: 'test-page-id',
|
|
385
|
+
command: 'update_properties',
|
|
386
|
+
properties: { Status: 'Done' },
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
)
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
it('should handle nested stringified JSON parameters', async () => {
|
|
393
|
+
const mockResponse = {
|
|
394
|
+
data: { success: true },
|
|
395
|
+
status: 200,
|
|
396
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
397
|
+
}
|
|
398
|
+
;(HttpClient.prototype.executeOperation as ReturnType<typeof vi.fn>).mockResolvedValue(mockResponse)
|
|
399
|
+
|
|
400
|
+
;(proxy as any).openApiLookup = {
|
|
401
|
+
'API-createPage': {
|
|
402
|
+
operationId: 'createPage',
|
|
403
|
+
responses: { '200': { description: 'Success' } },
|
|
404
|
+
method: 'post',
|
|
405
|
+
path: '/pages',
|
|
406
|
+
},
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const server = (proxy as any).server
|
|
410
|
+
const handlers = server.setRequestHandler.mock.calls.flatMap((x: unknown[]) => x).filter((x: unknown) => typeof x === 'function')
|
|
411
|
+
const callToolHandler = handlers[1]
|
|
412
|
+
|
|
413
|
+
// Nested stringified object
|
|
414
|
+
const nestedData = JSON.stringify({
|
|
415
|
+
parent: JSON.stringify({ page_id: 'parent-page-id' }),
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
await callToolHandler({
|
|
419
|
+
params: {
|
|
420
|
+
name: 'API-createPage',
|
|
421
|
+
arguments: {
|
|
422
|
+
data: nestedData,
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
// Verify nested objects were also deserialized
|
|
428
|
+
expect(HttpClient.prototype.executeOperation).toHaveBeenCalledWith(
|
|
429
|
+
expect.anything(),
|
|
430
|
+
{
|
|
431
|
+
data: {
|
|
432
|
+
parent: { page_id: 'parent-page-id' },
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
)
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
it('should preserve non-JSON string parameters', async () => {
|
|
439
|
+
const mockResponse = {
|
|
440
|
+
data: { success: true },
|
|
441
|
+
status: 200,
|
|
442
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
443
|
+
}
|
|
444
|
+
;(HttpClient.prototype.executeOperation as ReturnType<typeof vi.fn>).mockResolvedValue(mockResponse)
|
|
445
|
+
|
|
446
|
+
;(proxy as any).openApiLookup = {
|
|
447
|
+
'API-search': {
|
|
448
|
+
operationId: 'search',
|
|
449
|
+
responses: { '200': { description: 'Success' } },
|
|
450
|
+
method: 'post',
|
|
451
|
+
path: '/search',
|
|
452
|
+
},
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const server = (proxy as any).server
|
|
456
|
+
const handlers = server.setRequestHandler.mock.calls.flatMap((x: unknown[]) => x).filter((x: unknown) => typeof x === 'function')
|
|
457
|
+
const callToolHandler = handlers[1]
|
|
458
|
+
|
|
459
|
+
await callToolHandler({
|
|
460
|
+
params: {
|
|
461
|
+
name: 'API-search',
|
|
462
|
+
arguments: {
|
|
463
|
+
query: 'hello world', // Regular string, should NOT be parsed
|
|
464
|
+
filter: '{ not valid json }', // Looks like JSON but isn't valid
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
// Verify that non-JSON strings are preserved as-is
|
|
470
|
+
expect(HttpClient.prototype.executeOperation).toHaveBeenCalledWith(
|
|
471
|
+
expect.anything(),
|
|
472
|
+
{
|
|
473
|
+
query: 'hello world',
|
|
474
|
+
filter: '{ not valid json }',
|
|
475
|
+
},
|
|
476
|
+
)
|
|
477
|
+
})
|
|
478
|
+
})
|
|
479
|
+
})
|