@notionhq/notion-mcp-server 1.9.1 → 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.
@@ -81,8 +81,8 @@ describe('HttpClient File Upload', () => {
81
81
  const mockFormDataHeaders = { 'content-type': 'multipart/form-data; boundary=---123' }
82
82
 
83
83
  vi.mocked(fs.createReadStream).mockReturnValue(mockFileStream as any)
84
- vi.mocked(FormData.prototype.append).mockImplementation(() => {})
85
- vi.mocked(FormData.prototype.getHeaders).mockReturnValue(mockFormDataHeaders)
84
+ vi.spyOn(FormData.prototype, 'append').mockImplementation(() => {})
85
+ vi.spyOn(FormData.prototype, 'getHeaders').mockReturnValue(mockFormDataHeaders)
86
86
 
87
87
  const uploadPath = mockOpenApiSpec.paths['/upload']
88
88
  if (!uploadPath?.post) {
@@ -135,8 +135,8 @@ describe('HttpClient File Upload', () => {
135
135
  vi.mocked(fs.createReadStream)
136
136
  .mockReturnValueOnce(mockFileStream1 as any)
137
137
  .mockReturnValueOnce(mockFileStream2 as any)
138
- vi.mocked(FormData.prototype.append).mockImplementation(() => {})
139
- vi.mocked(FormData.prototype.getHeaders).mockReturnValue(mockFormDataHeaders)
138
+ vi.spyOn(FormData.prototype, 'append').mockImplementation(() => {})
139
+ vi.spyOn(FormData.prototype, 'getHeaders').mockReturnValue(mockFormDataHeaders)
140
140
 
141
141
  const operation: OpenAPIV3.OperationObject = {
142
142
  operationId: 'uploadFile',
@@ -1,10 +1,8 @@
1
- import { describe, it, expect, beforeAll, afterAll } from 'vitest'
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2
2
  import { HttpClient } from '../http-client'
3
- import type express from 'express'
4
- //@ts-ignore
5
- import { createPetstoreServer } from '../../../examples/petstore-server.cjs'
3
+ import express from 'express'
6
4
  import type { OpenAPIV3 } from 'openapi-types'
7
- import axios from 'axios'
5
+ import type { Server } from 'http'
8
6
 
9
7
  interface Pet {
10
8
  id: number
@@ -14,20 +12,132 @@ interface Pet {
14
12
  status: 'available' | 'pending' | 'sold'
15
13
  }
16
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
+
17
85
  describe('HttpClient Integration Tests', () => {
18
- const PORT = 3456
19
- const BASE_URL = `http://localhost:${PORT}`
20
- let server: ReturnType<typeof express>
86
+ let PORT: number
87
+ let BASE_URL: string
88
+ let server: Server
21
89
  let openApiSpec: OpenAPIV3.Document
22
90
  let client: HttpClient
23
91
 
24
- beforeAll(async () => {
25
- // Start the petstore server
26
- server = createPetstoreServer(PORT) as unknown as express.Express
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}`
27
96
 
28
- // Fetch the OpenAPI spec from the server
29
- const response = await axios.get(`${BASE_URL}/openapi.json`)
30
- openApiSpec = response.data
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
+ }
31
141
 
32
142
  // Create HTTP client
33
143
  client = new HttpClient(
@@ -41,8 +151,7 @@ describe('HttpClient Integration Tests', () => {
41
151
  )
42
152
  })
43
153
 
44
- afterAll(() => {
45
- //@ts-expect-error
154
+ afterEach(async () => {
46
155
  server.close()
47
156
  })
48
157
 
@@ -177,7 +177,14 @@ export class HttpClient {
177
177
  }
178
178
  } catch (error: any) {
179
179
  if (error.response) {
180
- console.error('Error in http client', error)
180
+ // Only log errors in non-test environments to keep test output clean
181
+ if (process.env.NODE_ENV !== 'test') {
182
+ console.error('Error in http client', {
183
+ status: error.response.status,
184
+ statusText: error.response.statusText,
185
+ data: error.response.data,
186
+ })
187
+ }
181
188
  const headers = new Headers()
182
189
  Object.entries(error.response.headers).forEach(([key, value]) => {
183
190
  if (value) headers.append(key, value.toString())
@@ -231,7 +231,7 @@ describe('MCPProxy', () => {
231
231
  })
232
232
 
233
233
  it('should return empty object and warn on invalid JSON', () => {
234
- const consoleSpy = vi.spyOn(console, 'warn')
234
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
235
235
  process.env.OPENAPI_MCP_HEADERS = 'invalid json'
236
236
 
237
237
  const proxy = new MCPProxy('test-proxy', mockOpenApiSpec)
@@ -242,10 +242,11 @@ describe('MCPProxy', () => {
242
242
  expect.anything(),
243
243
  )
244
244
  expect(consoleSpy).toHaveBeenCalledWith('Failed to parse OPENAPI_MCP_HEADERS environment variable:', expect.any(Error))
245
+ consoleSpy.mockRestore()
245
246
  })
246
247
 
247
248
  it('should return empty object and warn on non-object JSON', () => {
248
- const consoleSpy = vi.spyOn(console, 'warn')
249
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
249
250
  process.env.OPENAPI_MCP_HEADERS = '"string"'
250
251
 
251
252
  const proxy = new MCPProxy('test-proxy', mockOpenApiSpec)
@@ -256,6 +257,7 @@ describe('MCPProxy', () => {
256
257
  expect.anything(),
257
258
  )
258
259
  expect(consoleSpy).toHaveBeenCalledWith('OPENAPI_MCP_HEADERS environment variable must be a JSON object, got:', 'string')
260
+ consoleSpy.mockRestore()
259
261
  })
260
262
 
261
263
  it('should use NOTION_TOKEN when OPENAPI_MCP_HEADERS is not set', () => {
@@ -267,7 +269,7 @@ describe('MCPProxy', () => {
267
269
  expect.objectContaining({
268
270
  headers: {
269
271
  'Authorization': 'Bearer ntn_test_token_123',
270
- 'Notion-Version': '2022-06-28'
272
+ 'Notion-Version': '2025-09-03'
271
273
  },
272
274
  }),
273
275
  expect.anything(),
@@ -315,7 +317,7 @@ describe('MCPProxy', () => {
315
317
  expect.objectContaining({
316
318
  headers: {
317
319
  'Authorization': 'Bearer ntn_test_token_123',
318
- 'Notion-Version': '2022-06-28'
320
+ 'Notion-Version': '2025-09-03'
319
321
  },
320
322
  }),
321
323
  expect.anything(),
@@ -147,7 +147,7 @@ export class MCPProxy {
147
147
  if (notionToken) {
148
148
  return {
149
149
  'Authorization': `Bearer ${notionToken}`,
150
- 'Notion-Version': '2022-06-28'
150
+ 'Notion-Version': '2025-09-03'
151
151
  }
152
152
  }
153
153
 
@@ -185,7 +185,8 @@ export class OpenAPIToMCPConverter {
185
185
  if (mcpMethod) {
186
186
  const uniqueName = this.ensureUniqueName(mcpMethod.name)
187
187
  mcpMethod.name = uniqueName
188
- mcpMethod.description = this.getDescription(operation.summary || operation.description || '')
188
+ // Apply description prefix to the already-built description (which includes error responses)
189
+ mcpMethod.description = this.getDescription(mcpMethod.description)
189
190
  tools[apiName]!.methods.push(mcpMethod)
190
191
  openApiLookup[apiName + '-' + uniqueName] = { ...operation, method, path }
191
192
  zip[apiName + '-' + uniqueName] = { openApi: { ...operation, method, path }, mcp: mcpMethod }
@@ -519,6 +520,10 @@ export class OpenAPIToMCPConverter {
519
520
  }
520
521
 
521
522
  private getDescription(description: string): string {
522
- return "Notion | " + description
523
+ // Only add "Notion | " prefix for the Notion API
524
+ if (this.openApiSpec.info.title === 'Notion API') {
525
+ return "Notion | " + description
526
+ }
527
+ return description
523
528
  }
524
529
  }