Todos los artículos

// Agentes en producción

MCP en Producción: Patrones de Integración Real

Cómo implementar Model Context Protocol en producción con patrones probados: seguridad, observabilidad, circuit breakers y escalado de agentes a escala real.

25 de junio de 202614 min de lectura

Problem

El Model Context Protocol (MCP) revolucionó cómo los agentes IA interactúan con herramientas y servicios. Durante el desarrollo, MCP es increíble: plug-and-play, autodescubrimiento de herramientas, y una experiencia de desarrollo fluida. Pero cuando intentas llevar ese prototipo a producción, la realidad te golpea duro.

Los desafíos que enfrentamos al integrar MCP en producción no son triviales:

  • Seguridad: ¿Cómo autenticas y autorizas requests entre agentes y tools sin exponer credenciales?
  • Error Handling: ¿Qué pasa cuando un MCP tool falla mid-conversation? ¿El agent se bloquea?
  • Rate Limiting: ¿Cómo evitas que un agent mal configurado DDoS tus servicios subyacentes?
  • Observabilidad: ¿Cómo tracingas el flow completo desde el agent hasta el tool y de vuelta?
  • Versioning: ¿Cómo manejas cambios en la API de un MCP tool sin romper agents dependientes?

En Equifax LATAM, aprendimos esto de la manera difícil. Tuvimos un MCP server de herramientas financieras que, en producción, causó timeouts en cascada cuando el servicio de credit scoring tuvo un incidente. Los agents no tenían timeouts, no tenían retries, y no había circuit breakers. El resultado: conversaciones colgadas, usuarios frustrados, y un incidente que tardó 3 horas en mitigarse.

Core Concept

MCP es esencialmente un contrato entre agentes y herramientas. Define cómo los agents descubben, invocan y reciben respuestas de tools. Pero en producción, ese contrato no es suficiente. Necesitas una capa de infraestructura alrededor de MCP que proporcione:

  • Wrappers: Middleware que intercepta cada request/response
  • Retries con backoff exponencial: Para manejar fallos transitorios
  • Caching inteligente: Reducir latencia y carga en servicios
  • Monitoring real-time: Detectar anomalías antes de que se conviertan en incidentes
  • Rate limiting: Proteger servicios subyacentes
  • Timeout enforcement: Prevenir conversaciones colgadas
⚠️Atención

MCP alone is not production-ready. It's the contract layer, not the infrastructure layer. Think of MCP as HTTP and your production patterns as the reverse proxy/load balancer sitting in front of it.

Implementation

Arquitectura MCP en Producción

La arquitectura que ha funcionado para nosotros sigue un patrón de three-tier:

terminal
[Agent Orchestrator]
        ↓
   [MCP Gateway] ← JWT Auth, Rate Limiting, Telemetry
        ↓
   [MCP Servers] ← Circuit Breakers, Retries, Caching
        ↓
[Tools/Services] ← APIs, databases, external services

El MCP Gateway es el punto de entrada unificado. Todos los agents se conectan al gateway, que maneja autenticación, rate limiting, y routing al MCP server apropiado. Esto desacopla los agents de la infraestructura MCP subyacente.

Los MCP Servers son instancias específicas que implementan tools para dominios concretos (financial, database, API calls, etc.). Cada server tiene su propio middleware de retries, circuit breakers, y caching.

Veamos cómo implementar esto en código.

MCP Server con Middleware (Go)

Este ejemplo muestra un MCP server production-ready en Go con middleware completo:

go
package main

import (
    "context"
    "crypto/rsa"
    "encoding/json"
    "errors"
    "fmt"
    "log"
    "net/http"
    "strings"
    "sync"
    "time"

    "github.com/golang-jwt/jwt/v5"
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/exporters/jaeger"
    "go.opentelemetry.io/otel/sdk/resource"
    tracesdk "go.opentelemetry.io/otel/sdk/trace"
    semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
    "go.uber.org/ratelimit"
    "github.com/sony/gobreaker"
)

