index.ts•10.4 kB
#!/usr/bin/env node
import 'dotenv/config';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
import { NotionService } from './notion-service.js';
// 工具參數的 schema 定義
const GetPageSchema = z.object({
pageId: z.string().describe('Notion page ID to retrieve'),
});
const SearchPagesSchema = z.object({
query: z.string().describe('Search query for pages'),
filter: z.object({
property: z.string().optional(),
value: z.any().optional(),
}).optional(),
});
const CreatePageSchema = z.object({
parent: z.object({
database_id: z.string().optional(),
page_id: z.string().optional(),
}),
properties: z.record(z.any()),
children: z.array(z.any()).optional(),
});
const UpdatePageSchema = z.object({
pageId: z.string().describe('Page ID to update'),
properties: z.record(z.any()),
});
const QueryDatabaseSchema = z.object({
databaseId: z.string().describe('Database ID to query'),
filter: z.any().optional(),
sorts: z.array(z.any()).optional(),
startCursor: z.string().optional(),
pageSize: z.number().optional(),
});
class NotionMCPServer {
private server: Server;
private notionService: NotionService;
constructor() {
this.server = new Server(
{
name: 'notion-mcp-server',
version: '0.1.0',
},
{
capabilities: {
tools: {},
resources: {},
},
}
);
this.notionService = new NotionService();
this.setupToolHandlers();
this.setupResourceHandlers();
// 錯誤處理
this.server.onerror = (error) => console.error('[MCP Error]', error);
process.on('SIGINT', async () => {
await this.server.close();
process.exit(0);
});
}
private setupToolHandlers() {
// 列出所有工具
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'get-page',
description: 'Get a Notion page by ID',
inputSchema: {
type: 'object',
properties: {
pageId: {
type: 'string',
description: 'The ID of the Notion page to retrieve',
},
},
required: ['pageId'],
},
},
{
name: 'search-pages',
description: 'Search for Notion pages',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query',
},
filter: {
type: 'object',
description: 'Optional filter criteria',
properties: {
property: { type: 'string' },
value: {},
},
},
},
required: ['query'],
},
},
{
name: 'create-page',
description: 'Create a new Notion page',
inputSchema: {
type: 'object',
properties: {
parent: {
type: 'object',
properties: {
database_id: { type: 'string' },
page_id: { type: 'string' },
},
},
properties: {
type: 'object',
description: 'Page properties',
},
children: {
type: 'array',
description: 'Page content blocks',
},
},
required: ['parent', 'properties'],
},
},
{
name: 'update-page',
description: 'Update a Notion page',
inputSchema: {
type: 'object',
properties: {
pageId: {
type: 'string',
description: 'The ID of the page to update',
},
properties: {
type: 'object',
description: 'Properties to update',
},
},
required: ['pageId', 'properties'],
},
},
{
name: 'query-database',
description: 'Query a Notion database',
inputSchema: {
type: 'object',
properties: {
databaseId: {
type: 'string',
description: 'The ID of the database to query',
},
filter: {
type: 'object',
description: 'Optional filter criteria',
},
sorts: {
type: 'array',
description: 'Optional sort criteria',
},
startCursor: {
type: 'string',
description: 'Pagination cursor',
},
pageSize: {
type: 'number',
description: 'Number of results per page',
},
},
required: ['databaseId'],
},
},
],
};
});
// 處理工具調用
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'get-page': {
const { pageId } = GetPageSchema.parse(args);
const page = await this.notionService.getPage(pageId);
return {
content: [
{
type: 'text',
text: JSON.stringify(page, null, 2),
},
],
};
}
case 'search-pages': {
const { query, filter } = SearchPagesSchema.parse(args);
const results = await this.notionService.searchPages(query, filter);
return {
content: [
{
type: 'text',
text: JSON.stringify(results, null, 2),
},
],
};
}
case 'create-page': {
const { parent, properties, children } = CreatePageSchema.parse(args);
const page = await this.notionService.createPage(parent, properties, children);
return {
content: [
{
type: 'text',
text: `Page created successfully: ${JSON.stringify(page, null, 2)}`,
},
],
};
}
case 'update-page': {
const { pageId, properties } = UpdatePageSchema.parse(args);
const page = await this.notionService.updatePage(pageId, properties);
return {
content: [
{
type: 'text',
text: `Page updated successfully: ${JSON.stringify(page, null, 2)}`,
},
],
};
}
case 'query-database': {
const { databaseId, filter, sorts, startCursor, pageSize } = QueryDatabaseSchema.parse(args);
const results = await this.notionService.queryDatabase(
databaseId,
filter,
sorts,
startCursor,
pageSize
);
return {
content: [
{
type: 'text',
text: JSON.stringify(results, null, 2),
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
});
}
private setupResourceHandlers() {
// 列出所有資源
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: 'notion://recent-pages',
name: 'Recent Pages',
description: 'Recently accessed Notion pages',
mimeType: 'application/json',
},
{
uri: 'notion://databases',
name: 'Databases',
description: 'List of accessible Notion databases',
mimeType: 'application/json',
},
],
};
});
// 讀取資源
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
try {
switch (uri) {
case 'notion://recent-pages': {
const pages = await this.notionService.getRecentPages();
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(pages, null, 2),
},
],
};
}
case 'notion://databases': {
const databases = await this.notionService.getDatabases();
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(databases, null, 2),
},
],
};
}
default:
throw new Error(`Unknown resource: ${uri}`);
}
} catch (error) {
throw new Error(`Failed to read resource ${uri}: ${error instanceof Error ? error.message : String(error)}`);
}
});
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Notion MCP server running on stdio');
}
}
const server = new NotionMCPServer();
server.run().catch(console.error);