Módulo 6: MCP Apps y UI Interactivo

Forms y Workflows Interactivos via MCP

Forms y Workflows Interactivos via MCP

Descripción de la cápsula

Los dashboards de la cápsula anterior son output — el tool retorna información que el developer lee. Pero una MCP App completa también necesita input — capturar datos del usuario, confirmar acciones, y guiar flujos multi-paso. Eso es lo que cubre esta cápsula.

En MCP, no hay un concepto de "form" como en una web app con inputs HTML y botones de submit. Lo que hay son patrones para capturar información del usuario de forma estructurada usando las primitivas que ya conoces: tools con parámetros bien diseñados y prompts que guían la interacción. La combinación de ambos crea flujos que se sienten interactivos, aunque técnicamente son secuencias de llamadas a tools.

Al terminar esta cápsula, sabrás diseñar workflows multi-paso, implementar confirmaciones para acciones destructivas, y combinar tools con prompts para crear experiencias de usuario fluidas dentro de Claude Code.


El concepto: interactividad en MCP

Cómo funciona la interacción en Claude Code

Cuando usas Claude Code, la interacción es conversacional:

Tú: "Necesito configurar un nuevo microservicio"
Claude Code: [usa tool setup_service con los parámetros que infiere]
Claude Code: "He creado la estructura. ¿Quieres que configure la base de datos también?"
Tú: "Sí, PostgreSQL con las tablas de usuarios y productos"
Claude Code: [usa tool configure_db con los parámetros]

La "interactividad" no viene de widgets de UI — viene de un diseño inteligente de tools que permite a Claude Code:

  1. Inferir parámetros del contexto de la conversación
  2. Pedir confirmación antes de acciones irreversibles
  3. Encadenar tools en secuencias lógicas
  4. Retornar resultados que sugieren el siguiente paso

Tres niveles de interactividad

Nivel 1: Tool con parámetros (ya lo sabes)
  El usuario pide algo → Claude Code invoca el tool → resultado

Nivel 2: Tool con confirmación
  Claude Code prepara la acción → muestra preview → pide confirmación → ejecuta

Nivel 3: Workflow multi-paso
  Tool 1 (recopilar) → Tool 2 (validar) → Tool 3 (preview) → Tool 4 (ejecutar)

Patrón 1: Confirmación antes de ejecutar

El patrón más común en MCP Apps: el tool tiene un modo "preview" (dryRun) que muestra qué va a pasar, y un modo "execute" que lo hace. Claude Code puede mostrar el preview y pedir confirmación antes de ejecutar.

TypeScript

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import * as fs from "fs/promises";
import * as path from "path";

const server = new McpServer({ name: "safe-ops", version: "1.0.0" });

