Skip to main content
Glama
server.ts35.5 kB
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { YuqueService } from "./services/yuque"; import express, { Request, Response } from "express"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import { IncomingMessage, ServerResponse } from "http"; import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import { getFixedQuery, mcpHook_updateMessageBody, mcpHook_updateMessageEndpoint, } from "./mcp_hook"; export const Logger = { log: (...args: any[]) => {}, error: (...args: any[]) => {}, }; export class YuqueMcpServer { private readonly server: McpServer; private yuqueApiToken: string; private yuqueApiBaseUrl: string; constructor(yuqueApiToken: string, yuqueApiBaseUrl: string) { this.yuqueApiToken = yuqueApiToken; this.yuqueApiBaseUrl = yuqueApiBaseUrl; this.server = new McpServer( { name: "Yuque MCP Server", version: "0.1.0", }, { capabilities: { logging: {}, tools: {}, }, } ); this.registerTools(); } // 创建新的YuqueService实例的辅助方法 private createYuqueService(accessToken: string | undefined): YuqueService { return new YuqueService( accessToken ?? this.yuqueApiToken, this.yuqueApiBaseUrl ); } // 将长文档内容分割成较小的块 private splitDocumentContent(doc: any, chunkSize: number = 100000): any[] { // 先将整个文档对象转换为格式化的JSON字符串 const fullDocString = JSON.stringify(doc, null, 2); console.log("fullDocString length: " + fullDocString.length); // 如果整个文档字符串长度小于块大小,直接返回原文档 if (fullDocString.length <= chunkSize) { return [doc]; } // 使用简单的文本分割逻辑,添加重叠内容 const overlapSize = 200; // 块之间的重叠大小 const chunks: string[] = []; // 直接按照固定大小分割,不考虑内容边界 let startIndex = 0; while (startIndex < fullDocString.length) { // 计算当前块的结束位置 const endIndex = Math.min(startIndex + chunkSize, fullDocString.length); // 提取当前块内容 chunks.push(fullDocString.substring(startIndex, endIndex)); // 更新下一个块的起始位置,确保有重叠 startIndex = endIndex - overlapSize; // 如果已经到达文本末尾或下一次循环会导致无效分块,则退出循环 if (startIndex >= fullDocString.length - overlapSize) { break; } } // 为每个块创建对应的文档对象,添加分块和上下文信息 return chunks.map((chunk, index) => { // 创建一个返回对象 const result: any = { _original_doc_id: doc.id, _original_title: doc.title, _chunk_info: { index: index, total: chunks.length, is_chunked: true, chunk_size: chunkSize, overlap_size: overlapSize, content_type: "full_doc_json", // 添加上下文信息 context: { has_previous: index > 0, has_next: index < chunks.length - 1, // 添加提示,指出这是部分内容 note: index > 0 ? "此内容包含与前一块重叠的部分" : "", }, }, }; // 保存原始文本块 result.text_content = chunk; // 尝试将文本块解析回JSON(如果是完整的JSON对象) try { // 仅当文本以 { 开头且以 } 结尾时尝试解析 if (chunk.trim().startsWith("{") && chunk.trim().endsWith("}")) { const parsedChunk = JSON.parse(chunk); // 合并解析后的属性到结果对象 Object.assign(result, parsedChunk); } } catch (e) { // 解析失败,保留文本格式 result.parse_error = "块内容不是完整的JSON对象,保留为文本"; } // 修改标题,添加分块标记 result.title = `${doc.title} [部分 ${index + 1}/${chunks.length}]`; return result; }); } private registerTools(): void { // Tool to get current user information this.server.tool( "get_current_user", "获取当前认证用户的信息,包括用户ID、用户名、头像等语雀账号基本信息", { accessToken: z.string().optional().describe("用于认证 API 请求的令牌"), }, async ({ accessToken }) => { try { Logger.log("Fetching current user information"); const yuqueService = this.createYuqueService(accessToken); const user = await yuqueService.getCurrentUser(); Logger.log(`Successfully fetched user: ${user.name}`); Logger.log(JSON.stringify(user)); return { content: [{ type: "text", text: JSON.stringify(user) }], }; } catch (error) { Logger.error("Error fetching current user:", error); return { content: [ { type: "text", text: `Error fetching current user: ${error}` }, ], }; } } ); // Tool to get user's docs this.server.tool( "get_user_docs", "获取当前用户的所有知识库文档列表,包括私人和协作文档", { accessToken: z.string().optional().describe("用于认证 API 请求的令牌"), }, async ({ accessToken }) => { try { Logger.log("Fetching user's documents"); const yuqueService = this.createYuqueService(accessToken); const docs = await yuqueService.getUserDocs(); Logger.log(`Successfully fetched ${docs.length} documents`); return { content: [{ type: "text", text: JSON.stringify(docs) }], }; } catch (error) { Logger.error("Error fetching user docs:", error); return { content: [ { type: "text", text: `Error fetching user docs: ${error}` }, ], }; } } ); // Tool to get user's repos this.server.tool( "get_user_repos", "获取指定用户的知识库列表,知识库是语雀中组织文档的集合", { login: z.string().describe("用户的登录名或唯一标识"), accessToken: z.string().optional().describe("用于认证 API 请求的令牌"), }, async ({ login, accessToken }) => { try { Logger.log(`Fetching repositories for user: ${login}`); const yuqueService = this.createYuqueService(accessToken); const repos = await yuqueService.getUserRepos(login); Logger.log(`Successfully fetched ${repos.length} repositories`); return { content: [{ type: "text", text: JSON.stringify(repos, null, 2) }], }; } catch (error) { Logger.error(`Error fetching repos for user ${login}:`, error); return { content: [{ type: "text", text: `Error fetching repos: ${error}` }], }; } } ); // Tool to get docs in a repo this.server.tool( "get_repo_docs", "获取特定知识库中的所有文档列表,包括文档标题、更新时间等信息", { namespace: z.string().describe("知识库的命名空间,格式为 user/repo"), accessToken: z.string().optional().describe("用于认证 API 请求的令牌"), }, async ({ namespace, accessToken }) => { try { Logger.log(`Fetching documents for repository: ${namespace}`); const yuqueService = this.createYuqueService(accessToken); const docs = await yuqueService.getRepoDocs(namespace); Logger.log(`Successfully fetched ${docs.length} documents`); return { content: [{ type: "text", text: JSON.stringify(docs, null, 2) }], }; } catch (error) { Logger.error(`Error fetching docs for repo ${namespace}:`, error); return { content: [{ type: "text", text: `Error fetching docs: ${error}` }], }; } } ); // Tool to get a specific document this.server.tool( "get_doc", "获取语雀中特定文档的详细内容,包括正文、修改历史和权限信息(支持分块处理大型文档)", { namespace: z.string().describe("知识库的命名空间,格式为 user/repo"), slug: z.string().describe("文档的唯一标识或短链接名称"), chunk_index: z .number() .optional() .describe( "要获取的文档块索引,不提供则返回第一块或全部(如果内容较小)" ), chunk_size: z .number() .optional() .describe("分块大小(字符数),默认为100000"), accessToken: z.string().optional().describe("用于认证 API 请求的令牌"), }, async ({ namespace, slug, chunk_index, chunk_size = 100000, accessToken, }) => { try { Logger.log(`Fetching document ${slug} from repository: ${namespace}`); Logger.log(`accessToken: ${accessToken}`); const yuqueService = this.createYuqueService(accessToken); const doc = await yuqueService.getDoc(namespace, slug); Logger.log( `Successfully fetched document: ${doc.title}, content length: ${ doc.body?.length || 0 } chars` ); // 将文档内容分割成块 const docChunks = this.splitDocumentContent(doc, chunk_size); if (docChunks.length > 1) { Logger.log( `Document has been split into ${docChunks.length} chunks` ); // 如果没有指定块索引,默认返回第一块 if (chunk_index === undefined) { // 返回第一块的同时提供分块信息 const firstChunk = docChunks[0]; Logger.log(`Returning first chunk (1/${docChunks.length})`); return { content: [ { type: "text", text: JSON.stringify(firstChunk, null, 2) }, ], }; } // 如果指定了块索引,检查有效性 if (chunk_index < 0 || chunk_index >= docChunks.length) { const error = `Invalid chunk_index: ${chunk_index}. Valid range is 0-${ docChunks.length - 1 }`; Logger.error(error); return { content: [{ type: "text", text: error }], }; } // 返回指定的块 Logger.log( `Returning chunk ${chunk_index + 1}/${docChunks.length}` ); return { content: [ { type: "text", text: JSON.stringify(docChunks[chunk_index], null, 2), }, ], }; } else { // 如果文档很小,不需要分块,直接返回完整文档 Logger.log(`Document is small enough, no chunking needed`); return { content: [{ type: "text", text: JSON.stringify(doc, null, 2) }], }; } } catch (error) { Logger.error( `Error fetching doc ${slug} from repo ${namespace}:`, error ); return { content: [{ type: "text", text: `Error fetching doc: ${error}` }], }; } } ); // Tool to create a new document this.server.tool( "create_doc", "在指定知识库中创建新的语雀文档,支持多种格式内容", { namespace: z.string().describe("知识库的命名空间,格式为 user/repo"), title: z.string().describe("文档标题"), slug: z.string().describe("文档的短链接名称,用于URL路径"), body: z.string().describe("文档内容,支持Markdown格式"), format: z .string() .optional() .describe("内容格式,可选值:markdown、html、lake,默认为 markdown"), public_level: z .number() .optional() .describe( "公开性,可选值:0(私密)、1(公开)、2(企业内公开),默认为 1" ), accessToken: z.string().optional().describe("用于认证 API 请求的令牌"), }, async ({ namespace, title, slug, body, format = "markdown", public_level = 1, accessToken, }) => { try { Logger.log( `Creating document "${title}" in repository: ${namespace}` ); const yuqueService = this.createYuqueService(accessToken); const doc = await yuqueService.createDoc( namespace, title, slug, body, format, public_level ); Logger.log(`Successfully created document: ${doc.title}`); return { content: [{ type: "text", text: JSON.stringify(doc, null, 2) }], }; } catch (error) { Logger.error(`Error creating doc in repo ${namespace}:`, error); return { content: [{ type: "text", text: `Error creating doc: ${error}` }], }; } } ); // Tool to update a document this.server.tool( "update_doc", "更新语雀中已存在的文档,可以修改标题、内容或权限设置", { namespace: z.string().describe("知识库的命名空间,格式为 user/repo"), id: z.number().describe("要更新的文档ID"), title: z.string().optional().describe("文档的新标题"), slug: z.string().optional().describe("文档的新短链接名称"), body: z.string().optional().describe("文档的新内容,支持Markdown格式"), public: z .number() .optional() .describe("文档的公开状态,0(私密)、1(公开)、2(企业内公开)"), format: z .string() .optional() .describe("内容格式,可选值:markdown、html、lake"), accessToken: z.string().optional().describe("用于认证 API 请求的令牌"), }, async ({ namespace, id, title, slug, body, public: publicLevel, format, accessToken, }) => { try { Logger.log(`Updating document ${id} in repository: ${namespace}`); const yuqueService = this.createYuqueService(accessToken); const updateData: any = {}; if (title !== undefined) updateData.title = title; if (slug !== undefined) updateData.slug = slug; if (body !== undefined) updateData.body = body; if (publicLevel !== undefined) updateData.public = publicLevel; if (format !== undefined) updateData.format = format; const doc = await yuqueService.updateDoc(namespace, id, updateData); Logger.log(`Successfully updated document: ${doc.title}`); return { content: [{ type: "text", text: JSON.stringify(doc, null, 2) }], }; } catch (error) { Logger.error(`Error updating doc ${id} in repo ${namespace}:`, error); return { content: [{ type: "text", text: `Error updating doc: ${error}` }], }; } } ); // Tool to delete a document this.server.tool( "delete_doc", "从语雀知识库中删除指定文档,此操作不可撤销", { namespace: z.string().describe("知识库的命名空间,格式为 user/repo"), id: z.number().describe("要删除的文档ID"), accessToken: z.string().optional().describe("用于认证 API 请求的令牌"), }, async ({ namespace, id, accessToken }) => { try { Logger.log(`Deleting document ${id} from repository: ${namespace}`); const yuqueService = this.createYuqueService(accessToken); await yuqueService.deleteDoc(namespace, id); Logger.log(`Successfully deleted document ${id}`); return { content: [ { type: "text", text: `Document ${id} has been successfully deleted`, }, ], }; } catch (error) { Logger.error( `Error deleting doc ${id} from repo ${namespace}:`, error ); return { content: [{ type: "text", text: `Error deleting doc: ${error}` }], }; } } ); // Tool to search Yuque content this.server.tool( "search", "在语雀平台中搜索文档或知识库内容,支持范围和作者筛选", { query: z.string().describe("搜索关键词"), type: z .enum(["doc", "repo"]) .describe("要搜索的内容类型:doc(文档) 或 repo(知识库)"), scope: z .string() .optional() .describe("搜索范围,不填默认搜索当前用户/团队"), page: z.number().optional().describe("页码,默认为1"), creator: z.string().optional().describe("仅搜索指定作者的内容"), accessToken: z.string().optional().describe("用于认证 API 请求的令牌"), }, async ({ query, type, scope, page, creator, accessToken }) => { try { Logger.log(`Searching for: ${query} with type: ${type}`); const yuqueService = this.createYuqueService(accessToken); const results = await yuqueService.search( query, type, scope, page, creator ); Logger.log(`Successfully found ${results.length} results`); return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }], }; } catch (error) { Logger.error(`Error searching for ${query}:`, error); return { content: [{ type: "text", text: `Error searching: ${error}` }], }; } } ); // 团队统计汇总数据 this.server.tool( "get_group_statistics", "获取团队的汇总统计数据,包括成员人数、文档数量、阅读量和互动数据等", { login: z.string().describe("团队的登录名或唯一标识"), accessToken: z.string().optional().describe("用于认证 API 请求的令牌"), }, async ({ login, accessToken }) => { try { Logger.log(`Fetching statistics for group: ${login}`); const yuqueService = this.createYuqueService(accessToken); const stats = await yuqueService.getGroupStatistics(login); Logger.log(`Successfully fetched statistics for group: ${login}`); return { content: [{ type: "text", text: JSON.stringify(stats, null, 2) }], }; } catch (error) { Logger.error(`Error fetching statistics for group ${login}:`, error); return { content: [ { type: "text", text: `Error fetching group statistics: ${error}`, }, ], }; } } ); // 团队成员统计数据 this.server.tool( "get_group_member_statistics", "获取团队成员的统计数据,包括各成员的编辑次数、阅读量、点赞量等", { login: z.string().describe("团队的登录名或唯一标识"), name: z.string().optional().describe("成员名称,用于过滤特定成员"), range: z .number() .optional() .describe("时间范围(0: 全部, 30: 近30天, 365: 近一年)"), page: z.number().optional().describe("页码,默认为1"), limit: z.number().optional().describe("每页数量,默认为10,最大为20"), sortField: z .string() .optional() .describe( "排序字段,可选值:write_doc_count、write_count、read_count、like_count" ), sortOrder: z .enum(["desc", "asc"]) .optional() .describe("排序方向,可选值:desc(降序)、asc(升序),默认为desc"), accessToken: z.string().optional().describe("用于认证 API 请求的令牌"), }, async (params) => { try { const { login, accessToken, ...queryParams } = params; Logger.log(`Fetching member statistics for group: ${login}`); const yuqueService = this.createYuqueService(accessToken); const stats = await yuqueService.getGroupMemberStatistics( login, queryParams ); Logger.log( `Successfully fetched member statistics for group: ${login}` ); return { content: [{ type: "text", text: JSON.stringify(stats, null, 2) }], }; } catch (error) { Logger.error( `Error fetching member statistics for group ${params.login}:`, error ); return { content: [ { type: "text", text: `Error fetching group member statistics: ${error}`, }, ], }; } } ); // 团队知识库统计数据 this.server.tool( "get_group_book_statistics", "获取团队知识库的统计数据,包括各知识库的文档数、字数、阅读量等", { login: z.string().describe("团队的登录名或唯一标识"), name: z.string().optional().describe("知识库名称,用于过滤特定知识库"), range: z .number() .optional() .describe("时间范围(0: 全部, 30: 近30天, 365: 近一年)"), page: z.number().optional().describe("页码,默认为1"), limit: z.number().optional().describe("每页数量,默认为10,最大为20"), sortField: z .string() .optional() .describe( "排序字段,可选值:content_updated_at_ms、word_count、post_count、read_count、like_count、watch_count、comment_count" ), sortOrder: z .enum(["desc", "asc"]) .optional() .describe("排序方向,可选值:desc(降序)、asc(升序),默认为desc"), accessToken: z.string().optional().describe("用于认证 API 请求的令牌"), }, async (params) => { try { const { login, accessToken, ...queryParams } = params; Logger.log(`Fetching book statistics for group: ${login}`); const yuqueService = this.createYuqueService(accessToken); const stats = await yuqueService.getGroupBookStatistics( login, queryParams ); Logger.log( `Successfully fetched book statistics for group: ${login}` ); return { content: [{ type: "text", text: JSON.stringify(stats, null, 2) }], }; } catch (error) { Logger.error( `Error fetching book statistics for group ${params.login}:`, error ); return { content: [ { type: "text", text: `Error fetching group book statistics: ${error}`, }, ], }; } } ); // 团队文档统计数据 this.server.tool( "get_group_doc_statistics", "获取团队文档的统计数据,包括各文档的字数、阅读量、评论量等", { login: z.string().describe("团队的登录名或唯一标识"), bookId: z .number() .optional() .describe("知识库ID,用于过滤特定知识库的文档"), name: z.string().optional().describe("文档名称,用于过滤特定文档"), range: z .number() .optional() .describe("时间范围(0: 全部, 30: 近30天, 365: 近一年)"), page: z.number().optional().describe("页码,默认为1"), limit: z.number().optional().describe("每页数量,默认为10,最大为20"), sortField: z .string() .optional() .describe( "排序字段,可选值:content_updated_at、word_count、read_count、like_count、comment_count、created_at" ), sortOrder: z .enum(["desc", "asc"]) .optional() .describe("排序方向,可选值:desc(降序)、asc(升序),默认为desc"), accessToken: z.string().optional().describe("用于认证 API 请求的令牌"), }, async (params) => { try { const { login, accessToken, ...queryParams } = params; Logger.log(`Fetching doc statistics for group: ${login}`); const yuqueService = this.createYuqueService(accessToken); const stats = await yuqueService.getGroupDocStatistics( login, queryParams ); Logger.log(`Successfully fetched doc statistics for group: ${login}`); return { content: [{ type: "text", text: JSON.stringify(stats, null, 2) }], }; } catch (error) { Logger.error( `Error fetching doc statistics for group ${params.login}:`, error ); return { content: [ { type: "text", text: `Error fetching group doc statistics: ${error}`, }, ], }; } } ); // 获取文档分块元信息 this.server.tool( "get_doc_chunks_info", "获取文档的分块元信息,包括总块数、每块的字符数等", { namespace: z.string().describe("知识库的命名空间,格式为 user/repo"), slug: z.string().describe("文档的唯一标识或短链接名称"), chunk_size: z .number() .optional() .describe("分块大小(字符数),默认为100000"), accessToken: z.string().optional().describe("用于认证 API 请求的令牌"), }, async ({ namespace, slug, chunk_size = 100000, accessToken }) => { try { Logger.log( `Fetching document chunk info for ${slug} from repository: ${namespace}` ); const yuqueService = this.createYuqueService(accessToken); const doc = await yuqueService.getDoc(namespace, slug); // 将整个文档转换为JSON字符串来评估总长度 const fullDocString = JSON.stringify(doc, null, 2); // 计算会产生多少块 const overlapSize = 200; let totalChunks = 1; if (fullDocString.length > chunk_size) { // 简单计算分块数量,考虑重叠 // 公式:向上取整((总长度 - 重叠大小) / (块大小 - 重叠大小)) totalChunks = Math.ceil( (fullDocString.length - overlapSize) / (chunk_size - overlapSize) ); } // 构建分块元信息对象 const chunksInfo = { document_id: doc.id, title: doc.title, total_chunks: totalChunks, total_length: fullDocString.length, chunk_size: chunk_size, overlap_size: overlapSize, estimated_chunks: Array.from( { length: totalChunks }, (_, index) => { // 估计每个块的起始和结束位置 const startPosition = index === 0 ? 0 : index * (chunk_size - overlapSize); const endPosition = Math.min( startPosition + chunk_size, fullDocString.length ); return { index: index, title: `${doc.title} [部分 ${index + 1}/${totalChunks}]`, approximate_start: startPosition, approximate_end: endPosition, approximate_length: endPosition - startPosition, how_to_get: `使用 get_doc 工具,指定 chunk_index=${index}`, }; } ), }; Logger.log( `Document would be split into ${totalChunks} chunks with size ${chunk_size}` ); return { content: [ { type: "text", text: JSON.stringify(chunksInfo, null, 2) }, ], }; } catch (error) { Logger.error( `Error fetching doc chunks info for ${slug} from repo ${namespace}:`, error ); return { content: [ { type: "text", text: `Error fetching doc chunks info: ${error}`, }, ], }; } } ); } async connect(transport: Transport): Promise<void> { await this.server.connect(transport); Logger.log = (...args: any[]) => { this.server.server.sendLoggingMessage({ level: "info", data: args, }); }; Logger.error = (...args: any[]) => { this.server.server.sendLoggingMessage({ level: "error", data: args, }); }; Logger.log("Yuque MCP Server connected and ready to process requests"); } // Display available tools to the console displayAvailableTools(): void { // 手动定义工具列表,因为我们不能直接访问 server.tools const tools = [ { name: "get_current_user", description: "获取当前认证用户的信息,包括用户ID、用户名、头像等语雀账号基本信息", }, { name: "get_user_docs", description: "获取当前用户的所有知识库文档列表,包括私人和协作文档", }, { name: "get_user_repos", description: "获取指定用户的知识库列表,知识库是语雀中组织文档的集合", }, { name: "get_repo_docs", description: "获取特定知识库中的所有文档列表,包括文档标题、更新时间等信息", }, { name: "get_doc", description: "获取语雀中特定文档的详细内容,包括正文、修改历史和权限信息(支持分块处理大型文档)", }, { name: "get_doc_chunks_info", description: "获取文档的分块元信息,包括总块数、每块的字符数等", }, { name: "create_doc", description: "在指定知识库中创建新的语雀文档,支持多种格式内容", }, { name: "update_doc", description: "更新语雀中已存在的文档,可以修改标题、内容或权限设置", }, { name: "delete_doc", description: "从语雀知识库中删除指定文档,此操作不可撤销", }, { name: "search", description: "在语雀平台中搜索文档或知识库内容,支持范围和作者筛选", }, { name: "get_group_statistics", description: "获取团队的汇总统计数据,包括成员人数、文档数量、阅读量和互动数据等", }, { name: "get_group_member_statistics", description: "获取团队成员的统计数据,包括各成员的编辑次数、阅读量、点赞量等", }, { name: "get_group_book_statistics", description: "获取团队知识库的统计数据,包括各知识库的文档数、字数、阅读量等", }, { name: "get_group_doc_statistics", description: "获取团队文档的统计数据,包括各文档的字数、阅读量、评论量等", }, ]; console.log("\n======== 语雀 MCP 服务器可用工具 ========"); console.log("以下工具可用于与语雀知识库交互:"); console.log("----------------------------------------"); tools.forEach((tool) => { console.log(`\n• ${tool.name}`); console.log(` ${tool.description}`); }); console.log("\n========================================\n"); } async startHttpServer(port: number): Promise<void> { const app = express(); const transports: {[sessionId: string]: SSEServerTransport} = {}; // 添加健康检查端点 app.get("/health", (req, res) => { res.status(200).json({ status: "ok", version: "0.1.0", timestamp: new Date().toISOString(), }); }); app.get("/sse", async (req: Request, res: Response) => { try { const hookUrl = mcpHook_updateMessageEndpoint(req); console.log("hookUrl: " + hookUrl); const transport = new SSEServerTransport(hookUrl, res); transports[transport.sessionId] = transport; res.on("close", () => { delete transports[transport.sessionId]; }); await this.server.connect(transport); } catch (error) { Logger.error("Error connecting to SSE: " + error); res.status(500).send("Error connecting to SSE"); } }); app.post("/messages", async (req: Request, res: Response) => { const sessionId = getFixedQuery(req.query as Record<string, string>)["sessionId"]; const transport = transports[sessionId]; try { if (!transport) { res.status(400).send("No transport found for sessionId"); return; } try { // 处理请求并获取消息内容 const messageContent = await mcpHook_updateMessageBody(req); // 使用处理好的消息内容调用handleMessage await transport.handleMessage(messageContent); // 返回成功响应 if (!res.headersSent) { res.status(202).send("Accepted"); } } catch (error: any) { Logger.error("Error handling message: " + error); if (!res.headersSent) { res.status(500).send(`Error handling message: ${error.message || error}`); } } } catch (error: any) { Logger.error("Error in messages route: " + error); if (!res.headersSent) { res.status(500).send(`Server error: ${error.message || error}`); } } }); Logger.log = console.log; Logger.error = console.error; app.listen(port, () => { Logger.log(`Yuque MCP HTTP server listening on port ${port}`); Logger.log(`SSE endpoint available at http://localhost:${port}/sse`); Logger.log( `Message endpoint available at http://localhost:${port}/messages` ); // Display available tools this.displayAvailableTools(); }); } }

Implementation Reference

Latest Blog Posts

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/HenryHaoson/Yuque-MCP-Server'

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