@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.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +128 -0
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.js +36 -0
  5. package/dist/notion-client.d.ts +4 -0
  6. package/dist/notion-client.js +78 -0
  7. package/dist/tools/create-comment.d.ts +2 -0
  8. package/dist/tools/create-comment.js +54 -0
  9. package/dist/tools/create-database.d.ts +2 -0
  10. package/dist/tools/create-database.js +44 -0
  11. package/dist/tools/create-page.d.ts +2 -0
  12. package/dist/tools/create-page.js +60 -0
  13. package/dist/tools/duplicate-page.d.ts +2 -0
  14. package/dist/tools/duplicate-page.js +185 -0
  15. package/dist/tools/fetch-page.d.ts +2 -0
  16. package/dist/tools/fetch-page.js +120 -0
  17. package/dist/tools/get-comments.d.ts +2 -0
  18. package/dist/tools/get-comments.js +71 -0
  19. package/dist/tools/get-users.d.ts +2 -0
  20. package/dist/tools/get-users.js +86 -0
  21. package/dist/tools/move-pages.d.ts +2 -0
  22. package/dist/tools/move-pages.js +67 -0
  23. package/dist/tools/query-database.d.ts +2 -0
  24. package/dist/tools/query-database.js +53 -0
  25. package/dist/tools/search.d.ts +2 -0
  26. package/dist/tools/search.js +58 -0
  27. package/dist/tools/update-database.d.ts +2 -0
  28. package/dist/tools/update-database.js +71 -0
  29. package/dist/tools/update-page.d.ts +2 -0
  30. package/dist/tools/update-page.js +62 -0
  31. package/dist/utils/format-blocks.d.ts +1 -0
  32. package/dist/utils/format-blocks.js +234 -0
  33. package/dist/utils/format-properties.d.ts +1 -0
  34. package/dist/utils/format-properties.js +158 -0
  35. package/dist/utils/format-response.d.ts +3 -0
  36. package/dist/utils/format-response.js +85 -0
  37. package/package.json +50 -0