server.tool(
  "batch_rename",
  "Renombra archivos en un directorio. Por defecto muestra un preview sin ejecutar. Usa execute=true para aplicar los cambios.",
  {
    directory: z.string().describe("Directorio con los archivos"),
    find: z.string().min(1).describe("Texto a buscar en los nombres"),
    replace: z.string().describe("Texto de reemplazo"),
    execute: z.boolean().default(false)
      .describe("false = preview, true = ejecutar los cambios"),
  },
  async ({ directory, find, replace, execute }) => {
    let entries: string[];
    try {
      entries = await fs.readdir(directory);
    } catch {
      return {
        content: [{ type: "text" as const, text: `Error: no se pudo leer '${directory}'` }],
        isError: true,
      };
    }

    const changes = entries
      .filter(name => name.includes(find))
      .map(name => ({ from: name, to: name.replace(find, replace) }));

    if (changes.length === 0) {
      return {
        content: [{
          type: "text" as const,
          text: `## Batch Rename\n\nNo se encontraron archivos con '${find}' en \`${directory}\``,
        }],
      };
    }

    if (!execute) {
      let preview = `## 📋 Preview — Batch Rename\n\n`;
      preview += `**Directorio:** \`${directory}\`\n`;
      preview += `**Patrón:** "${find}" → "${replace}"\n`;
      preview += `**Archivos afectados:** ${changes.length}\n\n`;
      preview += `| # | Nombre actual | Nuevo nombre |\n`;
      preview += `|---|--------------|-------------|\n`;
      changes.forEach((c, i) => {
        preview += `| ${i + 1} | \`${c.from}\` | \`${c.to}\` |\n`;
      });
      preview += `\n---\n\n⚠️ **Esto es un preview.** Para ejecutar, usa \`execute: true\`.`;
      return { content: [{ type: "text" as const, text: preview }] };
    }

    const results: { from: string; to: string; status: string }[] = [];
    for (const { from, to } of changes) {
      try {
        await fs.rename(path.join(directory, from), path.join(directory, to));
        results.push({ from, to, status: "✅" });
      } catch (e) {
        results.push({ from, to, status: `❌ ${e instanceof Error ? e.message : "error"}` });
      }
    }

    const success = results.filter(r => r.status === "✅").length;
    let report = `## ✅ Batch Rename — Ejecutado\n\n`;
    report += `**Resultado:** ${success}/${changes.length} archivos renombrados\n\n`;
    report += `| Archivo | Nuevo nombre | Status |\n`;
    report += `|---------|-------------|--------|\n`;
    for (const r of results) {
      report += `| \`${r.from}\` | \`${r.to}\` | ${r.status} |\n`;
    }
    return { content: [{ type: "text" as const, text: report }] };
  }
);

Claude Code usa esto naturalmente: primero llama el tool con execute: false, muestra el preview al usuario, y si el usuario confirma, llama de nuevo con execute: true.


Patrón 2: Workflow multi-paso con estado

Un workflow más complejo que guía al usuario a través de un proceso de configuración:

Python

import json
import os
from datetime import datetime
from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel, Field

mcp = FastMCP("project-scaffolder")

active_sessions: dict[str, dict] = {}


class ProjectConfig(BaseModel):
    name: str = Field(min_length=1, max_length=50, description="Nombre del proyecto")
    language: str = Field(description="Lenguaje: python, typescript, o rust")
    features: list[str] = Field(default_factory=list, description="Features: api, db, auth, tests, docker")
    description: str = Field(default="", description="Descripción del proyecto")


@mcp.tool()
async def start_project_setup(name: str, language: str) -> str:
    """Inicia el setup de un nuevo proyecto. Retorna las opciones disponibles."""
    if language not in ("python", "typescript", "rust"):
        return f"Error: lenguaje '{language}' no soportado. Opciones: python, typescript, rust"

    session_id = f"{name}-{datetime.now().strftime('%H%M%S')}"
    active_sessions[session_id] = {
        "name": name,
        "language": language,
        "features": [],
        "step": "features",
    }

    features_by_lang = {
        "python": ["api (FastAPI)", "db (SQLAlchemy)", "auth (JWT)", "tests (pytest)", "docker"],
        "typescript": ["api (Express)", "db (Prisma)", "auth (Passport)", "tests (Vitest)", "docker"],
        "rust": ["api (Actix)", "db (Diesel)", "auth (JWT)", "tests (cargo test)", "docker"],
    }

    features = features_by_lang[language]

    return f"""## 🚀 Setup de Proyecto — Paso 1/3

**Proyecto:** {name}
**Lenguaje:** {language}
**Session:** `{session_id}`

---

### Selecciona features

| # | Feature | Descripción |
|---|---------|-------------|
{chr(10).join(f"| {i+1} | {f} | Incluir {f.split(' ')[0]} |" for i, f in enumerate(features))}

---

**Siguiente paso:** Usa `configure_features` con el session_id `{session_id}` y la lista de features que quieres incluir.

Ejemplo: `configure_features(session_id="{session_id}", features=["api", "db", "tests"])`
"""


