Cap 6: MCP Servers

Por: Artiko
claude-codemcpserversintegraciones

Qué es MCP

El Model Context Protocol (MCP) es un estándar abierto para conectar herramientas de IA con fuentes de datos externas. Con MCP, Claude Code puede leer documentos en Google Drive, actualizar tickets en Jira, interactuar con Slack, o usar tooling personalizado.

Tipos de transporte

Claude Code soporta cuatro transportes:

TransportetypeUsoConfigurable con --transport
stdiostdio (default si el entry no declara type)Servidor local por stdin/stdout
Streamable HTTPhttp (alias streamable-http)Recomendado para servidores remotos
SSEsseDEPRECADO
WebSocketwsSolo por .mcp.json o claude mcp add-jsonNo

stdio (local)

El servidor MCP se ejecuta como proceso local. Claude Code se comunica via stdin/stdout. Es el transporte por defecto cuando el entry no declara type:

{
  "mcpServers": {
    "context7": {
      "command": "npx",
      "args": ["-y", "@upstash/context7-mcp@latest"]
    }
  }
}

http (Streamable HTTP, remoto)

Es el transporte recomendado para servidores remotos. El campo type es obligatorio cuando hay url:

{
  "mcpServers": {
    "mi-servidor": {
      "type": "http",
      "url": "https://mi-servidor.com/mcp",
      "headers": {
        "Authorization": "Bearer ${MCP_TOKEN}"
      }
    }
  }
}

En JSON, type acepta streamable-http como alias de http.

Un entry con url pero sin type es un error de configuración: el servidor se omite y se reporta

MCP server "<name>" has a "url" but no "type"

Antes de v2.1.202 el error era mucho menos claro: command: expected string, received undefined.

sse (deprecado)

"type": "sse" sigue funcionando pero está deprecado. Los servidores nuevos deben usar http.

ws (WebSocket)

"type": "ws" solo se puede configurar editando .mcp.json o con claude mcp add-json; no existe la opción en --transport.

Configuración

Archivo .mcp.json (proyecto)

{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": ["@anthropic-ai/mcp-playwright@latest"]
    },
    "context7": {
      "command": "npx",
      "args": ["-y", "@upstash/context7-mcp@latest"]
    }
  }
}

Via CLI

# Agregar servidor
claude mcp add playwright npx @anthropic-ai/mcp-playwright@latest

# Listar servidores
claude mcp list

# Eliminar servidor
claude mcp remove playwright

# Configurar un servidor con JSON completo (única vía para ws)
claude mcp add-json mi-servidor '{"type":"ws","url":"wss://..."}'

# Autenticación OAuth desde la shell
claude mcp login mi-servidor
claude mcp logout mi-servidor

Scopes

Hay tres scopes (evita el término “global”, que era el nombre viejo de user):

ScopeAlcanceDónde se guarda
local (default)Solo el proyecto actual~/.claude.json, bajo la ruta del proyecto
projectCompartido con el equipo.mcp.json en la raíz del repo
userTodos tus proyectos~/.claude.json
# Scope local (default): solo este proyecto, no se comparte
claude mcp add playwright npx @anthropic-ai/mcp-playwright@latest

# Scope usuario: disponible en todos tus proyectos
claude mcp add --scope user context7 npx -y @upstash/context7-mcp@latest

# Scope proyecto: se versiona en .mcp.json y lo usa todo el equipo
claude mcp add --scope project playwright npx @anthropic-ai/mcp-playwright@latest

Precedencia

Cuando el mismo servidor aparece en varias fuentes, gana una sola y se usa su definición completa: no hay merge campo a campo.

flowchart TD
  L["1. local<br/>(~/.claude.json por proyecto)"] --> P["2. project<br/>(.mcp.json del repo)"]
  P --> U["3. user<br/>(~/.claude.json)"]
  U --> PL["4. servidores de plugins"]
  PL --> C["5. conectores de claude.ai"]

  style L fill:#1e3a5f,color:#fff
  style P fill:#2d5a27,color:#fff
  style U fill:#4a4a4a,color:#fff

