@atikk-co-jp/notion-mcp-server 0.6.0 → 0.8.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 (135) hide show
  1. package/README.ja.md +1 -0
  2. package/README.md +20 -2
  3. package/dist/src/converters/__tests__/block-to-markdown.test.js +482 -111
  4. package/dist/src/converters/__tests__/markdown-to-blocks.test.js +116 -13
  5. package/dist/src/converters/__tests__/page-to-markdown.test.js +83 -70
  6. package/dist/src/converters/__tests__/rich-text-to-markdown.test.js +23 -26
  7. package/dist/src/converters/block-to-markdown.d.ts +4 -13
  8. package/dist/src/converters/block-to-markdown.d.ts.map +1 -1
  9. package/dist/src/converters/block-to-markdown.js +117 -120
  10. package/dist/src/converters/index.d.ts +3 -3
  11. package/dist/src/converters/index.d.ts.map +1 -1
  12. package/dist/src/converters/index.js +1 -1
  13. package/dist/src/converters/markdown-to-blocks.d.ts +21 -7
  14. package/dist/src/converters/markdown-to-blocks.d.ts.map +1 -1
  15. package/dist/src/converters/markdown-to-blocks.js +56 -0
  16. package/dist/src/converters/page-to-markdown.d.ts +5 -20
  17. package/dist/src/converters/page-to-markdown.d.ts.map +1 -1
  18. package/dist/src/converters/page-to-markdown.js +47 -37
  19. package/dist/src/converters/rich-text-to-markdown.d.ts +3 -47
  20. package/dist/src/converters/rich-text-to-markdown.d.ts.map +1 -1
  21. package/dist/src/converters/rich-text-to-markdown.js +15 -14
  22. package/dist/src/notion-client.d.ts +5 -177
  23. package/dist/src/notion-client.d.ts.map +1 -1
  24. package/dist/src/notion-client.js +6 -172
  25. package/dist/src/schemas/descriptions/examples.d.ts +14 -0
  26. package/dist/src/schemas/descriptions/examples.d.ts.map +1 -0
  27. package/dist/src/schemas/descriptions/examples.js +87 -0
  28. package/dist/src/schemas/descriptions/fields.d.ts +146 -0
  29. package/dist/src/schemas/descriptions/fields.d.ts.map +1 -0
  30. package/dist/src/schemas/descriptions/fields.js +184 -0
  31. package/dist/src/schemas/descriptions/index.d.ts +3 -0
  32. package/dist/src/schemas/descriptions/index.d.ts.map +1 -0
  33. package/dist/src/schemas/descriptions/index.js +2 -0
  34. package/dist/src/schemas/index.d.ts +1 -5
  35. package/dist/src/schemas/index.d.ts.map +1 -1
  36. package/dist/src/schemas/index.js +2 -10
  37. package/dist/src/tools/__tests__/context-size.test.d.ts +2 -0
  38. package/dist/src/tools/__tests__/context-size.test.d.ts.map +1 -0
  39. package/dist/src/tools/__tests__/context-size.test.js +143 -0
  40. package/dist/src/tools/__tests__/error-handler.test.d.ts +2 -0
  41. package/dist/src/tools/__tests__/error-handler.test.d.ts.map +1 -0
  42. package/dist/src/tools/__tests__/error-handler.test.js +125 -0
  43. package/dist/src/tools/append-block-children.d.ts.map +1 -1
  44. package/dist/src/tools/append-block-children.js +8 -5
  45. package/dist/src/tools/append-blocks-simple.d.ts.map +1 -1
  46. package/dist/src/tools/append-blocks-simple.js +9 -13
  47. package/dist/src/tools/archive-database.d.ts.map +1 -1
  48. package/dist/src/tools/archive-database.js +4 -4
  49. package/dist/src/tools/archive-page.d.ts.map +1 -1
  50. package/dist/src/tools/archive-page.js +3 -3
  51. package/dist/src/tools/create-comment-simple.d.ts +4 -0
  52. package/dist/src/tools/create-comment-simple.d.ts.map +1 -0
  53. package/dist/src/tools/create-comment-simple.js +51 -0
  54. package/dist/src/tools/create-comment.d.ts.map +1 -1
  55. package/dist/src/tools/create-comment.js +28 -8
  56. package/dist/src/tools/create-database.d.ts.map +1 -1
  57. package/dist/src/tools/create-database.js +19 -25
  58. package/dist/src/tools/create-page-simple.d.ts +1 -1
  59. package/dist/src/tools/create-page-simple.d.ts.map +1 -1
  60. package/dist/src/tools/create-page-simple.js +41 -26
  61. package/dist/src/tools/create-page.d.ts.map +1 -1
  62. package/dist/src/tools/create-page.js +11 -7
  63. package/dist/src/tools/delete-block.d.ts.map +1 -1
  64. package/dist/src/tools/delete-block.js +2 -1
  65. package/dist/src/tools/get-block-children.d.ts +1 -1
  66. package/dist/src/tools/get-block-children.d.ts.map +1 -1
  67. package/dist/src/tools/get-block-children.js +13 -27
  68. package/dist/src/tools/index.d.ts +4 -3
  69. package/dist/src/tools/index.d.ts.map +1 -1
  70. package/dist/src/tools/index.js +5 -3
  71. package/dist/src/tools/list-comments.d.ts +1 -1
  72. package/dist/src/tools/list-comments.d.ts.map +1 -1
  73. package/dist/src/tools/list-comments.js +11 -10
  74. package/dist/src/tools/list-users.d.ts.map +1 -1
  75. package/dist/src/tools/list-users.js +4 -3
  76. package/dist/src/tools/move-page.d.ts.map +1 -1
  77. package/dist/src/tools/move-page.js +5 -4
  78. package/dist/src/tools/query-data-source.d.ts +1 -1
  79. package/dist/src/tools/query-data-source.d.ts.map +1 -1
  80. package/dist/src/tools/query-data-source.js +23 -24
  81. package/dist/src/tools/retrieve-block.d.ts +1 -1
  82. package/dist/src/tools/retrieve-block.d.ts.map +1 -1
  83. package/dist/src/tools/retrieve-block.js +13 -10
  84. package/dist/src/tools/retrieve-bot-user.js +1 -1
  85. package/dist/src/tools/retrieve-data-source.d.ts +1 -1
  86. package/dist/src/tools/retrieve-data-source.d.ts.map +1 -1
  87. package/dist/src/tools/retrieve-data-source.js +15 -11
  88. package/dist/src/tools/retrieve-database.d.ts +1 -1
  89. package/dist/src/tools/retrieve-database.d.ts.map +1 -1
  90. package/dist/src/tools/retrieve-database.js +11 -6
  91. package/dist/src/tools/retrieve-page-property.d.ts.map +1 -1
  92. package/dist/src/tools/retrieve-page-property.js +7 -7
  93. package/dist/src/tools/retrieve-page.d.ts +1 -1
  94. package/dist/src/tools/retrieve-page.d.ts.map +1 -1
  95. package/dist/src/tools/retrieve-page.js +15 -20
  96. package/dist/src/tools/retrieve-user.d.ts.map +1 -1
  97. package/dist/src/tools/retrieve-user.js +2 -1
  98. package/dist/src/tools/search.d.ts.map +1 -1
  99. package/dist/src/tools/search.js +10 -17
  100. package/dist/src/tools/update-block-simple.d.ts +1 -1
  101. package/dist/src/tools/update-block-simple.d.ts.map +1 -1
  102. package/dist/src/tools/update-block-simple.js +14 -4
  103. package/dist/src/tools/update-block.d.ts.map +1 -1
  104. package/dist/src/tools/update-block.js +8 -5
  105. package/dist/src/tools/update-data-source.d.ts.map +1 -1
  106. package/dist/src/tools/update-data-source.js +8 -8
  107. package/dist/src/tools/update-database.d.ts.map +1 -1
  108. package/dist/src/tools/update-database.js +22 -32
  109. package/dist/src/tools/update-page.d.ts.map +1 -1
  110. package/dist/src/tools/update-page.js +13 -8
  111. package/dist/src/utils/error-handler.d.ts +20 -0
  112. package/dist/src/utils/error-handler.d.ts.map +1 -1
  113. package/dist/src/utils/error-handler.js +63 -0
  114. package/dist/src/utils/index.d.ts +1 -1
  115. package/dist/src/utils/index.d.ts.map +1 -1
  116. package/dist/src/utils/index.js +1 -1
  117. package/package.json +2 -1
  118. package/dist/src/schemas/block.d.ts +0 -3787
  119. package/dist/src/schemas/block.d.ts.map +0 -1
  120. package/dist/src/schemas/block.js +0 -402
  121. package/dist/src/schemas/common.d.ts +0 -638
  122. package/dist/src/schemas/common.d.ts.map +0 -1
  123. package/dist/src/schemas/common.js +0 -163
  124. package/dist/src/schemas/database.d.ts +0 -687
  125. package/dist/src/schemas/database.d.ts.map +0 -1
  126. package/dist/src/schemas/database.js +0 -258
  127. package/dist/src/schemas/filter.d.ts +0 -611
  128. package/dist/src/schemas/filter.d.ts.map +0 -1
  129. package/dist/src/schemas/filter.js +0 -222
  130. package/dist/src/schemas/page.d.ts +0 -2607
  131. package/dist/src/schemas/page.d.ts.map +0 -1
  132. package/dist/src/schemas/page.js +0 -328
  133. package/dist/src/schemas/schemas.test.d.ts +0 -2
  134. package/dist/src/schemas/schemas.test.d.ts.map +0 -1
  135. package/dist/src/schemas/schemas.test.js +0 -418
