@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,282 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2
+ import { HttpClient } from '../http-client'
3
+ import express from 'express'
4
+ import type { OpenAPIV3 } from 'openapi-types'
5
+ import type { Server } from 'http'
6
+
7
+ interface Pet {
8
+ id: number
9
+ name: string
10
+ species: string
11
+ age: number
12
+ status: 'available' | 'pending' | 'sold'
13
+ }
14
+
15
+ // Simple in-memory pet store
16
+ let pets: Pet[] = []
17
+ let nextId = 1
18
+
19
+ // Initialize/reset pets data
20
+ function resetPets() {
21
+ pets = [
22
+ { id: 1, name: 'Fluffy', species: 'Cat', age: 3, status: 'available' },
23
+ { id: 2, name: 'Max', species: 'Dog', age: 5, status: 'available' },
24
+ { id: 3, name: 'Tweety', species: 'Bird', age: 1, status: 'sold' },
25
+ ]
26
+ nextId = 4
27
+ }
28
+
29
+ function createTestServer(port: number): Server {
30
+ const app = express()
31
+ app.use(express.json())
32
+
33
+ // GET /pets - List all pets
34
+ app.get('/pets', (req, res) => {
35
+ const status = req.query.status
36
+ const filtered = status ? pets.filter((p) => p.status === status) : pets
37
+ res.json(filtered)
38
+ })
39
+
40
+ // POST /pets - Create a pet
41
+ app.post('/pets', (req, res) => {
42
+ const newPet: Pet = {
43
+ id: nextId++,
44
+ name: req.body.name,
45
+ species: req.body.species,
46
+ age: req.body.age,
47
+ status: 'available',
48
+ }
49
+ pets.push(newPet)
50
+ res.status(201).json(newPet)
51
+ })
52
+
53
+ // GET /pets/:id - Get a pet by ID
54
+ app.get('/pets/:id', (req: any, res: any) => {
55
+ const pet = pets.find((p) => p.id === Number(req.params.id))
56
+ if (!pet) {
57
+ return res.status(404).json({ error: 'Pet not found' })
58
+ }
59
+ res.json(pet)
60
+ })
61
+
62
+ // PUT /pets/:id - Update a pet
63
+ app.put('/pets/:id', (req: any, res: any) => {
64
+ const pet = pets.find((p) => p.id === Number(req.params.id))
65
+ if (!pet) {
66
+ return res.status(404).json({ error: 'Pet not found' })
67
+ }
68
+ if (req.body.status) pet.status = req.body.status
69
+ res.json(pet)
70
+ })
71
+
72
+ // DELETE /pets/:id - Delete a pet
73
+ app.delete('/pets/:id', (req: any, res: any) => {
74
+ const index = pets.findIndex((p) => p.id === Number(req.params.id))
75
+ if (index === -1) {
76
+ return res.status(404).json({ error: 'Pet not found' })
77
+ }
78
+ pets.splice(index, 1)
79
+ res.status(204).send()
80
+ })
81
+
82
+ return app.listen(port)
83
+ }
84
+
85
+ describe('HttpClient Integration Tests', () => {
86
+ let PORT: number
87
+ let BASE_URL: string
88
+ let server: Server
89
+ let openApiSpec: OpenAPIV3.Document
90
+ let client: HttpClient
91
+
92
+ beforeEach(async () => {
93
+ // Use a random port to avoid conflicts
94
+ PORT = 3000 + Math.floor(Math.random() * 1000)
95
+ BASE_URL = `http://localhost:${PORT}`
96
+
97
+ // Initialize pets data
98
+ resetPets()
99
+
100
+ // Start the test server
101
+ server = createTestServer(PORT)
102
+
103
+ // Create a minimal OpenAPI spec for the test server
104
+ openApiSpec = {
105
+ openapi: '3.0.0',
106
+ info: { title: 'Pet Store API', version: '1.0.0' },
107
+ servers: [{ url: BASE_URL }],
108
+ paths: {
109
+ '/pets': {
110
+ get: {
111
+ operationId: 'listPets',
112
+ parameters: [{ name: 'status', in: 'query', schema: { type: 'string' } }],
113
+ responses: { '200': { description: 'Success' } },
114
+ },
115
+ post: {
116
+ operationId: 'createPet',
117
+ requestBody: { content: { 'application/json': { schema: { type: 'object' } } } },
118
+ responses: { '201': { description: 'Created' } },
119
+ },
120
+ },
121
+ '/pets/{id}': {
122
+ get: {
123
+ operationId: 'getPet',
124
+ parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'integer' } }],
125
+ responses: { '200': { description: 'Success' }, '404': { description: 'Not found' } },
126
+ },
127
+ put: {
128
+ operationId: 'updatePet',
129
+ parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'integer' } }],
130
+ requestBody: { content: { 'application/json': { schema: { type: 'object' } } } },
131
+ responses: { '200': { description: 'Success' }, '404': { description: 'Not found' } },
132
+ },
133
+ delete: {
134
+ operationId: 'deletePet',
135
+ parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'integer' } }],
136
+ responses: { '204': { description: 'Deleted' }, '404': { description: 'Not found' } },
137
+ },
138
+ },
139
+ },
140
+ }
141
+
142
+ // Create HTTP client
143
+ client = new HttpClient(
144
+ {
145
+ baseUrl: BASE_URL,
146
+ headers: {
147
+ Accept: 'application/json',
148
+ },
149
+ },
150
+ openApiSpec,
151
+ )
152
+ })
153
+
154
+ afterEach(async () => {
155
+ server.close()
156
+ })
157
+
158
+ it('should list all pets', async () => {
159
+ const operation = openApiSpec.paths['/pets']?.get
160
+ if (!operation) throw new Error('Operation not found')
161
+
162
+ const response = await client.executeOperation<Pet[]>(operation as OpenAPIV3.OperationObject & { method: string; path: string })
163
+
164
+ expect(response.status).toBe(200)
165
+ expect(Array.isArray(response.data)).toBe(true)
166
+ expect(response.data.length).toBeGreaterThan(0)
167
+ expect(response.data[0]).toHaveProperty('name')
168
+ expect(response.data[0]).toHaveProperty('species')
169
+ expect(response.data[0]).toHaveProperty('status')
170
+ })
171
+
172
+ it('should filter pets by status', async () => {
173
+ const operation = openApiSpec.paths['/pets']?.get as OpenAPIV3.OperationObject & { method: string; path: string }
174
+ if (!operation) throw new Error('Operation not found')
175
+
176
+ const response = await client.executeOperation<Pet[]>(operation, { status: 'available' })
177
+
178
+ expect(response.status).toBe(200)
179
+ expect(Array.isArray(response.data)).toBe(true)
180
+ response.data.forEach((pet: Pet) => {
181
+ expect(pet.status).toBe('available')
182
+ })
183
+ })
184
+
185
+ it('should get a specific pet by ID', async () => {
186
+ const operation = openApiSpec.paths['/pets/{id}']?.get as OpenAPIV3.OperationObject & { method: string; path: string }
187
+ if (!operation) throw new Error('Operation not found')
188
+
189
+ const response = await client.executeOperation<Pet>(operation, { id: 1 })
190
+
191
+ expect(response.status).toBe(200)
192
+ expect(response.data).toHaveProperty('id', 1)
193
+ expect(response.data).toHaveProperty('name')
194
+ expect(response.data).toHaveProperty('species')
195
+ })
196
+
197
+ it('should create a new pet', async () => {
198
+ const operation = openApiSpec.paths['/pets']?.post as OpenAPIV3.OperationObject & { method: string; path: string }
199
+ if (!operation) throw new Error('Operation not found')
200
+
201
+ const newPet = {
202
+ name: 'TestPet',
203
+ species: 'Dog',
204
+ age: 2,
205
+ }
206
+
207
+ const response = await client.executeOperation<Pet>(operation as OpenAPIV3.OperationObject & { method: string; path: string }, newPet)
208
+
209
+ expect(response.status).toBe(201)
210
+ expect(response.data).toMatchObject({
211
+ ...newPet,
212
+ status: 'available',
213
+ })
214
+ expect(response.data.id).toBeDefined()
215
+ })
216
+
217
+ it("should update a pet's status", async () => {
218
+ const operation = openApiSpec.paths['/pets/{id}']?.put
219
+ if (!operation) throw new Error('Operation not found')
220
+
221
+ const response = await client.executeOperation<Pet>(operation as OpenAPIV3.OperationObject & { method: string; path: string }, {
222
+ id: 1,
223
+ status: 'sold',
224
+ })
225
+
226
+ expect(response.status).toBe(200)
227
+ expect(response.data).toHaveProperty('id', 1)
228
+ expect(response.data).toHaveProperty('status', 'sold')
229
+ })
230
+
231
+ it('should delete a pet', async () => {
232
+ // First create a pet to delete
233
+ const createOperation = openApiSpec.paths['/pets']?.post
234
+ if (!createOperation) throw new Error('Operation not found')
235
+
236
+ const createResponse = await client.executeOperation<Pet>(
237
+ createOperation as OpenAPIV3.OperationObject & { method: string; path: string },
238
+ {
239
+ name: 'ToDelete',
240
+ species: 'Cat',
241
+ age: 3,
242
+ },
243
+ )
244
+ const petId = createResponse.data.id
245
+
246
+ // Then delete it
247
+ const deleteOperation = openApiSpec.paths['/pets/{id}']?.delete
248
+ if (!deleteOperation) throw new Error('Operation not found')
249
+
250
+ const deleteResponse = await client.executeOperation(deleteOperation as OpenAPIV3.OperationObject & { method: string; path: string }, {
251
+ id: petId,
252
+ })
253
+
254
+ expect(deleteResponse.status).toBe(204)
255
+
256
+ // Verify the pet is deleted
257
+ const getOperation = openApiSpec.paths['/pets/{id}']?.get
258
+ if (!getOperation) throw new Error('Operation not found')
259
+
260
+ try {
261
+ await client.executeOperation(getOperation as OpenAPIV3.OperationObject & { method: string; path: string }, { id: petId })
262
+ throw new Error('Should not reach here')
263
+ } catch (error: any) {
264
+ expect(error.message).toContain('404')
265
+ }
266
+ })
267
+
268
+ it('should handle errors appropriately', async () => {
269
+ const operation = openApiSpec.paths['/pets/{id}']?.get as OpenAPIV3.OperationObject & { method: string; path: string }
270
+ if (!operation) throw new Error('Operation not found')
271
+
272
+ try {
273
+ await client.executeOperation(
274
+ operation as OpenAPIV3.OperationObject & { method: string; path: string },
275
+ { id: 99999 }, // Non-existent ID
276
+ )
277
+ throw new Error('Should not reach here')
278
+ } catch (error: any) {
279
+ expect(error.message).toContain('404')
280
+ }
281
+ })
282
+ })