Módulo 8: Proyecto — MCP Server Real
Diseño y Arquitectura del MCP Server
Diseño y Arquitectura del MCP Server
Descripción de la cápsula
Antes de escribir una sola línea de código, diseñas. No por formalismo — porque las decisiones que tomas ahora determinan si tu server va a ser mantenible o un enredo. Qué datos exponer como resources. Qué operaciones convertir en tools. Qué interacciones repetitivas automatizar con prompts. Cómo organizar los archivos. Cómo manejar errores.
Esta cápsula te guía a través del proceso de diseño. Al terminar, tendrás un documento de arquitectura completo y la estructura de archivos lista para implementar.
El proceso de diseño
Paso 1: Define el dominio
Tu MCP server gira alrededor de un dominio — un conjunto de datos y operaciones coherentes. El dominio determina todo lo demás.
Preguntas que debes responder:
- ¿Qué datos maneja? — Tareas, productos, usuarios, archivos, repos, issues...
- ¿Qué operaciones necesitas? — Crear, leer, actualizar, borrar, buscar, reportar, analizar...
- ¿Qué preguntas haces frecuentemente? — "¿Cuántas tareas pendientes hay?", "¿Cuál es el producto más vendido?", "¿Qué archivos se modificaron ayer?"
- ¿Qué datos debería Claude Code poder ver sin que le pidas explícitamente? — Estos son tus resources.
- ¿Qué acciones debería Claude Code poder ejecutar cuando se lo pides? — Estos son tus tools.
- ¿Qué interacciones repites constantemente? — Estas son candidatas a prompts.
Paso 2: Mapea primitivas al dominio
Una vez que tienes claro el dominio, mapea cada necesidad a la primitiva correcta:
| Necesidad | Primitiva | Por qué |
|---|---|---|
| "Ver qué tablas existen" | Resource | Dato estático/de consulta, sin side effects |
| "Ver el schema de una tabla" | Resource | Metadata que Claude Code necesita para entender la estructura |
| "Leer los registros de una tabla" | Resource | Acceso de lectura a datos |
| "Crear un registro nuevo" | Tool | Operación con side effects (modifica la database) |
| "Actualizar un registro" | Tool | Operación con side effects |
| "Borrar un registro" | Tool | Operación con side effects |
| "Ejecutar una query SQL custom" | Tool | Operación flexible que puede tener side effects |
| "Buscar registros por criterio" | Tool | Operación parametrizada |
| "Analizar una tabla y generar reporte" | Prompt | Interacción multi-paso que combina resources y tools |
| "Generar un resumen semanal" | Prompt | Template reutilizable con parámetros |
La regla simple
- ¿Claude Code necesita leerlo para tener contexto? → Resource
- ¿Claude Code necesita ejecutar algo que cambia datos? → Tool
- ¿Es una interacción de múltiples pasos que repites? → Prompt
Diseño para Opción A: SQLite + Python
Este es el ejemplo principal. Si elegiste Opción B o C, lee esta sección para entender el proceso y luego adapta al tuyo.
Dominio: Task Manager
Un sistema de gestión de tareas con categorías y tags. Lo suficientemente simple para implementar en una cápsula, lo suficientemente complejo para demostrar todas las primitivas.
Schema de la database
CREATE TABLE categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
description TEXT,
color TEXT DEFAULT '#6B7280',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'in_progress', 'completed', 'cancelled')),
priority TEXT DEFAULT 'medium' CHECK(priority IN ('low', 'medium', 'high', 'critical')),
category_id INTEGER REFERENCES categories(id) ON DELETE SET NULL,
due_date TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE
);
CREATE TABLE task_tags (
task_id INTEGER REFERENCES tasks(id) ON DELETE CASCADE,
tag_id INTEGER REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (task_id, tag_id)
);
Por qué este schema:
categoriesytagsdan relaciones 1:N y M:N, demostrando queries con JOINsstatusyprioritycon CHECK constraints demuestran validación a nivel de databasedue_datepermite filtros temporales ("tareas vencidas", "tareas de esta semana")- Es suficiente para 5+ tools interesantes sin ser abrumador
Resources planeados
| URI | Descripción | Qué retorna |
|---|---|---|
taskdb://tables | Lista de tablas en la database | Nombres y conteo de registros |
taskdb://table/{name}/schema | Schema de una tabla específica | Columnas, tipos, constraints |
taskdb://stats | Estadísticas generales | Conteo por status, categoría, prioridad |
taskdb://tasks/overdue | Tareas vencidas | Tareas con due_date pasado y status != completed |
taskdb://categories | Todas las categorías | Lista con conteo de tareas por categoría |
Decisiones de diseño:
- Los URIs usan el prefijo
taskdb://para namespacing claro taskdb://tableses el punto de entrada — Claude Code lo lee primero para entender la estructurataskdb://statsda un resumen rápido sin necesidad de ejecutar queriestaskdb://tasks/overduees un resource derivado — datos calculados, no un table dump
Tools planeados
| Tool | Descripción | Inputs | Side effects |
|---|---|---|---|
create_task | Crear una nueva tarea | title, description?, category_id?, priority?, due_date?, tags? | INSERT en tasks + task_tags |
list_tasks | Listar tareas con filtros | status?, priority?, category_id?, limit? | Ninguno (solo lectura) |
update_task | Actualizar campos de una tarea | task_id, campos a actualizar | UPDATE en tasks |
delete_task | Eliminar una tarea | task_id | DELETE en tasks + task_tags |
search_tasks | Buscar tareas por texto | query, search_in? (title/description/both) | Ninguno |
create_category | Crear una categoría | name, description?, color? | INSERT en categories |
run_query | Ejecutar query SQL custom | sql (solo SELECT) | Ninguno (solo lectura) |
get_task_summary | Resumen por periodo | period (today/week/month) | Ninguno |
Decisiones de diseño:
list_taskses un tool porque tiene filtros complejos que necesitan parámetrosrun_querypermite queries arbitrarias pero restringidas a SELECT (sin modificaciones)get_task_summarypodría ser un resource, pero necesita un parámetro (periodo), así que es tool- Cada tool que modifica datos retorna el objeto modificado para confirmación
Prompts planeados
| Prompt | Descripción | Parámetros |
|---|---|---|
analyze_table | Analiza una tabla y sugiere mejoras | table_name |
weekly_report | Genera reporte de tareas de la semana | week_start? (default: esta semana) |
optimize_query | Analiza y optimiza una query SQL | sql_query |
Decisiones de diseño:
analyze_tablecombina resources (schema, datos) con análisis del modeloweekly_reportestandariza una interacción que harías regularmenteoptimize_queryes avanzado — Claude Code analiza la query, consulta el schema, y sugiere mejoras
Prompt template: analyze_table
@mcp.prompt()
async def analyze_table(table_name: str) -> str:
"""Analiza la estructura y datos de una tabla, sugiere mejoras."""
return f"""Analiza la tabla '{table_name}' en la base de datos.
Por favor:
1. Lee el resource taskdb://table/{table_name}/schema para ver la estructura
2. Usa el tool run_query para contar registros: SELECT COUNT(*) FROM {table_name}
3. Usa el tool run_query para ver una muestra: SELECT * FROM {table_name} LIMIT 5
4. Lee el resource taskdb://stats para contexto general
Con esa información, genera un análisis que incluya:
- Estructura de la tabla (columnas, tipos, constraints)
- Volumen de datos
- Distribución de valores en columnas clave
- Posibles mejoras de schema (índices, constraints adicionales)
- Queries útiles para esta tabla"""
Prompt template: weekly_report
@mcp.prompt()
async def weekly_report(week_start: str = "") -> str:
"""Genera un reporte semanal de tareas."""
date_filter = f"para la semana del {week_start}" if week_start else "para esta semana"
return f"""Genera un reporte de productividad {date_filter}.
Por favor:
1. Usa el tool get_task_summary con period='week'
2. Usa el tool list_tasks con status='completed' para ver tareas terminadas
3. Lee el resource taskdb://tasks/overdue para tareas vencidas
4. Lee el resource taskdb://stats para contexto general
Con esa información, genera un reporte que incluya:
- Resumen ejecutivo (tareas completadas vs pendientes)
- Tareas completadas esta semana (con categoría)
- Tareas pendientes prioritarias
- Tareas vencidas que requieren atención
- Métricas: tasa de completación, distribución por prioridad
- Recomendaciones para la próxima semana"""
Diseño para Opción B: File System + TypeScript
Si elegiste Opción B, aquí está el diseño correspondiente.
Dominio: Project Analyzer
Un server que analiza proyectos de código — estructura, dependencias, métricas.
Resources planeados
| URI | Descripción |
|---|---|
project://info | Metadata del proyecto (nombre, tipo, tamaño) |
project://structure | Árbol de directorios |
project://dependencies | Lista de dependencias (package.json / requirements.txt) |
project://stats | Métricas: líneas de código, archivos por tipo, tamaño total |
Tools planeados
| Tool | Descripción |
|---|---|
list_directory | Lista contenido de un directorio con filtros |
read_file | Lee contenido de un archivo (con límite de tamaño) |
search_content | Busca texto en archivos del proyecto |
analyze_file | Métricas de un archivo: LOC, complejidad, imports |
find_unused_files | Detecta archivos que no se importan en ningún lado |
generate_tree | Genera un árbol visual del proyecto |
Prompts planeados
| Prompt | Descripción |
|---|---|
project_review | Análisis completo de un directorio de proyecto |
dependency_audit | Revisa dependencias: versiones, vulnerabilidades, unused |
Ejemplo de interacción con Claude Code
Tú: "Analiza la estructura de mi proyecto en ./my-app"
Claude Code: [usa resource project://structure] → lee el árbol de directorios
Claude Code: [usa tool analyze_file para los archivos principales]
Claude Code: "Tu proyecto my-app tiene 23 archivos TypeScript en 8 directorios.
La mayoría del código está en src/components/ (42%). Hay 3 archivos que no se
importan en ningún lado: utils/legacy.ts, helpers/deprecated.ts, types/old.ts."
Estructura de archivos
project-analyzer/
├── src/
│ ├── index.ts # Entry point, registros MCP
│ ├── tools/
│ │ ├── filesystem.ts # Tools de lectura/búsqueda
│ │ └── analysis.ts # Tools de análisis
│ ├── resources/
│ │ └── project.ts # Resources del proyecto
│ └── utils/
│ ├── tree.ts # Generador de árboles
│ └── metrics.ts # Cálculos de métricas
├── tests/
│ ├── tools.test.ts
│ ├── resources.test.ts
│ └── integration.test.ts
├── package.json
├── tsconfig.json
├── vitest.config.ts
└── README.md
Diseño para Opción C: API Externa
Si elegiste Opción C, aquí está el framework de diseño. El ejemplo usa Todoist pero adapta a tu API.
Dominio: Todoist Integration
Un server que conecta Claude Code con la API de Todoist para gestión de tareas.
Resources planeados
| URI | Descripción |
|---|---|
todoist://projects | Lista de proyectos |
todoist://project/{id} | Detalles de un proyecto |
todoist://labels | Todas las labels disponibles |
todoist://stats | Estadísticas de productividad |
Tools planeados
| Tool | Descripción |
|---|---|
list_tasks | Lista tareas con filtros (proyecto, label, prioridad) |
create_task | Crea una nueva tarea |
complete_task | Marca una tarea como completada |
update_task | Actualiza una tarea existente |
move_task | Mueve una tarea a otro proyecto |
search_tasks | Busca tareas por texto |
Prompts planeados
| Prompt | Descripción |
|---|---|
daily_plan | Genera plan del día basado en tareas pendientes |
project_status | Status report de un proyecto específico |
Consideraciones especiales para APIs externas
- Autenticación: Necesitas un API token. Pásalo como variable de entorno, nunca hardcodeado.
- Rate limiting: Respeta los límites de la API. Implementa retry con backoff.
- Latencia: Las requests a APIs externas son más lentas que database local. Considera caching.
- Disponibilidad: La API puede caer. Tu server debe manejar timeouts y errores de conexión gracefully.
- Paginación: Muchas APIs paginan resultados. Tu tool debe manejar páginas o establecer un límite razonable.
- Versionamiento: Las APIs cambian. Fija la versión de API que usas (e.g., header
X-API-Version).
Estructura de archivos: Opción A (ejemplo principal)
task-manager-mcp/
├── src/
│ ├── __init__.py
│ ├── server.py # Entry point — FastMCP instance y registros
│ ├── database.py # Conexión SQLite, setup de tablas, seed data
│ ├── models.py # Pydantic models para inputs y outputs
│ ├── tools/
│ │ ├── __init__.py
│ │ ├── tasks.py # Tools CRUD de tareas
│ │ ├── categories.py # Tools de categorías
│ │ └── queries.py # Tools de queries y reportes
│ └── resources/
│ ├── __init__.py
│ └── database.py # Resources de la database
├── tests/
│ ├── __init__.py
│ ├── conftest.py # Fixtures compartidos (database en memoria)
│ ├── test_tools_tasks.py # Tests de tools de tareas
│ ├── test_tools_categories.py
│ ├── test_tools_queries.py
│ ├── test_resources.py # Tests de resources
│ └── test_integration.py # Tests end-to-end
├── data/
│ └── tasks.db # Database SQLite (generada por el server)
├── requirements.txt
└── README.md
Por qué esta estructura
src/server.pycomo entry point: Un solo archivo donde se registra todo. Fácil de encontrar y modificar.src/database.pyseparado: La conexión y setup de la database es independiente de MCP. Puedes testear la database sin el server.src/models.pycentralizado: Todos los Pydantic models en un solo lugar. Evita imports circulares.src/tools/en directorio propio: Cada grupo de tools en su archivo. Cuando tienes 8+ tools, un solo archivo se vuelve difícil de navegar.src/resources/separado de tools: Resources y tools tienen responsabilidades distintas. Separarlos hace explícita la diferencia.tests/conftest.pycon fixtures: La database de testing (en memoria) se configura una vez y se reutiliza en todos los tests.data/para la database: La database SQLite no va ensrc/. Es un artefacto de datos, no código.
Archivos que no deberían existir
src/utils.py— Demasiado genérico. Si necesitas utilidades, nómbralas por lo que hacen.src/helpers.py— Mismo problema. ¿Helper de qué?src/config.py— Para un proyecto de este tamaño, las configuraciones van en variables de entorno o enserver.py.
Diseño de Pydantic Models
Los models son el contrato entre Claude Code y tu server. Diseñarlos bien ahora te ahorra problemas después.
Models de input (para tools)
from pydantic import BaseModel, Field
from typing import Optional
from enum import Enum
class TaskStatus(str, Enum):
PENDING = "pending"
IN_PROGRESS = "in_progress"
COMPLETED = "completed"
CANCELLED = "cancelled"
class TaskPriority(str, Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
class CreateTaskInput(BaseModel):
title: str = Field(min_length=1, max_length=200, description="Título de la tarea")
description: Optional[str] = Field(default=None, max_length=2000, description="Descripción detallada")
status: TaskStatus = Field(default=TaskStatus.PENDING, description="Estado de la tarea")
priority: TaskPriority = Field(default=TaskPriority.MEDIUM, description="Prioridad: low, medium, high, critical")
category_id: Optional[int] = Field(default=None, description="ID de la categoría")
due_date: Optional[str] = Field(default=None, description="Fecha límite en formato YYYY-MM-DD")
tags: Optional[list[str]] = Field(default=None, description="Lista de tags para la tarea")
class UpdateTaskInput(BaseModel):
task_id: int = Field(description="ID de la tarea a actualizar")
title: Optional[str] = Field(default=None, min_length=1, max_length=200)
description: Optional[str] = Field(default=None, max_length=2000)
status: Optional[TaskStatus] = Field(default=None)
priority: Optional[TaskPriority] = Field(default=None)
category_id: Optional[int] = Field(default=None)
due_date: Optional[str] = Field(default=None)
class ListTasksInput(BaseModel):
status: Optional[TaskStatus] = Field(default=None, description="Filtrar por estado")
priority: Optional[TaskPriority] = Field(default=None, description="Filtrar por prioridad")
category_id: Optional[int] = Field(default=None, description="Filtrar por categoría")
limit: int = Field(default=20, ge=1, le=100, description="Máximo de resultados")
class SearchTasksInput(BaseModel):
query: str = Field(min_length=1, description="Texto a buscar")
search_in: str = Field(default="both", description="Buscar en: title, description, both")
class CreateCategoryInput(BaseModel):
name: str = Field(min_length=1, max_length=50, description="Nombre de la categoría")
description: Optional[str] = Field(default=None, max_length=200)
color: str = Field(default="#6B7280", pattern=r"^#[0-9A-Fa-f]{6}$", description="Color hex, e.g. '#FF5733'")
class RunQueryInput(BaseModel):
sql: str = Field(min_length=1, description="Query SQL (solo SELECT permitido)")
class TaskSummaryInput(BaseModel):
period: str = Field(description="Periodo: today, week, month")
Principios de diseño de los models
-
Cada campo tiene
description— Claude Code usa estas descripciones para entender qué enviar. Una description clara = menos errores del modelo. -
Constraints explícitos —
min_length,max_length,ge,le,pattern. Validan automáticamente antes de que tu código toque la database. -
Defaults razonables —
statusdefaultpending,prioritydefaultmedium,limitdefault20. Claude Code no necesita especificar todo siempre. -
Enums para valores fijos —
TaskStatusyTaskPrioritycomo Enums. Claude Code no puede enviar un status inválido. -
Optional para campos editables — En
UpdateTaskInput, todo es Optional exceptotask_id. Solo actualizas lo que envías.
Diseño de error handling
Define cómo tu server maneja errores antes de implementar. Los errores caen en categorías predecibles:
Categorías de errores
| Categoría | Ejemplo | Cómo manejar |
|---|---|---|
| Not found | Tarea con ID 999 no existe | Retornar mensaje claro: "Tarea con ID 999 no encontrada" |
| Validation | Priority "super_high" no es válida | Pydantic lo maneja automáticamente. Retorna error descriptivo. |
| Database | SQLite file corrupto o locked | Catch sqlite3.Error, retornar mensaje genérico + logging |
| Query | SQL syntax error en run_query | Catch sqlite3.OperationalError, retornar el error de SQLite |
| Permission | Query con DELETE/DROP en run_query | Detectar antes de ejecutar, retornar "Solo queries SELECT permitidas" |
| Connection | API externa no responde (Opción C) | Timeout + retry + mensaje: "API no disponible, intenta más tarde" |
Patrón de error consistente
Todos los errores retornan el mismo formato JSON:
import json
def error_response(error_type: str, message: str, details: str = "") -> str:
result = {
"error": True,
"error_type": error_type,
"message": message,
}
if details:
result["details"] = details
return json.dumps(result, ensure_ascii=False)
Ejemplo de uso:
async def delete_task(task_id: int) -> str:
conn = get_connection()
cursor = conn.execute("SELECT id FROM tasks WHERE id = ?", (task_id,))
if not cursor.fetchone():
return error_response("not_found", f"Tarea con ID {task_id} no encontrada")
conn.execute("DELETE FROM tasks WHERE id = ?", (task_id,))
conn.commit()
return json.dumps({"deleted": True, "task_id": task_id})
Claude Code recibe errores claros que puede comunicar al usuario. "Tarea con ID 999 no encontrada" es infinitamente mejor que un stack trace de Python.
Diseño de tool descriptions
Las descripciones de tus tools son más importantes de lo que parecen. Claude Code las lee para decidir cuándo usar cada tool. Una descripción vaga = Claude Code usa el tool incorrectamente o no lo usa cuando debería.
Buenas vs malas descripciones
# ❌ Malo: demasiado vago
async def list_tasks() -> str:
"""Lista tareas."""
...
# ❌ Malo: demasiado técnico
async def list_tasks(input: ListTasksInput) -> str:
"""Ejecuta SELECT * FROM tasks con filtros WHERE opcionales sobre status, priority y category_id."""
...
# ✅ Bueno: claro y orientado al uso
async def list_tasks(input: ListTasksInput) -> str:
"""Lista tareas con filtros opcionales por estado, prioridad y categoría.
Sin filtros retorna las 20 tareas más recientes. Usa status='completed'
para ver solo las terminadas, priority='high' para las urgentes.
"""
...
Principios para descriptions
- Primera frase = qué hace el tool. Claude Code a menudo solo lee la primera línea.
- Segunda frase = cómo usarlo. Ejemplo de parámetros comunes.
- No expliques la implementación. Claude Code no necesita saber que usas SQLite por debajo.
- Menciona los filtros disponibles. Si el tool acepta filtros, listar los más útiles.
- Menciona edge cases. "Si no se encuentra, retorna un mensaje de error."
Aplica lo mismo a resources — el description que pasas al decorador @mcp.resource() le dice a Claude Code qué datos contiene.
Diseño de URIs para resources
Los URIs de resources deben ser predecibles y descriptivos. Sigue estas convenciones:
Convenciones
{prefix}://{entity} → Lista o resumen de la entidad
{prefix}://{entity}/{id} → Entidad específica por ID
{prefix}://{entity}/{id}/{sub-entity} → Sub-entidad de una entidad
{prefix}://{metadata-type} → Metadata del sistema
Ejemplos para Task Manager
taskdb://tables → Lista de tablas
taskdb://table/tasks/schema → Schema de la tabla tasks
taskdb://table/categories/schema → Schema de la tabla categories
taskdb://stats → Estadísticas generales
taskdb://tasks/overdue → Tareas vencidas (resource derivado)
taskdb://categories → Lista de categorías
Anti-patrones de URIs
taskdb://getTaskById/5 → No uses verbos en URIs de resources
taskdb://all-the-tables → No uses nombres ambiguos
taskdb://data → Demasiado genérico
taskdb://tasks?status=pending → Resources no tienen query params; usa un tool
Decisiones de diseño para documentar
Antes de implementar, documenta tus decisiones. Estas preguntas te ayudan a pensar en los trade-offs:
Checklist de decisiones
-
¿Qué operaciones exponer como tools vs resources?
- Tools: operaciones parametrizadas o con side effects
- Resources: datos de contexto que Claude Code lee para entender la situación
-
¿El tool
run_querypermite queries arbitrarias?- Sí, pero solo SELECT. Rechaza INSERT/UPDATE/DELETE/DROP.
- Trade-off: flexibilidad vs seguridad. Para un server local es aceptable.
-
¿Los tools retornan el objeto completo o solo confirmación?
- Objeto completo después de create/update. Claude Code necesita ver qué se creó/cambió.
- Solo confirmación después de delete (el objeto ya no existe).
-
¿Cómo manejar la database en tests?
- Database en memoria (
sqlite3.connect(":memory:")) para tests. - Database en archivo para el server real.
- Fixture de pytest que crea y destruye la database por cada test.
- Database en memoria (
-
¿Seed data incluida?
- Sí. El server crea tablas y opcionalmente carga datos de ejemplo.
- Útil para demos y para que el usuario vea datos inmediatamente.
Tu turno: diseña tu server
Si estás siguiendo con la Opción A (SQLite + Python), los diseños de esta cápsula son tu punto de partida. Puedes usarlos tal cual o modificarlos para tu dominio.
Si elegiste Opción B o C, usa los frameworks de diseño de arriba para crear tu propio plan:
Template de diseño
Copia y completa para tu proyecto:
## Mi MCP Server: [Nombre]
### Dominio
[Describe qué datos maneja y qué problema resuelve]
### Stack
- Lenguaje: [Python / TypeScript]
- Data source: [SQLite / File system / API externa]
- Validación: [Pydantic / Zod]
- Testing: [pytest / Vitest]
### Resources (mínimo 3)
| URI | Descripción | Qué retorna |
|-----|-------------|-------------|
| | | |
### Tools (mínimo 5)
| Tool | Descripción | Inputs | Side effects |
|------|-------------|--------|--------------|
| | | | |
### Prompts (mínimo 2)
| Prompt | Descripción | Parámetros |
|--------|-------------|------------|
| | | |
### Estructura de archivos
[Árbol de directorios]
### Decisiones de diseño
1. [Decisión] → [Razón]
2. [Decisión] → [Razón]
3. [Decisión] → [Razón]
Crear la estructura de archivos
Una vez que tienes el diseño, crea la estructura vacía. Para Opción A:
mkdir task-manager-mcp
cd task-manager-mcp
python -m venv .venv
source .venv/bin/activate # Linux/macOS
# .venv\Scripts\activate # Windows
pip install "mcp[cli]" pydantic pytest pytest-asyncio
mkdir -p src/tools src/resources tests data
touch src/__init__.py
touch src/server.py
touch src/database.py
touch src/models.py
touch src/tools/__init__.py
touch src/tools/tasks.py
touch src/tools/categories.py
touch src/tools/queries.py
touch src/resources/__init__.py
touch src/resources/database.py
touch tests/__init__.py
touch tests/conftest.py
touch tests/test_tools_tasks.py
touch tests/test_tools_categories.py
touch tests/test_tools_queries.py
touch tests/test_resources.py
touch tests/test_integration.py
requirements.txt
mcp[cli]>=1.0.0
pydantic>=2.0.0
pytest>=8.0.0
pytest-asyncio>=0.23.0
.gitignore
.venv/
__pycache__/
*.pyc
data/tasks.db
.pytest_cache/
*.egg-info/
dist/
build/
No versiones la database (data/tasks.db) — se genera automáticamente. No versiones el virtualenv ni los caches de Python. Si tu proyecto usa API tokens, nunca los incluyas en el repo — usa .env y agrégalo a .gitignore.
Verificar que el entorno funciona
python -c "import mcp; print(f'MCP SDK version: {mcp.__version__}')"
python -c "import pydantic; print(f'Pydantic version: {pydantic.__version__}')"
pytest --version
Si los tres comandos ejecutan sin error, tu entorno está listo.
Milestone de esta cápsula
Al terminar esta cápsula, deberías tener:
- Dominio definido — Sabes exactamente qué datos y operaciones maneja tu server
- Resources planeados — Lista de al menos 3 resources con URIs definidos
- Tools planeados — Lista de al menos 5 tools con inputs definidos
- Prompts planeados — Lista de al menos 2 prompts con parámetros
- Pydantic/Zod models diseñados — Schemas de validación para cada tool input
- Estructura de archivos creada — Todos los archivos/directorios existen (vacíos)
- Entorno configurado — SDK, dependencias, y testing framework instalados
- Decisiones documentadas — Sabes por qué elegiste cada resource, tool, y prompt
Si todo esto está check, estás listo para implementar. Cápsula 03 es donde el código toma forma.
Errores comunes en la fase de diseño
Diseñar demasiados tools desde el inicio
Es tentador planear 15 tools antes de escribir código. El problema: la mitad de esos tools van a cambiar cuando empiezas a implementar. Planea los 5-8 core, implementa, y después añade si necesitas más.
Confundir resources con tools
Un error frecuente es crear un resource para algo que requiere parámetros variables. Si Claude Code necesita enviar un status filter, es un tool, no un resource. Los resources son para datos de contexto con URIs fijos o con templates simples como {table_name}.
Olvidar las descriptions
Dejar descriptions vacías o con un solo "Lista tareas" es como nombrar una función doStuff(). Claude Code necesita descriptions detalladas para decidir cuándo usar cada tool. Invierte tiempo en escribirlas bien ahora — te ahorra debugging después.
No diseñar el error handling
"Ya lo manejo después" es la frase que precede a stack traces en producción. Define el patrón de errores ahora (formato JSON consistente, categorías predecibles) y todos tus tools lo siguen.
Hacer el schema demasiado complejo
Para un proyecto de 4 horas, 3-4 tablas es ideal. Más de eso y pasas más tiempo en la database que en MCP. Recuerda: el objetivo es demostrar MCP, no diseñar un schema enterprise.
Troubleshooting
"No sé qué dominio elegir"
Piensa en una tarea que haces semanalmente con datos. ¿Gestionas tareas? ¿Llevas un inventario? ¿Organizas notas? Cualquiera de esas funciona. Si realmente no tienes preferencia, usa el Task Manager de esta cápsula.
"¿Necesito exactamente la estructura de archivos que se muestra?"
No. La estructura es una sugerencia probada. Si prefieres poner todo en un solo archivo para empezar, hazlo. Puedes refactorizar después. Lo importante es que funcione.
"¿Puedo agregar más tools/resources después?"
Sí. El diseño no es final. Implementa el mínimo primero (3 resources, 5 tools, 2 prompts). Si te sobra tiempo y quieres agregar más, hazlo. La rúbrica da puntos extra por ir más allá del mínimo.
"¿Cómo decido si algo es resource o tool?"
Pregúntate: "¿Claude Code necesita parámetros para obtener esto?" Si sí, probablemente es un tool. Si no, es un resource. Otra pregunta: "¿Esto modifica datos?" Si sí, es un tool obligatoriamente.
"¿Los prompts son obligatorios?"
Sí, mínimo 2. Los prompts demuestran que entiendes la tercera primitiva. Además, son extremadamente útiles en la práctica — convierten interacciones de 3 pasos en 1.
"¿Puedo usar PostgreSQL en vez de SQLite?"
Puedes, pero agrega complejidad (instalación, conexión, credenciales). SQLite viene incluido con Python y no necesita setup. Para este proyecto, SQLite es la opción recomendada. Si ya tienes PostgreSQL configurado y lo prefieres, adelante.
Resumen
- Diseñaste la arquitectura completa del MCP server: database schema, models, y estructura de carpetas
- El schema usa 3 tablas relacionadas: tasks, categories, y task_tags
- Los modelos Pydantic definen contratos claros para entrada y salida de datos
- Planificaste las 3 primitivas: Resources (lectura de datos), Tools (CRUD con side effects), Prompts (templates reutilizables)
- La estructura de carpetas separa responsabilidades:
database/,tools/,resources/,prompts/ - El diseño prioriza claridad sobre optimización — un server que es fácil de entender y mantener
Recursos
- SQLite Documentation — Referencia completa de SQL y tipos de datos
- Pydantic v2 — Fields — Documentación de Field constraints
- MCP Resources Specification — Especificación oficial de Resources
- MCP Tools Specification — Especificación oficial de Tools
- MCP Prompts Specification — Especificación oficial de Prompts
- Python Enums — Enums para status y priority
- Zod Documentation — Validación para TypeScript (Opción B)
- MCP URI Design — Convenciones de URIs