Construir un servidor MCP para un diario de trabajo buscable en 190 líneas de TypeScript
Un pequeño servidor MCP basado en archivos Markdown: cuatro herramientas basadas en intenciones, descripciones al estilo de prompt, un cliente de demostración de stdio que también sirve como prueba, además de verdaderos problemas ocultos.
Fuentes: I Built an MCP Server That Keeps My Work Journal — Here’s Everything I Learned
Un servidor de Protocolo de Contexto de Modelo compuesto por unas 190 líneas de TypeScript puede convertir una carpeta de archivos Markdown en un diario de trabajo buscable que cualquier asistente de IA pueda leer y escribir. También constituye un buen proyecto MCP inicial, y muestra cómo demostrar un servidor sin necesidad de un editor.
¿Por qué un diario?
Los hábitos de registro diario rara vez duran más de una semana. Mientras tanto, el asistente de IA que solucionó la condición de carrera, colaboró en la mitad de las pruebas y supervisó cada refactorización ya sabe todo lo que sucedió durante el día. La disciplina nunca fue el verdadero obstáculo. El registro se encuentra en algún lugar distinto al lugar de trabajo.
Intercambie el orden: en lugar de que una persona mantenga un diario, déle al asistente las herramientas para llevar uno. MCP, el estándar que permite a los clientes de LLM descubrir y llamar a herramientas externas, encaja perfectamente. Un servidor dev-diary que encapsula un simple directorio de archivos Markdown abarca todas las ideas fundamentales (herramientas, esquemas, transporte de stdio, almacenamiento sin estado) en menos de 200 líneas, lo que lo convierte en una forma adecuada para aprender.
El diario en sí no es el objetivo. Ya existen aplicaciones para llevar diarios. El tema es sencillo, poco interesante y de comprensión universal, lo cual es adecuado para un proyecto de aprendizaje. La pregunta clave es si alguien puede crear un servidor MCP para cualquier cosa. Sí, y resulta que el protocolo es la parte fácil. Decidir qué encapsular es más complicado. Un registro de entrenamiento, una lista de lecturas, una wiki interna o un hogar inteligente pueden estar detrás de un servidor MCP siempre y cuando el código pueda leerlo o escribir en él.
Las cuatro herramientas
| Herramienta | Propósito |
|---|---|
log_work | Añade una entrada con marca de tiempo y etiquetas al archivo del día actual |
search_diary | Búsqueda de texto completo en todas las entradas |
daily_summary | Todo lo registrado en una fecha seleccionada |
stats | Total general, secuencias continuas y etiquetas más frecuentes |
El almacenamiento es deliberadamente sencillo: un archivo Markdown por día, con entradas como estas.
## 14:32 #bugfix #websocket
Fixed the race condition in the WebSocket broadcast queue
## 16:10 #testing
Wrote E2E test covering two-client sync
No existe una base de datos. Los archivos pueden ser buscados, guardados y editados a mano, y esa opción rinde frutos más adelante.
Construcción del servidor
La base es el SDK oficial de TypeScript (v2, que implementa la especificación del 2026-07-28). La instalación requiere el paquete del servidor y Zod:
npm install @modelcontextprotocol/server zod
Las importaciones incluyen la clase del servidor, el transporte stdio y el punto de entrada v4 de Zod:
import { McpServer } from '@modelcontextprotocol/server';
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';
El objeto servidor acepta un nombre y una versión:
const server = new McpServer({ name: 'dev-diary', version: '1.0.0' });
Cada herramienta está registrada con un nombre, una configuración y un manejador. Este es 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.' }] };
},
);
Por último, conéctense mediante stdio:
await server.connect(new StdioServerTransport());
Ese es un servidor completo. Cuatro conceptos lo componen:
registerTool(name, config, handler)crea el catálogo de herramientas.inputSchemaes un esquema Zod. El SDK lo convierte en JSON Schema para el cliente y valida los argumentos antes de que se ejecute el manejador.- El valor de retorno, con la estructura
{ content: [{ type: 'text', text: '...' }] }, es lo que el LLM interpreta como resultado. - El mecanismo de transporte,
StdioServerTransport(), permite que el cliente inicie el proceso y comunique mediante JSON-RPC a través de stdin y stdout. Para uso local no se necesitan puertos ni autenticación.
Las descripciones son instrucciones
Este es el cambio mental más importante. La descripción de una herramienta no es documentación de API para personas; es una instrucción que el modelo lee para decidir cuándo y cómo llamar a la herramienta. Compare con una descripción al estilo de documentación:
// ❌ documentation-style
description: 'Appends an entry to the diary.'
con uno de estilo prompt:
// ✅ 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.'
El segundo indica al modelo cuándo debe utilizar la herramienta. Su cláusula “usar esto siempre que” convierte una observación casual sobre arreglar finalmente una prueba problemática en una llamada a la herramienta, sin necesidad de una instrucción explícita. Las descripciones de parámetros funcionan de la misma manera: una indicación como Etiquetas opcionales, p. ej. ["bugfix","api"] enseña la forma esperada mediante un ejemplo.
Las herramientas se relacionan con preguntas, no con operaciones
Un primer borrador con herramientas CRUD (add_entry, update_entry, delete_entry) tiene una estructura incorrecta. Nadie pide que un diario actualice la entrada 47; en cambio, preguntan qué sucedió con el error de WebSocket de la semana pasada. Por lo tanto, el conjunto final sigue la intención original: search_diary busca información, daily_summary resume el día, stats ofrece una visión motivadora y log_work es el único responsable de la escritura. Las cuatro herramientas no se superponen en ningún aspecto y cada una responde a algo que realmente pregunta un desarrollador. La regla general es: una herramienta por pregunta del usuario, no una por operación de datos.
Demuestra sin editor
Un problema más difícil es cómo mostrar que un servidor MCP funciona. Conectar Claude Desktop y tomar capturas de pantalla de un chat no demuestra nada reproducible, depende de una aplicación cerrada y no puede ejecutarse en entornos de integración continua ni en la máquina de un lector.
La fortaleza sutil de la especificación ayuda en este caso: MCP consiste simplemente en un cliente y un servidor a través de stdio. Nada impide que un segundo programa pequeño se conecte de la misma manera que lo haría un editor y controle completamente el lado del servidor. Sus importaciones:
import { Client } from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
Genera el servidor como un proceso hijo:
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);
Luego hace lo que hace un cliente de escritorio en segundo plano: enumera las herramientas y las llama:
// 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'],
}});
Unas cincuenta líneas más adelante, npm run demo muestra una sesión de protocolo completa como prueba. La lista de herramientas es la siguiente:
=== 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...
Tres escritos:
=== 2. log_work x3 ===
Logged to 2026-08-22.md at 19:05 (tags: bugfix, websocket)
...
Y la vista de estadísticas:
=== 5. stats ===
📊 1 entries across 2 day(s)
🔥 Streak: 2 consecutive day(s)
🏷️ Top tags: #bugfix (1), #websocket (1)
Dado que se trata de un script, también funciona como prueba de integración. Después de cada cambio en el servidor, se ejecuta la demostración, y cualquier error en el intercambio de datos o en el esquema actualizado se detecta de inmediato. La demostración, la documentación y el conjunto de pruebas se convierten en un único archivo. Para una interfaz visual, el Inspector oficial, que se inicia con npx @modelcontextprotocol/inspector node dist/server.js, abre una interfaz web que permite manipular las herramientas manualmente. Juntos, estos elementos cubren tanto el desarrollo como la demostración.
Puntos a tener en cuenta
- El uso de await en el nivel superior puede afectar la compilación. El README del SDK utiliza
await server.connect(...)en el ámbito del módulo. Esto funciona bien para paquetes ESM ("type": "module"), pero genera un error grave cuando TypeScript compila a CommonJS. Envolver el inicio del programa enmain()no tiene costo alguno y funciona en cualquier entorno. exactOptionalPropertyTypesentra en conflicto con los esquemas opcionales. Con TypeScript estricto, pasartags: undefineda un campoz.string().optional()genera errores. Es necesario relajar esta opción o normalizar los argumentos en el límite del manejador.- Las descripciones ocupan espacio en el contexto. Cada descripción de herramienta se almacena en el modelo de cada conversación en la que está conectado el servidor. El texto promocional desperdicia tokens y diluye las instrucciones útiles, por lo que prevalece el texto breve, directo y centrado en la intención.
- La falta de estado es ventajosa. El servidor no guarda nada en memoria y vuelve a leer el Markdown en cada llamada. Las ediciones manuales entre llamadas nunca generan datos obsoletos, y dos clientes en la misma carpeta comparten la información de forma gratuita. Se debe evitar el uso de cachés en herramientas basadas en archivos hasta que el análisis de rendimiento lo justifique.
- Las zonas horarias también afectan a los juguetes.
new Date().toISOString().slice(11,16)devuelve el tiempo en UTC. Alguien en UTC-3 que registre algo a las 6 p.m. verá la hora como 21:05. Una sola línea resuelve el problema una vez que se detecta.
Configuración
En Claude Desktop, Cursor o cualquier cliente compatible con MCP, la configuración (en el estilo de claude_desktop_config.json) se ve así:
{
"mcpServers": {
"dev-diary": {
"command": "node",
"args": ["/absolute/path/to/dev-diary-mcp/dist/server.js"],
"env": { "DIARY_DIR": "/home/you/journal" }
}
}
}
Dirija DIARY_DIR a una carpeta dentro de un repositorio de dotfiles y, como efecto secundario, se generarán las propias versiones del diario. Después de una semana, una solicitud como “resuma mi semana del diario, agrupada por etiqueta” produce un informe de estado que nadie tuvo que escribir.
Por qué Markdown puro
La elección más decidida merece una defensa. SQLite ofrece mejores consultas pero un almacenamiento opaco, y JSON se parsea fácilmente pero sus diferencias son difíciles de manejar. Markdown ganó por cuatro razones:
- El ser humano es un cliente de primera clase. El diario se lee bien en un editor, con grep o en los registros de git, sin necesidad de herramientas adicionales.
- Git proporciona copias de seguridad e historial.
git log diary/es un registro de auditoría que ninguna base de datos ofrece por defecto. - Los LLM manejan muy bien Markdown. Lo que escribe el modelo coincide con lo que se almacena, sin incoherencias en la serialización.
- Los fallos son sutiles. Una línea dañada es visible y se puede corregir a mano, algo que no ocurre con una página SQLite dañada.
La lección trasciende los diarios: cuando tanto las personas como los LLM consumen tus datos, guárdalos en un formato que ambos ya entiendan.
Ejecutarlo
Clona, compila y ejecuta la demostración:
git clone https://github.com/rogeriolaa/dev-diary-mcp
cd dev-diary-mcp && npm install && npm run build && npm run demo
Luego añada el servidor a la configuración MCP del editor y deje que el asistente comience a registrar datos. El servidor, el cliente de demostración y el README en conjunto suman aproximadamente 400 líneas, incluyendo la configuración.
Los servidores pequeños como este son una forma eficaz de aprender MCP: se aborda cada concepto, el resultado se utiliza a diario y el alcance es lo suficientemente reducido como para comprenderlo por completo.