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.

321 lines (320 loc) • 14 kB
import { z } from "zod"; import { notionId } from "./id.js"; import { preprocessJson } from "./preprocess.js"; import { NUMBER_FORMAT } from "./number.js"; export const EMPTY_OBJECT_SCHEMA = z.record(z.string(), z.never()).default({}); export const SELECT_COLOR_SCHEMA = z.enum([ "default", "gray", "brown", "orange", "yellow", "green", "blue", "purple", "pink", "red", ]); // Database property schemas // 1. Title property export const TITLE_DB_PROPERTY_SCHEMA = z.object({ type: z.literal("title").describe("Title property type"), title: EMPTY_OBJECT_SCHEMA.describe("There is no additional property configuration."), description: z.string().optional().describe("Property description"), }); // 2. Rich text property export const RICH_TEXT_DB_PROPERTY_SCHEMA = z.object({ type: z.literal("rich_text").describe("Rich text property type"), rich_text: EMPTY_OBJECT_SCHEMA.describe("There is no additional property configuration."), description: z.string().optional().describe("Property description"), }); // 3. Number property export const NUMBER_DB_PROPERTY_SCHEMA = z.object({ type: z.literal("number").describe("Number property type"), number: z .object({ format: NUMBER_FORMAT.describe("Number format"), }) .describe("Number property configuration"), description: z.string().optional().describe("Property description"), }); // 4. Select property export const SELECT_OPTION_SCHEMA = z.object({ name: z.string().describe("Name of the select option"), color: SELECT_COLOR_SCHEMA.optional().describe("Color of the select option"), id: z.string().optional().describe("ID of the select option"), }); export const SELECT_DB_PROPERTY_SCHEMA = z.object({ type: z.literal("select").describe("Select property type"), select: z .object({ options: z .array(SELECT_OPTION_SCHEMA) .optional() .describe("Select options"), }) .describe("Select property configuration"), description: z.string().optional().describe("Property description"), }); // 5. Multi-select property export const MULTI_SELECT_DB_PROPERTY_SCHEMA = z.object({ type: z.literal("multi_select").describe("Multi-select property type"), multi_select: z .object({ options: z .array(SELECT_OPTION_SCHEMA) .optional() .describe("Multi-select options"), }) .describe("Multi-select property configuration"), description: z.string().optional().describe("Property description"), }); // 6. Date property export const DATE_DB_PROPERTY_SCHEMA = z.object({ type: z.literal("date").describe("Date property type"), date: EMPTY_OBJECT_SCHEMA.describe("There is no additional property configuration."), description: z.string().optional().describe("Property description"), }); // 7. People property export const PEOPLE_DB_PROPERTY_SCHEMA = z.object({ type: z.literal("people").describe("People property type"), people: EMPTY_OBJECT_SCHEMA.describe("There is no additional property configuration."), description: z.string().optional().describe("Property description"), }); // 8. Files property export const FILES_DB_PROPERTY_SCHEMA = z.object({ type: z.literal("files").describe("Files property type"), files: EMPTY_OBJECT_SCHEMA.describe("There is no additional property configuration."), description: z.string().optional().describe("Property description"), }); // 9. Checkbox property export const CHECKBOX_DB_PROPERTY_SCHEMA = z.object({ type: z.literal("checkbox").describe("Checkbox property type"), checkbox: EMPTY_OBJECT_SCHEMA.describe("There is no additional property configuration."), description: z.string().optional().describe("Property description"), }); // 10. URL property export const URL_DB_PROPERTY_SCHEMA = z.object({ type: z.literal("url").describe("URL property type"), url: EMPTY_OBJECT_SCHEMA.describe("There is no additional property configuration."), description: z.string().optional().describe("Property description"), }); // 11. Email property export const EMAIL_DB_PROPERTY_SCHEMA = z.object({ type: z.literal("email").describe("Email property type"), email: EMPTY_OBJECT_SCHEMA.describe("There is no additional property configuration."), description: z.string().optional().describe("Property description"), }); // 12. Phone number property export const PHONE_NUMBER_DB_PROPERTY_SCHEMA = z.object({ type: z.literal("phone_number").describe("Phone number property type"), phone_number: EMPTY_OBJECT_SCHEMA.describe("There is no additional property configuration."), description: z.string().optional().describe("Property description"), }); // 13. Formula property export const FORMULA_DB_PROPERTY_SCHEMA = z.object({ type: z.literal("formula").describe("Formula property type"), formula: z .object({ expression: z.string().describe("Formula expression"), }) .describe("Formula property configuration"), description: z.string().optional().describe("Property description"), }); // 14. Relation property export const RELATION_DB_PROPERTY_SCHEMA = z.object({ type: z.literal("relation").describe("Relation property type"), relation: z .object({ data_source_id: notionId() .describe("The ID of the data source this relation refers to"), synced_property_name: z .string() .optional() .describe("Synced property name"), synced_property_id: z.string().optional().describe("Synced property ID"), single_property: EMPTY_OBJECT_SCHEMA.describe("Whether this is a single property relation"), }) .describe("Relation property configuration"), description: z.string().optional().describe("Property description"), }); // 15. Rollup property export const ROLLUP_FUNCTION = z.enum([ "average", "checked", "count_per_group", "count", "count_values", "date_range", "earliest_date", "empty", "latest_date", "max", "median", "min", "not_empty", "percent_checked", "percent_empty", "percent_not_empty", "percent_per_group", "percent_unchecked", "range", "show_original", "show_unique", "sum", "unchecked", "unique", ]); export const ROLLUP_DB_PROPERTY_SCHEMA = z.object({ type: z.literal("rollup").describe("Rollup property type"), rollup: z .object({ relation_property_name: z .string() .describe("Name of the relation property"), relation_property_id: z.string().describe("ID of the relation property"), rollup_property_name: z .string() .describe("Name of the property to roll up"), rollup_property_id: z.string().describe("ID of the property to roll up"), function: ROLLUP_FUNCTION.describe("Rollup function"), }) .describe("Rollup property configuration"), description: z.string().optional().describe("Property description"), }); // 16. Created time property export const CREATED_TIME_DB_PROPERTY_SCHEMA = z.object({ type: z.literal("created_time").describe("Created time property type"), created_time: EMPTY_OBJECT_SCHEMA.describe("There is no additional property configuration."), description: z.string().optional().describe("Property description"), }); // 17. Created by property export const CREATED_BY_DB_PROPERTY_SCHEMA = z.object({ type: z.literal("created_by").describe("Created by property type"), created_by: EMPTY_OBJECT_SCHEMA.describe("There is no additional property configuration."), description: z.string().optional().describe("Property description"), }); // 18. Last edited time property export const LAST_EDITED_TIME_DB_PROPERTY_SCHEMA = z.object({ type: z .literal("last_edited_time") .describe("Last edited time property type"), last_edited_time: EMPTY_OBJECT_SCHEMA.describe("There is no additional property configuration."), description: z.string().optional().describe("Property description"), }); // 19. Last edited by property export const LAST_EDITED_BY_DB_PROPERTY_SCHEMA = z.object({ type: z.literal("last_edited_by").describe("Last edited by property type"), last_edited_by: EMPTY_OBJECT_SCHEMA.describe("There is no additional property configuration."), description: z.string().optional().describe("Property description"), }); // Status property. Notion creates one since 2026-03-19: `status: {}` gives the // default options (Not started, In progress, Done). The three groups are fixed // and managed in Notion, so a request cannot define them; an option joins one // through its own `group` (since 2026-06-22). On update an option sent without // `group` keeps its group, and a new one lands in To-do. export const STATUS_GROUP_NAME_SCHEMA = z.enum(["To-do", "In progress", "Complete"]); // Notion's update request takes an option by name, or by id with the name // optional (dataSources.update); a create request has no id. Sent with an id, // the option keeps its name even when another one is given: the API cannot // rename a status option. The options list replaces the property's options. export const STATUS_OPTION_SCHEMA = z .object({ id: z .string() .optional() .describe("ID of an existing status option on update_data_source, to recolor it or move it to another group. Notion keeps the option's name, so a status option can't be renamed through the API. With an id, name is optional."), name: z .string() .optional() .describe("Name of the status option. Unique regardless of case; no commas. Required unless id is set."), color: SELECT_COLOR_SCHEMA.optional().describe("Color of the status option"), description: z.string().nullable().optional().describe("Description of the status option"), group: STATUS_GROUP_NAME_SCHEMA.optional().describe("Group the option belongs to. Omitted: an existing option keeps its group, a new one goes to To-do."), }) .refine((option) => option.name !== undefined || option.id !== undefined, { message: "A status option needs a name, or the id of an existing option on update_data_source.", }); export const STATUS_DB_PROPERTY_SCHEMA = z.object({ type: z.literal("status").describe("Status property type"), status: z .object({ options: z .array(STATUS_OPTION_SCHEMA) .optional() .describe("Status options. Omit for Notion's defaults: Not started, In progress, Done. On update_data_source the list replaces the property's options: list every option to keep, since one left out is removed."), }) .default({}) .describe("Status property configuration"), description: z.string().optional().describe("Property description"), }); export const BUTTON_DB_PROPERTY_SCHEMA = z.object({ type: z.literal("button"), button: EMPTY_OBJECT_SCHEMA, description: z.string().optional(), }); export const UNIQUE_ID_DB_PROPERTY_SCHEMA = z.object({ type: z.literal("unique_id"), unique_id: z .object({ prefix: z .string() .regex(/^[A-Za-z][A-Za-z0-9-]{1,9}$/, "prefix must start with a letter, then 1–9 more letters/digits/hyphens (total length 2–10)") .nullable() .optional() .describe("Prefix shown before the auto-incrementing number. 2–10 chars total, must start with a letter, alphanumeric + hyphen only. Single-letter prefixes are rejected by Notion."), }) .describe("Unique ID property configuration"), description: z.string().optional(), }); export const VERIFICATION_DB_PROPERTY_SCHEMA = z.object({ type: z.literal("verification"), verification: EMPTY_OBJECT_SCHEMA, description: z.string().optional(), }); // Combined database property schema. This is a property *definition* only: // the null-deletes-the-property form Notion takes on dataSources.update is // added at update_data_source, because create_database (initial_data_source) // has no such form and update_database no longer carries properties at all. const DATABASE_PROPERTY_UNION = z .discriminatedUnion("type", [ TITLE_DB_PROPERTY_SCHEMA, RICH_TEXT_DB_PROPERTY_SCHEMA, NUMBER_DB_PROPERTY_SCHEMA, SELECT_DB_PROPERTY_SCHEMA, MULTI_SELECT_DB_PROPERTY_SCHEMA, STATUS_DB_PROPERTY_SCHEMA, DATE_DB_PROPERTY_SCHEMA, PEOPLE_DB_PROPERTY_SCHEMA, FILES_DB_PROPERTY_SCHEMA, CHECKBOX_DB_PROPERTY_SCHEMA, URL_DB_PROPERTY_SCHEMA, EMAIL_DB_PROPERTY_SCHEMA, PHONE_NUMBER_DB_PROPERTY_SCHEMA, FORMULA_DB_PROPERTY_SCHEMA, RELATION_DB_PROPERTY_SCHEMA, ROLLUP_DB_PROPERTY_SCHEMA, CREATED_TIME_DB_PROPERTY_SCHEMA, CREATED_BY_DB_PROPERTY_SCHEMA, LAST_EDITED_TIME_DB_PROPERTY_SCHEMA, LAST_EDITED_BY_DB_PROPERTY_SCHEMA, BUTTON_DB_PROPERTY_SCHEMA, UNIQUE_ID_DB_PROPERTY_SCHEMA, VERIFICATION_DB_PROPERTY_SCHEMA, ]) .describe("Union of all possible database property types"); const DATABASE_PROPERTY_TYPES = new Set(DATABASE_PROPERTY_UNION.options.map((option) => option.shape.type.value)); // Notion itself takes `{ select: { options } }` without a `type`; the body key // says which kind it is. Fill `type` in from the sole body key so a definition // needs no more than the API does, but never override an explicit `type`. export function inferPropertyDefinitionType(val) { const v = preprocessJson(val); if (typeof v !== "object" || v === null || Array.isArray(v)) return v; const obj = v; if (typeof obj.type === "string") return v; const bodies = Object.keys(obj).filter((k) => DATABASE_PROPERTY_TYPES.has(k) && typeof obj[k] === "object" && obj[k] !== null); return bodies.length === 1 ? { ...obj, type: bodies[0] } : v; } export const DATABASE_PROPERTY_SCHEMA = z.preprocess(inferPropertyDefinitionType, DATABASE_PROPERTY_UNION);