@mcp.tool()
async def configure_features(session_id: str, features: list[str]) -> str:
    """Configura las features del proyecto. Paso 2 del setup."""
    if session_id not in active_sessions:
        return f"Error: sesión '{session_id}' no encontrada. Usa `start_project_setup` primero."

    session = active_sessions[session_id]
    valid_features = {"api", "db", "auth", "tests", "docker"}
    invalid = [f for f in features if f not in valid_features]
    if invalid:
        return f"Error: features no válidas: {invalid}. Opciones: {sorted(valid_features)}"

    session["features"] = features
    session["step"] = "confirm"

    feature_icons = {"api": "🌐", "db": "💾", "auth": "🔐", "tests": "🧪", "docker": "🐳"}

    structure_lines = [f"  {session['name']}/"]
    if "api" in features: structure_lines.append("  ├── src/api/")
    if "db" in features: structure_lines.append("  ├── src/db/")
    if "auth" in features: structure_lines.append("  ├── src/auth/")
    if "tests" in features: structure_lines.append("  ├── tests/")
    if "docker" in features: structure_lines.append("  ├── Dockerfile")
    structure_lines.append("  ├── README.md")
    if session["language"] == "python": structure_lines.append("  └── requirements.txt")
    elif session["language"] == "typescript": structure_lines.append("  └── package.json")
    else: structure_lines.append("  └── Cargo.toml")

    return f"""## 🚀 Setup de Proyecto — Paso 2/3

**Proyecto:** {session["name"]}
**Lenguaje:** {session["language"]}
**Features:** {" ".join(feature_icons.get(f, "📦") + " " + f for f in features)}

---

### Estructura propuesta

{chr(10).join(structure_lines)}


---

### Archivos que se crearán

| Archivo | Propósito |
|---------|-----------|
| `README.md` | Documentación del proyecto |
{"| `src/api/` | Endpoints de la API |" + chr(10) if "api" in features else ""}{"| `src/db/` | Modelos y migraciones |" + chr(10) if "db" in features else ""}{"| `src/auth/` | Autenticación |" + chr(10) if "auth" in features else ""}{"| `tests/` | Test suite |" + chr(10) if "tests" in features else ""}{"| `Dockerfile` | Containerización |" + chr(10) if "docker" in features else ""}
---

**Siguiente paso:** Usa `execute_setup(session_id="{session_id}")` para crear el proyecto, o `start_project_setup` para empezar de nuevo.
"""


@mcp.tool()
async def execute_setup(session_id: str, target_dir: str = ".") -> str:
    """Ejecuta el setup del proyecto. Paso 3 (final)."""
    if session_id not in active_sessions:
        return f"Error: sesión '{session_id}' no encontrada."

    session = active_sessions[session_id]
    if session["step"] != "confirm":
        return "Error: debes configurar features primero (paso 2)."

    project_dir = os.path.join(target_dir, session["name"])
    created_files: list[str] = []
    errors: list[str] = []

    try:
        os.makedirs(project_dir, exist_ok=True)
        created_files.append(f"{session['name']}/")

        for feature in session["features"]:
            dir_name = f"src/{feature}" if feature != "docker" else ""
            if dir_name:
                os.makedirs(os.path.join(project_dir, dir_name), exist_ok=True)
                created_files.append(f"{dir_name}/")

        readme = f"# {session['name']}\n\n{session['language'].title()} project with {', '.join(session['features'])}.\n"
        with open(os.path.join(project_dir, "README.md"), "w") as f:
            f.write(readme)
        created_files.append("README.md")

        if "docker" in session["features"]:
            dockerfile = f"FROM {'python:3.12-slim' if session['language'] == 'python' else 'node:20-slim' if session['language'] == 'typescript' else 'rust:1.75-slim'}\nWORKDIR /app\nCOPY . .\n"
            with open(os.path.join(project_dir, "Dockerfile"), "w") as f:
                f.write(dockerfile)
            created_files.append("Dockerfile")

    except Exception as e:
        errors.append(str(e))

    del active_sessions[session_id]

    status = "✅ Completado" if not errors else "⚠️ Completado con errores"

    report = f"""## {status} — Proyecto Creado

**Proyecto:** `{project_dir}`
**Lenguaje:** {session["language"]}
**Features:** {", ".join(session["features"])}

---

### Archivos creados

"""
    for f in created_files:
        report += f"- ✅ `{f}`\n"
    for e in errors:
        report += f"- ❌ Error: {e}\n"

    report += f"\n---\n\n**Próximos pasos:** `cd {session['name']}` → instalar dependencias → configurar .env"

    return report


