@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.
Files changed (36) hide show
  1. package/.devcontainer/devcontainer.json +4 -0
  2. package/.dockerignore +3 -0
  3. package/.github/pull_request_template.md +8 -0
  4. package/.github/workflows/ci.yml +42 -0
  5. package/Dockerfile +36 -0
  6. package/LICENSE +7 -0
  7. package/README.md +412 -0
  8. package/docker-compose.yml +6 -0
  9. package/docs/images/connections.png +0 -0
  10. package/docs/images/integration-access.png +0 -0
  11. package/docs/images/integrations-capabilities.png +0 -0
  12. package/docs/images/integrations-creation.png +0 -0
  13. package/docs/images/page-access-edit.png +0 -0
  14. package/package.json +63 -0
  15. package/scripts/build-cli.js +30 -0
  16. package/scripts/notion-openapi.json +2238 -0
  17. package/scripts/start-server.ts +243 -0
  18. package/src/init-server.ts +50 -0
  19. package/src/openapi-mcp-server/README.md +3 -0
  20. package/src/openapi-mcp-server/auth/index.ts +2 -0
  21. package/src/openapi-mcp-server/auth/template.ts +24 -0
  22. package/src/openapi-mcp-server/auth/types.ts +26 -0
  23. package/src/openapi-mcp-server/client/__tests__/http-client-upload.test.ts +205 -0
  24. package/src/openapi-mcp-server/client/__tests__/http-client.integration.test.ts +282 -0
  25. package/src/openapi-mcp-server/client/__tests__/http-client.test.ts +537 -0
  26. package/src/openapi-mcp-server/client/http-client.ts +198 -0
  27. package/src/openapi-mcp-server/client/polyfill-headers.ts +42 -0
  28. package/src/openapi-mcp-server/index.ts +3 -0
  29. package/src/openapi-mcp-server/mcp/__tests__/proxy.test.ts +479 -0
  30. package/src/openapi-mcp-server/mcp/proxy.ts +250 -0
  31. package/src/openapi-mcp-server/openapi/__tests__/file-upload.test.ts +100 -0
  32. package/src/openapi-mcp-server/openapi/__tests__/parser-multipart.test.ts +602 -0
  33. package/src/openapi-mcp-server/openapi/__tests__/parser.test.ts +1448 -0
  34. package/src/openapi-mcp-server/openapi/file-upload.ts +40 -0
  35. package/src/openapi-mcp-server/openapi/parser.ts +529 -0
  36. package/tsconfig.json +26 -0