// MCP Tool Definition
type MCPTool struct {
    Name        string                 `json:"name"`
    Description string                 `json:"description"`
    InputSchema map[string]interface{} `json:"input_schema"`
    Handler     ToolHandler
}

type ToolHandler func(ctx context.Context, params map[string]interface{}) (interface{}, error)

// MCP Server Structure
type MCPServer struct {
    tools           map[string]MCPTool
    jwtKey          *rsa.PrivateKey
    rateLimiter     ratelimit.Limiter
    circuitBreaker  *gobreaker.CircuitBreaker
    cache           *ToolCache
    metrics         *MCPMetrics
    tracer          tracesdk.Tracer
}

type MCPMetrics struct {
    requestsTotal    *prometheus.CounterVec
    requestDuration  *prometheus.HistogramVec
    errorsTotal      *prometheus.CounterVec
    circuitState     *prometheus.GaugeVec
}

type ToolCache struct {
    store sync.Map
    ttl   time.Duration
}

// Initialize MCP Server
func NewMCPServer() (*MCPServer, error) {
    // Initialize OpenTelemetry
    tracerProvider, err := initTracer()
    if err != nil {
        return nil, fmt.Errorf("failed to initialize tracer: %w", err)
    }
    otel.SetTracerProvider(tracerProvider)

    // Initialize metrics
    metrics := &MCPMetrics{
        requestsTotal: prometheus.NewCounterVec(
            prometheus.CounterOpts{
                Name: "mcp_requests_total",
                Help: "Total number of MCP tool requests",
            },
            []string{"tool", "status"},
        ),
        requestDuration: prometheus.NewHistogramVec(
            prometheus.HistogramOpts{
                Name:    "mcp_request_duration_seconds",
                Help:    "Duration of MCP tool requests",
                Buckets: prometheus.DefBuckets,
            },
            []string{"tool"},
        ),
        errorsTotal: prometheus.NewCounterVec(
            prometheus.CounterOpts{
                Name: "mcp_errors_total",
                Help: "Total number of MCP tool errors",
            },
            []string{"tool", "error_type"},
        ),
        circuitState: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "mcp_circuit_breaker_state",
                Help: "State of circuit breaker (0=closed, 1=open, 2=half-open)",
            },
            []string{"tool"},
        ),
    }

    prometheus.MustRegister(
        metrics.requestsTotal,
        metrics.requestDuration,
        metrics.errorsTotal,
        metrics.circuitState,
    )

    // Initialize circuit breaker
    cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{
        Name:        "mcp_tools",
        MaxRequests: 5,
        Interval:    30 * time.Second,
        Timeout:     60 * time.Second,
        ReadyToTrip: func(counts gobreaker.Counts) bool {
            failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
            return counts.Requests >= 3 && failureRatio >= 0.6
        },
        OnStateChange: func(name string, from gobreaker.State, to gobreaker.State) {
            metrics.circuitState.WithLabelValues(name).Set(float64(to))
        },
    })

    server := &MCPServer{
        tools:           make(map[string]MCPTool),
        rateLimiter:     ratelimit.New(100), // 100 requests/sec
        circuitBreaker:  cb,
        cache: &ToolCache{ttl: 5 * time.Minute},
        metrics:         metrics,
        tracer:          otel.Tracer("mcp-server"),
    }

    return server, nil
}

// Register Tool
func (s *MCPServer) RegisterTool(tool MCPTool) {
    s.tools[tool.Name] = tool
}

// Middleware Chain
func (s *MCPServer) middleware(next http.Handler) http.Handler {
    chain := []func(http.Handler) http.Handler{
        s.authMiddleware,
        s.rateLimitMiddleware,
        s.telemetryMiddleware,
    }

    for i := len(chain) - 1; i >= 0; i-- {
        next = chain[i](next)
    }

    return next
}