if __name__ == "__main__":
    mcp.run()

Lo que demuestra este ejemplo

  • Estado entre llamadas — active_sessions mantiene el contexto del workflow
  • Validación progresiva — cada paso valida antes de avanzar
  • Preview antes de ejecutar — paso 2 muestra la estructura antes de crearla
  • Instrucciones claras — cada respuesta indica el siguiente paso
  • Cleanup — la sesión se elimina al completar

Patrón 3: Prompts como templates de interacción

Los prompts MCP complementan los tools para crear flujos interactivos estandarizados:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("code-reviewer")


@mcp.prompt()
async def review_code(file_path: str, focus: str = "general") -> str:
    """Template para code review con foco configurable."""
    focus_instructions = {
        "general": "Revisa el código considerando claridad, mantenibilidad, y posibles bugs.",
        "security": "Enfócate en vulnerabilidades de seguridad: injection, auth, secrets, input validation.",
        "performance": "Enfócate en rendimiento: complejidad algorítmica, queries N+1, memory leaks.",
        "testing": "Enfócate en testabilidad: funciones puras, dependency injection, edge cases.",
    }

    instruction = focus_instructions.get(focus, focus_instructions["general"])

    return f"""Revisa el archivo `{file_path}`.

**Foco:** {focus}
**Instrucciones:** {instruction}

Por favor proporciona:
1. **Resumen** — qué hace el código en 2-3 frases
2. **Puntos positivos** — qué está bien implementado
3. **Issues encontrados** — problemas ordenados por severidad (🔴 crítico, 🟡 medio, 🟢 menor)
4. **Sugerencias** — mejoras concretas con ejemplos de código
5. **Veredicto** — ✅ aprobar, 🟡 aprobar con cambios menores, 🔴 requiere cambios

Formatea tu respuesta como un reporte de code review profesional."""


@mcp.prompt()
async def setup_env(project_type: str = "web") -> str:
    """Template para configurar un nuevo entorno de desarrollo."""
    return f"""Necesito configurar un entorno de desarrollo para un proyecto de tipo: {project_type}

Por favor:
1. Verifica qué herramientas tengo instaladas (node, python, docker, git)
2. Identifica qué falta
3. Sugiere los pasos de instalación para lo que falta
4. Crea archivos de configuración base si es necesario

Usa los tools disponibles para verificar el sistema y crear archivos."""

Los prompts estandarizan interacciones comunes. En lugar de que el developer escriba instrucciones cada vez, usa el prompt como template que Claude Code ejecuta con los tools disponibles.


Patrón 4: Captura de datos estructurada

Cuando necesitas que el usuario proporcione datos con un formato específico, diseña tools que guíen la entrada:

