Notion MCP Server
by v-3
Verified
- src
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import { Client } from "@notionhq/client";
import { z } from "zod";
// Initialize Notion client
const notion = new Client({
auth: process.env.NOTION_API_KEY,
});
// Validation schemas
const schemas = {
notionTitle: z.object({
type: z.literal("title"),
title: z.array(
z.object({
plain_text: z.string(),
}),
),
}),
notionPage: z.object({
id: z.string(),
url: z.string(),
properties: z.record(
z.union([
z.object({
type: z.literal("title"),
title: z.array(
z.object({
plain_text: z.string(),
}),
),
}),
z.any(),
]),
),
}),
toolInputs: {
searchPages: z.object({
query: z.string(),
}),
readPage: z.object({
pageId: z.string(),
}),
createPage: z.object({
title: z.string().optional(),
content: z.string().optional(),
parentPageId: z.string(),
properties: z.record(z.any()).optional()
}),
updatePage: z.object({
pageId: z.string(),
content: z.string(),
type: z.enum([
"paragraph",
"heading_1",
"heading_2",
"heading_3",
"bulleted_list_item",
"numbered_list_item",
"to_do",
"image"
]).optional(),
mode: z.enum(["replace", "append", "merge"]).default("replace"), // Add this
position: z.enum(["start", "end"]).default("end") // Add this
}),
retrieveDatabase: z.object({
databaseId: z.string(),
}),
updateDatabase: z.object({
databaseId: z.string(),
title: z.string().optional(),
description: z.string().optional(),
properties: z.record(z.any()).optional(),
}),
},
databaseProperties: z.record(z.union([
z.object({ title: z.object({}) }),
z.object({ rich_text: z.object({}) }),
z.object({ number: z.object({ format: z.string().optional() }) }),
z.object({
select: z.object({
options: z.array(
z.object({
name: z.string(),
color: z.string().optional()
})
).optional()
})
}),
z.object({
multi_select: z.object({
options: z.array(
z.object({
name: z.string(),
color: z.string().optional()
})
).optional()
})
}),
z.object({ date: z.object({}) }),
z.object({ checkbox: z.object({}) })
])),
};
// Add this after your schemas
function formatError(error: any): string {
console.error('Full error:', JSON.stringify(error, null, 2));
if (error.status === 404) {
return `Resource not found. Please check the provided ID. Details: ${error.body?.message || error.message}`;
}
if (error.status === 401) {
return `Authentication error. Please check your API token. Details: ${error.body?.message || error.message}`;
}
if (error.status === 400) {
return `Bad request. Details: ${error.body?.message || error.message}`;
}
if (error.code) {
return `API Error (${error.code}): ${error.body?.message || error.message}`;
}
return error.body?.message || error.message || "An unknown error occurred";
}
// Tool definitions
const TOOL_DEFINITIONS = [
{
name: "search_pages",
description: "Search through Notion pages",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query",
},
},
required: ["query"],
},
},
{
name: "read_page",
description: "Read a regular page's content (not for databases - use retrieve_database for databases). Shows block IDs with their types (needed for block operations)",
inputSchema: {
type: "object",
properties: {
pageId: {
type: "string",
description: "ID of the page to read",
},
},
required: ["pageId"],
},
},
{
name: "create_page",
description: "Create a new page or database item. For database items, include 'properties' matching database schema. For pages, use 'title' and 'content'",
inputSchema: {
type: "object",
properties: {
title: {
type: "string",
description: "Page title (optional)",
},
content: {
type: "string",
description: "Page content in markdown format (optional)",
},
parentPageId: {
type: "string",
description: "ID of the parent page where this page will be created",
},
properties: {
type: "object",
description: "Additional properties for database items (optional)",
}
},
required: ["parentPageId"],
},
},
{
name: "update_page",
description: "Update an existing Notion page",
inputSchema: {
type: "object",
properties: {
pageId: {
type: "string",
description: "ID of the page to update",
},
content: {
type: "string",
description: "New content to append",
},
type: {
type: "string",
enum: ["paragraph", "task", "todo", "heading", "image"],
description: "Type of content to append",
optional: true,
},
mode: { // Add this
type: "string",
enum: ["replace", "append", "merge"],
description: "Update mode: replace all content, append to existing, or merge",
optional: true,
},
position: { // Add this
type: "string",
enum: ["start", "end"],
description: "Position for merge mode: start or end",
optional: true,
}
},
required: ["pageId", "content"],
},
},
{
name: "retrieve_comments",
description: "Get all comments from a page",
inputSchema: {
type: "object",
properties: {
pageId: {
type: "string",
description: "ID of the page",
},
startCursor: {
type: "string",
description: "Pagination cursor",
},
pageSize: {
type: "number",
description: "Number of comments to retrieve (max 100)",
},
},
required: ["pageId"],
},
},
{
name: "add_comment",
description: "Add a comment to a page",
inputSchema: {
type: "object",
properties: {
pageId: {
type: "string",
description: "ID of the page to comment on",
},
content: {
type: "string",
description: "Comment text",
},
},
required: ["pageId", "content"],
},
},
{
name: "create_database",
description: "Create a new database in a page",
inputSchema: {
type: "object",
properties: {
parentPageId: {
type: "string",
description: "ID of the parent page",
},
title: {
type: "string",
description: "Database title",
},
properties: {
type: "object",
description: "Database schema properties",
},
},
required: ["parentPageId", "title", "properties"],
},
},
{
name: "query_database",
description: "Query a database",
inputSchema: {
type: "object",
properties: {
databaseId: {
type: "string",
description: "ID of the database",
},
filter: {
type: "object",
description: "Filter conditions",
},
sort: {
type: "object",
description: "Sort conditions",
},
},
required: ["databaseId"],
},
},
{
name: "update_block",
description: "Update a block's content (must use same type as original block, use read_page first to get block IDs and types)",
inputSchema: {
type: "object",
properties: {
blockId: {
type: "string",
description: "ID of the block to update",
},
content: {
type: "string",
description: "New content for the block",
},
type: {
type: "string",
enum: [
"paragraph",
"heading_1",
"heading_2",
"heading_3",
"bulleted_list_item",
"numbered_list_item",
],
description: "Type of block",
},
},
required: ["blockId", "content"],
},
},
{
name: "delete_block",
description: "Delete a specific block from a page",
inputSchema: {
type: "object",
properties: {
blockId: {
type: "string",
description: "ID of the block to delete",
},
},
required: ["blockId"],
},
},
{
name: "update_database_item",
description: "Update a database item's properties (use query_database first to see required property structure)",
inputSchema: {
type: "object",
properties: {
pageId: {
type: "string",
description: "ID of the database item (page) to update",
},
properties: {
type: "object",
description: "Properties to update",
},
},
required: ["pageId", "properties"],
},
},
// Add these to TOOL_DEFINITIONS
{
name: "retrieve_database",
description: "Retrieve a database's metadata",
inputSchema: {
type: "object",
properties: {
databaseId: {
type: "string",
description: "ID of the database to retrieve",
},
},
required: ["databaseId"],
},
},
{
name: "update_database",
description: "Update a database's properties",
inputSchema: {
type: "object",
properties: {
databaseId: {
type: "string",
description: "ID of the database to update",
},
title: {
type: "string",
description: "New title for the database",
},
description: {
type: "string",
description: "New description for the database",
},
properties: {
type: "object",
description: "Properties schema to update",
},
},
required: ["databaseId"],
},
},
];
// Tool implementation handlers
const toolHandlers = {
async search_pages(args: unknown) {
const { query } = schemas.toolInputs.searchPages.parse(args);
console.error(`Searching for: ${query}`);
const response = await notion.search({
query,
filter: { property: "object", value: "page" },
page_size: 10,
});
if (!response.results || response.results.length === 0) {
return {
content: [
{
type: "text" as const,
text: `No pages found matching "${query}"`,
},
],
};
}
const formattedResults = response.results
.map((page: any) => {
let title = "Untitled";
try {
// Extract title from URL
const urlMatch = page.url.match(/\/([^/]+)-[^/]+$/);
if (urlMatch) {
title = decodeURIComponent(urlMatch[1].replace(/-/g, ' '));
}
// If no title from URL or it's still "Untitled", try properties
if (title === "Untitled" && page.properties) {
const titleProperty = page.properties.title || page.properties.Name;
if (titleProperty?.title?.[0]?.plain_text) {
title = titleProperty.title[0].plain_text;
}
}
} catch (e) {
console.error("Error extracting title:", e);
}
return `• ${title}\n Link: ${page.url}`;
})
.join("\n\n");
return {
content: [
{
type: "text" as const,
text: `Found ${response.results.length} pages matching "${query}":\n\n${formattedResults}`,
},
],
};
},
async read_page(args: unknown) {
const { pageId } = schemas.toolInputs.readPage.parse(args);
try {
const [blocksResponse, pageResponse] = await Promise.all([
notion.blocks.children.list({ block_id: pageId }),
notion.pages.retrieve({ page_id: pageId }),
]);
const page = schemas.notionPage.parse(pageResponse);
// Get title
const titleProp = Object.values(page.properties).find((prop) => prop.type === "title");
const title = titleProp?.type === "title" ? titleProp.title[0]?.plain_text || "Untitled" : "Untitled";
// Process blocks and collect child pages/databases
const childPages: string[] = [];
const childDatabases: string[] = [];
const contentBlocks: string[] = [];
for (const block of blocksResponse.results as Array<{ type: string; id: string;[key: string]: any }>) {
const type = block.type;
if (type === "child_page") {
childPages.push(`📄 ${block.child_page.title || "Untitled Page"} (ID: ${block.id.replace(/-/g, "")})`);
continue;
}
if (type === "child_database") {
childDatabases.push(`📊 ${block.child_database.title || "Untitled Database"} (ID: ${block.id.replace(/-/g, "")})`);
continue;
}
const textContent = block[type]?.rich_text?.map((text: any) => text.plain_text).join("") || "";
let formattedContent = "";
switch (type) {
case "paragraph":
case "heading_1":
case "heading_2":
case "heading_3":
formattedContent = textContent;
break;
case "bulleted_list_item":
case "numbered_list_item":
formattedContent = "• " + textContent;
break;
case "to_do":
const checked = block.to_do?.checked ? "[x]" : "[ ]";
formattedContent = checked + " " + textContent;
break;
case "code":
formattedContent = "```\n" + textContent + "\n```";
break;
default:
formattedContent = textContent;
}
if (formattedContent) {
contentBlocks.push(formattedContent);
}
}
// Combine all content
let output = `# ${title}\n\n`;
if (contentBlocks.length > 0) {
output += contentBlocks.join("\n") + "\n\n";
}
if (childPages.length > 0) {
output += "## Child Pages\n" + childPages.join("\n") + "\n\n";
}
if (childDatabases.length > 0) {
output += "## Child Databases\n" + childDatabases.join("\n") + "\n";
}
return {
content: [
{
type: "text" as const,
text: output.trim(),
},
],
};
} catch (error) {
console.error("Error reading page:", error);
return {
content: [
{
type: "text" as const,
text: formatError(error),
},
],
};
}
},
async create_page(args: unknown) {
const { title, content, parentPageId, properties } = args as any;
try {
// First try to retrieve as database to check if it's a database parent
let isDatabase = false;
try {
await notion.databases.retrieve({ database_id: parentPageId });
isDatabase = true;
} catch {
// If not a database, verify it's a valid page
await notion.pages.retrieve({ page_id: parentPageId });
}
// Set up properties based on whether it's a database or page
const pageProperties = isDatabase ? properties : {
title: {
type: "title",
title: [
{
type: "text",
text: {
content: title || "",
},
},
],
}
};
// Parse content into blocks
const parseBlocks = (content: string) => {
const lines = content.split('\n');
const blocks: any[] = [];
let currentCodeBlock: any = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmedLine = line.trim();
// Handle code blocks
if (trimmedLine.startsWith('```')) {
if (currentCodeBlock) {
// End code block
blocks.push(currentCodeBlock);
currentCodeBlock = null;
} else {
// Start code block
const language = trimmedLine.slice(3).trim();
currentCodeBlock = {
object: "block",
type: "code",
code: {
rich_text: [],
language: language || "plain text"
}
};
}
continue;
}
if (currentCodeBlock) {
// Add line to current code block
currentCodeBlock.code.rich_text.push({
type: "text",
text: { content: line }
});
continue;
}
// Handle other block types
let block: any = null;
if (trimmedLine === '') {
block = {
object: "block",
type: "paragraph",
paragraph: { rich_text: [] }
};
} else if (trimmedLine.startsWith('# ')) {
block = {
object: "block",
type: "heading_1",
heading_1: {
rich_text: [{
type: "text",
text: { content: trimmedLine.substring(2) }
}]
}
};
} else if (trimmedLine.startsWith('## ')) {
block = {
object: "block",
type: "heading_2",
heading_2: {
rich_text: [{
type: "text",
text: { content: trimmedLine.substring(3) }
}]
}
};
} else if (trimmedLine.startsWith('### ')) {
block = {
object: "block",
type: "heading_3",
heading_3: {
rich_text: [{
type: "text",
text: { content: trimmedLine.substring(4) }
}]
}
};
} else if (trimmedLine.startsWith('- [ ] ')) {
block = {
object: "block",
type: "to_do",
to_do: {
rich_text: [{
type: "text",
text: { content: trimmedLine.substring(6) }
}],
checked: false
}
};
} else if (trimmedLine.startsWith('- [x] ')) {
block = {
object: "block",
type: "to_do",
to_do: {
rich_text: [{
type: "text",
text: { content: trimmedLine.substring(6) }
}],
checked: true
}
};
} else if (trimmedLine.startsWith('- ')) {
block = {
object: "block",
type: "bulleted_list_item",
bulleted_list_item: {
rich_text: [{
type: "text",
text: { content: trimmedLine.substring(2) }
}]
}
};
} else if (trimmedLine.startsWith('> ')) {
block = {
object: "block",
type: "quote",
quote: {
rich_text: [{
type: "text",
text: { content: trimmedLine.substring(2) }
}]
}
};
} else if (trimmedLine.startsWith('---')) {
block = {
object: "block",
type: "divider",
divider: {}
};
} else if (trimmedLine.match(/^!\[.*\]\(.*\)$/)) {
// Image in markdown format: 
const match = trimmedLine.match(/^!\[(.*)\]\((.*)\)$/);
if (match) {
block = {
object: "block",
type: "image",
image: {
type: "external",
external: { url: match[2] },
caption: match[1] ? [{
type: "text",
text: { content: match[1] }
}] : []
}
};
}
} else {
block = {
object: "block",
type: "paragraph",
paragraph: {
rich_text: [{
type: "text",
text: { content: line }
}]
}
};
}
if (block) {
blocks.push(block);
}
}
// Add any remaining code block
if (currentCodeBlock) {
blocks.push(currentCodeBlock);
}
return blocks;
};
const newPage = await notion.pages.create({
parent: isDatabase ? {
type: "database_id",
database_id: parentPageId
} : {
type: "page_id",
page_id: parentPageId
},
properties: pageProperties,
children: content ? parseBlocks(content) : []
});
return {
content: [
{
type: "text" as const,
text: `Successfully created page with ID: ${newPage.id}`,
},
],
};
} catch (error) {
console.error("Error creating page:", error);
return {
content: [
{
type: "text" as const,
text: formatError(error),
},
],
};
}
},
async update_page(args: unknown) {
const { pageId, content: newContent, type = "paragraph", mode = "replace", position = "end" } = schemas.toolInputs.updatePage.parse(args);
try {
// Get existing blocks and delete them
const blocks = await notion.blocks.children.list({ block_id: pageId });
const backup = blocks.results;
// Helper to create blocks array based on content type and handle multiple lines
const createBlocks = (content: string, _type: string): any[] => {
const lines = content.split('\n');
const blocks: any[] = [];
let currentCodeBlock: any = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmedLine = line.trim();
// Handle code blocks
if (trimmedLine.startsWith('```')) {
if (currentCodeBlock) {
// End code block
blocks.push(currentCodeBlock);
currentCodeBlock = null;
} else {
// Start code block
const language = trimmedLine.slice(3).trim();
currentCodeBlock = {
object: "block",
type: "code",
code: {
rich_text: [],
language: language || "plain text"
}
};
}
continue;
}
if (currentCodeBlock) {
// Add line to current code block
currentCodeBlock.code.rich_text.push({
type: "text",
text: { content: line }
});
continue;
}
// Handle other block types
let block: any = null;
if (trimmedLine === '') {
block = {
object: "block",
type: "paragraph",
paragraph: { rich_text: [] }
};
} else if (trimmedLine.startsWith('# ')) {
block = {
object: "block",
type: "heading_1",
heading_1: {
rich_text: [{
type: "text",
text: { content: trimmedLine.substring(2) }
}]
}
};
} else if (trimmedLine.startsWith('## ')) {
block = {
object: "block",
type: "heading_2",
heading_2: {
rich_text: [{
type: "text",
text: { content: trimmedLine.substring(3) }
}]
}
};
} else if (trimmedLine.startsWith('### ')) {
block = {
object: "block",
type: "heading_3",
heading_3: {
rich_text: [{
type: "text",
text: { content: trimmedLine.substring(4) }
}]
}
};
} else if (trimmedLine.startsWith('- [ ] ')) {
block = {
object: "block",
type: "to_do",
to_do: {
rich_text: [{
type: "text",
text: { content: trimmedLine.substring(6) }
}],
checked: false
}
};
} else if (trimmedLine.startsWith('- [x] ')) {
block = {
object: "block",
type: "to_do",
to_do: {
rich_text: [{
type: "text",
text: { content: trimmedLine.substring(6) }
}],
checked: true
}
};
} else if (trimmedLine.startsWith('- ')) {
block = {
object: "block",
type: "bulleted_list_item",
bulleted_list_item: {
rich_text: [{
type: "text",
text: { content: trimmedLine.substring(2) }
}]
}
};
} else if (trimmedLine.startsWith('> ')) {
block = {
object: "block",
type: "quote",
quote: {
rich_text: [{
type: "text",
text: { content: trimmedLine.substring(2) }
}]
}
};
} else if (trimmedLine.startsWith('---')) {
block = {
object: "block",
type: "divider",
divider: {}
};
} else if (trimmedLine.match(/^!\[.*\]\(.*\)$/)) {
// Image in markdown format: 
const match = trimmedLine.match(/^!\[(.*)\]\((.*)\)$/);
if (match) {
block = {
object: "block",
type: "image",
image: {
type: "external",
external: { url: match[2] },
caption: match[1] ? [{
type: "text",
text: { content: match[1] }
}] : []
}
};
}
} else {
block = {
object: "block",
type: "paragraph",
paragraph: {
rich_text: [{
type: "text",
text: { content: line }
}]
}
};
}
if (block) {
blocks.push(block);
}
}
// Add any remaining code block
if (currentCodeBlock) {
blocks.push(currentCodeBlock);
}
return blocks;
};
if (mode === "replace" || mode === "merge") {
if (backup.length > 0) {
console.warn(`Deleting ${backup.length} existing blocks`);
}
for (const block of backup) {
await notion.blocks.delete({ block_id: block.id });
}
} try {
const newBlocks = createBlocks(newContent, type);
if (mode === "merge") {
const mergedBlocks = position === "start"
? [...newBlocks, ...backup]
: [...backup, ...newBlocks];
await notion.blocks.children.append({
block_id: pageId,
children: mergedBlocks,
});
} else {
await notion.blocks.children.append({
block_id: pageId,
children: newBlocks,
});
}
return {
content: [
{
type: "text" as const,
text: `Successfully updated page: ${pageId}`,
},
],
};
}catch(error){
throw error;
}}catch (error) {
console.error("Error updating page:", error);
return {
content: [
{
type: "text" as const,
text: formatError(error),
},
],
};
}
},
async add_comment(args: unknown) {
const { pageId, content } = args as any;
try {
const response = await notion.comments.create({
parent: { page_id: pageId },
rich_text: [
{
type: "text",
text: { content },
},
],
});
return {
content: [
{
type: "text" as const,
text: `Successfully added comment`,
},
],
};
} catch (error) {
console.error("Error adding comment:", error);
return {
content: [
{
type: "text" as const,
text: `Error adding comment: ${formatError(error)}`,
},
],
};
}
},
async retrieve_comments(args: unknown) {
const { pageId, startCursor, pageSize } = args as any;
try {
const response = await notion.comments.list({
block_id: pageId,
start_cursor: startCursor,
page_size: pageSize,
});
return {
content: [
{
type: "text" as const,
text: JSON.stringify(response.results, null, 2),
},
],
};
} catch (error) {
console.error("Error retrieving comments:", error);
return {
content: [
{
type: "text" as const,
text: `Error retrieving comments: ${formatError(error)}`,
},
],
};
}
},
async create_database(args: unknown) {
const { parentPageId, title, properties } = args as any;
try {
const response = await notion.databases.create({
parent: {
type: "page_id",
page_id: parentPageId,
},
title: [
{
type: "text",
text: {
content: title,
},
},
],
properties,
});
return {
content: [
{
type: "text" as const,
text: `Successfully created database with ID: ${response.id}`,
},
],
};
} catch (error) {
console.error("Error creating database:", error);
return {
content: [
{
type: "text" as const,
text: `Error creating database: ${formatError(error)}`,
},
],
};
}
},
async query_database(args: unknown) {
const { databaseId, filter, sort } = args as any;
try {
const response = await notion.databases.query({
database_id: databaseId,
filter,
sorts: sort ? [sort] : undefined,
});
return {
content: [
{
type: "text" as const,
text: JSON.stringify(response.results, null, 2),
},
],
};
} catch (error) {
console.error("Error querying database:", error);
return {
content: [
{
type: "text" as const,
text: `Error querying database: ${formatError(error)}`,
},
],
};
}
},
async update_block(args: unknown) {
const { blockId, content, type = "paragraph" } = args as any;
try {
const response = await notion.blocks.update({
block_id: blockId,
[type]: {
rich_text: [
{
type: "text",
text: {
content: content,
},
},
],
},
});
return {
content: [
{
type: "text" as const,
text: `Successfully updated block`,
},
],
};
} catch (error) {
console.error("Error updating block:", error);
return {
content: [
{
type: "text" as const,
text: formatError(error),
},
],
};
}
},
async delete_block(args: unknown) {
const { blockId } = args as any;
try {
await notion.blocks.delete({
block_id: blockId,
});
return {
content: [
{
type: "text" as const,
text: "Successfully deleted block",
},
],
};
} catch (error) {
console.error("Error deleting block:", error);
return {
content: [
{
type: "text" as const,
text: formatError(error),
},
],
};
}
},
async update_database_item(args: unknown) {
const { pageId, properties } = args as any;
try {
const response = await notion.pages.update({
page_id: pageId,
properties,
});
return {
content: [
{
type: "text" as const,
text: `Successfully updated database item`,
},
],
};
} catch (error) {
console.error("Error updating database item:", error);
return {
content: [
{
type: "text" as const,
text: formatError(error),
},
],
};
}
},
async retrieve_database(args: unknown) {
const { databaseId } = args as any;
try {
const response = await notion.databases.retrieve({
database_id: databaseId,
});
return {
content: [
{
type: "text" as const,
text: JSON.stringify(response, null, 2),
},
],
};
} catch (error) {
console.error("Error retrieving database:", error);
return {
content: [
{
type: "text" as const,
text: formatError(error),
},
],
};
}
},
async update_database(args: unknown) {
const { databaseId, title, description, properties } = args as any;
try {
const response = await notion.databases.update({
database_id: databaseId,
title: title
? [
{
type: "text",
text: { content: title },
},
]
: undefined,
description: description
? [
{
type: "text",
text: { content: description },
},
]
: undefined,
properties,
});
return {
content: [
{
type: "text" as const,
text: `Successfully updated database with ID: ${response.id}`,
},
],
};
} catch (error) {
console.error("Error updating database:", error);
return {
content: [
{
type: "text" as const,
text: formatError(error),
},
],
};
}
},
};
// Initialize MCP server
const server = new Server(
{
name: "notion-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
},
);
// Register tool handlers
server.setRequestHandler(ListToolsRequestSchema, async () => {
console.error("Tools requested by client");
return { tools: TOOL_DEFINITIONS };
});
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
const { name, arguments: args } = request.params;
try {
const handler = toolHandlers[name as keyof typeof toolHandlers];
if (!handler) {
throw new Error(`Unknown tool: ${name}`);
}
return await handler(args);
} catch (error) {
console.error(`Error executing tool ${name}:`, error);
throw error;
}
});
// Start the server
async function main() {
if (!process.env.NOTION_API_KEY) {
throw new Error("NOTION_API_KEY environment variable is required");
}
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Notion MCP Server running on stdio");
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});