@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,237 @@
1
+ /**
2
+ * Notion API Types
3
+ */
4
+
5
+ // Common types
6
+ export interface NotionUser {
7
+ object: 'user';
8
+ id: string;
9
+ type?: 'person' | 'bot';
10
+ name?: string;
11
+ avatar_url?: string | null;
12
+ person?: {
13
+ email?: string;
14
+ };
15
+ }
16
+
17
+ export interface NotionParent {
18
+ type: 'database_id' | 'page_id' | 'workspace';
19
+ database_id?: string;
20
+ page_id?: string;
21
+ workspace?: boolean;
22
+ }
23
+
24
+ export interface NotionRichTextItem {
25
+ type: 'text' | 'mention' | 'equation';
26
+ text?: {
27
+ content: string;
28
+ link?: { url: string } | null;
29
+ };
30
+ annotations?: {
31
+ bold: boolean;
32
+ italic: boolean;
33
+ strikethrough: boolean;
34
+ underline: boolean;
35
+ code: boolean;
36
+ color: string;
37
+ };
38
+ plain_text?: string;
39
+ href?: string | null;
40
+ }
41
+
42
+ // Page types
43
+ export interface NotionPage {
44
+ object: 'page';
45
+ id: string;
46
+ created_time: string;
47
+ last_edited_time: string;
48
+ created_by: NotionUser;
49
+ last_edited_by: NotionUser;
50
+ parent: NotionParent;
51
+ archived: boolean;
52
+ properties: Record<string, NotionProperty>;
53
+ url: string;
54
+ icon?: NotionIcon | null;
55
+ cover?: NotionFile | null;
56
+ }
57
+
58
+ export interface NotionIcon {
59
+ type: 'emoji' | 'external' | 'file';
60
+ emoji?: string;
61
+ external?: { url: string };
62
+ file?: { url: string };
63
+ }
64
+
65
+ export interface NotionFile {
66
+ type: 'external' | 'file';
67
+ external?: { url: string };
68
+ file?: { url: string; expiry_time: string };
69
+ }
70
+
71
+ // Property types
72
+ export interface NotionProperty {
73
+ id: string;
74
+ type: string;
75
+ [key: string]: unknown;
76
+ }
77
+
78
+ export interface TitleProperty extends NotionProperty {
79
+ type: 'title';
80
+ title: NotionRichTextItem[];
81
+ }
82
+
83
+ export interface RichTextProperty extends NotionProperty {
84
+ type: 'rich_text';
85
+ rich_text: NotionRichTextItem[];
86
+ }
87
+
88
+ export interface NumberProperty extends NotionProperty {
89
+ type: 'number';
90
+ number: number | null;
91
+ }
92
+
93
+ export interface SelectProperty extends NotionProperty {
94
+ type: 'select';
95
+ select: { id: string; name: string; color: string } | null;
96
+ }
97
+
98
+ export interface MultiSelectProperty extends NotionProperty {
99
+ type: 'multi_select';
100
+ multi_select: { id: string; name: string; color: string }[];
101
+ }
102
+
103
+ export interface DateProperty extends NotionProperty {
104
+ type: 'date';
105
+ date: { start: string; end?: string | null; time_zone?: string | null } | null;
106
+ }
107
+
108
+ export interface CheckboxProperty extends NotionProperty {
109
+ type: 'checkbox';
110
+ checkbox: boolean;
111
+ }
112
+
113
+ export interface UrlProperty extends NotionProperty {
114
+ type: 'url';
115
+ url: string | null;
116
+ }
117
+
118
+ export interface EmailProperty extends NotionProperty {
119
+ type: 'email';
120
+ email: string | null;
121
+ }
122
+
123
+ export interface PhoneProperty extends NotionProperty {
124
+ type: 'phone_number';
125
+ phone_number: string | null;
126
+ }
127
+
128
+ export interface PeopleProperty extends NotionProperty {
129
+ type: 'people';
130
+ people: NotionUser[];
131
+ }
132
+
133
+ // Database types
134
+ export interface NotionDatabase {
135
+ object: 'database';
136
+ id: string;
137
+ created_time: string;
138
+ last_edited_time: string;
139
+ title: NotionRichTextItem[];
140
+ description: NotionRichTextItem[];
141
+ properties: Record<string, DatabaseProperty>;
142
+ parent: NotionParent;
143
+ url: string;
144
+ archived: boolean;
145
+ is_inline: boolean;
146
+ }
147
+
148
+ export interface DatabaseProperty {
149
+ id: string;
150
+ name: string;
151
+ type: string;
152
+ [key: string]: unknown;
153
+ }
154
+
155
+ // Block types
156
+ export interface NotionBlock {
157
+ object: 'block';
158
+ id: string;
159
+ parent: NotionParent;
160
+ type: string;
161
+ created_time: string;
162
+ last_edited_time: string;
163
+ created_by: NotionUser;
164
+ last_edited_by: NotionUser;
165
+ has_children: boolean;
166
+ archived: boolean;
167
+ [key: string]: unknown;
168
+ }
169
+
170
+ // Search types
171
+ export interface SearchRequest {
172
+ query?: string;
173
+ filter?: {
174
+ value: 'page' | 'database';
175
+ property: 'object';
176
+ };
177
+ sort?: {
178
+ direction: 'ascending' | 'descending';
179
+ timestamp: 'last_edited_time';
180
+ };
181
+ start_cursor?: string;
182
+ page_size?: number;
183
+ }
184
+
185
+ export interface SearchResponse {
186
+ object: 'list';
187
+ results: (NotionPage | NotionDatabase)[];
188
+ next_cursor: string | null;
189
+ has_more: boolean;
190
+ }
191
+
192
+ // Query types
193
+ export interface DatabaseQueryRequest {
194
+ filter?: NotionFilter;
195
+ sorts?: NotionSort[];
196
+ start_cursor?: string;
197
+ page_size?: number;
198
+ }
199
+
200
+ export interface NotionFilter {
201
+ and?: NotionFilter[];
202
+ or?: NotionFilter[];
203
+ property?: string;
204
+ [key: string]: unknown;
205
+ }
206
+
207
+ export interface NotionSort {
208
+ property?: string;
209
+ timestamp?: 'created_time' | 'last_edited_time';
210
+ direction: 'ascending' | 'descending';
211
+ }
212
+
213
+ export interface PaginatedResponse<T> {
214
+ object: 'list';
215
+ results: T[];
216
+ next_cursor: string | null;
217
+ has_more: boolean;
218
+ }
219
+
220
+ // User list response
221
+ export interface UsersListResponse extends PaginatedResponse<NotionUser> {}
222
+
223
+ // Helper to extract title from page
224
+ export function getPageTitle(page: NotionPage): string {
225
+ for (const [, prop] of Object.entries(page.properties)) {
226
+ if (prop.type === 'title') {
227
+ const titleProp = prop as TitleProperty;
228
+ return titleProp.title.map(t => t.plain_text || t.text?.content || '').join('');
229
+ }
230
+ }
231
+ return 'Untitled';
232
+ }
233
+
234
+ // Helper to extract title from database
235
+ export function getDatabaseTitle(database: NotionDatabase): string {
236
+ return database.title.map(t => t.plain_text || t.text?.content || '').join('') || 'Untitled';
237
+ }
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Tests for Markdown Converter
3
+ */
4
+
5
+ import {
6
+ parseInlineFormatting,
7
+ markdownToBlocks,
8
+ blocksToMarkdown,
9
+ blocksToPlainText
10
+ } from '../src/utils/markdown-converter';
11
+
12
+ describe('parseInlineFormatting', () => {
13
+ it('should handle plain text', () => {
14
+ const result = parseInlineFormatting('Hello world');
15
+ expect(result).toHaveLength(1);
16
+ expect(result[0].text.content).toBe('Hello world');
17
+ });
18
+
19
+ it('should handle bold text', () => {
20
+ const result = parseInlineFormatting('This is **bold** text');
21
+ expect(result).toHaveLength(3);
22
+ expect(result[0].text.content).toBe('This is ');
23
+ expect(result[1].text.content).toBe('bold');
24
+ expect(result[1].annotations?.bold).toBe(true);
25
+ expect(result[2].text.content).toBe(' text');
26
+ });
27
+
28
+ it('should handle italic text', () => {
29
+ const result = parseInlineFormatting('This is *italic* text');
30
+ expect(result).toHaveLength(3);
31
+ expect(result[1].text.content).toBe('italic');
32
+ expect(result[1].annotations?.italic).toBe(true);
33
+ });
34
+
35
+ it('should handle inline code', () => {
36
+ const result = parseInlineFormatting('Use `code` here');
37
+ expect(result).toHaveLength(3);
38
+ expect(result[1].text.content).toBe('code');
39
+ expect(result[1].annotations?.code).toBe(true);
40
+ });
41
+
42
+ it('should handle strikethrough', () => {
43
+ const result = parseInlineFormatting('This is ~~deleted~~ text');
44
+ expect(result).toHaveLength(3);
45
+ expect(result[1].text.content).toBe('deleted');
46
+ expect(result[1].annotations?.strikethrough).toBe(true);
47
+ });
48
+
49
+ it('should handle links', () => {
50
+ const result = parseInlineFormatting('Click [here](https://example.com) now');
51
+ expect(result).toHaveLength(3);
52
+ expect(result[1].text.content).toBe('here');
53
+ expect(result[1].text.link?.url).toBe('https://example.com');
54
+ });
55
+
56
+ it('should handle multiple formatting in sequence', () => {
57
+ const result = parseInlineFormatting('**bold** and *italic*');
58
+ expect(result.length).toBeGreaterThan(2);
59
+ });
60
+ });
61
+
62
+ describe('markdownToBlocks', () => {
63
+ it('should convert heading 1', () => {
64
+ const blocks = markdownToBlocks('# Heading 1');
65
+ expect(blocks).toHaveLength(1);
66
+ expect(blocks[0].type).toBe('heading_1');
67
+ });
68
+
69
+ it('should convert heading 2', () => {
70
+ const blocks = markdownToBlocks('## Heading 2');
71
+ expect(blocks).toHaveLength(1);
72
+ expect(blocks[0].type).toBe('heading_2');
73
+ });
74
+
75
+ it('should convert heading 3', () => {
76
+ const blocks = markdownToBlocks('### Heading 3');
77
+ expect(blocks).toHaveLength(1);
78
+ expect(blocks[0].type).toBe('heading_3');
79
+ });
80
+
81
+ it('should convert bullet list', () => {
82
+ const blocks = markdownToBlocks('- Item 1\n- Item 2');
83
+ expect(blocks).toHaveLength(2);
84
+ expect(blocks[0].type).toBe('bulleted_list_item');
85
+ expect(blocks[1].type).toBe('bulleted_list_item');
86
+ });
87
+
88
+ it('should convert numbered list', () => {
89
+ const blocks = markdownToBlocks('1. First\n2. Second');
90
+ expect(blocks).toHaveLength(2);
91
+ expect(blocks[0].type).toBe('numbered_list_item');
92
+ expect(blocks[1].type).toBe('numbered_list_item');
93
+ });
94
+
95
+ it('should convert checkbox unchecked', () => {
96
+ const blocks = markdownToBlocks('- [ ] Todo item');
97
+ expect(blocks).toHaveLength(1);
98
+ expect(blocks[0].type).toBe('to_do');
99
+ expect((blocks[0].to_do as { checked: boolean }).checked).toBe(false);
100
+ });
101
+
102
+ it('should convert checkbox checked', () => {
103
+ const blocks = markdownToBlocks('- [x] Done item');
104
+ expect(blocks).toHaveLength(1);
105
+ expect(blocks[0].type).toBe('to_do');
106
+ expect((blocks[0].to_do as { checked: boolean }).checked).toBe(true);
107
+ });
108
+
109
+ it('should convert blockquote', () => {
110
+ const blocks = markdownToBlocks('> This is a quote');
111
+ expect(blocks).toHaveLength(1);
112
+ expect(blocks[0].type).toBe('quote');
113
+ });
114
+
115
+ it('should convert divider', () => {
116
+ const blocks = markdownToBlocks('---');
117
+ expect(blocks).toHaveLength(1);
118
+ expect(blocks[0].type).toBe('divider');
119
+ });
120
+
121
+ it('should convert paragraph', () => {
122
+ const blocks = markdownToBlocks('This is a paragraph');
123
+ expect(blocks).toHaveLength(1);
124
+ expect(blocks[0].type).toBe('paragraph');
125
+ });
126
+
127
+ it('should convert code block', () => {
128
+ const blocks = markdownToBlocks('```javascript\nconst x = 1;\n```');
129
+ expect(blocks).toHaveLength(1);
130
+ expect(blocks[0].type).toBe('code');
131
+ expect((blocks[0].code as { language: string }).language).toBe('javascript');
132
+ });
133
+
134
+ it('should handle multiple elements', () => {
135
+ const markdown = `# Title
136
+
137
+ This is a paragraph.
138
+
139
+ - List item 1
140
+ - List item 2
141
+
142
+ > A quote`;
143
+
144
+ const blocks = markdownToBlocks(markdown);
145
+ expect(blocks.length).toBeGreaterThan(4);
146
+ expect(blocks[0].type).toBe('heading_1');
147
+ });
148
+
149
+ it('should handle empty input', () => {
150
+ const blocks = markdownToBlocks('');
151
+ expect(blocks).toHaveLength(0);
152
+ });
153
+ });
154
+
155
+ describe('blocksToMarkdown', () => {
156
+ it('should convert paragraph block to markdown', () => {
157
+ const blocks = [{
158
+ type: 'paragraph',
159
+ paragraph: {
160
+ rich_text: [{ type: 'text', text: { content: 'Hello' } }]
161
+ }
162
+ }];
163
+ const result = blocksToMarkdown(blocks);
164
+ expect(result).toBe('Hello');
165
+ });
166
+
167
+ it('should convert heading blocks to markdown', () => {
168
+ const blocks = [
169
+ {
170
+ type: 'heading_1',
171
+ heading_1: {
172
+ rich_text: [{ type: 'text', text: { content: 'Title' } }]
173
+ }
174
+ },
175
+ {
176
+ type: 'heading_2',
177
+ heading_2: {
178
+ rich_text: [{ type: 'text', text: { content: 'Subtitle' } }]
179
+ }
180
+ }
181
+ ];
182
+ const result = blocksToMarkdown(blocks);
183
+ expect(result).toContain('# Title');
184
+ expect(result).toContain('## Subtitle');
185
+ });
186
+
187
+ it('should convert list items to markdown', () => {
188
+ const blocks = [
189
+ {
190
+ type: 'bulleted_list_item',
191
+ bulleted_list_item: {
192
+ rich_text: [{ type: 'text', text: { content: 'Item' } }]
193
+ }
194
+ }
195
+ ];
196
+ const result = blocksToMarkdown(blocks);
197
+ expect(result).toContain('- Item');
198
+ });
199
+
200
+ it('should convert todo items to markdown', () => {
201
+ const blocks = [
202
+ {
203
+ type: 'to_do',
204
+ to_do: {
205
+ rich_text: [{ type: 'text', text: { content: 'Task' } }],
206
+ checked: true
207
+ }
208
+ }
209
+ ];
210
+ const result = blocksToMarkdown(blocks);
211
+ expect(result).toContain('- [x] Task');
212
+ });
213
+
214
+ it('should handle formatted text', () => {
215
+ const blocks = [{
216
+ type: 'paragraph',
217
+ paragraph: {
218
+ rich_text: [
219
+ { type: 'text', text: { content: 'bold' }, annotations: { bold: true } }
220
+ ]
221
+ }
222
+ }];
223
+ const result = blocksToMarkdown(blocks);
224
+ expect(result).toBe('**bold**');
225
+ });
226
+ });
227
+
228
+ describe('blocksToPlainText', () => {
229
+ it('should extract plain text from blocks', () => {
230
+ const blocks = [
231
+ {
232
+ type: 'paragraph',
233
+ paragraph: {
234
+ rich_text: [{ type: 'text', text: { content: 'Hello' } }]
235
+ }
236
+ },
237
+ {
238
+ type: 'paragraph',
239
+ paragraph: {
240
+ rich_text: [{ type: 'text', text: { content: 'World' } }]
241
+ }
242
+ }
243
+ ];
244
+ const result = blocksToPlainText(blocks);
245
+ expect(result).toBe('Hello\nWorld');
246
+ });
247
+
248
+ it('should handle empty blocks', () => {
249
+ const result = blocksToPlainText([]);
250
+ expect(result).toBe('');
251
+ });
252
+ });
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Tests for Notion Client
3
+ */
4
+
5
+ import { extractNotionId } from '../src/utils/notion-client';
6
+
7
+ describe('extractNotionId', () => {
8
+ describe('UUID format handling', () => {
9
+ it('should return normalized UUID with dashes when given 32 char hex', () => {
10
+ const input = '1234567890abcdef1234567890abcdef';
11
+ const result = extractNotionId(input);
12
+ expect(result).toBe('12345678-90ab-cdef-1234-567890abcdef');
13
+ });
14
+
15
+ it('should keep UUID format with dashes unchanged', () => {
16
+ const input = '12345678-90ab-cdef-1234-567890abcdef';
17
+ const result = extractNotionId(input);
18
+ expect(result).toBe('12345678-90ab-cdef-1234-567890abcdef');
19
+ });
20
+
21
+ it('should normalize mixed case UUID', () => {
22
+ const input = 'ABCDEF12-3456-7890-ABCD-EF1234567890';
23
+ const result = extractNotionId(input);
24
+ expect(result).toBe('abcdef12-3456-7890-abcd-ef1234567890');
25
+ });
26
+ });
27
+
28
+ describe('URL extraction', () => {
29
+ it('should extract ID from notion.so page URL', () => {
30
+ const input = 'https://www.notion.so/myworkspace/Page-Title-1234567890abcdef1234567890abcdef';
31
+ const result = extractNotionId(input);
32
+ expect(result).toBe('12345678-90ab-cdef-1234-567890abcdef');
33
+ });
34
+
35
+ it('should extract ID from notion.site URL', () => {
36
+ const input = 'https://example.notion.site/Page-1234567890abcdef1234567890abcdef';
37
+ const result = extractNotionId(input);
38
+ expect(result).toBe('12345678-90ab-cdef-1234-567890abcdef');
39
+ });
40
+
41
+ it('should extract ID from URL with query parameters', () => {
42
+ const input = 'https://www.notion.so/Page-1234567890abcdef1234567890abcdef?v=abc123';
43
+ const result = extractNotionId(input);
44
+ expect(result).toBe('12345678-90ab-cdef-1234-567890abcdef');
45
+ });
46
+
47
+ it('should extract ID from URL with dashes in the ID', () => {
48
+ const input = 'https://www.notion.so/workspace/12345678-90ab-cdef-1234-567890abcdef';
49
+ const result = extractNotionId(input);
50
+ expect(result).toBe('12345678-90ab-cdef-1234-567890abcdef');
51
+ });
52
+ });
53
+
54
+ describe('edge cases', () => {
55
+ it('should return input as-is if no pattern matches', () => {
56
+ const input = 'invalid-id';
57
+ const result = extractNotionId(input);
58
+ expect(result).toBe('invalid-id');
59
+ });
60
+
61
+ it('should handle empty string', () => {
62
+ const input = '';
63
+ const result = extractNotionId(input);
64
+ expect(result).toBe('');
65
+ });
66
+ });
67
+ });