server.tool(
  "create_env_config",
  "Crea un archivo .env con las variables necesarias para el proyecto. Usa template para ver qué variables se necesitan, o generate para crear el archivo.",
  {
    action: z.enum(["template", "generate"]).describe("template = ver qué se necesita, generate = crear .env"),
    projectType: z.enum(["api", "fullstack", "worker"]).describe("Tipo de proyecto"),
    values: z.record(z.string()).optional()
      .describe("Valores para las variables de entorno (solo para action=generate)"),
  },
  async ({ action, projectType, values }) => {
    const templates: Record<string, { key: string; description: string; required: boolean; example: string }[]> = {
      api: [
        { key: "PORT", description: "Puerto del servidor", required: true, example: "3000" },
        { key: "DATABASE_URL", description: "Connection string de la DB", required: true, example: "postgresql://user:pass@localhost:5432/mydb" },
        { key: "JWT_SECRET", description: "Secret para tokens JWT", required: true, example: "your-secret-here" },
        { key: "LOG_LEVEL", description: "Nivel de logging", required: false, example: "info" },
        { key: "CORS_ORIGIN", description: "Origen permitido para CORS", required: false, example: "http://localhost:3001" },
      ],
      fullstack: [
        { key: "PORT", description: "Puerto del backend", required: true, example: "3000" },
        { key: "DATABASE_URL", description: "Connection string", required: true, example: "postgresql://localhost/mydb" },
        { key: "JWT_SECRET", description: "Secret para tokens", required: true, example: "change-me" },
        { key: "NEXT_PUBLIC_API_URL", description: "URL del API para el frontend", required: true, example: "http://localhost:3000/api" },
        { key: "REDIS_URL", description: "URL de Redis para cache", required: false, example: "redis://localhost:6379" },
      ],
      worker: [
        { key: "QUEUE_URL", description: "URL del message queue", required: true, example: "amqp://localhost" },
        { key: "DATABASE_URL", description: "Connection string", required: true, example: "postgresql://localhost/mydb" },
        { key: "CONCURRENCY", description: "Workers concurrentes", required: false, example: "4" },
        { key: "RETRY_ATTEMPTS", description: "Intentos de retry", required: false, example: "3" },
      ],
    };

    const vars = templates[projectType];

    if (action === "template") {
      let output = `## ⚙️ Variables de Entorno — ${projectType}\n\n`;
      output += `| Variable | Descripción | Requerida | Ejemplo |\n`;
      output += `|----------|-------------|-----------|----------|\n`;
      for (const v of vars) {
        output += `| \`${v.key}\` | ${v.description} | ${v.required ? "✅ Sí" : "No"} | \`${v.example}\` |\n`;
      }
      output += `\n---\n\n**Siguiente paso:** Usa este tool con \`action: "generate"\` y proporciona los valores en el parámetro \`values\`.\n`;
      output += `\nEjemplo:\n\`\`\`\nvalues: { "PORT": "3000", "DATABASE_URL": "postgresql://..." }\n\`\`\``;
      return { content: [{ type: "text" as const, text: output }] };
    }

    const missing = vars.filter(v => v.required && !values?.[v.key]);
    if (missing.length > 0) {
      let error = `## ❌ Faltan variables requeridas\n\n`;
      for (const v of missing) {
        error += `- \`${v.key}\` — ${v.description} (ejemplo: \`${v.example}\`)\n`;
      }
      return { content: [{ type: "text" as const, text: error }], isError: true };
    }

    let envContent = `# ${projectType} environment configuration\n`;
    envContent += `# Generated: ${new Date().toISOString()}\n\n`;
    for (const v of vars) {
      const value = values?.[v.key] || v.example;
      envContent += `# ${v.description}\n${v.key}=${value}\n\n`;
    }

    let report = `## ✅ Archivo .env generado\n\n\`\`\`env\n${envContent}\`\`\`\n\n`;
    report += `**Variables configuradas:** ${vars.length}\n`;
    report += `**⚠️ Recuerda:** No commits este archivo a git. Agrega \`.env\` a tu \`.gitignore\`.`;
    return { content: [{ type: "text" as const, text: report }] };
  }
);

Lo que demuestra este ejemplo

  • Dos modos en un solo tool: template para descubrir qué se necesita, generate para crear
  • Validación de campos requeridos — informa qué falta antes de generar
  • Ejemplos — cada variable tiene un ejemplo que guía al usuario
  • Output seguro — recuerda no commitear el .env

Combinando tools y prompts

La combinación más poderosa en MCP Apps es usar prompts para iniciar workflows que usan tools:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("deploy-assistant")


