@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
package/src/index.ts ADDED
@@ -0,0 +1,435 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Notion MCP Server
5
+ *
6
+ * Provides AI assistants with full access to Notion workspaces.
7
+ * Enables Claude to search, read, create, and update pages,
8
+ * databases, and blocks.
9
+ */
10
+
11
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
12
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
13
+ import {
14
+ CallToolRequestSchema,
15
+ ListToolsRequestSchema,
16
+ Tool
17
+ } from '@modelcontextprotocol/sdk/types.js';
18
+
19
+ import { NotionClient } from './utils/notion-client.js';
20
+ import {
21
+ search,
22
+ SearchParams,
23
+ getPage,
24
+ GetPageParams,
25
+ createPage,
26
+ CreatePageParams,
27
+ updatePage,
28
+ UpdatePageParams,
29
+ getPageContent,
30
+ GetPageContentParams,
31
+ appendBlocks,
32
+ AppendBlocksParams,
33
+ updateBlock,
34
+ UpdateBlockParams,
35
+ deleteBlock,
36
+ DeleteBlockParams,
37
+ getDatabase,
38
+ GetDatabaseParams,
39
+ queryDatabase,
40
+ QueryDatabaseParams,
41
+ createDatabase,
42
+ CreateDatabaseParams,
43
+ listUsers,
44
+ ListUsersParams,
45
+ getUser,
46
+ GetUserParams
47
+ } from './tools/index.js';
48
+
49
+ // Get API key from environment
50
+ const NOTION_API_KEY = process.env.NOTION_API_KEY;
51
+
52
+ if (!NOTION_API_KEY) {
53
+ console.error('Error: NOTION_API_KEY environment variable is required');
54
+ console.error('Get your API key from https://www.notion.so/my-integrations');
55
+ process.exit(1);
56
+ }
57
+
58
+ // Initialize Notion client
59
+ const notionClient = new NotionClient({ apiKey: NOTION_API_KEY });
60
+
61
+ // Tool definitions
62
+ const TOOLS: Tool[] = [
63
+ {
64
+ name: 'search',
65
+ description: 'Search across all pages and databases in the workspace. Use when user wants to find content by keyword or title.',
66
+ inputSchema: {
67
+ type: 'object',
68
+ properties: {
69
+ query: {
70
+ type: 'string',
71
+ description: 'Search query text'
72
+ },
73
+ filter_type: {
74
+ type: 'string',
75
+ enum: ['page', 'database'],
76
+ description: 'Filter by object type'
77
+ },
78
+ sort_direction: {
79
+ type: 'string',
80
+ enum: ['ascending', 'descending'],
81
+ description: 'Sort by last edited time'
82
+ },
83
+ page_size: {
84
+ type: 'number',
85
+ description: 'Number of results (max 100)'
86
+ }
87
+ },
88
+ required: ['query']
89
+ }
90
+ },
91
+ {
92
+ name: 'get_page',
93
+ description: 'Retrieve a page\'s properties and metadata. Use when user wants to view page details or check properties.',
94
+ inputSchema: {
95
+ type: 'object',
96
+ properties: {
97
+ page_id: {
98
+ type: 'string',
99
+ description: 'Notion page ID or URL'
100
+ }
101
+ },
102
+ required: ['page_id']
103
+ }
104
+ },
105
+ {
106
+ name: 'create_page',
107
+ description: 'Create a new page in a database or as a child of another page. Use when user wants to add new content to Notion.',
108
+ inputSchema: {
109
+ type: 'object',
110
+ properties: {
111
+ parent_id: {
112
+ type: 'string',
113
+ description: 'Parent database ID or page ID'
114
+ },
115
+ parent_type: {
116
+ type: 'string',
117
+ enum: ['database_id', 'page_id'],
118
+ description: 'Type of parent'
119
+ },
120
+ title: {
121
+ type: 'string',
122
+ description: 'Page title'
123
+ },
124
+ properties: {
125
+ type: 'object',
126
+ description: 'Database properties (for database parents)'
127
+ },
128
+ content: {
129
+ type: 'string',
130
+ description: 'Initial page content in markdown format'
131
+ }
132
+ },
133
+ required: ['parent_id', 'parent_type', 'title']
134
+ }
135
+ },
136
+ {
137
+ name: 'update_page',
138
+ description: 'Update a page\'s properties (not content blocks). Use when user wants to modify page metadata or database properties.',
139
+ inputSchema: {
140
+ type: 'object',
141
+ properties: {
142
+ page_id: {
143
+ type: 'string',
144
+ description: 'Page ID to update'
145
+ },
146
+ properties: {
147
+ type: 'object',
148
+ description: 'Properties to update'
149
+ },
150
+ archived: {
151
+ type: 'boolean',
152
+ description: 'Archive or unarchive the page'
153
+ }
154
+ },
155
+ required: ['page_id', 'properties']
156
+ }
157
+ },
158
+ {
159
+ name: 'get_page_content',
160
+ description: 'Retrieve all content blocks from a page. Use when user wants to read the full content of a page.',
161
+ inputSchema: {
162
+ type: 'object',
163
+ properties: {
164
+ page_id: {
165
+ type: 'string',
166
+ description: 'Page ID to get content from'
167
+ },
168
+ format: {
169
+ type: 'string',
170
+ enum: ['markdown', 'blocks', 'plain_text'],
171
+ description: 'Output format'
172
+ }
173
+ },
174
+ required: ['page_id']
175
+ }
176
+ },
177
+ {
178
+ name: 'append_blocks',
179
+ description: 'Add new content blocks to a page or block. Use when user wants to add content to an existing page.',
180
+ inputSchema: {
181
+ type: 'object',
182
+ properties: {
183
+ parent_id: {
184
+ type: 'string',
185
+ description: 'Page or block ID to append to'
186
+ },
187
+ content: {
188
+ type: 'string',
189
+ description: 'Content to add (supports markdown)'
190
+ }
191
+ },
192
+ required: ['parent_id', 'content']
193
+ }
194
+ },
195
+ {
196
+ name: 'update_block',
197
+ description: 'Update an existing block\'s content. Use when user wants to modify specific content.',
198
+ inputSchema: {
199
+ type: 'object',
200
+ properties: {
201
+ block_id: {
202
+ type: 'string',
203
+ description: 'Block ID to update'
204
+ },
205
+ content: {
206
+ type: 'string',
207
+ description: 'New content for the block'
208
+ }
209
+ },
210
+ required: ['block_id', 'content']
211
+ }
212
+ },
213
+ {
214
+ name: 'delete_block',
215
+ description: 'Delete a block (archive it). Use when user wants to remove content from a page.',
216
+ inputSchema: {
217
+ type: 'object',
218
+ properties: {
219
+ block_id: {
220
+ type: 'string',
221
+ description: 'Block ID to delete'
222
+ }
223
+ },
224
+ required: ['block_id']
225
+ }
226
+ },
227
+ {
228
+ name: 'get_database',
229
+ description: 'Retrieve database schema and properties. Use when user wants to understand database structure.',
230
+ inputSchema: {
231
+ type: 'object',
232
+ properties: {
233
+ database_id: {
234
+ type: 'string',
235
+ description: 'Database ID'
236
+ }
237
+ },
238
+ required: ['database_id']
239
+ }
240
+ },
241
+ {
242
+ name: 'query_database',
243
+ description: 'Query a database with optional filters and sorts. Use when user wants to retrieve specific entries from a database.',
244
+ inputSchema: {
245
+ type: 'object',
246
+ properties: {
247
+ database_id: {
248
+ type: 'string',
249
+ description: 'Database ID to query'
250
+ },
251
+ filter: {
252
+ type: 'object',
253
+ description: 'Notion filter object'
254
+ },
255
+ sorts: {
256
+ type: 'array',
257
+ description: 'Sort configuration'
258
+ },
259
+ page_size: {
260
+ type: 'number',
261
+ description: 'Results per page (max 100)'
262
+ }
263
+ },
264
+ required: ['database_id']
265
+ }
266
+ },
267
+ {
268
+ name: 'create_database',
269
+ description: 'Create a new database as a child of a page. Use when user wants to set up a new structured data collection.',
270
+ inputSchema: {
271
+ type: 'object',
272
+ properties: {
273
+ parent_page_id: {
274
+ type: 'string',
275
+ description: 'Parent page ID'
276
+ },
277
+ title: {
278
+ type: 'string',
279
+ description: 'Database title'
280
+ },
281
+ properties: {
282
+ type: 'object',
283
+ description: 'Database properties schema'
284
+ }
285
+ },
286
+ required: ['parent_page_id', 'title', 'properties']
287
+ }
288
+ },
289
+ {
290
+ name: 'list_users',
291
+ description: 'List all users in the workspace. Use when user needs to reference team members.',
292
+ inputSchema: {
293
+ type: 'object',
294
+ properties: {
295
+ page_size: {
296
+ type: 'number',
297
+ description: 'Number of users to return'
298
+ }
299
+ }
300
+ }
301
+ },
302
+ {
303
+ name: 'get_user',
304
+ description: 'Get details about a specific user. Use when user needs info about a team member.',
305
+ inputSchema: {
306
+ type: 'object',
307
+ properties: {
308
+ user_id: {
309
+ type: 'string',
310
+ description: 'User ID'
311
+ }
312
+ },
313
+ required: ['user_id']
314
+ }
315
+ }
316
+ ];
317
+
318
+ // Create MCP server
319
+ const server = new Server(
320
+ {
321
+ name: 'notion-mcp',
322
+ version: '0.1.0'
323
+ },
324
+ {
325
+ capabilities: {
326
+ tools: {}
327
+ }
328
+ }
329
+ );
330
+
331
+ // Handle tool listing
332
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
333
+ return { tools: TOOLS };
334
+ });
335
+
336
+ // Handle tool execution
337
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
338
+ const { name, arguments: args } = request.params;
339
+
340
+ try {
341
+ let result: unknown;
342
+
343
+ switch (name) {
344
+ case 'search':
345
+ result = await search(notionClient, args as unknown as SearchParams);
346
+ break;
347
+
348
+ case 'get_page':
349
+ result = await getPage(notionClient, args as unknown as GetPageParams);
350
+ break;
351
+
352
+ case 'create_page':
353
+ result = await createPage(notionClient, args as unknown as CreatePageParams);
354
+ break;
355
+
356
+ case 'update_page':
357
+ result = await updatePage(notionClient, args as unknown as UpdatePageParams);
358
+ break;
359
+
360
+ case 'get_page_content':
361
+ result = await getPageContent(notionClient, args as unknown as GetPageContentParams);
362
+ break;
363
+
364
+ case 'append_blocks':
365
+ result = await appendBlocks(notionClient, args as unknown as AppendBlocksParams);
366
+ break;
367
+
368
+ case 'update_block':
369
+ result = await updateBlock(notionClient, args as unknown as UpdateBlockParams);
370
+ break;
371
+
372
+ case 'delete_block':
373
+ result = await deleteBlock(notionClient, args as unknown as DeleteBlockParams);
374
+ break;
375
+
376
+ case 'get_database':
377
+ result = await getDatabase(notionClient, args as unknown as GetDatabaseParams);
378
+ break;
379
+
380
+ case 'query_database':
381
+ result = await queryDatabase(notionClient, args as unknown as QueryDatabaseParams);
382
+ break;
383
+
384
+ case 'create_database':
385
+ result = await createDatabase(notionClient, args as unknown as CreateDatabaseParams);
386
+ break;
387
+
388
+ case 'list_users':
389
+ result = await listUsers(notionClient, args as unknown as ListUsersParams);
390
+ break;
391
+
392
+ case 'get_user':
393
+ result = await getUser(notionClient, args as unknown as GetUserParams);
394
+ break;
395
+
396
+ default:
397
+ return {
398
+ content: [{
399
+ type: 'text' as const,
400
+ text: JSON.stringify({ error: `Unknown tool: ${name}` })
401
+ }],
402
+ isError: true
403
+ };
404
+ }
405
+
406
+ return {
407
+ content: [{
408
+ type: 'text' as const,
409
+ text: JSON.stringify(result, null, 2)
410
+ }]
411
+ };
412
+
413
+ } catch (error) {
414
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
415
+ return {
416
+ content: [{
417
+ type: 'text' as const,
418
+ text: JSON.stringify({ error: errorMessage })
419
+ }],
420
+ isError: true
421
+ };
422
+ }
423
+ });
424
+
425
+ // Run server
426
+ async function main() {
427
+ const transport = new StdioServerTransport();
428
+ await server.connect(transport);
429
+ console.error('Notion MCP Server running on stdio');
430
+ }
431
+
432
+ main().catch((error) => {
433
+ console.error('Server error:', error);
434
+ process.exit(1);
435
+ });
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Block Tools
3
+ * append_blocks, update_block, delete_block
4
+ */
5
+
6
+ import { NotionClient, extractNotionId } from '../utils/notion-client.js';
7
+ import { NotionBlock, PaginatedResponse } from '../utils/types.js';
8
+ import { markdownToBlocks, NotionBlock as ConverterBlock } from '../utils/markdown-converter.js';
9
+
10
+ // Append Blocks
11
+ export interface AppendBlocksParams {
12
+ parent_id: string;
13
+ content: string;
14
+ }
15
+
16
+ export interface AppendBlocksResult {
17
+ success: boolean;
18
+ blocks?: {
19
+ id: string;
20
+ type: string;
21
+ }[];
22
+ block_count?: number;
23
+ error?: string;
24
+ }
25
+
26
+ export async function appendBlocks(client: NotionClient, params: AppendBlocksParams): Promise<AppendBlocksResult> {
27
+ const { parent_id, content } = params;
28
+ const parentId = extractNotionId(parent_id);
29
+
30
+ // Convert markdown content to Notion blocks
31
+ const blocks = markdownToBlocks(content);
32
+
33
+ if (blocks.length === 0) {
34
+ return {
35
+ success: false,
36
+ error: 'No valid content to append'
37
+ };
38
+ }
39
+
40
+ const response = await client.patch<PaginatedResponse<NotionBlock>>(
41
+ `/blocks/${parentId}/children`,
42
+ { children: blocks }
43
+ );
44
+
45
+ if (!response.success || !response.data) {
46
+ return {
47
+ success: false,
48
+ error: response.error || 'Failed to append blocks'
49
+ };
50
+ }
51
+
52
+ return {
53
+ success: true,
54
+ blocks: response.data.results.map(block => ({
55
+ id: block.id,
56
+ type: block.type
57
+ })),
58
+ block_count: response.data.results.length
59
+ };
60
+ }
61
+
62
+ // Update Block
63
+ export interface UpdateBlockParams {
64
+ block_id: string;
65
+ content: string;
66
+ }
67
+
68
+ export interface UpdateBlockResult {
69
+ success: boolean;
70
+ block?: {
71
+ id: string;
72
+ type: string;
73
+ last_edited_time: string;
74
+ };
75
+ error?: string;
76
+ }
77
+
78
+ export async function updateBlock(client: NotionClient, params: UpdateBlockParams): Promise<UpdateBlockResult> {
79
+ const { block_id, content } = params;
80
+ const blockId = extractNotionId(block_id);
81
+
82
+ // First, get the current block to determine its type
83
+ const getResponse = await client.get<NotionBlock>(`/blocks/${blockId}`);
84
+
85
+ if (!getResponse.success || !getResponse.data) {
86
+ return {
87
+ success: false,
88
+ error: getResponse.error || 'Failed to get block for update'
89
+ };
90
+ }
91
+
92
+ const currentBlock = getResponse.data;
93
+ const blockType = currentBlock.type;
94
+
95
+ // Convert the new content to a block and extract the content part
96
+ const newBlocks = markdownToBlocks(content);
97
+
98
+ if (newBlocks.length === 0) {
99
+ return {
100
+ success: false,
101
+ error: 'Invalid content provided'
102
+ };
103
+ }
104
+
105
+ // Use the first block's content for the update
106
+ const newBlock = newBlocks[0] as ConverterBlock;
107
+
108
+ // Build update body based on block type
109
+ const updateBody: Record<string, unknown> = {};
110
+
111
+ // Handle common block types
112
+ if (blockType === 'paragraph' && newBlock.type === 'paragraph') {
113
+ updateBody.paragraph = (newBlock as Record<string, unknown>).paragraph;
114
+ } else if (blockType === 'heading_1' && newBlock.type === 'heading_1') {
115
+ updateBody.heading_1 = (newBlock as Record<string, unknown>).heading_1;
116
+ } else if (blockType === 'heading_2' && newBlock.type === 'heading_2') {
117
+ updateBody.heading_2 = (newBlock as Record<string, unknown>).heading_2;
118
+ } else if (blockType === 'heading_3' && newBlock.type === 'heading_3') {
119
+ updateBody.heading_3 = (newBlock as Record<string, unknown>).heading_3;
120
+ } else if (blockType === 'bulleted_list_item' && newBlock.type === 'bulleted_list_item') {
121
+ updateBody.bulleted_list_item = (newBlock as Record<string, unknown>).bulleted_list_item;
122
+ } else if (blockType === 'numbered_list_item' && newBlock.type === 'numbered_list_item') {
123
+ updateBody.numbered_list_item = (newBlock as Record<string, unknown>).numbered_list_item;
124
+ } else if (blockType === 'to_do' && newBlock.type === 'to_do') {
125
+ updateBody.to_do = (newBlock as Record<string, unknown>).to_do;
126
+ } else if (blockType === 'quote' && newBlock.type === 'quote') {
127
+ updateBody.quote = (newBlock as Record<string, unknown>).quote;
128
+ } else if (blockType === 'code' && newBlock.type === 'code') {
129
+ updateBody.code = (newBlock as Record<string, unknown>).code;
130
+ } else {
131
+ // For type mismatch or unsupported types, update as paragraph with rich text
132
+ updateBody[blockType] = {
133
+ rich_text: [{ type: 'text', text: { content } }]
134
+ };
135
+ }
136
+
137
+ const response = await client.patch<NotionBlock>(`/blocks/${blockId}`, updateBody);
138
+
139
+ if (!response.success || !response.data) {
140
+ return {
141
+ success: false,
142
+ error: response.error || 'Failed to update block'
143
+ };
144
+ }
145
+
146
+ return {
147
+ success: true,
148
+ block: {
149
+ id: response.data.id,
150
+ type: response.data.type,
151
+ last_edited_time: response.data.last_edited_time
152
+ }
153
+ };
154
+ }
155
+
156
+ // Delete Block
157
+ export interface DeleteBlockParams {
158
+ block_id: string;
159
+ }
160
+
161
+ export interface DeleteBlockResult {
162
+ success: boolean;
163
+ deleted_id?: string;
164
+ error?: string;
165
+ }
166
+
167
+ export async function deleteBlock(client: NotionClient, params: DeleteBlockParams): Promise<DeleteBlockResult> {
168
+ const { block_id } = params;
169
+ const blockId = extractNotionId(block_id);
170
+
171
+ const response = await client.delete<NotionBlock>(`/blocks/${blockId}`);
172
+
173
+ if (!response.success) {
174
+ return {
175
+ success: false,
176
+ error: response.error || 'Failed to delete block'
177
+ };
178
+ }
179
+
180
+ return {
181
+ success: true,
182
+ deleted_id: blockId
183
+ };
184
+ }