@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,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
|
+
})
|