@@ -0,0 +1,120 @@
1
+ import { z } from 'zod';
2
+ import { getNotionClient, parseNotionId, formatApiError } from '../notion-client.js';
3
+ import { formatProperties } from '../utils/format-properties.js';
4
+ import { formatBlocks } from '../utils/format-blocks.js';
5
+ import { truncateIfNeeded } from '../utils/format-response.js';
6
+ async function fetchBlocksRecursive(notion, blockId, maxDepth, currentDepth) {
7
+ const response = await notion.blocks.children.list({ block_id: blockId, page_size: 100 });
8
+ const blocks = response.results ?? [];
9
+ if (currentDepth < maxDepth) {
10
+ for (const block of blocks) {
11
+ if (block.has_children) {
12
+ block._children = await fetchBlocksRecursive(notion, block.id, maxDepth, currentDepth + 1);
13
+ }
14
+ }
15
+ }
16
+ return blocks;
17
+ }
18
+ function flattenBlocks(blocks) {
19
+ const result = [];
20
+ for (const block of blocks) {
21
+ result.push(block);
22
+ if (block._children && block._children.length > 0) {
23
+ result.push(...flattenBlocks(block._children));
24
+ }
25
+ }
26
+ return result;
27
+ }
28
+ export function register(server) {
29
+ server.tool('fetch_page', '페이지 또는 데이터베이스의 상세 정보를 조회합니다. 프로퍼티와 콘텐츠(블록)를 함께 반환합니다.', {
30
+ page_id: z.string().describe('페이지 또는 데이터베이스 ID / Notion URL'),
31
+ include_children: z
32
+ .boolean()
33
+ .optional()
34
+ .default(true)
35
+ .describe('페이지 콘텐츠 블록 포함 여부 (기본값 true)'),
36
+ max_depth: z
37
+ .number()
38
+ .int()
39
+ .min(1)
40
+ .max(3)
41
+ .optional()
42
+ .default(2)
43
+ .describe('블록 재귀 깊이 (1-3, 기본값 2)'),
44
+ }, async ({ page_id, include_children, max_depth }) => {
45
+ try {
46
+ const notion = getNotionClient();
47
+ const parsedId = parseNotionId(page_id);
48
+ let pageData;
49
+ let objectType;
50
+ try {
51
+ pageData = await notion.pages.retrieve({ page_id: parsedId });
52
+ objectType = 'page';
53
+ }
54
+ catch (err) {
55
+ if (err?.status === 404 || err?.code === 'object_not_found') {
56
+ pageData = await notion.databases.retrieve({ database_id: parsedId });
57
+ objectType = 'database';
58
+ }
59
+ else {
60
+ throw err;
61
+ }
62
+ }
63
+ const lines = [];
64
+ // Title
65
+ const properties = pageData.properties ?? {};
66
+ let title = 'Untitled';
67
+ if (objectType === 'page') {
68
+ for (const prop of Object.values(properties)) {
69
+ if (prop?.type === 'title') {
70
+ const texts = prop.title ?? [];
71
+ title = texts.map((t) => t?.plain_text ?? '').join('') || 'Untitled';
72
+ break;
73
+ }
74
+ }
75
+ }
76
+ else {
77
+ const titleArr = pageData.title ?? [];
78
+ title = titleArr.map((t) => t?.plain_text ?? '').join('') || 'Untitled';
79
+ }
80
+ lines.push(`# ${title}`);
81
+ lines.push('');
82
+ lines.push(`**ID:** ${pageData.id}`);
83
+ lines.push(`**Type:** ${objectType}`);
84
+ lines.push(`**URL:** ${pageData.url ?? ''}`);
85
+ lines.push(`**Created:** ${pageData.created_time ?? ''}`);
86
+ lines.push(`**Last Edited:** ${pageData.last_edited_time ?? ''}`);
87
+ lines.push('');
88
+ // Properties
89
+ if (Object.keys(properties).length > 0) {
90
+ lines.push('## Properties');
91
+ const formattedProps = formatProperties(properties);
92
+ for (const [key, value] of Object.entries(formattedProps)) {
93
+ if (value) {
94
+ lines.push(`- **${key}:** ${value}`);
95
+ }
96
+ }
97
+ lines.push('');
98
+ }
99
+ // Content blocks
100
+ if (include_children && objectType === 'page') {
101
+ const blocks = await fetchBlocksRecursive(notion, parsedId, max_depth, 1);
102
+ const flatBlocks = flattenBlocks(blocks);
103
+ if (flatBlocks.length > 0) {
104
+ lines.push('## Content');
105
+ lines.push('');
106
+ lines.push(formatBlocks(flatBlocks));
107
+ }
108
+ }
109
+ return {
110
+ content: [{ type: 'text', text: truncateIfNeeded(lines.join('\n')) }],
111
+ };
112
+ }
113
+ catch (error) {
114
+ return {
115
+ content: [{ type: 'text', text: formatApiError(error) }],
116
+ isError: true,
117
+ };
118
+ }
119
+ });
120
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function register(server: McpServer): void;
@@ -0,0 +1,71 @@
1
+ import { z } from 'zod';
2
+ import { getNotionClient, parseNotionId, formatApiError } from '../notion-client.js';
3
+ function richTextToPlain(richText) {
4
+ if (!Array.isArray(richText))
5
+ return '';
6
+ return richText.map((t) => t?.plain_text ?? '').join('');
7
+ }
8
+ export function register(server) {
9
+ server.tool('get_comments', '페이지 또는 블록의 댓글을 조회합니다.', {
10
+ block_id: z.string().describe('댓글을 조회할 페이지 또는 블록 ID / URL'),
11
+ start_cursor: z.string().optional().describe('페이지네이션 커서 (선택)'),
12
+ page_size: z
13
+ .number()
14
+ .int()
15
+ .min(1)
16
+ .max(100)
17
+ .optional()
18
+ .default(50)
19
+ .describe('반환할 댓글 수 (1-100, 기본값 50)'),
20
+ }, async ({ block_id, start_cursor, page_size }) => {
21
+ try {
22
+ const notion = getNotionClient();
23
+ const parsedId = parseNotionId(block_id);
24
+ const params = {
25
+ block_id: parsedId,
26
+ page_size: page_size ?? 50,
27
+ };
28
+ if (start_cursor)
29
+ params['start_cursor'] = start_cursor;
30
+ const response = await notion.comments.list(params);
31
+ const comments = response?.results ?? [];
32
+ const hasMore = response?.has_more ?? false;
33
+ const nextCursor = response?.next_cursor ?? null;
34
+ if (comments.length === 0) {
35
+ return {
36
+ content: [{ type: 'text', text: '댓글이 없습니다.' }],
37
+ };
38
+ }
39
+ const lines = [
40
+ `## 댓글 목록 (${comments.length}개)`,
41
+ '',
42
+ ];
43
+ for (let i = 0; i < comments.length; i++) {
44
+ const comment = comments[i];
45
+ const author = comment?.created_by?.name ?? comment?.created_by?.id ?? '알 수 없음';
46
+ const createdTime = comment?.created_time ?? '';
47
+ const text = richTextToPlain(comment?.rich_text ?? []);
48
+ lines.push(`### ${i + 1}. ${author}`);
49
+ lines.push(`- 시간: ${createdTime}`);
50
+ lines.push(`- 내용: ${text}`);
51
+ lines.push(`- ID: ${comment?.id ?? ''}`);
52
+ if (comment?.discussion_id) {
53
+ lines.push(`- 토론 ID: ${comment.discussion_id}`);
54
+ }
55
+ lines.push('');
56
+ }
57
+ if (hasMore) {
58
+ lines.push(`*더 많은 댓글이 있습니다. Next cursor: ${nextCursor ?? ''}*`);
59
+ }
60
+ return {
61
+ content: [{ type: 'text', text: lines.join('\n').trim() }],
62
+ };
63
+ }
64
+ catch (error) {
65
+ return {
66
+ content: [{ type: 'text', text: formatApiError(error) }],
67
+ isError: true,
68
+ };
69
+ }
70
+ });
71
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function register(server: McpServer): void;
@@ -0,0 +1,86 @@
1
+ import { z } from 'zod';
2
+ import { getNotionClient, parseNotionId, formatApiError } from '../notion-client.js';
3
+ function formatUser(user) {
4
+ const lines = [];
5
+ const name = user?.name ?? '이름 없음';
6
+ const type = user?.type ?? 'unknown';
7
+ const id = user?.id ?? '';
8
+ const avatarUrl = user?.avatar_url ?? '';
9
+ lines.push(`- 이름: ${name}`);
10
+ lines.push(`- 유형: ${type}`);
11
+ lines.push(`- ID: ${id}`);
12
+ if (type === 'person') {
13
+ const email = user?.person?.email ?? '';
14
+ if (email)
15
+ lines.push(`- 이메일: ${email}`);
16
+ }
17
+ else if (type === 'bot') {
18
+ const ownerType = user?.bot?.owner?.type ?? '';
19
+ if (ownerType)
20
+ lines.push(`- 봇 소유자 유형: ${ownerType}`);
21
+ const workspaceName = user?.bot?.workspace_name ?? '';
22
+ if (workspaceName)
23
+ lines.push(`- 워크스페이스: ${workspaceName}`);
24
+ }
25
+ if (avatarUrl)
26
+ lines.push(`- 아바타: ${avatarUrl}`);
27
+ return lines;
28
+ }
29
+ export function register(server) {
30
+ server.tool('get_users', '워크스페이스 사용자 목록을 조회하거나 특정 사용자 정보를 가져옵니다.', {
31
+ user_id: z.string().optional().describe('특정 사용자 ID (선택, 생략 시 전체 목록)'),
32
+ start_cursor: z.string().optional().describe('페이지네이션 커서 (선택)'),
33
+ page_size: z
34
+ .number()
35
+ .int()
36
+ .min(1)
37
+ .max(100)
38
+ .optional()
39
+ .default(50)
40
+ .describe('반환할 사용자 수 (1-100, 기본값 50)'),
41
+ }, async ({ user_id, start_cursor, page_size }) => {
42
+ try {
43
+ const notion = getNotionClient();
44
+ if (user_id) {
45
+ const parsedId = parseNotionId(user_id);
46
+ const user = await notion.users.retrieve({ user_id: parsedId });
47
+ const lines = [`## 사용자 정보`, '', ...formatUser(user)];
48
+ return {
49
+ content: [{ type: 'text', text: lines.join('\n') }],
50
+ };
51
+ }
52
+ const params = {
53
+ page_size: page_size ?? 50,
54
+ };
55
+ if (start_cursor)
56
+ params['start_cursor'] = start_cursor;
57
+ const response = await notion.users.list(params);
58
+ const users = response?.results ?? [];
59
+ const hasMore = response?.has_more ?? false;
60
+ const nextCursor = response?.next_cursor ?? null;
61
+ if (users.length === 0) {
62
+ return {
63
+ content: [{ type: 'text', text: '사용자가 없습니다.' }],
64
+ };
65
+ }
66
+ const lines = [`## 사용자 목록 (${users.length}명)`, ''];
67
+ for (let i = 0; i < users.length; i++) {
68
+ lines.push(`### ${i + 1}. ${users[i]?.name ?? '이름 없음'}`);
69
+ lines.push(...formatUser(users[i]));
70
+ lines.push('');
71
+ }
72
+ if (hasMore) {
73
+ lines.push(`*더 많은 사용자가 있습니다. Next cursor: ${nextCursor ?? ''}*`);
74
+ }
75
+ return {
76
+ content: [{ type: 'text', text: lines.join('\n').trim() }],
77
+ };
78
+ }
79
+ catch (error) {
80
+ return {
81
+ content: [{ type: 'text', text: formatApiError(error) }],
82
+ isError: true,
83
+ };
84
+ }
85
+ });
86
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function register(server: McpServer): void;
@@ -0,0 +1,67 @@
1
+ import { z } from 'zod';
2
+ import { getNotionClient, parseNotionId, formatApiError } from '../notion-client.js';
3
+ export function register(server) {
4
+ server.tool('move_pages', '페이지를 다른 부모로 이동합니다.', {
5
+ page_ids: z
6
+ .array(z.string())
7
+ .min(1)
8
+ .max(50)
9
+ .describe('이동할 페이지 ID 목록 (최소 1개, 최대 50개)'),
10
+ parent_type: z
11
+ .enum(['page_id', 'database_id'])
12
+ .describe('새 부모의 유형: "page_id" 또는 "database_id"'),
13
+ parent_id: z.string().describe('새 부모 페이지 또는 데이터베이스 ID / URL'),
14
+ }, async ({ page_ids, parent_type, parent_id }) => {
15
+ try {
16
+ const notion = getNotionClient();
17
+ const parsedParentId = parseNotionId(parent_id);
18
+ const parent = parent_type === 'database_id'
19
+ ? { type: 'database_id', database_id: parsedParentId }
20
+ : { type: 'page_id', page_id: parsedParentId };
21
+ const results = [];
22
+ for (const rawPageId of page_ids) {
23
+ const parsedPageId = parseNotionId(rawPageId);
24
+ try {
25
+ await notion.pages.update({
26
+ page_id: parsedPageId,
27
+ parent: parent,
28
+ });
29
+ results.push({ id: parsedPageId, success: true, message: '이동 완료' });
30
+ }
31
+ catch (err) {
32
+ results.push({
33
+ id: parsedPageId,
34
+ success: false,
35
+ message: formatApiError(err),
36
+ });
37
+ }
38
+ }
39
+ const successCount = results.filter((r) => r.success).length;
40
+ const failCount = results.length - successCount;
41
+ const lines = [
42
+ `## 페이지 이동 결과`,
43
+ `- 성공: ${successCount}개 / 실패: ${failCount}개`,
44
+ `- 대상 부모: ${parent_type} = ${parsedParentId}`,
45
+ '',
46
+ ];
47
+ for (const result of results) {
48
+ const status = result.success ? '성공' : '실패';
49
+ lines.push(`- [${status}] ${result.id}: ${result.message}`);
50
+ }
51
+ if (failCount > 0) {
52
+ lines.push('');
53
+ lines.push('참고: Notion API는 페이지 부모 변경에 제한이 있을 수 있습니다. ' +
54
+ '특히 다른 워크스페이스나 공유 권한이 다른 페이지로의 이동은 지원되지 않습니다.');
55
+ }
56
+ return {
57
+ content: [{ type: 'text', text: lines.join('\n') }],
58
+ };
59
+ }
60
+ catch (error) {
61
+ return {
62
+ content: [{ type: 'text', text: formatApiError(error) }],
63
+ isError: true,
64
+ };
65
+ }
66
+ });
67
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function register(server: McpServer): void;
@@ -0,0 +1,53 @@
1
+ import { z } from 'zod';
2
+ import { getNotionClient, parseNotionId, formatApiError } from '../notion-client.js';
3
+ import { formatQueryResults, truncateIfNeeded } from '../utils/format-response.js';
4
+ export function register(server) {
5
+ server.tool('query_database', '데이터베이스를 필터, 정렬, 페이지네이션으로 쿼리합니다. 프로퍼티 기반 정밀 필터링이 가능합니다.', {
6
+ database_id: z.string().describe('데이터베이스 ID 또는 Notion URL'),
7
+ filter: z
8
+ .record(z.any())
9
+ .optional()
10
+ .describe('Notion 필터 객체. 단일: {"property": "Status", "select": {"equals": "Done"}}, ' +
11
+ '복합: {"and": [{"property": "Status", "select": {"equals": "Done"}}, ...]}'),
12
+ sorts: z
13
+ .array(z.record(z.any()))
14
+ .optional()
15
+ .describe('정렬 배열. 예: [{"property": "Name", "direction": "ascending"}] 또는 ' +
16
+ '[{"timestamp": "created_time", "direction": "descending"}]'),
17
+ start_cursor: z.string().optional().describe('페이지네이션 커서'),
18
+ page_size: z
19
+ .number()
20
+ .int()
21
+ .min(1)
22
+ .max(100)
23
+ .optional()
24
+ .default(50)
25
+ .describe('반환할 결과 수 (1-100, 기본값 50)'),
26
+ }, async ({ database_id, filter, sorts, start_cursor, page_size }) => {
27
+ try {
28
+ const notion = getNotionClient();
29
+ const parsedId = parseNotionId(database_id);
30
+ const queryParams = {
31
+ database_id: parsedId,
32
+ page_size,
33
+ };
34
+ if (filter !== undefined)
35
+ queryParams.filter = filter;
36
+ if (sorts !== undefined)
37
+ queryParams.sorts = sorts;
38
+ if (start_cursor !== undefined)
39
+ queryParams.start_cursor = start_cursor;
40
+ const response = await notion.databases.query(queryParams);
41
+ const formatted = formatQueryResults(response);
42
+ return {
43
+ content: [{ type: 'text', text: truncateIfNeeded(formatted) }],
44
+ };
45
+ }
46
+ catch (error) {
47
+ return {
48
+ content: [{ type: 'text', text: formatApiError(error) }],
49
+ isError: true,
50
+ };
51
+ }
52
+ });
53
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function register(server: McpServer): void;
@@ -0,0 +1,58 @@
1
+ import { z } from 'zod';
2
+ import { getNotionClient, formatApiError } from '../notion-client.js';
3
+ import { formatSearchResults, truncateIfNeeded } from '../utils/format-response.js';
4
+ export function register(server) {
5
+ server.tool('search', '워크스페이스에서 페이지/데이터베이스를 검색합니다.', {
6
+ query: z.string().optional().describe('검색어 (선택)'),
7
+ filter_object_type: z
8
+ .enum(['page', 'database'])
9
+ .optional()
10
+ .describe('검색 대상 타입 필터: "page" 또는 "database"'),
11
+ sort_direction: z
12
+ .enum(['ascending', 'descending'])
13
+ .optional()
14
+ .default('descending')
15
+ .describe('정렬 방향 (기본값 descending)'),
16
+ start_cursor: z.string().optional().describe('페이지네이션 커서'),
17
+ page_size: z
18
+ .number()
19
+ .int()
20
+ .min(1)
21
+ .max(100)
22
+ .optional()
23
+ .default(20)
24
+ .describe('반환할 결과 수 (1-100, 기본값 20)'),
25
+ }, async ({ query, filter_object_type, sort_direction, start_cursor, page_size }) => {
26
+ try {
27
+ const notion = getNotionClient();
28
+ const searchParams = {
29
+ page_size,
30
+ sort: {
31
+ direction: sort_direction,
32
+ timestamp: 'last_edited_time',
33
+ },
34
+ };
35
+ if (query !== undefined)
36
+ searchParams.query = query;
37
+ if (filter_object_type !== undefined) {
38
+ searchParams.filter = {
39
+ value: filter_object_type,
40
+ property: 'object',
41
+ };
42
+ }
43
+ if (start_cursor !== undefined)
44
+ searchParams.start_cursor = start_cursor;
45
+ const response = await notion.search(searchParams);
46
+ const formatted = formatSearchResults(response);
47
+ return {
48
+ content: [{ type: 'text', text: truncateIfNeeded(formatted) }],
49
+ };
50
+ }
51
+ catch (error) {
52
+ return {
53
+ content: [{ type: 'text', text: formatApiError(error) }],
54
+ isError: true,
55
+ };
56
+ }
57
+ });
58
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function register(server: McpServer): void;
@@ -0,0 +1,71 @@
1
+ import { z } from 'zod';
2
+ import { getNotionClient, parseNotionId, formatApiError } from '../notion-client.js';
3
+ export function register(server) {
4
+ server.tool('update_database', '데이터베이스의 제목, 설명, 프로퍼티 스키마를 수정합니다.', {
5
+ database_id: z.string().describe('수정할 데이터베이스 ID 또는 URL'),
6
+ title: z.string().optional().describe('새 데이터베이스 제목 (선택)'),
7
+ description: z.string().optional().describe('새 데이터베이스 설명 (선택)'),
8
+ properties: z
9
+ .record(z.any())
10
+ .optional()
11
+ .describe('프로퍼티 스키마 변경사항 (선택). 추가/수정: {"NewProp": {"select": {...}}}, 삭제: {"OldProp": null}'),
12
+ }, async ({ database_id, title, description, properties }) => {
13
+ try {
14
+ const notion = getNotionClient();
15
+ const parsedId = parseNotionId(database_id);
16
+ const updateParams = { database_id: parsedId };
17
+ if (title !== undefined) {
18
+ updateParams['title'] = [
19
+ { type: 'text', text: { content: title } },
20
+ ];
21
+ }
22
+ if (description !== undefined) {
23
+ updateParams['description'] = [
24
+ { type: 'text', text: { content: description } },
25
+ ];
26
+ }
27
+ if (properties !== undefined) {
28
+ updateParams['properties'] = properties;
29
+ }
30
+ const response = await notion.databases.update(updateParams);
31
+ const responseTitle = (() => {
32
+ const titleArr = response.title ?? [];
33
+ return titleArr.map((t) => t?.plain_text ?? '').join('') || '제목 없음';
34
+ })();
35
+ const responseDesc = (() => {
36
+ const descArr = response.description ?? [];
37
+ return descArr.map((t) => t?.plain_text ?? '').join('');
38
+ })();
39
+ const propCount = Object.keys(response.properties ?? {}).length;
40
+ const url = response.url ?? '';
41
+ const lines = [
42
+ `## 데이터베이스 업데이트 완료`,
43
+ `- ID: ${response.id}`,
44
+ `- 제목: ${responseTitle}`,
45
+ ];
46
+ if (responseDesc)
47
+ lines.push(`- 설명: ${responseDesc}`);
48
+ lines.push(`- 프로퍼티 수: ${propCount}`);
49
+ if (url)
50
+ lines.push(`- URL: ${url}`);
51
+ const changed = [];
52
+ if (title !== undefined)
53
+ changed.push('제목');
54
+ if (description !== undefined)
55
+ changed.push('설명');
56
+ if (properties !== undefined)
57
+ changed.push('프로퍼티 스키마');
58
+ if (changed.length > 0)
59
+ lines.push(`- 변경된 항목: ${changed.join(', ')}`);
60
+ return {
61
+ content: [{ type: 'text', text: lines.join('\n') }],
62
+ };
63
+ }
64
+ catch (error) {
65
+ return {
66
+ content: [{ type: 'text', text: formatApiError(error) }],
67
+ isError: true,
68
+ };
69
+ }
70
+ });
71
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function register(server: McpServer): void;
@@ -0,0 +1,62 @@
1
+ import { z } from 'zod';
2
+ import { getNotionClient, parseNotionId, formatApiError } from '../notion-client.js';
3
+ import { formatProperties } from '../utils/format-properties.js';
4
+ export function register(server) {
5
+ server.tool('update_page', '기존 페이지의 프로퍼티를 업데이트합니다. 아카이브/복원도 가능합니다.', {
6
+ page_id: z.string().describe('업데이트할 페이지 ID 또는 Notion URL'),
7
+ properties: z
8
+ .record(z.any())
9
+ .optional()
10
+ .describe('업데이트할 프로퍼티 객체 (변경할 항목만 포함)'),
11
+ archived: z
12
+ .boolean()
13
+ .optional()
14
+ .describe('true: 페이지 아카이브, false: 복원'),
15
+ icon: z
16
+ .record(z.any())
17
+ .optional()
18
+ .describe('아이콘 설정. 예: {"type": "emoji", "emoji": "🎯"}'),
19
+ cover: z
20
+ .record(z.any())
21
+ .optional()
22
+ .describe('커버 설정. 예: {"type": "external", "external": {"url": "https://..."}}'),
23
+ }, async ({ page_id, properties, archived, icon, cover }) => {
24
+ try {
25
+ const notion = getNotionClient();
26
+ const parsedId = parseNotionId(page_id);
27
+ const updateParams = { page_id: parsedId };
28
+ if (properties !== undefined)
29
+ updateParams.properties = properties;
30
+ if (archived !== undefined)
31
+ updateParams.archived = archived;
32
+ if (icon !== undefined)
33
+ updateParams.icon = icon;
34
+ if (cover !== undefined)
35
+ updateParams.cover = cover;
36
+ const response = await notion.pages.update(updateParams);
37
+ const formattedProps = formatProperties(response.properties ?? {});
38
+ const lines = [
39
+ '## 페이지 업데이트 완료',
40
+ `- ID: ${response.id}`,
41
+ `- URL: ${response.url ?? ''}`,
42
+ `- 수정 시간: ${response.last_edited_time ?? ''}`,
43
+ `- 아카이브 상태: ${response.archived ? '아카이브됨' : '활성'}`,
44
+ '',
45
+ '### 현재 프로퍼티',
46
+ ];
47
+ for (const [key, value] of Object.entries(formattedProps)) {
48
+ if (value)
49
+ lines.push(`- ${key}: ${value}`);
50
+ }
51
+ return {
52
+ content: [{ type: 'text', text: lines.join('\n') }],
53
+ };
54
+ }
55
+ catch (error) {
56
+ return {
57
+ content: [{ type: 'text', text: formatApiError(error) }],
58
+ isError: true,
59
+ };
60
+ }
61
+ });
62
+ }
@@ -0,0 +1 @@
1
+ export declare function formatBlocks(blocks: any[]): string;