MCP stdio o sockets Unix depende del límite de confianza
Comparación de MCP stdio y sockets Unix para un gestor de secretos de escritorio: vida útil, permisos, identidad, limpieza y despliegue.

Un gestor de secretos de escritorio debería usar stdio cuando la autoridad pertenece a un solo proceso agente y un socket de dominio Unix cuando pertenece a un servicio local compartido. Parece una elección de transporte, pero mover bytes es la parte fácil. Lo difícil es decidir quién puede provocar una acción, cuánto dura ese permiso y qué pruebas quedan cuando cualquiera de los dos lados falla.
He visto equipos empezar con un socket porque parece infraestructura y pasar después semanas reconstruyendo la atribución del llamante y los controles de ciclo de vida que el árbol de procesos ya proporcionaba. También he visto cómo se estiraba stdio hasta convertirlo en un servicio residente con capas de lanzadores, multiplexores y estado oculto. Ambos transportes pueden ser seguros. Cualquiera puede invalidar el modelo de amenazas sin hacer ruido cuando sus supuestos de duración e identidad no coinciden con la autoridad concedida.
Esta comparación parte de un gestor de escritorio para macOS que conserva credenciales de API o SSH y ejecuta acciones para agentes de IA locales. El agente nunca debe recibir el secreto. Por tanto, el gestor protege algo más importante que una caché local: decide qué proceso puede convertir la autoridad almacenada en un efecto externo.
Elige el límite de confianza antes que el transporte
El transporte correcto se deriva de la unidad de autorización. Si una persona aprueba una ejecución del agente, un servidor stdio hijo ofrece un límite natural: la tubería existe para esa relación entre procesos y se cierra cuando termina uno de sus extremos. Si varias herramientas deben compartir un gestor desbloqueado, un socket Unix les da un punto de encuentro estable, pero el gestor debe crear su propio límite de sesión sobre el socket.
Redacta el modelo de amenazas como actores y acciones, no como un deseo genérico de tener IPC seguro. Nombra los procesos locales que pueden conectarse, las credenciales que guarda el gestor, las operaciones que permiten y los sucesos que deben cortar el acceso. Incluye malware ejecutado como el mismo usuario. Los permisos de archivo suelen detener a otros usuarios, pero hacen poco contra otro proceso de la misma cuenta. Incluye también una copia del binario del agente, un complemento modificado, una shell iniciada por el agente y un proceso antiguo que siga vivo después de terminar el trabajo visible.
Después define qué significa una aprobación. Podría autorizar un ejecutable firmado, un proceso del sistema operativo, un árbol de procesos, un trabajo del terminal o a todos los clientes del usuario conectado. Son promesas distintas. Un transporte no puede elegir entre ellas. Solo puede facilitar el cumplimiento de algunas.
La revocación es una buena prueba de diseño. Pregunta qué puede revocar el gestor de inmediato sin bloquear toda la bóveda. Con stdio, cerrar una tubería o terminar el hijo corta ese canal, aunque los descendientes podrían haber heredado descriptores si el lanzador actuó mal. Con un socket, cerrar una conexión aceptada corta a ese cliente y deja disponible el socket de escucha. Si el registro de autorización vive más que la conexión, ninguno de esos cierres basta.
No sitúes a atacantes de red en el centro de esta decisión local salvo que el gestor exponga también un listener de red. Los riesgos más precisos son una identidad confusa, la autoridad ambiental de la sesión, la herencia de descriptores, la sustitución del socket en el sistema de archivos y una autorización que dure más que el trabajo aprobado.
Stdio vincula el canal a la vida del proceso
MCP sobre stdio resulta más sólido cuando el shim del gestor debe nacer y morir con el proceso agente. El cliente inicia un comando servidor, escribe mensajes JSON-RPC en su entrada estándar y lee las respuestas de su salida estándar. EOF es una señal de ciclo de vida que ofrece el sistema operativo, no una convención escondida en un latido de la aplicación.
La especificación de transporte de Model Context Protocol impone además una exigencia operativa útil: un servidor stdio no debe escribir datos ajenos al protocolo en stdout. Los registros van a stderr. Es fácil descartarla como una regla de enmarcado, pero evita que una línea de diagnóstico se convierta en un mensaje inválido dentro de un límite de seguridad. Trata stdout como memoria del protocolo. Configura las bibliotecas y los informes de fallos antes de la primera respuesta para que no la contaminen.
Stdio no requiere nombre en el sistema de archivos, directorio de sockets, modo de permisos, archivo de descubrimiento ni listener residente. La configuración del agente apunta a un ejecutable y sus argumentos. Es una ventaja real para un producto de escritorio: no hay endpoint que localizar ni ruta obsoleta que reparar. Las actualizaciones tienen un punto claro de activación: el siguiente shim usa el ejecutable nuevo. Una sesión activa puede continuar con la versión anterior, así que registra la identidad y versión del ejecutable con la sesión en vez de suponer que todos los clientes cambiaron al instalar.
La relación entre procesos es una prueba útil, pero no una identidad completa. El gestor puede examinar el shim conectado a su backend privado, y el shim puede examinar a su padre. Los identificadores de proceso pueden reutilizarse después de una salida, y puede haber un lanzador entre el agente visible y el shim. Captura el token de auditoría o la firma de código mientras el proceso vive. No guardes solo un PID para resolverlo después.
Las tuberías también tienen trampas de herencia. Si un lanzador marca descriptores como heredables, un nieto puede mantener abierto el extremo de escritura cuando el agente ya terminó. El gestor esperará un EOF que nunca llega. Activa close on exec donde la plataforma no lo haga, cierra los extremos sin usar inmediatamente después del spawn y haz que la sesión observe tanto la tubería como el proceso cliente esperado. EOF debe revocar el acceso, y la salida del proceso debe revocarlo por separado.
La concurrencia es estrecha a propósito. Una instancia de servidor stdio suele atender a un proceso cliente. El aislamiento queda claro a cambio de otro proceso por agente. Si la bóveda real vive en una aplicación de escritorio, el ejecutable stdio suele ser un shim pequeño que reenvía peticiones tipadas a la aplicación. Ese salto interno aún necesita autenticación y vínculo de sesión. Usar stdio en el borde MCP no hace seguro un backend compartido sin autenticar.
Un socket Unix crea la vida de un servicio
Un socket de dominio Unix encaja con un gestor que ya es un servicio residente y espera clientes independientes a lo largo del tiempo. El endpoint de escucha sobrevive a la salida de los clientes, de modo que una aplicación de barra de menús acepta llamadas sin que cada agente sea dueño del proceso gestor. Varios clientes, la contrapresión y las actualizaciones centrales son sencillos. El coste es que el servicio debe definir todos los límites que antes aportaba implícitamente un proceso cliente único.
La ruta del socket sirve para descubrirlo, no para autenticar. Un cliente capaz de encontrarlo aún necesita permiso para conectar, y uno capaz de conectar aún necesita una decisión de autorización. Coloca el socket en un directorio controlado por el usuario o la aplicación, crea el directorio con modo 0700 y pon el socket a 0600. Evita una ruta predecible dentro de un directorio compartido con escritura. El sticky bit de un directorio temporal frena ciertos ataques de borrado, pero no convierte ese directorio en un espacio de nombres fiable.
Los permisos dependen de algo más que el modo final. El umask del proceso afecta a la creación. Los directorios padre determinan si otra cuenta puede recorrer o sustituir nombres. Puede haber un enlace simbólico o nodo obsoleto en la ruta elegida. El servicio debe abrir un directorio padre fiable, inspeccionar la entrada sin seguir enlaces y borrarla solo tras demostrar que es el socket de una instancia muerta o una ruta de la instalación actual. Hacer unlink a ciegas sobre un nombre conocido crea una carrera de sustitución.
En macOS, un socket local aceptado permite obtener credenciales del par mediante llamadas como getpeereid; otras API de nivel inferior exponen un token de auditoría. Los identificadores de usuario y grupo indican qué cuenta posee el proceso. No indican qué aplicación quería autorizar la persona. Para eso, resuelve el token contra el proceso vivo y evalúa la identidad de firma, la ruta del ejecutable y el contexto de lanzamiento según la política declarada del producto.
Un socket facilita multiplexar, pero eso puede difuminar sesiones. Nunca trates una conexión válida como aprobación de toda petición que presente un identificador de sesión arbitrario. El servidor asigna la identidad de conexión, vincula la autorización a ella y rechaza los intentos del cliente de cambiar de identidad. Si un helper reconecta tras reiniciarse el servicio, exige una decisión nueva salvo que el modelo permita de forma explícita que la aprobación sobreviva.
El rendimiento rara vez decide esta elección. Tanto las tuberías locales como los sockets Unix llevan peticiones JSON-RPC pequeñas mucho más rápido de lo que tarda una operación HTTP o SSH. Mide si el gestor transfiere respuestas grandes, pero no cambies un modelo de autoridad legible por una reducción especulativa del coste de IPC local.
Los permisos del socket no prueban la intención
El modo 0600 significa que procesos con otros identificadores de usuario no pueden abrir el socket mediante los controles discrecionales normales. No significa que todos los procesos con el mismo identificador merezcan las credenciales guardadas. El malware de escritorio, un script de paquete no fiable, una extensión del editor y el agente aprobado suelen compartir usuario.
La distinción se difumina porque los permisos Unix son concretos y fáciles de inspeccionar. Un equipo ve un socket privado y llama autenticado al endpoint. Solo está autenticado hasta el límite de la cuenta. Puede bastar si el modelo se detiene en otros usuarios de inicio de sesión. Un gestor para herramientas autónomas suele prometer control dentro de una sesión, por lo que necesita una identidad más estrecha.
En macOS, la firma de código permite estrechar esa identidad. Valida al par vivo, no una ruta enviada en la petición. La ruta puede apuntar a un archivo sustituido y un proceso puede seguir ejecutando una imagen antigua ya mapeada tras una actualización. Registra la autoridad de firma y el requisito designado que informa el sistema. Decide cómo tratar builds de desarrollo con firma ad hoc; aceptarlas en silencio como la aplicación de producción convierte una comodidad en una vía de evasión.
La identidad y la autoridad del llamante también difieren. La identidad responde qué proceso abrió la conexión. La autoridad responde si ese proceso puede usar una credencial concreta para una acción concreta ahora. Un agente con firma válida puede trabajar sobre un repositorio no revisado o recibir influencia de contenido hostil. Aprobar toda acción porque el ejecutable resulta familiar confunde procedencia con consentimiento.
Con stdio, el lanzador puede presentar la identidad del padre inmediato al iniciar el shim y el gestor puede vincularla a un nonce de canal nuevo. Con un socket, el gestor obtiene la identidad del par al aceptar y la vincula al descriptor aceptado. En ambos diseños, el gestor debe ser la fuente de identidad. Un campo como client_name solo es información de pantalla, nunca una prueba.
Una tarjeta de aprobación honesta muestra lo que el sistema puede establecer y qué se aprueba. Si un script envolvente impide atribuir con fiabilidad al agente superior, indícalo o deniega la llamada. Subir por el árbol hasta encontrar un nombre familiar crea una búsqueda controlada por el atacante.
La limpieza forma parte del modelo de seguridad
La limpieza de stdio gira alrededor de descriptores e hijos. El caso normal es simple: el cliente cierra stdin, el servidor ve EOF, termina o cancela trabajo y sale. Los casos anómalos importan más. El cliente puede fallar mientras un descendiente conserva la tubería. El servidor puede quedarse bloqueado mientras el cliente cree que murió. Una acción privilegiada puede terminar después de la revocación si la cancelación no llega al worker que la ejecuta.
Da a cada petición un identificador de operación asignado por el gestor y un estado como queued, executing, completed, denied o indeterminate. Al cerrarse el canal, revoca de inmediato las peticiones futuras. Para una acción ya enviada a una API remota, no prometas cancelación si el remoto no la admite. Registra un resultado indeterminate si el proceso local muere antes de conocerlo. Esa entrada evita que un bucle de reintentos ejecute dos veces la acción.
La limpieza del socket tiene dos objetos: conexiones aceptadas y ruta de escucha. Cierra cada conexión ante un error de protocolo, autorización fallida, caducidad por inactividad o parada. Borra la ruta solo si el servicio aún posee el mismo objeto. Al reiniciar, EADDRINUSE exige investigar, no concede permiso para hacer unlink de lo que exista.
Esta comprobación shell es útil en desarrollo porque muestra tipo, propietario, modo y proceso sin cambiar nada:
sock="$TMPDIR/com.example.broker.sock"
stat -f 'type=%HT owner=%Su mode=%Sp inode=%i' "$sock"
lsof -n -U "$sock"
Un resultado normal de macOS tiene esta forma:
type=Socket owner=alice mode=srw------- inode=123456
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
Broker 48102 alice 9u unix 0x0123456789abcdef 0t0 /.../com.example.broker.sock
No analices los valores de muestra. Comprueba que el tipo sea socket, el propietario sea el usuario esperado, el modo niegue grupo y otros, y el proceso sea la instancia instalada. Repite tras un fallo forzado, una actualización, el cierre de sesión y un segundo arranque. Un diseño que solo limpia tras un cierre normal no está probado.
Si launchd inicia el servicio, conserva su propiedad de forma coherente. Mezclar inicio gestionado por la aplicación con un launch agent puede producir dos instancias que compitan por la ruta. Un componente debe poseer creación, disponibilidad y retirada; los clientes necesitan un estado unavailable explícito en vez de iniciar gestores una y otra vez.
El esfuerzo de despliegue cambia de sitio
Stdio cuesta menos al instalar y más cuando muchos clientes comparten estado residente. Cada cliente MCP necesita una entrada de comando. El shim debe localizar la aplicación firmada o su endpoint privado, negociar una versión compatible y mostrar un error claro si la aplicación falta o está bloqueada. El empaquetado debe conservar permisos ejecutables y firmas. No dependas de archivos de inicio de shell: las aplicaciones GUI suelen abrir sin el entorno esperado en el terminal.
Un socket Unix necesita instalación del servicio, dueño del inicio, descubrimiento, permisos de directorio, recuperación y compatibilidad entre actualizaciones independientes. A cambio, todos los clientes usan un endpoint estable y el servicio mantiene estado y orden de auditoría en un proceso. Resulta atractivo si la aplicación ya funciona continuamente.
El descubrimiento merece un contrato. En macOS, $TMPDIR es propio de cada usuario, pero el entorno heredado puede variar entre terminal, editor y lanzador GUI. Una ruta fija en un directorio protegido de soporte es más fácil de razonar, aunque el sandbox y la instalación pueden limitarla. Si un comando de arranque entrega la ruta, autentica ese resultado y no aceptes una ruta arbitraria de la configuración del proyecto.
Ambos modelos sufren diferencias de versión. Con stdio, el cliente elige el ejecutable shim. Con un socket residente, durante actualizaciones graduales un cliente viejo puede alcanzar un servicio nuevo o al revés. Negocia una versión en el primer intercambio. Rechaza combinaciones no admitidas antes de pedir aprobación, porque el usuario no puede aprobar una petición que cliente y gestor interpretan de modo distinto.
A veces operaciones prefiere sockets porque las herramientas conocidas los enumeran. A veces desarrollo prefiere stdio porque ejecuta un comando en terminal. Ninguna comodidad debe ser una puerta de depuración. Un cliente de diagnóstico capaz de enviar peticiones privilegiadas pasa por la misma atribución y aprobación. Un flag que omita autorización acabará saliendo del equipo de desarrollo.
La comparación cambia si el gestor no es residente. Iniciar una aplicación gráfica completa con cada conexión stdio causa demoras y avisos torpes. Mantener un servicio de socket solo para evitar un shim pequeño añade actualizaciones y limpieza. Decide primero la vida de la aplicación y ajusta después el transporte externo.
Vincula el diseño con amenazas explícitas
El equipo de plataforma debe registrar qué propiedad responde a cada amenaza. La tabla es un registro de decisión, no una puntuación. Un transporte gana una fila solo si la implementación que lo rodea aporta ese control.
| Amenaza o requisito | Diseño stdio | Diseño con socket Unix |
|---|---|---|
| Otro usuario intenta acceder | Descriptores privados y herencia correcta | Directorio padre protegido y modo 0600 |
| Otro proceso del mismo usuario conecta | Más difícil si solo el lanzador posee la tubería, pero queda el riesgo de herencia | Se espera por defecto, por lo que hacen falta credenciales del par e identidad de aplicación |
| La aprobación termina con una ejecución | EOF y salida observada ofrecen señales naturales de revocación | El servidor crea una sesión y la vincula con conexión y proceso |
| Varios agentes comparten una bóveda | Cada shim necesita un salto privado autenticado al proceso de bóveda | El servicio residente acepta conexiones autenticadas separadas |
| El gestor falla y reinicia | El cliente ve EOF e inicia otra instancia o informa del fallo | El servicio trata la ruta obsoleta y fuerza reconexión y nueva autorización |
| El ejecutable cambia durante una ejecución | La identidad capturada sigue unida a la sesión | La identidad capturada sigue unida a la conexión; reconectar obliga a evaluarla de nuevo |
| Primera instalación simple | Comando y ejecutable firmado, sin endpoint | Registro de inicio, ruta protegida, descubrimiento y limpieza |
| Orden de auditoría central | El núcleo compartido ordena eventos entre shims | Natural en un servicio residente, aunque el registro duradero aún requiere diseño |
Suelen quedar dos conclusiones. Un atacante bajo el mismo usuario borra casi todo el consuelo de los bits de modo. Además, la duración de la aprobación importa tanto como el acceso al endpoint. Un socket privado con aprobación para todo el día puede conceder más autoridad que un canal stdio bien acotado.
No asignes pesos numéricos si el equipo no puede justificarlos. Una credencial sensible puede hacer que una sola omisión de identidad pese más que toda ventaja de despliegue. Escribe una prueba de aceptación por fila. Inicia, por ejemplo, un proceso no aprobado con el mismo usuario y demuestra la denegación; aprueba un agente, termínalo, deja vivo un descendiente y demuestra que no reutiliza la autorización.
Las opciones también pueden superponerse. MCP usa stdio entre agente y shim, y el shim habla con el gestor residente mediante un socket privado u otro IPC. Suele ser una buena forma de escritorio, pero crea dos límites. Autentica ambos. El shim debe probar qué ejecución representa, y la aplicación debe probar que llegó al gestor previsto y no a un endpoint sustituido por archivos del proyecto.
Incluye la identidad en una sesión verificada
Un sobre de protocolo pequeño permite revisar la decisión de seguridad. No sustituye la identidad del sistema. Vincula hechos ya verificados por el gestor a la petición que va a ejecutar.
El gestor crea el identificador de sesión y nonce tras inspeccionar al llamante vivo, y los devuelve por el canal autenticado. Cada petición posterior incluye ese identificador, un número de secuencia creciente, la capacidad solicitada y parámetros. El gestor rechaza una sesión desconocida, una secuencia repetida o saltada cuando exige orden, una capacidad fuera de aprobación o una petición llegada por otro canal.
{"session_id":"s_7M4K","sequence":12,"capability":"http:billing.read","action":{"method":"GET","path":"/v1/invoices"}}
La respuesta repite la identidad y estado de operación, no secretos ni cabeceras inyectadas:
{"operation_id":"op_01J8","sequence":12,"state":"completed","result":{"status":200,"body_ref":"activity:8841"}}
Son formas del protocolo, no nombres universales. Lo útil es que el servidor asigna identidad, impone orden y devuelve una referencia duradera. No firmes el sobre con una clave entregada al agente: lo convertirías en titular de una credencial. Vincúlalo al canal local autenticado y conserva los secretos en el gestor.
En stdio, el vínculo puede incluir un valor aleatorio entregado solo por la tubería nueva y la identidad del lanzador. En un socket, puede incluir la conexión aceptada y el token del par. Si las peticiones se mueven entre conexiones, define reanudación deliberada con duración corta y tokens de un uso. Un identificador copiado de un registro no debe reanudar autoridad.
Separa datos de aprobación y de presentación. Ruta del repositorio, etiqueta del agente, nombre de herramienta y motivo ayudan a decidir, pero el atacante puede elegirlos. El registro distingue hechos verificados, afirmaciones del usuario, capacidades aprobadas y contenido. Durante una investigación evita que una etiqueta cuidada parezca una prueba.
Un gestor de escritorio puede usar ambos sin confundirlos
Para una aplicación macOS firmada y siempre activa, el diseño más claro suele usar stdio en el límite MCP y un salto local autenticado hacia la aplicación. El agente obtiene el modelo de lanzamiento esperado y una vida ligada al proceso. La aplicación mantiene una bóveda, una interfaz de aprobación y un historial ordenado. Solo funciona si el salto conserva la identidad exterior en vez de reducir todos los shims a un cliente de confianza.
Sallyport usa esta forma: los agentes compatibles con MCP lanzan el shim sp mcp, y la aplicación ejecuta acciones HTTP y SSH para que los secretos no lleguen al agente. Su aprobación de sesión empieza por la autoridad de firma del proceso, justo el punto que los permisos de socket no resuelven.
Eso no vuelve incorrectos los sockets ni suficiente a stdio. Un equipo con varios clientes nativos puede exponer un socket protegido como API principal. Entonces asume inspección del par, sesiones, recuperación de endpoint y compatibilidad. Si no puede explicar cada punto, el socket no está listo para transportar credenciales.
Elige el diseño cuyo fallo sea más fácil de denegar. Si falla la atribución, deniega. Si el gestor no prueba que un endpoint pertenece a su instancia, no conectes ni hagas unlink. Si un cliente reconecta tras reiniciarse un proceso, crea sesión nueva. Se pierde algo de comodidad y desaparece la continuidad silenciosa que vuelve peligrosos estos gestores.
La revisión final debe caber en una página: unidad de autorización, prueba del llamante, inicio y fin de sesión, dueño del endpoint, fallos, actualizaciones y auditoría. Si alguna respuesta depende de "mismo usuario", indica si el malware del mismo usuario queda fuera del modelo. Esa frase revela si se comparan transportes o se usan como sustitutos de una decisión de seguridad.
FAQ
¿MCP stdio es más seguro que un socket Unix?
No por sí solo. Stdio limita naturalmente un canal a un proceso iniciado, mientras el socket favorece un servicio residente; la seguridad depende de que esa duración coincida con la unidad de autorización.
¿El modo 0600 autentica a la aplicación llamante?
No. Suele limitar el acceso al usuario propietario, pero cualquier proceso de ese usuario puede intentar conectar. Inspecciona al par vivo y toma otra decisión de autorización.
¿Puede un gestor admitir varios agentes por stdio?
Sí. Ejecuta un shim por agente y da a cada uno un salto privado autenticado al núcleo compartido. Separa las identidades para que un shim no reclame la aprobación de otro.
¿Qué ocurre cuando falla un cliente stdio?
El gestor revoca llamadas futuras al ver EOF o salida y registra el resultado del trabajo activo. Si una acción remota pudo terminar, márcala indeterminate en vez de reintentar a ciegas.
¿Cómo debe eliminarse un socket Unix obsoleto?
Inspecciona el objeto sin seguir enlaces y verifica tipo, propietario y relación con una instancia muerta. Nunca hagas unlink solo porque bind devolvió EADDRINUSE.
¿Los sockets Unix son bastante rápidos para MCP?
Sí para peticiones normales. El transporte local suele ser menor frente a HTTP o SSH, así que identidad y ciclo de vida deben decidir antes que el rendimiento.
¿Debe sobrevivir la autorización a un reinicio?
Normalmente no. El reinicio rompe canal y pruebas del proceso; exige reconexión y sesión nueva salvo que el producto prometa y proteja autorización duradera.
¿Puede el cliente enviar su nombre de proceso?
Puede enviar una etiqueta para mostrar, pero el gestor no debe confiar en ella. Deriva identidad del proceso vivo, token de auditoría, firma o relación de lanzamiento del sistema.
¿Dónde debe colocar macOS un socket Unix?
Usa un directorio padre controlado por usuario o aplicación y niega acceso a grupo y otros. Define también cómo descubren la ruta terminal, editor y GUI sin confiar en configuración del proyecto.
¿Cuándo conviene mezclar stdio y socket?
Encaja con una bóveda residente para agentes MCP iniciados por separado. El borde stdio aporta sesiones por proceso; el socket interno aún necesita autenticación y contexto verificado.