Los tres scopes hacen match por nombre; plugins y conectores hacen match por endpoint (URL o comando).

Este es hoy el comportamiento por defecto de MCP en Claude Code y cambia por completo el criterio de cuántos servidores conectar.

Las definiciones de herramientas MCP se difieren: no entran al contexto al arrancar. Solo entran los nombres de las herramientas y las instrucciones del servidor; la definición completa se carga bajo demanda vía la herramienta ToolSearch. Por eso no hay un tope fijo de herramientas por servidor: el límite práctico es el presupuesto de contexto.

ValorComportamiento
sin definirTodo diferido
trueTodo diferido, forzando el header beta
autoCarga upfront si las definiciones caben en el 10% de la ventana; difiere el resto
auto:NIgual que auto con umbral porcentual custom (0-100)
falseTodo upfront

También se desactiva con permissions.deny: ["ToolSearch"].

Está desactivado por defecto en Google Cloud Agent Platform y cuando ANTHROPIC_BASE_URL apunta a un host que no es de primera parte. CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS lo mantiene apagado y ENABLE_TOOL_SEARCH no lo sobrescribe.

Exenciones

{
  "mcpServers": {
    "critico": {
      "command": "npx",
      "args": ["-y", "mi-server"],
      "alwaysLoad": true
    }
  }
}
  • "alwaysLoad": true en el entry del servidor (v2.1.121+) carga sus definiciones upfront, pero bloquea el arranque hasta conectar, con tope de 5 s.
  • "anthropic/alwaysLoad": true en el _meta de una herramienta concreta exime solo a esa herramienta.

Claude Code trunca a 2KB cada descripción de herramienta y las instrucciones del servidor. Para quien escribe servidores, con tool search activo el campo de instrucciones es lo más importante: debe explicar qué categoría de tareas cubren las herramientas y cuándo buscarlas, con lo crítico al principio.

Autenticación

OAuth 2.0

Algunos servidores MCP requieren autenticación OAuth. El flujo se puede iniciar desde la shell, sin entrar a una sesión interactiva:

# Autenticar (v2.1.186+)
claude mcp login google-drive

# Entorno sin display: imprime la URL en vez de abrir el navegador (v2.1.191+)
claude mcp login google-drive --no-browser

# Fijar el puerto del callback
claude mcp login google-drive --callback-port 8976

# Credenciales preconfiguradas (en CI se puede usar MCP_CLIENT_SECRET)
claude mcp login google-drive --client-id "$ID" --client-secret "$SECRET"

# Borrar credenciales
claude mcp logout google-drive

El secreto se guarda en el keychain del sistema, nunca en el archivo de configuración.

Descubrimiento: Claude Code sigue RFC 9728 (Protected Resource Metadata en /.well-known/oauth-protected-resource) y desde ahí RFC 8414 (/.well-known/oauth-authorization-server). Soporta Client ID Metadata Document (CIMD) además de Dynamic Client Registration.

Ese descubrimiento se puede sobrescribir en el config, y lo sobrescrito tiene precedencia:

{
  "mcpServers": {
    "google-drive": {
      "type": "http",
      "url": "https://mcp-google.com/mcp",
      "oauth": {
        "authServerMetadataUrl": "https://auth.ejemplo.com/.well-known/oauth-authorization-server",
        "scopes": "drive.readonly drive.metadata"
      }
    }
  }
}
  • authServerMetadataUrl debe ser https.
  • scopes es un string separado por espacios (RFC 6749 §3.3), no un array.
  • No existe un campo oauth.clientId: las credenciales preconfiguradas se pasan por CLI con --client-id/--client-secret.

No-OAuth: headersHelper

Para Kerberos, SSO interno o tokens efímeros, headersHelper define un comando que escribe en stdout un objeto JSON de headers:

