@devheerim/notion-mcp-server 1.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.
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +36 -0
- package/dist/notion-client.d.ts +4 -0
- package/dist/notion-client.js +78 -0
- package/dist/tools/create-comment.d.ts +2 -0
- package/dist/tools/create-comment.js +54 -0
- package/dist/tools/create-database.d.ts +2 -0
- package/dist/tools/create-database.js +44 -0
- package/dist/tools/create-page.d.ts +2 -0
- package/dist/tools/create-page.js +60 -0
- package/dist/tools/duplicate-page.d.ts +2 -0
- package/dist/tools/duplicate-page.js +185 -0
- package/dist/tools/fetch-page.d.ts +2 -0
- package/dist/tools/fetch-page.js +120 -0
- package/dist/tools/get-comments.d.ts +2 -0
- package/dist/tools/get-comments.js +71 -0
- package/dist/tools/get-users.d.ts +2 -0
- package/dist/tools/get-users.js +86 -0
- package/dist/tools/move-pages.d.ts +2 -0
- package/dist/tools/move-pages.js +67 -0
- package/dist/tools/query-database.d.ts +2 -0
- package/dist/tools/query-database.js +53 -0
- package/dist/tools/search.d.ts +2 -0
- package/dist/tools/search.js +58 -0
- package/dist/tools/update-database.d.ts +2 -0
- package/dist/tools/update-database.js +71 -0
- package/dist/tools/update-page.d.ts +2 -0
- package/dist/tools/update-page.js +62 -0
- package/dist/utils/format-blocks.d.ts +1 -0
- package/dist/utils/format-blocks.js +234 -0
- package/dist/utils/format-properties.d.ts +1 -0
- package/dist/utils/format-properties.js +158 -0
- package/dist/utils/format-response.d.ts +3 -0
- package/dist/utils/format-response.js +85 -0
- package/package.json +50 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
export function formatBlocks(blocks) {
|
|
2
|
+
const lines = [];
|
|
3
|
+
let numberedListCounter = 0;
|
|
4
|
+
for (const block of blocks) {
|
|
5
|
+
if (!block || typeof block !== 'object')
|
|
6
|
+
continue;
|
|
7
|
+
const type = block.type;
|
|
8
|
+
// Reset numbered list counter when we hit a non-numbered block
|
|
9
|
+
if (type !== 'numbered_list_item') {
|
|
10
|
+
numberedListCounter = 0;
|
|
11
|
+
}
|
|
12
|
+
switch (type) {
|
|
13
|
+
case 'paragraph': {
|
|
14
|
+
const text = richTextToString(block.paragraph?.rich_text ?? []);
|
|
15
|
+
lines.push(text);
|
|
16
|
+
lines.push('');
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
case 'heading_1': {
|
|
20
|
+
const text = richTextToString(block.heading_1?.rich_text ?? []);
|
|
21
|
+
lines.push(`# ${text}`);
|
|
22
|
+
lines.push('');
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
case 'heading_2': {
|
|
26
|
+
const text = richTextToString(block.heading_2?.rich_text ?? []);
|
|
27
|
+
lines.push(`## ${text}`);
|
|
28
|
+
lines.push('');
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
case 'heading_3': {
|
|
32
|
+
const text = richTextToString(block.heading_3?.rich_text ?? []);
|
|
33
|
+
lines.push(`### ${text}`);
|
|
34
|
+
lines.push('');
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
case 'bulleted_list_item': {
|
|
38
|
+
const text = richTextToString(block.bulleted_list_item?.rich_text ?? []);
|
|
39
|
+
lines.push(`- ${text}`);
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
case 'numbered_list_item': {
|
|
43
|
+
numberedListCounter++;
|
|
44
|
+
const text = richTextToString(block.numbered_list_item?.rich_text ?? []);
|
|
45
|
+
lines.push(`${numberedListCounter}. ${text}`);
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
case 'to_do': {
|
|
49
|
+
const checked = block.to_do?.checked ?? false;
|
|
50
|
+
const text = richTextToString(block.to_do?.rich_text ?? []);
|
|
51
|
+
lines.push(`- [${checked ? 'x' : ' '}] ${text}`);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
case 'toggle': {
|
|
55
|
+
const text = richTextToString(block.toggle?.rich_text ?? []);
|
|
56
|
+
lines.push(`> ${text}`);
|
|
57
|
+
if (block.has_children) {
|
|
58
|
+
lines.push('> *(has children)*');
|
|
59
|
+
}
|
|
60
|
+
lines.push('');
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
case 'code': {
|
|
64
|
+
const text = richTextToString(block.code?.rich_text ?? []);
|
|
65
|
+
const language = block.code?.language ?? '';
|
|
66
|
+
lines.push('```' + language);
|
|
67
|
+
lines.push(text);
|
|
68
|
+
lines.push('```');
|
|
69
|
+
lines.push('');
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
case 'quote': {
|
|
73
|
+
const text = richTextToString(block.quote?.rich_text ?? []);
|
|
74
|
+
lines.push(`> ${text}`);
|
|
75
|
+
lines.push('');
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
case 'callout': {
|
|
79
|
+
const text = richTextToString(block.callout?.rich_text ?? []);
|
|
80
|
+
const emoji = block.callout?.icon?.emoji ?? '';
|
|
81
|
+
lines.push(`> ${emoji} ${text}`.trim());
|
|
82
|
+
lines.push('');
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
case 'divider': {
|
|
86
|
+
lines.push('---');
|
|
87
|
+
lines.push('');
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
case 'image': {
|
|
91
|
+
const image = block.image;
|
|
92
|
+
let url = '';
|
|
93
|
+
if (image?.type === 'external') {
|
|
94
|
+
url = image.external?.url ?? '';
|
|
95
|
+
}
|
|
96
|
+
else if (image?.type === 'file') {
|
|
97
|
+
url = image.file?.url ?? '';
|
|
98
|
+
}
|
|
99
|
+
const caption = richTextToString(image?.caption ?? []);
|
|
100
|
+
lines.push(``);
|
|
101
|
+
lines.push('');
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
case 'bookmark': {
|
|
105
|
+
const url = block.bookmark?.url ?? '';
|
|
106
|
+
const caption = richTextToString(block.bookmark?.caption ?? []);
|
|
107
|
+
lines.push(`[${caption || 'bookmark'}](${url})`);
|
|
108
|
+
lines.push('');
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
case 'table': {
|
|
112
|
+
// Table rows are nested as children; the table block itself just has metadata
|
|
113
|
+
// We emit a note; actual rows come as child blocks
|
|
114
|
+
lines.push('');
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case 'table_row': {
|
|
118
|
+
const cells = block.table_row?.cells ?? [];
|
|
119
|
+
const cellStrings = cells.map((cell) => richTextToString(cell));
|
|
120
|
+
lines.push(`| ${cellStrings.join(' | ')} |`);
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
case 'child_page': {
|
|
124
|
+
const title = block.child_page?.title ?? 'Untitled';
|
|
125
|
+
lines.push(`📄 Page: ${title}`);
|
|
126
|
+
lines.push('');
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case 'child_database': {
|
|
130
|
+
const title = block.child_database?.title ?? 'Untitled';
|
|
131
|
+
lines.push(`📊 Database: ${title}`);
|
|
132
|
+
lines.push('');
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
case 'embed': {
|
|
136
|
+
const url = block.embed?.url ?? '';
|
|
137
|
+
lines.push(`[embed](${url})`);
|
|
138
|
+
lines.push('');
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
case 'video': {
|
|
142
|
+
const video = block.video;
|
|
143
|
+
let url = '';
|
|
144
|
+
if (video?.type === 'external') {
|
|
145
|
+
url = video.external?.url ?? '';
|
|
146
|
+
}
|
|
147
|
+
else if (video?.type === 'file') {
|
|
148
|
+
url = video.file?.url ?? '';
|
|
149
|
+
}
|
|
150
|
+
lines.push(`[video](${url})`);
|
|
151
|
+
lines.push('');
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
case 'file': {
|
|
155
|
+
const file = block.file;
|
|
156
|
+
let url = '';
|
|
157
|
+
if (file?.type === 'external') {
|
|
158
|
+
url = file.external?.url ?? '';
|
|
159
|
+
}
|
|
160
|
+
else if (file?.type === 'file') {
|
|
161
|
+
url = file.file?.url ?? '';
|
|
162
|
+
}
|
|
163
|
+
lines.push(`[file](${url})`);
|
|
164
|
+
lines.push('');
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
case 'pdf': {
|
|
168
|
+
const pdf = block.pdf;
|
|
169
|
+
let url = '';
|
|
170
|
+
if (pdf?.type === 'external') {
|
|
171
|
+
url = pdf.external?.url ?? '';
|
|
172
|
+
}
|
|
173
|
+
else if (pdf?.type === 'file') {
|
|
174
|
+
url = pdf.file?.url ?? '';
|
|
175
|
+
}
|
|
176
|
+
lines.push(`[pdf](${url})`);
|
|
177
|
+
lines.push('');
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
case 'link_preview': {
|
|
181
|
+
const url = block.link_preview?.url ?? '';
|
|
182
|
+
lines.push(`[link](${url})`);
|
|
183
|
+
lines.push('');
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
case 'synced_block': {
|
|
187
|
+
// Children are the actual content; mark as synced
|
|
188
|
+
if (block.has_children) {
|
|
189
|
+
lines.push('*(synced block with children)*');
|
|
190
|
+
}
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
case 'column_list':
|
|
194
|
+
case 'column': {
|
|
195
|
+
// Children hold the actual content
|
|
196
|
+
if (block.has_children) {
|
|
197
|
+
lines.push('');
|
|
198
|
+
}
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
case 'equation': {
|
|
202
|
+
const expression = block.equation?.expression ?? '';
|
|
203
|
+
lines.push(`$${expression}$`);
|
|
204
|
+
lines.push('');
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
case 'table_of_contents': {
|
|
208
|
+
lines.push('[TOC]');
|
|
209
|
+
lines.push('');
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
case 'breadcrumb': {
|
|
213
|
+
// Skip breadcrumb blocks
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
case 'unsupported': {
|
|
217
|
+
lines.push('[unsupported block]');
|
|
218
|
+
lines.push('');
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
default: {
|
|
222
|
+
lines.push('[unsupported block]');
|
|
223
|
+
lines.push('');
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return lines.join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
229
|
+
}
|
|
230
|
+
function richTextToString(richText) {
|
|
231
|
+
if (!Array.isArray(richText))
|
|
232
|
+
return '';
|
|
233
|
+
return richText.map((t) => t?.plain_text ?? '').join('');
|
|
234
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function formatProperties(properties: Record<string, any>): Record<string, string>;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
export function formatProperties(properties) {
|
|
2
|
+
const result = {};
|
|
3
|
+
for (const [key, property] of Object.entries(properties)) {
|
|
4
|
+
result[key] = formatProperty(property);
|
|
5
|
+
}
|
|
6
|
+
return result;
|
|
7
|
+
}
|
|
8
|
+
function formatProperty(property) {
|
|
9
|
+
if (!property || typeof property !== 'object') {
|
|
10
|
+
return String(property ?? '');
|
|
11
|
+
}
|
|
12
|
+
const type = property.type;
|
|
13
|
+
switch (type) {
|
|
14
|
+
case 'title': {
|
|
15
|
+
const texts = property.title ?? [];
|
|
16
|
+
return texts.map((t) => t.plain_text ?? '').join('');
|
|
17
|
+
}
|
|
18
|
+
case 'rich_text': {
|
|
19
|
+
const texts = property.rich_text ?? [];
|
|
20
|
+
return texts.map((t) => t.plain_text ?? '').join('');
|
|
21
|
+
}
|
|
22
|
+
case 'number': {
|
|
23
|
+
const num = property.number;
|
|
24
|
+
return num === null || num === undefined ? '' : String(num);
|
|
25
|
+
}
|
|
26
|
+
case 'select': {
|
|
27
|
+
return property.select?.name ?? '';
|
|
28
|
+
}
|
|
29
|
+
case 'multi_select': {
|
|
30
|
+
const options = property.multi_select ?? [];
|
|
31
|
+
return options.map((o) => o.name ?? '').join(', ');
|
|
32
|
+
}
|
|
33
|
+
case 'date': {
|
|
34
|
+
const date = property.date;
|
|
35
|
+
if (!date)
|
|
36
|
+
return '';
|
|
37
|
+
if (date.end) {
|
|
38
|
+
return `${date.start} ~ ${date.end}`;
|
|
39
|
+
}
|
|
40
|
+
return date.start ?? '';
|
|
41
|
+
}
|
|
42
|
+
case 'people': {
|
|
43
|
+
const people = property.people ?? [];
|
|
44
|
+
return people
|
|
45
|
+
.map((p) => p.name ?? p.id ?? '')
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
.join(', ');
|
|
48
|
+
}
|
|
49
|
+
case 'files': {
|
|
50
|
+
const files = property.files ?? [];
|
|
51
|
+
return files
|
|
52
|
+
.map((f) => {
|
|
53
|
+
if (f.type === 'external')
|
|
54
|
+
return f.external?.url ?? '';
|
|
55
|
+
if (f.type === 'file')
|
|
56
|
+
return f.file?.url ?? '';
|
|
57
|
+
return '';
|
|
58
|
+
})
|
|
59
|
+
.filter(Boolean)
|
|
60
|
+
.join(', ');
|
|
61
|
+
}
|
|
62
|
+
case 'checkbox': {
|
|
63
|
+
return property.checkbox ? 'Yes' : 'No';
|
|
64
|
+
}
|
|
65
|
+
case 'url': {
|
|
66
|
+
return property.url ?? '';
|
|
67
|
+
}
|
|
68
|
+
case 'email': {
|
|
69
|
+
return property.email ?? '';
|
|
70
|
+
}
|
|
71
|
+
case 'phone_number': {
|
|
72
|
+
return property.phone_number ?? '';
|
|
73
|
+
}
|
|
74
|
+
case 'formula': {
|
|
75
|
+
const formula = property.formula;
|
|
76
|
+
if (!formula)
|
|
77
|
+
return '';
|
|
78
|
+
switch (formula.type) {
|
|
79
|
+
case 'string':
|
|
80
|
+
return formula.string ?? '';
|
|
81
|
+
case 'number':
|
|
82
|
+
return formula.number === null || formula.number === undefined
|
|
83
|
+
? ''
|
|
84
|
+
: String(formula.number);
|
|
85
|
+
case 'boolean':
|
|
86
|
+
return formula.boolean ? 'true' : 'false';
|
|
87
|
+
case 'date':
|
|
88
|
+
if (formula.date?.end) {
|
|
89
|
+
return `${formula.date.start} ~ ${formula.date.end}`;
|
|
90
|
+
}
|
|
91
|
+
return formula.date?.start ?? '';
|
|
92
|
+
default:
|
|
93
|
+
return '';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
case 'relation': {
|
|
97
|
+
const relations = property.relation ?? [];
|
|
98
|
+
return relations.map((r) => r.id ?? '').filter(Boolean).join(', ');
|
|
99
|
+
}
|
|
100
|
+
case 'rollup': {
|
|
101
|
+
const rollup = property.rollup;
|
|
102
|
+
if (!rollup)
|
|
103
|
+
return '';
|
|
104
|
+
switch (rollup.type) {
|
|
105
|
+
case 'number':
|
|
106
|
+
return rollup.number === null || rollup.number === undefined
|
|
107
|
+
? ''
|
|
108
|
+
: String(rollup.number);
|
|
109
|
+
case 'date':
|
|
110
|
+
if (rollup.date?.end) {
|
|
111
|
+
return `${rollup.date.start} ~ ${rollup.date.end}`;
|
|
112
|
+
}
|
|
113
|
+
return rollup.date?.start ?? '';
|
|
114
|
+
case 'array': {
|
|
115
|
+
const items = rollup.array ?? [];
|
|
116
|
+
return items
|
|
117
|
+
.map((item) => formatProperty(item))
|
|
118
|
+
.filter(Boolean)
|
|
119
|
+
.join(', ');
|
|
120
|
+
}
|
|
121
|
+
default:
|
|
122
|
+
return '';
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
case 'status': {
|
|
126
|
+
return property.status?.name ?? '';
|
|
127
|
+
}
|
|
128
|
+
case 'created_time': {
|
|
129
|
+
return property.created_time ?? '';
|
|
130
|
+
}
|
|
131
|
+
case 'created_by': {
|
|
132
|
+
return property.created_by?.name ?? property.created_by?.id ?? '';
|
|
133
|
+
}
|
|
134
|
+
case 'last_edited_time': {
|
|
135
|
+
return property.last_edited_time ?? '';
|
|
136
|
+
}
|
|
137
|
+
case 'last_edited_by': {
|
|
138
|
+
return property.last_edited_by?.name ?? property.last_edited_by?.id ?? '';
|
|
139
|
+
}
|
|
140
|
+
case 'unique_id': {
|
|
141
|
+
const uid = property.unique_id;
|
|
142
|
+
if (!uid)
|
|
143
|
+
return '';
|
|
144
|
+
if (uid.prefix) {
|
|
145
|
+
return `${uid.prefix}-${uid.number}`;
|
|
146
|
+
}
|
|
147
|
+
return String(uid.number ?? '');
|
|
148
|
+
}
|
|
149
|
+
default: {
|
|
150
|
+
try {
|
|
151
|
+
return JSON.stringify(property);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return '';
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { formatProperties } from './format-properties.js';
|
|
2
|
+
export function formatQueryResults(response) {
|
|
3
|
+
const results = response?.results ?? [];
|
|
4
|
+
const total = response?.total_count ?? results.length;
|
|
5
|
+
const hasMore = response?.has_more ?? false;
|
|
6
|
+
const nextCursor = response?.next_cursor ?? null;
|
|
7
|
+
const lines = [];
|
|
8
|
+
const showing = results.length;
|
|
9
|
+
lines.push(`## Query Results (${total} total, showing 1-${showing})`);
|
|
10
|
+
lines.push('');
|
|
11
|
+
for (let i = 0; i < results.length; i++) {
|
|
12
|
+
const page = results[i];
|
|
13
|
+
const properties = page?.properties ?? {};
|
|
14
|
+
const formattedProps = formatProperties(properties);
|
|
15
|
+
// Find the title property
|
|
16
|
+
const title = findTitle(properties, formattedProps);
|
|
17
|
+
lines.push(`### ${i + 1}. ${title || 'Untitled'}`);
|
|
18
|
+
for (const [key, value] of Object.entries(formattedProps)) {
|
|
19
|
+
if (value) {
|
|
20
|
+
lines.push(`- ${key}: ${value}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
lines.push(` ID: ${page?.id ?? ''}`);
|
|
24
|
+
lines.push('');
|
|
25
|
+
}
|
|
26
|
+
if (hasMore) {
|
|
27
|
+
lines.push(`*More results available. Next cursor: ${nextCursor ?? ''}*`);
|
|
28
|
+
}
|
|
29
|
+
return lines.join('\n').trim();
|
|
30
|
+
}
|
|
31
|
+
export function formatSearchResults(response) {
|
|
32
|
+
const results = response?.results ?? [];
|
|
33
|
+
const hasMore = response?.has_more ?? false;
|
|
34
|
+
const nextCursor = response?.next_cursor ?? null;
|
|
35
|
+
const lines = [];
|
|
36
|
+
lines.push(`## Search Results (${results.length} found)`);
|
|
37
|
+
lines.push('');
|
|
38
|
+
for (let i = 0; i < results.length; i++) {
|
|
39
|
+
const item = results[i];
|
|
40
|
+
const objectType = item?.object ?? 'unknown';
|
|
41
|
+
const id = item?.id ?? '';
|
|
42
|
+
const lastEdited = item?.last_edited_time ?? '';
|
|
43
|
+
let title = 'Untitled';
|
|
44
|
+
if (objectType === 'page') {
|
|
45
|
+
const properties = item?.properties ?? {};
|
|
46
|
+
title = extractPageTitle(properties) || item?.title?.[0]?.plain_text || 'Untitled';
|
|
47
|
+
}
|
|
48
|
+
else if (objectType === 'database') {
|
|
49
|
+
const titleArr = item?.title ?? [];
|
|
50
|
+
title = titleArr.map((t) => t?.plain_text ?? '').join('') || 'Untitled';
|
|
51
|
+
}
|
|
52
|
+
lines.push(`${i + 1}. **${title}** (${objectType})`);
|
|
53
|
+
lines.push(` Last edited: ${lastEdited} | ID: ${id}`);
|
|
54
|
+
lines.push('');
|
|
55
|
+
}
|
|
56
|
+
if (hasMore) {
|
|
57
|
+
lines.push(`*More results available. Next cursor: ${nextCursor ?? ''}*`);
|
|
58
|
+
}
|
|
59
|
+
return lines.join('\n').trim();
|
|
60
|
+
}
|
|
61
|
+
export function truncateIfNeeded(text, maxLength = 50000) {
|
|
62
|
+
if (text.length <= maxLength)
|
|
63
|
+
return text;
|
|
64
|
+
const truncated = text.slice(0, maxLength);
|
|
65
|
+
return truncated + `\n\n...[truncated, ${text.length - maxLength} characters omitted]`;
|
|
66
|
+
}
|
|
67
|
+
function findTitle(properties, formattedProps) {
|
|
68
|
+
// Try to find a 'title' type property first
|
|
69
|
+
for (const [key, prop] of Object.entries(properties)) {
|
|
70
|
+
if (prop?.type === 'title') {
|
|
71
|
+
return formattedProps[key] ?? '';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Fall back to a key literally named "Name" or "Title"
|
|
75
|
+
return formattedProps['Name'] ?? formattedProps['Title'] ?? '';
|
|
76
|
+
}
|
|
77
|
+
function extractPageTitle(properties) {
|
|
78
|
+
for (const prop of Object.values(properties)) {
|
|
79
|
+
if (prop?.type === 'title') {
|
|
80
|
+
const texts = prop.title ?? [];
|
|
81
|
+
return texts.map((t) => t?.plain_text ?? '').join('');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return '';
|
|
85
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@devheerim/notion-mcp-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Notion API MCP server with powerful database query support including filters, sorts, and pagination",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "devheerim",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/Henry-Hong/Notion-MCP.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/Henry-Hong/Notion-MCP#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/Henry-Hong/Notion-MCP/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"notion",
|
|
18
|
+
"mcp",
|
|
19
|
+
"model-context-protocol",
|
|
20
|
+
"notion-api",
|
|
21
|
+
"claude",
|
|
22
|
+
"ai",
|
|
23
|
+
"database",
|
|
24
|
+
"mcp-server"
|
|
25
|
+
],
|
|
26
|
+
"bin": {
|
|
27
|
+
"notion-mcp-server": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc",
|
|
36
|
+
"dev": "tsx src/index.ts",
|
|
37
|
+
"start": "node dist/index.js",
|
|
38
|
+
"prepublishOnly": "npm run build"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
42
|
+
"@notionhq/client": "^2.2.0",
|
|
43
|
+
"zod": "^3.25.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"typescript": "^5.5.0",
|
|
47
|
+
"tsx": "^4.19.0",
|
|
48
|
+
"@types/node": "^22.0.0"
|
|
49
|
+
}
|
|
50
|
+
}
|