@@ -0,0 +1,537 @@
1
+ import { HttpClient, HttpClientError } from '../http-client'
2
+ import { OpenAPIV3 } from 'openapi-types'
3
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
4
+ import OpenAPIClientAxios from 'openapi-client-axios'
5
+
6
+ // Mock the OpenAPIClientAxios initialization
7
+ vi.mock('openapi-client-axios', () => {
8
+ const mockApi = {
9
+ getPet: vi.fn(),
10
+ testOperation: vi.fn(),
11
+ complexOperation: vi.fn(),
12
+ }
13
+ return {
14
+ default: vi.fn().mockImplementation(() => ({
15
+ init: vi.fn().mockResolvedValue(mockApi),
16
+ })),
17
+ }
18
+ })
19
+
20
+ describe('HttpClient', () => {
21
+ let client: HttpClient
22
+ let mockApi: any
23
+
24
+ const sampleSpec: OpenAPIV3.Document = {
25
+ openapi: '3.0.0',
26
+ info: { title: 'Test API', version: '1.0.0' },
27
+ paths: {
28
+ '/pets/{petId}': {
29
+ get: {
30
+ operationId: 'getPet',
31
+ parameters: [
32
+ {
33
+ name: 'petId',
34
+ in: 'path',
35
+ required: true,
36
+ schema: { type: 'integer' },
37
+ },
38
+ ],
39
+ responses: {
40
+ '200': {
41
+ description: 'OK',
42
+ content: {
43
+ 'application/json': {
44
+ schema: { type: 'object' },
45
+ },
46
+ },
47
+ },
48
+ },
49
+ },
50
+ },
51
+ },
52
+ }
53
+
54
+ const getPetOperation = sampleSpec.paths['/pets/{petId}']?.get as OpenAPIV3.OperationObject & { method: string; path: string }
55
+ if (!getPetOperation) {
56
+ throw new Error('Test setup error: getPet operation not found in sample spec')
57
+ }
58
+
59
+ beforeEach(async () => {
60
+ // Create a new instance of HttpClient
61
+ client = new HttpClient({ baseUrl: 'https://api.example.com' }, sampleSpec)
62
+ // Await the initialization to ensure mockApi is set correctly
63
+ mockApi = await client['api']
64
+ })
65
+
66
+ afterEach(() => {
67
+ vi.clearAllMocks()
68
+ })
69
+
70
+ it('successfully executes an operation', async () => {
71
+ const mockResponse = {
72
+ data: { id: 1, name: 'Fluffy' },
73
+ status: 200,
74
+ headers: {
75
+ 'content-type': 'application/json',
76
+ },
77
+ }
78
+
79
+ mockApi.getPet.mockResolvedValueOnce(mockResponse)
80
+
81
+ const response = await client.executeOperation(getPetOperation, { petId: 1 })
82
+
83
+ // Note GET requests should have a null Content-Type header!
84
+ expect(mockApi.getPet).toHaveBeenCalledWith({ petId: 1 }, undefined, { headers: { 'Content-Type': null } })
85
+ expect(response.data).toEqual(mockResponse.data)
86
+ expect(response.status).toBe(200)
87
+ expect(response.headers).toBeInstanceOf(Headers)
88
+ expect(response.headers.get('content-type')).toBe('application/json')
89
+ })
90
+
91
+ it('throws error when operation ID is missing', async () => {
92
+ const operationWithoutId: OpenAPIV3.OperationObject & { method: string; path: string } = {
93
+ method: 'GET',
94
+ path: '/unknown',
95
+ responses: {
96
+ '200': {
97
+ description: 'OK',
98
+ },
99
+ },
100
+ }
101
+
102
+ await expect(client.executeOperation(operationWithoutId)).rejects.toThrow('Operation ID is required')
103
+ })
104
+
105
+ it('throws error when operation is not found', async () => {
106
+ const operation: OpenAPIV3.OperationObject & { method: string; path: string } = {
107
+ method: 'GET',
108
+ path: '/unknown',
109
+ operationId: 'nonexistentOperation',
110
+ responses: {
111
+ '200': {
112
+ description: 'OK',
113
+ },
114
+ },
115
+ }
116
+
117
+ await expect(client.executeOperation(operation)).rejects.toThrow('Operation nonexistentOperation not found')
118
+ })
119
+
120
+ it('handles API errors correctly', async () => {
121
+ const error = {
122
+ response: {
123
+ status: 404,
124
+ statusText: 'Not Found',
125
+ data: {
126
+ code: 'RESOURCE_NOT_FOUND',
127
+ message: 'Pet not found',
128
+ petId: 999,
129
+ },
130
+ headers: {
131
+ 'content-type': 'application/json',
132
+ },
133
+ },
134
+ }
135
+ mockApi.getPet.mockRejectedValueOnce(error)
136
+
137
+ await expect(client.executeOperation(getPetOperation, { petId: 999 })).rejects.toMatchObject({
138
+ status: 404,
139
+ message: '404 Not Found',
140
+ data: {
141
+ code: 'RESOURCE_NOT_FOUND',
142
+ message: 'Pet not found',
143
+ petId: 999,
144
+ },
145
+ })
146
+ })
147
+
148
+ it('handles validation errors (400) correctly', async () => {
149
+ const error = {
150
+ response: {
151
+ status: 400,
152
+ statusText: 'Bad Request',
153
+ data: {
154
+ code: 'VALIDATION_ERROR',
155
+ message: 'Invalid input data',
156
+ errors: [
157
+ {
158
+ field: 'age',
159
+ message: 'Age must be a positive number',
160
+ },
161
+ {
162
+ field: 'name',
163
+ message: 'Name is required',
164
+ },
165
+ ],
166
+ },
167
+ headers: {
168
+ 'content-type': 'application/json',
169
+ },
170
+ },
171
+ }
172
+ mockApi.getPet.mockRejectedValueOnce(error)
173
+
174
+ await expect(client.executeOperation(getPetOperation, { petId: 1 })).rejects.toMatchObject({
175
+ status: 400,
176
+ message: '400 Bad Request',
177
+ data: {
178
+ code: 'VALIDATION_ERROR',
179
+ message: 'Invalid input data',
180
+ errors: [
181
+ {
182
+ field: 'age',
183
+ message: 'Age must be a positive number',
184
+ },
185
+ {
186
+ field: 'name',
187
+ message: 'Name is required',
188
+ },
189
+ ],
190
+ },
191
+ })
192
+ })
193
+
194
+ it('handles server errors (500) with HTML response', async () => {
195
+ const error = {
196
+ response: {
197
+ status: 500,
198
+ statusText: 'Internal Server Error',
199
+ data: '<html><body><h1>500 Internal Server Error</h1></body></html>',
200
+ headers: {
201
+ 'content-type': 'text/html',
202
+ },
203
+ },
204
+ }
205
+ mockApi.getPet.mockRejectedValueOnce(error)
206
+
207
+ await expect(client.executeOperation(getPetOperation, { petId: 1 })).rejects.toMatchObject({
208
+ status: 500,
209
+ message: '500 Internal Server Error',
210
+ data: '<html><body><h1>500 Internal Server Error</h1></body></html>',
211
+ })
212
+ })
213
+
214
+ it('handles rate limit errors (429)', async () => {
215
+ const error = {
216
+ response: {
217
+ status: 429,
218
+ statusText: 'Too Many Requests',
219
+ data: {
220
+ code: 'RATE_LIMIT_EXCEEDED',
221
+ message: 'Rate limit exceeded',
222
+ retryAfter: 60,
223
+ },
224
+ headers: {
225
+ 'content-type': 'application/json',
226
+ 'retry-after': '60',
227
+ },
228
+ },
229
+ }
230
+ mockApi.getPet.mockRejectedValueOnce(error)
231
+
232
+ await expect(client.executeOperation(getPetOperation, { petId: 1 })).rejects.toMatchObject({
233
+ status: 429,
234
+ message: '429 Too Many Requests',
235
+ data: {
236
+ code: 'RATE_LIMIT_EXCEEDED',
237
+ message: 'Rate limit exceeded',
238
+ retryAfter: 60,
239
+ },
240
+ })
241
+ })
242
+
243
+ it('should send body parameters in request body for POST operations', async () => {
244
+ // Setup mock API with the new operation
245
+ mockApi.testOperation = vi.fn().mockResolvedValue({
246
+ data: {},
247
+ status: 200,
248
+ headers: {},
249
+ })
250
+
251
+ const testSpec: OpenAPIV3.Document = {
252
+ openapi: '3.0.0',
253
+ info: { title: 'Test API', version: '1.0.0' },
254
+ paths: {
255
+ '/test': {
256
+ post: {
257
+ operationId: 'testOperation',
258
+ requestBody: {
259
+ content: {
260
+ 'application/json': {
261
+ schema: {
262
+ type: 'object',
263
+ properties: {
264
+ foo: { type: 'string' },
265
+ },
266
+ },
267
+ },
268
+ },
269
+ },
270
+ responses: {
271
+ '200': {
272
+ description: 'Success response',
273
+ content: {
274
+ 'application/json': {
275
+ schema: {
276
+ type: 'object',
277
+ },
278
+ },
279
+ },
280
+ },
281
+ },
282
+ },
283
+ },
284
+ },
285
+ }
286
+
287
+ const postOperation = testSpec.paths['/test']?.post as OpenAPIV3.OperationObject & { method: string; path: string }
288
+ if (!postOperation) {
289
+ throw new Error('Test setup error: post operation not found')
290
+ }
291
+
292
+ const client = new HttpClient({ baseUrl: 'http://test.com' }, testSpec)
293
+
294
+ await client.executeOperation(postOperation, { foo: 'bar' })
295
+
296
+ expect(mockApi.testOperation).toHaveBeenCalledWith({}, { foo: 'bar' }, { headers: { 'Content-Type': 'application/json' } })
297
+ })
298
+
299
+ it('should handle query, path, and body parameters correctly', async () => {
300
+ mockApi.complexOperation = vi.fn().mockResolvedValue({
301
+ data: { success: true },
302
+ status: 200,
303
+ headers: {
304
+ 'content-type': 'application/json',
305
+ },
306
+ })
307
+
308
+ const complexSpec: OpenAPIV3.Document = {
309
+ openapi: '3.0.0',
310
+ info: { title: 'Test API', version: '1.0.0' },
311
+ paths: {
312
+ '/users/{userId}/posts': {
313
+ post: {
314
+ operationId: 'complexOperation',
315
+ parameters: [
316
+ {
317
+ name: 'userId',
318
+ in: 'path',
319
+ required: true,
320
+ schema: { type: 'integer' },
321
+ },
322
+ {
323
+ name: 'include',
324
+ in: 'query',
325
+ required: false,
326
+ schema: { type: 'string' },
327
+ },
328
+ ],
329
+ requestBody: {
330
+ content: {
331
+ 'application/json': {
332
+ schema: {
333
+ type: 'object',
334
+ properties: {
335
+ title: { type: 'string' },
336
+ content: { type: 'string' },
337
+ },
338
+ },
339
+ },
340
+ },
341
+ },
342
+ responses: {
343
+ '200': {
344
+ description: 'Success response',
345
+ content: {
346
+ 'application/json': {
347
+ schema: {
348
+ type: 'object',
349
+ properties: {
350
+ success: { type: 'boolean' },
351
+ },
352
+ },
353
+ },
354
+ },
355
+ },
356
+ },
357
+ },
358
+ },
359
+ },
360
+ }
361
+
362
+ const complexOperation = complexSpec.paths['/users/{userId}/posts']?.post as OpenAPIV3.OperationObject & {
363
+ method: string
364
+ path: string
365
+ }
366
+ if (!complexOperation) {
367
+ throw new Error('Test setup error: complex operation not found')
368
+ }
369
+
370
+ const client = new HttpClient({ baseUrl: 'http://test.com' }, complexSpec)
371
+
372
+ await client.executeOperation(complexOperation, {
373
+ // Path parameter
374
+ userId: 123,
375
+ // Query parameter
376
+ include: 'comments',
377
+ // Body parameters
378
+ title: 'Test Post',
379
+ content: 'Test Content',
380
+ })
381
+
382
+ expect(mockApi.complexOperation).toHaveBeenCalledWith(
383
+ {
384
+ userId: 123,
385
+ include: 'comments',
386
+ },
387
+ {
388
+ title: 'Test Post',
389
+ content: 'Test Content',
390
+ },
391
+ { headers: { 'Content-Type': 'application/json' } },
392
+ )
393
+ })
394
+
395
+ const mockOpenApiSpec: OpenAPIV3.Document = {
396
+ openapi: '3.0.0',
397
+ info: { title: 'Test API', version: '1.0.0' },
398
+ paths: {
399
+ '/test': {
400
+ post: {
401
+ operationId: 'testOperation',
402
+ parameters: [
403
+ {
404
+ name: 'queryParam',
405
+ in: 'query',
406
+ schema: { type: 'string' },
407
+ },
408
+ {
409
+ name: 'pathParam',
410
+ in: 'path',
411
+ schema: { type: 'string' },
412
+ },
413
+ ],
414
+ requestBody: {
415
+ content: {
416
+ 'application/json': {
417
+ schema: {
418
+ type: 'object',
419
+ properties: {
420
+ bodyParam: { type: 'string' },
421
+ },
422
+ },
423
+ },
424
+ },
425
+ },
426
+ responses: {
427
+ '200': {
428
+ description: 'Success',
429
+ },
430
+ '400': {
431
+ description: 'Bad Request',
432
+ },
433
+ },
434
+ },
435
+ },
436
+ },
437
+ }
438
+
439
+ const mockConfig = {
440
+ baseUrl: 'http://test-api.com',
441
+ }
442
+
443
+ beforeEach(() => {
444
+ vi.clearAllMocks()
445
+ })
446
+
447
+ it('should properly propagate structured error responses', async () => {
448
+ const errorResponse = {
449
+ response: {
450
+ data: {
451
+ code: 'VALIDATION_ERROR',
452
+ message: 'Invalid input',
453
+ details: ['Field x is required'],
454
+ },
455
+ status: 400,
456
+ statusText: 'Bad Request',
457
+ headers: {
458
+ 'content-type': 'application/json',
459
+ },
460
+ },
461
+ }
462
+
463
+ // Mock axios instance
464
+ const mockAxiosInstance = {
465
+ testOperation: vi.fn().mockRejectedValue(errorResponse),
466
+ }
467
+
468
+ // Mock the OpenAPIClientAxios initialization
469
+ const MockOpenAPIClientAxios = vi.fn().mockImplementation(() => ({
470
+ init: () => Promise.resolve(mockAxiosInstance),
471
+ }))
472
+
473
+ vi.mocked(OpenAPIClientAxios).mockImplementation(() => MockOpenAPIClientAxios())
474
+
475
+ const client = new HttpClient(mockConfig, mockOpenApiSpec)
476
+ const operation = mockOpenApiSpec.paths['/test']?.post
477
+ if (!operation) {
478
+ throw new Error('Operation not found in mock spec')
479
+ }
480
+
481
+ try {
482
+ await client.executeOperation(operation as OpenAPIV3.OperationObject & { method: string; path: string }, {})
483
+ // Should not reach here
484
+ expect(true).toBe(false)
485
+ } catch (error: any) {
486
+ expect(error.status).toBe(400)
487
+ expect(error.data).toEqual({
488
+ code: 'VALIDATION_ERROR',
489
+ message: 'Invalid input',
490
+ details: ['Field x is required'],
491
+ })
492
+ expect(error.message).toBe('400 Bad Request')
493
+ }
494
+ })
495
+
496
+ it('should handle query, path, and body parameters correctly', async () => {
497
+ const mockAxiosInstance = {
498
+ testOperation: vi.fn().mockResolvedValue({
499
+ data: { success: true },
500
+ status: 200,
501
+ headers: { 'content-type': 'application/json' },
502
+ }),
503
+ }
504
+
505
+ const MockOpenAPIClientAxios = vi.fn().mockImplementation(() => ({
506
+ init: () => Promise.resolve(mockAxiosInstance),
507
+ }))
508
+
509
+ vi.mocked(OpenAPIClientAxios).mockImplementation(() => MockOpenAPIClientAxios())
510
+
511
+ const client = new HttpClient(mockConfig, mockOpenApiSpec)
512
+ const operation = mockOpenApiSpec.paths['/test']?.post
513
+ if (!operation) {
514
+ throw new Error('Operation not found in mock spec')
515
+ }
516
+
517
+ const response = await client.executeOperation(operation as OpenAPIV3.OperationObject & { method: string; path: string }, {
518
+ queryParam: 'query1',
519
+ pathParam: 'path1',
520
+ bodyParam: 'body1',
521
+ })
522
+
523
+ expect(mockAxiosInstance.testOperation).toHaveBeenCalledWith(
524
+ {
525
+ queryParam: 'query1',
526
+ pathParam: 'path1',
527
+ },
528
+ {
529
+ bodyParam: 'body1',
530
+ },
531
+ { headers: { 'Content-Type': 'application/json' } },
532
+ )
533
+
534
+ // Additional check to ensure headers are correctly processed
535
+ expect(response.headers.get('content-type')).toBe('application/json')
536
+ })
537
+ })