@m2ai-mcp/notion-mcp 0.1.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 (60) hide show
  1. package/.env.example +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +228 -0
  4. package/dist/index.d.ts +10 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +374 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/tools/blocks.d.ts +43 -0
  9. package/dist/tools/blocks.d.ts.map +1 -0
  10. package/dist/tools/blocks.js +124 -0
  11. package/dist/tools/blocks.js.map +1 -0
  12. package/dist/tools/databases.d.ts +71 -0
  13. package/dist/tools/databases.d.ts.map +1 -0
  14. package/dist/tools/databases.js +121 -0
  15. package/dist/tools/databases.js.map +1 -0
  16. package/dist/tools/index.d.ts +9 -0
  17. package/dist/tools/index.d.ts.map +1 -0
  18. package/dist/tools/index.js +9 -0
  19. package/dist/tools/index.js.map +1 -0
  20. package/dist/tools/pages.d.ts +72 -0
  21. package/dist/tools/pages.d.ts.map +1 -0
  22. package/dist/tools/pages.js +153 -0
  23. package/dist/tools/pages.js.map +1 -0
  24. package/dist/tools/search.d.ts +28 -0
  25. package/dist/tools/search.d.ts.map +1 -0
  26. package/dist/tools/search.js +62 -0
  27. package/dist/tools/search.js.map +1 -0
  28. package/dist/tools/users.d.ts +33 -0
  29. package/dist/tools/users.d.ts.map +1 -0
  30. package/dist/tools/users.js +51 -0
  31. package/dist/tools/users.js.map +1 -0
  32. package/dist/utils/markdown-converter.d.ts +31 -0
  33. package/dist/utils/markdown-converter.d.ts.map +1 -0
  34. package/dist/utils/markdown-converter.js +355 -0
  35. package/dist/utils/markdown-converter.js.map +1 -0
  36. package/dist/utils/notion-client.d.ts +32 -0
  37. package/dist/utils/notion-client.d.ts.map +1 -0
  38. package/dist/utils/notion-client.js +111 -0
  39. package/dist/utils/notion-client.js.map +1 -0
  40. package/dist/utils/types.d.ts +212 -0
  41. package/dist/utils/types.d.ts.map +1 -0
  42. package/dist/utils/types.js +18 -0
  43. package/dist/utils/types.js.map +1 -0
  44. package/jest.config.cjs +33 -0
  45. package/package.json +53 -0
  46. package/server.json +92 -0
  47. package/src/index.ts +435 -0
  48. package/src/tools/blocks.ts +184 -0
  49. package/src/tools/databases.ts +216 -0
  50. package/src/tools/index.ts +9 -0
  51. package/src/tools/pages.ts +253 -0
  52. package/src/tools/search.ts +96 -0
  53. package/src/tools/users.ts +93 -0
  54. package/src/utils/markdown-converter.ts +408 -0
  55. package/src/utils/notion-client.ts +159 -0
  56. package/src/utils/types.ts +237 -0
  57. package/tests/markdown-converter.test.ts +252 -0
  58. package/tests/notion-client.test.ts +67 -0
  59. package/tests/tools.test.ts +448 -0
  60. package/tsconfig.json +20 -0
