Skip to content

Tool annotations are derived from the HTTP verb, mislabelling search/query as destructive #333

Description

@danowenuk

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

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