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:

  1. ¿Qué datos maneja? — Tareas, productos, usuarios, archivos, repos, issues...
  2. ¿Qué operaciones necesitas? — Crear, leer, actualizar, borrar, buscar, reportar, analizar...
  3. ¿Qué preguntas haces frecuentemente? — "¿Cuántas tareas pendientes hay?", "¿Cuál es el producto más vendido?", "¿Qué archivos se modificaron ayer?"
  4. ¿Qué datos debería Claude Code poder ver sin que le pidas explícitamente? — Estos son tus resources.
  5. ¿Qué acciones debería Claude Code poder ejecutar cuando se lo pides? — Estos son tus tools.
  6. ¿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:

NecesidadPrimitivaPor qué
"Ver qué tablas existen"ResourceDato estático/de consulta, sin side effects
"Ver el schema de una tabla"ResourceMetadata que Claude Code necesita para entender la estructura
"Leer los registros de una tabla"ResourceAcceso de lectura a datos
"Crear un registro nuevo"ToolOperación con side effects (modifica la database)
"Actualizar un registro"ToolOperación con side effects
"Borrar un registro"ToolOperación con side effects
"Ejecutar una query SQL custom"ToolOperación flexible que puede tener side effects
"Buscar registros por criterio"ToolOperación parametrizada
"Analizar una tabla y generar reporte"PromptInteracción multi-paso que combina resources y tools
"Generar un resumen semanal"PromptTemplate 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:

  • categories y tags dan relaciones 1:N y M:N, demostrando queries con JOINs
  • status y priority con CHECK constraints demuestran validación a nivel de database
  • due_date permite filtros temporales ("tareas vencidas", "tareas de esta semana")
  • Es suficiente para 5+ tools interesantes sin ser abrumador

Resources planeados

URIDescripciónQué retorna
taskdb://tablesLista de tablas en la databaseNombres y conteo de registros
taskdb://table/{name}/schemaSchema de una tabla específicaColumnas, tipos, constraints
taskdb://statsEstadísticas generalesConteo por status, categoría, prioridad
taskdb://tasks/overdueTareas vencidasTareas con due_date pasado y status != completed
taskdb://categoriesTodas las categoríasLista con conteo de tareas por categoría

Decisiones de diseño:

  • Los URIs usan el prefijo taskdb:// para namespacing claro
  • taskdb://tables es el punto de entrada — Claude Code lo lee primero para entender la estructura
  • taskdb://stats da un resumen rápido sin necesidad de ejecutar queries
  • taskdb://tasks/overdue es un resource derivado — datos calculados, no un table dump

Tools planeados

ToolDescripciónInputsSide effects
create_taskCrear una nueva tareatitle, description?, category_id?, priority?, due_date?, tags?INSERT en tasks + task_tags
list_tasksListar tareas con filtrosstatus?, priority?, category_id?, limit?Ninguno (solo lectura)
update_taskActualizar campos de una tareatask_id, campos a actualizarUPDATE en tasks
delete_taskEliminar una tareatask_idDELETE en tasks + task_tags
search_tasksBuscar tareas por textoquery, search_in? (title/description/both)Ninguno
create_categoryCrear una categoríaname, description?, color?INSERT en categories
run_queryEjecutar query SQL customsql (solo SELECT)Ninguno (solo lectura)
get_task_summaryResumen por periodoperiod (today/week/month)Ninguno

Decisiones de diseño:

  • list_tasks es un tool porque tiene filtros complejos que necesitan parámetros
  • run_query permite queries arbitrarias pero restringidas a SELECT (sin modificaciones)
  • get_task_summary podrí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

PromptDescripciónParámetros
analyze_tableAnaliza una tabla y sugiere mejorastable_name
weekly_reportGenera reporte de tareas de la semanaweek_start? (default: esta semana)
optimize_queryAnaliza y optimiza una query SQLsql_query

Decisiones de diseño:

  • analyze_table combina resources (schema, datos) con análisis del modelo
  • weekly_report estandariza una interacción que harías regularmente
  • optimize_query es 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

URIDescripción
project://infoMetadata del proyecto (nombre, tipo, tamaño)
project://structureÁrbol de directorios
project://dependenciesLista de dependencias (package.json / requirements.txt)
project://statsMétricas: líneas de código, archivos por tipo, tamaño total

Tools planeados

