Skip to main content
Glama

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

TableJSON Schema
NameRequiredDescriptionDefault
actionYesAction to perform
block_idYesBlock ID
contentNoMarkdown content (for append/update)
positionNoInsert position for append: start (prepend), end (default), after_block (requires after_block_id)
after_block_idNoBlock ID to insert after (when position is after_block)

Implementation Reference

  • 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'
            )
        }
      })()
    }
  • 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
    }
  • 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']
      }
    },
  • 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()
    }
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond annotations (which are minimal), the description discloses that update only works on specific block types, image/file blocks contain signed URLs with 1h expiry, and append supports positions ('start', 'end', 'after_block') with requirements. This adds significant behavioral context.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is concise, front-loaded with purpose, uses bullet points for actions, and every sentence serves a purpose. No wasted words, and critical details are presented efficiently.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given no output schema and moderate complexity, the description covers all actions, parameter constraints, and important behaviors (e.g., update limitations, signed URLs). It does not detail return values, but the information provided is sufficient for correct invocation. Slightly incomplete but mostly adequate.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Input schema has 100% description coverage. The description adds semantic value by clarifying markdown content, explaining position values ('start (prepend)', 'end (default)', 'after_block (requires after_block_id)'), and listing constraints for actions. Baseline is 3 due to coverage, but extra detail justifies 4.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states it reads and modifies block-level content, lists five specific actions with their required and optional parameters, and explicitly differentiates from sibling tool 'pages' which handles page metadata/properties. This meets the 'specific verb+resource+distinguishes from siblings' criteria.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides explicit guidance on when to use this tool vs. alternatives ('Use pages for page metadata/properties'), notes that page IDs are valid block IDs, that update only works on text blocks, and explains image/file block behavior with signed URLs. This offers clear context and exclusions.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/n24q02m/better-notion-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server