{
  "mcpServers": {
    "interno": {
      "type": "http",
      "url": "https://mcp.interno.corp/mcp",
      "headersHelper": "./scripts/auth-headers.sh"
    }
  }
}
  • Timeout de 10 s.
  • Se ejecuta en cada conexión, sin caché.
  • Recibe CLAUDE_CODE_MCP_SERVER_NAME, CLAUDE_CODE_MCP_SERVER_URL y CLAUDE_PLUGIN_ROOT.
  • Desde v2.1.193 se re-ejecuta y reintenta una vez ante un 401/403.

Servers recomendados

Context7 — Documentación actualizada

Consulta documentación de cualquier librería en tiempo real:

{
  "context7": {
    "command": "npx",
    "args": ["-y", "@upstash/context7-mcp@latest"]
  }
}

Playwright — Automatización de navegador

Testing y automatización web:

{
  "playwright": {
    "command": "npx",
    "args": ["@anthropic-ai/mcp-playwright@latest"]
  }
}

Chrome DevTools — Debugging web

Inspección y debugging de páginas web en Chrome:

{
  "chrome-devtools": {
    "command": "npx",
    "args": ["@anthropic-ai/mcp-chrome-devtools@latest"]
  }
}

Excalidraw — Diagramas

Crear y editar diagramas:

{
  "excalidraw": {
    "command": "npx",
    "args": ["@anthropic-ai/mcp-excalidraw@latest"]
  }
}

Las tres primitivas: tools, resources y prompts

MCP no expone solo herramientas.

Resources

Se referencian con menciones @servidor:protocolo://ruta/recurso:

@github:issue://123
@postgres:schema://users

El autocompletado de @ hace búsqueda difusa sobre los recursos disponibles y el recurso seleccionado se adjunta automáticamente al contexto.

Prompts

Los prompts que expone un servidor aparecen como slash commands /mcp__servidor__prompt, con argumentos separados por espacios:

/mcp__github__pr_review 456

Se descubren dinámicamente al conectar el servidor y sus nombres se normalizan (los espacios pasan a guiones bajos).

Límites de salida y timeouts

Son los que explican, en la práctica, los truncados y los cuelgues.

Tamaño de salida

UmbralValorAjustable
Aviso por salida grande10.000 tokensNo (fijo)
Límite máximo por defecto25.000 tokensMAX_MCP_OUTPUT_TOKENS
Persistencia a disco por herramienta_meta["anthropic/maxResultSizeChars"]Techo duro de 500.000 caracteres

maxResultSizeChars lo declara el servidor y aplica solo a texto: las herramientas que devuelven imágenes siguen sujetas a MAX_MCP_OUTPUT_TOKENS.

Timeouts

TimeoutDefaultVariable / campo
Arranque del servidorMCP_TIMEOUT
Llamada a herramienta~28 htimeout por servidor (ms) o MCP_TOOL_TIMEOUT
Primer byte en HTTP/SSE/conectores60 s por request
Idle (v2.1.187+) HTTP/SSE/WS/conectores5 minCLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT
Idle (v2.1.187+) stdio30 minCLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT

Los valores de timeout por servidor menores a 1000 se ignoran y caen a MCP_TOOL_TIMEOUT. Un CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT de 0 desactiva el idle timeout.

Paso automático a background

Desde v2.1.212, una llamada MCP de la conversación principal que supera 2 minutos pasa automáticamente a background task y aparece en /tasks. Se ajusta con CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS (0 lo apaga). No aplica a subagentes, servidores IDE ni modo no interactivo, salvo CLAUDE_AUTO_BACKGROUND_TASKS=1.

Permisos para herramientas MCP

Las herramientas MCP siguen el patrón mcp__servidor__herramienta:

{
  "permissions": {
    "allow": [
      "mcp__context7__*",
      "mcp__playwright__browser_navigate"
    ],
    "deny": [
      "mcp__*__delete_*"
    ]
  }
}

Servidores que vienen de plugins

Un servidor MCP provisto por un plugin no usa la clave desnuda. Sus herramientas se llaman:

mcp__plugin_<plugin>_<server>__<tool>

