UNPKG

notion-mcp-server

Version:

Notion MCP server with token auth: connect Claude Code, Claude Desktop, Cursor, VS Code, Codex and any MCP client to Notion. Search, create and edit pages, query databases, comment and upload files. Runs headless in CI and agents.

198 lines (197 loc) • 9.67 kB
import { APIResponseError } from "@notionhq/client"; import { AuthError } from "../services/auth.js"; /** * Error codes from Notion API * @see https://developers.notion.com/reference/status-codes#error-codes */ export var NotionErrorCode; (function (NotionErrorCode) { NotionErrorCode["InvalidJson"] = "invalid_json"; NotionErrorCode["InvalidRequestUrl"] = "invalid_request_url"; NotionErrorCode["InvalidRequest"] = "invalid_request"; NotionErrorCode["ValidationError"] = "validation_error"; NotionErrorCode["MissingVersion"] = "missing_version"; NotionErrorCode["UnsupportedVersion"] = "unsupported_version"; NotionErrorCode["UnsupportedExport"] = "unsupported_export"; NotionErrorCode["UnsupportedJsonType"] = "unsupported_json_type"; NotionErrorCode["UnsupportedJsonKey"] = "unsupported_json_key"; NotionErrorCode["Unauthorized"] = "unauthorized"; NotionErrorCode["InvalidApiKey"] = "invalid_api_key"; NotionErrorCode["RestrictedResource"] = "restricted_resource"; NotionErrorCode["InsufficientPermissions"] = "insufficient_permissions"; NotionErrorCode["ObjectNotFound"] = "object_not_found"; NotionErrorCode["ConflictError"] = "conflict_error"; NotionErrorCode["AlreadyExists"] = "already_exists"; NotionErrorCode["RateLimited"] = "rate_limited"; NotionErrorCode["InternalServerError"] = "internal_server_error"; NotionErrorCode["ServiceUnavailable"] = "service_unavailable"; NotionErrorCode["GatewayTimeout"] = "gateway_timeout"; NotionErrorCode["DatabaseConnectionUnavailable"] = "database_connection_unavailable"; NotionErrorCode["ServiceOverload"] = "service_overload"; })(NotionErrorCode || (NotionErrorCode = {})); /** * Codes that indicate a transient failure, where trying again may succeed. * Only 429 and 529 mean Notion turned the request away without running it. * A 500, 503 or 504 does not: the request may have run, and Notion's docs say * a write "may still have been saved". So these are safe to retry only for a * request that is safe to repeat (see requestRetryOpts in dispatch/retry.ts); * a write's request retries on RETRYABLE_WRITE_CODES alone. */ export const RETRYABLE_NOTION_CODES = new Set([ NotionErrorCode.RateLimited, NotionErrorCode.ServiceOverload, NotionErrorCode.InternalServerError, NotionErrorCode.ServiceUnavailable, NotionErrorCode.GatewayTimeout, NotionErrorCode.DatabaseConnectionUnavailable, ]); /** * Codes where Notion did not run the request and asks the caller to come back * later. Notion's docs say to retry them for any request, so a write retries * on these and nothing else. */ export const RETRYABLE_WRITE_CODES = new Set([ NotionErrorCode.RateLimited, NotionErrorCode.ServiceOverload, ]); export function isRetryableNotionCode(code) { return code !== undefined && RETRYABLE_NOTION_CODES.has(code); } export function isRetryableWriteCode(code) { return code !== undefined && RETRYABLE_WRITE_CODES.has(code); } const ERROR_MESSAGES = { [NotionErrorCode.InvalidJson]: { message: "The request body could not be decoded as JSON.", fix: "Pass the payload as an object, not as a JSON-encoded string.", }, [NotionErrorCode.InvalidRequestUrl]: { message: "The request URL is not valid.", }, [NotionErrorCode.InvalidRequest]: { message: "This request is not supported.", }, [NotionErrorCode.ValidationError]: { message: "The request body does not match the schema for the expected parameters.", fix: "Notion rejected the request body: read the message for the field it names (the `path` field, when present, points at it), fix that field and retry. notion_describe shows the operation's schema and a working example.", }, [NotionErrorCode.MissingVersion]: { message: "The request is missing the required Notion-Version header.", }, [NotionErrorCode.UnsupportedVersion]: { message: "The specified Notion-Version is not supported.", }, [NotionErrorCode.UnsupportedExport]: { message: "The specified export type is not supported.", }, [NotionErrorCode.UnsupportedJsonType]: { message: "The specified JSON type is not supported.", }, [NotionErrorCode.UnsupportedJsonKey]: { message: "The specified JSON key is not supported.", }, [NotionErrorCode.Unauthorized]: { message: "The bearer token is not valid.", fix: "Set the NOTION_TOKEN environment variable to a valid Notion integration token (starts with `ntn_` or `secret_`).", }, [NotionErrorCode.InvalidApiKey]: { message: "The API key is invalid.", fix: "Generate a new internal integration token in Notion → Settings → Integrations → My integrations.", }, [NotionErrorCode.RestrictedResource]: { message: "The resource is restricted and cannot be accessed with this token.", fix: "In Notion, open Settings → Connections → [your integration] → Capabilities and enable the missing scope (e.g. 'Read user information' for /users endpoints, 'Insert content' for block writes). For pages/databases, also confirm the integration is shared with the resource via the page's ••• → Add connections menu.", }, [NotionErrorCode.InsufficientPermissions]: { message: "The bearer token does not have permission to perform this operation.", fix: "Open the integration in Notion → Settings → Connections and enable the missing capability, then share the target page/database with the integration.", }, [NotionErrorCode.ObjectNotFound]: { message: "The requested resource does not exist.", fix: "Either the ID is wrong, the integration hasn't been shared with the page/database (use the page's ••• → Add connections menu in Notion), or the object is in trash.", }, [NotionErrorCode.ConflictError]: { message: "The transaction could not be completed due to a conflict.", fix: "Retry the operation after a short delay.", }, [NotionErrorCode.AlreadyExists]: { message: "The resource already exists.", }, [NotionErrorCode.RateLimited]: { message: "The request was rate limited.", fix: "Retry later with exponential backoff. Notion limits to ~3 requests per second per integration.", }, [NotionErrorCode.InternalServerError]: { message: "An unexpected error occurred on the Notion servers.", fix: "Retry the operation; if it persists, check status.notion.so. A write may have been saved anyway: read the target back before you repeat it.", }, [NotionErrorCode.ServiceUnavailable]: { message: "The Notion service is unavailable.", fix: "Retry later with exponential backoff. A write may have been saved anyway: read the target back before you repeat it.", }, [NotionErrorCode.GatewayTimeout]: { message: "The Notion gateway timed out before the request could complete.", fix: "Retry later with exponential backoff. A write may have been saved anyway: read the target back before you repeat it.", }, [NotionErrorCode.DatabaseConnectionUnavailable]: { message: "The database connection is unavailable.", fix: "Retry later with exponential backoff.", }, [NotionErrorCode.ServiceOverload]: { message: "Notion is overloaded and did not run the request.", fix: "Retry later with exponential backoff.", }, }; function lookup(code) { return ERROR_MESSAGES[code] ?? { message: "An unknown error occurred." }; } export function toErrorEnvelope(error) { if (error instanceof APIResponseError) { const entry = lookup(error.code); const body = error.body; return { code: error.code, message: entry.message + (error.message && error.message !== entry.message ? ` (${error.message})` : ""), ...(entry.fix ? { fix: entry.fix } : {}), ...(String(error.code) === NotionErrorCode.ValidationError && body ? { path: extractValidationPath(body) } : {}), }; } if (error instanceof AuthError) { return { code: "auth_error", message: `Notion auth failed: ${error.message}`, fix: "Set NOTION_TOKEN in your MCP client config to a personal access token or an integration token, then restart the server.", }; } if (error instanceof Error) { return { code: "internal_error", message: error.message }; } return { code: "unknown_error", message: String(error) }; } // The error of a write that failed after one of its requests had already // changed something in Notion (an append of batch_mixed_blocks, the create // step of upload_file). Repeating the whole call can apply that part twice. export function partlyAppliedError(error) { const fix = "Read the target back to see what was saved, then repeat only the part that is missing: running the whole call again can apply the saved part twice."; return { ...error, message: `${error.message} Part of this call was already applied in Notion before it failed.`, fix: error.fix ? `${fix} ${error.fix}` : fix, }; } function extractValidationPath(body) { if (typeof body !== "object" || body === null) return undefined; const maybe = body.details; if (!Array.isArray(maybe)) return undefined; const first = maybe[0]; if (typeof first === "object" && first !== null && Array.isArray(first.path)) { return first.path; } return undefined; }