@atikk-co-jp/notion-mcp-server 0.9.0 → 0.10.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/README.ja.md +69 -3
- package/README.md +64 -3
- package/dist/src/notion-client.d.ts +1 -2
- package/dist/src/notion-client.d.ts.map +1 -1
- package/dist/src/tools/__tests__/find-and-replace.test.d.ts +2 -0
- package/dist/src/tools/__tests__/find-and-replace.test.d.ts.map +1 -0
- package/dist/src/tools/__tests__/find-and-replace.test.js +92 -0
- package/dist/src/tools/archive-page.d.ts.map +1 -1
- package/dist/src/tools/create-page-simple.d.ts.map +1 -1
- package/dist/src/tools/create-page-simple.js +6 -2
- package/dist/src/tools/find-and-replace-in-page.d.ts +14 -0
- package/dist/src/tools/find-and-replace-in-page.d.ts.map +1 -0
- package/dist/src/tools/find-and-replace-in-page.js +203 -0
- package/dist/src/tools/index.d.ts +3 -1
- package/dist/src/tools/index.d.ts.map +1 -1
- package/dist/src/tools/index.js +5 -1
- package/dist/src/tools/replace-page-content.d.ts +4 -0
- package/dist/src/tools/replace-page-content.d.ts.map +1 -0
- package/dist/src/tools/replace-page-content.js +96 -0
- package/dist/src/tools/update-block-simple.d.ts.map +1 -1
- package/dist/src/tools/update-block-simple.js +7 -3
- package/package.json +1 -1
package/README.ja.md
CHANGED
|
@@ -4,9 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Notion API用のMCP(Model Context Protocol)サーバー。AIアシスタントがNotionのページ、データベース、ブロックを操作できるようにします。
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
> **APIバージョン**: 2025-09-03(最新)
|
|
7
|
+
**APIバージョン**: 2025-09-03(最新)
|
|
10
8
|
|
|
11
9
|
## 特徴
|
|
12
10
|
|
|
@@ -52,6 +50,9 @@ Notion API用のMCP(Model Context Protocol)サーバー。AIアシスタン
|
|
|
52
50
|
| | [Retrieve block children](https://developers.notion.com/reference/get-block-children) | `get-block-children` | JSON | **markdown**/json |
|
|
53
51
|
| | [Append block children](https://developers.notion.com/reference/patch-block-children) | `append-block-children` 📤 | JSON | `{block_ids}` |
|
|
54
52
|
| | | `append-blocks-simple` ⭐📤 | Markdown | `{block_ids}` |
|
|
53
|
+
| **ページコンテンツ** | | | | |
|
|
54
|
+
| | - | `replace-page-content` ⭐📤 | Markdown | `{deleted_count, created_count}` |
|
|
55
|
+
| | - | `find-and-replace-in-page` ⭐📤 | Markdown | `{updated_count, updated_block_ids}` |
|
|
55
56
|
| **コメント** | | | | |
|
|
56
57
|
| | [Create comment](https://developers.notion.com/reference/create-a-comment) | `create-comment` 📤 | JSON | `{id}` |
|
|
57
58
|
| | | `create-comment-simple` ⭐📤 | Markdown | `{id}` |
|
|
@@ -418,6 +419,71 @@ Markdownを使ってブロックを追加します。`append-block-children`と
|
|
|
418
419
|
}
|
|
419
420
|
```
|
|
420
421
|
|
|
422
|
+
### replace-page-content ⭐
|
|
423
|
+
|
|
424
|
+
ページの全コンテンツをMarkdownで置換します。`child_database`と`child_page`ブロックは自動的に保護されます(削除されません)。
|
|
425
|
+
|
|
426
|
+
**パラメータ:**
|
|
427
|
+
- `page_id` (必須): 更新するページのID
|
|
428
|
+
- `content` (必須): 新しいコンテンツ(Markdown形式)
|
|
429
|
+
- `dry_run` (任意): 削除されるブロックをプレビュー(実際には変更しない)(デフォルト: false)
|
|
430
|
+
|
|
431
|
+
**⚠️ 注意:** Markdownで表現できないブロック(bookmark, callout, equation, table_of_contents, synced_block等)は**削除されます**。実行前に `dry_run: true` でプレビューを確認してください。
|
|
432
|
+
|
|
433
|
+
**サポートするMarkdown記法:**
|
|
434
|
+
見出し(#)、リスト(- または 1.)、チェックボックス(- [ ])、コードブロック(```)、引用(>)、テーブル(| |)、画像(![]())、太字(**)、イタリック(*)、リンク([]())
|
|
435
|
+
|
|
436
|
+
```json
|
|
437
|
+
{
|
|
438
|
+
"page_id": "ページのUUID",
|
|
439
|
+
"content": "# 新しいタイトル\n\nこれは完全に新しいコンテンツです。\n\n## セクション1\n\n- 項目1\n- 項目2"
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
**削除プレビュー (dry run):**
|
|
444
|
+
```json
|
|
445
|
+
{
|
|
446
|
+
"page_id": "ページのUUID",
|
|
447
|
+
"content": "# 新しいコンテンツ",
|
|
448
|
+
"dry_run": true
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
**使い分け:**
|
|
453
|
+
- ページ全体を書き換えたい場合 → `replace-page-content`
|
|
454
|
+
- 特定のテキストだけ置換したい場合 → `find-and-replace-in-page`
|
|
455
|
+
- 単一ブロックを更新したい(block_idがわかる)場合 → `update-block-simple`
|
|
456
|
+
|
|
457
|
+
### find-and-replace-in-page ⭐
|
|
458
|
+
|
|
459
|
+
ページ内のテキストを検索して置換します。正規表現にも対応。
|
|
460
|
+
|
|
461
|
+
**パラメータ:**
|
|
462
|
+
- `page_id` (必須): 対象ページのID
|
|
463
|
+
- `find` (必須): 検索文字列(`use_regex: true`の場合は正規表現パターン)
|
|
464
|
+
- `replace` (必須): 置換テキスト(Markdown対応: **太字**, *イタリック*, [リンク](url)など)
|
|
465
|
+
- `use_regex` (任意): trueの場合、`find`を正規表現として解釈(デフォルト: false)
|
|
466
|
+
|
|
467
|
+
```json
|
|
468
|
+
{
|
|
469
|
+
"page_id": "ページのUUID",
|
|
470
|
+
"find": "古いテキスト",
|
|
471
|
+
"replace": "**新しいテキスト**"
|
|
472
|
+
}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
**正規表現の例:**
|
|
476
|
+
```json
|
|
477
|
+
{
|
|
478
|
+
"page_id": "ページのUUID",
|
|
479
|
+
"find": "item\\d+",
|
|
480
|
+
"replace": "アイテム",
|
|
481
|
+
"use_regex": true
|
|
482
|
+
}
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
**対象ブロックタイプ:** paragraph, heading_1/2/3, bulleted_list_item, numbered_list_item, to_do, quote, callout, toggle
|
|
486
|
+
|
|
421
487
|
## 開発
|
|
422
488
|
|
|
423
489
|
```bash
|
package/README.md
CHANGED
|
@@ -4,9 +4,7 @@ English | [日本語](./README.ja.md)
|
|
|
4
4
|
|
|
5
5
|
MCP (Model Context Protocol) server for Notion API. Enables AI assistants to interact with Notion pages, databases, and blocks.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
> **API Version**: 2025-09-03 (latest)
|
|
7
|
+
**API Version**: 2025-09-03 (latest)
|
|
10
8
|
|
|
11
9
|
## Features
|
|
12
10
|
|
|
@@ -52,6 +50,8 @@ MCP (Model Context Protocol) server for Notion API. Enables AI assistants to int
|
|
|
52
50
|
| | [Retrieve block children](https://developers.notion.com/reference/get-block-children) | `get-block-children` | JSON | **markdown**/json |
|
|
53
51
|
| | [Append block children](https://developers.notion.com/reference/patch-block-children) | `append-block-children` 📤 | JSON | `{block_ids}` |
|
|
54
52
|
| | | `append-blocks-simple` ⭐📤 | Markdown | `{block_ids}` |
|
|
53
|
+
| | | `replace-page-content` ⭐📤 | Markdown | `{deleted_count, created_count}` |
|
|
54
|
+
| | | `find-and-replace-in-page` ⭐📤 | Markdown | `{updated_count, updated_block_ids}` |
|
|
55
55
|
| **Comments** | | | | |
|
|
56
56
|
| | [Create comment](https://developers.notion.com/reference/create-a-comment) | `create-comment` 📤 | JSON | `{id}` |
|
|
57
57
|
| | | `create-comment-simple` ⭐📤 | Markdown | `{id}` |
|
|
@@ -407,6 +407,67 @@ Same Markdown support as `create-page-simple`.
|
|
|
407
407
|
| append-block-children (blocks) | ~201 | - |
|
|
408
408
|
| append-blocks-simple (markdown) | ~42 | **79%** |
|
|
409
409
|
|
|
410
|
+
### replace-page-content ⭐
|
|
411
|
+
|
|
412
|
+
Replace all content of a page with new Markdown content. Automatically preserves `child_database` and `child_page` blocks.
|
|
413
|
+
|
|
414
|
+
**Parameters:**
|
|
415
|
+
- `page_id` (required): The page ID to update
|
|
416
|
+
- `content` (required): New content in Markdown
|
|
417
|
+
- `dry_run` (optional): Preview which blocks will be deleted without making changes (default: false)
|
|
418
|
+
|
|
419
|
+
**⚠️ Warning:** Blocks not supported by Markdown (bookmark, callout, equation, table_of_contents, synced_block, etc.) will be **DELETED**. Use `dry_run: true` to preview before executing.
|
|
420
|
+
|
|
421
|
+
**Use when:** You want to completely rewrite page content without finding individual block IDs.
|
|
422
|
+
|
|
423
|
+
Same Markdown support as `create-page-simple`.
|
|
424
|
+
|
|
425
|
+
```json
|
|
426
|
+
{
|
|
427
|
+
"page_id": "page-uuid-here",
|
|
428
|
+
"content": "# New Page Title\n\nThis is the new content.\n\n## Section 1\n\n- Item 1\n- Item 2"
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
**Preview deletions (dry run):**
|
|
433
|
+
```json
|
|
434
|
+
{
|
|
435
|
+
"page_id": "page-uuid-here",
|
|
436
|
+
"content": "# New content",
|
|
437
|
+
"dry_run": true
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### find-and-replace-in-page ⭐
|
|
442
|
+
|
|
443
|
+
Find text in a page and replace it with new content. Supports regex patterns for advanced matching.
|
|
444
|
+
|
|
445
|
+
**Parameters:**
|
|
446
|
+
- `page_id` (required): The page ID to search in
|
|
447
|
+
- `find` (required): Text to find (string or regex pattern)
|
|
448
|
+
- `replace` (required): Replacement text (supports Markdown: `**bold**`, `*italic*`, etc.)
|
|
449
|
+
- `use_regex` (optional): If true, treat `find` as a regex pattern (default: false)
|
|
450
|
+
|
|
451
|
+
**Use when:** You want to update specific text without rewriting the entire page.
|
|
452
|
+
|
|
453
|
+
```json
|
|
454
|
+
{
|
|
455
|
+
"page_id": "page-uuid-here",
|
|
456
|
+
"find": "old text",
|
|
457
|
+
"replace": "**new text**"
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
**With regex:**
|
|
462
|
+
```json
|
|
463
|
+
{
|
|
464
|
+
"page_id": "page-uuid-here",
|
|
465
|
+
"find": "item\\d+",
|
|
466
|
+
"replace": "updated item",
|
|
467
|
+
"use_regex": true
|
|
468
|
+
}
|
|
469
|
+
```
|
|
470
|
+
|
|
410
471
|
### create-comment
|
|
411
472
|
|
|
412
473
|
Add a comment to a page.
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Client } from '@notionhq/client';
|
|
2
|
-
export type { AppendBlockChildrenParameters, AppendBlockChildrenResponse, BlockObjectRequest, BlockObjectResponse, BotUserObjectResponse, CommentObjectResponse, CreateCommentParameters, CreateCommentResponse, CreateDatabaseParameters, CreateDatabaseResponse, CreateDataSourceParameters, CreateDataSourceResponse, CreatePageParameters, CreatePageResponse, DatabaseObjectResponse, DataSourceObjectResponse, DeleteBlockParameters, DeleteBlockResponse, EquationRichTextItemResponse, GetBlockParameters, GetBlockResponse, GetDatabaseParameters, GetDatabaseResponse, GetDataSourceParameters, GetDataSourceResponse, GetPageParameters, GetPagePropertyParameters, GetPagePropertyResponse, GetPageResponse, GetSelfParameters, GetSelfResponse, GetUserParameters, GetUserResponse, ListBlockChildrenParameters, ListBlockChildrenResponse, ListCommentsParameters, ListCommentsResponse, ListUsersParameters, ListUsersResponse, MentionRichTextItemResponse, MovePageParameters, MovePageResponse, PageObjectResponse, PartialBlockObjectResponse, PartialCommentObjectResponse, PartialDatabaseObjectResponse, PartialDataSourceObjectResponse, PartialPageObjectResponse, PartialUserObjectResponse, PersonUserObjectResponse, PropertyItemListResponse, PropertyItemObjectResponse, QueryDataSourceParameters, QueryDataSourceResponse, RichTextItemResponse, SearchParameters, SearchResponse, TextRichTextItemResponse, UpdateBlockParameters, UpdateBlockResponse, UpdateDatabaseParameters, UpdateDatabaseResponse, UpdateDataSourceParameters, UpdateDataSourceResponse, UpdatePageParameters, UpdatePageResponse, UserObjectResponse, } from '@notionhq/client';
|
|
2
|
+
export type { AppendBlockChildrenParameters, AppendBlockChildrenResponse, BlockObjectRequest, BlockObjectResponse, BotUserObjectResponse, CommentObjectResponse, CreateCommentParameters, CreateCommentResponse, CreateDatabaseParameters, CreateDatabaseResponse, CreateDataSourceParameters, CreateDataSourceResponse, CreatePageParameters, CreatePageResponse, DatabaseObjectResponse, DataSourceObjectResponse, DeleteBlockParameters, DeleteBlockResponse, EquationRichTextItemResponse, GetBlockParameters, GetBlockResponse, GetDatabaseParameters, GetDatabaseResponse, GetDataSourceParameters, GetDataSourceResponse, GetPageParameters, GetPagePropertyParameters, GetPagePropertyResponse, GetPageResponse, GetSelfParameters, GetSelfResponse, GetUserParameters, GetUserResponse, ListBlockChildrenParameters, ListBlockChildrenResponse, ListCommentsParameters, ListCommentsResponse, ListUsersParameters, ListUsersResponse, MentionRichTextItemResponse, MovePageParameters, MovePageResponse, NotionClientError, PageObjectResponse, PartialBlockObjectResponse, PartialCommentObjectResponse, PartialDatabaseObjectResponse, PartialDataSourceObjectResponse, PartialPageObjectResponse, PartialUserObjectResponse, PersonUserObjectResponse, PropertyItemListResponse, PropertyItemObjectResponse, QueryDataSourceParameters, QueryDataSourceResponse, RichTextItemResponse, SearchParameters, SearchResponse, TextRichTextItemResponse, UpdateBlockParameters, UpdateBlockResponse, UpdateDatabaseParameters, UpdateDatabaseResponse, UpdateDataSourceParameters, UpdateDataSourceResponse, UpdatePageParameters, UpdatePageResponse, UserObjectResponse, } from '@notionhq/client';
|
|
3
3
|
export { APIResponseError, Client as NotionClient, collectPaginatedAPI, isFullBlock, isFullComment, isFullDatabase, isFullDataSource, isFullPage, isFullUser, isNotionClientError, iteratePaginatedAPI, } from '@notionhq/client';
|
|
4
|
-
export type { NotionClientError } from '@notionhq/client';
|
|
5
4
|
export interface NotionClientOptions {
|
|
6
5
|
token: string;
|
|
7
6
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notion-client.d.ts","sourceRoot":"","sources":["../../src/notion-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"notion-client.d.ts","sourceRoot":"","sources":["../../src/notion-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAKzC,YAAY,EACV,6BAA6B,EAC7B,2BAA2B,EAC3B,kBAAkB,EAClB,mBAAmB,EACnB,qBAAqB,EAErB,qBAAqB,EACrB,uBAAuB,EACvB,qBAAqB,EACrB,wBAAwB,EACxB,sBAAsB,EACtB,0BAA0B,EAC1B,wBAAwB,EACxB,oBAAoB,EACpB,kBAAkB,EAElB,sBAAsB,EAEtB,wBAAwB,EACxB,qBAAqB,EACrB,mBAAmB,EACnB,4BAA4B,EAC5B,kBAAkB,EAClB,gBAAgB,EAChB,qBAAqB,EACrB,mBAAmB,EACnB,uBAAuB,EACvB,qBAAqB,EACrB,iBAAiB,EACjB,yBAAyB,EACzB,uBAAuB,EACvB,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,2BAA2B,EAC3B,yBAAyB,EACzB,sBAAsB,EACtB,oBAAoB,EACpB,mBAAmB,EACnB,iBAAiB,EACjB,2BAA2B,EAC3B,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EAEjB,kBAAkB,EAClB,0BAA0B,EAC1B,4BAA4B,EAC5B,6BAA6B,EAC7B,+BAA+B,EAC/B,yBAAyB,EACzB,yBAAyB,EACzB,wBAAwB,EACxB,wBAAwB,EAExB,0BAA0B,EAC1B,yBAAyB,EACzB,uBAAuB,EAEvB,oBAAoB,EAEpB,gBAAgB,EAChB,cAAc,EACd,wBAAwB,EACxB,qBAAqB,EACrB,mBAAmB,EACnB,wBAAwB,EACxB,sBAAsB,EACtB,0BAA0B,EAC1B,wBAAwB,EACxB,oBAAoB,EACpB,kBAAkB,EAElB,kBAAkB,GACnB,MAAM,kBAAkB,CAAA;AAIzB,OAAO,EACL,gBAAgB,EAChB,MAAM,IAAI,YAAY,EACtB,mBAAmB,EACnB,WAAW,EACX,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,UAAU,EACV,UAAU,EACV,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,kBAAkB,CAAA;AAEzB,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAA;CACd;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAS3C"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"find-and-replace.test.d.ts","sourceRoot":"","sources":["../../../../src/tools/__tests__/find-and-replace.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { replaceText, textMatches } from '../find-and-replace-in-page.js';
|
|
3
|
+
describe('find-and-replace-in-page utilities', () => {
|
|
4
|
+
describe('textMatches', () => {
|
|
5
|
+
describe('string matching (useRegex: false)', () => {
|
|
6
|
+
it('should match exact substring', () => {
|
|
7
|
+
expect(textMatches('Hello World', 'World', false)).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
it('should match partial substring', () => {
|
|
10
|
+
expect(textMatches('Hello World', 'llo', false)).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
it('should return false when no match', () => {
|
|
13
|
+
expect(textMatches('Hello World', 'Foo', false)).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
it('should be case sensitive', () => {
|
|
16
|
+
expect(textMatches('Hello World', 'world', false)).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
it('should handle empty search string', () => {
|
|
19
|
+
expect(textMatches('Hello World', '', false)).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
it('should handle empty text', () => {
|
|
22
|
+
expect(textMatches('', 'Hello', false)).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
it('should handle Japanese text', () => {
|
|
25
|
+
expect(textMatches('こんにちは世界', '世界', false)).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
describe('regex matching (useRegex: true)', () => {
|
|
29
|
+
it('should match with simple regex', () => {
|
|
30
|
+
expect(textMatches('Hello World', 'World', true)).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
it('should match with regex pattern', () => {
|
|
33
|
+
expect(textMatches('Hello123World', '\\d+', true)).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
it('should match with character class', () => {
|
|
36
|
+
expect(textMatches('item1', 'item\\d', true)).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
it('should return false when regex does not match', () => {
|
|
39
|
+
expect(textMatches('Hello World', '^World', true)).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
it('should handle invalid regex gracefully (fallback to string match)', () => {
|
|
42
|
+
expect(textMatches('Hello [World', '[World', true)).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
it('should handle regex special characters', () => {
|
|
45
|
+
expect(textMatches('Hello.World', '\\.', true)).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe('replaceText', () => {
|
|
50
|
+
describe('string replacement (useRegex: false)', () => {
|
|
51
|
+
it('should replace exact match', () => {
|
|
52
|
+
expect(replaceText('Hello World', 'World', 'Universe', false)).toBe('Hello Universe');
|
|
53
|
+
});
|
|
54
|
+
it('should replace all occurrences', () => {
|
|
55
|
+
expect(replaceText('foo bar foo baz foo', 'foo', 'qux', false)).toBe('qux bar qux baz qux');
|
|
56
|
+
});
|
|
57
|
+
it('should handle no match', () => {
|
|
58
|
+
expect(replaceText('Hello World', 'Foo', 'Bar', false)).toBe('Hello World');
|
|
59
|
+
});
|
|
60
|
+
it('should handle empty replacement', () => {
|
|
61
|
+
expect(replaceText('Hello World', 'World', '', false)).toBe('Hello ');
|
|
62
|
+
});
|
|
63
|
+
it('should handle Japanese text', () => {
|
|
64
|
+
expect(replaceText('こんにちは世界', '世界', 'ワールド', false)).toBe('こんにちはワールド');
|
|
65
|
+
});
|
|
66
|
+
it('should handle markdown in replacement', () => {
|
|
67
|
+
expect(replaceText('Hello World', 'World', '**Universe**', false)).toBe('Hello **Universe**');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe('regex replacement (useRegex: true)', () => {
|
|
71
|
+
it('should replace with simple regex', () => {
|
|
72
|
+
expect(replaceText('Hello World', 'World', 'Universe', true)).toBe('Hello Universe');
|
|
73
|
+
});
|
|
74
|
+
it('should replace with pattern', () => {
|
|
75
|
+
expect(replaceText('item1 item2 item3', 'item\\d', 'x', true)).toBe('x x x');
|
|
76
|
+
});
|
|
77
|
+
it('should handle capture groups', () => {
|
|
78
|
+
expect(replaceText('Hello World', '(World)', '[$1]', true)).toBe('Hello [World]');
|
|
79
|
+
});
|
|
80
|
+
it('should handle invalid regex gracefully (fallback to string replace)', () => {
|
|
81
|
+
// Unclosed bracket is invalid regex, should fallback to string replacement
|
|
82
|
+
expect(replaceText('Hello [incomplete [incomplete', '[incomplete', 'X', true)).toBe('Hello X X');
|
|
83
|
+
});
|
|
84
|
+
it('should handle global replacement', () => {
|
|
85
|
+
expect(replaceText('aaa', 'a', 'b', true)).toBe('bbb');
|
|
86
|
+
});
|
|
87
|
+
it('should handle case-sensitive replacement', () => {
|
|
88
|
+
expect(replaceText('Hello HELLO hello', 'hello', 'hi', true)).toBe('Hello HELLO hi');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-page.d.ts","sourceRoot":"","sources":["../../../src/tools/archive-page.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAExE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAQvD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"archive-page.d.ts","sourceRoot":"","sources":["../../../src/tools/archive-page.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAExE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAQvD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,CAuBjF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-page-simple.d.ts","sourceRoot":"","sources":["../../../src/tools/create-page-simple.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAIxE,OAAO,EAAoB,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAiBzE,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"create-page-simple.d.ts","sourceRoot":"","sources":["../../../src/tools/create-page-simple.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAIxE,OAAO,EAAoB,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAiBzE,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,CA0EtF"}
|
|
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { markdownToBlocks } from '../converters/index.js';
|
|
3
3
|
import { isFullDataSource } from '../notion-client.js';
|
|
4
4
|
import { F } from '../schemas/descriptions/index.js';
|
|
5
|
-
import {
|
|
5
|
+
import { formatSimpleResponse, handleErrorWithContext } from '../utils/index.js';
|
|
6
6
|
// Minimal schema for MCP
|
|
7
7
|
const inputSchema = {
|
|
8
8
|
data_source_id: z.string().describe(F.data_source_id),
|
|
@@ -56,7 +56,11 @@ export function registerCreatePageSimple(server, notion) {
|
|
|
56
56
|
...(children && { children }),
|
|
57
57
|
...(icon && { icon: { type: 'emoji', emoji: icon } }),
|
|
58
58
|
});
|
|
59
|
-
|
|
59
|
+
// Return minimal response (id + url only)
|
|
60
|
+
return formatSimpleResponse({
|
|
61
|
+
id: response.id,
|
|
62
|
+
url: 'url' in response ? response.url : undefined,
|
|
63
|
+
});
|
|
60
64
|
}
|
|
61
65
|
catch (error) {
|
|
62
66
|
return handleErrorWithContext(error, notion, {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { NotionClient } from '../notion-client.js';
|
|
3
|
+
/**
|
|
4
|
+
* Replace text in string with regex/string matching
|
|
5
|
+
* @internal Exported for testing
|
|
6
|
+
*/
|
|
7
|
+
export declare function replaceText(text: string, find: string, replace: string, useRegex: boolean): string;
|
|
8
|
+
/**
|
|
9
|
+
* Check if text matches the find pattern
|
|
10
|
+
* @internal Exported for testing
|
|
11
|
+
*/
|
|
12
|
+
export declare function textMatches(text: string, find: string, useRegex: boolean): boolean;
|
|
13
|
+
export declare function registerFindAndReplaceInPage(server: McpServer, notion: NotionClient): void;
|
|
14
|
+
//# sourceMappingURL=find-and-replace-in-page.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"find-and-replace-in-page.d.ts","sourceRoot":"","sources":["../../../src/tools/find-and-replace-in-page.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAIxE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAyDvD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,CAWlG;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,OAAO,CAWlF;AA4CD,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,CAsG1F"}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { parseInlineMarkdown } from '../converters/index.js';
|
|
3
|
+
import { F } from '../schemas/descriptions/index.js';
|
|
4
|
+
import { formatSimpleResponse, handleError } from '../utils/index.js';
|
|
5
|
+
const inputSchema = {
|
|
6
|
+
page_id: z.string().describe(F.page_id),
|
|
7
|
+
find: z.string().describe('Text to find (string or regex pattern if use_regex is true)'),
|
|
8
|
+
replace: z.string().describe('Replacement text (supports Markdown: **bold**, *italic*, etc.)'),
|
|
9
|
+
use_regex: z
|
|
10
|
+
.boolean()
|
|
11
|
+
.optional()
|
|
12
|
+
.describe('If true, treat find as a regex pattern (default: false)'),
|
|
13
|
+
};
|
|
14
|
+
// Block types that support text replacement
|
|
15
|
+
const SUPPORTED_TYPES = [
|
|
16
|
+
'paragraph',
|
|
17
|
+
'heading_1',
|
|
18
|
+
'heading_2',
|
|
19
|
+
'heading_3',
|
|
20
|
+
'bulleted_list_item',
|
|
21
|
+
'numbered_list_item',
|
|
22
|
+
'to_do',
|
|
23
|
+
'quote',
|
|
24
|
+
'callout',
|
|
25
|
+
'toggle',
|
|
26
|
+
];
|
|
27
|
+
// ReDoS protection: timeout for regex execution
|
|
28
|
+
const REGEX_TIMEOUT_MS = 1000;
|
|
29
|
+
/**
|
|
30
|
+
* Extract plain text from rich_text array
|
|
31
|
+
*/
|
|
32
|
+
function extractPlainText(richText) {
|
|
33
|
+
return richText.map((rt) => rt.plain_text).join('');
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Execute regex with timeout protection (ReDoS mitigation)
|
|
37
|
+
*/
|
|
38
|
+
function safeRegexTest(pattern, text, timeoutMs = REGEX_TIMEOUT_MS) {
|
|
39
|
+
const start = Date.now();
|
|
40
|
+
try {
|
|
41
|
+
// Simple timeout check (not perfect but helps with obvious ReDoS)
|
|
42
|
+
const result = pattern.test(text);
|
|
43
|
+
if (Date.now() - start > timeoutMs) {
|
|
44
|
+
throw new Error('Regex execution timeout');
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Replace text in string with regex/string matching
|
|
54
|
+
* @internal Exported for testing
|
|
55
|
+
*/
|
|
56
|
+
export function replaceText(text, find, replace, useRegex) {
|
|
57
|
+
if (useRegex) {
|
|
58
|
+
try {
|
|
59
|
+
const regex = new RegExp(find, 'g');
|
|
60
|
+
return text.replace(regex, replace);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Invalid regex, fall back to string replace
|
|
64
|
+
return text.split(find).join(replace);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return text.split(find).join(replace);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Check if text matches the find pattern
|
|
71
|
+
* @internal Exported for testing
|
|
72
|
+
*/
|
|
73
|
+
export function textMatches(text, find, useRegex) {
|
|
74
|
+
if (useRegex) {
|
|
75
|
+
try {
|
|
76
|
+
const regex = new RegExp(find);
|
|
77
|
+
return safeRegexTest(regex, text);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Invalid regex, fall back to string match
|
|
81
|
+
return text.includes(find);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return text.includes(find);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Get all child blocks of a page (handles pagination)
|
|
88
|
+
*/
|
|
89
|
+
async function getAllChildBlocks(notion, pageId) {
|
|
90
|
+
const allBlocks = [];
|
|
91
|
+
let cursor;
|
|
92
|
+
do {
|
|
93
|
+
const response = await notion.blocks.children.list({
|
|
94
|
+
block_id: pageId,
|
|
95
|
+
start_cursor: cursor,
|
|
96
|
+
page_size: 100,
|
|
97
|
+
});
|
|
98
|
+
for (const block of response.results) {
|
|
99
|
+
if ('type' in block) {
|
|
100
|
+
allBlocks.push(block);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
cursor = response.has_more ? response.next_cursor ?? undefined : undefined;
|
|
104
|
+
} while (cursor);
|
|
105
|
+
return allBlocks;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Get rich_text from a block based on its type
|
|
109
|
+
*/
|
|
110
|
+
function getBlockRichText(block) {
|
|
111
|
+
const blockType = block.type;
|
|
112
|
+
if (!SUPPORTED_TYPES.includes(blockType)) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const blockData = block[blockType];
|
|
116
|
+
return blockData?.rich_text ?? null;
|
|
117
|
+
}
|
|
118
|
+
export function registerFindAndReplaceInPage(server, notion) {
|
|
119
|
+
server.registerTool('find-and-replace-in-page', {
|
|
120
|
+
description: 'Find text in a page and replace it with new content. ' +
|
|
121
|
+
'Supports regex patterns (use_regex: true) for advanced matching (e.g., "item\\d+" matches item1, item2). ' +
|
|
122
|
+
'All matching blocks will be updated. ' +
|
|
123
|
+
'Replacement text supports Markdown formatting: **bold**, *italic*, ~~strikethrough~~, `code`, [links](url). ' +
|
|
124
|
+
'Only works with text-based blocks: paragraph, headings, lists, to_do, quote, callout, toggle. ' +
|
|
125
|
+
'Use this for partial updates. For full page rewrite, use replace-page-content instead.',
|
|
126
|
+
inputSchema,
|
|
127
|
+
}, async ({ page_id, find, replace, use_regex }) => {
|
|
128
|
+
try {
|
|
129
|
+
const useRegex = use_regex ?? false;
|
|
130
|
+
// Validate regex if enabled
|
|
131
|
+
if (useRegex) {
|
|
132
|
+
try {
|
|
133
|
+
new RegExp(find);
|
|
134
|
+
}
|
|
135
|
+
catch (e) {
|
|
136
|
+
return {
|
|
137
|
+
content: [
|
|
138
|
+
{
|
|
139
|
+
type: 'text',
|
|
140
|
+
text: `Error: Invalid regex pattern: ${find}. ${e instanceof Error ? e.message : ''}`,
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
isError: true,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// 1. Get all blocks from the page
|
|
148
|
+
const allBlocks = await getAllChildBlocks(notion, page_id);
|
|
149
|
+
// 2. Find blocks that match the search pattern
|
|
150
|
+
const matchingBlocks = [];
|
|
151
|
+
for (const block of allBlocks) {
|
|
152
|
+
const richText = getBlockRichText(block);
|
|
153
|
+
if (!richText)
|
|
154
|
+
continue;
|
|
155
|
+
const plainText = extractPlainText(richText);
|
|
156
|
+
if (textMatches(plainText, find, useRegex)) {
|
|
157
|
+
const newText = replaceText(plainText, find, replace, useRegex);
|
|
158
|
+
matchingBlocks.push({
|
|
159
|
+
block,
|
|
160
|
+
originalText: plainText,
|
|
161
|
+
newText,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (matchingBlocks.length === 0) {
|
|
166
|
+
return formatSimpleResponse({
|
|
167
|
+
updated_count: 0,
|
|
168
|
+
message: 'No matching blocks found',
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
// 3. Update matching blocks
|
|
172
|
+
const updatedIds = [];
|
|
173
|
+
for (const { block, newText } of matchingBlocks) {
|
|
174
|
+
const blockType = block.type;
|
|
175
|
+
const richText = parseInlineMarkdown(newText);
|
|
176
|
+
const params = {
|
|
177
|
+
block_id: block.id,
|
|
178
|
+
};
|
|
179
|
+
// Preserve checked state for to_do blocks
|
|
180
|
+
if (blockType === 'to_do' && block.type === 'to_do') {
|
|
181
|
+
params[blockType] = {
|
|
182
|
+
rich_text: richText,
|
|
183
|
+
checked: block.to_do.checked ?? false,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
params[blockType] = {
|
|
188
|
+
rich_text: richText,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
await notion.blocks.update(params);
|
|
192
|
+
updatedIds.push(block.id);
|
|
193
|
+
}
|
|
194
|
+
return formatSimpleResponse({
|
|
195
|
+
updated_count: updatedIds.length,
|
|
196
|
+
updated_block_ids: updatedIds,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
return handleError(error);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
@@ -10,6 +10,8 @@ import { registerCreateDatabase } from './create-database.js';
|
|
|
10
10
|
import { registerCreatePage } from './create-page.js';
|
|
11
11
|
import { registerCreatePageSimple } from './create-page-simple.js';
|
|
12
12
|
import { registerDeleteBlock } from './delete-block.js';
|
|
13
|
+
import { registerFindAndReplaceInPage } from './find-and-replace-in-page.js';
|
|
14
|
+
import { registerReplacePageContent } from './replace-page-content.js';
|
|
13
15
|
import { registerGetBlockChildren } from './get-block-children.js';
|
|
14
16
|
import { registerListComments } from './list-comments.js';
|
|
15
17
|
import { registerListUsers } from './list-users.js';
|
|
@@ -29,5 +31,5 @@ import { registerUpdateDataSource } from './update-data-source.js';
|
|
|
29
31
|
import { registerUpdateDatabase } from './update-database.js';
|
|
30
32
|
import { registerUpdatePage } from './update-page.js';
|
|
31
33
|
export declare function registerAllTools(server: McpServer, notion: NotionClient): void;
|
|
32
|
-
export { registerRetrievePage, registerCreatePage, registerCreatePageSimple, registerUpdatePage, registerArchivePage, registerRetrievePageProperty, registerMovePage, registerCreateDatabase, registerUpdateDatabase, registerArchiveDatabase, registerRetrieveDatabase, registerRetrieveDataSource, registerQueryDataSource, registerUpdateDataSource, registerSearch, registerGetBlockChildren, registerAppendBlockChildren, registerAppendBlocksSimple, registerRetrieveBlock, registerUpdateBlock, registerUpdateBlockSimple, registerDeleteBlock, registerCreateComment, registerCreateCommentSimple, registerListComments, registerListUsers, registerRetrieveUser, registerRetrieveBotUser, };
|
|
34
|
+
export { registerRetrievePage, registerCreatePage, registerCreatePageSimple, registerUpdatePage, registerArchivePage, registerRetrievePageProperty, registerMovePage, registerCreateDatabase, registerUpdateDatabase, registerArchiveDatabase, registerRetrieveDatabase, registerRetrieveDataSource, registerQueryDataSource, registerUpdateDataSource, registerSearch, registerGetBlockChildren, registerAppendBlockChildren, registerAppendBlocksSimple, registerRetrieveBlock, registerUpdateBlock, registerUpdateBlockSimple, registerDeleteBlock, registerReplacePageContent, registerFindAndReplaceInPage, registerCreateComment, registerCreateCommentSimple, registerListComments, registerListUsers, registerRetrieveUser, registerRetrieveBotUser, };
|
|
33
35
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AACxE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAA;AACxE,OAAO,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAA;AACtE,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAA;AAC/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AACvD,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AAC3D,OAAO,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAA;AACxE,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAA;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AACrD,OAAO,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAA;AAClE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AACvD,OAAO,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAA;AAClE,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AACnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AACjD,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAA;AAChE,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AAC3D,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAA;AAChE,OAAO,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAA;AACtE,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAA;AACjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,EAAE,4BAA4B,EAAE,MAAM,6BAA6B,CAAA;AAC1E,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAC5C,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AACvD,OAAO,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAA;AACpE,OAAO,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAA;AAClE,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAA;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAErD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AACxE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAA;AACxE,OAAO,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAA;AACtE,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAA;AAC/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AACvD,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AAC3D,OAAO,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAA;AACxE,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAA;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AACrD,OAAO,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAA;AAClE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AACvD,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAA;AAC5E,OAAO,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAA;AACtE,OAAO,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAA;AAClE,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AACnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AACjD,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAA;AAChE,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AAC3D,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAA;AAChE,OAAO,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAA;AACtE,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAA;AACjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,EAAE,4BAA4B,EAAE,MAAM,6BAA6B,CAAA;AAC1E,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAC5C,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AACvD,OAAO,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAA;AACpE,OAAO,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAA;AAClE,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAA;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAErD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,CA4C9E;AAED,OAAO,EACL,oBAAoB,EACpB,kBAAkB,EAClB,wBAAwB,EACxB,kBAAkB,EAClB,mBAAmB,EACnB,4BAA4B,EAC5B,gBAAgB,EAChB,sBAAsB,EACtB,sBAAsB,EACtB,uBAAuB,EACvB,wBAAwB,EACxB,0BAA0B,EAC1B,uBAAuB,EACvB,wBAAwB,EACxB,cAAc,EACd,wBAAwB,EACxB,2BAA2B,EAC3B,0BAA0B,EAC1B,qBAAqB,EACrB,mBAAmB,EACnB,yBAAyB,EACzB,mBAAmB,EACnB,0BAA0B,EAC1B,4BAA4B,EAC5B,qBAAqB,EACrB,2BAA2B,EAC3B,oBAAoB,EACpB,iBAAiB,EACjB,oBAAoB,EACpB,uBAAuB,GACxB,CAAA"}
|
package/dist/src/tools/index.js
CHANGED
|
@@ -8,6 +8,8 @@ import { registerCreateDatabase } from './create-database.js';
|
|
|
8
8
|
import { registerCreatePage } from './create-page.js';
|
|
9
9
|
import { registerCreatePageSimple } from './create-page-simple.js';
|
|
10
10
|
import { registerDeleteBlock } from './delete-block.js';
|
|
11
|
+
import { registerFindAndReplaceInPage } from './find-and-replace-in-page.js';
|
|
12
|
+
import { registerReplacePageContent } from './replace-page-content.js';
|
|
11
13
|
import { registerGetBlockChildren } from './get-block-children.js';
|
|
12
14
|
import { registerListComments } from './list-comments.js';
|
|
13
15
|
import { registerListUsers } from './list-users.js';
|
|
@@ -54,6 +56,8 @@ export function registerAllTools(server, notion) {
|
|
|
54
56
|
registerUpdateBlock(server, notion);
|
|
55
57
|
registerUpdateBlockSimple(server, notion);
|
|
56
58
|
registerDeleteBlock(server, notion);
|
|
59
|
+
registerReplacePageContent(server, notion);
|
|
60
|
+
registerFindAndReplaceInPage(server, notion);
|
|
57
61
|
// Comment operations
|
|
58
62
|
registerCreateComment(server, notion);
|
|
59
63
|
registerCreateCommentSimple(server, notion);
|
|
@@ -63,4 +67,4 @@ export function registerAllTools(server, notion) {
|
|
|
63
67
|
registerRetrieveUser(server, notion);
|
|
64
68
|
registerRetrieveBotUser(server, notion);
|
|
65
69
|
}
|
|
66
|
-
export { registerRetrievePage, registerCreatePage, registerCreatePageSimple, registerUpdatePage, registerArchivePage, registerRetrievePageProperty, registerMovePage, registerCreateDatabase, registerUpdateDatabase, registerArchiveDatabase, registerRetrieveDatabase, registerRetrieveDataSource, registerQueryDataSource, registerUpdateDataSource, registerSearch, registerGetBlockChildren, registerAppendBlockChildren, registerAppendBlocksSimple, registerRetrieveBlock, registerUpdateBlock, registerUpdateBlockSimple, registerDeleteBlock, registerCreateComment, registerCreateCommentSimple, registerListComments, registerListUsers, registerRetrieveUser, registerRetrieveBotUser, };
|
|
70
|
+
export { registerRetrievePage, registerCreatePage, registerCreatePageSimple, registerUpdatePage, registerArchivePage, registerRetrievePageProperty, registerMovePage, registerCreateDatabase, registerUpdateDatabase, registerArchiveDatabase, registerRetrieveDatabase, registerRetrieveDataSource, registerQueryDataSource, registerUpdateDataSource, registerSearch, registerGetBlockChildren, registerAppendBlockChildren, registerAppendBlocksSimple, registerRetrieveBlock, registerUpdateBlock, registerUpdateBlockSimple, registerDeleteBlock, registerReplacePageContent, registerFindAndReplaceInPage, registerCreateComment, registerCreateCommentSimple, registerListComments, registerListUsers, registerRetrieveUser, registerRetrieveBotUser, };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { NotionClient } from '../notion-client.js';
|
|
3
|
+
export declare function registerReplacePageContent(server: McpServer, notion: NotionClient): void;
|
|
4
|
+
//# sourceMappingURL=replace-page-content.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replace-page-content.d.ts","sourceRoot":"","sources":["../../../src/tools/replace-page-content.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAIxE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AA+CvD,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,CA+ExF"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { markdownToBlocks } from '../converters/index.js';
|
|
3
|
+
import { F } from '../schemas/descriptions/index.js';
|
|
4
|
+
import { formatSimpleResponse, handleError } from '../utils/index.js';
|
|
5
|
+
const inputSchema = {
|
|
6
|
+
page_id: z.string().describe(F.page_id),
|
|
7
|
+
content: z.string().describe(F.content),
|
|
8
|
+
dry_run: z
|
|
9
|
+
.boolean()
|
|
10
|
+
.optional()
|
|
11
|
+
.describe('Preview mode: shows which blocks will be deleted without actually modifying the page (default: false)'),
|
|
12
|
+
};
|
|
13
|
+
// Block types to preserve (not delete)
|
|
14
|
+
const PRESERVE_TYPES = ['child_database', 'child_page'];
|
|
15
|
+
/**
|
|
16
|
+
* Get all child blocks of a page (handles pagination)
|
|
17
|
+
*/
|
|
18
|
+
async function getAllChildBlocks(notion, pageId) {
|
|
19
|
+
const allBlocks = [];
|
|
20
|
+
let cursor;
|
|
21
|
+
do {
|
|
22
|
+
const response = await notion.blocks.children.list({
|
|
23
|
+
block_id: pageId,
|
|
24
|
+
start_cursor: cursor,
|
|
25
|
+
page_size: 100,
|
|
26
|
+
});
|
|
27
|
+
for (const block of response.results) {
|
|
28
|
+
if ('type' in block) {
|
|
29
|
+
allBlocks.push(block);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
cursor = response.has_more ? response.next_cursor ?? undefined : undefined;
|
|
33
|
+
} while (cursor);
|
|
34
|
+
return allBlocks;
|
|
35
|
+
}
|
|
36
|
+
export function registerReplacePageContent(server, notion) {
|
|
37
|
+
server.registerTool('replace-page-content', {
|
|
38
|
+
description: 'Replace all page content with Markdown. Preserves child_database/child_page only. ' +
|
|
39
|
+
'WARNING: Non-Markdown blocks (bookmark, callout, equation, etc.) will be DELETED. ' +
|
|
40
|
+
'Use dry_run: true to preview deletions. For partial updates, use find-and-replace-in-page.',
|
|
41
|
+
inputSchema,
|
|
42
|
+
}, async ({ page_id, content, dry_run }) => {
|
|
43
|
+
try {
|
|
44
|
+
const isDryRun = dry_run ?? false;
|
|
45
|
+
// 1. Get all existing blocks
|
|
46
|
+
const existingBlocks = await getAllChildBlocks(notion, page_id);
|
|
47
|
+
// 2. Identify blocks to delete (not in PRESERVE_TYPES)
|
|
48
|
+
const blocksToDelete = existingBlocks.filter((block) => !PRESERVE_TYPES.includes(block.type));
|
|
49
|
+
// 3. Identify blocks to preserve
|
|
50
|
+
const blocksToPreserve = existingBlocks.filter((block) => PRESERVE_TYPES.includes(block.type));
|
|
51
|
+
// If dry_run, return preview without making changes
|
|
52
|
+
if (isDryRun) {
|
|
53
|
+
// Group blocks by type for readable output
|
|
54
|
+
const deleteByType = {};
|
|
55
|
+
for (const block of blocksToDelete) {
|
|
56
|
+
deleteByType[block.type] = (deleteByType[block.type] ?? 0) + 1;
|
|
57
|
+
}
|
|
58
|
+
const preserveByType = {};
|
|
59
|
+
for (const block of blocksToPreserve) {
|
|
60
|
+
preserveByType[block.type] = (preserveByType[block.type] ?? 0) + 1;
|
|
61
|
+
}
|
|
62
|
+
return formatSimpleResponse({
|
|
63
|
+
dry_run: true,
|
|
64
|
+
will_delete_count: blocksToDelete.length,
|
|
65
|
+
will_delete_by_type: deleteByType,
|
|
66
|
+
will_preserve_count: blocksToPreserve.length,
|
|
67
|
+
will_preserve_by_type: preserveByType,
|
|
68
|
+
message: 'No changes made. Set dry_run: false to execute.',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
// 4. Actually delete blocks
|
|
72
|
+
const deletedIds = [];
|
|
73
|
+
for (const block of blocksToDelete) {
|
|
74
|
+
await notion.blocks.delete({ block_id: block.id });
|
|
75
|
+
deletedIds.push(block.id);
|
|
76
|
+
}
|
|
77
|
+
// 5. Convert markdown to blocks
|
|
78
|
+
const newBlocks = markdownToBlocks(content);
|
|
79
|
+
// 6. Append new blocks
|
|
80
|
+
const appendResponse = await notion.blocks.children.append({
|
|
81
|
+
block_id: page_id,
|
|
82
|
+
children: newBlocks,
|
|
83
|
+
});
|
|
84
|
+
const createdIds = appendResponse.results.map((block) => block.id);
|
|
85
|
+
// Return minimal response
|
|
86
|
+
return formatSimpleResponse({
|
|
87
|
+
deleted_count: deletedIds.length,
|
|
88
|
+
created_count: createdIds.length,
|
|
89
|
+
preserved_types: PRESERVE_TYPES,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
return handleError(error);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update-block-simple.d.ts","sourceRoot":"","sources":["../../../src/tools/update-block-simple.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAGxE,OAAO,EAAe,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAyBpE,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"update-block-simple.d.ts","sourceRoot":"","sources":["../../../src/tools/update-block-simple.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAGxE,OAAO,EAAe,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAyBpE,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,CAsEvF"}
|
|
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { parseInlineMarkdown } from '../converters/index.js';
|
|
3
3
|
import { isFullBlock } from '../notion-client.js';
|
|
4
4
|
import { F } from '../schemas/descriptions/index.js';
|
|
5
|
-
import {
|
|
5
|
+
import { formatSimpleResponse, handleError } from '../utils/index.js';
|
|
6
6
|
const inputSchema = {
|
|
7
7
|
block_id: z.string().describe(F.block_id),
|
|
8
8
|
content: z.string().describe(F.content),
|
|
@@ -24,7 +24,8 @@ export function registerUpdateBlockSimple(server, notion) {
|
|
|
24
24
|
server.registerTool('update-block-simple', {
|
|
25
25
|
description: 'Update a text block using Markdown. Simpler than update-block: just provide markdown text. ' +
|
|
26
26
|
'Supports inline formatting: **bold**, *italic*, ~~strikethrough~~, `code`, [links](url). ' +
|
|
27
|
-
'Only works with text-based blocks: paragraph, headings, lists, to_do, quote, callout, toggle.'
|
|
27
|
+
'Only works with text-based blocks: paragraph, headings, lists, to_do, quote, callout, toggle. ' +
|
|
28
|
+
'Requires block_id. If you want to update by text search, use find-and-replace-in-page. For full page rewrite, use replace-page-content.',
|
|
28
29
|
inputSchema,
|
|
29
30
|
}, async ({ block_id, content }) => {
|
|
30
31
|
try {
|
|
@@ -69,7 +70,10 @@ export function registerUpdateBlockSimple(server, notion) {
|
|
|
69
70
|
};
|
|
70
71
|
}
|
|
71
72
|
const response = await notion.blocks.update(params);
|
|
72
|
-
|
|
73
|
+
// Return minimal response (id only)
|
|
74
|
+
return formatSimpleResponse({
|
|
75
|
+
id: response.id,
|
|
76
|
+
});
|
|
73
77
|
}
|
|
74
78
|
catch (error) {
|
|
75
79
|
return handleError(error);
|