y el servidor se registra como plugin:<plugin>:<server>. Una regla de permisos o un hook matcher escrito contra la clave desnuda (mcp__database-tools__.*) nunca dispara para un servidor de plugin.

Wildcard patterns

deny/ask y allow no aceptan lo mismo:

ListaQué aceptaEjemplos válidos
deny, askGlobs completos en el nombre de herramienta"*", "mcp__*", "mcp__*__delete_*"
allowGlob solo tras el prefijo literal mcp__<server>__"mcp__context7__*", "mcp__playwright__browser_navigate"

Un allow no anclado como "mcp__*" se descarta con warning.

Recuerda además que las reglas de permisos se fusionan entre todos los scopes y que el orden de evaluación es deny → ask → allow: un deny en cualquier scope bloquea un allow de cualquier otro.

Aprobación humana forzada por el servidor

Un servidor puede marcar una herramienta con _meta["anthropic/requiresUserInteraction"]: true (booleano JSON estricto, requiere v2.1.199+). Con eso:

  • El prompt de aprobación aparece incluso en acceptEdits, auto y bypassPermissions.
  • No hay opción de “no volver a preguntar”.
  • En dontAsk, la llamada se deniega.

Control empresarial: managed-mcp.json

MCP tiene su propio archivo de política, separado de managed-settings.json:

SORuta
macOS/Library/Application Support/ClaudeCode/managed-mcp.json
Linux / WSL/etc/claude-code/managed-mcp.json
WindowsC:\Program Files\ClaudeCode\managed-mcp.json

Si el archivo existe, da control exclusivo: solo cargan esos servidores y los de plugins quedan suprimidos. {"mcpServers": {}} desactiva MCP por completo. No se puede entregar por server-managed settings.

Allowlist y denylist

allowedMcpServers y deniedMcpServers filtran por:

  • serverUrl — admite wildcards *.
  • serverCommand — match exacto de todos los args, en orden.
  • serverName — literal; no es un control de seguridad, porque el nombre lo elige el usuario.

Reglas de resolución:

  • El denylist siempre gana y siempre fusiona todas las fuentes.
  • allowManagedMcpServersOnly: true limita el allowlist a fuentes gestionadas.
  • allowedMcpServers sin definir permite todo; [] bloquea todo.

MCP en sub-agents

Puedes restringir qué servidores MCP tiene disponible un agente:

<!-- .claude/agents/web-tester.md -->
---
mcpServers: ["playwright", "chrome-devtools"]
---

Dos advertencias que acotan esta restricción:

  • Los subagentes de plugin ignoran por seguridad los campos hooks, mcpServers y permissionMode.
  • Un subagente no es una frontera de seguridad: «subagents run in the same process as the parent session and use the same sandbox configuration». Su valor es aislamiento de contexto y restricción de herramientas, no contención.

Diagnóstico

Si un servidor MCP no funciona:

# Verificar estado
claude mcp list

# Modo debug
claude --debug "mcp"

Crear un MCP Server desde cero

El examen pregunta sobre implementación, no solo consumo.

Estructura mínima en TypeScript

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";

const server = new Server(
  { name: "mi-servidor", version: "1.0.0" },
  { capabilities: { tools: {} } }
);

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "get_weather",
      description: "Obtiene el clima de una ciudad",
      inputSchema: {
        type: "object",
        properties: { city: { type: "string" } },
        required: ["city"],
      },
    },
    {
      name: "search_products",
      description: "Busca productos por nombre",
      inputSchema: {
        type: "object",
        properties: { query: { type: "string" }, limit: { type: "number" } },
        required: ["query"],
      },
    },
  ],
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  if (name === "get_weather") {
    const city = args?.city as string;
    // Llamada real a API de clima
    const data = await fetch(`https://wttr.in/${city}?format=3`);
    const text = await data.text();
    return { content: [{ type: "text", text }] };
  }

  if (name === "search_products") {
    const { query, limit = 5 } = args as { query: string; limit?: number };
    const results = await searchDB(query, limit);
    return { content: [{ type: "text", text: JSON.stringify(results) }] };
  }

  return {
    isError: true,
    content: [{ type: "text", text: `Tool desconocida: ${name}` }],
  };
});

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main().catch(console.error);

