#!/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, YuqueUser, YuqueRepo, YuqueDoc } from './yuque-client.js';
class YuqueMCPServer {
private server: Server;
private yuqueClient: YuqueClient;
constructor() {
const token = process.env.YUQUE_TOKEN;
if (!token) {
throw new Error('YUQUE_TOKEN environment variable is required');
}
this.server = new Server(
{
name: 'yuque-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
this.yuqueClient = new YuqueClient(token);
this.setupToolHandlers();
}
private setupToolHandlers() {
this.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',
},
},
required: ['docId'],
},
},
{
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',
},
title: {
type: 'string',
description: '文档标题',
},
content: {
type: 'string',
description: '文档内容',
},
format: {
type: 'string',
enum: ['markdown', 'lake', 'html'],
description: '文档格式',
},
},
required: ['docId'],
},
},
{
name: 'yuque_delete_doc',
description: '删除文档',
inputSchema: {
type: 'object',
properties: {
docId: {
type: 'number',
description: '文档ID',
},
},
required: ['docId'],
},
},
{
name: 'yuque_search_docs',
description: '搜索文档',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: '搜索关键词',
},
repoId: {
type: 'number',
description: '知识库ID,可选,不提供则全局搜索',
},
},
required: ['query'],
},
},
],
};
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'yuque_get_user':
return await this.handleGetUser();
case 'yuque_get_repos':
return await this.handleGetRepos(args as { userId?: string });
case 'yuque_get_docs':
return await this.handleGetDocs(args as { repoId: number; limit?: number; offset?: number });
case 'yuque_get_doc':
return await this.handleGetDoc(args as { docId: number });
case 'yuque_create_doc':
return await this.handleCreateDoc(args as { repoId: number; title: string; content: string; format?: 'markdown' | 'lake' | 'html' });
case 'yuque_update_doc':
return await this.handleUpdateDoc(args as { docId: number; title?: string; content?: string; format?: 'markdown' | 'lake' | 'html' });
case 'yuque_delete_doc':
return await this.handleDeleteDoc(args as { docId: number });
case 'yuque_search_docs':
return await this.handleSearchDocs(args as { query: string; repoId?: number });
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}`);
}
});
}
private async handleGetUser() {
const user = await this.yuqueClient.getUser();
return {
content: [
{
type: 'text',
text: JSON.stringify(user, null, 2),
},
],
};
}
private async handleGetRepos(args: { userId?: string }) {
const repos = await this.yuqueClient.getRepos(args.userId);
return {
content: [
{
type: 'text',
text: JSON.stringify(repos, null, 2),
},
],
};
}
private async handleGetDocs(args: { repoId: number; limit?: number; offset?: number }) {
const docs = await this.yuqueClient.getDocs(args.repoId, {
limit: args.limit,
offset: args.offset,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(docs, null, 2),
},
],
};
}
private async handleGetDoc(args: { docId: number }) {
const doc = await this.yuqueClient.getDoc(args.docId);
return {
content: [
{
type: 'text',
text: JSON.stringify(doc, null, 2),
},
],
};
}
private async handleCreateDoc(args: { repoId: number; title: string; content: string; format?: 'markdown' | 'lake' | 'html' }) {
const doc = await this.yuqueClient.createDoc(args.repoId, args.title, args.content, args.format);
return {
content: [
{
type: 'text',
text: JSON.stringify(doc, null, 2),
},
],
};
}
private async handleUpdateDoc(args: { docId: number; title?: string; content?: string; format?: 'markdown' | 'lake' | 'html' }) {
const doc = await this.yuqueClient.updateDoc(args.docId, args.title, args.content, args.format);
return {
content: [
{
type: 'text',
text: JSON.stringify(doc, null, 2),
},
],
};
}
private async handleDeleteDoc(args: { docId: number }) {
await this.yuqueClient.deleteDoc(args.docId);
return {
content: [
{
type: 'text',
text: '文档删除成功',
},
],
};
}
private async handleSearchDocs(args: { query: string; repoId?: number }) {
const docs = await this.yuqueClient.searchDocs(args.query, args.repoId);
return {
content: [
{
type: 'text',
text: JSON.stringify(docs, null, 2),
},
],
};
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Yuque MCP server running on stdio');
}
}
// 直接启动服务器
const server = new YuqueMCPServer();
server.run().catch(console.error);