660 lines
25 KiB
JavaScript
Vendored
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* DooTask MCP Server
*
* DooTask 的 Electron 客户端集成了 Model Context Protocol (MCP) 服务,
* 允许 AI 助手(如 Claude)直接与 DooTask 任务进行交互。
*
* 提供的工具(共 7 个):
*
* === 任务管理 ===
* 1. list_tasks - 获取任务列表,支持按状态/项目/主任务筛选、搜索、分页
* 2. get_task - 获取任务详情,包含完整内容、负责人、协助人员、标签等所有信息
* 3. complete_task - 快速标记任务完成
* 4. create_task - 创建新任务
* 5. update_task - 更新任务,支持修改名称、内容、负责人、时间、状态等所有属性
*
* === 项目管理 ===
* 6. list_projects - 获取项目列表,支持按归档状态筛选、搜索
* 7. get_project - 获取项目详情,包含列(看板列)、成员等完整信息
*
* 配置方法:
* {
* "mcpServers": {
* "DooTask": {
* "url": "http://localhost:22224/sse"
* }
* }
* }
*
* 使用示例:
* - "查看我未完成的任务"
* - "搜索包含'报告'的任务"
* - "显示任务123的详细信息"
* - "标记任务456为已完成"
* - "在项目1中创建任务完成用户手册负责人100协助人员101"
* - "把任务789的截止时间改为下周五并分配给用户200"
* - "取消任务234的完成状态"
* - "我有哪些项目?"
* - "查看项目5的详情包括所有列和成员"
*/
const { FastMCP } = require('fastmcp');
const { z } = require('zod');
const loger = require("electron-log");
let mcpServer = null;
class DooTaskMCP {
constructor(mainWindow) {
this.mainWindow = mainWindow;
this.mcp = new FastMCP({
name: 'DooTask MCP Server',
version: '1.0.0',
description: 'DooTask 任务管理 MCP 接口',
});
this.setupTools();
}
/**
* 调用接口
*/
async request(method, path, data = {}) {
try {
// 通过主窗口执行前端代码来调用API
if (!this.mainWindow || !this.mainWindow.webContents) {
throw new Error('Main window not available, please open DooTask application first');
}
// 检查 webContents 是否 ready
if (this.mainWindow.webContents.isLoading()) {
loger.warn(`[MCP] WebContents is loading, waiting...`);
await new Promise(resolve => {
this.mainWindow.webContents.once('did-finish-load', resolve);
});
}
// 通过前端已有的API调用机制添加超时处理
const executePromise = this.mainWindow.webContents.executeJavaScript(`
(async () => {
try {
// 检查API是否可用
if (typeof $A === 'undefined') {
return { error: 'API not available - $A is undefined' };
}
if (typeof $A.apiCall !== 'function') {
return { error: 'API not available - $A.apiCall is not a function' };
}
// 调用 API
const result = await $A.apiCall({
url: '${path}',
data: ${JSON.stringify(data)},
method: '${method}'
});
try {
return { data: JSON.parse(JSON.stringify(result.data)) };
} catch (serError) {
return { error: 'Result contains non-serializable data' };
}
} catch (error) {
return { error: error.msg || error.message || String(error) || 'API request failed' };
}
})()
`);
// 添加超时处理30秒
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Request timeout after 30 seconds')), 30000);
});
// 返回结果
const result = await Promise.race([executePromise, timeoutPromise]);
if (result && result.error) {
throw new Error(result.error);
}
return result;
} catch (error) {
throw error;
}
}
// 设置 MCP 工具
setupTools() {
// 1. 获取任务列表
this.mcp.addTool({
name: 'list_tasks',
description: '获取任务列表。可以按状态筛选(已完成/未完成)、搜索任务名称、按时间范围筛选等。',
parameters: z.object({
status: z.enum(['all', 'completed', 'uncompleted'])
.optional()
.describe('任务状态: all(所有), completed(已完成), uncompleted(未完成)'),
search: z.string()
.optional()
.describe('搜索关键词(可搜索任务ID、名称、描述)'),
time: z.string()
.optional()
.describe('时间范围: today(今天), week(本周), month(本月), year(今年), 自定义时间范围,如2025-12-12,2025-12-30'),
project_id: z.number()
.optional()
.describe('项目ID,只获取指定项目的任务'),
parent_id: z.number()
.optional()
.describe('主任务ID。大于0:获取该主任务的子任务; -1:仅获取主任务; 不传:所有任务'),
page: z.number()
.optional()
.describe('页码,默认1'),
pagesize: z.number()
.optional()
.describe('每页数量,默认20,最大100'),
}),
execute: async (params) => {
const requestData = {
page: params.page || 1,
pagesize: params.pagesize || 20,
};
// 构建 keys 对象用于筛选
const keys = {};
if (params.search) {
keys.name = params.search;
}
if (params.status && params.status !== 'all') {
keys.status = params.status;
}
if (Object.keys(keys).length > 0) {
requestData.keys = keys;
}
// 其他筛选参数
if (params.time !== undefined) {
requestData.time = params.time;
}
if (params.project_id !== undefined) {
requestData.project_id = params.project_id;
}
if (params.parent_id !== undefined) {
requestData.parent_id = params.parent_id;
}
const result = await this.request('GET', 'project/task/lists', requestData);
if (result.error) {
throw new Error(result.error);
}
const tasks = result.data.data.map(task => ({
id: task.id,
name: task.name,
status: task.complete_at ? '已完成' : '未完成',
complete_at: task.complete_at || '未完成',
end_at: task.end_at || '无截止时间',
project_id: task.project_id,
project_name: task.project_name || '',
column_name: task.column_name || '',
parent_id: task.parent_id,
owners: task.taskUser?.filter(u => u.owner === 1).map(u => ({
userid: u.userid,
username: u.username || u.nickname || `用户${u.userid}`
})) || [],
sub_num: task.sub_num || 0,
sub_complete: task.sub_complete || 0,
percent: task.percent || 0,
created_at: task.created_at,
}));
return {
content: [{
type: 'text',
text: JSON.stringify({
total: result.data.total,
page: result.data.current_page,
pagesize: result.data.per_page,
tasks: tasks,
}, null, 2)
}]
};
}
});
// 2. 获取任务详情
this.mcp.addTool({
name: 'get_task',
description: '获取指定任务的详细信息,包括任务描述、完整内容、负责人、协助人员、标签、时间等所有信息。',
parameters: z.object({
task_id: z.number()
.describe('任务ID'),
}),
execute: async (params) => {
const taskResult = await this.request('GET', 'project/task/one', {
task_id: params.task_id,
});
if (taskResult.error) {
throw new Error(taskResult.error);
}
const task = taskResult.data;
// 获取任务完整内容
let fullContent = task.desc || '无描述';
try {
const contentResult = await this.request('GET', 'project/task/content', {
task_id: params.task_id,
});
if (contentResult && contentResult.data) {
if (typeof contentResult.data === 'object' && contentResult.data.content) {
fullContent = contentResult.data.content;
} else if (typeof contentResult.data === 'string') {
fullContent = contentResult.data;
}
}
} catch (error) {
loger.warn(`Failed to get task content: ${error.message}`);
}
const taskDetail = {
id: task.id,
name: task.name,
desc: task.desc || '无描述',
content: fullContent,
status: task.complete_at ? '已完成' : '未完成',
complete_at: task.complete_at || '未完成',
project_id: task.project_id,
project_name: task.project_name,
column_id: task.column_id,
column_name: task.column_name,
parent_id: task.parent_id,
start_at: task.start_at || '无开始时间',
end_at: task.end_at || '无截止时间',
flow_item_id: task.flow_item_id,
flow_item_name: task.flow_item_name,
visibility: task.visibility === 1 ? '公开' : '指定人员',
owners: task.taskUser?.filter(u => u.owner === 1).map(u => ({
userid: u.userid,
username: u.username || u.nickname || `用户${u.userid}`
})) || [],
assistants: task.taskUser?.filter(u => u.owner === 0).map(u => ({
userid: u.userid,
username: u.username || u.nickname || `用户${u.userid}`
})) || [],
tags: task.taskTag?.map(t => t.name) || [],
created_at: task.created_at,
updated_at: task.updated_at,
};
return {
content: [{
type: 'text',
text: JSON.stringify(taskDetail, null, 2)
}]
};
}
});
// 3. 标记任务完成
this.mcp.addTool({
name: 'complete_task',
description: '快速标记任务完成(自动使用当前时间)。如需指定完成时间或取消完成,请使用 update_task。注意:主任务必须在所有子任务完成后才能标记完成。',
parameters: z.object({
task_id: z.number()
.describe('要标记完成的任务ID'),
}),
execute: async (params) => {
const now = new Date().toISOString().slice(0, 19).replace('T', ' ');
const result = await this.request('POST', 'project/task/update', {
task_id: params.task_id,
complete_at: now,
});
if (result.error) {
throw new Error(result.error);
}
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
message: '任务已标记为完成',
task_id: params.task_id,
complete_at: result.data.complete_at,
}, null, 2)
}]
};
}
});
// 4. 创建任务
this.mcp.addTool({
name: 'create_task',
description: '创建新任务。可以指定任务名称、内容、负责人、时间等信息。',
parameters: z.object({
project_id: z.number()
.describe('项目ID'),
name: z.string()
.describe('任务名称'),
content: z.string()
.optional()
.describe('任务内容描述(支持富文本)'),
owner: z.array(z.number())
.optional()
.describe('负责人用户ID数组'),
assist: z.array(z.number())
.optional()
.describe('协助人员用户ID数组'),
column_id: z.number()
.optional()
.describe('列ID(看板列)'),
start_at: z.string()
.optional()
.describe('开始时间,格式: YYYY-MM-DD HH:mm:ss'),
end_at: z.string()
.optional()
.describe('结束时间,格式: YYYY-MM-DD HH:mm:ss'),
}),
execute: async (params) => {
const requestData = {
project_id: params.project_id,
name: params.name,
};
// 添加可选参数
if (params.content) requestData.content = params.content;
if (params.owner) requestData.owner = params.owner;
if (params.assist) requestData.assist = params.assist;
if (params.column_id) requestData.column_id = params.column_id;
if (params.start_at) requestData.start_at = params.start_at;
if (params.end_at) requestData.end_at = params.end_at;
const result = await this.request('POST', 'project/task/add', requestData);
if (result.error) {
throw new Error(result.error);
}
const task = result.data;
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
message: '任务创建成功',
task: {
id: task.id,
name: task.name,
project_id: task.project_id,
column_id: task.column_id,
created_at: task.created_at,
}
}, null, 2)
}]
};
}
});
// 5. 更新任务(完整版)
this.mcp.addTool({
name: 'update_task',
description: '更新任务信息。可以修改任务名称、内容、负责人、时间、状态等所有属性。',
parameters: z.object({
task_id: z.number()
.describe('任务ID'),
name: z.string()
.optional()
.describe('任务名称'),
content: z.string()
.optional()
.describe('任务内容描述'),
owner: z.array(z.number())
.optional()
.describe('负责人用户ID数组'),
assist: z.array(z.number())
.optional()
.describe('协助人员用户ID数组'),
column_id: z.number()
.optional()
.describe('移动到指定列ID'),
start_at: z.string()
.optional()
.describe('开始时间,格式: YYYY-MM-DD HH:mm:ss'),
end_at: z.string()
.optional()
.describe('结束时间,格式: YYYY-MM-DD HH:mm:ss'),
complete_at: z.union([z.string(), z.boolean()])
.optional()
.describe('完成时间。传时间字符串标记完成传false标记未完成'),
}),
execute: async (params) => {
const requestData = {
task_id: params.task_id,
};
// 添加要更新的字段
if (params.name !== undefined) requestData.name = params.name;
if (params.content !== undefined) requestData.content = params.content;
if (params.owner !== undefined) requestData.owner = params.owner;
if (params.assist !== undefined) requestData.assist = params.assist;
if (params.column_id !== undefined) requestData.column_id = params.column_id;
if (params.start_at !== undefined) requestData.start_at = params.start_at;
if (params.end_at !== undefined) requestData.end_at = params.end_at;
if (params.complete_at !== undefined) requestData.complete_at = params.complete_at;
const result = await this.request('POST', 'project/task/update', requestData);
if (result.error) {
throw new Error(result.error);
}
const task = result.data;
// 构建更新摘要
const updates = [];
if (params.name !== undefined) updates.push('名称');
if (params.content !== undefined) updates.push('内容');
if (params.owner !== undefined) updates.push('负责人');
if (params.assist !== undefined) updates.push('协助人员');
if (params.column_id !== undefined) updates.push('列');
if (params.start_at !== undefined || params.end_at !== undefined) updates.push('时间');
if (params.complete_at !== undefined) updates.push('完成状态');
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
message: `任务已更新: ${updates.join('、')}`,
task: {
id: task.id,
name: task.name,
status: task.complete_at ? '已完成' : '未完成',
complete_at: task.complete_at || '未完成',
updated_at: task.updated_at,
}
}, null, 2)
}]
};
}
});
// 6. 获取项目列表
this.mcp.addTool({
name: 'list_projects',
description: '获取项目列表。可以按归档状态筛选、搜索项目名称等。',
parameters: z.object({
archived: z.enum(['no', 'yes', 'all'])
.optional()
.describe('归档状态: no(未归档), yes(已归档), all(全部)默认no'),
search: z.string()
.optional()
.describe('搜索关键词(可搜索项目名称)'),
page: z.number()
.optional()
.describe('页码默认1'),
pagesize: z.number()
.optional()
.describe('每页数量默认20'),
}),
execute: async (params) => {
const requestData = {
archived: params.archived || 'no',
page: params.page || 1,
pagesize: params.pagesize || 20,
};
// 添加搜索参数
if (params.search) {
requestData.keys = {
name: params.search
};
}
const result = await this.request('GET', 'project/lists', requestData);
if (result.error) {
throw new Error(result.error);
}
const projects = result.data.data.map(project => ({
id: project.id,
name: project.name,
desc: project.desc || '无描述',
archived_at: project.archived_at || '未归档',
owner_userid: project.owner_userid || 0,
created_at: project.created_at,
}));
return {
content: [{
type: 'text',
text: JSON.stringify({
total: result.data.total,
page: result.data.current_page,
pagesize: result.data.per_page,
projects: projects,
}, null, 2)
}]
};
}
});
// 7. 获取项目详情
this.mcp.addTool({
name: 'get_project',
description: '获取指定项目的详细信息,包括项目的列(看板列)、成员等完整信息。',
parameters: z.object({
project_id: z.number()
.describe('项目ID'),
}),
execute: async (params) => {
const result = await this.request('GET', 'project/one', {
project_id: params.project_id,
});
if (result.error) {
throw new Error(result.error);
}
const project = result.data;
const projectDetail = {
id: project.id,
name: project.name,
desc: project.desc || '无描述',
archived_at: project.archived_at || '未归档',
owner_userid: project.owner_userid,
owner_username: project.owner_username,
columns: project.projectColumn?.map(col => ({
id: col.id,
name: col.name,
sort: col.sort,
})) || [],
members: project.projectUser?.map(user => ({
userid: user.userid,
username: user.username,
owner: user.owner === 1 ? '管理员' : '成员',
})) || [],
created_at: project.created_at,
updated_at: project.updated_at,
};
return {
content: [{
type: 'text',
text: JSON.stringify(projectDetail, null, 2)
}]
};
}
});
}
// 启动 MCP 服务器
async start(port = 22224) {
try {
await this.mcp.start({
transportType: 'httpStream',
httpStream: {
port: port
}
});
} catch (error) {
throw error;
}
}
// 停止服务器
async stop() {
if (this.mcp && typeof this.mcp.stop === 'function') {
await this.mcp.stop();
}
}
}
/**
* 启动 MCP 服务器
*/
function startMCPServer(mainWindow, mcpPort) {
if (mcpServer) {
loger.info('MCP server already running');
return;
}
mcpServer = new DooTaskMCP(mainWindow);
mcpServer.start(mcpPort).then(() => {
loger.info(`DooTask MCP Server started on http://localhost:${mcpPort}/sse`);
}).catch((error) => {
loger.error('Failed to start MCP server:', error);
mcpServer = null;
});
}
/**
* 停止 MCP 服务器
*/
function stopMCPServer() {
if (!mcpServer) {
loger.info('MCP server is not running');
return;
}
mcpServer.stop().then(() => {
loger.info('MCP server stopped');
}).catch((error) => {
loger.error('Failed to stop MCP server:', error);
}).finally(() => {
mcpServer = null;
});
}
module.exports = {
DooTaskMCP,
startMCPServer,
stopMCPServer,
};