Skip to main content
Glama
yuque-proxy.js55.9 kB
from flask import Flask, request, jsonify, Response import requests import os import logging import json import time from typing import Dict, Any app = Flask(__name__) # 配置日志 logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) # 语雀 API 配置 YUQUE_BASE_URL = "https://www.yuque.com/api/v2" # MCP 协议配置 MCP_PROTOCOL_VERSION = "2024-11-05" DEFAULT_CORS_ORIGIN = "*" def get_yuque_token(): """ 获取语雀 Token,优先级: 1. HTTP Header: X-Yuque-Token 2. 环境变量: YUQUE_TOKEN 如果都没有,抛出异常 """ # 优先从 HTTP Header 读取 token = request.headers.get('X-Yuque-Token') # 如果没有 Header,从环境变量读取 if not token: token = os.getenv('YUQUE_TOKEN') # 如果都没有,返回错误 if not token: raise ValueError( "缺少语雀 Token 配置。请通过以下方式之一提供:\n" "1. HTTP Header: X-Yuque-Token\n" "2. 环境变量: YUQUE_TOKEN" ) return token class YuqueMCPClient: """语雀 API 客户端封装""" def __init__(self, token: str): self.token = token self.base_url = YUQUE_BASE_URL self.session = requests.Session() self.session.headers.update({ 'X-Auth-Token': self.token, 'Content-Type': 'application/json', 'User-Agent': 'Yuque-MCP-Server/2.0' }) def _request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]: """发送请求到语雀 API""" url = f"{self.base_url}{endpoint}" try: response = self.session.request(method, url, **kwargs) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: logger.error(f"语雀 API 请求失败: {e}") raise def get_user_info(self) -> Dict[str, Any]: """获取当前用户信息""" return self._request('GET', '/user') def list_repos(self) -> Dict[str, Any]: """列出用户的知识库""" user_info = self.get_user_info() login = user_info["data"]["login"] return self._request('GET', f'/users/{login}/repos') def get_repo(self, namespace: str) -> Dict[str, Any]: """获取知识库详情""" return self._request('GET', f'/repos/{namespace}') def list_docs(self, namespace: str) -> Dict[str, Any]: """列出知识库中的文档""" return self._request('GET', f'/repos/{namespace}/docs') def get_doc(self, namespace: str, slug: str) -> Dict[str, Any]: """获取文档内容""" return self._request('GET', f'/repos/{namespace}/docs/{slug}') def create_doc(self, namespace: str, title: str, content: str, format: str = "markdown") -> Dict[str, Any]: """创建文档""" data = { "title": title, "format": format, "body": content } return self._request('POST', f'/repos/{namespace}/docs', json=data) def update_doc(self, namespace: str, doc_id: int, title: str = None, content: str = None) -> Dict[str, Any]: """更新文档""" data = {} if title: data["title"] = title if content: data["body"] = content return self._request('PUT', f'/repos/{namespace}/docs/{doc_id}', json=data) def delete_doc(self, namespace: str, doc_id: int) -> Dict[str, Any]: """删除文档""" return self._request('DELETE', f'/repos/{namespace}/docs/{doc_id}') def search(self, query: str, type: str = "doc") -> Dict[str, Any]: """搜索文档或知识库""" return self._request('GET', f'/search?q={query}&type={type}') def list_groups(self) -> Dict[str, Any]: """列出用户的团队""" user_info = self.get_user_info() login = user_info["data"]["login"] return self._request('GET', f'/users/{login}/groups') def list_user_repos(self, login: str) -> Dict[str, Any]: """列出指定用户的知识库""" return self._request('GET', f'/users/{login}/repos') def list_group_repos(self, login: str) -> Dict[str, Any]: """列出指定团队的知识库""" return self._request('GET', f'/groups/{login}/repos') def create_repo( self, owner_login: str, name: str, slug: str = None, description: str = None, public: int = 0, owner_type: str = "user" ) -> Dict[str, Any]: """创建知识库,可指定 owner_type=user/group""" data = { "name": name, "public": public } if slug: data["slug"] = slug if description: data["description"] = description if owner_type == "group": endpoint = f'/groups/{owner_login}/repos' else: endpoint = f'/users/{owner_login}/repos' return self._request('POST', endpoint, json=data) def _build_repo_path(self, repo_id: int = None, namespace: str = None) -> str: if repo_id is not None: return f'/repos/{repo_id}' if namespace: if '/' not in namespace: raise ValueError("namespace 必须形如 owner/slug") owner, slug = namespace.split('/', 1) return f'/repos/{owner}/{slug}' raise ValueError("必须提供 repo_id 或 namespace") def update_repo( self, repo_id: int = None, namespace: str = None, name: str = None, slug: str = None, description: str = None, public: int = None, toc: str = None ) -> Dict[str, Any]: """更新知识库""" data = {} if name: data["name"] = name if slug: data["slug"] = slug if description: data["description"] = description if public is not None: data["public"] = public if toc is not None: data["toc"] = toc path = self._build_repo_path(repo_id, namespace) return self._request('PUT', path, json=data) def delete_repo(self, repo_id: int = None, namespace: str = None) -> Dict[str, Any]: """删除知识库""" path = self._build_repo_path(repo_id, namespace) return self._request('DELETE', path) def get_user(self, login: str) -> Dict[str, Any]: """获取指定用户信息""" return self._request('GET', f'/users/{login}') def get_group(self, group_id: int) -> Dict[str, Any]: """获取团队信息""" return self._request('GET', f'/groups/{group_id}') def list_group_users(self, group_id: int) -> Dict[str, Any]: """列出团队成员""" return self._request('GET', f'/groups/{group_id}/users') def update_group_member(self, group_login: str, user_identity: str, role: int) -> Dict[str, Any]: """变更团队成员角色""" return self._request( 'PUT', f'/groups/{group_login}/users/{user_identity}', json={"role": role} ) def remove_group_member(self, group_login: str, user_identity: str) -> Dict[str, Any]: """删除团队成员""" return self._request('DELETE', f'/groups/{group_login}/users/{user_identity}') def get_group_statistics(self, login: str) -> Dict[str, Any]: """团队汇总统计""" return self._request('GET', f'/groups/{login}/statistics') def get_group_member_statistics(self, login: str, **params) -> Dict[str, Any]: """团队成员统计""" return self._request('GET', f'/groups/{login}/statistics/members', params=params) def get_group_book_statistics(self, login: str, **params) -> Dict[str, Any]: """团队知识库统计""" return self._request('GET', f'/groups/{login}/statistics/books', params=params) def get_group_doc_statistics(self, login: str, **params) -> Dict[str, Any]: """团队文档统计""" return self._request('GET', f'/groups/{login}/statistics/docs', params=params) def get_repo_toc(self, repo_id: int = None, namespace: str = None) -> Dict[str, Any]: """获取知识库目录""" path = self._build_repo_path(repo_id, namespace) return self._request('GET', f'{path}/toc') def update_repo_toc(self, repo_id: int = None, namespace: str = None, toc_markdown: str = "") -> Dict[str, Any]: """更新知识库目录(整体替换)""" path = self._build_repo_path(repo_id, namespace) return self._request('PUT', path, json={"toc": toc_markdown}) def list_doc_versions(self, doc_id: int) -> Dict[str, Any]: """列出文档版本(最新100条)""" return self._request('GET', '/doc_versions', params={"doc_id": doc_id}) def get_doc_version(self, version_id: int) -> Dict[str, Any]: """获取指定文档版本详情""" return self._request('GET', f'/doc_versions/{version_id}') @app.after_request def add_cors_headers(response): """为所有响应附加 CORS 头,便于 Chatbox 直接访问""" response.headers['Access-Control-Allow-Origin'] = DEFAULT_CORS_ORIGIN response.headers['Access-Control-Allow-Methods'] = 'GET,POST,OPTIONS' response.headers['Access-Control-Allow-Headers'] = 'Content-Type,Authorization,X-Yuque-Token' response.headers['Access-Control-Max-Age'] = '86400' return response @app.route('/mcp', methods=['GET', 'POST', 'OPTIONS']) def handle_mcp(): """处理 MCP 协议请求""" if request.method == 'OPTIONS': return '', 200 elif request.method == 'POST': data = request.get_json(silent=True) or {} try: logger.info( "收到 MCP 请求: method=%s id=%s path=%s", data.get("method", "unknown"), data.get("id"), request.path ) if data.get("jsonrpc") != "2.0": return jsonify({ "jsonrpc": "2.0", "id": data.get("id"), "error": {"code": -32600, "message": "Invalid Request"} }), 400 method = data.get("method") request_id = data.get("id") if method != "notifications/initialized" and request_id is None: logger.warning("JSON-RPC 请求缺少 id 字段") return jsonify({ "jsonrpc": "2.0", "id": None, "error": {"code": -32600, "message": "Missing id in request"} }), 400 if method == "initialize": return handle_initialize(data) elif method == "tools/list": return handle_tools_list(data) elif method == "tools/call": return handle_tools_call(data) elif method == "ping": return handle_ping(data) elif method == "notifications/initialized": logger.info("收到 notifications/initialized 通知,已完成握手。") return '', 204 else: return jsonify({ "jsonrpc": "2.0", "id": data.get("id"), "error": {"code": -32601, "message": f"Method not found: {method}"} }), 200 except Exception as e: logger.error(f"处理请求时出错: {e}", exc_info=True) return jsonify({ "jsonrpc": "2.0", "id": data.get("id") if 'data' in locals() else None, "error": {"code": -32603, "message": f"Internal error: {str(e)}"} }), 500 elif request.method == 'GET': # SSE 连接保持 def generate_heartbeat(): while True: yield f"data: {json.dumps({'type': 'heartbeat', 'timestamp': time.time()})}\n\n" time.sleep(30) headers = { 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' } return Response( generate_heartbeat(), content_type='text/event-stream', headers=headers ) def handle_initialize(data): """处理初始化请求""" response = { "jsonrpc": "2.0", "id": data.get("id"), "result": { "protocolVersion": MCP_PROTOCOL_VERSION, "capabilities": { "tools": {}, "logs": {}, "progress": {}, "readers": {} }, "serverInfo": { "name": "yuque-mcp-server", "version": "2.0.0" } } } return jsonify(response) def handle_tools_list(data): """返回可用的工具列表""" tools = [ { "name": "get_user_info", "description": "获取当前语雀用户信息", "inputSchema": {"type": "object", "properties": {}} }, { "name": "get_user", "description": "根据登录名获取用户信息", "inputSchema": { "type": "object", "properties": { "login": {"type": "string", "description": "语雀登录名"} }, "required": ["login"] } }, { "name": "list_repos", "description": "列出当前用户的所有知识库", "inputSchema": {"type": "object", "properties": {}} }, { "name": "list_user_repos", "description": "列出指定用户的知识库", "inputSchema": { "type": "object", "properties": { "login": {"type": "string", "description": "语雀登录名"} }, "required": ["login"] } }, { "name": "list_group_repos", "description": "列出指定团队的知识库", "inputSchema": { "type": "object", "properties": { "login": {"type": "string", "description": "团队登录名"} }, "required": ["login"] } }, { "name": "get_repo", "description": "获取知识库详细信息", "inputSchema": { "type": "object", "properties": { "namespace": {"type": "string", "description": "知识库命名空间"} }, "required": ["namespace"] } }, { "name": "create_repo", "description": "创建知识库", "inputSchema": { "type": "object", "properties": { "owner_login": {"type": "string", "description": "所属用户或团队登录名(推荐)"}, "login": {"type": "string", "description": "兼容旧版字段,与 owner_login 等价"}, "owner_type": {"type": "string", "description": "owner 类型 user 或 group", "enum": ["user", "group"]}, "name": {"type": "string", "description": "知识库名称"}, "slug": {"type": "string", "description": "知识库路径(可选)"}, "description": {"type": "string", "description": "知识库描述"}, "public": { "type": "integer", "description": "公开范围:0私密,1团队内,2公开", "enum": [0, 1, 2] } }, "required": ["name"] } }, { "name": "update_repo", "description": "更新知识库信息", "inputSchema": { "type": "object", "properties": { "repo_id": {"type": "integer", "description": "知识库ID(与 namespace 二选一)"}, "namespace": {"type": "string", "description": "知识库命名空间,如 team/book"}, "name": {"type": "string", "description": "新的名称"}, "slug": {"type": "string", "description": "新的路径"}, "description": {"type": "string", "description": "新的描述"}, "public": { "type": "integer", "description": "公开范围:0私密,1团队内,2公开", "enum": [0, 1, 2] }, "toc": {"type": "string", "description": "Markdown 目录文本"} }, "required": [] } }, { "name": "delete_repo", "description": "删除知识库(谨慎操作)", "inputSchema": { "type": "object", "properties": { "repo_id": {"type": "integer", "description": "知识库ID(与 namespace 二选一)"}, "namespace": {"type": "string", "description": "知识库命名空间,如 team/book"} }, "required": [] } }, { "name": "list_docs", "description": "列出知识库中的文档", "inputSchema": { "type": "object", "properties": { "namespace": {"type": "string", "description": "知识库命名空间"} }, "required": ["namespace"] } }, { "name": "get_doc", "description": "获取文档详细内容", "inputSchema": { "type": "object", "properties": { "namespace": {"type": "string", "description": "知识库命名空间"}, "slug": {"type": "string", "description": "文档标识"} }, "required": ["namespace", "slug"] } }, { "name": "list_doc_versions", "description": "查看文档版本历史", "inputSchema": { "type": "object", "properties": { "doc_id": {"type": "integer", "description": "文档ID"} }, "required": ["doc_id"] } }, { "name": "get_doc_version", "description": "获取指定版本详情", "inputSchema": { "type": "object", "properties": { "version_id": {"type": "integer", "description": "版本ID"} }, "required": ["version_id"] } }, { "name": "create_doc", "description": "创建新文档", "inputSchema": { "type": "object", "properties": { "namespace": {"type": "string", "description": "知识库命名空间"}, "title": {"type": "string", "description": "文档标题"}, "content": {"type": "string", "description": "文档内容"}, "format": {"type": "string", "description": "文档格式", "enum": ["markdown", "lake"]} }, "required": ["namespace", "title", "content"] } }, { "name": "update_doc", "description": "更新现有文档内容", "inputSchema": { "type": "object", "properties": { "namespace": {"type": "string", "description": "知识库命名空间"}, "doc_id": {"type": "integer", "description": "文档ID"}, "title": {"type": "string", "description": "新标题(可选)"}, "content": {"type": "string", "description": "新内容"}, "format": {"type": "string", "description": "文档格式", "enum": ["markdown", "lake"]} }, "required": ["namespace", "doc_id"] } }, { "name": "delete_doc", "description": "删除文档", "inputSchema": { "type": "object", "properties": { "namespace": {"type": "string", "description": "知识库命名空间"}, "doc_id": {"type": "integer", "description": "文档ID"} }, "required": ["namespace", "doc_id"] } }, { "name": "search_docs", "description": "搜索文档或知识库", "inputSchema": { "type": "object", "properties": { "query": {"type": "string", "description": "搜索关键词"}, "type": {"type": "string", "description": "搜索类型", "enum": ["doc", "repo"]} }, "required": ["query"] } }, { "name": "list_groups", "description": "列出用户的团队", "inputSchema": {"type": "object", "properties": {}} }, { "name": "get_group", "description": "获取指定团队信息", "inputSchema": { "type": "object", "properties": { "group_id": {"type": "integer", "description": "团队ID"} }, "required": ["group_id"] } }, { "name": "list_group_users", "description": "列出团队成员列表", "inputSchema": { "type": "object", "properties": { "group_id": {"type": "integer", "description": "团队ID"} }, "required": ["group_id"] } }, { "name": "update_group_member", "description": "更新团队成员角色", "inputSchema": { "type": "object", "properties": { "group_login": {"type": "string", "description": "团队登录名或ID"}, "user_identity": {"type": "string", "description": "成员登录名或ID"}, "role": { "type": "integer", "description": "角色:0管理员 1成员 2只读", "enum": [0, 1, 2] } }, "required": ["group_login", "user_identity", "role"] } }, { "name": "remove_group_member", "description": "从团队移除成员", "inputSchema": { "type": "object", "properties": { "group_login": {"type": "string", "description": "团队登录名或ID"}, "user_identity": {"type": "string", "description": "成员登录名或ID"} }, "required": ["group_login", "user_identity"] } }, { "name": "get_group_statistics", "description": "获取团队汇总统计数据", "inputSchema": { "type": "object", "properties": { "group_login": {"type": "string", "description": "团队登录名或ID"} }, "required": ["group_login"] } }, { "name": "get_group_member_stats", "description": "获取团队成员统计数据", "inputSchema": { "type": "object", "properties": { "group_login": {"type": "string", "description": "团队登录名或ID"}, "name": {"type": "string", "description": "成员名(可选)"}, "range": {"type": "integer", "enum": [0, 30, 365], "description": "时间范围"}, "page": {"type": "integer", "description": "页码"}, "limit": {"type": "integer", "description": "分页数量"}, "sortField": { "type": "string", "description": "排序字段", "enum": ["write_doc_count", "write_count", "read_count", "like_count"] }, "sortOrder": {"type": "string", "enum": ["asc", "desc"], "description": "排序方向"} }, "required": ["group_login"] } }, { "name": "get_group_book_stats", "description": "获取团队知识库统计数据", "inputSchema": { "type": "object", "properties": { "group_login": {"type": "string", "description": "团队登录名或ID"}, "name": {"type": "string", "description": "知识库名(可选)"}, "range": {"type": "integer", "enum": [0, 30, 365], "description": "时间范围"}, "page": {"type": "integer", "description": "页码"}, "limit": {"type": "integer", "description": "分页数量"} }, "required": ["group_login"] } }, { "name": "get_group_doc_stats", "description": "获取团队文档统计数据", "inputSchema": { "type": "object", "properties": { "group_login": {"type": "string", "description": "团队登录名或ID"}, "title": {"type": "string", "description": "文档名(可选)"}, "range": {"type": "integer", "enum": [0, 30, 365], "description": "时间范围"}, "page": {"type": "integer", "description": "页码"}, "limit": {"type": "integer", "description": "分页数量"} }, "required": ["group_login"] } }, { "name": "get_repo_toc", "description": "查看知识库目录", "inputSchema": { "type": "object", "properties": { "repo_id": {"type": "integer", "description": "知识库ID"} }, "required": ["repo_id"] } }, { "name": "update_repo_toc", "description": "更新知识库目录(Markdown 格式)", "inputSchema": { "type": "object", "properties": { "repo_id": {"type": "integer", "description": "知识库ID"}, "toc": {"type": "string", "description": "Markdown 目录文本"} }, "required": ["repo_id", "toc"] } } ] return jsonify({ "jsonrpc": "2.0", "id": data.get("id"), "result": {"tools": tools} }) def handle_tools_call(data): """处理工具调用请求""" params = data.get("params", {}) tool_name = params.get("name") arguments = params.get("arguments", {}) request_id = data.get("id") logger.info(f"调用工具: {tool_name}, 参数: {arguments}") # 获取 Token 并创建客户端 try: token = get_yuque_token() yuque_client = YuqueMCPClient(token) except ValueError as e: return jsonify({ "jsonrpc": "2.0", "id": request_id, "error": { "code": -32602, "message": str(e) } }), 400 try: if tool_name == "get_user_info": result = yuque_client.get_user_info() return jsonify({ "jsonrpc": "2.0", "id": request_id, "result": { "content": [{"type": "text", "text": format_user_info(result)}] } }) elif tool_name == "get_user": login = arguments["login"] result = yuque_client.get_user(login) return jsonify({ "jsonrpc": "2.0", "id": request_id, "result": { "content": [{"type": "text", "text": format_user_info(result)}] } }) elif tool_name == "list_repos": result = yuque_client.list_repos() return jsonify({ "jsonrpc": "2.0", "id": request_id, "result": { "content": [{"type": "text", "text": format_repos_list(result)}] } }) elif tool_name == "list_user_repos": login = arguments["login"] result = yuque_client.list_user_repos(login) return jsonify({ "jsonrpc": "2.0", "id": request_id, "result": { "content": [{"type": "text", "text": format_repos_list(result)}] } }) elif tool_name == "list_group_repos": login = arguments["login"] result = yuque_client.list_group_repos(login) return jsonify({ "jsonrpc": "2.0", "id": request_id, "result": { "content": [{"type": "text", "text": format_repos_list(result)}] } }) elif tool_name == "get_repo": namespace = arguments["namespace"] result = yuque_client.get_repo(namespace) return jsonify({ "jsonrpc": "2.0", "id": request_id, "result": { "content": [{"type": "text", "text": format_repo_info(result)}] } }) elif tool_name == "create_repo": owner_login = arguments.get("owner_login") or arguments.get("login") if not owner_login: raise ValueError("创建知识库需要提供 owner_login(或 login)字段") owner_type = arguments.get("owner_type", "user") name = arguments["name"] slug = arguments.get("slug") description = arguments.get("description") public = arguments.get("public", 0) result = yuque_client.create_repo( owner_login, name, slug, description, public, owner_type=owner_type ) return jsonify({ "jsonrpc": "2.0", "id": request_id, "result": { "content": [{"type": "text", "text": format_repo_created(result, owner_login)}] } }) elif tool_name == "update_repo": repo_id = arguments.get("repo_id") namespace = arguments.get("namespace") if repo_id is None and not namespace: raise ValueError("需要提供 repo_id 或 namespace") result = yuque_client.update_repo( repo_id=repo_id, namespace=namespace, name=arguments.get("name"), slug=arguments.get("slug"), description=arguments.get("description"), public=arguments.get("public"), toc=arguments.get("toc") ) return jsonify({ "jsonrpc": "2.0", "id": request_id, "result": { "content": [{"type": "text", "text": format_repo_info(result)}] } }) elif tool_name == "delete_repo": repo_id = arguments.get("repo_id") namespace = arguments.get("namespace") if repo_id is None and not namespace: raise ValueError("需要提供 repo_id 或 namespace") yuque_client.delete_repo(repo_id=repo_id, namespace=namespace) return jsonify({ "jsonrpc": "2.0", "id": request_id, "result": { "content": [{"type": "text", "text": "知识库已删除"}] } }) elif tool_name == "list_docs": namespace = arguments["namespace"] result = yuque_client.list_docs(namespace) return jsonify({ "jsonrpc": "2.0", "id": request_id, "result": { "content": [{"type": "text", "text": format_docs_list(result, namespace)}] } }) elif tool_name == "get_doc": namespace = arguments["namespace"] slug = arguments["slug"] result = yuque_client.get_doc(namespace, slug) return jsonify({ "jsonrpc": "2.0", "id": request_id, "result": { "content": [{"type": "text", "text": format_doc_content(result)}] } }) elif tool_name == "list_doc_versions": doc_id = arguments["doc_id"] result = yuque_client.list_doc_versions(doc_id) return jsonify({ "jsonrpc": "2.0", "id": request_id, "result": { "content": [{"type": "text", "text": format_doc_versions(result, doc_id)}] } }) elif tool_name == "get_doc_version": version_id = arguments["version_id"] result = yuque_client.get_doc_version(version_id) return jsonify({ "jsonrpc": "2.0", "id": request_id, "result": { "content": [{"type": "text", "text": format_doc_version_detail(result)}] } }) elif tool_name == "create_doc": namespace = arguments["namespace"] title = arguments["title"] content = arguments["content"] format_type = arguments.get("format", "markdown") result = yuque_client.create_doc(namespace, title, content, format_type) return jsonify({ "jsonrpc": "2.0", "id": request_id, "result": { "content": [{"type": "text", "text": format_created_doc(result, namespace)}] } }) elif tool_name == "update_doc": namespace = arguments["namespace"] doc_id = arguments["doc_id"] title = arguments.get("title") content = arguments.get("content") if not title and not content: raise ValueError("更新文档时至少提供 title 或 content 之一") result = yuque_client.update_doc(namespace, doc_id, title, content) return jsonify({ "jsonrpc": "2.0", "id": request_id, "result": { "content": [{"type": "text", "text": f"文档更新成功!文档ID: {doc_id}"}] } }) elif tool_name == "delete_doc": namespace = arguments["namespace"] doc_id = arguments["doc_id"] result = yuque_client.delete_doc(namespace, doc_id) return jsonify({ "jsonrpc": "2.0", "id": request_id, "result": { "content": [{"type": "text", "text": f"文档删除成功!文档ID: {doc_id}"}] } }) elif tool_name == "search_docs": query = arguments["query"] search_type = arguments.get("type", "doc") result = yuque_client.search(query, search_type) return jsonify({ "jsonrpc": "2.0", "id": request_id, "result": { "content": [{"type": "text", "text": format_search_results(result, query)}] } }) elif tool_name == "list_groups": result = yuque_client.list_groups() return jsonify({ "jsonrpc": "2.0", "id": request_id, "result": { "content": [{"type": "text", "text": format_groups_list(result)}] } }) elif tool_name == "get_group": group_id = arguments["group_id"] result = yuque_client.get_group(group_id) return jsonify({ "jsonrpc": "2.0", "id": request_id, "result": { "content": [{"type": "text", "text": format_group_info(result)}] } }) elif tool_name == "list_group_users": group_id = arguments["group_id"] result = yuque_client.list_group_users(group_id) return jsonify({ "jsonrpc": "2.0", "id": request_id, "result": { "content": [{"type": "text", "text": format_group_users(result, group_id)}] } }) elif tool_name == "update_group_member": group_login = arguments["group_login"] user_identity = arguments["user_identity"] role = arguments["role"] yuque_client.update_group_member(group_login, user_identity, role) return jsonify({ "jsonrpc": "2.0", "id": request_id, "result": { "content": [{"type": "text", "text": f"已更新 {user_identity} 在 {group_login} 的角色为 {role}"}] } }) elif tool_name == "remove_group_member": group_login = arguments["group_login"] user_identity = arguments["user_identity"] yuque_client.remove_group_member(group_login, user_identity) return jsonify({ "jsonrpc": "2.0", "id": request_id, "result": { "content": [{"type": "text", "text": f"已将 {user_identity} 从团队 {group_login} 移除"}] } }) elif tool_name == "get_group_statistics": group_login = arguments["group_login"] result = yuque_client.get_group_statistics(group_login) return jsonify({ "jsonrpc": "2.0", "id": request_id, "result": { "content": [{"type": "text", "text": format_group_statistics(result)}] } }) elif tool_name == "get_group_member_stats": group_login = arguments["group_login"] params = {k: v for k, v in arguments.items() if k not in {"group_login"} and v is not None} result = yuque_client.get_group_member_statistics(group_login, **params) return jsonify({ "jsonrpc": "2.0", "id": request_id, "result": { "content": [{"type": "text", "text": format_group_member_stats(result)}] } }) elif tool_name == "get_group_book_stats": group_login = arguments["group_login"] params = {k: v for k, v in arguments.items() if k not in {"group_login"} and v is not None} result = yuque_client.get_group_book_statistics(group_login, **params) return jsonify({ "jsonrpc": "2.0", "id": request_id, "result": { "content": [{"type": "text", "text": format_group_book_stats(result)}] } }) elif tool_name == "get_group_doc_stats": group_login = arguments["group_login"] params = {k: v for k, v in arguments.items() if k not in {"group_login"} and v is not None} result = yuque_client.get_group_doc_statistics(group_login, **params) return jsonify({ "jsonrpc": "2.0", "id": request_id, "result": { "content": [{"type": "text", "text": format_group_doc_stats(result)}] } }) elif tool_name == "get_repo_toc": repo_id = arguments["repo_id"] result = yuque_client.get_repo_toc(repo_id=repo_id) return jsonify({ "jsonrpc": "2.0", "id": request_id, "result": { "content": [{"type": "text", "text": format_repo_toc(result)}] } }) elif tool_name == "update_repo_toc": repo_id = arguments["repo_id"] toc_markdown = arguments["toc"] yuque_client.update_repo_toc(repo_id=repo_id, toc_markdown=toc_markdown) return jsonify({ "jsonrpc": "2.0", "id": request_id, "result": { "content": [{"type": "text", "text": "目录更新成功"}] } }) else: return jsonify({ "jsonrpc": "2.0", "id": request_id, "error": {"code": -32601, "message": f"未知工具: {tool_name}"} }) except requests.exceptions.HTTPError as e: if e.response.status_code == 403: error_msg = "语雀API权限不足:请检查Token是否有写权限" elif e.response.status_code == 404: error_msg = "资源未找到:请检查命名空间和文档ID" else: error_msg = f"HTTP错误: {e.response.status_code}" logger.error(f"语雀API错误: {error_msg}") return jsonify({ "jsonrpc": "2.0", "id": request_id, "error": { "code": -32000, "message": error_msg, "data": { "status_code": e.response.status_code, "yuque_error": e.response.text } } }) except Exception as e: logger.error(f"工具调用失败: {e}") return jsonify({ "jsonrpc": "2.0", "id": request_id, "error": {"code": -32000, "message": f"工具执行失败: {str(e)}"} }) def handle_ping(data): """处理 ping 请求""" return jsonify({ "jsonrpc": "2.0", "id": data.get("id"), "result": {} }) # 格式化函数 def format_user_info(user_data: Dict) -> str: """格式化用户信息""" user = user_data.get("data", {}) return f"""👤 语雀用户信息 姓名: {user.get('name', '未知')} 登录名: {user.get('login', '未知')} 用户ID: {user.get('id', '未知')} 知识库数量: {user.get('books_count', 0)} 关注: {user.get('following_count', 0)} | 粉丝: {user.get('followers_count', 0)} 注册时间: {user.get('created_at', '未知')}""" def format_repos_list(repos_data: Dict) -> str: """格式化知识库列表""" repos = repos_data.get("data", []) if not repos: return "暂无知识库" # 按文档数量排序 repos.sort(key=lambda x: x.get('items_count', 0), reverse=True) result = ["📚 您的语雀知识库列表 (按文档数量排序):"] for repo in repos: result.append(f"📖 {repo.get('name', '未知')}") result.append(f" 命名空间: {repo.get('namespace', '未知')}") result.append(f" 文档数: {repo.get('items_count', 0)} | 更新: {repo.get('updated_at', '未知')[:10]}") result.append("") return "\n".join(result) def format_repo_info(repo_data: Dict) -> str: """格式化知识库信息""" repo = repo_data.get("data", {}) return f"""知识库详情: 📖 名称: {repo.get('name', '未知')} 🔗 命名空间: {repo.get('namespace', '未知')} 📄 文档数量: {repo.get('items_count', 0)} 👀 关注数: {repo.get('watches_count', 0)} ❤️ 点赞数: {repo.get('likes_count', 0)} 📝 描述: {repo.get('description', '暂无描述')} 🕐 创建时间: {repo.get('created_at', '未知')} ✏️ 最后更新: {repo.get('updated_at', '未知')}""" def format_docs_list(docs_data: Dict, namespace: str) -> str: """格式化文档列表""" docs = docs_data.get("data", []) if not docs: return f"知识库 '{namespace}' 暂无文档" result = [f"📄 知识库 '{namespace}' 中的文档:"] for i, doc in enumerate(docs, 1): result.append(f"{i}. {doc.get('title', '未知标题')}") result.append(f" 文档ID: {doc.get('id', '未知')}") result.append(f" 最后更新: {doc.get('updated_at', '未知')[:10]}") result.append("") return "\n".join(result) def format_doc_content(doc_data: Dict) -> str: """格式化文档内容""" doc = doc_data.get("data", {}) return f"""文档内容: 📖 标题: {doc.get('title', '未知')} 🆔 ID: {doc.get('id', '未知')} 📝 格式: {doc.get('format', '未知')} 📅 创建: {doc.get('created_at', '未知')} ✏️ 更新: {doc.get('updated_at', '未知')} 内容预览: {doc.get('body', '暂无内容')[:500]}...""" def format_created_doc(doc_data: Dict, namespace: str) -> str: """格式化创建的文档信息""" doc = doc_data.get("data", {}) doc_url = f"https://www.yuque.com/{namespace}/{doc.get('slug', '')}" return f"""✅ 文档创建成功! 📖 标题: {doc.get('title', '未知')} 🆔 文档ID: {doc.get('id', '未知')} 🔗 访问链接: {doc_url} 📅 创建时间: {doc.get('created_at', '未知')}""" def format_repo_created(repo_data: Dict, owner_login: str) -> str: repo = repo_data.get("data", {}) namespace = repo.get("namespace", "未知") visibility = {0: "私密", 1: "团队可见", 2: "公开"}.get(repo.get("public", 0), "未知") return f"""✅ 知识库创建成功! 📚 名称: {repo.get('name', '未知')} 👤 所属: {owner_login} 🔗 命名空间: {namespace} 🌐 可见性: {visibility} 📅 创建时间: {repo.get('created_at', '未知')}""" def format_doc_versions(versions_data: Dict, doc_id: int) -> str: versions = versions_data.get("data", []) if not versions: return f"文档 {doc_id} 暂无版本历史。" lines = [f"📜 文档 {doc_id} 版本历史(最多 10 条):"] for version in versions[:10]: creator = version.get("creator", {}).get("name") if isinstance(version.get("creator"), dict) else version.get("creator") lines.append( f"- 版本 {version.get('version', version.get('id', '未知'))} · " f"{version.get('title', '未命名')} · " f"{creator or '匿名'} @ {version.get('created_at', '未知')}" ) if len(versions) > 10: lines.append("... 其余版本请在语雀查看。") return "\n".join(lines) def format_doc_version_detail(version_data: Dict) -> str: version = version_data.get("data", {}) creator = version.get("creator", {}) return f"""📘 文档版本详情 版本号: {version.get('version', '未知')} 标题: {version.get('title', '未命名')} 作者: {creator.get('name') or creator.get('login', '未知')} 创建时间: {version.get('created_at', '未知')} 变更说明: {version.get('description', '无')} """ def format_search_results(search_data: Dict, query: str) -> str: """格式化搜索结果""" results = search_data.get("data", []) if not results: return f"未找到与 '{query}' 相关的文档" result = [f"🔍 搜索 '{query}' 的结果 (前10个):"] for item in results[:10]: result.append(f"📄 {item.get('title', '未知')}") result.append(f" 知识库: {item.get('book', {}).get('name', '未知')}") result.append(f" 摘要: {item.get('summary', '')[:100]}...") result.append("") return "\n".join(result) def format_groups_list(groups_data: Dict) -> str: """格式化团队列表""" groups = groups_data.get("data", []) if not groups: return "您尚未加入任何团队" result = ["👥 我的团队列表:"] for group in groups: result.append(f"- {group.get('name', '未知')} (ID: {group.get('id', '未知')})") result.append(f" 描述: {group.get('description', '暂无描述')}") result.append(f" 成员数: {group.get('members_count', 0)}") return "\n".join(result) def format_group_info(group_data: Dict) -> str: group = group_data.get("data", {}) return f"""👥 团队信息 名称: {group.get('name', '未知')} ID: {group.get('id', '未知')} 描述: {group.get('description', '暂无描述')} 成员数: {group.get('members_count', 0)} 创建时间: {group.get('created_at', '未知')} 更新: {group.get('updated_at', '未知')}""" def format_group_users(users_data: Dict, group_id: int) -> str: users = users_data.get("data", []) if not users: return f"团队 {group_id} 暂无成员信息。" result = [f"👤 团队 {group_id} 的成员:"] for user in users: result.append(f"- {user.get('name', '未知')} ({user.get('login', '未知')}) 角色: {user.get('role', 'member')}") return "\n".join(result) def format_repo_toc(toc_data: Dict) -> str: data = toc_data.get("data") if not data: return "目录为空或未配置。" toc_text = "" if isinstance(data, dict): toc_text = data.get("toc_yml") or data.get("toc_yaml") or data.get("toc") or "" elif isinstance(data, str): toc_text = data if not toc_text: return "目录内容为空。" preview = "\n".join(toc_text.splitlines()[:40]) if len(toc_text.splitlines()) > 40: preview += "\n..." return f"📚 当前目录(Markdown):\n{preview}" def format_group_statistics(stats_data: Dict) -> str: stats = stats_data.get("data", {}) if not stats: return "暂无团队统计数据。" lines = [ "📊 团队汇总统计", f"成员数: {stats.get('member_count', '未知')}", f"知识库数: {stats.get('book_count', '未知')} (公开 {stats.get('public_book_count', '未知')})", f"文档数: {stats.get('doc_count', '未知')}", f"近30天阅读: {stats.get('read_count_30', '未知')}, 写作: {stats.get('write_count_30', '未知')}", f"累计点赞: {stats.get('like_count', '未知')} · 评论: {stats.get('comment_count', '未知')}", f"数据占用: {stats.get('data_usage', '未知')}" ] return "\n".join(lines) def format_group_member_stats(stats_data: Dict) -> str: members = stats_data.get("data", {}).get("members") if not members: return "未查询到成员统计信息。" if isinstance(members, dict): members = members.values() lines = ["👥 成员活跃度(前 10 条):"] for item in list(members)[:10]: user = item.get("user", {}) lines.append( f"- {user.get('name', '未知')} · 写作 {item.get('write_count', 0)} 次 · " f"阅读 {item.get('read_count', 0)} 次 · 点赞 {item.get('like_count', 0)}" ) total = stats_data.get("data", {}).get("total") if total: lines.append(f"共 {total} 人") return "\n".join(lines) def format_group_book_stats(stats_data: Dict) -> str: books = stats_data.get("data", {}).get("books") or stats_data.get("data", []) if isinstance(books, dict): books = books.values() books = list(books) if not books: return "未查询到知识库统计信息。" lines = ["📚 知识库统计(前 10 条):"] for book in books[:10]: lines.append( f"- {book.get('name', '未命名')} · 阅读 {book.get('read_count', 0)} · " f"写作 {book.get('write_count', 0)} · 点赞 {book.get('like_count', 0)}" ) total = stats_data.get("data", {}).get("total") if total: lines.append(f"共 {total} 个知识库") return "\n".join(lines) def format_group_doc_stats(stats_data: Dict) -> str: docs = stats_data.get("data", {}).get("docs") or stats_data.get("data", {}).get("documents") or stats_data.get("data", []) if isinstance(docs, dict): docs = docs.values() docs = list(docs) if not docs: return "未查询到文档统计信息。" lines = ["📄 文档统计(前 10 条):"] for doc in docs[:10]: lines.append( f"- {doc.get('title', '未命名')} · 阅读 {doc.get('read_count', 0)} · " f"评论 {doc.get('comment_count', 0)} · 点赞 {doc.get('like_count', 0)}" ) total = stats_data.get("data", {}).get("total") if total: lines.append(f"共 {total} 篇文档统计数据") return "\n".join(lines) users = users_data.get("data", []) if not users: return f"团队 {group_id} 暂无成员信息。" result = [f"👤 团队 {group_id} 的成员:"] for user in users: result.append(f"- {user.get('name', '未知')} ({user.get('login', '未知')}) 角色: {user.get('role', 'member')}") return "\n".join(result) @app.route('/health', methods=['GET']) def health_check(): """健康检查端点""" try: # 尝试获取 Token 并创建客户端 token = get_yuque_token() yuque_client = YuqueMCPClient(token) user_info = yuque_client.get_user_info() user_login = user_info.get("data", {}).get("login", "unknown") return jsonify({ 'status': 'healthy', 'message': '语雀MCP服务器运行正常', 'user': user_login, 'token_source': 'header' if request.headers.get('X-Yuque-Token') else 'environment' }) except ValueError as e: # Token 配置缺失 return jsonify({ 'status': 'configured', 'message': '服务器运行正常,但缺少语雀 Token 配置', 'error': str(e) }), 200 except Exception as e: return jsonify({ 'status': 'error', 'error': str(e) }), 500 @app.route('/test', methods=['GET']) def test_endpoint(): """测试端点""" return jsonify({ 'server': 'yuque-mcp-server', 'version': '2.0.0', 'status': 'running' }) if __name__ == '__main__': port = int(os.environ.get('PORT', 3000)) print(f"🚀 语雀 MCP 服务器启动在 http://localhost:{port}") print(f"📊 健康检查: http://localhost:{port}/health") print(f"🔗 MCP 端点: http://localhost:{port}/mcp") print(f"🧪 测试端点: http://localhost:{port}/test") print(f"📚 支持功能: 用户信息、知识库管理、文档CRUD、搜索、团队管理") app.run(host='0.0.0.0', port=port, debug=False)

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/suonian/yuque-mcp-server'

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