#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
McpError,
ErrorCode,
} from '@modelcontextprotocol/sdk/types.js';
import { YuqueClient } from './yuque-client.js';
async function createServer() {
const token = process.env.YUQUE_TOKEN;
if (!token) {
console.error('YUQUE_TOKEN environment variable is required');
process.exit(1);
}
const server = new Server(
{
name: 'yuque-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
const yuqueClient = new YuqueClient(token);
// 工具列表处理
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'yuque_get_user',
description: '获取当前用户信息',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'yuque_get_repos',
description: '获取知识库列表',
inputSchema: {
type: 'object',
properties: {
userId: {
type: 'string',
description: '用户ID,不提供则获取当前用户的知识库',
},
},
},
},
{
name: 'yuque_get_docs',
description: '获取文档列表',
inputSchema: {
type: 'object',
properties: {
repoId: {
type: 'number',
description: '知识库ID',
},
limit: {
type: 'number',
description: '返回数量限制,默认20',
},
offset: {
type: 'number',
description: '偏移量,默认0',
},
},
required: ['repoId'],
},
},
{
name: 'yuque_get_doc',
description: '获取文档详情',
inputSchema: {
type: 'object',
properties: {
docId: {
type: 'number',
description: '文档ID',
},
repoId: {
type: 'number',
description: '知识库ID',
},
},
required: ['docId', 'repoId'],
},
},
{
name: 'yuque_create_doc',
description: '创建新文档',
inputSchema: {
type: 'object',
properties: {
repoId: {
type: 'number',
description: '知识库ID',
},
title: {
type: 'string',
description: '文档标题',
},
content: {
type: 'string',
description: '文档内容',
},
format: {
type: 'string',
enum: ['markdown', 'lake', 'html'],
description: '文档格式,默认markdown',
},
},
required: ['repoId', 'title', 'content'],
},
},
{
name: 'yuque_update_doc',
description: '更新文档',
inputSchema: {
type: 'object',
properties: {
docId: {
type: 'number',
description: '文档ID',
},
repoId: {
type: 'number',
description: '知识库ID',
},
title: {
type: 'string',
description: '文档标题',
},
content: {
type: 'string',
description: '文档内容',
},
format: {
type: 'string',
enum: ['markdown', 'lake', 'html'],
description: '文档格式',
},
},
required: ['docId', 'repoId'],
},
},
{
name: 'yuque_delete_doc',
description: '删除文档',
inputSchema: {
type: 'object',
properties: {
docId: {
type: 'number',
description: '文档ID',
},
repoId: {
type: 'number',
description: '知识库ID',
},
},
required: ['docId', 'repoId'],
},
},
{
name: 'yuque_search_docs',
description: '搜索文档',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: '搜索关键词',
},
repoId: {
type: 'number',
description: '知识库ID,可选,不提供则全局搜索',
},
},
required: ['query'],
},
},
{
name: 'yuque_create_repo',
description: '创建知识库',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: '知识库名称',
},
description: {
type: 'string',
description: '知识库描述,可选',
},
isPublic: {
type: 'boolean',
description: '是否公开,默认false',
},
},
required: ['name'],
},
},
],
};
});
// 工具调用处理
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'yuque_get_user':
return await handleGetUser();
case 'yuque_get_repos':
return await handleGetRepos(args as { userId?: string });
case 'yuque_get_docs':
return await handleGetDocs(args as { repoId: number; limit?: number; offset?: number });
case 'yuque_get_doc':
return await handleGetDoc(args as { docId: number; repoId: number });
case 'yuque_create_doc':
return await handleCreateDoc(args as { repoId: number; title: string; content: string; format?: 'markdown' | 'lake' | 'html' });
case 'yuque_update_doc':
return await handleUpdateDoc(args as { docId: number; repoId: number; title?: string; content?: string; format?: 'markdown' | 'lake' | 'html' });
case 'yuque_delete_doc':
return await handleDeleteDoc(args as { docId: number; repoId: number });
case 'yuque_search_docs':
return await handleSearchDocs(args as { query: string; repoId?: number });
case 'yuque_create_repo':
return await handleCreateRepo(args as { name: string; description?: string; isPublic?: boolean });
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
}
} catch (error) {
if (error instanceof McpError) {
throw error;
}
throw new McpError(ErrorCode.InternalError, `Error executing ${name}: ${error}`);
}
});
// 处理函数
async function handleGetUser() {
const user = await yuqueClient.getUser();
return {
content: [
{
type: 'text',
text: JSON.stringify(user, null, 2),
},
],
};
}
async function handleGetRepos(args: { userId?: string }) {
const repos = await yuqueClient.getRepos(args.userId);
return {
content: [
{
type: 'text',
text: JSON.stringify(repos, null, 2),
},
],
};
}
async function handleGetDocs(args: { repoId: number; limit?: number; offset?: number }) {
const docs = await yuqueClient.getDocs(args.repoId, {
limit: args.limit,
offset: args.offset,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(docs, null, 2),
},
],
};
}
async function handleGetDoc(args: { docId: number; repoId: number }) {
const doc = await yuqueClient.getDoc(args.docId, args.repoId);
return {
content: [
{
type: 'text',
text: JSON.stringify(doc, null, 2),
},
],
};
}
async function handleCreateDoc(args: { repoId: number; title: string; content: string; format?: 'markdown' | 'lake' | 'html' }) {
const doc = await yuqueClient.createDoc(args.repoId, args.title, args.content, args.format);
return {
content: [
{
type: 'text',
text: JSON.stringify(doc, null, 2),
},
],
};
}
async function handleUpdateDoc(args: { docId: number; repoId: number; title?: string; content?: string; format?: 'markdown' | 'lake' | 'html' }) {
const doc = await yuqueClient.updateDoc(args.docId, args.repoId, args.title, args.content, args.format);
return {
content: [
{
type: 'text',
text: JSON.stringify(doc, null, 2),
},
],
};
}
async function handleDeleteDoc(args: { docId: number; repoId: number }) {
await yuqueClient.deleteDoc(args.docId, args.repoId);
return {
content: [
{
type: 'text',
text: '文档删除成功',
},
],
};
}
async function handleSearchDocs(args: { query: string; repoId?: number }) {
const docs = await yuqueClient.searchDocs(args.query, args.repoId);
return {
content: [
{
type: 'text',
text: JSON.stringify(docs, null, 2),
},
],
};
}
async function handleCreateRepo(args: { name: string; description?: string; isPublic?: boolean }) {
const repo = await yuqueClient.createRepo(args.name, args.description, args.isPublic || false);
return {
content: [
{
type: 'text',
text: JSON.stringify(repo, null, 2),
},
],
};
}
return server;
}
async function main() {
console.error('Starting Yuque MCP Server...');
try {
const server = await createServer();
const transport = new StdioServerTransport();
console.error('Connecting to transport...');
await server.connect(transport);
console.error('Yuque MCP Server running on stdio');
} catch (error) {
console.error('Failed to start server:', error);
process.exit(1);
}
}
// 启动服务器
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((error) => {
console.error('Server startup failed:', error);
process.exit(1);
});
}