// JWT Authentication Middleware
func (s *MCPServer) authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        authHeader := r.Header.Get("Authorization")
        if authHeader == "" {
            http.Error(w, "Authorization header required", http.StatusUnauthorized)
            return
        }

        tokenString := strings.TrimPrefix(authHeader, "Bearer ")
        token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
            if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
                return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
            }
            return &s.jwtKey.PublicKey, nil
        })

        if err != nil || !token.Valid {
            http.Error(w, "Invalid token", http.StatusUnauthorized)
            return
        }

        claims, ok := token.Claims.(jwt.MapClaims)
        if !ok {
            http.Error(w, "Invalid claims", http.StatusUnauthorized)
            return
        }

        // Add claims to context
        ctx := context.WithValue(r.Context(), "claims", claims)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// Rate Limiting Middleware
func (s *MCPServer) rateLimitMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        s.rateLimiter.Take()
        next.ServeHTTP(w, r)
    })
}

// Telemetry Middleware
func (s *MCPServer) telemetryMiddleware(next http.Handler) http.Handler {
    return otelhttp.NewHandler(next, "mcp-request")
}

// Handle Tool Invocation
func (s *MCPServer) handleToolInvocation(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    span := s.tracer.Start(ctx, "handle_tool_invocation")
    defer span.End()

    var req struct {
        ToolName string                 `json:"tool_name"`
        Params   map[string]interface{} `json:"params"`
    }

    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        s.metrics.errorsTotal.WithLabelValues("", "decode_error").Inc()
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    tool, exists := s.tools[req.ToolName]
    if !exists {
        s.metrics.errorsTotal.WithLabelValues(req.ToolName, "not_found").Inc()
        http.Error(w, "Tool not found", http.StatusNotFound)
        return
    }

    span.SetAttributes(
        attribute.String("tool.name", tool.Name),
        attribute.String("tool.params", fmt.Sprintf("%v", req.Params)),
    )

    start := time.Now()

    // Check cache first
    cacheKey := fmt.Sprintf("%s:%v", req.ToolName, req.Params)
    if cached, ok := s.cache.get(cacheKey); ok {
        s.metrics.requestsTotal.WithLabelValues(tool.Name, "cache_hit").Inc()
        json.NewEncoder(w).Encode(map[string]interface{}{
            "result": cached,
            "cached": true,
        })
        return
    }

    // Execute with circuit breaker
    result, err := s.circuitBreaker.Execute(func() (interface{}, error) {
        return tool.Handler(ctx, req.Params)
    })

    duration := time.Since(start).Seconds()
    s.metrics.requestDuration.WithLabelValues(tool.Name).Observe(duration)

    if err != nil {
        s.metrics.errorsTotal.WithLabelValues(tool.Name, "execution_error").Inc()
        s.metrics.requestsTotal.WithLabelValues(tool.Name, "error").Inc()

        if errors.Is(err, gobreaker.ErrOpenState) {
            span.SetAttributes(attribute.Bool("circuit.open", true))
            http.Error(w, "Service unavailable (circuit open)", http.StatusServiceUnavailable)
            return
        }

        span.RecordError(err)
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    // Cache result
    s.cache.set(cacheKey, result)

    s.metrics.requestsTotal.WithLabelValues(tool.Name, "success").Inc()
    json.NewEncoder(w).Encode(map[string]interface{}{
        "result": result,
        "cached": false,
    })
}

// Tool Cache Implementation
func (c *ToolCache) get(key string) (interface{}, bool) {
    val, ok := c.store.Load(key)
    if !ok {
        return nil, false
    }

    entry := val.(*cacheEntry)
    if time.Since(entry.timestamp) > c.ttl {
        c.store.Delete(key)
        return nil, false
    }

    return entry.value, true
}

func (c *ToolCache) set(key string, value interface{}) {
    c.store.Store(key, &cacheEntry{
        value:     value,
        timestamp: time.Now(),
    })
}

type cacheEntry struct {
    value     interface{}
    timestamp time.Time
}

// Initialize OpenTelemetry Tracer
func initTracer() (*tracesdk.TracerProvider, error) {
    exp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint("http://localhost:14268/api/traces")))
    if err != nil {
        return nil, err
    }

    tp := tracesdk.NewTracerProvider(
        tracesdk.WithBatcher(exp),
        tracesdk.WithResource(resource.NewWithAttributes(
            semconv.SchemaURL,
            semconv.ServiceNameKey.String("mcp-server"),
        )),
    )

    return tp, nil
}