@mcp.prompt()
async def deploy_checklist(environment: str = "staging") -> str:
    """Checklist de deploy con verificaciones automáticas."""
    return f"""Ejecuta un checklist de deploy para el entorno: {environment}

Usa los siguientes tools en orden:
1. `check_tests()` — Verificar que todos los tests pasan
2. `check_dependencies()` — Verificar dependencias actualizadas
3. `check_env_vars(environment="{environment}")` — Verificar variables de entorno
4. `deploy_preview(environment="{environment}")` — Preview del deploy

Si algún paso falla, detente e informa el problema.
Presenta los resultados como un checklist con ✅/❌ por cada paso."""


@mcp.tool()
async def check_tests() -> str:
    """Verifica que los tests pasan."""
    return "## 🧪 Tests\n\n✅ Unit: 45/45 (2.3s) | ✅ Integration: 12/12 (8.1s)"


@mcp.tool()
async def check_dependencies() -> str:
    """Verifica el estado de las dependencias."""
    return "## 📦 Deps\n\n✅ 42 up-to-date | ⚠️ 3 minor updates | 🔴 0 vulnerabilidades"


@mcp.tool()
async def check_env_vars(environment: str = "staging") -> str:
    """Verifica variables de entorno."""
    return f"## ⚙️ Vars ({environment})\n\n✅ DATABASE_URL | ✅ JWT_SECRET | ✅ API_KEY | ⚠️ SENTRY_DSN (opcional)"


@mcp.tool()
async def deploy_preview(environment: str = "staging") -> str:
    """Preview del deploy."""
    return f"## 🚀 Preview ({environment})\n\nBranch: main | Commit: abc1234 | 5 archivos cambiados\n\n⚠️ **Preview.** Confirma para proceder."

if __name__ == "__main__":
    mcp.run()

Cuando el usuario invoca el prompt deploy_checklist, Claude Code ejecuta cada tool en secuencia y presenta un reporte consolidado — exactamente como un pipeline de CI/CD pero dentro de Claude Code.


Ejercicios

Ejercicio 1: Tool con preview/execute (Fácil)

Crea un tool cleanup_logs con parámetro execute (default: false). En preview, muestra los archivos .log que se eliminarían con su tamaño. En execute, los elimina.

Ver solución
import os
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("log-cleaner")


@mcp.tool()
async def cleanup_logs(directory: str, execute: bool = False) -> str:
    """Limpia archivos .log. Preview por defecto, execute=True para eliminar."""
    if not os.path.isdir(directory):
        return f"Error: '{directory}' no existe"

    log_files = []
    for f in os.listdir(directory):
        if f.endswith(".log"):
            path = os.path.join(directory, f)
            size = os.path.getsize(path)
            log_files.append({"name": f, "path": path, "size": size})

    if not log_files:
        return f"## 🧹 Log Cleanup\n\nNo se encontraron archivos .log en `{directory}`"

    total_size = sum(f["size"] for f in log_files)
    size_str = f"{total_size / 1024:.1f} KB" if total_size < 1024 * 1024 else f"{total_size / 1024 / 1024:.1f} MB"

    if not execute:
        lines = [f"## 🧹 Log Cleanup — Preview", "",
                 f"**Archivos:** {len(log_files)} | **Tamaño total:** {size_str}", "",
                 "| Archivo | Tamaño |", "|---------|--------|"]
        for f in sorted(log_files, key=lambda x: -x["size"]):
            s = f"{f['size'] / 1024:.1f} KB"
            lines.append(f"| `{f['name']}` | {s} |")
        lines.append(f"\n⚠️ **Preview.** Usa `execute=True` para eliminar.")
        return "\n".join(lines)

    deleted = 0
    for f in log_files:
        try:
            os.remove(f["path"])
            deleted += 1
        except OSError:
            pass
    return f"## ✅ Log Cleanup\n\n**Eliminados:** {deleted}/{len(log_files)} archivos | **Liberado:** {size_str}"

if __name__ == "__main__":
    mcp.run()

Ejercicio 2: Prompt de onboarding (Fácil)

Crea un prompt onboard_developer que guíe a un nuevo developer a configurar su entorno. El prompt debe indicar qué tools usar y en qué orden.