@@ -1,10 +1,2 @@
1
- // Database schemas
2
- // Block schemas
3
- export { AudioBlockSchema, BlockChildrenSchema, BlockSchema, BookmarkBlockSchema, BreadcrumbBlockSchema, BulletedListItemBlockSchema, CalloutBlockSchema, ChildDatabaseBlockSchema, ChildPageBlockSchema, CodeBlockSchema, ColumnBlockSchema, ColumnListBlockSchema, DividerBlockSchema, EmbedBlockSchema, EmojiIconBlockSchema, EquationBlockSchema, ExternalFileSchema, ExternalIconBlockSchema, FileBlockSchema, FileIconBlockSchema, FileObjectSchema, FileUploadSchema, Heading1BlockSchema, Heading2BlockSchema, Heading3BlockSchema, IconBlockSchema, ImageBlockSchema, LinkPreviewBlockSchema, LinkToPageBlockSchema, NotionFileSchema, NumberedListItemBlockSchema, ParagraphBlockSchema, PdfBlockSchema, QuoteBlockSchema, SyncedBlockDuplicateSchema, SyncedBlockOriginalSchema, SyncedBlockSchema, TableBlockSchema, TableOfContentsBlockSchema, TableRowBlockSchema, TemplateBlockSchema, ToDoBlockSchema, ToggleBlockSchema, UnsupportedBlockSchema, VideoBlockSchema, } from './block.js';
4
- // Common schemas
5
- export { AnnotationsSchema, ColorSchema, CoverSchema, DatabaseMentionSchema, DatabaseParentSchema, DateMentionSchema, EmojiIconSchema, EquationRichTextSchema, ExternalIconSchema, IconSchema, LinkPreviewMentionSchema, MentionRichTextSchema, MentionSchema, PageMentionSchema, PageParentSchema, ParentSchema, RichTextArraySchema, RichTextSchema, SimpleTextSchema, TemplateMentionDateSchema, TemplateMentionSchema, TemplateMentionUserSchema, TextRichTextSchema, UserMentionSchema, } from './common.js';
6
- export { CheckboxPropertySchemaSchema, CreatedByPropertySchemaSchema, CreatedTimePropertySchemaSchema, DatabasePropertiesSchema, DatabasePropertySchemaSchema, DatabaseTitleSchema, DatePropertySchemaSchema, EmailPropertySchemaSchema, FilesPropertySchemaSchema, FormulaPropertySchemaSchema, LastEditedByPropertySchemaSchema, LastEditedTimePropertySchemaSchema, MultiSelectPropertySchemaSchema, NumberPropertySchemaSchema, PeoplePropertySchemaSchema, PhoneNumberPropertySchemaSchema, RelationPropertySchemaSchema, RichTextPropertySchemaSchema, RollupPropertySchemaSchema, SelectOptionSchema, SelectPropertySchemaSchema, TitlePropertySchemaSchema, UrlPropertySchemaSchema, } from './database.js';
7
- // Filter and sort schemas
8
- export { FilterSchema, PropertyFilterSchema, SortSchema, SortsSchema, TimestampFilterSchema, } from './filter.js';
9
- // Page property schemas
10
- export { CheckboxPropertySchema, CreatedByPropertySchema, CreatedTimePropertySchema, DatePropertySchema, EmailPropertySchema, FilesPropertySchema, FormulaPropertySchema, LastEditedByPropertySchema, LastEditedTimePropertySchema, MultiSelectPropertySchema, NumberPropertySchema, PeoplePropertySchema, PhoneNumberPropertySchema, PropertiesSchema, PropertyValueSchema, RelationPropertySchema, RichTextPropertySchema, RollupPropertySchema, SelectPropertySchema, StatusPropertySchema, TitlePropertySchema, UniqueIdPropertySchema, UrlPropertySchema, UserObjectSchema, VerificationPropertySchema, } from './page.js';
1
+ // Field descriptions for MCP tool schemas
2
+ export { F, Fields } from './descriptions/index.js';
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=context-size.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-size.test.d.ts","sourceRoot":"","sources":["../../../../src/tools/__tests__/context-size.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,143 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { toJSONSchema } from 'zod/v4/core';
4
+ import { NotionClient } from '../../notion-client.js';
5
+ import { registerAllTools } from '../index.js';
6
+ /**
7
+ * Test that all tools don't consume excessive context when registered with MCP.
8
+ *
9
+ * Context is consumed by:
10
+ * 1. Tool description - the description string passed to registerTool
11
+ * 2. Input schema - converted to JSON Schema via toJSONSchema (Zod v4)
12
+ *
13
+ * These tests measure the actual JSON Schema that MCP sends to LLMs.
14
+ * Token counts are approximated as chars/4 (rough estimate for JSON).
15
+ */
16
+ // Thresholds in estimated tokens (chars / 4)
17
+ // Based on actual MCP tool registration token counts from user data
18
+ const SINGLE_TOOL_MAX_TOKENS = 1500; // Max tokens for a single tool
19
+ const TOTAL_TOOLS_MAX_TOKENS = 30000; // Max total tokens for all tools combined
20
+ const TOTAL_TOOLS_WARNING_TOKENS = 35000; // Warning threshold
21
+ // Known large tools that need optimization (tracked for future improvement)
22
+ // NOTE: query-data-source, update-database, create-database were optimized using
23
+ // z.any() with FieldDescriptions, reducing ~13,500 tokens. All tools now fit within
24
+ // the standard SINGLE_TOOL_MAX_TOKENS threshold.
25
+ const KNOWN_LARGE_TOOLS = {
26
+ // Currently empty - all tools have been optimized
27
+ };
28
+ // Access internal registered tools from McpServer
29
+ // Using type assertion chain to avoid explicit any
30
+ function getRegisteredTools(server) {
31
+ return server
32
+ ._registeredTools;
33
+ }
34
+ // Create a mock notion client for testing (won't make actual API calls)
35
+ function createMockNotionClient() {
36
+ return new NotionClient({ auth: 'test-token' });
37
+ }
38
+ // Convert Zod schema to JSON Schema (same as MCP does internally)
39
+ function zodToJsonSchema(zodSchema) {
40
+ try {
41
+ // Use Zod v4's toJSONSchema - same as MCP SDK uses
42
+ return toJSONSchema(zodSchema);
43
+ }
44
+ catch {
45
+ // Fallback for non-Zod schemas
46
+ return {};
47
+ }
48
+ }
49
+ // Calculate the context size for a single tool (in estimated tokens)
50
+ function calculateToolContextSize(tool) {
51
+ const descriptionChars = tool.description?.length ?? 0;
52
+ // Convert to JSON Schema (same as MCP does)
53
+ const jsonSchema = tool.inputSchema ? zodToJsonSchema(tool.inputSchema) : {};
54
+ const schemaChars = JSON.stringify(jsonSchema).length;
55
+ const totalChars = descriptionChars + schemaChars;
56
+ // Rough token estimate: ~4 chars per token for JSON
57
+ const estimatedTokens = Math.ceil(totalChars / 4);
58
+ return {
59
+ descriptionChars,
60
+ schemaChars,
61
+ totalChars,
62
+ estimatedTokens,
63
+ };
64
+ }
65
+ describe('Tool context size', () => {
66
+ const server = new McpServer({ name: 'test-server', version: '1.0.0' });
67
+ const notion = createMockNotionClient();
68
+ // Register all tools
69
+ registerAllTools(server, notion);
70
+ const registeredTools = getRegisteredTools(server);
71
+ const toolNames = Object.keys(registeredTools);
72
+ describe('Individual tool sizes', () => {
73
+ it.each(toolNames)('%s should not exceed token limit', (toolName) => {
74
+ const tool = registeredTools[toolName];
75
+ const { estimatedTokens, descriptionChars, schemaChars } = calculateToolContextSize(tool);
76
+ // Check if this is a known large tool with special limits
77
+ const knownLargeTool = KNOWN_LARGE_TOOLS[toolName];
78
+ const maxAllowed = knownLargeTool?.maxAllowed ?? SINGLE_TOOL_MAX_TOKENS;
79
+ // Log sizes for debugging (visible in verbose mode)
80
+ console.log(`${toolName}: ~${estimatedTokens} tokens (desc=${descriptionChars}, schema=${schemaChars} chars)` +
81
+ (knownLargeTool ? ` [KNOWN LARGE: ${knownLargeTool.reason}]` : ''));
82
+ expect(estimatedTokens).toBeLessThanOrEqual(maxAllowed);
83
+ });
84
+ });
85
+ describe('Total context size', () => {
86
+ it('should report total context size and check limits', () => {
87
+ let totalTokens = 0;
88
+ let totalTokensExcludingKnownLarge = 0;
89
+ const toolSizes = [];
90
+ for (const [name, tool] of Object.entries(registeredTools)) {
91
+ const { estimatedTokens } = calculateToolContextSize(tool);
92
+ const isKnownLarge = name in KNOWN_LARGE_TOOLS;
93
+ totalTokens += estimatedTokens;
94
+ if (!isKnownLarge) {
95
+ totalTokensExcludingKnownLarge += estimatedTokens;
96
+ }
97
+ toolSizes.push({ name, tokens: estimatedTokens, isKnownLarge });
98
+ }
99
+ // Sort by size descending for the report
100
+ toolSizes.sort((a, b) => b.tokens - a.tokens);
101
+ console.log('\n=== Tool Context Size Report (Estimated Tokens) ===');
102
+ console.log(`Total tools: ${toolNames.length}`);
103
+ console.log(`Total estimated tokens: ~${totalTokens}`);
104
+ console.log(`Total (excluding known large): ~${totalTokensExcludingKnownLarge}`);
105
+ console.log(`Target limit: ${TOTAL_TOOLS_MAX_TOKENS} tokens`);
106
+ console.log(`Warning limit: ${TOTAL_TOOLS_WARNING_TOKENS} tokens`);
107
+ console.log(`Usage (excluding known large): ${((totalTokensExcludingKnownLarge / TOTAL_TOOLS_MAX_TOKENS) * 100).toFixed(1)}%`);
108
+ console.log('\nTop 10 largest tools:');
109
+ toolSizes.slice(0, 10).forEach(({ name, tokens, isKnownLarge }, i) => {
110
+ const marker = isKnownLarge ? ' [KNOWN LARGE]' : '';
111
+ console.log(` ${i + 1}. ${name}: ~${tokens} tokens${marker}`);
112
+ });
113
+ // Check that tools excluding known large ones are within target
114
+ expect(totalTokensExcludingKnownLarge).toBeLessThanOrEqual(TOTAL_TOOLS_MAX_TOKENS);
115
+ // Warn if total exceeds warning threshold (test still passes but logs warning)
116
+ if (totalTokens > TOTAL_TOOLS_WARNING_TOKENS) {
117
+ console.warn(`\n⚠️ WARNING: Total tokens (~${totalTokens}) exceeds warning threshold (${TOTAL_TOOLS_WARNING_TOKENS})`);
118
+ console.warn(' Consider optimizing known large tools by replacing complex schemas with z.any()');
119
+ }
120
+ });
121
+ });
122
+ describe('Schema complexity checks', () => {
123
+ it.each(toolNames)('%s should use z.any() for complex nested structures', (toolName) => {
124
+ const tool = registeredTools[toolName];
125
+ if (!tool.inputSchema)
126
+ return;
127
+ const schemaStr = JSON.stringify(tool.inputSchema);
128
+ // Check that complex properties don't have deeply nested definitions
129
+ // If they did, the schema would contain many "properties" keys
130
+ const propertiesCount = (schemaStr.match(/"properties":/g) || []).length;
131
+ // Allow max 2 levels of nesting (root + one level of additionalProperties)
132
+ expect(propertiesCount).toBeLessThanOrEqual(3);
133
+ });
134
+ });
135
+ describe('Description length checks', () => {
136
+ const MAX_DESCRIPTION_LENGTH = 500;
137
+ it.each(toolNames)('%s description should be concise', (toolName) => {
138
+ const tool = registeredTools[toolName];
139
+ const descLength = tool.description?.length ?? 0;
140
+ expect(descLength).toBeLessThanOrEqual(MAX_DESCRIPTION_LENGTH);
141
+ });
142
+ });
143
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=error-handler.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-handler.test.d.ts","sourceRoot":"","sources":["../../../../src/tools/__tests__/error-handler.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,125 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { describe, expect, it } from 'vitest';
4
+ const toolsDir = resolve(import.meta.dirname, '..');
5
+ const toolErrorConfigs = [
6
+ // Tools using handleErrorWithContext
7
+ { toolFile: 'append-block-children.ts', handler: 'handleErrorWithContext', exampleType: 'block' },
8
+ {
9
+ toolFile: 'create-comment.ts',
10
+ handler: 'handleErrorWithContext',
11
+ exampleType: 'richTextArray',
12
+ },
13
+ { toolFile: 'create-database.ts', handler: 'handleErrorWithContext', exampleType: 'schema' },
14
+ {
15
+ toolFile: 'create-page.ts',
16
+ handler: 'handleErrorWithContext',
17
+ exampleType: 'page',
18
+ hasDataSourceId: true,
19
+ },
20
+ {
21
+ toolFile: 'create-page-simple.ts',
22
+ handler: 'handleErrorWithContext',
23
+ exampleType: 'page',
24
+ hasDataSourceId: true,
25
+ },
26
+ { toolFile: 'query-data-source.ts', handler: 'handleErrorWithContext', exampleType: 'filter' },
27
+ { toolFile: 'search.ts', handler: 'handleErrorWithContext', exampleType: 'filter' },
28
+ { toolFile: 'update-block.ts', handler: 'handleErrorWithContext', exampleType: 'block' },
29
+ {
30
+ toolFile: 'update-data-source.ts',
31
+ handler: 'handleErrorWithContext',
32
+ exampleType: 'schema',
33
+ hasDataSourceId: true,
34
+ },
35
+ {
36
+ toolFile: 'update-database.ts',
37
+ handler: 'handleErrorWithContext',
38
+ exampleType: 'richTextArray',
39
+ },
40
+ { toolFile: 'update-page.ts', handler: 'handleErrorWithContext', exampleType: 'page' },
41
+ // Tools using handleError (no contextual help needed)
42
+ { toolFile: 'append-blocks-simple.ts', handler: 'handleError' },
43
+ { toolFile: 'archive-database.ts', handler: 'handleError' },
44
+ { toolFile: 'archive-page.ts', handler: 'handleError' },
45
+ { toolFile: 'create-comment-simple.ts', handler: 'handleError' },
46
+ { toolFile: 'delete-block.ts', handler: 'handleError' },
47
+ { toolFile: 'get-block-children.ts', handler: 'handleError' },
48
+ { toolFile: 'list-comments.ts', handler: 'handleError' },
49
+ { toolFile: 'list-users.ts', handler: 'handleError' },
50
+ { toolFile: 'move-page.ts', handler: 'handleError' },
51
+ { toolFile: 'retrieve-block.ts', handler: 'handleError' },
52
+ { toolFile: 'retrieve-bot-user.ts', handler: 'handleError' },
53
+ { toolFile: 'retrieve-data-source.ts', handler: 'handleError' },
54
+ { toolFile: 'retrieve-database.ts', handler: 'handleError' },
55
+ { toolFile: 'retrieve-page.ts', handler: 'handleError' },
56
+ { toolFile: 'retrieve-page-property.ts', handler: 'handleError' },
57
+ { toolFile: 'retrieve-user.ts', handler: 'handleError' },
58
+ { toolFile: 'update-block-simple.ts', handler: 'handleError' },
59
+ ];
60
+ describe('Tool error handlers', () => {
61
+ describe.each(toolErrorConfigs)('$toolFile', ({ toolFile, handler, exampleType, hasDataSourceId, }) => {
62
+ const filePath = resolve(toolsDir, toolFile);
63
+ const content = readFileSync(filePath, 'utf-8');
64
+ it(`should import ${handler}`, () => {
65
+ const importRegex = new RegExp(`import\\s+\\{[^}]*\\b${handler}\\b[^}]*\\}`);
66
+ expect(content).toMatch(importRegex);
67
+ });
68
+ it(`should call ${handler} in catch block`, () => {
69
+ const callRegex = new RegExp(`return\\s+${handler}\\(error`);
70
+ expect(content).toMatch(callRegex);
71
+ });
72
+ if (handler === 'handleErrorWithContext') {
73
+ it(`should use exampleType: '${exampleType}'`, () => {
74
+ const exampleTypeRegex = new RegExp(`exampleType:\\s*['"]${exampleType}['"]`);
75
+ expect(content).toMatch(exampleTypeRegex);
76
+ });
77
+ if (hasDataSourceId) {
78
+ it('should pass dataSourceId option', () => {
79
+ expect(content).toMatch(/dataSourceId:\s*\w+/);
80
+ });
81
+ }
82
+ }
83
+ if (handler === 'handleError') {
84
+ it('should not use handleErrorWithContext', () => {
85
+ expect(content).not.toMatch(/handleErrorWithContext/);
86
+ });
87
+ }
88
+ });
89
+ });
90
+ describe('All tool files are covered', () => {
91
+ it('should have a config for each tool file (excluding index.ts)', () => {
92
+ const expectedToolFiles = [
93
+ 'append-block-children.ts',
94
+ 'append-blocks-simple.ts',
95
+ 'archive-database.ts',
96
+ 'archive-page.ts',
97
+ 'create-comment.ts',
98
+ 'create-comment-simple.ts',
99
+ 'create-database.ts',
100
+ 'create-page.ts',
101
+ 'create-page-simple.ts',
102
+ 'delete-block.ts',
103
+ 'get-block-children.ts',
104
+ 'list-comments.ts',
105
+ 'list-users.ts',
106
+ 'move-page.ts',
107
+ 'query-data-source.ts',
108
+ 'retrieve-block.ts',
109
+ 'retrieve-bot-user.ts',
110
+ 'retrieve-data-source.ts',
111
+ 'retrieve-database.ts',
112
+ 'retrieve-page.ts',
113
+ 'retrieve-page-property.ts',
114
+ 'retrieve-user.ts',
115
+ 'search.ts',
116
+ 'update-block.ts',
117
+ 'update-block-simple.ts',
118
+ 'update-data-source.ts',
119
+ 'update-database.ts',
120
+ 'update-page.ts',
121
+ ];
122
+ const configuredFiles = toolErrorConfigs.map((c) => c.toolFile).sort();
123
+ expect(configuredFiles).toEqual(expectedToolFiles.sort());
124
+ });
125
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"append-block-children.d.ts","sourceRoot":"","sources":["../../../src/tools/append-block-children.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAExE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAWvD,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,CAiCzF"}
1
+ {"version":3,"file":"append-block-children.d.ts","sourceRoot":"","sources":["../../../src/tools/append-block-children.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAExE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAevD,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,CAmCzF"}
@@ -1,10 +1,11 @@
1
1
  import { z } from 'zod';
2
- import { formatResponse, handleError } from '../utils/index.js';
2
+ import { F } from '../schemas/descriptions/index.js';
3
+ import { formatResponse, handleErrorWithContext } from '../utils/index.js';
3
4
  // Minimal schema for MCP (full validation by Notion API)
4
5
  const inputSchema = {
5
- block_id: z.string().describe('Block or page ID'),
6
- children: z.array(z.any()).describe('Block objects to append'),
7
- after: z.string().optional().describe('Insert after this block ID'),
6
+ block_id: z.string().describe(F.block_id),
7
+ children: z.array(z.any()).describe(F.children),
8
+ after: z.string().optional().describe(F.after),
8
9
  };
9
10
  export function registerAppendBlockChildren(server, notion) {
10
11
  server.registerTool('append-block-children', {
@@ -26,7 +27,9 @@ export function registerAppendBlockChildren(server, notion) {
26
27
  return formatResponse(response);
27
28
  }
28
29
  catch (error) {
29
- return handleError(error);
30
+ return handleErrorWithContext(error, notion, {
31
+ exampleType: 'block',
32
+ });
30
33
  }
31
34
  });
32
35
  }
@@ -1 +1 @@
1
- {"version":3,"file":"append-blocks-simple.d.ts","sourceRoot":"","sources":["../../../src/tools/append-blocks-simple.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAGxE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAUvD,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,CAqCxF"}
1
+ {"version":3,"file":"append-blocks-simple.d.ts","sourceRoot":"","sources":["../../../src/tools/append-blocks-simple.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAIxE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAWvD,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,CA0BxF"}
@@ -1,32 +1,28 @@
1
1
  import { z } from 'zod';
2
2
  import { markdownToBlocks } from '../converters/index.js';
3
+ import { F } from '../schemas/descriptions/index.js';
3
4
  import { formatResponse, handleError } from '../utils/index.js';
4
5
  // Minimal schema for MCP
5
6
  const inputSchema = {
6
- block_id: z.string().describe('Page or block ID to append to'),
7
- content: z.string().describe('Content in Markdown'),
8
- after: z.string().optional().describe('Insert after this block ID'),
7
+ block_id: z.string().describe(F.block_id),
8
+ content: z.string().describe(F.content),
9
+ after: z.string().optional().describe(F.after),
9
10
  };
10
11
  export function registerAppendBlocksSimple(server, notion) {
11
12
  server.registerTool('append-blocks-simple', {
12
13
  description: 'Append blocks to a page using Markdown. ' +
13
14
  'Simpler than append-block-children: just provide markdown text. ' +
14
- 'Supports: headings (#), lists (- or 1.), checkboxes (- [ ]), code blocks (```), quotes (>), images (![]()), bold (**), italic (*), links ([]()), etc.',
15
+ 'Supports: headings (#), lists (- or 1.), checkboxes (- [ ]), code blocks (```), quotes (>), tables (| |), images (![]()), bold (**), italic (*), links ([]()), etc.',
15
16
  inputSchema,
16
17
  }, async ({ block_id, content, after }) => {
17
18
  try {
18
- // Convert markdown to blocks
19
+ // Convert markdown to blocks and cast to SDK type
19
20
  const children = markdownToBlocks(content);
20
- // Build params
21
- const params = {
21
+ const response = await notion.blocks.children.append({
22
22
  block_id,
23
23
  children,
24
- };
25
- if (after) {
26
- params.after = after;
27
- }
28
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
- const response = await notion.blocks.children.append(params);
24
+ ...(after && { after }),
25
+ });
30
26
  return formatResponse(response);
31
27
  }
32
28
  catch (error) {
@@ -1 +1 @@
1
- {"version":3,"file":"archive-database.d.ts","sourceRoot":"","sources":["../../../src/tools/archive-database.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAExE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAOvD,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,CAqBrF"}
1
+ {"version":3,"file":"archive-database.d.ts","sourceRoot":"","sources":["../../../src/tools/archive-database.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAExE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAQvD,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,CAmBrF"}
@@ -1,18 +1,18 @@
1
1
  import { z } from 'zod';
2
+ import { F } from '../schemas/descriptions/index.js';
2
3
  import { formatResponse, handleError } from '../utils/index.js';
3
4
  const inputSchema = {
4
- database_id: z.string().describe('Database ID to archive'),
5
+ database_id: z.string().describe(F.database_id),
5
6
  };
6
7
  export function registerArchiveDatabase(server, notion) {
7
8
  server.registerTool('archive-database', {
8
- description: 'Archive (delete) a Notion database by moving it to trash. ' +
9
- 'This is equivalent to update-database with archived: true.',
9
+ description: 'Move a database to trash. Recoverable for 30 days via Notion UI.',
10
10
  inputSchema,
11
11
  }, async ({ database_id }) => {
12
12
  try {
13
13
  const response = await notion.databases.update({
14
14
  database_id,
15
- archived: true,
15
+ in_trash: true,
16
16
  });
17
17
  return formatResponse(response);
18
18
  }
@@ -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;AAOvD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,CAqBjF"}
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,CAmBjF"}
@@ -1,12 +1,12 @@
1
1
  import { z } from 'zod';
2
+ import { F } from '../schemas/descriptions/index.js';
2
3
  import { formatResponse, handleError } from '../utils/index.js';
3
4
  const inputSchema = {
4
- page_id: z.string().describe('Page ID to archive'),
5
+ page_id: z.string().describe(F.page_id),
5
6
  };
6
7
  export function registerArchivePage(server, notion) {
7
8
  server.registerTool('archive-page', {
8
- description: 'Archive (delete) a Notion page by moving it to trash. ' +
9
- 'This is equivalent to update-page with archived: true.',
9
+ description: 'Move a page to trash. Recoverable for 30 days via Notion UI.',
10
10
  inputSchema,
11
11
  }, async ({ page_id }) => {
12
12
  try {
@@ -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 registerCreateCommentSimple(server: McpServer, notion: NotionClient): void;
4
+ //# sourceMappingURL=create-comment-simple.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-comment-simple.d.ts","sourceRoot":"","sources":["../../../src/tools/create-comment-simple.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAGxE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAWvD,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,CAmDzF"}
@@ -0,0 +1,51 @@
1
+ import { z } from 'zod';
2
+ import { parseInlineMarkdown } from '../converters/index.js';
3
+ import { F } from '../schemas/descriptions/index.js';
4
+ import { formatResponse, handleError } from '../utils/index.js';
5
+ const inputSchema = {
6
+ page_id: z.string().optional().describe(F.page_id),
7
+ block_id: z.string().optional().describe(F.block_id),
8
+ discussion_id: z.string().optional().describe(F.discussion_id),
9
+ content: z.string().describe(F.content),
10
+ };
11
+ export function registerCreateCommentSimple(server, notion) {
12
+ server.registerTool('create-comment-simple', {
13
+ description: 'Add a comment using Markdown. Simpler than create-comment.',
14
+ inputSchema,
15
+ }, async ({ page_id, block_id, discussion_id, content }) => {
16
+ try {
17
+ // Validate: exactly one of page_id, block_id, or discussion_id must be provided
18
+ const providedCount = [page_id, block_id, discussion_id].filter(Boolean).length;
19
+ if (providedCount !== 1) {
20
+ return {
21
+ content: [
22
+ {
23
+ type: 'text',
24
+ text: 'Error: Provide exactly one of page_id, block_id, or discussion_id.',
25
+ },
26
+ ],
27
+ isError: true,
28
+ };
29
+ }
30
+ const rich_text = parseInlineMarkdown(content);
31
+ // Build params based on which ID was provided
32
+ const params = {
33
+ rich_text,
34
+ };
35
+ if (discussion_id) {
36
+ params.discussion_id = discussion_id;
37
+ }
38
+ else if (page_id) {
39
+ params.parent = { page_id };
40
+ }
41
+ else if (block_id) {
42
+ params.parent = { block_id };
43
+ }
44
+ const response = await notion.comments.create(params);
45
+ return formatResponse(response);
46
+ }
47
+ catch (error) {
48
+ return handleError(error);
49
+ }
50
+ });
51
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"create-comment.d.ts","sourceRoot":"","sources":["../../../src/tools/create-comment.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAExE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAUvD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,CAqCnF"}
1
+ {"version":3,"file":"create-comment.d.ts","sourceRoot":"","sources":["../../../src/tools/create-comment.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAExE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAYvD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,CAuDnF"}
@@ -1,10 +1,12 @@
1
1
  import { z } from 'zod';
2
- import { formatResponse, handleError } from '../utils/index.js';
2
+ import { F } from '../schemas/descriptions/index.js';
3
+ import { formatResponse, handleErrorWithContext } from '../utils/index.js';
3
4
  // Minimal schema for MCP (full validation by Notion API)
4
5
  const inputSchema = {
5
- page_id: z.string().describe('Page ID'),
6
- rich_text: z.array(z.any()).describe('Comment content as rich text'),
7
- discussion_id: z.string().optional().describe('Reply to existing discussion'),
6
+ page_id: z.string().optional().describe(F.page_id),
7
+ block_id: z.string().optional().describe(F.block_id),
8
+ discussion_id: z.string().optional().describe(F.discussion_id),
9
+ rich_text: z.array(z.any()).describe(F.rich_text),
8
10
  };
9
11
  export function registerCreateComment(server, notion) {
10
12
  server.registerTool('create-comment', {
@@ -13,23 +15,41 @@ export function registerCreateComment(server, notion) {
13
15
  'Use discussion_id to reply to an existing comment thread. ' +
14
16
  'Returns the created comment with its ID.',
15
17
  inputSchema,
16
- }, async ({ page_id, rich_text, discussion_id }) => {
18
+ }, async ({ page_id, block_id, discussion_id, rich_text }) => {
17
19
  try {
18
- // The Notion API requires either parent or discussion_id
20
+ // Validate: exactly one of page_id, block_id, or discussion_id must be provided
21
+ const providedCount = [page_id, block_id, discussion_id].filter(Boolean).length;
22
+ if (providedCount !== 1) {
23
+ return {
24
+ content: [
25
+ {
26
+ type: 'text',
27
+ text: 'Error: Provide exactly one of page_id, block_id, or discussion_id.',
28
+ },
29
+ ],
30
+ isError: true,
31
+ };
32
+ }
33
+ // Build params based on which ID was provided
19
34
  const params = {
20
35
  rich_text,
21
36
  };
22
37
  if (discussion_id) {
23
38
  params.discussion_id = discussion_id;
24
39
  }
25
- else {
40
+ else if (page_id) {
26
41
  params.parent = { page_id };
27
42
  }
43
+ else if (block_id) {
44
+ params.parent = { block_id };
45
+ }
28
46
  const response = await notion.comments.create(params);
29
47
  return formatResponse(response);
30
48
  }
31
49
  catch (error) {
32
- return handleError(error);
50
+ return handleErrorWithContext(error, notion, {
51
+ exampleType: 'richTextArray',
52
+ });
33
53
  }
34
54
  });
35
55
  }
@@ -1 +1 @@
1
- {"version":3,"file":"create-database.d.ts","sourceRoot":"","sources":["../../../src/tools/create-database.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAExE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAevD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,CAgDpF"}
1
+ {"version":3,"file":"create-database.d.ts","sourceRoot":"","sources":["../../../src/tools/create-database.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAGxE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAevD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,CA+BpF"}
@@ -1,15 +1,15 @@
1
1
  import { z } from 'zod';
2
- import { formatResponse, handleError } from '../utils/index.js';
2
+ import { F } from '../schemas/descriptions/index.js';
3
+ import { formatResponse, handleErrorWithContext } from '../utils/index.js';
3
4
  // Minimal schema for MCP (full validation by Notion API)
5
+ // Uses z.any() for title/icon/cover to reduce context size (~2,300 tokens saved)
4
6
  const inputSchema = {
5
- parent_page_id: z.string().describe('Parent page ID'),
6
- title: z.array(z.any()).optional().describe('Database title'),
7
- properties: z
8
- .record(z.string(), z.any())
9
- .describe('Property schema (must include one title property)'),
10
- icon: z.any().optional().describe('Database icon { type: "emoji", emoji: "📝" } or { type: "external", external: { url: "..." } }. Emoji must be an actual emoji character.'),
11
- cover: z.any().optional().describe('Cover image'),
12
- is_inline: z.boolean().optional().describe('Inline database'),
7
+ parent_page_id: z.string().describe(F.parent_page_id),
8
+ title: z.any().optional().describe(F.title),
9
+ properties: z.record(z.string(), z.any()).describe(F.properties_schema),
10
+ icon: z.any().optional().describe(F.icon),
11
+ cover: z.any().optional().describe(F.cover),
12
+ is_inline: z.boolean().optional().describe(F.is_inline),
13
13
  };
14
14
  export function registerCreateDatabase(server, notion) {
15
15
  server.registerTool('create-database', {
@@ -19,28 +19,22 @@ export function registerCreateDatabase(server, notion) {
19
19
  inputSchema,
20
20
  }, async ({ parent_page_id, title, properties, icon, cover, is_inline }) => {
21
21
  try {
22
+ // Build params with only defined values (API validates full structure)
22
23
  const params = {
23
- parent: { page_id: parent_page_id },
24
- initial_data_source: { properties: properties },
24
+ parent: { type: 'page_id', page_id: parent_page_id },
25
+ initial_data_source: { properties },
26
+ ...(title !== undefined && { title }),
27
+ ...(icon !== undefined && { icon }),
28
+ ...(cover !== undefined && { cover }),
29
+ ...(is_inline !== undefined && { is_inline }),
25
30
  };
26
- if (title) {
27
- params.title = title;
28
- }
29
- if (icon) {
30
- params.icon = icon;
31
- }
32
- if (cover) {
33
- params.cover = cover;
34
- }
35
- if (is_inline !== undefined) {
36
- params.is_inline = is_inline;
37
- }
38
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
31
  const response = await notion.databases.create(params);
40
32
  return formatResponse(response);
41
33
  }
42
34
  catch (error) {
43
- return handleError(error);
35
+ return handleErrorWithContext(error, notion, {
36
+ exampleType: 'schema',
37
+ });
44
38
  }
45
39
  });
46
40
  }
@@ -1,4 +1,4 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import type { NotionClient } from '../notion-client.js';
2
+ import { type NotionClient } from '../notion-client.js';
3
3
  export declare function registerCreatePageSimple(server: McpServer, notion: NotionClient): void;
4
4
  //# sourceMappingURL=create-page-simple.d.ts.map