feat: 更新 MCP 服务器配置和工具

This commit is contained in:
kuaifan 2025-10-27 06:30:45 +08:00
parent 3ced00de1f
commit 342e8725bd
2 changed files with 641 additions and 26 deletions

648
electron/lib/mcp.js vendored
View File

@ -4,24 +4,37 @@
* DooTask Electron 客户端集成了 Model Context Protocol (MCP) 服务
* 允许 AI 助手( Claude)直接与 DooTask 任务进行交互
*
* 提供的工具 7 :
* 提供的工具 15 :
*
* === 用户管理 ===
* - get_users_basic - 根据用户ID列表获取基础信息便于匹配负责人/协助人
* - search_user - 按关键字或项目筛选用户支持分页与更多过滤项
*
* === 任务管理 ===
* 1. list_tasks - 获取任务列表支持按状态/项目/主任务筛选搜索分页
* 2. get_task - 获取任务详情包含完整内容负责人协助人员标签等所有信息
* 3. complete_task - 快速标记任务完成
* 4. create_task - 创建新任务
* 5. update_task - 更新任务支持修改名称内容负责人时间状态等所有属性
* - list_tasks - 获取任务列表支持按状态/项目/主任务筛选搜索分页
* - get_task - 获取任务详情包含完整内容负责人协助人员标签等所有信息
* - complete_task - 快速标记任务完成
* - create_task - 创建新任务
* - update_task - 更新任务支持修改名称内容负责人时间状态等所有属性
* - create_sub_task - 为指定主任务创建子任务
* - get_task_files - 获取任务附件列表
* - delete_task - 删除或还原任务
*
* === 项目管理 ===
* 6. list_projects - 获取项目列表支持按归档状态筛选搜索
* 7. get_project - 获取项目详情包含列看板列成员等完整信息
* - list_projects - 获取项目列表支持按归档状态筛选搜索
* - get_project - 获取项目详情包含列看板列成员等完整信息
* - create_project - 创建新项目
* - update_project - 修改项目信息名称描述等
*
* === 消息通知 ===
* - send_message_to_user - 给指定用户发送私信
* - get_message_list - 获取对话消息或执行关键词搜索
*
* 配置方法:
* {
* "mcpServers": {
* "DooTask": {
* "url": "http://localhost:22224/sse"
* "url": "http://localhost:22224/mcp"
* }
* }
* }
@ -125,7 +138,161 @@ class DooTaskMCP {
// 设置 MCP 工具
setupTools() {
// 1. 获取任务列表
// 用户管理:获取用户基础信息
this.mcp.addTool({
name: 'get_users_basic',
description: '根据用户ID列表获取用户基础信息昵称、邮箱、头像等方便在分配任务前确认成员身份。',
parameters: z.object({
userids: z.array(z.number())
.min(1)
.max(50)
.describe('用户ID数组至少1个最多50个'),
}),
execute: async (params) => {
const ids = params.userids;
const requestData = {
userid: ids.length === 1 ? ids[0] : JSON.stringify(ids),
};
const result = await this.request('GET', 'users/basic', requestData);
if (result.error) {
throw new Error(result.error);
}
const rawList = Array.isArray(result.data)
? result.data
: (Array.isArray(result.data?.data) ? result.data.data : []);
const users = rawList.map(user => ({
userid: user.userid,
nickname: user.nickname || '',
email: user.email || '',
avatar: user.avatar || '',
identity: user.identity || '',
department: user.department || '',
}));
return {
content: [{
type: 'text',
text: JSON.stringify({
count: users.length,
users: users,
}, null, 2)
}]
};
}
});
// 用户管理:搜索用户
this.mcp.addTool({
name: 'search_user',
description: '按关键词搜索或筛选用户,支持按项目/对话过滤并返回分页结果。',
parameters: z.object({
keyword: z.string()
.min(1)
.describe('搜索关键词,支持昵称、邮箱、拼音等'),
project_id: z.number()
.optional()
.describe('仅返回指定项目的成员'),
dialog_id: z.number()
.optional()
.describe('仅返回指定对话的成员'),
include_disabled: z.boolean()
.optional()
.describe('是否同时包含已离职/禁用用户'),
include_bot: z.boolean()
.optional()
.describe('是否同时包含机器人账号'),
with_department: z.boolean()
.optional()
.describe('是否返回部门信息'),
page: z.number()
.optional()
.describe('页码默认1'),
pagesize: z.number()
.optional()
.describe('每页数量默认20最大100'),
}),
execute: async (params) => {
const page = params.page && params.page > 0 ? params.page : 1;
const pagesize = params.pagesize && params.pagesize > 0 ? Math.min(params.pagesize, 100) : 20;
const keys = {
key: params.keyword,
};
if (params.project_id !== undefined) {
keys.project_id = params.project_id;
}
if (params.dialog_id !== undefined) {
keys.dialog_id = params.dialog_id;
}
if (params.include_disabled) {
keys.disable = 2;
}
if (params.include_bot) {
keys.bot = 2;
}
const requestData = {
page,
pagesize,
keys,
};
if (params.with_department) {
requestData.with_department = 1;
}
const result = await this.request('GET', 'users/search', requestData);
if (result.error) {
throw new Error(result.error);
}
const data = result.data || {};
let users = [];
let total = 0;
let perPage = pagesize;
let currentPage = page;
if (Array.isArray(data.data)) {
users = data.data;
total = data.total ?? users.length;
perPage = data.per_page ?? perPage;
currentPage = data.current_page ?? currentPage;
} else if (Array.isArray(data)) {
users = data;
total = users.length;
}
const simplified = users.map(user => ({
userid: user.userid,
nickname: user.nickname || '',
email: user.email || '',
tags: user.tags || [],
department: user.department_info || user.department || '',
online: user.online ?? null,
identity: user.identity || '',
}));
return {
content: [{
type: 'text',
text: JSON.stringify({
total,
page: currentPage,
pagesize: perPage,
users: simplified,
}, null, 2)
}]
};
}
});
// 获取任务列表
this.mcp.addTool({
name: 'list_tasks',
description: '获取任务列表。可以按状态筛选(已完成/未完成)、搜索任务名称、按时间范围筛选等。',
@ -223,7 +390,7 @@ class DooTaskMCP {
}
});
// 2. 获取任务详情
// 获取任务详情
this.mcp.addTool({
name: 'get_task',
description: '获取指定任务的详细信息,包括任务描述、完整内容、负责人、协助人员、标签、时间等所有信息。',
@ -299,7 +466,7 @@ class DooTaskMCP {
}
});
// 3. 标记任务完成
// 标记任务完成
this.mcp.addTool({
name: 'complete_task',
description: '快速标记任务完成(自动使用当前时间)。如需指定完成时间或取消完成,请使用 update_task。注意:主任务必须在所有子任务完成后才能标记完成。',
@ -333,7 +500,7 @@ class DooTaskMCP {
}
});
// 4. 创建任务
// 创建任务
this.mcp.addTool({
name: 'create_task',
description: '创建新任务。可以指定任务名称、内容、负责人、时间等信息。',
@ -402,7 +569,7 @@ class DooTaskMCP {
}
});
// 5. 更新任务(完整版)
// 更新任务
this.mcp.addTool({
name: 'update_task',
description: '更新任务信息。可以修改任务名称、内容、负责人、时间、状态等所有属性。',
@ -486,7 +653,131 @@ class DooTaskMCP {
}
});
// 6. 获取项目列表
// 创建子任务
this.mcp.addTool({
name: 'create_sub_task',
description: '为指定主任务新增子任务,自动继承主任务所属项目与看板列配置。',
parameters: z.object({
task_id: z.number()
.describe('主任务ID'),
name: z.string()
.min(1)
.describe('子任务名称'),
}),
execute: async (params) => {
const result = await this.request('GET', 'project/task/addsub', {
task_id: params.task_id,
name: params.name,
});
if (result.error) {
throw new Error(result.error);
}
const subTask = result.data || {};
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
sub_task: {
id: subTask.id,
name: subTask.name,
project_id: subTask.project_id,
parent_id: subTask.parent_id,
column_id: subTask.column_id,
start_at: subTask.start_at,
end_at: subTask.end_at,
created_at: subTask.created_at,
}
}, null, 2)
}]
};
}
});
// 获取任务附件
this.mcp.addTool({
name: 'get_task_files',
description: '获取指定任务的附件列表,包含文件名称、大小、下载地址等信息。',
parameters: z.object({
task_id: z.number()
.describe('任务ID'),
}),
execute: async (params) => {
const result = await this.request('GET', 'project/task/files', {
task_id: params.task_id,
});
if (result.error) {
throw new Error(result.error);
}
const files = Array.isArray(result.data) ? result.data : [];
const normalized = files.map(file => ({
id: file.id,
name: file.name,
ext: file.ext,
size: file.size,
url: file.path,
thumb: file.thumb,
userid: file.userid,
download: file.download,
created_at: file.created_at,
}));
return {
content: [{
type: 'text',
text: JSON.stringify({
task_id: params.task_id,
files: normalized,
}, null, 2)
}]
};
}
});
// 删除或还原任务
this.mcp.addTool({
name: 'delete_task',
description: '删除或还原任务。默认执行删除,可通过 action=recovery 将任务从回收站恢复。',
parameters: z.object({
task_id: z.number()
.describe('任务ID'),
action: z.enum(['delete', 'recovery'])
.optional()
.describe('操作类型delete(默认) 删除recovery 还原'),
}),
execute: async (params) => {
const action = params.action || 'delete';
const result = await this.request('GET', 'project/task/remove', {
task_id: params.task_id,
type: action,
});
if (result.error) {
throw new Error(result.error);
}
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
action: action,
task_id: params.task_id,
data: result.data,
}, null, 2)
}]
};
}
});
// 获取项目列表
this.mcp.addTool({
name: 'list_projects',
description: '获取项目列表。可以按归档状态筛选、搜索项目名称等。',
@ -548,7 +839,7 @@ class DooTaskMCP {
}
});
// 7. 获取项目详情
// 获取项目详情
this.mcp.addTool({
name: 'get_project',
description: '获取指定项目的详细信息,包括项目的列(看板列)、成员等完整信息。',
@ -597,6 +888,328 @@ class DooTaskMCP {
};
}
});
// 创建项目
this.mcp.addTool({
name: 'create_project',
description: '创建新项目,可选设置项目描述、初始化列及流程状态。',
parameters: z.object({
name: z.string()
.min(2)
.describe('项目名称至少2个字符'),
desc: z.string()
.optional()
.describe('项目描述'),
columns: z.union([z.string(), z.array(z.string())])
.optional()
.describe('初始化列名称,字符串使用逗号分隔,也可直接传字符串数组'),
flow: z.enum(['open', 'close'])
.optional()
.describe('是否开启流程open/close默认close'),
personal: z.boolean()
.optional()
.describe('是否创建个人项目,仅支持创建一个个人项目'),
}),
execute: async (params) => {
const requestData = {
name: params.name,
};
if (params.desc !== undefined) {
requestData.desc = params.desc;
}
if (params.columns !== undefined) {
requestData.columns = Array.isArray(params.columns) ? params.columns.join(',') : params.columns;
}
if (params.flow !== undefined) {
requestData.flow = params.flow;
}
if (params.personal !== undefined) {
requestData.personal = params.personal ? 1 : 0;
}
const result = await this.request('GET', 'project/add', requestData);
if (result.error) {
throw new Error(result.error);
}
const project = result.data || {};
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
project: {
id: project.id,
name: project.name,
desc: project.desc || '',
columns: project.projectColumn || [],
created_at: project.created_at,
}
}, null, 2)
}]
};
}
});
// 更新项目
this.mcp.addTool({
name: 'update_project',
description: '修改项目信息(名称、描述、归档策略等)。若未传 name 将自动沿用项目当前名称。',
parameters: z.object({
project_id: z.number()
.describe('项目ID'),
name: z.string()
.optional()
.describe('项目名称'),
desc: z.string()
.optional()
.describe('项目描述'),
archive_method: z.string()
.optional()
.describe('归档方式'),
archive_days: z.number()
.optional()
.describe('自动归档天数'),
}),
execute: async (params) => {
const requestData = {
project_id: params.project_id,
};
if (params.name && params.name.trim().length > 0) {
requestData.name = params.name;
} else {
const projectResult = await this.request('GET', 'project/one', {
project_id: params.project_id,
});
if (projectResult.error) {
throw new Error(projectResult.error);
}
const currentName = projectResult.data?.name;
if (!currentName) {
throw new Error('无法获取项目名称,请手动提供 name 参数');
}
requestData.name = currentName;
}
if (params.desc !== undefined) {
requestData.desc = params.desc;
}
if (params.archive_method !== undefined) {
requestData.archive_method = params.archive_method;
}
if (params.archive_days !== undefined) {
requestData.archive_days = params.archive_days;
}
const result = await this.request('GET', 'project/update', requestData);
if (result.error) {
throw new Error(result.error);
}
const project = result.data || {};
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
project: {
id: project.id,
name: project.name,
desc: project.desc || '',
archived_at: project.archived_at || null,
archive_method: project.archive_method ?? requestData.archive_method ?? null,
archive_days: project.archive_days ?? requestData.archive_days ?? null,
updated_at: project.updated_at,
}
}, null, 2)
}]
};
}
});
// 发送消息给用户
this.mcp.addTool({
name: 'send_message_to_user',
description: '给指定用户发送私信,可选择 Markdown 或 HTML 格式,并支持静默发送。',
parameters: z.object({
user_id: z.number()
.describe('接收方用户ID'),
text: z.string()
.min(1)
.describe('消息内容'),
text_type: z.enum(['md', 'html'])
.optional()
.describe('消息类型默认md可选md/html'),
silence: z.boolean()
.optional()
.describe('是否静默发送(不触发提醒)'),
}),
execute: async (params) => {
const dialogResult = await this.request('GET', 'dialog/open/user', {
userid: params.user_id,
});
if (dialogResult.error) {
throw new Error(dialogResult.error);
}
const dialogData = dialogResult.data || {};
const dialogId = dialogData.id;
if (!dialogId) {
throw new Error('未能获取会话ID无法发送消息');
}
const payload = {
dialog_id: dialogId,
text: params.text,
};
if (params.text_type) {
payload.text_type = params.text_type;
} else {
payload.text_type = 'md';
}
if (params.silence !== undefined) {
payload.silence = params.silence ? 'yes' : 'no';
}
const sendResult = await this.request('POST', 'dialog/msg/sendtext', payload);
if (sendResult.error) {
throw new Error(sendResult.error);
}
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
dialog_id: dialogId,
data: sendResult.data,
}, null, 2)
}]
};
}
});
// 获取消息列表或搜索消息
this.mcp.addTool({
name: 'get_message_list',
description: '获取指定对话的消息列表,或按关键字搜索消息位置/内容。',
parameters: z.object({
dialog_id: z.number()
.optional()
.describe('对话ID获取消息列表时必填'),
keyword: z.string()
.optional()
.describe('搜索关键词,提供时执行消息搜索'),
msg_id: z.number()
.optional()
.describe('围绕某条消息加载相关内容'),
position_id: z.number()
.optional()
.describe('以position_id为中心加载消息'),
prev_id: z.number()
.optional()
.describe('获取此消息之前的历史'),
next_id: z.number()
.optional()
.describe('获取此消息之后的新消息'),
msg_type: z.enum(['tag', 'todo', 'link', 'text', 'image', 'file', 'record', 'meeting'])
.optional()
.describe('按消息类型筛选'),
take: z.number()
.optional()
.describe('获取条数列表模式最大100搜索模式受接口限制'),
}),
execute: async (params) => {
const keyword = params.keyword?.trim();
if (keyword) {
const searchPayload = {
key: keyword,
};
if (params.dialog_id) {
searchPayload.dialog_id = params.dialog_id;
}
if (params.take && params.take > 0) {
const takeValue = params.take;
searchPayload.take = params.dialog_id
? Math.min(takeValue, 200)
: Math.min(takeValue, 50);
}
const searchResult = await this.request('GET', 'dialog/msg/search', searchPayload);
if (searchResult.error) {
throw new Error(searchResult.error);
}
return {
content: [{
type: 'text',
text: JSON.stringify({
mode: params.dialog_id ? 'position_search' : 'global_search',
keyword: keyword,
dialog_id: params.dialog_id || null,
data: searchResult.data,
}, null, 2)
}]
};
}
if (!params.dialog_id) {
throw new Error('请提供 dialog_id 以获取消息列表,或提供 keyword 执行搜索');
}
const requestData = {
dialog_id: params.dialog_id,
};
if (params.msg_id !== undefined) requestData.msg_id = params.msg_id;
if (params.position_id !== undefined) requestData.position_id = params.position_id;
if (params.prev_id !== undefined) requestData.prev_id = params.prev_id;
if (params.next_id !== undefined) requestData.next_id = params.next_id;
if (params.msg_type !== undefined) requestData.msg_type = params.msg_type;
if (params.take !== undefined) {
const takeValue = params.take > 0 ? params.take : 1;
requestData.take = Math.min(takeValue, 100);
}
const result = await this.request('GET', 'dialog/msg/list', requestData);
if (result.error) {
throw new Error(result.error);
}
const data = result.data || {};
const messages = Array.isArray(data.list) ? data.list : [];
return {
content: [{
type: 'text',
text: JSON.stringify({
dialog_id: params.dialog_id,
count: messages.length,
time: data.time,
dialog: data.dialog,
top: data.top,
todo: data.todo,
messages: messages,
}, null, 2)
}]
};
}
});
}
// 启动 MCP 服务器
@ -632,7 +1245,8 @@ function startMCPServer(mainWindow, mcpPort) {
mcpServer = new DooTaskMCP(mainWindow);
mcpServer.start(mcpPort).then(() => {
loger.info(`DooTask MCP Server started on http://localhost:${mcpPort}/sse`);
loger.info(`MCP Server started on http://localhost:${mcpPort}/mcp`);
loger.info(`Legacy SSE endpoint also available at http://localhost:${mcpPort}/sse`);
}).catch((error) => {
loger.error('Failed to start MCP server:', error);
mcpServer = null;

View File

@ -4,7 +4,7 @@
<Alert type="success" show-icon>
{{ $L('MCP 服务器已启动成功!') }}
<span slot="desc">
{{ $L('服务地址') }}: <code>http://localhost:22224/sse</code>
{{ $L('服务地址') }}: <code>{{ mcpConfig.mcpServers.DooTask.url }}</code>
</span>
</Alert>
@ -12,7 +12,7 @@
<h3><span class="emoji-original">🔗</span> {{ $L('接入配置') }}</h3>
<p>{{ $L('以接入 Claude 为例,在配置文件中添加以下配置') }}:</p>
<div class="mcp-code-block">
<pre ref="mcpConfig">{{ mcpConfig }}</pre>
<pre ref="mcpConfig">{{ JSON.stringify(mcpConfig, null, 2) }}</pre>
<Button size="small" class="mcp-copy-btn" @click="copyMcpConfig">{{ $L('复制配置') }}</Button>
</div>
</div>
@ -27,6 +27,7 @@
<li>"{{ $L("在项目1中创建任务完成用户手册") }}"</li>
<li>"{{ $L("把任务789的截止时间改为下周五") }}"</li>
<li>"{{ $L("我有哪些项目") }}"</li>
<li>"{{ $L("查看项目5的详情包括所有列和成员") }}"</li>
</ul>
</div>
</div>
@ -142,13 +143,13 @@ export default {
},
data() {
return {
mcpConfig: `{
"mcpServers": {
"DooTask": {
"url": "http://localhost:22224/sse"
}
}
}`
mcpConfig: {
mcpServers: {
DooTask: {
url: "http://localhost:22224/mcp"
}
}
}
}
},
computed: {