Tu servidor MCP no debería funcionar con una clave de API compartida

ai-agents

Imagina una publicación de tu tablero de comentarios que pasó a la columna Declined durante la noche. El historial dice que lo hizo el proyecto.

Nadie del equipo recuerda haberlo hecho. Lo hizo el asistente de alguien, con la clave MCP que todos copiaron de la misma página de configuración, y averiguar de quién cuesta una ronda de mensajes.

Así funcionan hoy la mayoría de los servidores MCP, porque toman prestado el modelo de las claves de API. En Kookee acabo de sustituirlo por tokens personales. Explico por qué la clave compartida no aguanta, qué lancé en su lugar y qué detalles resultaron importantes.

Una clave compartida es una costumbre heredada de las claves de API

Una clave de API identifica una aplicación. Una app, una clave, y la clave solo puede hacer lo que la app necesita: en la API pública de Kookee, leer contenido publicado. Para aplicaciones, ese modelo funciona bien.

Un servidor MCP conecta otra cosa: el asistente de una persona, que sigue las instrucciones de esa persona y cambia cosas. Dale una clave de proyecto y se rompen tres cosas a la vez.

Nadie lo hizo. Cada cambio se atribuye al proyecto. Cuando una publicación cambia de categoría, de responsable o recibe una respuesta, no sabes qué asistente de qué compañero lo hizo, ni si lo hizo una persona.

Todos pueden hacerlo todo. Una clave significa un único nivel de acceso. El compañero que solo quería que su asistente resumiera los comentarios tiene una clave que puede publicar.

No puedes retirársela a una sola persona. Cuando alguien deja el equipo, la única forma de cortarle el acceso a su asistente es rotar la clave, y eso rompe la configuración de todos los demás. Así que, en la práctica, nadie la rota.

Y una clave que todos copian acaba en todas partes: en dotfiles, en un .mcp.json subido al repositorio, en una captura de pantalla de un hilo de soporte.

Lo que lancé: un token por persona y por proyecto

Ahora cada miembro crea su propio token en la página del servidor MCP del proyecto. El token actúa como ese miembro —su rol, sus permisos, comprobados en cada petición— y cada cambio que hace queda registrado a su nombre.

Diálogo para crear un token MCP: un nombre para el token y una elección entre Solo lectura, seleccionado por defecto, y Lectura y escritura

Solo lectura es la opción por defecto. El acceso de escritura es un segundo clic deliberado.

Seis detalles que deciden si funciona

1. Solo lectura significa que las herramientas de escritura no están

La implementación obvia rechaza la escritura cuando se llama a la herramienta. Pero la herramienta sigue anunciándose: el modelo ve update_feedback en su lista de herramientas, planifica toda una clasificación en torno a ella y falla en el último paso con un error que luego intenta sortear.

Por eso una sesión de solo lectura nunca registra las herramientas de escritura, y sus instrucciones de servidor omiten todos los flujos de escritura y explican cómo conseguir acceso de escritura. A grandes rasgos:

TypeScript
registerReadTools(server, auth);
if (auth.token.canWrite) {
  registerWriteTools(server, auth);
}

Así el asistente le dice claramente al usuario que su token es de solo lectura, en vez de improvisar.

2. Comprueba en cada petición a la persona que hay detrás del token

Un token sobrevive al motivo por el que se emitió. Por eso validarlo no termina en «el hash coincide»: se carga al miembro que hay detrás del token y se comprueba que su cuenta está activa, que el proyecto no está baneado y que sigue siendo miembro.

TypeScript
if (!plain.startsWith('kkm_')) return fail('not_mcp_token');
const token = await findTokenByHash(sha256(plain));
if (!token) return fail('not_found');
if (!token.user.isActive) return fail('inactive');
if (token.project.bannedAt) return fail('banned');
if (!(await isMember(token.projectId, token.userId))) return fail('not_member');

Eliminar o banear a un miembro también borra sus tokens, en la misma transacción que la membresía. La comprobación en cada petición ya lo deja fuera; borrar las filas mantiene honesta la lista de tokens.

