Módulo 4: MCP Server en TypeScript
Proyecto: MCP Server TypeScript Completo
Proyecto: MCP Server TypeScript Completo
Descripción de la cápsula
Este es el momento de integración. En las cápsulas 02-05 aprendiste cada pieza por separado: setup de proyecto, tools con Zod, resources con URI templates, y transports. Ahora vas a construir un MCP server completo que combina todo en un caso de uso real.
El proyecto es un TaskFlow MCP Server — un servidor de gestión de tareas que Claude Code puede usar para crear, organizar, buscar y gestionar tareas de desarrollo. No es un toy example: es el tipo de server que un developer construiría para integrar su sistema de gestión de tareas con Claude Code.
Al terminar esta cápsula, tendrás un MCP server funcional con múltiples tools, resources, validación completa con Zod, error handling robusto, y conectado a Claude Code.
El proyecto: TaskFlow MCP Server
Qué construirás
taskflow-mcp-server/
├── package.json
├── tsconfig.json
└── src/
├── index.ts ← Entry point
├── tools/
│ ├── task-tools.ts ← CRUD de tareas
│ ├── tag-tools.ts ← Gestión de tags
│ └── search-tools.ts ← Búsqueda y filtros
├── resources/
│ ├── task-resources.ts ← Resources de tareas
│ └── stats-resources.ts ← Estadísticas
└── store/
└── task-store.ts ← Almacenamiento in-memory
Capabilities
Tools:
create_task— Crear una nueva tarea con título, descripción, prioridad, tagsupdate_task— Actualizar campos de una tarea existentecomplete_task— Marcar una tarea como completadadelete_task— Eliminar una tareasearch_tasks— Buscar tareas con filtros (estado, prioridad, tags, texto)manage_tags— Crear, listar y eliminar tags
Resources:
tasks://all— Lista completa de tareastasks://task/{id}— Detalle de una tarea específicatasks://stats— Estadísticas: total, por estado, por prioridad
Paso 1: Setup del proyecto
Crear el proyecto
mkdir taskflow-mcp-server
cd taskflow-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
package.json
{
"name": "taskflow-mcp-server",
"version": "1.0.0",
"description": "MCP Server para gestión de tareas de desarrollo",
"type": "module",
"main": "build/index.js",
"bin": {
"taskflow-mcp": "build/index.js"
},
"scripts": {
"build": "tsc",
"start": "node build/index.js",
"dev": "tsc --watch",
"inspect": "npm run build && npx @modelcontextprotocol/inspector node build/index.js"
}
}
tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "build"]
}
Crear la estructura
mkdir -p src/tools src/resources src/store
Paso 2: El store — almacenamiento de datos
Empezamos por el store porque tools y resources lo necesitan. Es un almacén in-memory con persistencia a archivo JSON.
src/store/task-store.ts:
import * as fs from "fs/promises";
import * as path from "path";
import { randomUUID } from "crypto";
export type Priority = "low" | "medium" | "high" | "critical";
export type TaskStatus = "pending" | "in_progress" | "completed" | "cancelled";
export interface Task {
id: string;
title: string;
description: string;
status: TaskStatus;
priority: Priority;
tags: string[];
createdAt: string;
updatedAt: string;
completedAt?: string;
}
interface StoreData {
tasks: Task[];
tags: string[];
}
const DATA_FILE = process.env.TASKFLOW_DATA || path.join(process.cwd(), "taskflow-data.json");
let store: StoreData = {
tasks: [],
tags: ["bug", "feature", "refactor", "docs", "test", "chore"],
};
export async function loadStore(): Promise<void> {
try {
const data = await fs.readFile(DATA_FILE, "utf-8");
store = JSON.parse(data);
} catch {
await saveStore();
}
}
async function saveStore(): Promise<void> {
await fs.writeFile(DATA_FILE, JSON.stringify(store, null, 2), "utf-8");
}
export function getAllTasks(): Task[] {
return [...store.tasks];
}
export function getTaskById(id: string): Task | undefined {
return store.tasks.find(t => t.id === id);
}
export async function createTask(
title: string,
description: string,
priority: Priority,
tags: string[]
): Promise<Task> {
const task: Task = {
id: randomUUID().slice(0, 8),
title,
description,
status: "pending",
priority,
tags,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
store.tasks.push(task);
await saveStore();
return task;
}
export async function updateTask(
id: string,
updates: Partial<Pick<Task, "title" | "description" | "priority" | "tags" | "status">>
): Promise<Task | null> {
const task = store.tasks.find(t => t.id === id);
if (!task) return null;
if (updates.title !== undefined) task.title = updates.title;
if (updates.description !== undefined) task.description = updates.description;
if (updates.priority !== undefined) task.priority = updates.priority;
if (updates.tags !== undefined) task.tags = updates.tags;
if (updates.status !== undefined) task.status = updates.status;
task.updatedAt = new Date().toISOString();
if (updates.status === "completed") {
task.completedAt = new Date().toISOString();
}
await saveStore();
return task;
}
export async function deleteTask(id: string): Promise<Task | null> {
const index = store.tasks.findIndex(t => t.id === id);
if (index === -1) return null;
const removed = store.tasks.splice(index, 1)[0];
await saveStore();
return removed;
}
export function searchTasks(filters: {
status?: TaskStatus;
priority?: Priority;
tags?: string[];
query?: string;
}): Task[] {
return store.tasks.filter(task => {
if (filters.status && task.status !== filters.status) return false;
if (filters.priority && task.priority !== filters.priority) return false;
if (filters.tags?.length) {
const hasMatchingTag = filters.tags.some(tag => task.tags.includes(tag));
if (!hasMatchingTag) return false;
}
if (filters.query) {
const q = filters.query.toLowerCase();
const inTitle = task.title.toLowerCase().includes(q);
const inDesc = task.description.toLowerCase().includes(q);
if (!inTitle && !inDesc) return false;
}
return true;
});
}
export function getAllTags(): string[] {
return [...store.tags];
}
export async function addTag(tag: string): Promise<boolean> {
if (store.tags.includes(tag)) return false;
store.tags.push(tag);
await saveStore();
return true;
}
export async function removeTag(tag: string): Promise<boolean> {
const index = store.tags.indexOf(tag);
if (index === -1) return false;
store.tags.splice(index, 1);
await saveStore();
return true;
}
export function getStats(): {
total: number;
byStatus: Record<TaskStatus, number>;
byPriority: Record<Priority, number>;
completionRate: string;
avgCompletionTime: string | null;
} {
const tasks = store.tasks;
const total = tasks.length;
const byStatus: Record<TaskStatus, number> = {
pending: 0, in_progress: 0, completed: 0, cancelled: 0,
};
const byPriority: Record<Priority, number> = {
low: 0, medium: 0, high: 0, critical: 0,
};
const completionTimes: number[] = [];
for (const task of tasks) {
byStatus[task.status]++;
byPriority[task.priority]++;
if (task.completedAt) {
const created = new Date(task.createdAt).getTime();
const completed = new Date(task.completedAt).getTime();
completionTimes.push(completed - created);
}
}
const completionRate = total > 0
? ((byStatus.completed / total) * 100).toFixed(1) + "%"
: "N/A";
let avgCompletionTime: string | null = null;
if (completionTimes.length > 0) {
const avgMs = completionTimes.reduce((a, b) => a + b, 0) / completionTimes.length;
const avgHours = avgMs / (1000 * 60 * 60);
avgCompletionTime = avgHours < 1
? `${(avgMs / (1000 * 60)).toFixed(0)} minutos`
: `${avgHours.toFixed(1)} horas`;
}
return { total, byStatus, byPriority, completionRate, avgCompletionTime };
}
Paso 3: Los tools
Task tools
src/tools/task-tools.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import * as store from "../store/task-store.js";
export function registerTaskTools(server: McpServer): void {
server.tool(
"create_task",
"Crea una nueva tarea de desarrollo con título, descripción, prioridad y tags. Retorna la tarea creada con su ID único.",
{
title: z.string().min(1).max(200)
.describe("Título de la tarea (e.g., 'Implementar autenticación JWT')"),
description: z.string().min(1)
.describe("Descripción detallada de la tarea"),
priority: z.enum(["low", "medium", "high", "critical"]).default("medium")
.describe("Prioridad de la tarea"),
tags: z.array(z.string()).default([])
.describe("Tags para categorizar (e.g., ['bug', 'backend'])"),
},
async ({ title, description, priority, tags }) => {
const task = await store.createTask(title, description, priority, tags);
return {
content: [{
type: "text" as const,
text: JSON.stringify({
message: `Tarea creada: "${task.title}"`,
task,
}, null, 2),
}],
};
}
);
server.tool(
"update_task",
"Actualiza los campos de una tarea existente. Solo envía los campos que quieres cambiar.",
{
taskId: z.string().describe("ID de la tarea a actualizar"),
title: z.string().min(1).max(200).optional()
.describe("Nuevo título"),
description: z.string().optional()
.describe("Nueva descripción"),
priority: z.enum(["low", "medium", "high", "critical"]).optional()
.describe("Nueva prioridad"),
status: z.enum(["pending", "in_progress", "completed", "cancelled"]).optional()
.describe("Nuevo estado"),
tags: z.array(z.string()).optional()
.describe("Nuevos tags (reemplaza los existentes)"),
},
async ({ taskId, title, description, priority, status, tags }) => {
const updates: Record<string, unknown> = {};
if (title !== undefined) updates.title = title;
if (description !== undefined) updates.description = description;
if (priority !== undefined) updates.priority = priority;
if (status !== undefined) updates.status = status;
if (tags !== undefined) updates.tags = tags;
if (Object.keys(updates).length === 0) {
return {
content: [{ type: "text" as const, text: "Error: no se especificaron campos para actualizar" }],
isError: true,
};
}
const task = await store.updateTask(taskId, updates as any);
if (!task) {
return {
content: [{ type: "text" as const, text: `Error: tarea '${taskId}' no encontrada` }],
isError: true,
};
}
return {
content: [{
type: "text" as const,
text: JSON.stringify({
message: `Tarea actualizada: "${task.title}"`,
updatedFields: Object.keys(updates),
task,
}, null, 2),
}],
};
}
);
server.tool(
"complete_task",
"Marca una tarea como completada. Registra la fecha de completado automáticamente.",
{
taskId: z.string().describe("ID de la tarea a completar"),
},
async ({ taskId }) => {
const task = await store.updateTask(taskId, { status: "completed" });
if (!task) {
return {
content: [{ type: "text" as const, text: `Error: tarea '${taskId}' no encontrada` }],
isError: true,
};
}
return {
content: [{
type: "text" as const,
text: JSON.stringify({
message: `✅ Tarea completada: "${task.title}"`,
completedAt: task.completedAt,
task,
}, null, 2),
}],
};
}
);
server.tool(
"delete_task",
"Elimina una tarea permanentemente. Esta acción no se puede deshacer.",
{
taskId: z.string().describe("ID de la tarea a eliminar"),
},
async ({ taskId }) => {
const task = await store.deleteTask(taskId);
if (!task) {
return {
content: [{ type: "text" as const, text: `Error: tarea '${taskId}' no encontrada` }],
isError: true,
};
}
return {
content: [{
type: "text" as const,
text: JSON.stringify({
message: `Tarea eliminada: "${task.title}"`,
deletedTask: task,
}, null, 2),
}],
};
}
);
}
Search tools
src/tools/search-tools.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import * as store from "../store/task-store.js";
export function registerSearchTools(server: McpServer): void {
server.tool(
"search_tasks",
"Busca y filtra tareas por estado, prioridad, tags o texto. Combina filtros para búsquedas precisas.",
{
status: z.enum(["pending", "in_progress", "completed", "cancelled"]).optional()
.describe("Filtrar por estado"),
priority: z.enum(["low", "medium", "high", "critical"]).optional()
.describe("Filtrar por prioridad"),
tags: z.array(z.string()).optional()
.describe("Filtrar por tags (cualquier coincidencia)"),
query: z.string().optional()
.describe("Buscar texto en título y descripción"),
sortBy: z.enum(["created", "updated", "priority"]).default("created")
.describe("Ordenar resultados"),
limit: z.number().int().positive().default(20)
.describe("Número máximo de resultados"),
},
async ({ status, priority, tags, query, sortBy, limit }) => {
let results = store.searchTasks({ status, priority, tags, query });
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
switch (sortBy) {
case "created":
results.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
break;
case "updated":
results.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
break;
case "priority":
results.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
break;
}
const truncated = results.length > limit;
results = results.slice(0, limit);
const activeFilters: string[] = [];
if (status) activeFilters.push(`status=${status}`);
if (priority) activeFilters.push(`priority=${priority}`);
if (tags?.length) activeFilters.push(`tags=${tags.join(",")}`);
if (query) activeFilters.push(`query="${query}"`);
return {
content: [{
type: "text" as const,
text: JSON.stringify({
filters: activeFilters.length > 0 ? activeFilters : ["none (showing all)"],
sortedBy: sortBy,
totalResults: results.length,
truncated,
tasks: results,
}, null, 2),
}],
};
}
);
}
Tag tools
src/tools/tag-tools.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import * as store from "../store/task-store.js";
export function registerTagTools(server: McpServer): void {
server.tool(
"manage_tags",
"Gestiona los tags disponibles: crear nuevos, listar existentes, o eliminar tags.",
{
action: z.enum(["list", "create", "delete"])
.describe("Acción a realizar"),
tag: z.string().optional()
.describe("Nombre del tag (requerido para create y delete)"),
},
async ({ action, tag }) => {
switch (action) {
case "list": {
const tags = store.getAllTags();
const tasks = store.getAllTasks();
const tagCounts: Record<string, number> = {};
for (const t of tags) {
tagCounts[t] = tasks.filter(task => task.tags.includes(t)).length;
}
return {
content: [{
type: "text" as const,
text: JSON.stringify({
totalTags: tags.length,
tags: tags.map(t => ({
name: t,
taskCount: tagCounts[t],
})),
}, null, 2),
}],
};
}
case "create": {
if (!tag) {
return {
content: [{ type: "text" as const, text: "Error: 'tag' es requerido para crear" }],
isError: true,
};
}
const created = await store.addTag(tag.toLowerCase());
if (!created) {
return {
content: [{ type: "text" as const, text: `Tag '${tag}' ya existe` }],
isError: true,
};
}
return {
content: [{
type: "text" as const,
text: JSON.stringify({
message: `Tag '${tag}' creado`,
allTags: store.getAllTags(),
}, null, 2),
}],
};
}
case "delete": {
if (!tag) {
return {
content: [{ type: "text" as const, text: "Error: 'tag' es requerido para eliminar" }],
isError: true,
};
}
const deleted = await store.removeTag(tag);
if (!deleted) {
return {
content: [{ type: "text" as const, text: `Tag '${tag}' no encontrado` }],
isError: true,
};
}
return {
content: [{
type: "text" as const,
text: JSON.stringify({
message: `Tag '${tag}' eliminado`,
remainingTags: store.getAllTags(),
}, null, 2),
}],
};
}
}
}
);
}
Paso 4: Los resources
Task resources
src/resources/task-resources.ts:
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import * as store from "../store/task-store.js";
export function registerTaskResources(server: McpServer): void {
server.resource(
"all-tasks",
"tasks://all",
{
description: "Lista completa de todas las tareas con su estado actual",
mimeType: "application/json",
},
async (uri) => {
const tasks = store.getAllTasks();
return {
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify({
totalTasks: tasks.length,
tasks: tasks.map(t => ({
id: t.id,
title: t.title,
status: t.status,
priority: t.priority,
tags: t.tags,
createdAt: t.createdAt,
})),
generatedAt: new Date().toISOString(),
}, null, 2),
}],
};
}
);
server.resource(
"task-detail",
new ResourceTemplate("tasks://task/{taskId}", {
list: async () => {
const tasks = store.getAllTasks();
return tasks.map(t => ({
uri: `tasks://task/${t.id}`,
name: `${t.title} [${t.status}]`,
description: `Tarea ${t.id}: ${t.title} — ${t.priority} priority`,
}));
},
}),
{
description: "Detalle completo de una tarea específica por su ID",
mimeType: "application/json",
},
async (uri, params) => {
const taskId = params.taskId as string;
const task = store.getTaskById(taskId);
if (!task) {
throw new Error(`Tarea '${taskId}' no encontrada`);
}
return {
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(task, null, 2),
}],
};
}
);
}
Stats resources
src/resources/stats-resources.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import * as store from "../store/task-store.js";
export function registerStatsResources(server: McpServer): void {
server.resource(
"task-stats",
"tasks://stats",
{
description: "Estadísticas de tareas: totales, por estado, por prioridad, tasa de completado",
mimeType: "application/json",
},
async (uri) => {
const stats = store.getStats();
const tags = store.getAllTags();
const tasks = store.getAllTasks();
const tagStats = tags.map(tag => ({
tag,
count: tasks.filter(t => t.tags.includes(tag)).length,
})).sort((a, b) => b.count - a.count);
return {
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify({
...stats,
topTags: tagStats.slice(0, 5),
availableTags: tags,
generatedAt: new Date().toISOString(),
}, null, 2),
}],
};
}
);
}
Paso 5: El entry point
src/index.ts:
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { loadStore } from "./store/task-store.js";
import { registerTaskTools } from "./tools/task-tools.js";
import { registerSearchTools } from "./tools/search-tools.js";
import { registerTagTools } from "./tools/tag-tools.js";
import { registerTaskResources } from "./resources/task-resources.js";
import { registerStatsResources } from "./resources/stats-resources.js";
const server = new McpServer({
name: "taskflow-mcp-server",
version: "1.0.0",
});
registerTaskTools(server);
registerSearchTools(server);
registerTagTools(server);
registerTaskResources(server);
registerStatsResources(server);
async function main() {
await loadStore();
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("TaskFlow MCP Server running on stdio");
console.error(`Data file: ${process.env.TASKFLOW_DATA || "taskflow-data.json"}`);
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});
Paso 6: Compilar y probar
Compilar
npm run build
Si hay errores de compilación, revísalos. Los más comunes:
- Import paths sin
.js: Todos los imports locales necesitan extensión.js - Tipos faltantes: Verifica que cada función tiene tipos de retorno explícitos o inferidos
- strict mode violations: Verifica que no haya
anyimplícito
Verificar que arranca
node build/index.js
Output esperado:
TaskFlow MCP Server running on stdio
Data file: taskflow-data.json
Probar con MCP Inspector
npm run inspect
En el Inspector, verifica:
Tab Tools:
create_task— visible con todos los parámetrosupdate_task— visiblecomplete_task— visibledelete_task— visiblesearch_tasks— visible con filtrosmanage_tags— visible
Tab Resources:
tasks://all— lista vacía (aún no hay tareas)tasks://stats— estadísticas con zerostasks://task/{taskId}— template visible
Prueba funcional
- Crear una tarea:
{
"title": "Implementar autenticación JWT",
"description": "Agregar login/register endpoints con JWT tokens",
"priority": "high",
"tags": ["feature", "backend"]
}
- Crear otra tarea:
{
"title": "Corregir bug en validación de email",
"description": "El regex actual acepta emails sin dominio",
"priority": "critical",
"tags": ["bug", "backend"]
}
-
Leer el resource
tasks://all— deberías ver ambas tareas. -
Buscar tareas:
{
"priority": "critical"
}
Debería retornar solo la tarea del bug.
-
Completar una tarea: Usa el ID de la primera tarea.
-
Leer
tasks://stats— debería mostrar 2 tareas totales, 1 completed, 1 pending.
Paso 7: Conectar a Claude Code
claude mcp add taskflow -s user -- node $(pwd)/build/index.js
Verificar
claude
/mcp
Deberías ver:
MCP Servers:
taskflow: connected
Tools:
- create_task
- update_task
- complete_task
- delete_task
- search_tasks
- manage_tags
Resources:
- tasks://all
- tasks://task/{taskId}
- tasks://stats
Pruebas en Claude Code
Prueba estos prompts:
"Crea una tarea de alta prioridad para implementar rate limiting en la API, con tags backend y feature"
"Muéstrame todas las tareas pendientes"
"¿Cuáles son las estadísticas actuales de mis tareas?"
"Busca tareas con el tag 'bug' ordenadas por prioridad"
"Marca como completada la tarea de rate limiting"
Observa cómo Claude Code selecciona automáticamente el tool o resource correcto basándose en lo que le pides.
Paso 8: Verificación final
Checklist
- El proyecto compila sin errores (
npm run build) - El server arranca (muestra log en stderr)
- MCP Inspector muestra 6 tools y 3 resources
- create_task funciona y retorna el ID
- update_task actualiza campos correctamente
- complete_task marca como completada con fecha
- delete_task elimina y retorna la tarea eliminada
- search_tasks filtra por estado, prioridad, tags, y texto
- manage_tags lista, crea y elimina tags
- tasks://all retorna la lista completa
- tasks://task/{id} retorna el detalle
- tasks://stats retorna estadísticas correctas
- Claude Code conecta y usa los tools/resources
- Los datos persisten entre reinicios del server (archivo JSON)
Extensiones sugeridas
Si quieres llevar este proyecto más lejos, prueba:
Extensión 1: Agregar prompts
Agrega un prompt daily_standup que genere un reporte de standup basado en las tareas:
server.prompt(
"daily-standup",
"Genera un reporte de daily standup basado en las tareas",
{},
async () => {
const tasks = store.getAllTasks();
const completed = tasks.filter(t => t.status === "completed").slice(-5);
const inProgress = tasks.filter(t => t.status === "in_progress");
const pending = tasks.filter(t => t.status === "pending" && t.priority === "critical");
return {
messages: [{
role: "user" as const,
content: {
type: "text" as const,
text: `Genera un reporte de daily standup basado en estas tareas:
**Completadas recientemente:**
${completed.map(t => `- ${t.title}`).join("\n") || "- Ninguna"}
**En progreso:**
${inProgress.map(t => `- ${t.title} [${t.priority}]`).join("\n") || "- Ninguna"}
**Críticas pendientes:**
${pending.map(t => `- ${t.title}`).join("\n") || "- Ninguna"}
Formato: ayer hice / hoy haré / bloqueos`,
},
}],
};
}
);
Extensión 2: Agregar subtareas
Extiende el modelo Task para soportar subtareas: un array subtasks con título y estado. Agrega un tool add_subtask y actualiza el resource de detalle para incluir subtareas.
Extensión 3: Export a Markdown
Agrega un tool export_tasks que genere un archivo Markdown con todas las tareas organizadas por estado y prioridad.
Conexión con el Módulo 8
El TaskFlow MCP Server que construiste en esta cápsula tiene los mismos patrones que usarás en el proyecto integrador del Módulo 8:
TaskFlow (Módulo 4) → Proyecto Final (Módulo 8)
─────────────────────────────────────────────────────────────
In-memory store con JSON → Base de datos real (SQLite/PostgreSQL)
6 tools básicos → Tools conectados a APIs reales
3 resources → Resources dinámicos con caching
Validación Zod → Misma validación + schemas más complejos
Transport stdio → Stdio + opción HTTP para deploy
Sin tests → Test suite completo (Módulo 7)
Sin autenticación → Auth si es server HTTP
Los patrones son idénticos — el store pattern, la organización de tools en archivos separados, la validación Zod, el error handling con isError. Lo que cambia en el Módulo 8 es la escala y la conexión con servicios reales.
Troubleshooting
"Error: Cannot find module '../store/task-store.js'"
Causa: Los imports relativos necesitan la extensión .js.
Solución: Verifica que todos los imports locales terminan en .js:
import * as store from "../store/task-store.js"; // ✅
import * as store from "../store/task-store"; // ❌
"Los datos se pierden al reiniciar"
Causa: El archivo taskflow-data.json no se está creando.
Solución: Verifica permisos de escritura en el directorio actual. El server crea el archivo en process.cwd().
"Claude Code no muestra los tools"
Causa: Error silencioso al registrar los tools.
Solución:
# Remueve y re-agrega con ruta absoluta
claude mcp remove taskflow
claude mcp add taskflow -s user -- node $(pwd)/build/index.js
# Reinicia Claude Code
claude
/mcp
"El resource template no lista las tareas"
Causa: El callback list del ResourceTemplate se ejecuta antes de que haya tareas.
Solución: Es normal — las tareas se descubren después de crearlas. Crea algunas tareas primero y luego verifica que el resource template las lista.
"Conflicto de IDs al crear tareas rápidamente"
Causa: El randomUUID().slice(0, 8) genera IDs cortos que podrían colisionar en teoría.
Solución: Para producción, usa el UUID completo o un generador de IDs secuenciales.
Resumen
En esta cápsula:
- Construiste un MCP server completo (
taskflow-mcp-server) con 6 tools y 3 resources - Organizaste el código en módulos: tools/, resources/, store/
- Implementaste CRUD completo para tareas con Zod validation
- Implementaste búsqueda con filtros combinables (estado, prioridad, tags, texto)
- Implementaste resources estáticos y dinámicos (URI templates con list callback)
- Implementaste un store con persistencia a JSON
- Conectaste a Claude Code y verificaste que todo funciona end-to-end
- Entendiste la conexión directa con el proyecto integrador del Módulo 8
Este es el primer MCP server "de verdad" que construyes. Ya no es un prototipo — es un server funcional con múltiples capabilities que podrías extender y usar en tu trabajo real.
Recursos adicionales
- MCP TypeScript SDK - SDK oficial
- Zod Documentation - Referencia de validación
- MCP Inspector - Testing visual
- Claude Code MCP Configuration - Configuración oficial
- MCP Servers Examples - Servers de referencia
- Awesome MCP Servers - Directorio de la comunidad
- TypeScript Handbook - Referencia de TypeScript
- Node.js crypto.randomUUID - Generación de IDs
Siguiente módulo: MCP Server en Python — los mismos conceptos, diferente lenguaje. Decoradores en vez de métodos, Pydantic en vez de Zod, FastMCP como abstracción de alto nivel. Si dominas TypeScript, Python será natural.