func main() {
    server, err := NewMCPServer()
    if err != nil {
        log.Fatalf("Failed to create MCP server: %v", err)
    }

    // Register example tools
    server.RegisterTool(MCPTool{
        Name:        "get_credit_score",
        Description: "Retrieve credit score for a user",
        InputSchema: map[string]interface{}{
            "user_id": map[string]interface{}{
                "type":        "string",
                "description": "User identifier",
            },
        },
        Handler: func(ctx context.Context, params map[string]interface{}) (interface{}, error) {
            // Simulate tool execution
            userID := params["user_id"].(string)
            time.Sleep(100 * time.Millisecond) // Simulate latency
            return map[string]interface{}{
                "user_id":     userID,
                "credit_score": 720,
                "factors":     []string{"payment_history", "credit_utilization"},
            }, nil
        },
    })

    // Setup HTTP routes
    mux := http.NewServeMux()
    mux.Handle("/mcp/tools", otelhttp.NewHandler(server.listTools(), "list_tools"))
    mux.Handle("/mcp/invoke", otelhttp.NewHandler(server.handleToolInvocation, "invoke_tool"))
    mux.Handle("/metrics", promhttp.Handler())

    wrapped := server.middleware(mux)

    log.Println("MCP Server listening on :8080")
    log.Fatal(http.ListenAndServe(":8080", wrapped))
}

// List available tools
func (s *MCPServer) listTools() http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        tools := make([]map[string]interface{}, 0, len(s.tools))
        for name, tool := range s.tools {
            tools = append(tools, map[string]interface{}{
                "name":        name,
                "description": tool.Description,
                "input_schema": tool.InputSchema,
            })
        }

        json.NewEncoder(w).Encode(map[string]interface{}{
            "tools": tools,
        })
    })
}

Este server implementa todos los patrones de producción que discutiremos. Nota cómo cada request pasa por el middleware chain antes de ejecutar el tool.

Patrón Circuit Breaker

El circuit breaker es crucial para evitar cascadas de fallos. Cuando un tool comienza a fallar consistentemente, el circuit breaker se abre y retorna errores rápidamente sin intentar ejecutar el tool. Esto permite que el service se recupere sin ser saturado por retries.

1

Estado Closed

Normal operation. Requests pasan al tool. Si hay fallos, se trackean el ratio de éxito.

2

Umbral excedido

Si el ratio de fallos excede el umbral (ej. 60% de requests fallan), el circuit breaker transiciona a Open.

3

Estado Open

Los requests fallan inmediatamente sin alcanzar el tool. El service tiene tiempo para recuperarse.

4

Estado Half-Open

Después de un timeout, el circuit breaker permite requests limitados para probar si el tool se ha recuperado.

Observabilidad con OpenTelemetry

Tracing es esencial para entender el flow completo de una conversación de agent. Con OpenTelemetry, puedes tracear desde el agent hasta el MCP Gateway, al MCP Server, y a los servicios subyacentes.

python
# Python example of MCP client with tracing
import time
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.jaeger import JaegerExporter
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
import httpx

# Initialize tracing
trace.set_tracer_provider(TracerProvider())
jaeger_exporter = JaegerExporter(
    agent_host_name="localhost",
    agent_port=6831,
)
trace.get_tracer_provider().add_span_processor(
    BatchSpanProcessor(jaeger_exporter)
)

tracer = trace.get_tracer("mcp-client")
HTTPXClientInstrumentor().instrument()

