用190行TypeScript构建可搜索的工作日志MCP服务器
基于 Markdown 文件的小型 MCP 服务器:四种基于意图的工具、提示词风格的描述、兼具测试功能的 stdio 演示客户端,以及实际使用中可能遇到的问题。
来源: I Built an MCP Server That Keeps My Work Journal — Here’s Everything I Learned
一个约190行TypeScript代码的Model Context Protocol服务器可以将一组Markdown文件转换为可搜索的工作日志,任何AI助手都能读取和编写内容。它也是一个不错的入门级MCP项目,同时展示了如何在不使用编辑器的情况下演示服务器功能。
为何选择日记形式
每日记录的习惯很少能坚持超过一周。而那些已经帮我们排查过竞态条件、共同编写了一半测试用例、还见证了所有代码重构的AI助手,其实早已知晓当天发生的一切。自律从来都不是真正的障碍——日志本应保存在工作之外。
换个思路:不必由人手动维护日志,而是给助手提供工具来管理日志。MCP这一能让大语言模型客户端发现并调用外部工具的标准恰好很适用。一个封装了普通 Markdown 文件目录的dev-diary服务器,用不到200行代码就涵盖了所有核心要素(工具、架构、标准输入输出传输、无状态存储),是极佳的学习范例。
日志本身并非重点——现有的应用早已具备日志功能。这个主题简单、枯燥且人人皆懂,正适合用于学习项目。关键问题在于是否有人能为任何事物构建MCP服务器。答案是肯定的,而且协议部分其实相对简单,真正困难的是决定要封装什么内容。只要代码能够读取或写入,健身记录、阅读清单、内部维基或智能家居都可以通过MCP服务器来实现。
四种工具
| 工具 | 用途 |
|---|---|
log_work | 在当天的文件中添加带有时间戳和标签的记录 |
search_diary | 对所有记录进行全文搜索 |
daily_summary | 显示所选日期的所有记录 |
stats | 显示总数、连续记录次数及最常用标签 |
存储方式十分简单:每天一个 Markdown 文件,内容即为上述类型的记录。
## 14:32 #bugfix #websocket
Fixed the race condition in the WebSocket broadcast queue
## 16:10 #testing
Wrote E2E test covering two-client sync
没有数据库。这些文件可以通过 grep 检索、手动提交并编辑,这样的选择日后会带来好处。
构建服务器
官方的 TypeScript SDK(v2版本,遵循2026-07-28的规范)是基础。安装时需要服务器相关包以及 Zod:
npm install @modelcontextprotocol/server zod
导入项包括服务器类、stdio传输层以及Zod的v4入口点:
import { McpServer } from '@modelcontextprotocol/server';
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';
服务器对象需要一个名称和版本:
const server = new McpServer({ name: 'dev-diary', version: '1.0.0' });
每个工具都配有名称、配置和处理程序。这是 log_work:
server.registerTool(
'log_work',
{
description: 'Append a timestamped entry to the developer diary...',
inputSchema: z.object({
text: z.string().min(1),
tags: z.array(z.string()).optional(),
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
}),
},
async ({ text, tags = [], date }) => {
// ...append to diary/YYYY-MM-DD.md...
return { content: [{ type: 'text', text: 'Logged.' }] };
},
);
最后,通过标准输入输出进行连接:
await server.connect(new StdioServerTransport());
那是一台完整的服务器。其运行依赖于四个核心概念:
registerTool(name, config, handler)用于构建工具目录。inputSchema是Zod格式的架构定义。SDK会将其转换为客户端可用的JSON Schema格式,并在处理器执行前对参数进行验证。- 返回值采用
{ content: [{ type: 'text', text: '...' }] }的结构,这就是大语言模型所读取的结果。 - 传输层采用
StdioServerTransport(),使客户端能够启动进程并通过标准输入输出进行JSON-RPC通信。本地使用时无需端口也不需要身份验证。
描述即提示
这是最需要调整思维方式的一点。工具描述并非面向人类的API文档,而是模型用来判断何时以及如何调用该工具的指令。以下是文档式描述的对比:
// ❌ documentation-style
description: 'Appends an entry to the diary.'
采用提示词式的方式:
// ✅ prompt-style
description: 'Append a timestamped entry to the developer diary for today.
Use this whenever the user says they finished/did/fixed something and
wants it recorded.'
第二个部分告诉模型何时使用该工具。其“随时使用此工具”的设定可将关于最终修复某个不稳定测试的随意表述转化为工具调用,而无需明确的指令。参数描述也是如此:诸如 可选标签,例如 ["bugfix","api"] 这样的提示可通过示例来说明期望的格式。
工具对应的是问题,而非操作
带有CRUD工具(add_entry、update_entry、delete_entry)的初稿结构有误。没人会要求日记应用更新第47条记录,他们只会询问上周的WebSocket漏洞解决了没有。因此最终的设计遵循实际需求:search_diary用于查找内容,daily_summary用于总结当日情况,stats提供激励性的数据视图,log_work则作为唯一的记录工具。这四个工具之间没有重叠,且每个工具都能回应开发者真正关心的问题。基本原则是:每个用户问题对应一个工具,而非每个数据操作对应一个工具。
无需编辑器的演示
更棘手的问题是如何展示MCP服务器的实际运行效果。通过连接Claude Desktop并截取聊天界面截图无法证明功能的可重复性,这种方式依赖于封闭的应用程序,且无法在CI环境中或读者的机器上运行。
该规范的隐性优势在此处发挥了作用:MCP仅通过标准输入输出实现客户端与服务器之间的通信。没有任何机制阻止第二个小型程序以编辑器的形式连接并全程控制服务器端。其导入内容如下:
import { Client } from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
它以子进程的形式启动服务器:
const transport = new StdioClientTransport({
command: 'node',
args: ['dist/server.js'], // spawns the server as a child process
});
const client = new Client({ name: 'demo-client', version: '1.0.0' });
await client.connect(transport);
接着它会像桌面客户端在后台所做的那样,列出各种工具并调用它们:
// Exactly what Claude Desktop does under the hood:
const { tools } = await client.listTools();
await client.callTool({ name: 'log_work', arguments: {
text: 'Fixed the race condition in the broadcast queue',
tags: ['bugfix', 'websocket'],
}});
大约五十行之后,npm run demo会输出完整的协议会话内容作为证据。该工具列出了:
=== 1. listTools ===
• log_work — Append a timestamped entry to the developer diary...
• search_diary — Full-text search across every entry...
• daily_summary — Everything logged on a given date...
• stats — Totals, active days, streaks, top tags...
三篇作品:
=== 2. log_work x3 ===
Logged to 2026-08-22.md at 19:05 (tags: bugfix, websocket)
...
统计信息视图:
=== 5. stats ===
📊 1 entries across 2 day(s)
🔥 Streak: 2 consecutive day(s)
🏷️ Top tags: #bugfix (1), #websocket (1)
由于这是一段脚本,它同时也充当了集成测试的功能。每次服务器代码变更后,都会运行该演示程序,如果握手失败或架构发生变化,就会立即抛出错误提示。演示、文档和测试套件 thus 合为了一体。对于可视化操作,可通过 npx @modelcontextprotocol/inspector node dist/server.js 启动官方 Inspector 工具,从而手动操作各种功能。这两者结合便涵盖了开发与演示的全过程。
注意事项
- 顶层 await 语句可能会破坏构建过程。 SDK 的 README 文件在模块级别使用了
await server.connect(...)语句。这种写法在 ESM 包("type": "module")中没有问题,但当用 TypeScript 编译为 CommonJS 格式时则会引发严重错误。将启动逻辑封装在main()函数中则不会带来任何问题,且可在所有环境中正常使用。
exactOptionalPropertyTypes 与可选模式存在冲突。在严格的 TypeScript 环境下,将 tags: undefined 传递给 z.string().optional() 类型的字段会引发错误。应降低该标志的严格程度,或在对参数进行处理时进行标准化处理。new Date().toISOString().slice(11,16) 返回的是 UTC 时间。位于 UTC-3 时区、在下午 6 点记录数据的人会看到 21:05。一旦发现这个问题,只需修改一行代码即可解决。配置方法
在 Claude Desktop、Cursor 或任何支持 MCP 的客户端中,配置文件(采用 claude_desktop_config.json 的格式)如下所示:
{
"mcpServers": {
"dev-diary": {
"command": "node",
"args": ["/absolute/path/to/dev-diary-mcp/dist/server.js"],
"env": { "DIARY_DIR": "/home/you/journal" }
}
}
}
将 DIARY_DIR 指向 dotfiles 仓库中的某个文件夹,这样就能顺便生成日志版本。一周后,像“按标签汇总我的周记”这样的请求就能自动生成无需人工撰写的状态报告。
为何选择纯 Markdown
如此有主见的选型自然需要相应的理由。SQLite 的查询功能更强大,但存储方式不透明;JSON 解析容易,但差异对比效果差。Markdown 获胜的原因有四点:
- 人类是首要的使用者。无需任何工具,日志在编辑器中、通过 grep 或 git 日志中都能正常显示。
- Git 提供备份与历史记录功能。
git log diary/能生成数据库默认不会提供的审计追踪信息。 - 大语言模型对 Markdown 的处理能力很强。模型生成的文本与存储的内容完全一致,不会出现序列化不匹配的问题。
这一教训的意义不仅限于日志文件:当人类和大型语言模型都需要使用你的数据时,应将其存储在双方都能理解的格式中。
运行示例
克隆、构建并运行演示程序:
git clone https://github.com/rogeriolaa/dev-diary-mcp
cd dev-diary-mcp && npm install && npm run build && npm run demo
接着将服务器添加到编辑器的MCP配置中,让助手开始记录日志。包括配置在内,服务器、演示客户端以及README文件的总行数大约为400行。
像这样的小型服务器是学习MCP的有效方式:能够接触到每一个概念,所得成果可每日使用,且规模足够小,便于彻底理解。
相关阅读
- 将MCP服务器迁移到2026-07-28规范:二十分钟,分步指南 ——会话功能已消失,但其他功能均未出问题。了解MCP 2026-07-28版本的变更、Python SDK 2.0的迁移步骤、MRTR的重写方式以及“废弃”意味着什么。