Servidores MCP dentro de un plugin
Servidores MCP dentro de un plugin
En el capítulo 4 empaquetamos el primero de los dos tipos de componente que define Agent Plugins v1. Este capítulo cubre el segundo y último: los servidores MCP.
El reparto de responsabilidades está escrito sin ambigüedad en spec/1.0.0.md §7.2:
The Model Context Protocol specification defines MCP wire behavior and lifecycle semantics. Agent Plugins defines the
mcp.jsonconfiguration format used to locate and connect to MCP servers in a plugin.
Agent Plugins no define el protocolo, ni el handshake, ni el ciclo de vida de la conexión. Define exactamente una cosa: el archivo portable que dice dónde está el servidor y cómo arrancarlo o conectarse a él. Y la misma sección aclara que ese archivo no es el formato nativo de nadie: “Clients map this portable format to their native configuration; its field names and values need not match a client-native format.”
1. Ubicación fija y prohibiciones
El §6.1 fija la ubicación de cada tipo de componente:
| Tipo de componente | Ubicación fija | Patrón |
|---|---|---|
| Skills | skills/ | Subdirectorios que contienen SKILL.md |
| MCP servers | mcp.json | Configuración JSON |
El §7.2.1 lo reafirma: “The MCP configuration path is mcp.json at the plugin root. MCP configuration MUST NOT be declared inline in plugin.json or loaded from any alternative core path.”
Con el lenguaje normativo RFC 2119 que vimos en el capítulo 3: la configuración MCP NO DEBE (MUST NOT) declararse dentro de plugin.json.
Esto tiene una consecuencia práctica. Si escribes esto:
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "mi-plugin",
"mcpServers": {}
}
mcpServers es un campo raíz desconocido en el esquema cerrado de plugin.json. Por §5.2 el cliente DEBE (MUST) reportarlo e ignorarlo y DEBE seguir cargando el plugin. Resultado: el plugin carga, pero tu servidor MCP no existe para nadie. Es el error clásico y es silencioso salvo que el cliente lo reporte.
El §6.2 separa además dos situaciones:
- Si
mcp.jsonno existe, el cliente NO DEBE tratarlo como error. Un plugin sin servidores MCP es válido. - Si
mcp.jsonexiste pero no resuelve a un archivo regular —por ejemplo, es un directorio— el cliente DEBE tratar ese tipo de componente como inválido y continuar cargando los demás.
2. El documento mcp.json
El §7.2.1 define el sobre exterior: “mcp.json MUST be a JSON object containing the required $schema and mcpServers fields, with no other top-level fields. […] An empty mcpServers object is valid.”
El esquema schemas/1.0.0/mcp.schema.json lo codifica así:
{
"type": "object",
"properties": {
"$schema": { "const": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json" },
"mcpServers": {
"type": "object",
"additionalProperties": { "$ref": "#/$defs/server" }
}
},
"required": ["$schema", "mcpServers"],
"additionalProperties": false
}
Tres consecuencias:
$schemaes obligatorio y su valor es unconst: para 1.0.0 debe ser exactamentehttps://agent-plugins.org/schemas/1.0.0/mcp.schema.json.mcpServerses obligatorio aunque esté vacío."mcpServers": {}es válido; omitir la clave, no.additionalProperties: false. A diferencia deplugin.json, aquí no hay excepción no fatal: incumplir los requisitos de nivel superior desactiva MCP para todo el plugin.
El documento mínimo válido es:
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
"mcpServers": {}
}
2.1 $schema como selector de reglas
El texto normativo dice algo que el JSON Schema no puede expresar: el cliente DEBE usar un valor de $schema reconocido para seleccionar las reglas de validación e interpretación que soporta localmente; PUEDE (MAY) mapear varios identificadores canónicos a la misma implementación, pero solo si reconoce explícitamente esas versiones como compatibles; y NO DEBE descargar el esquema mientras carga un plugin. Cero peticiones de red por validar.
El §10.1 añade una coherencia entre archivos: “When mcp.json is present, the version in its $schema value MUST match the version declared by plugin.json. A mismatch makes the MCP configuration invalid under §7.2.2 but does not invalidate other component types.” Lo retomamos en el capítulo 7.
3. El objeto servidor: unión cerrada de tres variantes
Each server configuration MUST contain a
typefield and match exactly one of the closed variants below. An unknown field, an unknowntypevalue, or a field belonging to another variant makes that server entry invalid.
El esquema lo implementa con un oneOf:
"server": {
"title": "MCP server",
"oneOf": [
{ "$ref": "#/$defs/stdioServer" },
{ "$ref": "#/$defs/streamableHttpServer" },
{ "$ref": "#/$defs/sseServer" }
]
}
Las tres variantes llevan additionalProperties: false. Poner url en una entrada stdio o command en una streamable-http invalida esa entrada.
flowchart TD
A[Entrada en mcpServers] --> B{campo type}
B -->|stdio| C[Variante stdio con command args env cwd]
B -->|streamable-http| D[Variante Streamable HTTP con url y headers]
B -->|sse| E[Variante HTTP+SSE heredada con url y headers]
B -->|otro valor| F[Entrada invalida y se omite ese servidor]
C --> G[Las demas entradas y componentes continuan]
D --> G
E --> G
F --> G
El §7.2.1 explica por qué el esquema expone #/$defs/server: “so that clients can validate each server independently and preserve the failure boundaries in §7.2.2.” Validando entrada por entrada, una entrada rota no tumba a las demás.
3.1 Variante stdio
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
type | "stdio" | Sí | Selecciona el transporte MCP stdio. |
command | string | Sí | Token ejecutable a lanzar. |
args | string[] | No | Argumentos pasados al ejecutable. |
env | objeto de strings | No | Variables de entorno del proceso. |
cwd | string | No | Directorio de trabajo del proceso. |
command: un token, no una línea de shell. Es el campo con más reglas:
The
commandfield MUST contain a single executable token, not a shell command string. It MUST be either a bare executable name or a plugin-relative path beginning with./. Clients MUST resolve bare names using the platform’s executable search rules and MUST resolve plugin-relative paths against the plugin root. Clients MUST NOT perform placeholder expansion incommand.
- Un solo token.
"command": "node server.js --port 3000"está mal: son cuatro tokens. - Solo dos formas: nombre desnudo —
npx,node,uvx— o ruta relativa al plugin que empieza por./. - No hay expansión de placeholders en
command."${PLUGIN_ROOT}/bin/x"no se expande y, además, no es ninguna de las dos formas admitidas: la entrada de servidor queda inválida y el cliente DEBE omitirla (§7.2.2, regla 3). - El esquema solo comprueba
minLength: 1; la forma la valida el cliente.
Sobre PATH, la spec avisa: si un valor de PATH configurado participa en resolver un command desnudo es comportamiento definido por el cliente, y los plugins que reclamen conformidad NO DEBEN depender de él. Un plugin que empaqueta su ejecutable DEBE usar un command relativo al plugin.
Para Windows hay una concesión: el cliente PUEDE usar un intérprete de comandos de la plataforma —un .bat o .cmd— pero DEBE preservar command como un token y pasar args por separado.
args y env. args es un array de strings; cada elemento es un argumento independiente y el cliente no lo parte por espacios. env es un objeto de strings con una restricción muy específica en el esquema:
"env": {
"type": "object",
"propertyNames": {
"not": { "enum": ["PLUGIN_ROOT", "PLUGIN_DATA"] }
},
"additionalProperties": { "type": "string" }
}
Es la codificación del §9.2: un env NO DEBE contener entradas llamadas PLUGIN_ROOT o PLUGIN_DATA; hacerlo invalida esa configuración de servidor. Son variables reservadas que DEBE suministrar el cliente.
cwd. Cuando se omite, el cliente DEBE usar la raíz del plugin como directorio de trabajo del subproceso. Cuando está presente, DEBE tener una de estas tres formas:
- Ruta relativa al plugin que empiece por
./. - Exactamente
${PLUGIN_ROOT}o una ruta que empiece por${PLUGIN_ROOT}/. - Exactamente
${PLUGIN_DATA}o una ruta que empiece por${PLUGIN_DATA}/.
El esquema lo expresa con un pattern:
"cwd": {
"type": "string",
"pattern": "^(?:\\./|\\$\\{PLUGIN_ROOT\\}(?:/|$)|\\$\\{PLUGIN_DATA\\}(?:/|$))"
}
La descripción del propio esquema marca su límite: “Filesystem containment is validated separately.” El pattern comprueba la forma, no dónde acaba resolviendo. El cliente DEBE expandir placeholders antes de resolver; un valor relativo o anclado en ${PLUGIN_ROOT} DEBE quedar dentro de la raíz del plugin resuelta, y uno anclado en ${PLUGIN_DATA} dentro del directorio de datos. Cualquier otra forma o cualquier escape tras la resolución invalida esa entrada. Un "${PLUGIN_ROOT}/../otro" pasa el pattern y muere en la contención del cliente.
3.2 Variantes remotas: streamable-http y sse
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
type | "streamable-http" o "sse" | Sí | Selecciona el transporte MCP remoto. |
url | string | Sí | URL del endpoint MCP. |
headers | objeto de strings | No | Cabeceras HTTP fijas enviadas al origen configurado. |
La distinción entre ambas está explicada así:
streamable-httpselects the current MCP Streamable HTTP transport.sseselects the deprecated HTTP+SSE transport defined by the MCP 2024-11-05 specification; it does not refer to SSE responses or streams used within Streamable HTTP.
sse no significa “quiero streaming”: significa “quiero el transporte HTTP+SSE antiguo”. Streamable HTTP también usa streams SSE internamente y no por eso se declara sse.
Restricciones de url (§7.2.1): URL absoluta HTTP o HTTPS; sin información de usuario —https://user:pass@host/ prohibido— y sin fragmento; los endpoints que no sean loopback DEBEN usar HTTPS; HTTP PUEDE usarse solo si el host es exactamente localhost o un literal IP en rango loopback. Nada de esto está en el esquema, donde url es solo "type": "string", "minLength": 1.
Restricciones de headers: nombres y valores DEBEN ser campos de cabecera HTTP válidos. Los nombres son insensibles a mayúsculas, así que {"X-Tenant": "a", "x-tenant": "b"} es una entrada inválida. Y en las variantes remotas no hay expansión de placeholders en ningún sitio: ni en url, ni en nombres, ni en valores. Solo stdio tiene expansión.
3.3 Soporte de transportes
A client that supports Agent Plugins MCP servers MUST support at least one of
stdioorstreamable-httpand SHOULD support both. Support forsseis OPTIONAL. A client MUST use the transport declared bytypefor its initial connection attempt. Agent Plugins does not define fallback behavior if that attempt fails.
El cliente DEBE usar el transporte declarado en type para su primer intento de conexión. Qué pasa después de un fallo —reintentos, degradación a otro transporte— queda fuera del formato portable. La sección no normativa Design Decisions lo justifica: stdio y Streamable HTTP sirven a modelos de despliegue y confianza distintos, y exigir ambos ampliaría la superficie de implementación y de confianza del cliente sin cambiar el formato portable.
4. Variables de entorno y expansión
4.1 Las dos variables reservadas
Clients that launch plugin subprocesses (i.e., stdio MCP servers) MUST provide
PLUGIN_ROOTandPLUGIN_DATAin each subprocess environment.
| Variable | Qué es |
|---|---|
PLUGIN_ROOT | Ruta absoluta a la raíz del plugin ya resuelta en el sistema de archivos. |
PLUGIN_DATA | Ruta absoluta a un directorio persistente gestionado por el cliente y dedicado a esa instancia instalada. |
Sobre PLUGIN_DATA el cliente elige la ubicación, DEBE crear el directorio antes de lanzar el subproceso, DEBE hacerlo escribible para ese subproceso y DEBE preservar su contenido a través de las actualizaciones del plugin. PUEDE borrarlo al desinstalar.
El reparto de uso que recomienda la spec: PLUGIN_DATA para dependencias instaladas —node_modules, entornos virtuales—, código generado, cachés y estado que deba sobrevivir a las actualizaciones; PLUGIN_ROOT para referenciar scripts, binarios y archivos de configuración que vienen dentro del paquete.
Ejemplo literal de la spec para un cliente que carga el plugin devtools:
PLUGIN_ROOT=/home/alex/.agents/plugins/devtools
PLUGIN_DATA=/home/alex/.agents/plugins/data/devtools
4.2 Orden de construcción del entorno
flowchart LR
A[Entorno base elegido por el cliente] --> B[Se superpone env del servidor ya expandido]
B --> C[El cliente fija PLUGIN_ROOT y PLUGIN_DATA]
C --> D[Entorno final del subproceso]
El cliente elige el entorno base y PUEDE heredar, omitir o sanear variables del ambiente. Después, las entradas de env ya expandidas se superponen y reemplazan las de igual nombre. Por último, el cliente fija PLUGIN_ROOT y PLUGIN_DATA, reemplazando cualquier entrada de nombre equivalente según la semántica de nombres de entorno de la plataforma. Por eso el esquema prohíbe declararlas: aunque colaras una, el cliente la pisaría al final.
Y una obligación para los autores: salvo la búsqueda de ejecutables de la plataforma usada para resolver un command desnudo, los plugins que reclamen conformidad NO DEBEN depender de una variable del entorno base a menos que la spec la exija o la configuración del servidor la suministre explícitamente.
4.3 Reglas exactas de expansión
Expansion is a single, non-recursive textual replacement of every exact occurrence of either placeholder. Text introduced by a replacement MUST NOT be scanned for further placeholders.
| Aspecto | Regla |
|---|---|
| Placeholders reconocidos | Solo ${PLUGIN_ROOT} y ${PLUGIN_DATA} |
| Tipo de reemplazo | Textual, único, no recursivo |
| Dónde aplica | Cada string de args, cada valor de env, el string cwd |
| Dónde NO aplica | Claves de env, command, ubicaciones fijas de componentes |
| Texto no reconocido | Permanece literal |
| Otras expansiones | Prohibidas: el cliente NO DEBE hacer ninguna otra |
Que el texto no reconocido permanezca literal significa que ${HOME} o $PLUGIN_ROOT sin llaves llegan al subproceso tal como están escritos.
Ejemplo literal de la spec (§9.2):
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
"mcpServers": {
"database": {
"type": "stdio",
"command": "npx",
"args": ["--config", "${PLUGIN_ROOT}/config/db.json"],
"cwd": "${PLUGIN_ROOT}",
"env": {
"DATA_DIR": "${PLUGIN_DATA}/database"
}
}
}
}
4.4 args y env no son rutas de paquete
Un detalle del §4.1 que se pasa por alto:
Configuration values not defined as paths, including command arguments and environment variable values, are opaque strings. Clients MUST NOT interpret them as package paths for the purpose of enforcing this section.
La contención a la raíz del plugin aplica a command y cwd, que sí están definidos como rutas. No aplica a args ni a valores de env. La spec añade que estas reglas no ponen el subproceso en un sandbox ni restringen rutas suministradas en tiempo de ejecución.
5. Reglas de carga y fronteras de fallo
flowchart TD
A[Carga del plugin] --> B{mcp.json valido a nivel superior}
B -->|No| C[Se desactiva MCP para este plugin]
C --> D[Skills y demas componentes siguen cargando]
B -->|Si| E{Entrada de servidor valida}
E -->|No| F[Se omite esa entrada]
E -->|Si| G{Transporte soportado por el cliente}
G -->|No| H[Se omite esa entrada]
G -->|Si| I{Arranca conecta y completa el handshake}
I -->|No| J[Se reporta el fallo de conexion]
I -->|Si| K[Servidor disponible]
F --> L[Los demas servidores y componentes continuan]
H --> L
J --> L
K --> L
Las cinco reglas del §7.2.2:
- Los clientes que soportan MCP DEBEN cargar la configuración solo desde
mcp.jsonen la raíz del plugin. - Si
mcp.jsonno es JSON válido, apunta a una versión de Agent Plugins que el cliente no soporta ni reconoce como compatible, apunta a una versión distinta de la deplugin.json, o incumple los demás requisitos de nivel superior, el cliente DEBE desactivar MCP para ese plugin y continuar con los otros tipos de componente. DEBERÍA (SHOULD) reportarlo. - Si una entrada individual incumple los requisitos, el cliente DEBE omitir ese servidor y continuar con los demás servidores y tipos de componente. DEBERÍA reportar la entrada inválida.
- Si el cliente no soporta el transporte declarado por una entrada por lo demás válida, DEBE omitir ese servidor y continuar. DEBERÍA reportar el transporte no soportado.
- Si un servidor falla al arrancar, conectar, autenticar o completar el handshake MCP, el cliente DEBE continuar cargando los demás servidores y tipos de componente. DEBERÍA reportar el fallo de conexión.
Fíjate en la escala del daño: un $schema mal puesto tumba todo el MCP del plugin; una entrada mal formada tumba solo esa entrada; un fallo de conexión no tumba nada. Design Decisions lo resume: un plugin que aporta skills y un servidor MCP no debería quedar inutilizable porque un servidor no esté disponible.
6. Qué valida el esquema y qué valida el cliente
La descripción del propio mcp.schema.json lo anuncia: “The Agent Plugins specification defines additional semantic and operational requirements.” Y el §7.2.1 zanja quién manda: “The specification text is authoritative if it conflicts with the schema.”
| Requisito | ¿Lo pilla el JSON Schema? | ¿Quién lo aplica? |
|---|---|---|
$schema y mcpServers presentes | Sí | Esquema |
Campo raíz desconocido en mcp.json | Sí, additionalProperties: false | Esquema |
Valor exacto del identificador canónico de $schema | Sí, const | Esquema |
type con valor desconocido | Sí, ninguna variante casa | Esquema |
| Campo de otra variante mezclado | Sí, variantes cerradas | Esquema |
command no vacío | Sí, minLength: 1 | Esquema |
env con clave PLUGIN_ROOT o PLUGIN_DATA | Sí, propertyNames.not | Esquema |
Forma del string de cwd | Sí, pattern | Esquema |
command es un token único, no una línea de shell | No | Cliente |
command es nombre desnudo o ruta que empieza por ./ | No | Cliente |
Contención de command y cwd tras resolver | No | Cliente |
url absoluta, sin userinfo, sin fragmento | No | Cliente |
| HTTPS obligatorio fuera de loopback | No | Cliente |
| Cabeceras válidas y sin duplicados por capitalización | No | Cliente |
Coincidencia de versión entre plugin.json y mcp.json | No | Cliente |
Expansión de ${PLUGIN_ROOT} y ${PLUGIN_DATA} | No | Cliente |
Regla mental: el esquema valida forma; el cliente valida significado. Pasar el validador es condición necesaria, nunca suficiente. La automatización va en el capítulo 8.
Hay un tercer nivel que la spec deja fuera de su alcance: cómo el cliente expone el servidor y sus herramientas al usuario o al modelo. Igual que con las skills, eso es territorio de cada cliente.
7. Ejemplo completo: servidor MCP más skill que lo usa
Construimos release-notes: un plugin con un servidor MCP local empaquetado, uno remoto y una skill que le enseña al agente cómo usarlos.
release-notes/
├── plugin.json
├── mcp.json
├── bin/
│ └── changelog-server
├── config/
│ └── fuentes.json
└── skills/
└── publicar-notas/
├── SKILL.md
└── references/
└── formato-changelog.md
plugin.json:
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "release-notes",
"version": "1.3.0",
"description": "Genera y publica notas de version a partir del historial de commits",
"author": {
"name": "Equipo de Plataforma",
"url": "https://example.com"
},
"license": "Apache-2.0",
"keywords": ["changelog", "releases", "mcp"]
}
Sin mcpServers aquí. mcp.json:
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
"mcpServers": {
"changelog": {
"type": "stdio",
"command": "./bin/changelog-server",
"args": ["--fuentes", "${PLUGIN_ROOT}/config/fuentes.json"],
"env": {
"CACHE_DIR": "${PLUGIN_DATA}/cache",
"LOG_LEVEL": "info"
},
"cwd": "${PLUGIN_DATA}"
},
"publicador": {
"type": "streamable-http",
"url": "https://releases.example.com/mcp",
"headers": {
"X-Client-Name": "release-notes-plugin"
}
}
}
}
Cada decisión contra la spec:
"command": "./bin/changelog-server"— el ejecutable viene dentro del paquete, así que la spec obliga a la forma relativa al plugin. Unchangelog-serverdesnudo dependería de la resolución porPATH, que es comportamiento definido por el cliente.${PLUGIN_ROOT}enargsapunta a un archivo empaquetado que no cambia.${PLUGIN_DATA}enenvapunta a estado escribible que sobrevive a las actualizaciones. Una caché enPLUGIN_ROOTse perdería al actualizar."cwd": "${PLUGIN_DATA}"es una de las tres formas permitidas; el cliente expande antes de resolver y comprueba la contención.- El servidor remoto usa HTTPS porque no es loopback.
X-Client-Namees un identificador público, no una credencial.
skills/publicar-notas/SKILL.md:
---
name: publicar-notas
description: Genera notas de version desde el historial de commits y las publica en el portal de releases. Usar cuando se cierre una version, se prepare un changelog o se pida publicar release notes.
---
# Publicar notas de version
## Flujo
1. Determina el rango de commits entre la ultima etiqueta publicada y HEAD.
2. Usa la herramienta del servidor MCP `changelog` para agrupar los commits
por tipo de cambio.
3. Aplica el formato descrito en `references/formato-changelog.md`.
4. Muestra el borrador y espera confirmacion explicita antes de publicar.
5. Publica con la herramienta del servidor MCP `publicador`.
## Reglas
- Nunca publiques sin confirmacion del usuario.
- Si el servidor `publicador` no esta disponible, entrega el borrador en
formato Markdown y detente ahi.
Esa última regla no es adorno: por la regla 5 del §7.2.2, un servidor caído no impide que la skill cargue, así que conviene que su propio texto contemple ese caso. El formato de SKILL.md no lo define Agent Plugins sino la especificación de Agent Skills, que cubrimos en el curso de Agent Skills y aplicamos al empaquetado en el capítulo 4.
Nota sobre el acoplamiento: la spec no define ningún mecanismo para que una skill declare que depende de un servidor MCP del mismo plugin. No hay campo requires ni equivalente. Los dos tipos de componente se descubren de forma independiente desde sus ubicaciones fijas y sus fallos son independientes. El único vínculo real es el texto de la skill.
8. Seguridad al distribuir configuración ejecutable
Un mcp.json con una entrada stdio es, en el fondo, una instrucción para ejecutar un proceso en la máquina de quien instale el plugin. Eso cambia el perfil de riesgo respecto a un plugin que solo trae skills.
8.1 Lo que la spec prohíbe explícitamente
Dos prohibiciones idénticas en fondo, una para cada superficie. Sobre env (§9.2):
Configured
envvalues are visible package data, not a portable secret mechanism. Plugins MUST NOT embed credentials or other secrets inenv.
Sobre headers (§7.2.1):
Header values are visible package data, not a portable secret mechanism. Plugins MUST NOT embed credentials or other secrets in
headers.
Datos visibles del paquete. Un plugin es un directorio inspeccionable con cat y versionable con git. Cualquier token que metas ahí queda en el repositorio, en la caché del cliente y en cualquier copia del paquete.
8.2 Qué hacer con las credenciales
Agent Plugins v1 defines no OAuth configuration or portable credential-reference fields. Authorization discovery, user interaction, and credential storage are client-managed. An authorization failure is a connection failure for that server, not invalid plugin configuration.
No existe mecanismo portable de credenciales en v1: ni campos de OAuth, ni referencias a secretos. Todo eso lo gestiona el cliente. Y la clasificación importa: si falta autorización, eso es un fallo de conexión de ese servidor, no una configuración inválida. El plugin sigue siendo válido.
8.3 Protecciones que la spec impone al cliente
| Protección | Sección |
|---|---|
command y cwd no pueden resolver fuera de la raíz del plugin | §4.1 |
cwd anclado en ${PLUGIN_DATA} no puede salir de ese directorio | §7.2.1 |
command es un token, nunca una línea de shell interpretada | §7.2.1 |
Cero expansión de placeholders en command | §7.2.1 |
Cero expansión en url y cabeceras | §7.2.1 |
HTTPS obligatorio fuera de loopback y sin userinfo en la url | §7.2.1 |
| El cliente no descarga esquemas al cargar un plugin | §7.2.1 |
| Las cabeceras generadas por el cliente ganan a las configuradas | §7.2.1 |
| Sin reenvío de cabeceras a otro origen sin autorización explícita | §7.2.1 |
Esta última merece cita literal:
Headers generated by the client to implement HTTP, MCP, or authorization take precedence over configured headers with the same case-insensitive name. A client MUST NOT forward configured headers to a different origin through a redirect or legacy SSE endpoint event without explicit user authorization.
Un plugin no puede secuestrar la cabecera Authorization que construye el cliente, ni conseguir que sus cabeceras viajen a un tercer origen por una redirección.
8.4 Lo que la spec no cubre
These containment rules govern access to files supplied by the plugin package. They do not sandbox a plugin subprocess or restrict paths supplied at runtime.
Una vez lanzado, el subproceso es un proceso normal del sistema. Agent Plugins no lo aísla: puede leer y escribir donde el usuario pueda. La contención de rutas protege el acceso del cliente a los archivos del paquete, no lo que el proceso haga después.
8.5 Checklist antes de publicar
- Ninguna credencial en
envni enheaders. -
commandes un token: nombre desnudo o ruta./. - Si empaquetas el binario,
commandusa la forma./. - No dependes de que
PATHenenvafecte a uncommanddesnudo. - Todo endpoint que no sea loopback usa HTTPS, sin userinfo ni fragmento.
- Sin nombres de cabecera repetidos con distinta capitalización.
- Estado escribible bajo
${PLUGIN_DATA}, nunca bajo${PLUGIN_ROOT}. -
envno declaraPLUGIN_ROOTniPLUGIN_DATA. -
$schemademcp.jsoncoincide en versión con el deplugin.json. - Las skills describen qué hacer cuando un servidor no está.
-
mcp.jsonvalidado contraschemas/1.0.0/mcp.schema.json.
La validación mecánica es un comando:
uvx check-jsonschema \
--schemafile aps/schemas/1.0.0/mcp.schema.json \
ruta/a/tu-plugin/mcp.json
Aviso práctico: por el oneOf de las tres variantes, los mensajes de error son ruidosos. Un env con PLUGIN_ROOT produce un Best Match engañoso del tipo 'url' is a required property —viene de intentar casar contra la variante HTTP— y el error real aparece bajo Best Deep Match. Lo desmenuzamos en el capítulo 8.
9. MCP en catálogos reales
El repositorio público google/skills distribuye plugins que empaquetan skills y, donde aplica, servidores MCP. Su README del directorio de plugins de datos lo dice así: “These plugins package product-specific Skills and (where applicable) MCP servers for their product’s common user journeys.”
Un matiz de honestidad técnica: esos plugins no declaran conformidad con Agent Plugins 1.0.0 ni usan necesariamente el mcp.json de esta especificación. Son un catálogo real de la misma familia de problemas, útil para ver cómo se distribuyen paquetes de skills y servidores MCP en la práctica. Lo recorremos en el curso de Google Skills y en el capítulo 9.
Resumen
- La configuración MCP vive solo en
mcp.jsonen la raíz del plugin. Declararla dentro deplugin.jsonestá prohibido (MUST NOT) y produce un fallo silencioso: el campo se ignora y el servidor no existe. mcp.jsones un objeto cerrado con exactamente dos campos:$schema—identificador canónico exacto— ymcpServers, que puede estar vacío.- Cada entrada de
mcpServerscasa con exactamente una de tres variantes cerradas:stdio,streamable-httpysse. Mezclar campos entre variantes invalida la entrada. commandes un token: nombre desnudo o ruta que empieza por./. Nunca una línea de shell, nunca con placeholders.${PLUGIN_ROOT}y${PLUGIN_DATA}se expanden solo enargs, valores deenvycwd. Reemplazo textual único y no recursivo.envno puede declarar esos nombres.- Un cliente que soporte MCP DEBE implementar al menos
stdioostreamable-httpy DEBERÍA implementar ambos;ssees OPCIONAL. - Las fronteras de fallo tienen tres tamaños: documento inválido desactiva MCP del plugin; entrada inválida omite solo esa entrada; fallo de conexión no desactiva nada.
- El JSON Schema valida forma; el cliente valida significado: contención de rutas, forma de
command, URLs absolutas, HTTPS fuera de loopback, cabeceras válidas y coincidencia de versión entre los dos archivos. envyheadersson datos visibles del paquete. Los plugins NO DEBEN incrustar credenciales ahí. v1 no define ningún mecanismo portable de credenciales.- Las reglas de contención protegen el acceso a los archivos del paquete; no ponen el subproceso en un sandbox.
Siguiente: Descubrimiento, resolución y carga por parte del cliente