blocks
Read, append, update, or delete block-level content in Notion pages. Supports text, headings, lists, and code blocks with markdown input.
Instructions
Read and modify block-level content within pages.
Actions (required params -> optional):
get (block_id): retrieve single block
children (block_id): list child blocks
append (block_id, content -> position, after_block_id): add markdown content at position
update (block_id, content): replace text block content
delete (block_id): remove block
Use pages for page metadata/properties. Page IDs are valid block IDs. update only works on text blocks (paragraph, headings, lists, quote, to_do, code). Image/file blocks contain signed URLs (1h expiry). append supports position: "start" (prepend), "end" (default), "after_block" (requires after_block_id).
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| action | Yes | Action to perform | |
| block_id | Yes | Block ID | |
| content | No | Markdown content (for append/update) | |
| position | No | Insert position for append: start (prepend), end (default), after_block (requires after_block_id) | |
| after_block_id | No | Block ID to insert after (when position is after_block) |
Implementation Reference
- src/tools/composite/blocks.ts:23-185 (handler)Main blocks tool handler function implementing get, children, append, update, delete actions for Notion blocks. Uses Notion API Client and returns unified results with markdown support.
export async function blocks(notion: Client, input: BlocksInput): Promise<any> { return withErrorHandling(async () => { if (!input.block_id) { throw new NotionMCPError('block_id required', 'VALIDATION_ERROR', 'Provide block_id') } switch (input.action) { case 'get': { const block: any = await notion.blocks.retrieve({ block_id: input.block_id }) return { action: 'get', block_id: block.id, type: block.type, has_children: block.has_children, archived: block.archived, block } } case 'children': { const blocksList = await autoPaginate((cursor) => notion.blocks.children.list({ block_id: input.block_id, start_cursor: cursor, page_size: 100 }) ) // Recursively fetch children for blocks that need them (tables, toggles, columns) await populateDeepChildren(notion, blocksList as any[]) const markdown = blocksToMarkdown(blocksList as any) return { action: 'children', block_id: input.block_id, total_children: blocksList.length, markdown, blocks: blocksList } } case 'append': { if (!input.content) { throw new NotionMCPError('content required for append', 'VALIDATION_ERROR', 'Provide markdown content') } if (input.position === 'after_block' && !input.after_block_id) { throw new NotionMCPError( 'after_block_id required when position is after_block', 'VALIDATION_ERROR', 'Provide after_block_id with the block ID to insert after' ) } const blocksList = markdownToBlocks(input.content) const appendParams: any = { block_id: input.block_id, children: blocksList as any } if (input.position === 'start') { appendParams.position = { type: 'start' } } else if (input.position === 'after_block' && input.after_block_id) { appendParams.position = { type: 'after_block', after_block: { id: input.after_block_id } } } await notion.blocks.children.append(appendParams) return { action: 'append', block_id: input.block_id, appended_count: blocksList.length } } case 'update': { if (!input.content) { throw new NotionMCPError('content required for update', 'VALIDATION_ERROR', 'Provide markdown content') } const block: any = await notion.blocks.retrieve({ block_id: input.block_id }) const blockType = block.type const newBlocks = markdownToBlocks(input.content) if (newBlocks.length === 0) { throw new NotionMCPError('Content must produce at least one block', 'VALIDATION_ERROR', 'Invalid markdown') } const newContent = newBlocks[0] // Validate block type match if (newContent.type !== blockType) { throw new NotionMCPError( `Block type mismatch: cannot update ${blockType} with content that parses to ${newContent.type}`, 'VALIDATION_ERROR', `Provide markdown that parses to ${blockType}` ) } const updatePayload: any = {} // Build update based on block type if ( [ 'paragraph', 'heading_1', 'heading_2', 'heading_3', 'bulleted_list_item', 'numbered_list_item', 'quote', 'to_do', 'code' ].includes(blockType) ) { if (blockType === 'to_do') { updatePayload.to_do = { rich_text: (newContent as any).to_do?.rich_text || [], checked: (newContent as any).to_do?.checked ?? block.to_do?.checked ?? false } } else if (blockType === 'code') { updatePayload.code = { rich_text: (newContent as any).code?.rich_text || [], language: (newContent as any).code?.language || block.code?.language || 'plain text' } } else { updatePayload[blockType] = { rich_text: (newContent as any)[blockType]?.rich_text || [] } } } else { throw new NotionMCPError( `Block type '${blockType}' cannot be updated`, 'VALIDATION_ERROR', 'Only text-based blocks (paragraph, headings, lists, quote, to_do, code) can be updated' ) } await notion.blocks.update({ block_id: input.block_id, ...updatePayload } as any) return { action: 'update', block_id: input.block_id, type: blockType, updated: true } } case 'delete': { await notion.blocks.delete({ block_id: input.block_id }) return { action: 'delete', block_id: input.block_id, deleted: true } } default: throw new NotionMCPError( `Unknown action: ${input.action}`, 'VALIDATION_ERROR', 'Supported actions: get, children, append, update, delete' ) } })() } - src/tools/composite/blocks.ts:11-17 (schema)BlocksInput interface defining the input schema with action (get/children/append/update/delete), block_id, content (markdown), position, and after_block_id fields.
export interface BlocksInput { action: 'get' | 'children' | 'append' | 'update' | 'delete' block_id: string content?: string // Markdown format position?: 'start' | 'end' | 'after_block' after_block_id?: string } - src/tools/registry.ts:186-216 (registration)Tool registration in the TOOLS array: defines name='blocks', description, annotations and inputSchema for MCP tool listing.
name: 'blocks', description: 'Read and modify block-level content within pages.\n\nActions (required params -> optional):\n- get (block_id): retrieve single block\n- children (block_id): list child blocks\n- append (block_id, content -> position, after_block_id): add markdown content at position\n- update (block_id, content): replace text block content\n- delete (block_id): remove block\n\nUse `pages` for page metadata/properties. Page IDs are valid block IDs. update only works on text blocks (paragraph, headings, lists, quote, to_do, code). Image/file blocks contain signed URLs (1h expiry). append supports position: "start" (prepend), "end" (default), "after_block" (requires after_block_id).', annotations: { title: 'Blocks', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['get', 'children', 'append', 'update', 'delete'], description: 'Action to perform' }, block_id: { type: 'string', description: 'Block ID' }, content: { type: 'string', description: 'Markdown content (for append/update)' }, position: { type: 'string', enum: ['start', 'end', 'after_block'], description: 'Insert position for append: start (prepend), end (default), after_block (requires after_block_id)' }, after_block_id: { type: 'string', description: 'Block ID to insert after (when position is after_block)' } }, required: ['action', 'block_id'] } }, - src/tools/registry.ts:528-530 (registration)Import and routing of the blocks tool: imports from './composite/blocks.js' and dispatches calls to blocks(notion, args) in the CallToolRequestSchema handler.
case 'blocks': result = await blocks(notion, args as any) break - markdownToBlocks function used by the blocks tool to convert markdown content to Notion block format for append/update actions.
export function markdownToBlocks(markdown: string): NotionBlock[] { const parser = new MarkdownParser(markdown) return parser.parse() }