Ver solución
@mcp.prompt()
async def onboard_developer(name: str, role: str = "backend") -> str:
    """Guía de onboarding para un nuevo developer."""
    return f"""Bienvenido al equipo, {name}! Tu rol: {role}.

Por favor ejecuta los siguientes pasos usando los tools disponibles:

1. **Verificar sistema:** Revisa versiones de node, python, docker, git
2. **Clonar repos:** Lista los repositorios del equipo y clona los relevantes para {role}
3. **Configurar entorno:** Crea archivos .env con las variables necesarias
4. **Verificar acceso:** Confirma acceso a staging y bases de datos de desarrollo
5. **Ejecutar tests:** Corre la suite de tests para verificar que todo funciona

Presenta cada paso como un checklist:
- ✅ Completado correctamente
- ❌ Problema encontrado (con instrucciones para resolver)
- ⏭️ Pendiente

Al final, genera un resumen del estado del onboarding de {name}."""

Ejercicio 3: Workflow multi-paso para crear API endpoint (Medio)

Crea tres tools que formen un workflow: design_endpoint (define el endpoint con método, path, params), preview_endpoint (muestra el código que se generaría), create_endpoint (genera los archivos).

Ver solución
from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel, Field

mcp = FastMCP("endpoint-creator")

endpoint_sessions: dict[str, dict] = {}


class EndpointDesign(BaseModel):
    method: str = Field(description="HTTP method: GET, POST, PUT, DELETE")
    path: str = Field(description="Ruta del endpoint, e.g. /api/users")
    params: list[str] = Field(default_factory=list, description="Parámetros del endpoint")
    description: str = Field(default="", description="Descripción del endpoint")


@mcp.tool()
async def design_endpoint(method: str, path: str, params: list[str] = [], description: str = "") -> str:
    """Paso 1: Diseña un nuevo endpoint."""
    session_id = f"{method}-{path.replace('/', '-')}"
    endpoint_sessions[session_id] = {
        "method": method.upper(), "path": path,
        "params": params, "description": description,
    }
    return f"""## 📐 Endpoint Design

| Campo | Valor |
|-------|-------|
| Method | `{method.upper()}` |
| Path | `{path}` |
| Params | {', '.join(f'`{p}`' for p in params) or 'ninguno'} |
| Descripción | {description or 'N/A'} |

**Siguiente:** `preview_endpoint(session_id="{session_id}")`"""


@mcp.tool()
async def preview_endpoint(session_id: str) -> str:
    """Paso 2: Preview del código que se generará."""
    if session_id not in endpoint_sessions:
        return f"Error: sesión '{session_id}' no encontrada"
    ep = endpoint_sessions[session_id]
    params_str = ", ".join(f"{p}: str" for p in ep["params"])
    code = f'''from fastapi import APIRouter

router = APIRouter()

@router.{ep["method"].lower()}("{ep["path"]}")
async def handler({params_str}):
    """{ep["description"]}"""
    return {{"status": "ok"}}
'''
    return f"## 👀 Preview\n\n```python\n{code}```\n\n**Siguiente:** `create_endpoint(session_id=\"{session_id}\")` para crear los archivos."


@mcp.tool()
async def create_endpoint(session_id: str) -> str:
    """Paso 3: Crea el endpoint (simulated)."""
    if session_id not in endpoint_sessions:
        return f"Error: sesión '{session_id}' no encontrada"
    ep = endpoint_sessions.pop(session_id)
    return f"## ✅ Endpoint Creado\n\n`{ep['method']} {ep['path']}` listo.\n\nArchivos: `routes/{ep['path'].split('/')[-1]}.py`"

if __name__ == "__main__":
    mcp.run()

Ejercicio 4: Tool de configuración con validación (Difícil)

