@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,216 @@
1
+ /**
2
+ * Database Tools
3
+ * get_database, query_database, create_database
4
+ */
5
+
6
+ import { NotionClient, extractNotionId } from '../utils/notion-client.js';
7
+ import { NotionDatabase, NotionPage, PaginatedResponse, getPageTitle, getDatabaseTitle, NotionFilter, NotionSort } from '../utils/types.js';
8
+
9
+ // Get Database
10
+ export interface GetDatabaseParams {
11
+ database_id: string;
12
+ }
13
+
14
+ export interface GetDatabaseResult {
15
+ success: boolean;
16
+ database?: {
17
+ id: string;
18
+ title: string;
19
+ description: string;
20
+ url: string;
21
+ created_time: string;
22
+ last_edited_time: string;
23
+ properties: Record<string, {
24
+ id: string;
25
+ name: string;
26
+ type: string;
27
+ }>;
28
+ is_inline: boolean;
29
+ archived: boolean;
30
+ };
31
+ error?: string;
32
+ }
33
+
34
+ export async function getDatabase(client: NotionClient, params: GetDatabaseParams): Promise<GetDatabaseResult> {
35
+ const databaseId = extractNotionId(params.database_id);
36
+ const response = await client.get<NotionDatabase>(`/databases/${databaseId}`);
37
+
38
+ if (!response.success || !response.data) {
39
+ return {
40
+ success: false,
41
+ error: response.error || 'Failed to get database'
42
+ };
43
+ }
44
+
45
+ const db = response.data;
46
+
47
+ // Simplify properties schema
48
+ const properties: Record<string, { id: string; name: string; type: string }> = {};
49
+ for (const [name, prop] of Object.entries(db.properties)) {
50
+ properties[name] = {
51
+ id: prop.id,
52
+ name: prop.name,
53
+ type: prop.type
54
+ };
55
+ }
56
+
57
+ return {
58
+ success: true,
59
+ database: {
60
+ id: db.id,
61
+ title: getDatabaseTitle(db),
62
+ description: db.description.map(d => d.plain_text || d.text?.content || '').join(''),
63
+ url: db.url,
64
+ created_time: db.created_time,
65
+ last_edited_time: db.last_edited_time,
66
+ properties,
67
+ is_inline: db.is_inline,
68
+ archived: db.archived
69
+ }
70
+ };
71
+ }
72
+
73
+ // Query Database
74
+ export interface QueryDatabaseParams {
75
+ database_id: string;
76
+ filter?: NotionFilter;
77
+ sorts?: NotionSort[];
78
+ page_size?: number;
79
+ }
80
+
81
+ export interface QueryDatabaseResult {
82
+ success: boolean;
83
+ pages?: {
84
+ id: string;
85
+ title: string;
86
+ url: string;
87
+ created_time: string;
88
+ last_edited_time: string;
89
+ properties: Record<string, unknown>;
90
+ }[];
91
+ total_count?: number;
92
+ has_more?: boolean;
93
+ next_cursor?: string | null;
94
+ error?: string;
95
+ }
96
+
97
+ export async function queryDatabase(client: NotionClient, params: QueryDatabaseParams): Promise<QueryDatabaseResult> {
98
+ const { database_id, filter, sorts, page_size = 100 } = params;
99
+ const databaseId = extractNotionId(database_id);
100
+
101
+ const requestBody: Record<string, unknown> = {
102
+ page_size: Math.min(Math.max(page_size, 1), 100)
103
+ };
104
+
105
+ if (filter) {
106
+ requestBody.filter = filter;
107
+ }
108
+
109
+ if (sorts && sorts.length > 0) {
110
+ requestBody.sorts = sorts;
111
+ }
112
+
113
+ const response = await client.post<PaginatedResponse<NotionPage>>(
114
+ `/databases/${databaseId}/query`,
115
+ requestBody
116
+ );
117
+
118
+ if (!response.success || !response.data) {
119
+ return {
120
+ success: false,
121
+ error: response.error || 'Failed to query database'
122
+ };
123
+ }
124
+
125
+ const pages = response.data.results.map(page => ({
126
+ id: page.id,
127
+ title: getPageTitle(page),
128
+ url: page.url,
129
+ created_time: page.created_time,
130
+ last_edited_time: page.last_edited_time,
131
+ properties: page.properties
132
+ }));
133
+
134
+ return {
135
+ success: true,
136
+ pages,
137
+ total_count: pages.length,
138
+ has_more: response.data.has_more,
139
+ next_cursor: response.data.next_cursor
140
+ };
141
+ }
142
+
143
+ // Create Database
144
+ export interface CreateDatabaseParams {
145
+ parent_page_id: string;
146
+ title: string;
147
+ properties: Record<string, DatabasePropertySchema>;
148
+ }
149
+
150
+ export interface DatabasePropertySchema {
151
+ type: string;
152
+ [key: string]: unknown;
153
+ }
154
+
155
+ export interface CreateDatabaseResult {
156
+ success: boolean;
157
+ database?: {
158
+ id: string;
159
+ title: string;
160
+ url: string;
161
+ };
162
+ error?: string;
163
+ }
164
+
165
+ export async function createDatabase(client: NotionClient, params: CreateDatabaseParams): Promise<CreateDatabaseResult> {
166
+ const { parent_page_id, title, properties } = params;
167
+ const pageId = extractNotionId(parent_page_id);
168
+
169
+ // Ensure there's at least a title property
170
+ const dbProperties: Record<string, unknown> = { ...properties };
171
+
172
+ // Every database needs a title property
173
+ let hasTitleProperty = false;
174
+ for (const prop of Object.values(dbProperties)) {
175
+ if ((prop as Record<string, unknown>).type === 'title') {
176
+ hasTitleProperty = true;
177
+ break;
178
+ }
179
+ }
180
+
181
+ if (!hasTitleProperty) {
182
+ dbProperties['Name'] = { title: {} };
183
+ }
184
+
185
+ const requestBody = {
186
+ parent: {
187
+ type: 'page_id',
188
+ page_id: pageId
189
+ },
190
+ title: [
191
+ {
192
+ type: 'text',
193
+ text: { content: title }
194
+ }
195
+ ],
196
+ properties: dbProperties
197
+ };
198
+
199
+ const response = await client.post<NotionDatabase>('/databases', requestBody);
200
+
201
+ if (!response.success || !response.data) {
202
+ return {
203
+ success: false,
204
+ error: response.error || 'Failed to create database'
205
+ };
206
+ }
207
+
208
+ return {
209
+ success: true,
210
+ database: {
211
+ id: response.data.id,
212
+ title: getDatabaseTitle(response.data),
213
+ url: response.data.url
214
+ }
215
+ };
216
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Tool Exports
3
+ */
4
+
5
+ export * from './search.js';
6
+ export * from './pages.js';
7
+ export * from './blocks.js';
8
+ export * from './databases.js';
9
+ export * from './users.js';
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Page Tools
3
+ * get_page, create_page, update_page, get_page_content
4
+ */
5
+
6
+ import { NotionClient, extractNotionId } from '../utils/notion-client.js';
7
+ import { NotionPage, NotionBlock, PaginatedResponse, getPageTitle } from '../utils/types.js';
8
+ import { markdownToBlocks, blocksToMarkdown, blocksToPlainText } from '../utils/markdown-converter.js';
9
+
10
+ // Get Page
11
+ export interface GetPageParams {
12
+ page_id: string;
13
+ }
14
+
15
+ export interface GetPageResult {
16
+ success: boolean;
17
+ page?: {
18
+ id: string;
19
+ title: string;
20
+ url: string;
21
+ created_time: string;
22
+ last_edited_time: string;
23
+ archived: boolean;
24
+ properties: Record<string, unknown>;
25
+ parent: {
26
+ type: string;
27
+ id?: string;
28
+ };
29
+ };
30
+ error?: string;
31
+ }
32
+
33
+ export async function getPage(client: NotionClient, params: GetPageParams): Promise<GetPageResult> {
34
+ const pageId = extractNotionId(params.page_id);
35
+ const response = await client.get<NotionPage>(`/pages/${pageId}`);
36
+
37
+ if (!response.success || !response.data) {
38
+ return {
39
+ success: false,
40
+ error: response.error || 'Failed to get page'
41
+ };
42
+ }
43
+
44
+ const page = response.data;
45
+ return {
46
+ success: true,
47
+ page: {
48
+ id: page.id,
49
+ title: getPageTitle(page),
50
+ url: page.url,
51
+ created_time: page.created_time,
52
+ last_edited_time: page.last_edited_time,
53
+ archived: page.archived,
54
+ properties: page.properties,
55
+ parent: {
56
+ type: page.parent.type,
57
+ id: page.parent.database_id || page.parent.page_id
58
+ }
59
+ }
60
+ };
61
+ }
62
+
63
+ // Create Page
64
+ export interface CreatePageParams {
65
+ parent_id: string;
66
+ parent_type: 'database_id' | 'page_id';
67
+ title: string;
68
+ properties?: Record<string, unknown>;
69
+ content?: string;
70
+ }
71
+
72
+ export interface CreatePageResult {
73
+ success: boolean;
74
+ page?: {
75
+ id: string;
76
+ url: string;
77
+ title: string;
78
+ };
79
+ error?: string;
80
+ }
81
+
82
+ export async function createPage(client: NotionClient, params: CreatePageParams): Promise<CreatePageResult> {
83
+ const { parent_id, parent_type, title, properties = {}, content } = params;
84
+ const parentId = extractNotionId(parent_id);
85
+
86
+ // Build parent object
87
+ const parent: Record<string, string> = {};
88
+ parent[parent_type] = parentId;
89
+
90
+ // Build properties - always include title
91
+ const pageProperties: Record<string, unknown> = { ...properties };
92
+
93
+ // For database parents, title is usually a property called "Name" or "Title"
94
+ // For page parents, we need to set the title via page properties
95
+ if (parent_type === 'database_id') {
96
+ // If no title property is set, try "Name" first, then "Title"
97
+ if (!pageProperties['Name'] && !pageProperties['Title'] && !pageProperties['title']) {
98
+ pageProperties['Name'] = {
99
+ title: [{ type: 'text', text: { content: title } }]
100
+ };
101
+ }
102
+ } else {
103
+ // For page parents, set title property
104
+ pageProperties['title'] = {
105
+ title: [{ type: 'text', text: { content: title } }]
106
+ };
107
+ }
108
+
109
+ // Build request body
110
+ const requestBody: Record<string, unknown> = {
111
+ parent,
112
+ properties: pageProperties
113
+ };
114
+
115
+ // Add content as children blocks if provided
116
+ if (content) {
117
+ requestBody.children = markdownToBlocks(content);
118
+ }
119
+
120
+ const response = await client.post<NotionPage>('/pages', requestBody);
121
+
122
+ if (!response.success || !response.data) {
123
+ return {
124
+ success: false,
125
+ error: response.error || 'Failed to create page'
126
+ };
127
+ }
128
+
129
+ return {
130
+ success: true,
131
+ page: {
132
+ id: response.data.id,
133
+ url: response.data.url,
134
+ title: getPageTitle(response.data)
135
+ }
136
+ };
137
+ }
138
+
139
+ // Update Page
140
+ export interface UpdatePageParams {
141
+ page_id: string;
142
+ properties: Record<string, unknown>;
143
+ archived?: boolean;
144
+ }
145
+
146
+ export interface UpdatePageResult {
147
+ success: boolean;
148
+ page?: {
149
+ id: string;
150
+ url: string;
151
+ title: string;
152
+ last_edited_time: string;
153
+ };
154
+ error?: string;
155
+ }
156
+
157
+ export async function updatePage(client: NotionClient, params: UpdatePageParams): Promise<UpdatePageResult> {
158
+ const { page_id, properties, archived } = params;
159
+ const pageId = extractNotionId(page_id);
160
+
161
+ const requestBody: Record<string, unknown> = {
162
+ properties
163
+ };
164
+
165
+ if (archived !== undefined) {
166
+ requestBody.archived = archived;
167
+ }
168
+
169
+ const response = await client.patch<NotionPage>(`/pages/${pageId}`, requestBody);
170
+
171
+ if (!response.success || !response.data) {
172
+ return {
173
+ success: false,
174
+ error: response.error || 'Failed to update page'
175
+ };
176
+ }
177
+
178
+ return {
179
+ success: true,
180
+ page: {
181
+ id: response.data.id,
182
+ url: response.data.url,
183
+ title: getPageTitle(response.data),
184
+ last_edited_time: response.data.last_edited_time
185
+ }
186
+ };
187
+ }
188
+
189
+ // Get Page Content
190
+ export interface GetPageContentParams {
191
+ page_id: string;
192
+ format?: 'markdown' | 'blocks' | 'plain_text';
193
+ }
194
+
195
+ export interface GetPageContentResult {
196
+ success: boolean;
197
+ content?: string | unknown[];
198
+ format?: string;
199
+ block_count?: number;
200
+ error?: string;
201
+ }
202
+
203
+ export async function getPageContent(client: NotionClient, params: GetPageContentParams): Promise<GetPageContentResult> {
204
+ const { page_id, format = 'markdown' } = params;
205
+ const pageId = extractNotionId(page_id);
206
+
207
+ // Fetch all blocks (with pagination)
208
+ const allBlocks: NotionBlock[] = [];
209
+ let cursor: string | undefined;
210
+ let hasMore = true;
211
+
212
+ while (hasMore) {
213
+ const endpoint = cursor
214
+ ? `/blocks/${pageId}/children?start_cursor=${cursor}&page_size=100`
215
+ : `/blocks/${pageId}/children?page_size=100`;
216
+
217
+ const response = await client.get<PaginatedResponse<NotionBlock>>(endpoint);
218
+
219
+ if (!response.success || !response.data) {
220
+ return {
221
+ success: false,
222
+ error: response.error || 'Failed to get page content'
223
+ };
224
+ }
225
+
226
+ allBlocks.push(...response.data.results);
227
+ hasMore = response.data.has_more;
228
+ cursor = response.data.next_cursor || undefined;
229
+ }
230
+
231
+ // Convert blocks to requested format
232
+ let content: string | unknown[];
233
+
234
+ switch (format) {
235
+ case 'blocks':
236
+ content = allBlocks;
237
+ break;
238
+ case 'plain_text':
239
+ content = blocksToPlainText(allBlocks);
240
+ break;
241
+ case 'markdown':
242
+ default:
243
+ content = blocksToMarkdown(allBlocks);
244
+ break;
245
+ }
246
+
247
+ return {
248
+ success: true,
249
+ content,
250
+ format,
251
+ block_count: allBlocks.length
252
+ };
253
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Search Tool
3
+ * Search across all pages and databases in the workspace
4
+ */
5
+
6
+ import { NotionClient } from '../utils/notion-client.js';
7
+ import { SearchResponse, getPageTitle, getDatabaseTitle, NotionPage, NotionDatabase } from '../utils/types.js';
8
+
9
+ export interface SearchParams {
10
+ query: string;
11
+ filter_type?: 'page' | 'database';
12
+ sort_direction?: 'ascending' | 'descending';
13
+ page_size?: number;
14
+ }
15
+
16
+ export interface SearchResult {
17
+ id: string;
18
+ type: 'page' | 'database';
19
+ title: string;
20
+ url: string;
21
+ last_edited_time: string;
22
+ }
23
+
24
+ export interface SearchToolResult {
25
+ success: boolean;
26
+ results?: SearchResult[];
27
+ total_count?: number;
28
+ has_more?: boolean;
29
+ error?: string;
30
+ suggestion?: string;
31
+ }
32
+
33
+ export async function search(client: NotionClient, params: SearchParams): Promise<SearchToolResult> {
34
+ const { query, filter_type, sort_direction = 'descending', page_size = 10 } = params;
35
+
36
+ const requestBody: Record<string, unknown> = {
37
+ query,
38
+ page_size: Math.min(Math.max(page_size, 1), 100)
39
+ };
40
+
41
+ if (filter_type) {
42
+ requestBody.filter = {
43
+ value: filter_type,
44
+ property: 'object'
45
+ };
46
+ }
47
+
48
+ requestBody.sort = {
49
+ direction: sort_direction,
50
+ timestamp: 'last_edited_time'
51
+ };
52
+
53
+ const response = await client.post<SearchResponse>('/search', requestBody);
54
+
55
+ if (!response.success || !response.data) {
56
+ return {
57
+ success: false,
58
+ error: response.error || 'Failed to search'
59
+ };
60
+ }
61
+
62
+ const results: SearchResult[] = response.data.results.map(item => {
63
+ if (item.object === 'page') {
64
+ const page = item as NotionPage;
65
+ return {
66
+ id: page.id,
67
+ type: 'page' as const,
68
+ title: getPageTitle(page),
69
+ url: page.url,
70
+ last_edited_time: page.last_edited_time
71
+ };
72
+ } else {
73
+ const database = item as NotionDatabase;
74
+ return {
75
+ id: database.id,
76
+ type: 'database' as const,
77
+ title: getDatabaseTitle(database),
78
+ url: database.url,
79
+ last_edited_time: database.last_edited_time
80
+ };
81
+ }
82
+ });
83
+
84
+ const result: SearchToolResult = {
85
+ success: true,
86
+ results,
87
+ total_count: results.length,
88
+ has_more: response.data.has_more
89
+ };
90
+
91
+ if (results.length === 0) {
92
+ result.suggestion = `No results found for "${query}". Try alternative search terms or check that the content is shared with the integration.`;
93
+ }
94
+
95
+ return result;
96
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * User Tools
3
+ * list_users, get_user
4
+ */
5
+
6
+ import { NotionClient, extractNotionId } from '../utils/notion-client.js';
7
+ import { NotionUser, PaginatedResponse } from '../utils/types.js';
8
+
9
+ // List Users
10
+ export interface ListUsersParams {
11
+ page_size?: number;
12
+ }
13
+
14
+ export interface UserInfo {
15
+ id: string;
16
+ type: 'person' | 'bot' | undefined;
17
+ name: string | undefined;
18
+ avatar_url: string | null | undefined;
19
+ email?: string;
20
+ }
21
+
22
+ export interface ListUsersResult {
23
+ success: boolean;
24
+ users?: UserInfo[];
25
+ total_count?: number;
26
+ has_more?: boolean;
27
+ error?: string;
28
+ }
29
+
30
+ export async function listUsers(client: NotionClient, params: ListUsersParams = {}): Promise<ListUsersResult> {
31
+ const { page_size = 100 } = params;
32
+
33
+ const endpoint = `/users?page_size=${Math.min(Math.max(page_size, 1), 100)}`;
34
+ const response = await client.get<PaginatedResponse<NotionUser>>(endpoint);
35
+
36
+ if (!response.success || !response.data) {
37
+ return {
38
+ success: false,
39
+ error: response.error || 'Failed to list users'
40
+ };
41
+ }
42
+
43
+ const users: UserInfo[] = response.data.results.map(user => ({
44
+ id: user.id,
45
+ type: user.type,
46
+ name: user.name,
47
+ avatar_url: user.avatar_url,
48
+ email: user.person?.email
49
+ }));
50
+
51
+ return {
52
+ success: true,
53
+ users,
54
+ total_count: users.length,
55
+ has_more: response.data.has_more
56
+ };
57
+ }
58
+
59
+ // Get User
60
+ export interface GetUserParams {
61
+ user_id: string;
62
+ }
63
+
64
+ export interface GetUserResult {
65
+ success: boolean;
66
+ user?: UserInfo;
67
+ error?: string;
68
+ }
69
+
70
+ export async function getUser(client: NotionClient, params: GetUserParams): Promise<GetUserResult> {
71
+ const userId = extractNotionId(params.user_id);
72
+ const response = await client.get<NotionUser>(`/users/${userId}`);
73
+
74
+ if (!response.success || !response.data) {
75
+ return {
76
+ success: false,
77
+ error: response.error || 'Failed to get user'
78
+ };
79
+ }
80
+
81
+ const user = response.data;
82
+
83
+ return {
84
+ success: true,
85
+ user: {
86
+ id: user.id,
87
+ type: user.type,
88
+ name: user.name,
89
+ avatar_url: user.avatar_url,
90
+ email: user.person?.email
91
+ }
92
+ };
93
+ }