Summary
ListTools derives MCP tool annotations from the endpoint's HTTP method, so every non-GET operation is advertised as destructiveHint: true. Notion's search and query endpoints are reads that take a POST body, so they are mislabelled:
| Tool |
Endpoint |
Advertised |
Actual |
API-post-search |
POST /v1/search |
destructiveHint: true |
read-only |
API-query-data-source |
POST /v1/data_sources/{id}/query |
destructiveHint: true |
read-only |
The other 22 tools are annotated correctly — 12 GETs as readOnlyHint, 10 genuine writes as destructiveHint.
Impact
Per the MCP spec, these hints are how clients reason about tool safety. Clients that gate on them force a permission prompt on every Notion search, with no option for a standing approval.
In Claude Desktop specifically, a read-only-annotated tool is auto-approved while a destructive-annotated one prompts every call — so API-retrieve-a-page passes silently but API-post-search interrupts, and the two most common read paths behave inconsistently for no reason the user can see. Searching is typically the first thing a user asks for, so this is the common case, not an edge case.
Root cause
src/openapi-mcp-server/mcp/proxy.ts (~L173–186):
// Look up the HTTP method to determine annotations
const operation = this.openApiLookup[toolNameWithMethod];
const httpMethod = operation?.method?.toLowerCase();
const isReadOnly = httpMethod === 'get';
tools.push({
name: truncatedToolName,
description: method.description,
inputSchema: method.inputSchema as Tool['inputSchema'],
annotations: {
title: this.operationIdToTitle(method.name),
...(isReadOnly
? { readOnlyHint: true }
: { destructiveHint: true }),
},
})
The HTTP verb is a proxy for intent, and it breaks wherever a REST API uses POST for a read — which Notion's own API does for both of its primary query paths.
Secondarily, this conflates "not read-only" with "destructive". Per the spec destructiveHint is only meaningful when readOnlyHint is false, and an additive operation like API-create-a-comment arguably isn't destructive either.
Reproduction
npx @notionhq/notion-mcp-server@2.4.1
# issue a tools/list request
# observe: API-post-search -> annotations.destructiveHint === true
Suggested fix
Keep an explicit set for the POST-shaped reads rather than inferring purely from the verb:
const READ_ONLY_OPERATIONS = new Set(['API-post-search', 'API-query-data-source']);
const isReadOnly = httpMethod === 'get' || READ_ONLY_OPERATIONS.has(truncatedToolName);
Better still would be carrying the intent in the OpenAPI spec itself (e.g. an x-readonly extension) so annotations stay correct as endpoints are added, rather than requiring the list to be maintained by hand.
Happy to open a PR if the maintainers have a preference on which shape they'd take.
Version: @notionhq/notion-mcp-server@2.4.1
Summary
ListToolsderives MCP tool annotations from the endpoint's HTTP method, so every non-GEToperation is advertised asdestructiveHint: true. Notion's search and query endpoints are reads that take aPOSTbody, so they are mislabelled:API-post-searchPOST /v1/searchdestructiveHint: trueAPI-query-data-sourcePOST /v1/data_sources/{id}/querydestructiveHint: trueThe other 22 tools are annotated correctly — 12
GETs asreadOnlyHint, 10 genuine writes asdestructiveHint.Impact
Per the MCP spec, these hints are how clients reason about tool safety. Clients that gate on them force a permission prompt on every Notion search, with no option for a standing approval.
In Claude Desktop specifically, a read-only-annotated tool is auto-approved while a destructive-annotated one prompts every call — so
API-retrieve-a-pagepasses silently butAPI-post-searchinterrupts, and the two most common read paths behave inconsistently for no reason the user can see. Searching is typically the first thing a user asks for, so this is the common case, not an edge case.Root cause
src/openapi-mcp-server/mcp/proxy.ts(~L173–186):The HTTP verb is a proxy for intent, and it breaks wherever a REST API uses
POSTfor a read — which Notion's own API does for both of its primary query paths.Secondarily, this conflates "not read-only" with "destructive". Per the spec
destructiveHintis only meaningful whenreadOnlyHintis false, and an additive operation likeAPI-create-a-commentarguably isn't destructive either.Reproduction
Suggested fix
Keep an explicit set for the POST-shaped reads rather than inferring purely from the verb:
Better still would be carrying the intent in the OpenAPI spec itself (e.g. an
x-readonlyextension) so annotations stay correct as endpoints are added, rather than requiring the list to be maintained by hand.Happy to open a PR if the maintainers have a preference on which shape they'd take.
Version:
@notionhq/notion-mcp-server@2.4.1