基于MCP协议的Coding Agent待办看板与像素桌宠实战
1. 这个看板到底解决了什么问题Coding Agent 现在几乎成了开发者的标配工具不管是命令行的还是 IDE 里嵌的用起来确实爽——你说一句需求它帮你把代码写了、测试跑了、甚至 PR 都提了。但用久了你会发现一个很别扭的事Agent 的待办事项是“一次性”的。你让它做三件事它做完两件第三件因为上下文超了或者会话断了就彻底消失了。下次你再开一个会话它完全不记得之前还有什么没干完。我自己就踩过这个坑。有一次让 Agent 帮我重构一个模块拆了七个步骤跑到第五步的时候 token 用完了会话中断。我重新开了一个会话它一脸茫然地问我“你想做什么”。那一刻我就想能不能让 Agent 的待办事项“落地”到一个它自己能读写的地方而不是飘在对话上下文里这个项目就是干这个的一个开源的桌面看板让 Coding Agent 通过 MCP 协议自己管理待办事项。Agent 可以往看板里加任务、更新状态、标记完成而我在桌面上能实时看到它在干什么、还剩什么没干。顺带还做了一个像素桌宠把 Agent 的工作状态可视化——它干活的时候桌宠在敲键盘它卡住的时候桌宠在挠头它闲下来的时候桌宠在打瞌睡。适合谁看如果你日常用 Coding Agent 写代码或者你在做 Agent 相关的工具链开发再或者你对 MCP 协议的实际落地感兴趣这篇内容应该能给你一些可以直接抄的作业。哪怕你只是想给自己的桌面加个像素小宠物后半部分也有完整的实现思路。2. 整体设计思路为什么是 MCP 桌面看板 桌宠2.1 核心矛盾Agent 的“记忆”和人的“可见性”Coding Agent 的工作模式本质上是一个无状态的请求-响应循环。每次你给它一个指令它在一个上下文窗口里规划、执行、返回结果。上下文窗口就是它的全部“记忆”——一旦超出前面的内容就被截断或压缩待办事项自然就丢了。有人会说那让 Agent 把待办写到一个文件里不就行了确实可以但有几个问题第一Agent 写文件需要额外的工具调用每次读写都消耗 token第二文件是纯文本人看起来不直观你得打开编辑器去找第三多个 Agent 或者多个会话同时操作同一个文件容易冲突。所以我的思路是把待办事项做成一个独立的服务通过标准协议暴露给 Agent同时提供一个人类可读的桌面界面。Agent 通过协议调用增删改查人通过看板看状态两边互不干扰但又实时同步。2.2 为什么选 MCP 而不是自定义 APIMCPModel Context Protocol是 Anthropic 推出的一个开放协议专门用来让 AI 模型和外部工具、数据源之间建立标准化的连接。你可能会问我自己写个 HTTP API 让 Agent 调用不行吗行但有几个现实问题。第一工具接入成本。现在主流的 Coding Agent 工具——不管是命令行的还是 IDE 集成的——都在快速支持 MCP。你写一个 MCP Server理论上所有支持 MCP 的客户端都能直接用不需要为每个客户端单独适配。第二协议本身处理了工具发现和参数校验。MCP 有标准的tools/list和tools/call机制Agent 能自动发现你提供了哪些工具、每个工具需要什么参数不需要你在 prompt 里手动描述。第三生态趋势。从热搜词也能看出来MCP 相关的搜索量在暴涨各种工具都在往 MCP 上靠现在投入学习成本后面能复用很久。当然MCP 也不是没有缺点。它目前还在快速迭代中不同客户端的实现细节有差异调试起来有时候比较麻烦。但整体来看对于“让 Agent 管理待办”这个场景MCP 是最合适的切入点。2.3 桌面看板的形态选择看板这个东西Web 版最省事但我不想每次看待办还要开个浏览器标签页。桌面应用更合适但 Electron 太重Tauri 又要学 Rust。最后我选了一个折中方案用本地 Web 服务 系统托盘窗口。核心逻辑跑在一个本地 HTTP 服务里界面用最轻量的前端框架渲染然后套一个系统托盘窗口壳。这样开发快、体积小、跨平台也还行。看板的交互设计遵循一个原则Agent 优先人类只读。也就是说看板的主要用户是 Agent它通过 MCP 工具来操作任务人类打开看板主要是“看”偶尔手动调整一下优先级或者补充说明。这个原则决定了数据模型和 API 设计——所有操作都要考虑 Agent 调用的便利性比如批量操作、幂等性、错误信息的清晰度。2.4 像素桌宠的定位状态可视化桌宠不是核心功能但它解决了一个很实际的问题Agent 在后台干活的时候你不知道它是在跑还是在卡住。命令行工具还好能看到输出但如果是 IDE 里的 Agent或者你切到别的窗口去了就完全不知道进度。像素桌宠通过监听看板的状态变化来驱动动画。具体来说看板里每个任务有状态字段待办、进行中、已完成、阻塞桌宠根据“进行中”任务的数量和最后更新时间来判断 Agent 的活跃度。活跃的时候播放敲键盘动画超过一定时间没更新就切换到挠头表示可能卡住了所有任务完成就进入打瞌睡状态。这个设计的好处是零侵入——Agent 不需要知道桌宠的存在它只管操作看板桌宠自己从看板状态推导出该播什么动画。这样即使你关掉桌宠Agent 的工作也完全不受影响。3. 核心细节解析数据模型、MCP 工具与状态同步3.1 任务数据模型的设计取舍任务模型看起来简单但实际设计的时候有几个关键决策。先看最终的结构{ id: tsk_a1b2c3d4, title: 重构用户认证模块, description: 将 JWT 校验逻辑抽取到独立中间件, status: in_progress, priority: 2, created_at: 1718000000, updated_at: 1718003600, completed_at: null, tags: [refactor, auth], parent_id: null, agent_session: sess_x9y8z7 }几个设计点值得展开说。ID 用带前缀的随机字符串而不是自增整数原因是 Agent 可能会在多个会话里并发创建任务自增 ID 在分布式场景下容易冲突而且前缀能让日志里一眼看出这是什么类型的对象。status 用枚举而不是布尔值因为“完成”和“未完成”二分法不够用实际场景里“进行中”和“阻塞”是两个很有价值的状态——Agent 可以标记某个任务为阻塞并附上原因人一看就知道需要介入。parent_id 支持子任务这个是为了应对 Agent 拆解大任务的情况比如“重构认证模块”下面可以挂“抽取 JWT 校验”“更新测试用例”“修改文档”三个子任务。还有一个隐藏字段agent_session记录这个任务是哪个会话创建的。这个字段在看板上不直接显示但在排查问题的时候很有用——比如你发现某个会话创建了大量重复任务可以通过这个字段过滤出来批量清理。3.2 MCP 工具集的设计MCP Server 对外暴露的工具不需要多关键是好用。我最终定了六个工具工具名用途关键参数task_create创建新任务title, description, priority, tags, parent_idtask_update更新任务字段id, status, priority, descriptiontask_complete标记完成id, summarytask_list查询任务列表status_filter, tag_filter, limittask_delete删除任务idtask_batch_create批量创建tasks[]这里重点说三个设计决策。第一为什么把task_complete单独拿出来而不是用task_update改状态因为完成一个任务通常需要附带一个总结summary而更新状态不需要。分开之后Agent 在完成时会被提示填写总结这样看板上就能看到每个完成任务的简要说明而不是只有一个冷冰冰的“已完成”。第二task_list支持过滤和限制因为 Agent 的上下文窗口有限如果任务列表很长全量返回会浪费 token。默认 limit 是 20按更新时间倒序。第三task_batch_create的存在是因为 Agent 经常需要一次性拆解出多个子任务逐个调用task_create会产生大量往返批量接口能显著减少 token 消耗。每个工具的 description 字段我都写得很详细因为 Agent 是根据这个描述来决定什么时候调用、怎么传参的。比如task_create的描述里明确写了“当用户要求你做多步骤任务时先调用此工具创建待办事项每完成一步更新状态”。这种引导性的描述能显著提高 Agent 的使用意愿。3.3 状态同步机制从 MCP 调用到看板刷新整个系统的数据流是这样的Agent 通过 MCP 协议调用工具 → MCP Server 处理请求并更新数据库 → 数据库变更触发事件 → 看板前端通过 WebSocket 收到更新 → 界面刷新 → 桌宠读取最新状态并切换动画。这里的关键是事件推送的实时性。我试过轮询延迟太高Agent 创建任务后要等好几秒看板才更新体验很差。后来改成 WebSocket 推送延迟降到毫秒级。具体实现上MCP Server 和看板服务跑在同一个进程里共享一个内存中的事件总线。当 MCP 工具修改了任务数据就往事件总线发一个task_changed事件WebSocket 服务监听到之后推送给所有连接的客户端。有一个细节需要注意WebSocket 推送的消息要足够轻量。我一开始把整个任务对象推过去后来发现任务多了之后带宽消耗不小。改成只推{action: update, task_id: xxx, changed_fields: [status]}前端收到之后自己决定要不要重新拉取完整数据。这样大部分情况下只需要更新一个字段不需要全量刷新。3.4 像素桌宠的动画状态机桌宠的动画逻辑是一个简单的状态机输入是看板的聚合状态输出是动画名称。聚合状态的计算方式如下统计status in_progress的任务数量记为active_count找到最近一次任务更新的时间戳记为last_update当前时间减去last_update得到idle_seconds状态转移规则条件动画状态说明active_count 0 且 idle_seconds 30workingAgent 正在活跃工作active_count 0 且 idle_seconds 30thinking可能卡住了或在思考active_count 0 且 有已完成任务idle空闲但今天干过活active_count 0 且 无任务sleeping完全空闲这个状态机的好处是不需要 Agent 主动上报状态完全从看板数据推导。Agent 只要正常使用 MCP 工具桌宠就能自动反映它的工作节奏。实测下来30 秒的阈值比较合理——大部分 Agent 操作间隔在几秒到十几秒超过 30 秒没动静通常意味着它在处理复杂逻辑或者真的卡住了。4. 实操过程从零搭建的完整步骤4.1 环境准备与依赖安装整个项目分三块MCP ServerNode.js、看板前端Vite 原生 JS、桌宠Electron 托盘窗口。先装基础环境# 确认 Node.js 版本建议 18 以上 node -v # 初始化项目 mkdir agent-kanban cd agent-kanban npm init -y # 安装核心依赖 npm install modelcontextprotocol/sdk better-sqlite3 ws express npm install -D vite electron electron-builder这里重点说两个依赖的选择理由。better-sqlite3 而不是 sqlite3因为前者是同步 API在 MCP Server 这种单线程场景下写起来更简单不需要处理回调地狱而且性能更好。ws 而不是 socket.io因为看板前端只需要最基础的 WebSocket 功能socket.io 的额外特性用不上反而增加体积。数据库初始化脚本// db.js const Database require(better-sqlite3); const db new Database(./kanban.db); db.exec( CREATE TABLE IF NOT EXISTS tasks ( id TEXT PRIMARY KEY, title TEXT NOT NULL, description TEXT DEFAULT , status TEXT DEFAULT todo, priority INTEGER DEFAULT 3, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, completed_at INTEGER, tags TEXT DEFAULT [], parent_id TEXT, agent_session TEXT ); CREATE INDEX IF NOT EXISTS idx_status ON tasks(status); CREATE INDEX IF NOT EXISTS idx_updated ON tasks(updated_at); ); module.exports db;索引的选择也有讲究。idx_status是为了加速按状态过滤的查询idx_updated是为了加速按更新时间排序。这两个是看板和桌宠最频繁的查询模式加上索引之后查询时间从几十毫秒降到亚毫秒级。4.2 MCP Server 的核心实现MCP Server 的入口文件需要注册工具并启动传输层。我用的是 stdio 传输因为大部分 Coding Agent 客户端都支持这种方式配置最简单。// mcp-server.js const { Server } require(modelcontextprotocol/sdk/server/index.js); const { StdioServerTransport } require(modelcontextprotocol/sdk/server/stdio.js); const db require(./db); const { emit } require(./events); const server new Server( { name: agent-kanban, version: 1.0.0 }, { capabilities: { tools: {} } } ); server.setRequestHandler(tools/list, async () ({ tools: [ { name: task_create, description: 创建一个新的待办任务。当用户要求执行多步骤任务时先调用此工具创建待办事项。, inputSchema: { type: object, properties: { title: { type: string, description: 任务标题简洁明了 }, description: { type: string, description: 任务详细说明 }, priority: { type: number, description: 优先级 1-51 最高, default: 3 }, tags: { type: array, items: { type: string } }, parent_id: { type: string, description: 父任务 ID用于创建子任务 } }, required: [title] } }, // ... 其他工具定义 ] })); server.setRequestHandler(tools/call, async (request) { const { name, arguments: args } request.params; if (name task_create) { const id tsk_ Math.random().toString(36).slice(2, 10); const now Math.floor(Date.now() / 1000); db.prepare( INSERT INTO tasks (id, title, description, status, priority, created_at, updated_at, tags, parent_id) VALUES (?, ?, ?, todo, ?, ?, ?, ?, ?) ).run(id, args.title, args.description || , args.priority || 3, now, now, JSON.stringify(args.tags || []), args.parent_id || null); emit(task_changed, { action: create, task_id: id }); return { content: [{ type: text, text: 任务已创建ID: ${id} }] }; } // ... 其他工具的处理逻辑 }); const transport new StdioServerTransport(); server.connect(transport);这里有一个容易踩的坑MCP 工具的返回值格式。必须返回{ content: [{ type: text, text: ... }] }这样的结构不能直接返回字符串或对象。我一开始没注意Agent 调用之后一直报解析错误排查了半天才发现是返回格式不对。4.3 看板前端的实时刷新看板前端用 Vite 做开发服务器生产环境打包成静态文件由 Express 托管。核心是 WebSocket 连接和 DOM 更新。// kanban.js const ws new WebSocket(ws://localhost:3456); ws.onmessage (event) { const msg JSON.parse(event.data); if (msg.type task_changed) { // 根据变更类型决定更新策略 if (msg.action create) { fetchTask(msg.task_id).then(renderTask); } else if (msg.action update) { updateTaskInDOM(msg.task_id, msg.changed_fields); } else if (msg.action delete) { removeTaskFromDOM(msg.task_id); } } }; // 初始加载 fetch(/api/tasks?limit50) .then(r r.json()) .then(tasks tasks.forEach(renderTask));看板的布局分三列待办、进行中、已完成。每张任务卡片显示标题、优先级标签、标签和最后更新时间。进行中的任务卡片会有一个脉冲动画边框已完成的任务卡片会变灰并显示完成时间。有一个交互细节我特意做了处理任务卡片的排序。待办列按优先级升序优先级高的在上面进行中列按更新时间降序最近更新的在上面已完成列按完成时间降序。这样你一眼就能看到最该关注的任务。4.4 像素桌宠的实现桌宠用 Electron 的透明窗口实现窗口大小 128x128无边框、置顶、鼠标穿透点击穿透到下层窗口。// pet-window.js const { BrowserWindow } require(electron); const petWindow new BrowserWindow({ width: 128, height: 128, transparent: true, frame: false, alwaysOnTop: true, resizable: false, hasShadow: false, webPreferences: { preload: path.join(__dirname, pet-preload.js) } }); petWindow.setIgnoreMouseEvents(true, { forward: true }); petWindow.loadFile(pet.html);动画用 CSS sprite 实现每个状态对应一张精灵图通过background-position切换帧。精灵图我用 Aseprite 画的每个状态 4 帧帧率 8fps。状态切换通过 WebSocket 接收看板推送然后调用setState函数切换 CSS 类。// pet.html 中的状态切换逻辑 function setState(newState) { if (currentState newState) return; document.body.className state- newState; currentState newState; } // 接收看板状态 ws.onmessage (event) { const msg JSON.parse(event.data); if (msg.type pet_state) { setState(msg.state); } };桌宠的状态计算放在看板服务端每 5 秒计算一次然后推送给桌宠窗口。这样桌宠本身不需要维护复杂的逻辑只负责渲染。5. 常见问题与排查技巧实录5.1 Agent 不调用 MCP 工具怎么办这是最常见的问题。Agent 明明支持 MCP但就是不用你提供的工具。原因通常有三个工具描述不够清晰、系统提示没有引导、工具名称不够直观。排查步骤先看 Agent 的日志确认它有没有收到tools/list的响应。如果收到了但没调用检查工具描述里有没有明确说明“什么时候该用”。我实测下来在描述里加上“当用户要求执行多步骤任务时先调用此工具”这样的引导语调用率能提升很多。另外工具名称用task_create比create_task更好因为 Agent 在检索工具时更倾向于匹配动词在前的命名。还有一个技巧在 MCP Server 的初始化响应里可以附带一个instructions字段给 Agent 一些全局性的使用建议。比如“你有一个待办看板建议在开始多步骤任务前先创建待办事项每完成一步更新状态”。这个字段会被注入到 Agent 的上下文中效果比单个工具描述更好。5.2 任务重复创建的问题Agent 有时候会在一个会话里反复创建同一个任务导致看板上出现大量重复项。这个问题的根源是Agent 没有“先查再建”的习惯。解决方案有两个层面。第一个层面是在工具描述里引导task_create的描述里加上“创建前先用task_list检查是否已存在相同任务”。第二个层面是在服务端做去重创建任务时检查最近 5 分钟内是否有标题相似度超过 80% 的任务如果有就返回已存在的任务 ID 而不是新建。相似度计算我用的是简单的编辑距离没有上复杂的 NLP 模型因为任务标题通常很短编辑距离足够了。实测下来这个去重逻辑能减少大约 70% 的重复任务。5.3 WebSocket 断连重连看板前端和桌宠都依赖 WebSocket 连接网络波动或者服务重启会导致断连。如果不处理界面就会卡在旧状态。解决方案是实现自动重连加状态同步。let reconnectDelay 1000; function connect() { const ws new WebSocket(ws://localhost:3456); ws.onopen () { reconnectDelay 1000; // 重置延迟 // 重连后全量同步一次 fetch(/api/tasks?limit50).then(r r.json()).then(renderAll); }; ws.onclose () { setTimeout(connect, reconnectDelay); reconnectDelay Math.min(reconnectDelay * 2, 30000); // 指数退避最大 30 秒 }; ws.onerror () ws.close(); }指数退避是关键避免服务重启期间大量重连请求打爆服务。最大延迟设 30 秒因为再长的话用户会感觉界面“死了”。5.4 桌宠动画卡顿桌宠用 CSS sprite 动画理论上很轻量但实测在部分 Windows 机器上会有卡顿。排查后发现是transparent: true的窗口在部分显卡驱动下合成开销较大。解决方案是降低动画帧率从 12fps 降到 8fps并缩小窗口尺寸从 256x256 降到 128x128。另外setIgnoreMouseEvents的forward: true参数在部分系统上会导致额外的合成开销如果不需要鼠标穿透可以去掉。还有一个隐藏问题Electron 的透明窗口在多个显示器之间移动时可能会闪烁。这个目前没有完美的解决方案我的做法是限制桌宠只能在主显示器上显示通过screen.getPrimaryDisplay()获取主显示器的工作区域然后限制窗口位置。5.5 常见问题速查表现象可能原因排查方法解决方案Agent 不调用工具描述不清晰查看 Agent 日志中的 tools/list 响应优化工具描述加引导语任务重复创建Agent 未先查询查看数据库中的重复记录服务端去重 描述引导看板不刷新WebSocket 断连浏览器控制台看连接状态实现自动重连 全量同步桌宠卡顿透明窗口合成开销任务管理器看 GPU 占用降帧率、缩尺寸、去 forwardMCP 调用报错返回格式不对查看 MCP Server 日志确保返回 content 数组格式数据库锁死并发写入冲突查看 SQLite 错误日志启用 WAL 模式SQLite 的 WAL 模式这里补充一句默认的 journal 模式在并发读写时容易锁死启用 WAL 之后读写可以并行对看板这种读多写少的场景提升明显。启用方式是在数据库初始化时执行db.pragma(journal_mode WAL)。6. 一些实操心得和扩展思路这个项目从最初的想法到能跑起来大概花了一个周末的时间。踩过的坑主要集中在 MCP 协议的细节和 Electron 透明窗口的兼容性上。如果让我重新做一遍我会先把 MCP Server 单独跑通用官方的 inspector 工具验证每个工具的调用然后再接看板和桌宠。这样出问题的时候能快速定位是哪一层的毛病。关于扩展有几个方向我觉得挺有意思。一是任务依赖现在只支持父子任务不支持“A 完成才能开始 B”这种依赖关系。加上依赖之后Agent 可以更智能地规划执行顺序。二是多 Agent 协作多个 Agent 共享同一个看板通过任务认领机制避免重复劳动。三是历史统计记录每个任务的耗时、Agent 的活跃时段生成周报之类的总结。四是语音提醒任务阻塞超过一定时间用 TTS 播报提醒。桌宠这块还可以做得更有趣。比如根据任务标签切换不同的桌宠皮肤refactor标签显示戴眼镜的桌宠bugfix标签显示拿扳手的桌宠。或者根据优先级调整桌宠的活跃程度高优先级任务进行中时桌宠跑得更快。这些都不影响核心功能但能让工具用起来更有意思。最后分享一个配置上的小技巧MCP Server 的启动命令建议用绝对路径因为不同 Agent 客户端的工作目录不一样相对路径容易找不到文件。另外数据库文件也建议放在用户目录下比如~/.agent-kanban/kanban.db而不是项目目录里这样多个项目可以共享同一个看板Agent 的待办不会因为切换项目而丢失。