ToolDescripción
list_directoryLista contenido de un directorio con filtros
read_fileLee contenido de un archivo (con límite de tamaño)
search_contentBusca texto en archivos del proyecto
analyze_fileMétricas de un archivo: LOC, complejidad, imports
find_unused_filesDetecta archivos que no se importan en ningún lado
generate_treeGenera un árbol visual del proyecto

Prompts planeados

PromptDescripción
project_reviewAnálisis completo de un directorio de proyecto
dependency_auditRevisa 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

URIDescripción
todoist://projectsLista de proyectos
todoist://project/{id}Detalles de un proyecto
todoist://labelsTodas las labels disponibles
todoist://statsEstadísticas de productividad

Tools planeados

ToolDescripción
list_tasksLista tareas con filtros (proyecto, label, prioridad)
create_taskCrea una nueva tarea
complete_taskMarca una tarea como completada
update_taskActualiza una tarea existente
move_taskMueve una tarea a otro proyecto
search_tasksBusca tareas por texto

Prompts planeados

PromptDescripción
daily_planGenera plan del día basado en tareas pendientes
project_statusStatus report de un proyecto específico

Consideraciones especiales para APIs externas

  1. Autenticación: Necesitas un API token. Pásalo como variable de entorno, nunca hardcodeado.
  2. Rate limiting: Respeta los límites de la API. Implementa retry con backoff.
  3. Latencia: Las requests a APIs externas son más lentas que database local. Considera caching.
  4. Disponibilidad: La API puede caer. Tu server debe manejar timeouts y errores de conexión gracefully.
  5. Paginación: Muchas APIs paginan resultados. Tu tool debe manejar páginas o establecer un límite razonable.
  6. 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.py como entry point: Un solo archivo donde se registra todo. Fácil de encontrar y modificar.
  • src/database.py separado: La conexión y setup de la database es independiente de MCP. Puedes testear la database sin el server.
  • src/models.py centralizado: 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.py con 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 en src/. 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 en server.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

  1. Cada campo tiene description — Claude Code usa estas descripciones para entender qué enviar. Una description clara = menos errores del modelo.

  2. Constraints explícitos — min_length, max_length, ge, le, pattern. Validan automáticamente antes de que tu código toque la database.

  3. Defaults razonables — status default pending, priority default medium, limit default 20. Claude Code no necesita especificar todo siempre.

  4. Enums para valores fijos — TaskStatus y TaskPriority como Enums. Claude Code no puede enviar un status inválido.

  5. Optional para campos editables — En UpdateTaskInput, todo es Optional excepto task_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íaEjemploCómo manejar
Not foundTarea con ID 999 no existeRetornar mensaje claro: "Tarea con ID 999 no encontrada"
ValidationPriority "super_high" no es válidaPydantic lo maneja automáticamente. Retorna error descriptivo.
DatabaseSQLite file corrupto o lockedCatch sqlite3.Error, retornar mensaje genérico + logging
QuerySQL syntax error en run_queryCatch sqlite3.OperationalError, retornar el error de SQLite
PermissionQuery con DELETE/DROP en run_queryDetectar antes de ejecutar, retornar "Solo queries SELECT permitidas"
ConnectionAPI 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

  1. Primera frase = qué hace el tool. Claude Code a menudo solo lee la primera línea.
  2. Segunda frase = cómo usarlo. Ejemplo de parámetros comunes.
  3. No expliques la implementación. Claude Code no necesita saber que usas SQLite por debajo.
  4. Menciona los filtros disponibles. Si el tool acepta filtros, listar los más útiles.
  5. 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

  1. ¿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
  2. ¿El tool run_query permite queries arbitrarias?

    • Sí, pero solo SELECT. Rechaza INSERT/UPDATE/DELETE/DROP.
    • Trade-off: flexibilidad vs seguridad. Para un server local es aceptable.
  3. ¿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).
  4. ¿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.
  5. ¿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

  1. SQLite Documentation — Referencia completa de SQL y tipos de datos
  2. Pydantic v2 — Fields — Documentación de Field constraints
  3. MCP Resources Specification — Especificación oficial de Resources
  4. MCP Tools Specification — Especificación oficial de Tools
  5. MCP Prompts Specification — Especificación oficial de Prompts
  6. Python Enums — Enums para status y priority
  7. Zod Documentation — Validación para TypeScript (Opción B)
  8. MCP URI Design — Convenciones de URIs