3. Guarda un hash, muéstralo una vez, dale un prefijo

Solo se guarda un hash SHA-256 del token. El texto en claro se muestra una vez, en el diálogo que lo crea, y a partir de ahí la lista muestra los cuatro últimos caracteres.

El prefijo cumple dos funciones. kkm_ es distinto del kk_ de una clave de API, así que un escáner de secretos, o una persona, sabe de qué tipo es lo que se filtró. Y con él el servidor devuelve un error útil: una clave de API enviada al endpoint MCP recibe MCP_PERSONAL_TOKEN_REQUIRED, que le dice a la persona exactamente qué hacer. Cualquier otro fallo recibe un único MCP_TOKEN_INVALID genérico, para que nadie pueda sondear un token y averiguar por qué se rechazó; el motivo exacto solo va al log del servidor.

4. Pon la atribución donde la gente mira

Un registro de auditoría es el sitio donde la responsabilidad va a que nadie la mire. Kookee anota ahí cada cambio hecho por MCP, pero lo que importa es lo visible: cada escritura guarda el id del token junto al evento de actividad o al comentario que crea, y el panel los marca como vía MCP.

Una publicación del tablero de comentarios movida a Planned, asignada y respondida por un asistente, con cada cambio marcado como vía MCP

La herramienta de historial que lee el asistente informa de la misma marca, así que la pregunta «¿quién movió esto a Planned, y fue un asistente?» tiene respuesta desde los dos lados. La API pública de comentarios la elimina: los visitantes ven una respuesta del equipo, nunca un id de token.

5. Limita las escrituras por persona y por herramienta

Un agente desbocado que reintenta una edición fallida puede hacer cientos de llamadas por minuto. Un límite de escritura por proyecto bloquearía a los compañeros que no han hecho nada malo, y un límite por token se esquiva creando un segundo token.

Por eso las herramientas de escritura tienen un límite por miembro y por herramienta: 60 llamadas por minuto a update_feedback, y otras 60 aparte a create_comment. La ventana MCP de todo el proyecto también va separada de la de la API pública, así que un agente que lanza llamadas en paralelo no deja sin cuota a tu sitio de cara a los clientes.

6. Mantén la configuración compartida sin secretos

Un equipo quiere un único .mcp.json en el repositorio, para que todos tengan la misma configuración. Con tokens personales, ese archivo no debe contener ninguno. En su lugar, lee el token del entorno de cada persona:

JSON
{
  "mcpServers": {
    "kookee": {
      "type": "http",
      "url": "https://api.kookee.dev/api/mcp",
      "headers": { "Authorization": "Bearer ${KOOKEE_MCP_TOKEN}" }
    }
  }
}

Cada compañero pone su propio token en KOOKEE_MCP_TOKEN. El comando que muestra el panel para Claude Code hace lo mismo con $KOOKEE_MCP_TOKEN, salvo justo después de crear un token, cuando lo rellena por ti.

Si tienes un servidor MCP

  • Emite credenciales por persona y por espacio de trabajo. Un cliente MCP es el agente de una persona, no una aplicación.

  • Haz que solo lectura sea la opción por defecto, y no registres herramientas que un token no puede usar.

  • Vuelve a comprobar en cada petición a la persona que hay detrás del token: cuenta, membresía, suspensión.

  • Guarda hashes, muestra los tokens una sola vez y dales un prefijo propio.

  • Distingue «tipo de credencial equivocado» de «no válida», y no digas nada más.

  • Registra el token con cada cambio, y muéstralo donde la gente lee el historial.

  • Limita las escrituras por persona y por herramienta, aparte de tu API pública.

  • Mantén sin secretos la configuración compartida de los clientes, con variables de entorno.


El servidor MCP de Kookee ya usa tokens personales, y los asistentes pueden leer y clasificar tu tablero de comentarios. Consulta Gestionar tu contenido desde un asistente de IA (MCP) para la configuración, o Clasificar comentarios desde un asistente de IA para las nuevas herramientas.