Cómo crear un plugin para Claude Cowork (y Claude Code) a partir de tus skills

Para convertir tus skills en un plugin basta un .claude-plugin/plugin.json que apunte a ellas, sin moverlas de sitio. Si además quieres incluir un MCP remoto con OAuth, lo declaras en .mcp.json sin header de autorización; después lo pruebas en local con claude --plugin-dir y lo publicas con un marketplace.json.
Si ya tienes skills que usas a diario en Claude Code, empaquetarlas en un plugin es el paso que las vuelve compartibles, versionables y reutilizables entre proyectos. Y el mismo plugin funciona en Claude Cowork y en Claude Code, sin cambios. Si todavía no tienes claro qué empaqueta un plugin ni cómo se instala, empieza por Plugins de Claude Code: crear, instalar y compartir.
Claude Cowork y Claude Code usan el mismo formato
El repositorio oficial de plugins para Claude Cowork lo dice de forma literal:
"Built for Claude Cowork, also compatible with Claude Code."
Y en su sección de instalación, los mismos plugins se instalan en Cowork desde claude.com/plugins y en Claude Code con claude plugin marketplace add anthropics/knowledge-work-plugins.
Es decir, construyes el plugin una vez y corre en los dos productos. La estructura de directorios, el manifest y la forma de declarar componentes son idénticos. Solo cambia la distribución: en Cowork los plugins se instalan desde el portal en claude.com/plugins/.
Por eso el resto del artículo se apoya en la referencia de Claude Code: es la misma especificación.
La estructura de un plugin
Un plugin es un directorio con un manifest y, opcionalmente, carpetas para cada tipo de componente:
mi-plugin/
├── .claude-plugin/
│ └── plugin.json # El manifest (lo único que va aquí dentro)
├── skills/ # Skills: <nombre>/SKILL.md, Claude las usa automáticamente
├── commands/ # Skills como archivos .md planos (formato heredado)
├── agents/ # Subagentes
├── hooks/ # hooks.json con manejadores de eventos
└── .mcp.json # Servidores MCP, en la RAÍZ del pluginHay un error que la propia doc advierte de forma explícita:
"Only
plugin.jsongoes inside.claude-plugin/. Components saved there don't load."
Dentro de .claude-plugin/ va solo el plugin.json. Todo lo demás, incluido .mcp.json, vive en la raíz del plugin, un nivel más arriba. Si pones skills/ dentro de .claude-plugin/, el plugin carga pero tus skills no aparecen.
El manifest: plugin.json
El manifest describe la identidad del plugin. Lo bueno es que casi todo es opcional, empezando por el propio manifest: sin él, Claude Code carga los componentes que encuentra en las carpetas estándar. Si lo incluyes, la referencia de plugins es clara:
"
nameis the only required key."
Un manifest mínimo válido es literalmente esto:
{
"name": "mi-plugin"
}El name cumple doble función: identifica el plugin y sirve de namespace para sus componentes. Una skill review dentro de un plugin llamado mi-plugin se invoca como /mi-plugin:review. Ese namespacing es lo que evita choques cuando tienes varios plugins con skills del mismo nombre.
En la práctica vas a querer agregar metadatos. El schema completo soporta, entre otros campos:
{
"name": "mi-plugin",
"displayName": "Mi Plugin",
"version": "1.0.0",
"description": "Qué hace el plugin, en una línea",
"author": {
"name": "Tu Nombre",
"email": "tu@correo.com",
"url": "https://github.com/tu-usuario"
},
"homepage": "https://tu-sitio.com/mi-plugin",
"repository": "https://github.com/tu-usuario/mi-plugin",
"license": "MIT",
"keywords": ["skills", "automatizacion"]
}El campo version cambia cómo reciben las actualizaciones tus usuarios. Según la documentación de versiones, si lo fijas, el plugin queda en esa versión hasta que lo cambies; si no hay version ni en el manifest ni en la entrada del marketplace y el plugin sale de un repo git, Claude Code usa el SHA del commit como versión.
Si fijas version, controlas tú el ritmo de releases. Si la omites y distribuyes por git, cada commit cuenta como versión nueva. Para algo que cambia seguido, fijar la versión te ahorra ruido.
Apuntar a skills que ya tienes, sin moverlas
El manifest acepta rutas custom para cada tipo de componente. El campo skills está documentado así:
skills(tipo "Path, or array of paths"): "Directories to scan for skills, each a directory of<name>/SKILL.mdfolders or one folder holdingSKILL.mddirectly. [...] Adds to the defaultskills/scan"
O sea, no tienes que mover tus skills a la carpeta skills/ del plugin. Puedes dejarlas donde viven dentro del repo y apuntar el manifest hacia ellas:
{
"name": "mi-plugin",
"skills": "./ruta/a/mis/skills/"
}La condición es que la ruta empiece con ./ y quede dentro de la raíz del plugin: una ruta que sale de ella (con .., por ejemplo) no carga. commands, agents, hooks y mcpServers también aceptan rutas, con una diferencia: en commands y agents la ruta reemplaza la carpeta por defecto, mientras que skills, hooks y mcpServers se suman a ella. Así un repo que ya tiene una carpeta de skills se convierte en plugin con un plugin.json que apenas las referencia.
De dónde sale el nombre con el que invocas una skill
En un plugin, el name del frontmatter manda sobre el nombre de la carpeta. La doc de skills lo dice así:
"In a plugin skill,
namesets the last segment of the command and the plugin prefix stays in place."
En concreto:
Ubicación del SKILL.md |
Qué determina el comando |
|---|---|
Subdirectorio skills/ del plugin |
El name del frontmatter o, si no lo hay, el nombre del directorio, con el namespace del plugin: mi-plugin/skills/review/SKILL.md da /mi-plugin:review, o /mi-plugin:fancy con name: fancy |
SKILL.md en la raíz del plugin |
El campo name del frontmatter, con el nombre del directorio del plugin como fallback |
O sea: si no pones name, manda el nombre de la carpeta; si lo pones, ese valor reemplaza el último segmento del comando y el prefijo del plugin se mantiene. El description del frontmatter, eso sí, siempre importa: es lo que Claude lee para decidir cuándo cargar la skill automáticamente.
Probar el plugin en local antes de publicar
No necesitas publicar nada para probar. La doc da dos herramientas:
# Cargar el plugin directamente, sin instalarlo
claude --plugin-dir ./mi-plugin
# Validar la estructura y el manifest
claude plugin validate ./mi-pluginMientras desarrollas, /reload-plugins recarga skills, agentes, hooks y servidores MCP del plugin sin reiniciar la sesión. Y antes de publicar, vale correr la validación con --strict, que convierte los warnings (por ejemplo, un campo mal escrito en el manifest) en errores:
claude plugin validate ./mi-plugin --strictConectar un MCP remoto con OAuth, sin API key
Un plugin puede traer servidores MCP que se conectan solos cuando el plugin está activo. Se declaran en .mcp.json, en la raíz del plugin. Para un servidor remoto el formato es mínimo:
{
"mcpServers": {
"thatseoagent": {
"type": "http",
"url": "https://thatseoagent.com/api/mcp"
}
}
}No hay header Authorization ni API key, y es a propósito.
Por qué omitir el header dispara OAuth
Cuando Claude Code intenta usar un servidor remoto, lo marca como "necesita autenticación" si el servidor responde 401 Unauthorized o 403 Forbidden:
"Claude Code marks a remote server as needing authentication when the server responds with
401 Unauthorizedor403 Forbidden. ... A custom server that returns aWWW-Authenticateheader pointing to its authorization server gets the same automatic discovery as any other remote server."
A partir de ese 401, Claude Code descubre los endpoints de OAuth por el camino estándar:
"By default, Claude Code first checks RFC 9728 Protected Resource Metadata at
/.well-known/oauth-protected-resource, then falls back to RFC 8414 authorization server metadata at/.well-known/oauth-authorization-server."
Si el authorization server soporta Dynamic Client Registration (DCR), Claude Code se registra como cliente OAuth solo, sin que tú ni el usuario tengan que crear una app a mano. Lo sabemos por el contrario: la doc dice que cuando el server no soporta DCR aparece el error "Incompatible auth server: does not support dynamic client registration" y hay que cargar credenciales manualmente. Si DCR está disponible, ese paso desaparece.
Para el usuario, todo se reduce a instalar el plugin: la primera vez que usa la herramienta se abre el navegador, hace login y listo, sin API key ni tokens que copiar.
La verificación contra el endpoint real
Lo confirmé inspeccionando los .well-known del propio servidor MCP de That SEO Agent. El primero, el protected-resource metadata:
curl https://thatseoagent.com/.well-known/oauth-protected-resource{
"resource": "https://thatseoagent.com/api/mcp",
"authorization_servers": ["https://thatseoagent.com"],
"scopes_supported": ["mcp"],
"bearer_methods_supported": ["header"]
}Ese authorization_servers apunta a dónde buscar el segundo documento, el del authorization server:
curl https://thatseoagent.com/.well-known/oauth-authorization-server{
"issuer": "https://thatseoagent.com",
"authorization_endpoint": "https://thatseoagent.com/oauth/authorize",
"token_endpoint": "https://thatseoagent.com/oauth/token",
"client_id_metadata_document_supported": true,
"registration_endpoint": "https://thatseoagent.com/oauth/register",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code"],
"token_endpoint_auth_methods_supported": ["none"],
"code_challenge_methods_supported": ["S256"],
"scopes_supported": ["mcp"]
}Las dos claves que hacen funcionar todo el flujo sin configuración:
registration_endpointpresente: el server soporta DCR, así que Claude Code se registra solo.code_challenge_methods_supported: ["S256"]: hay PKCE, el flujo de autorización es seguro para un cliente público.
El server-card (/.well-known/mcp/server-card.json) declara transporte streamable-http y authentication.type igual a oauth2, lo mismo que describe la doc.
Un header inválido no cae a OAuth
Por eso conviene omitir el header en lugar de poner uno "por las dudas": si configuras un Authorization inválido, el flujo OAuth no se activa como fallback:
"If you configured
headers.Authorizationfor the server and the server rejects that header, Claude Code reports the connection as failed instead of falling back to OAuth. Check that the token is valid for the MCP endpoint, or remove the header to use the OAuth flow."
Para que OAuth se dispare solo, no pongas el header. Un header presente pero rechazado se interpreta como "el usuario quiso autenticarse con token y falló", no como "probemos OAuth".
Distinto es si tu MCP usa una API key estática y no OAuth. Ahí el patrón correcto es otro, con el header y expansión de variables de entorno para no hardcodear el secreto:
{
"mcpServers": {
"mi-api": {
"type": "http",
"url": "https://api.example.com/mcp",
"headers": {
"Authorization": "Bearer ${MI_API_KEY}"
}
}
}
}Pero si el server soporta OAuth con DCR, como el del ejemplo, el camino sin header es más limpio para quien instala el plugin.
Distribuir: el marketplace.json
Para que otros instalen tu plugin con un par de comandos, necesitas un marketplace: un catálogo marketplace.json que vive en .claude-plugin/marketplace.json dentro de tu repo. Los campos requeridos son name, owner y plugins:
{
"name": "mis-plugins",
"owner": {
"name": "Tu Nombre"
},
"plugins": [
{
"name": "mi-plugin",
"source": "./plugins/mi-plugin",
"description": "Qué hace el plugin"
}
]
}Cada entrada en plugins[] necesita como mínimo name y source. El source dice de dónde sacar ese plugin. Para uno que vive en el mismo repo, se usa una ruta relativa, y la doc es precisa sobre la regla:
Relative path: "A directory inside the marketplace, resolved from the marketplace root. Must start with
./[...]."."on its own means the root itself"
Auto-hospedar cuando el plugin es el repo
Si tu repositorio es un único plugin (su plugin.json está en .claude-plugin/plugin.json, en la raíz), puedes poner el marketplace.json al lado y apuntar el source a la raíz misma:
{
"name": "mis-plugins",
"owner": { "name": "Tu Nombre" },
"plugins": [
{
"name": "mi-plugin",
"source": "./",
"description": "El plugin es el repo entero"
}
]
}Como el source se resuelve desde la raíz del marketplace (el directorio que contiene .claude-plugin/), "./" apunta al repo completo, que es justo donde está el plugin. Es la forma más simple de auto-hospedar: un repo, un plugin, un marketplace, sin subdirectorios. La referencia del marketplace lo contempla de forma explícita: "." sola también significa la raíz.
Instalación para quien usa tu plugin
Con el marketplace en un repo de GitHub, cualquiera lo instala en dos pasos:
# 1. Registrar tu marketplace (atajo owner/repo de GitHub)
/plugin marketplace add tu-usuario/tu-repo
# 2. Instalar el plugin desde ese marketplace
/plugin install mi-plugin@mis-pluginsEn el formato <plugin>@<marketplace>, el marketplace es el name que pusiste en tu marketplace.json, no el nombre del repo. A partir de ahí, las skills quedan disponibles como /mi-plugin:nombre-skill y, si incluiste un .mcp.json con un server OAuth, la primera vez que se use se abrirá el navegador para autenticar.
Si lo que quieres compartir son skills sueltas y no un plugin con MCP y comandos, el plugin es demasiada ceremonia: npx skills las instala directo desde cualquier repo de GitHub, sin manifest ni marketplace.
En resumen
- Junta tus skills (no hace falta moverlas: el manifest apunta a ellas con el campo
skills). - Crea
.claude-plugin/plugin.jsoncon al menosname. - Si vas a traer un MCP remoto con OAuth, agrega
.mcp.jsonen la raíz sin header de autorización. - Prueba en local con
claude --plugin-dir ./mi-pluginy valida conclaude plugin validate --strict. - Publica con un
marketplace.jsony comparte los dos comandos de instalación.
Preguntas frecuentes
¿Un plugin de Claude Code funciona igual en Claude Cowork?
Sí. El repositorio oficial de plugins para Cowork lo confirma: ambas plataformas usan la misma estructura de plugin. Construyes el paquete una vez y corre en los dos productos; lo único que cambia es la vía de distribución.
¿Tengo que mover mis skills a la carpeta del plugin?
No. El campo skills del manifest acepta rutas custom, así que puedes dejar tus skills donde ya viven y apuntar el plugin.json hacia esa carpeta.
¿Necesito publicar un marketplace para probar el plugin?
No. Cárgalo directo con claude --plugin-dir ./mi-plugin, sin instalar nada. Usa /reload-plugins para recargar cambios sin reiniciar la sesión, y claude plugin validate para revisar el manifest.
¿Cómo conecto un MCP con OAuth sin pedirle una API key al usuario?
Declara el server en .mcp.json sin header Authorization. Si el servidor responde 401 y expone /.well-known/oauth-protected-resource con un authorization server que soporta Dynamic Client Registration, Claude Code se registra solo y abre el navegador para el login.
¿Por qué mi MCP falla en vez de abrir el navegador para OAuth?
Casi siempre porque dejaste un header Authorization inválido. Según la doc, si configuras ese header y el servidor lo rechaza, Claude Code reporta la conexión como fallida en lugar de caer a OAuth. Quita el header para que se dispare el flujo del navegador.
¿Qué pasa si no defino el campo version en el manifest?
Si lo omites y distribuyes por git, Claude Code usa el commit SHA y cada commit cuenta como una versión nueva. Si fijas version, tus usuarios solo reciben actualizaciones cuando subes ese número.
Instalé el plugin pero mi skill no aparece, ¿qué reviso?
Lo primero: que la carpeta skills/ esté en la raíz del plugin y no dentro de .claude-plugin/, que es el error más común. Dentro de .claude-plugin/ va solo el plugin.json. Si la estructura está bien, corre /reload-plugins.
¿El nombre con el que invoco la skill sale del frontmatter o del archivo?
De los dos. Si el frontmatter tiene name, ese valor fija el último segmento del comando, con el namespace del plugin delante (name: fancy da /mi-plugin:fancy). Si no lo tiene, se usa el nombre del directorio de la skill (mi-plugin/skills/review/SKILL.md da /mi-plugin:review).
Fuentes
- Create plugins (code.claude.com)
- Plugins reference (code.claude.com)
- Create and distribute a plugin marketplace (code.claude.com)
- Connect Claude Code to tools via MCP (code.claude.com)
- Extend Claude with skills (code.claude.com)
- anthropics/knowledge-work-plugins (GitHub)
- oauth-protected-resource de That SEO Agent (.well-known)
- oauth-authorization-server de That SEO Agent (.well-known)
- server-card del MCP de That SEO Agent (.well-known)
¿Tienes un proyecto en mente?
Trabajo con Laravel, WordPress, SEO técnico y servidores MCP. El primer paso es una llamada de descubrimiento, sin costo ni compromiso, donde me cuentas qué necesitas y te digo con honestidad si puedo ayudarte.