Skip to main content
Glama

Notion Knowledge MCP Server

by YuHuanHsu
mcp-worker.js18.9 kB
// Notion Knowledge MCP Server - Full MCP Protocol Implementation // Cloudflare Workers with complete MCP 2024-11-05 protocol support class NotionMCPServer { constructor(env) { this.notionToken = env.NOTION_TOKEN; this.databaseId = env.NOTION_DATABASE_ID; if (!this.databaseId) { throw new Error("NOTION_DATABASE_ID environment variable is required"); } this.headers = { 'Authorization': `Bearer ${this.notionToken}`, 'Content-Type': 'application/json', 'Notion-Version': '2022-06-28' }; } // MCP Protocol: Initialize async initialize(params = {}) { return { protocolVersion: "2024-11-05", capabilities: { tools: {}, resources: {}, logging: {} }, serverInfo: { name: "notion-knowledge-mcp", version: "1.0.0", description: "Notion Knowledge Base MCP Server" } }; } // MCP Protocol: List Tools async listTools() { return { tools: [ { name: "add_knowledge", description: "添加新知識到程式開發知識庫", inputSchema: { type: "object", properties: { title: { type: "string", description: "知識條目標題" }, content: { type: "string", description: "詳細內容,支援 Markdown 格式" }, project: { type: "string", description: "專案分類", enum: ["Web應用", "Mobile應用", "後端API", "DevOps工具", "數據分析", "其他"], default: "其他" }, type: { type: "string", description: "知識類型", enum: ["代碼片段", "解決方案", "錯誤記錄", "學習筆記", "配置文件", "最佳實踐"], default: "學習筆記" }, keywords: { type: "array", items: { type: "string" }, description: "關鍵字標籤", default: [] }, language: { type: "string", description: "程式語言", enum: ["JavaScript", "TypeScript", "Python", "Go", "Rust", "Java", "HTML/CSS", "SQL", "Shell", ""], default: "" }, importance: { type: "string", description: "重要程度", enum: ["高", "中", "低"], default: "中" }, file_path: { type: "string", description: "相關檔案路徑(可選)", default: "" } }, required: ["title", "content"] } }, { name: "search_knowledge", description: "在知識庫中搜索相關內容", inputSchema: { type: "object", properties: { query: { type: "string", description: "搜索關鍵字" }, project_filter: { type: "string", description: "按專案過濾", enum: ["", "Web應用", "Mobile應用", "後端API", "DevOps工具", "數據分析", "其他"], default: "" }, type_filter: { type: "string", description: "按知識類型過濾", enum: ["", "代碼片段", "解決方案", "錯誤記錄", "學習筆記", "配置文件", "最佳實踐"], default: "" }, limit: { type: "integer", description: "返回結果數量", default: 10 } }, required: ["query"] } }, { name: "get_recent_knowledge", description: "獲取最近的知識條目", inputSchema: { type: "object", properties: { limit: { type: "integer", description: "返回數量", default: 5 } } } }, { name: "get_knowledge_stats", description: "獲取知識庫統計信息", inputSchema: { type: "object", properties: {} } } ] }; } // MCP Protocol: Call Tool async callTool(name, args = {}) { try { let result; switch (name) { case "add_knowledge": result = await this.addKnowledge(args); break; case "search_knowledge": result = await this.searchKnowledge(args); break; case "get_recent_knowledge": result = await this.getRecentKnowledge(args); break; case "get_knowledge_stats": result = await this.getKnowledgeStats(); break; default: throw new Error(`Unknown tool: ${name}`); } return { content: [ { type: "text", text: result } ] }; } catch (error) { return { content: [ { type: "text", text: `❌ 工具執行錯誤: ${error.message}` } ], isError: true }; } } // 添加知識條目 async addKnowledge(params) { try { const { title, content, project = "其他", type = "學習筆記", keywords = [], language = "", importance = "中", file_path = "" } = params; const keywordOptions = keywords.length > 0 ? keywords.map(kw => ({ name: kw })) : []; const data = { parent: { database_id: this.databaseId }, properties: { "標題": { title: [{ text: { content: title } }] }, "專案名稱": { select: { name: project } }, "知識類型": { select: { name: type } }, "重要程度": { select: { name: importance } } }, children: [{ object: "block", type: "paragraph", paragraph: { rich_text: [{ type: "text", text: { content: content } }] } }] }; if (keywordOptions.length > 0) { data.properties["關鍵字"] = { multi_select: keywordOptions }; } if (language) { data.properties["程式語言"] = { select: { name: language } }; } if (file_path) { data.properties["檔案路徑"] = { rich_text: [{ text: { content: file_path } }] }; } const response = await fetch('https://api.notion.com/v1/pages', { method: 'POST', headers: this.headers, body: JSON.stringify(data) }); if (response.ok) { const result = await response.json(); return `✅ 知識已成功保存到 Notion 📄 標題: ${title} 📁 專案: ${project} 📋 類型: ${type} 🔗 連結: ${result.url}`; } else { const errorText = await response.text(); return `❌ 保存失敗: ${response.status} - ${errorText}`; } } catch (error) { return `❌ 添加知識時發生錯誤: ${error.message}`; } } async searchKnowledge(params) { try { const { query, project_filter = "", type_filter = "", limit = 10 } = params; const filters = []; if (project_filter) { filters.push({ property: "專案名稱", select: { equals: project_filter } }); } if (type_filter) { filters.push({ property: "知識類型", select: { equals: type_filter } }); } const searchData = { page_size: Math.min(limit, 100), sorts: [{ property: "最後修改", direction: "descending" }] }; if (filters.length > 0) { searchData.filter = filters.length === 1 ? filters[0] : { and: filters }; } const response = await fetch(`https://api.notion.com/v1/databases/${this.databaseId}/query`, { method: 'POST', headers: this.headers, body: JSON.stringify(searchData) }); if (response.ok) { const data = await response.json(); const filtered = data.results.filter(result => { const title = this.extractTitle(result); return title.toLowerCase().includes(query.toLowerCase()); }); return this.formatSearchResults(filtered, query, { project_filter, type_filter }); } else { const errorText = await response.text(); return `❌ 搜索失敗: ${response.status} - ${errorText}`; } } catch (error) { return `❌ 搜索時發生錯誤: ${error.message}`; } } async getRecentKnowledge(params) { try { const { limit = 5 } = params; const searchData = { page_size: Math.min(limit, 20), sorts: [{ property: "最後修改", direction: "descending" }] }; const response = await fetch(`https://api.notion.com/v1/databases/${this.databaseId}/query`, { method: 'POST', headers: this.headers, body: JSON.stringify(searchData) }); if (response.ok) { const data = await response.json(); return this.formatRecentResults(data.results); } else { const errorText = await response.text(); return `❌ 獲取最近知識失敗: ${response.status} - ${errorText}`; } } catch (error) { return `❌ 獲取最近知識時發生錯誤: ${error.message}`; } } async getKnowledgeStats() { try { const response = await fetch(`https://api.notion.com/v1/databases/${this.databaseId}/query`, { method: 'POST', headers: this.headers, body: JSON.stringify({ page_size: 100 }) }); if (response.ok) { const data = await response.json(); return this.formatStats(data.results); } else { const errorText = await response.text(); return `❌ 獲取統計失敗: ${response.status} - ${errorText}`; } } catch (error) { return `❌ 獲取統計時發生錯誤: ${error.message}`; } } // 輔助方法 extractTitle(result) { return result.properties["標題"]?.title?.[0]?.text?.content || "無標題"; } extractSelectValue(result, property) { return result.properties[property]?.select?.name || "未設定"; } extractMultiSelectValues(result, property) { return result.properties[property]?.multi_select?.map(item => item.name) || []; } formatSearchResults(results, query, filters) { if (!results || results.length === 0) { return `🔍 搜索 "${query}" 沒有找到相關結果`; } let output = [`🔍 搜索 "${query}" 找到 ${results.length} 個結果:\n`]; results.forEach((result, index) => { const title = this.extractTitle(result); const project = this.extractSelectValue(result, "專案名稱"); const type = this.extractSelectValue(result, "知識類型"); const keywords = this.extractMultiSelectValues(result, "關鍵字"); output.push(`${index + 1}. 📄 ${title}`); output.push(` 📁 專案: ${project} | 📋 類型: ${type}`); if (keywords.length > 0) output.push(` 🏷️ 標籤: ${keywords.join(", ")}`); output.push(` 🔗 連結: ${result.url}`); output.push(""); }); return output.join("\n"); } formatRecentResults(results) { if (!results || results.length === 0) { return `📅 最近的知識條目: 沒有找到結果`; } let output = [`📅 最近的知識條目 (${results.length} 個):\n`]; results.forEach((result, index) => { const title = this.extractTitle(result); const project = this.extractSelectValue(result, "專案名稱"); const type = this.extractSelectValue(result, "知識類型"); output.push(`${index + 1}. 📄 ${title}`); output.push(` 📁 ${project} | 📋 ${type}`); output.push(` 🔗 ${result.url}`); output.push(""); }); return output.join("\n"); } formatStats(results) { if (!results || results.length === 0) { return "📊 知識庫統計: 暫無數據"; } const stats = { total: results.length, byProject: {}, byType: {}, byLanguage: {} }; results.forEach(result => { const project = this.extractSelectValue(result, "專案名稱"); const type = this.extractSelectValue(result, "知識類型"); const language = this.extractSelectValue(result, "程式語言"); stats.byProject[project] = (stats.byProject[project] || 0) + 1; stats.byType[type] = (stats.byType[type] || 0) + 1; if (language !== "未設定") { stats.byLanguage[language] = (stats.byLanguage[language] || 0) + 1; } }); let output = [ "📊 程式開發知識庫統計報告", "==============================", "", `📈 總計: ${stats.total} 個知識條目`, "", "📁 按專案分布:" ]; Object.entries(stats.byProject) .sort(([,a], [,b]) => b - a) .forEach(([project, count]) => { output.push(` • ${project}: ${count} 個`); }); output.push(""); output.push("📋 按類型分布:"); Object.entries(stats.byType) .sort(([,a], [,b]) => b - a) .forEach(([type, count]) => { output.push(` • ${type}: ${count} 個`); }); if (Object.keys(stats.byLanguage).length > 0) { output.push(""); output.push("💻 按程式語言分布:"); Object.entries(stats.byLanguage) .sort(([,a], [,b]) => b - a) .forEach(([language, count]) => { output.push(` • ${language}: ${count} 個`); }); } return output.join("\n"); } } // MCP JSON-RPC 2.0 Handler class MCPHandler { constructor(server) { this.server = server; } async handleRequest(request) { const { id, method, params } = request; try { let result; switch (method) { case 'initialize': result = await this.server.initialize(params); break; case 'tools/list': result = await this.server.listTools(); break; case 'tools/call': if (!params?.name) { throw new Error("Missing tool name"); } result = await this.server.callTool(params.name, params.arguments); break; default: throw new Error(`Unknown method: ${method}`); } return { jsonrpc: "2.0", id, result }; } catch (error) { return { jsonrpc: "2.0", id, error: { code: -1, message: error.message } }; } } } // Cloudflare Workers Export export default { async fetch(request, env, ctx) { const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', }; if (request.method === 'OPTIONS') { return new Response(null, { headers: corsHeaders }); } try { if (!env.NOTION_TOKEN) { return new Response(JSON.stringify({ error: "Missing NOTION_TOKEN environment variable" }), { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }); } const server = new NotionMCPServer(env); const handler = new MCPHandler(server); const url = new URL(request.url); // MCP JSON-RPC endpoint if (url.pathname === '/mcp' && request.method === 'POST') { const requestData = await request.json(); const response = await handler.handleRequest(requestData); return new Response(JSON.stringify(response), { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }); } // SSE endpoint for MCP transport if (url.pathname === '/sse') { const { readable, writable } = new TransformStream(); const writer = writable.getWriter(); // Send MCP initialization await writer.write(new TextEncoder().encode( `data: ${JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized", params: {} })}\n\n` )); return new Response(readable, { headers: { ...corsHeaders, 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' } }); } // Health check if (url.pathname === '/health') { return new Response(JSON.stringify({ status: 'ok', protocolVersion: "2024-11-05", serverInfo: { name: "notion-knowledge-mcp", version: "1.0.0" } }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }); } // Legacy API endpoints for backward compatibility if (url.pathname === '/tools') { const tools = await server.listTools(); return new Response(JSON.stringify(tools), { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }); } if (url.pathname === '/call' && request.method === 'POST') { const { method, params } = await request.json(); const result = await server.callTool(method, params); return new Response(JSON.stringify({ result: result.content[0].text, timestamp: new Date().toISOString() }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }); } // Default response return new Response(` 🎉 Notion Knowledge MCP Server (Full Protocol) ============================================= 📊 服務狀態: ✅ 正常運行 (MCP 2024-11-05) 🔗 MCP 端點: ${request.url.replace(url.pathname, '')}/mcp 📡 SSE 傳輸: ${request.url.replace(url.pathname, '')}/sse 💾 資料庫 ID: ${server.databaseId} 🛠️ MCP 協議端點: • POST /mcp - JSON-RPC 2.0 MCP 協議 • GET /sse - Server-Sent Events 傳輸 • GET /health - 健康檢查 📡 舊版 API 端點 (兼容性): • GET /tools - 工具列表 • POST /call - 工具調用 💡 MCP 客戶端配置: Claude Desktop: 使用 SSE 傳輸協議 Claude Code: 使用 HTTP 傳輸協議 Gemini CLI: 使用 JSON-RPC 協議 powered by Cloudflare Workers ⚡ `.trim(), { headers: { ...corsHeaders, 'Content-Type': 'text/plain; charset=utf-8' } }); } catch (error) { return new Response(JSON.stringify({ error: error.message, help: "請檢查環境變數設定" }), { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }); } } };

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/YuHuanHsu/notion-knowledge-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server