class MCPClient:
    def __init__(self, gateway_url: str, jwt_token: str):
        self.gateway_url = gateway_url
        self.jwt_token = jwt_token
        self.client = httpx.Client()

    async def invoke_tool(self, tool_name: str, params: dict) -> dict:
        with tracer.start_as_current_span("mcp_tool_invocation") as span:
            span.set_attribute("tool.name", tool_name)
            span.set_attribute("tool.params", str(params))

            start_time = time.time()

            try:
                response = self.client.post(
                    f"{self.gateway_url}/mcp/invoke",
                    json={
                        "tool_name": tool_name,
                        "params": params
                    },
                    headers={
                        "Authorization": f"Bearer {self.jwt_token}"
                    },
                    timeout=30.0
                )

                duration = time.time() - start_time
                span.set_attribute("http.status_code", response.status_code)
                span.set_attribute("mcp.duration_ms", duration * 1000)

                response.raise_for_status()
                return response.json()

            except httpx.HTTPStatusError as e:
                span.record_exception(e)
                span.set_attribute("error.type", "http_error")
                raise
            except httpx.TimeoutException as e:
                span.record_exception(e)
                span.set_attribute("error.type", "timeout")
                raise
            except Exception as e:
                span.record_exception(e)
                span.set_attribute("error.type", "unknown")
                raise

# Usage
async def agent_tool_call():
    client = MCPClient(
        gateway_url="http://mcp-gateway:8080",
        jwt_token="your-jwt-token"
    )

    result = await client.invoke_tool(
        tool_name="get_credit_score",
        params={"user_id": "user-123"}
    )

    print(f"Result: {result}")

Este tracing permite que veas el complete flow en Jaeger o cualquier backend de tracing compatible.

Security: JWT y Tool Whitelisting

La autenticación JWT protege tu MCP server de accesos no autorizados. Cada request debe incluir un JWT válido con claims que incluyen:

  • sub: Agent identifier
  • aud: MCP server identifier
  • scopes: List of tools the agent is allowed to invoke
  • exp: Expiration time
go
// JWT Claims structure
type MCPClaims struct {
    AgentID  string   `json:"sub"`
    Audience string   `json:"aud"`
    Scopes   []string `json:"scopes"`
    jwt.RegisteredClaims
}

// Tool whitelisting in middleware
func (s *MCPServer) checkToolAccess(claims *MCPClaims, toolName string) bool {
    for _, scope := range claims.Scopes {
        if scope == "*" || scope == fmt.Sprintf("tool:%s", toolName) {
            return true
        }
    }
    return false
}

Escalado de MCP Servers

Para escalar MCP servers horizontalmente, seguimos estos patrones:

  1. Stateless design: MCP servers no maintain state entre requests. Todo el state está en el cache o en servicios externos.

  2. Load balancing: Usamos HAProxy o NGINX como load balancer con consistent hashing para mantener cache locality.

  3. Auto-scaling: Kubernetes HPA scale up/down MCP servers basado en CPU/memory usage y request rate.

  4. Blue-green deployments: Zero-downtime deployments de nuevas versiones de MCP servers.

yaml
# Kubernetes HPA configuration
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: mcp-server-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: mcp-server
  minReplicas: 3
  maxReplicas: 20
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
      - type: Percent
        value: 50
        periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
      - type: Percent
        value: 100
        periodSeconds: 30
      - type: Pods
        value: 4
        periodSeconds: 30
      selectPolicy: Max

Lessons Learned

Después de 18 meses running MCP en producción con miles de agents y millones de requests por día, aquí está lo que aprendimos:

Patrones que Funcionan

Timeout + Backoff Exponencial Nunca ejecutes un MCP tool sin timeout. Usa backoff exponencial con jitter para retries. Esto reduce la carga en servicios subyacentes cuando hay fallos.

go
// Retry with exponential backoff and jitter
func (s *MCPServer) retryWithBackoff(ctx context.Context, operation func() (interface{}, error), maxRetries int) (interface{}, error) {
    var lastErr error

    for attempt := 0; attempt < maxRetries; attempt++ {
        result, err := operation()
        if err == nil {
            return result, nil
        }

        lastErr = err

        if attempt == maxRetraits-1 {
            break
        }

        // Exponential backoff with jitter
        backoff := time.Duration(math.Pow(2, float64(attempt))) * time.Second
        jitter := time.Duration(rand.Float64() * float64(backoff) * 0.1)
        wait := backoff + jitter

        select {
        case <-ctx.Done():
            return nil, ctx.Err()
        case <-time.After(wait):
            continue
        }
    }

    return nil, lastErr
}