@@ -0,0 +1,448 @@
1
+ /**
2
+ * Tests for MCP Tools
3
+ * Uses mock NotionClient for testing tool logic
4
+ */
5
+
6
+ import { NotionClient, NotionResponse } from '../src/utils/notion-client';
7
+ import { search, SearchParams } from '../src/tools/search';
8
+ import { getPage, createPage, updatePage, getPageContent } from '../src/tools/pages';
9
+ import { appendBlocks, updateBlock, deleteBlock } from '../src/tools/blocks';
10
+ import { getDatabase, queryDatabase, createDatabase } from '../src/tools/databases';
11
+ import { listUsers, getUser } from '../src/tools/users';
12
+
13
+ // Mock NotionClient
14
+ class MockNotionClient {
15
+ private mockResponses: Map<string, NotionResponse<unknown>> = new Map();
16
+
17
+ setMockResponse(endpoint: string, response: NotionResponse<unknown>) {
18
+ this.mockResponses.set(endpoint, response);
19
+ }
20
+
21
+ async get<T>(endpoint: string): Promise<NotionResponse<T>> {
22
+ return (this.mockResponses.get(endpoint) || { success: false, error: 'Not mocked' }) as NotionResponse<T>;
23
+ }
24
+
25
+ async post<T>(endpoint: string, _body?: unknown): Promise<NotionResponse<T>> {
26
+ return (this.mockResponses.get(endpoint) || { success: false, error: 'Not mocked' }) as NotionResponse<T>;
27
+ }
28
+
29
+ async patch<T>(endpoint: string, _body?: unknown): Promise<NotionResponse<T>> {
30
+ return (this.mockResponses.get(endpoint) || { success: false, error: 'Not mocked' }) as NotionResponse<T>;
31
+ }
32
+
33
+ async delete<T>(endpoint: string): Promise<NotionResponse<T>> {
34
+ return (this.mockResponses.get(endpoint) || { success: false, error: 'Not mocked' }) as NotionResponse<T>;
35
+ }
36
+ }
37
+
38
+ describe('Search Tool', () => {
39
+ let mockClient: MockNotionClient;
40
+
41
+ beforeEach(() => {
42
+ mockClient = new MockNotionClient();
43
+ });
44
+
45
+ it('should return search results', async () => {
46
+ mockClient.setMockResponse('/search', {
47
+ success: true,
48
+ data: {
49
+ object: 'list',
50
+ results: [
51
+ {
52
+ object: 'page',
53
+ id: 'page-123',
54
+ url: 'https://notion.so/page-123',
55
+ last_edited_time: '2024-01-01T00:00:00Z',
56
+ properties: {
57
+ title: {
58
+ id: 'title',
59
+ type: 'title',
60
+ title: [{ plain_text: 'Test Page' }]
61
+ }
62
+ }
63
+ }
64
+ ],
65
+ has_more: false
66
+ }
67
+ });
68
+
69
+ const result = await search(mockClient as unknown as NotionClient, { query: 'test' });
70
+
71
+ expect(result.success).toBe(true);
72
+ expect(result.results).toHaveLength(1);
73
+ expect(result.results?.[0].title).toBe('Test Page');
74
+ expect(result.results?.[0].type).toBe('page');
75
+ });
76
+
77
+ it('should suggest alternatives when no results', async () => {
78
+ mockClient.setMockResponse('/search', {
79
+ success: true,
80
+ data: {
81
+ object: 'list',
82
+ results: [],
83
+ has_more: false
84
+ }
85
+ });
86
+
87
+ const result = await search(mockClient as unknown as NotionClient, { query: 'nonexistent' });
88
+
89
+ expect(result.success).toBe(true);
90
+ expect(result.results).toHaveLength(0);
91
+ expect(result.suggestion).toContain('No results found');
92
+ });
93
+
94
+ it('should handle API errors', async () => {
95
+ mockClient.setMockResponse('/search', {
96
+ success: false,
97
+ error: 'API Error'
98
+ });
99
+
100
+ const result = await search(mockClient as unknown as NotionClient, { query: 'test' });
101
+
102
+ expect(result.success).toBe(false);
103
+ expect(result.error).toBe('API Error');
104
+ });
105
+ });
106
+
107
+ describe('Page Tools', () => {
108
+ let mockClient: MockNotionClient;
109
+
110
+ beforeEach(() => {
111
+ mockClient = new MockNotionClient();
112
+ });
113
+
114
+ describe('getPage', () => {
115
+ it('should return page details', async () => {
116
+ mockClient.setMockResponse('/pages/test-page-id', {
117
+ success: true,
118
+ data: {
119
+ object: 'page',
120
+ id: 'test-page-id',
121
+ url: 'https://notion.so/test-page',
122
+ created_time: '2024-01-01T00:00:00Z',
123
+ last_edited_time: '2024-01-02T00:00:00Z',
124
+ archived: false,
125
+ parent: { type: 'workspace', workspace: true },
126
+ properties: {
127
+ title: {
128
+ id: 'title',
129
+ type: 'title',
130
+ title: [{ plain_text: 'My Page' }]
131
+ }
132
+ }
133
+ }
134
+ });
135
+
136
+ const result = await getPage(mockClient as unknown as NotionClient, { page_id: 'test-page-id' });
137
+
138
+ expect(result.success).toBe(true);
139
+ expect(result.page?.title).toBe('My Page');
140
+ expect(result.page?.id).toBe('test-page-id');
141
+ });
142
+ });
143
+
144
+ describe('createPage', () => {
145
+ it('should create a page in database', async () => {
146
+ mockClient.setMockResponse('/pages', {
147
+ success: true,
148
+ data: {
149
+ object: 'page',
150
+ id: 'new-page-id',
151
+ url: 'https://notion.so/new-page',
152
+ properties: {
153
+ Name: {
154
+ id: 'title',
155
+ type: 'title',
156
+ title: [{ plain_text: 'New Page' }]
157
+ }
158
+ }
159
+ }
160
+ });
161
+
162
+ const result = await createPage(mockClient as unknown as NotionClient, {
163
+ parent_id: 'db-123',
164
+ parent_type: 'database_id',
165
+ title: 'New Page'
166
+ });
167
+
168
+ expect(result.success).toBe(true);
169
+ expect(result.page?.id).toBe('new-page-id');
170
+ });
171
+ });
172
+
173
+ describe('updatePage', () => {
174
+ it('should update page properties', async () => {
175
+ mockClient.setMockResponse('/pages/page-123', {
176
+ success: true,
177
+ data: {
178
+ object: 'page',
179
+ id: 'page-123',
180
+ url: 'https://notion.so/page-123',
181
+ last_edited_time: '2024-01-03T00:00:00Z',
182
+ properties: {
183
+ title: {
184
+ id: 'title',
185
+ type: 'title',
186
+ title: [{ plain_text: 'Updated' }]
187
+ }
188
+ }
189
+ }
190
+ });
191
+
192
+ const result = await updatePage(mockClient as unknown as NotionClient, {
193
+ page_id: 'page-123',
194
+ properties: { Status: { select: { name: 'Done' } } }
195
+ });
196
+
197
+ expect(result.success).toBe(true);
198
+ expect(result.page?.title).toBe('Updated');
199
+ });
200
+ });
201
+
202
+ describe('getPageContent', () => {
203
+ it('should return page content as markdown', async () => {
204
+ mockClient.setMockResponse('/blocks/page-123/children?page_size=100', {
205
+ success: true,
206
+ data: {
207
+ object: 'list',
208
+ results: [
209
+ {
210
+ object: 'block',
211
+ type: 'paragraph',
212
+ paragraph: {
213
+ rich_text: [{ type: 'text', text: { content: 'Hello' } }]
214
+ }
215
+ }
216
+ ],
217
+ has_more: false
218
+ }
219
+ });
220
+
221
+ const result = await getPageContent(mockClient as unknown as NotionClient, {
222
+ page_id: 'page-123',
223
+ format: 'markdown'
224
+ });
225
+
226
+ expect(result.success).toBe(true);
227
+ expect(result.content).toContain('Hello');
228
+ expect(result.format).toBe('markdown');
229
+ });
230
+ });
231
+ });
232
+
233
+ describe('Block Tools', () => {
234
+ let mockClient: MockNotionClient;
235
+
236
+ beforeEach(() => {
237
+ mockClient = new MockNotionClient();
238
+ });
239
+
240
+ describe('appendBlocks', () => {
241
+ it('should append blocks to page', async () => {
242
+ mockClient.setMockResponse('/blocks/page-123/children', {
243
+ success: true,
244
+ data: {
245
+ object: 'list',
246
+ results: [
247
+ { id: 'block-1', type: 'paragraph' }
248
+ ]
249
+ }
250
+ });
251
+
252
+ const result = await appendBlocks(mockClient as unknown as NotionClient, {
253
+ parent_id: 'page-123',
254
+ content: 'New content'
255
+ });
256
+
257
+ expect(result.success).toBe(true);
258
+ expect(result.block_count).toBe(1);
259
+ });
260
+
261
+ it('should fail with empty content', async () => {
262
+ const result = await appendBlocks(mockClient as unknown as NotionClient, {
263
+ parent_id: 'page-123',
264
+ content: ''
265
+ });
266
+
267
+ expect(result.success).toBe(false);
268
+ expect(result.error).toContain('No valid content');
269
+ });
270
+ });
271
+
272
+ describe('deleteBlock', () => {
273
+ it('should delete a block', async () => {
274
+ mockClient.setMockResponse('/blocks/block-123', {
275
+ success: true,
276
+ data: { archived: true }
277
+ });
278
+
279
+ const result = await deleteBlock(mockClient as unknown as NotionClient, {
280
+ block_id: 'block-123'
281
+ });
282
+
283
+ expect(result.success).toBe(true);
284
+ expect(result.deleted_id).toBe('block-123');
285
+ });
286
+ });
287
+ });
288
+
289
+ describe('Database Tools', () => {
290
+ let mockClient: MockNotionClient;
291
+
292
+ beforeEach(() => {
293
+ mockClient = new MockNotionClient();
294
+ });
295
+
296
+ describe('getDatabase', () => {
297
+ it('should return database schema', async () => {
298
+ mockClient.setMockResponse('/databases/db-123', {
299
+ success: true,
300
+ data: {
301
+ object: 'database',
302
+ id: 'db-123',
303
+ title: [{ plain_text: 'Tasks' }],
304
+ description: [],
305
+ url: 'https://notion.so/db-123',
306
+ created_time: '2024-01-01T00:00:00Z',
307
+ last_edited_time: '2024-01-01T00:00:00Z',
308
+ properties: {
309
+ Name: { id: 'title', name: 'Name', type: 'title' },
310
+ Status: { id: 'status', name: 'Status', type: 'select' }
311
+ },
312
+ is_inline: false,
313
+ archived: false
314
+ }
315
+ });
316
+
317
+ const result = await getDatabase(mockClient as unknown as NotionClient, {
318
+ database_id: 'db-123'
319
+ });
320
+
321
+ expect(result.success).toBe(true);
322
+ expect(result.database?.title).toBe('Tasks');
323
+ expect(result.database?.properties).toHaveProperty('Name');
324
+ expect(result.database?.properties).toHaveProperty('Status');
325
+ });
326
+ });
327
+
328
+ describe('queryDatabase', () => {
329
+ it('should return database entries', async () => {
330
+ mockClient.setMockResponse('/databases/db-123/query', {
331
+ success: true,
332
+ data: {
333
+ object: 'list',
334
+ results: [
335
+ {
336
+ object: 'page',
337
+ id: 'entry-1',
338
+ url: 'https://notion.so/entry-1',
339
+ created_time: '2024-01-01T00:00:00Z',
340
+ last_edited_time: '2024-01-01T00:00:00Z',
341
+ properties: {
342
+ Name: {
343
+ id: 'title',
344
+ type: 'title',
345
+ title: [{ plain_text: 'Task 1' }]
346
+ }
347
+ }
348
+ }
349
+ ],
350
+ has_more: false
351
+ }
352
+ });
353
+
354
+ const result = await queryDatabase(mockClient as unknown as NotionClient, {
355
+ database_id: 'db-123'
356
+ });
357
+
358
+ expect(result.success).toBe(true);
359
+ expect(result.pages).toHaveLength(1);
360
+ expect(result.pages?.[0].title).toBe('Task 1');
361
+ });
362
+ });
363
+
364
+ describe('createDatabase', () => {
365
+ it('should create a new database', async () => {
366
+ mockClient.setMockResponse('/databases', {
367
+ success: true,
368
+ data: {
369
+ object: 'database',
370
+ id: 'new-db-id',
371
+ title: [{ plain_text: 'New Database' }],
372
+ url: 'https://notion.so/new-db'
373
+ }
374
+ });
375
+
376
+ const result = await createDatabase(mockClient as unknown as NotionClient, {
377
+ parent_page_id: 'page-123',
378
+ title: 'New Database',
379
+ properties: {
380
+ Status: { type: 'select', select: { options: [] } }
381
+ }
382
+ });
383
+
384
+ expect(result.success).toBe(true);
385
+ expect(result.database?.id).toBe('new-db-id');
386
+ });
387
+ });
388
+ });
389
+
390
+ describe('User Tools', () => {
391
+ let mockClient: MockNotionClient;
392
+
393
+ beforeEach(() => {
394
+ mockClient = new MockNotionClient();
395
+ });
396
+
397
+ describe('listUsers', () => {
398
+ it('should return list of users', async () => {
399
+ mockClient.setMockResponse('/users?page_size=100', {
400
+ success: true,
401
+ data: {
402
+ object: 'list',
403
+ results: [
404
+ {
405
+ object: 'user',
406
+ id: 'user-1',
407
+ type: 'person',
408
+ name: 'John Doe',
409
+ avatar_url: 'https://example.com/avatar.jpg',
410
+ person: { email: 'john@example.com' }
411
+ }
412
+ ],
413
+ has_more: false
414
+ }
415
+ });
416
+
417
+ const result = await listUsers(mockClient as unknown as NotionClient);
418
+
419
+ expect(result.success).toBe(true);
420
+ expect(result.users).toHaveLength(1);
421
+ expect(result.users?.[0].name).toBe('John Doe');
422
+ expect(result.users?.[0].email).toBe('john@example.com');
423
+ });
424
+ });
425
+
426
+ describe('getUser', () => {
427
+ it('should return user details', async () => {
428
+ mockClient.setMockResponse('/users/user-123', {
429
+ success: true,
430
+ data: {
431
+ object: 'user',
432
+ id: 'user-123',
433
+ type: 'person',
434
+ name: 'Jane Doe',
435
+ avatar_url: null,
436
+ person: { email: 'jane@example.com' }
437
+ }
438
+ });
439
+
440
+ const result = await getUser(mockClient as unknown as NotionClient, {
441
+ user_id: 'user-123'
442
+ });
443
+
444
+ expect(result.success).toBe(true);
445
+ expect(result.user?.name).toBe('Jane Doe');
446
+ });
447
+ });
448
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022"],
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true,
16
+ "resolveJsonModule": true
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules", "dist", "tests"]
20
+ }