Skip to content

API-update-a-block 400s for every block type: block-type key nested under type instead of body root #342

Description

@adwung

Summary

API-update-a-block (PATCH /v1/blocks/{block_id}) fails with 400 validation_error for every block type. The tool nests the block-type object under a type key in the request body, but the Notion API expects the block-type key at the body root.

Confirmed on @notionhq/notion-mcp-server@2.5.1 (latest) and on main.

Root cause

Two facts combine:

1. The request body is the tool arguments, verbatim. In src/openapi-mcp-server/client/http-client.ts:

const bodyParams: Record<string, any> = formData || { ...params }   // line ~171
// ...
const response = await operationFn(urlParameters, hasBody ? bodyParams : undefined, requestConfig)  // line ~218

After path params (block_id) are stripped, the remaining args are serialized straight into the body.

2. The OpenAPI spec wraps the payload under a type object. In scripts/notion-openapi.json, operation update-a-block:

{
  "type": "object",
  "properties": {
    "type":     { "type": "object", "description": "The block object `type` value ...", "properties": {} },
    "archived": { "type": "boolean", "default": true }
  }
}

So a caller sends { block_id, type: { paragraph: { rich_text: [...] } } } and the proxy forwards { "type": { "paragraph": {...} } } to Notion. Notion has no settable root type property and finds no block-type key at the root, so it rejects the request and lists every block type as undefined.

By contrast, patch-block-children exposes children as a root property whose items are full block objects carrying their own type key — which is why it works.

Reproduction

Update any block, e.g. a bulleted_list_item:

  • Via the MCP tool — args { "block_id": "...", "type": { "bulleted_list_item": { "rich_text": [{ "type": "text", "text": { "content": "hi" } }] } } }
    400 validation_error: body.bulleted_list_item should be defined, instead was undefined (every block type listed likewise).
  • Direct REST PATCH with the same inner body at the root{ "bulleted_list_item": { "rich_text": [{ "type": "text", "text": { "content": "hi" } }] } }
    200 OK, block updated.

The passing direct call proves the payload and the API are correct; the 400 comes solely from the MCP layer nesting the block-type key under type. Notion's own error message (demanding body.<block_type> at the root) is the proof that the wrapper is wrong.

Proposed fixes

A — spec-level (preferred): In scripts/notion-openapi.json, replace the single type object property on update-a-block with the real root-level shape Notion accepts — the updatable block-type properties (paragraph, heading_1..3, bulleted_list_item, numbered_list_item, to_do, toggle, quote, callout, code, embed, image, ... each an object with rich_text / checked / etc.) plus archived. This mirrors the block-type keys already used inside patch-block-children's children items, needs no proxy code changes, and fixes the misleading advertised schema.

B — code-level: Keep the spec's type convenience field but unwrap it in the proxy for this operation. After bodyParams is built in http-client.ts:

if (operation.operationId === 'update-a-block' && bodyParams.type && typeof bodyParams.type === 'object') {
  const { type, ...rest } = bodyParams
  bodyParams = { ...rest, ...type }
}

Fix A is cleaner (no operation-specific branching in a generic proxy) and also corrects the tool's advertised schema; Fix B is a smaller, backward-tolerant patch.

Environment

  • @notionhq/notion-mcp-server 2.5.1 (also verified on main)
  • Notion-Version 2022-06-28

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions