Claude Code

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

Autorangel cruz
Actualizado
Publicado
Lectura12 min de lectura
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 plugin

Hay un error que la propia doc advierte de forma explícita:

"Only plugin.json goes 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:

"name is 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.md folders or one folder holding SKILL.md directly. [...] Adds to the default skills/ 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, name sets 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-plugin

Mientras 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 --strict

Conectar 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 Unauthorized or 403 Forbidden. ... A custom server that returns a WWW-Authenticate header 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_endpoint presente: 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.Authorization for 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-plugins

En 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

  1. Junta tus skills (no hace falta moverlas: el manifest apunta a ellas con el campo skills).
  2. Crea .claude-plugin/plugin.json con al menos name.
  3. Si vas a traer un MCP remoto con OAuth, agrega .mcp.json en la raíz sin header de autorización.
  4. Prueba en local con claude --plugin-dir ./mi-plugin y valida con claude plugin validate --strict.
  5. Publica con un marketplace.json y 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

¿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.

Hablemos