Cap 6: MCP Servers
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:
| Transporte | type | Uso | Configurable con --transport |
|---|---|---|---|
| stdio | stdio (default si el entry no declara type) | Servidor local por stdin/stdout | Sí |
| Streamable HTTP | http (alias streamable-http) | Recomendado para servidores remotos | Sí |
| SSE | sse | DEPRECADO | Sí |
| WebSocket | ws | Solo por .mcp.json o claude mcp add-json | No |
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):
| Scope | Alcance | Dónde se guarda |
|---|---|---|
local (default) | Solo el proyecto actual | ~/.claude.json, bajo la ruta del proyecto |
project | Compartido con el equipo | .mcp.json en la raíz del repo |
user | Todos 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).
Carga de herramientas: tool search
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.
ENABLE_TOOL_SEARCH
| Valor | Comportamiento |
|---|---|
| sin definir | Todo diferido |
true | Todo diferido, forzando el header beta |
auto | Carga upfront si las definiciones caben en el 10% de la ventana; difiere el resto |
auto:N | Igual que auto con umbral porcentual custom (0-100) |
false | Todo 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": trueen el entry del servidor (v2.1.121+) carga sus definiciones upfront, pero bloquea el arranque hasta conectar, con tope de 5 s."anthropic/alwaysLoad": trueen el_metade 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"
}
}
}
}
authServerMetadataUrldebe ser https.scopeses 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_URLyCLAUDE_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
| Umbral | Valor | Ajustable |
|---|---|---|
| Aviso por salida grande | 10.000 tokens | No (fijo) |
| Límite máximo por defecto | 25.000 tokens | MAX_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
| Timeout | Default | Variable / campo |
|---|---|---|
| Arranque del servidor | — | MCP_TIMEOUT |
| Llamada a herramienta | ~28 h | timeout por servidor (ms) o MCP_TOOL_TIMEOUT |
| Primer byte en HTTP/SSE/conectores | 60 s por request | — |
| Idle (v2.1.187+) HTTP/SSE/WS/conectores | 5 min | CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT |
| Idle (v2.1.187+) stdio | 30 min | CLAUDE_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:
| Lista | Qué acepta | Ejemplos válidos |
|---|---|---|
deny, ask | Globs completos en el nombre de herramienta | "*", "mcp__*", "mcp__*__delete_*" |
allow | Glob 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,autoybypassPermissions. - 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:
| SO | Ruta |
|---|---|
| macOS | /Library/Application Support/ClaudeCode/managed-mcp.json |
| Linux / WSL | /etc/claude-code/managed-mcp.json |
| Windows | C:\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: truelimita el allowlist a fuentes gestionadas.allowedMcpServerssin 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,mcpServersypermissionMode. - 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
| Tipo | Estructura | Cuándo usar |
|---|---|---|
| Éxito | content: [{ type: "text", text: "..." }] | Operación completada |
| Error manejado | isError: true, content: [...] | Error esperado (API caída, input inválido) |
| Throw | throw 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:
| Error | Causa | Solución |
|---|---|---|
| Timeout en startup | Server tarda en iniciar o crashea | Revisar logs, verificar dependencias |
| JSON malformado | Output extra en stdout (console.log) | Usar console.error para logs, nunca console.log |
| Tool no encontrada | Nombre incorrecto en tools/list | Verificar que el nombre coincide exactamente |
| Permission denied | Server sin permisos de ejecución | chmod +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-browsersi no hay display)
Siguiente: Settings