---
title: "Cómo crear un plugin para Claude Cowork (y Claude Code) a partir de tus skills"
excerpt: "Guía práctica para empaquetar tus skills en un plugin que funciona igual en Claude Cowork y Claude Code, con el truco de OAuth para conectar un MCP remoto sin API key."
date: "2026-05-31T12:00:00.000Z"
lastModified: "2026-10-11T12:00:00.000Z"
category: "Claude Code"
tech_article: true
author:
  name: "angel cruz"
  picture: "/images/me/angel-cruz.png"
ogImage:
  url: "/images/open-graph/og-claude-cowork.jpg"
seo_title: "Crear un plugin de Claude Cowork y Claude Code con skills"
seo_description: "Empaqueta tus skills en un plugin para Claude Cowork y Claude Code: manifest, .mcp.json y marketplace, más el truco de OAuth para un MCP remoto."
---

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](/post/plugins-claude-code).

## 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:

```text
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](https://code.claude.com/docs/en/plugins-reference) es clara:

> "`name` is the only required key."

Un manifest mínimo válido es literalmente esto:

```json
{
  "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:

```json
{
  "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](https://code.claude.com/docs/en/plugins/loading), 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:

```json
{
  "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](https://code.claude.com/docs/en/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:

```bash
# 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:

```bash
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:

```json
{
  "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:

```bash
curl https://thatseoagent.com/.well-known/oauth-protected-resource
```

```json
{
  "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:

```bash
curl https://thatseoagent.com/.well-known/oauth-authorization-server
```

```json
{
  "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:

```json
{
  "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`:

```json
{
  "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:

```json
{
  "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:

```bash
# 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`](/post/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

- [Create plugins (code.claude.com)](https://code.claude.com/docs/en/plugins)
- [Plugins reference (code.claude.com)](https://code.claude.com/docs/en/plugins-reference)
- [Create and distribute a plugin marketplace (code.claude.com)](https://code.claude.com/docs/en/plugin-marketplaces)
- [Connect Claude Code to tools via MCP (code.claude.com)](https://code.claude.com/docs/en/mcp)
- [Extend Claude with skills (code.claude.com)](https://code.claude.com/docs/en/skills)
- [anthropics/knowledge-work-plugins (GitHub)](https://github.com/anthropics/knowledge-work-plugins)
- [oauth-protected-resource de That SEO Agent (.well-known)](https://thatseoagent.com/.well-known/oauth-protected-resource)
- [oauth-authorization-server de That SEO Agent (.well-known)](https://thatseoagent.com/.well-known/oauth-authorization-server)
- [server-card del MCP de That SEO Agent (.well-known)](https://thatseoagent.com/.well-known/mcp/server-card.json)

---

## Sitemap

Índice completo del sitio: [/sitemap.md](https://www.angelcruz.dev/sitemap.md)

Canónico HTML: [https://www.angelcruz.dev/post/crear-plugin-claude-cowork-claude-code-desde-skills](https://www.angelcruz.dev/post/crear-plugin-claude-cowork-claude-code-desde-skills)