Bulk Operations Cuando tengas múltiples calls al mismo tool, agrégalos en una sola request. Esto reduce latency y overhead.

Request Validation Valida los parámetros de entrada antes de ejecutar el tool. Esto previene errores downstream y reduce superficie de ataque.

Anti-Patrones a Evitar

Sin Retries

🚨Peligro

Nunca asumas que un tool funcionará siempre. Network issues, timeouts, y service restarts son normales en producción. Sin retries, un fallo transitorio se convierte en un incidente.

Sync Blocking Calls Evita blocking calls en el main thread del agent. Usa async/await o goroutines para concurrent tool invocations.

Monolithic MCP Servers No pongas todos tus tools en un solo MCP server. Separa por dominio (financial, database, api) para escalar independientemente y reducir blast radius de fallos.

Hard-coded Endpoints Usa service discovery o config management para endpoints. Never hardcode URLs en código.

Tradeoffs: MCP vs Integraciones Directas

AspectoMCP en ProducciónIntegración Directa
Setup inicialMás complejo (gateway, middleware)Más simple (direct API call)
FlexibilidadAlta (plug-and-play tools)Baja (hard-coded integrations)
ObservabilidadExcelente (unified tracing)Variable (por implementar)
OverheadBajo-medio (gateway + proxies)Cero (directo)
EscaladoFácil (stateless servers)Depende (service específico)
TestingFácil (mock MCP tools)Más complejo (mock services)
VersioningAutomático (MCP schema)Manual (API versioning)
Use case idealSistemas multi-agent heterogéneosServicios monolíticos simples

Conclusion

MCP en producción no es plug-and-play como en desarrollo. Requiere una capa de infraestructura que provea seguridad, observabilidad, resiliencia, y escalado. Los patrones que cubrimos en este post — middleware chains, circuit breakers, OpenTelemetry tracing, JWT auth, y horizontal scaling — son esenciales para systems de agents a escala real.

Pero la inversión vale la pena. MCP te permite:

  • Desacoplamento: Agents y tools evolucionan independientemente
  • Reusabilidad: Tools pueden ser compartidos entre múltiples agents
  • Observabilidad: Unified tracing del complete flow
  • Flexibilidad: Easy addition/removal de tools sin cambios en agents

¿Cuándo deberías usar MCP en producción?

  • Usa MCP cuando: Tienes múltiples agents que necesitan los mismos tools, tu sistema es heterogéneo (lenguajes, frameworks), o necesitas flexibilidad para agregar/remover tools dinámicamente.
  • Usa integraciones directas cuando: Tienes un single monolithic agent, tu stack es homogéneo, o el overhead de MCP no justifica los beneficios.
Tip

Para deep dive en arquitecturas multi-agent, check Orchestrator-Worker Multi-Agente en Producción. Para deployment y scaling strategies, lee Agentes IA en Kubernetes: Deploy y Escalado. Y si quieres entender mejor el contrato MCP, revisa MCP como Contrato entre Agents.

En el próximo post de esta serie, cubriremos Testing de Agents IA — estrategias para testing determinístico, evaluation de outputs, y maintaining test suites para sistemas que son inherentemente non-deterministic.

Hasta la próxima, happy coding en producción.


¿Qué patrones has usado para MCP en producción? ¿Cuál fue tu mayor challenge? Déjame un comment y discutamos.

Referencias rápidas

Vista general

Recursos externos

Incluye recursos adicionales en el frontmatter para que aparezcan aquí.

Más en esta serie

Serie: Construyendo con IA: Lo que nadie te dice

// newsletter

¿Te sirvió este artículo?

Recibe los siguientes en tu inbox. Sin spam, cancela cuando quieras.

Discusión

Escrito por Jorge Ochoa. ¿Encontraste un error?

Abrir en GitHub