package.json mínimo:

{
  "name": "mi-mcp-server",
  "version": "1.0.0",
  "type": "module",
  "main": "dist/index.js",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.0"
  },
  "devDependencies": {
    "typescript": "^5.0.0"
  }
}

Tipos de respuesta

TipoEstructuraCuándo usar
Éxitocontent: [{ type: "text", text: "..." }]Operación completada
Error manejadoisError: true, content: [...]Error esperado (API caída, input inválido)
Throwthrow new Error(...)Error de protocolo o bug interno

Usar isError: true en lugar de throw permite que Claude razone sobre el error y decida qué hacer (reintentar, cambiar estrategia, informar al usuario). Con throw, Claude recibe un error de protocolo sin contexto.

Debugging de MCP servers

# Logs verbosos del protocolo MCP
MCP_DEBUG=1 claude

# Probar el server manualmente antes de integrar
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | node dist/index.js

# Ver qué servidores están conectados
claude mcp list

Errores comunes:

ErrorCausaSolución
Timeout en startupServer tarda en iniciar o crasheaRevisar logs, verificar dependencias
JSON malformadoOutput extra en stdout (console.log)Usar console.error para logs, nunca console.log
Tool no encontradaNombre incorrecto en tools/listVerificar que el nombre coincide exactamente
Permission deniedServer sin permisos de ejecuciónchmod +x dist/index.js

Security model

Claude se comunica con el server via protocolo MCP y no lee directamente su filesystem ni sus variables de entorno. Pero un proceso aparte no es una frontera de seguridad: un servidor stdio corre localmente con tus permisos de usuario.

La regla oficial es explícita: «Verify you trust each server before connecting it. Servers that fetch external content can expose you to prompt injection risk».

flowchart LR
  C["Claude Code"]
  P["MCP Protocol\n(JSON-RPC)"]
  S["Server Process"]
  E["Variables de entorno"]
  D["Base de datos"]
  A["APIs externas"]

  C <--> P <--> S
  E --> S
  D --> S
  A --> S

  style C fill:#1e3a5f,color:#fff
  style S fill:#2d5a27,color:#fff
  style P fill:#4a4a4a,color:#fff
  • Claude NO puede leer las variables de entorno del server directamente
  • Claude NO puede acceder a datos de un server que no está configurado
  • Las tools no son la única superficie de ataque: el riesgo principal documentado es la prompt injection a través del contenido que el servidor devuelve. Cualquier servidor que traiga contenido externo (páginas, issues, tickets, documentos) puede inyectar instrucciones en el contexto
  • Anthropic revisa los conectores contra criterios de listado antes de añadirlos al Directory, pero no audita ni gestiona servidores MCP

Cuántos servidores conectar: con tool search activo por defecto, el coste de arranque en tokens es marginal, así que el viejo criterio de “conecta pocos servidores para no quemar contexto” ya no aplica. El riesgo real de conectar muchos servidores es de superficie de ataque y permisos, no de contexto.

OAuth en MCP

sequenceDiagram
  participant U as Usuario
  participant CC as Claude Code
  participant S as MCP Server
  participant O as OAuth Provider

  U->>CC: Usa tool que requiere auth
  CC->>U: Redirige a pantalla OAuth
  U->>O: Autoriza acceso
  O->>CC: Retorna token
  CC->>CC: Almacena token en credentials store
  CC->>S: Request con Authorization header
  S->>S: Valida token
  S->>CC: Respuesta de la tool
  • Claude Code almacena el token, no el server
  • El server recibe el token en cada request via Authorization: Bearer <token>
  • Si el token expira, Claude Code inicia el flujo OAuth nuevamente
  • El mismo flujo se puede disparar fuera de la sesión con claude mcp login <name> (y --no-browser si no hay display)

Siguiente: Settings