Crea un tool TypeScript configure_database con dos modos: validate (verifica que la connection string sea válida para el tipo de DB — postgres, mysql, sqlite — y muestra un resumen) y generate (crea un JSON de configuración). Valida que la connection string empiece con el prefijo correcto (postgresql://, mysql://, sqlite:///).

Ver solución
server.tool(
  "configure_database",
  "Configura conexión a DB: validate para verificar, generate para crear config",
  {
    dbType: z.enum(["postgres", "mysql", "sqlite"]),
    connectionString: z.string(),
    poolSize: z.number().int().min(1).max(100).default(10),
    action: z.enum(["validate", "generate"]).default("validate"),
  },
  async ({ dbType, connectionString, poolSize, action }) => {
    const prefixes = { postgres: "postgresql://", mysql: "mysql://", sqlite: "sqlite:///" };
    const isValid = connectionString.startsWith(prefixes[dbType]);

    if (action === "validate") {
      let out = `## 🔍 DB Config\n\n| Campo | Status |\n|-------|--------|\n`;
      out += `| Tipo: ${dbType} | ✅ |\n`;
      out += `| Connection | ${isValid ? "✅" : "❌ Prefix: " + prefixes[dbType]} |\n`;
      out += `| Pool: ${poolSize} | ${poolSize <= 50 ? "✅" : "⚠️"} |\n`;
      if (isValid) out += `\nUsa \`action: "generate"\` para crear config.`;
      return { content: [{ type: "text" as const, text: out }] };
    }
    if (!isValid) return { content: [{ type: "text" as const, text: "❌ Valida primero" }], isError: true };
    const cfg = JSON.stringify({ database: { type: dbType, url: connectionString, pool: { size: poolSize } } }, null, 2);
    return { content: [{ type: "text" as const, text: `## ✅ Config\n\n\`\`\`json\n${cfg}\n\`\`\`` }] };
  }
);

Troubleshooting

"Claude Code no sigue el workflow multi-paso"

Causa: El modelo no siempre ejecuta los pasos en el orden esperado. Puede saltarse pasos o combinarlos.

Solución: Diseña cada tool para que valide su precondición:

if session["step"] != "confirm":
    return "Error: debes completar el paso anterior primero"

"Las sesiones se pierden entre invocaciones"

Causa: El proceso MCP puede reiniciarse, perdiendo el estado in-memory.

Solución: Para workflows críticos, persiste el estado en un archivo:

import json

def save_session(session_id: str, data: dict):
    sessions = load_all_sessions()
    sessions[session_id] = data
    with open(".mcp_sessions.json", "w") as f:
        json.dump(sessions, f)

"El prompt no produce el resultado esperado"

Causa: Los prompts son sugerencias, no instrucciones absolutas. El modelo puede interpretar diferente.

Solución: Sé más específico en el prompt, incluyendo el nombre exacto de los tools y los parámetros:

return f"""Usa el tool `check_tests()` primero. Si retorna ✅, usa `deploy_preview(environment="{env}")`."""

Resumen

En esta cápsula aprendiste:

  • Confirmación preview/execute — todo tool destructivo debe tener un modo preview (default) y un modo execute
  • Workflows multi-paso — tools con estado que guían al usuario a través de un proceso
  • Prompts como orchestradores — templates que definen secuencias de tools
  • Captura de datos estructurada — tools con modo template + generate para configuraciones
  • Combinación tools + prompts — la forma más poderosa de crear flujos interactivos en MCP

La interactividad en MCP no viene de widgets de UI — viene de un diseño inteligente de tools que hace que la conversación con Claude Code sea un flujo natural y productivo.


Recursos adicionales

  1. MCP Specification — Prompts — Referencia oficial de prompts
  2. MCP Python SDK — Prompts — Implementación de prompts en Python
  3. MCP TypeScript SDK — Prompts — Implementación en TypeScript
  4. Wizard Pattern (UX) — Patrón de diseño de workflows paso a paso

Siguiente cápsula: Proyecto — MCP App con Dashboard Interactivo. Vas a combinar dashboards, visualizaciones, y workflows interactivos en un MCP App completo conectado a Claude Code.