Servidores Ecosistema

Los servidores MCP de Inditex por dentro: qué hacen bien y qué no

Revisamos los dos servidores MCP de Inditex, Teams y simulador de iOS: SDK, tools, un fallo de cursores corregido, HTTP sin credenciales y qué copiar.

Por

Datos a 9 de octubre de 2026 · especificación MCP 2026-07-28

Los servidores MCP de Inditex son dos proyectos abiertos que publica InditexTech, el equipo técnico de la empresa. Uno conecta un agente con Microsoft Teams y está escrito en Python. El otro controla el simulador de iOS desde lenguaje natural y está escrito en TypeScript. En este artículo los leemos con los criterios del curso, con el código delante, para separar lo que conviene copiar de lo que conviene evitar. No es una crítica a la empresa. Son proyectos útiles, con licencia Apache-2.0, y precisamente por estar abiertos se puede aprender de ellos.

El 8 de octubre de 2026, Miguel Ángel Durán (@midudev) anunció en X que Inditex abría su portal de proyectos open source, con el MCP de Teams, el MCP del simulador de iOS, Weave.js y Cerbia como ejemplos. Ese post es solo el origen del tema. Lo técnico sale de los repositorios. La organización InditexTech tenía 40 repositorios públicos el 9 de octubre, contando algunos forks, y publica su manifiesto FOSS con plantillas y CLA. Los dos repositorios con más estrellas son, justamente, los dos servidores MCP: mcp-teams-server con 428 y mcp-server-simulator-ios-idb con 327. Les siguen weavejs con 227 y k8s-overcommit-operator con 130.

Cómo lo hemos hecho: leímos el código de los dos repositorios el 9 de octubre de 2026. Las pruebas las ejecutamos en local con los SDK oficiales y servidores espejo que reproducen el mismo patrón. No hemos ejecutado código de Inditex.

mcp-teams-server: Python al día con el protocolo

El servidor de Teams va por la versión 1.0.15, publicada el 2 de octubre de 2026. Ambos servidores se crearon el 18 de marzo de 2025, así que este lleva año y medio de mantenimiento, y se nota.

Usa el SDK oficial de Python, mcp[cli]==2.2.0, con Python 3.12 o superior. Según su CHANGELOG, la versión 1.0.12 (25 de agosto de 2026) migró del SDK 1.28.1 al 2.1.0 y sustituyó FastMCP por MCPServer. Es la clase que enseñamos en la lección del servidor en Python y en el tutorial de Python. Con esa versión del SDK, el servidor habla la versión 2026-07-28 sin estado con los clientes modernos y sigue atendiendo a los de la era 2025. Lo comprobamos en nuestro espejo: una llamada tools/call con la cabecera Mcp-Protocol-Version: 2026-07-28 y el sobre _meta responde con resultType: "complete".

Seis tools con tipos y límites

El servidor expone seis tools: start_thread, update_thread, read_thread, list_threads, get_member_by_name y list_members. Las de escritura aceptan un member_name opcional para mencionar a alguien en el mensaje.

Lo mejor del diseño está en los parámetros. Cada uno se declara con pydantic.Field y sus límites. title y content exigen al menos un carácter. El limit de read_thread y list_threads va de 1 a 50, y el page_size de list_members va de 1 a 100. Las tools devuelven modelos tipados (TeamsThread, TeamsMessage, PagedTeamsMessages, list[TeamsMember]), así que el SDK genera salida estructurada sin código extra. La versión 1.0.12 lo dejó escrito en el CHANGELOG: “Added MCP input validation for required strings, pagination limits, cursors, and member page size”.

Esto es lo que pedimos en la lección de herramientas y en diseño de servidores. Un esquema estricto le dice al modelo qué puede mandar, y un límite de página impide que una sola llamada llene el contexto.

Alcance fijo: un equipo y un canal

El servidor no navega por Teams. Lee y escribe en un equipo y un canal fijos, que salen de la configuración (TEAM_ID y TEAMS_CHANNEL_ID, junto con TEAMS_APP_ID, TEAMS_APP_PASSWORD y TEAMS_APP_TENANT_ID). Si falta alguna variable, termina con código 1 y no arranca. Para escribir usa el Microsoft 365 Agents SDK, como un bot. Para leer usa Microsoft Graph con credenciales de aplicación.

Fijar el alcance en la configuración es una buena decisión. El modelo no elige equipo ni canal, así que no puede equivocarse de sitio. Pero esa promesa solo vale si ningún argumento permite salir de ella, y ahí estaba el problema que cuenta el siguiente apartado.

El fallo de los cursores de paginación

Para paginar, el servidor devuelve como cursor la URL que da Graph (@odata.nextLink). El cliente la guarda y la devuelve en la siguiente llamada, y el servidor la usa directamente con el SDK de Graph:

.messages.with_url(cursor)
.replies.with_url(cursor)

Antes de la versión 1.0.14, el valor que mandaba el cliente iba tal cual a with_url, y el servidor lo pedía a Graph con las credenciales de la aplicación. Por lo que leemos en el código, quien controlara ese argumento podía apuntar a otra ruta de Graph. Si la aplicación tenía el permiso ChannelMessage.Read.All, eso alcanzaba los canales de todo el tenant, no solo el canal configurado.

La corrección llegó en el commit 8c97499 del 1 de septiembre de 2026, con el mensaje “fixed advisory reported by Syed Anas Mohiuddin”. El CHANGELOG de la 1.0.14 lo resume así: “Restricted Graph pagination cursors to the configured team, channel, and thread”. En teams.py, el diff añade la función _validate_pagination_cursor y una llamada a ella delante de esas dos líneas. La función exige https, el host graph.microsoft.com y que la ruta decodificada sea exactamente la de los mensajes del canal configurado (o la de las respuestas del hilo). Rechaza \, los segmentos . y .. y los fragmentos. Si algo no cuadra, lanza este error:

ValueError("Invalid pagination cursor for the configured channel")

También rechaza un thread_id que contenga /, \ o caracteres codificados, o que sea . o ... No hay un advisory público en el repositorio, e Inditex no ha publicado más detalles del reporte.

La lección general encaja con el modelo de amenazas de seguridad avanzada: un cursor que viene del cliente es entrada no confiable, aunque lo haya emitido tu propio servidor. El cliente puede devolverte cualquier cosa. Si el cursor es una URL completa, como aquí, tienes que validar la URL entera contra el alcance que esperas. Otra opción es emitir cursores opacos que solo tu servidor sepa interpretar. En el artículo sobre seguridad en servidores MCP tienes otros fallos reales de servidores y SDK de MCP.

Permisos: mínimo privilegio en Graph

El mismo arreglo corrigió la guía de configuración, doc/MS-Teams-setup.md. Ahora avisa de que ChannelMessage.Read.All da acceso a todo el tenant y no se puede limitar a un equipo:

ChannelMessage.Read.All grants tenant-wide access and cannot be scoped to a team or group. Prefer the Resource Specific Consent permission ChannelMessage.Read.Group in the Teams application manifest when access should be limited to teams where the application is installed.

La guía lista los permisos que necesita la aplicación de Teams, todos de ámbito de grupo: TeamMember.Read.Group, ChannelMessage.Read.Group, ChannelMessage.Send.Group, ChannelSettings.Read.Group, ChannelMember.Read.Group, Member.Read.Group y Owner.Read.Group.

Los dos cambios juntos son un buen ejemplo de defensa en profundidad. La validación del cursor cierra el camino. El permiso de grupo limita el daño si mañana aparece otro camino. Si tu servidor actúa con credenciales de aplicación, pide el permiso más estrecho que te deje hacer el trabajo, no el que resulta más cómodo de configurar.

Streamable HTTP sin autenticación

El servidor admite tres transportes con --transport: stdio por defecto, streamable-http y sse, que el código marca como “legacy”. El arranque es mínimo:

mcp = MCPServer(
    "mcp-teams-server",
    version=__version__,
    lifespan=app_lifespan,
)
mcp.run(transport=args.transport)

El constructor no recibe auth, token_verifier ni auth_server_provider, aunque los tres existen en MCPServer 2.2.0. Es decir, por HTTP el servidor no pide ninguna credencial al cliente y actúa siempre con la identidad de la aplicación.

Para ver qué significa en la práctica, montamos un espejo con mcp[cli]==2.2.0: un MCPServer con el mismo nombre, una tool list_threads y el mismo mcp.run(transport="streamable-http"). El SDK arrancó en http://127.0.0.1:8000. Le mandamos peticiones POST /mcp con la cabecera Mcp-Protocol-Version: 2026-07-28. Esto es lo que respondió:

Petición Respuesta
tools/call sin _meta 400, error -32602: falta el sobre con la versión y las capacidades del cliente
tools/call con el sobre _meta y sin credenciales 200, resultado de la tool con "isError":false y "resultType":"complete"
Con Host: evil.example 421 Invalid Host header
Con Origin: https://evil.example 403 Invalid Origin header

Las dos últimas filas son lo que el SDK protege por defecto. Cuando sirve HTTP en 127.0.0.1, localhost o ::1 y no le pasas transport_security, activa por su cuenta la protección contra DNS rebinding, que rechaza las peticiones con un Host o un Origin ajenos. Es la misma validación de Host y Origin que configuramos en la lección de despliegue.

La segunda fila es lo que no protege. Cualquier proceso de la máquina, o cualquiera que alcance el puerto si lo expones, puede llamar a las tools sin credenciales. Y lo hace con los permisos de Graph y del bot de la aplicación, que pueden leer y escribir en el canal.

Para usarlo en local con un solo usuario, el valor por defecto es razonable. Para un servidor remoto o compartido hace falta autorización. Una vía es la de OAuth de MCP, que explicamos en APIs externas y autorización. En el SDK de Python, el propio servidor valida los tokens con un token_verifier. La otra es poner delante una pasarela que la aplique, como hace Uber con su MCP Gateway. Y si sirves fuera de localhost, la referencia del SDK indica que hay que configurar los hosts y orígenes permitidos con TransportSecuritySettings(allowed_hosts=..., allowed_origins=...).

Imagen de contenedor

El Dockerfile tiene dos etapas. La primera parte de ghcr.io/astral-sh/uv:python3.12-alpine y la final de python:3.12-alpine, con USER nonroot. La versión 1.0.13 lo resume en su CHANGELOG: “Hardened the Docker image with a multi-stage build and a minimal non-root runtime”. Ejecutar sin root es lo correcto para un proceso que guarda secretos de una aplicación de Microsoft 365. La imagen está publicada en el registro de contenedores de GitHub.

mcp-server-simulator-ios-idb: una sola tool para todo

El servidor del simulador de iOS sigue otro camino. Funciona solo en macOS, con Node 20 o superior, Xcode e idb, el iOS Development Bridge de Facebook, que instala un script postinstall. Su package.json está en la versión 1.0.1. El CHANGELOG registra la 1.0.0 el 20 de marzo de 2025 y la 1.0.1 el 2 de abril de 2025. El repositorio sigue vivo: los últimos cambios de código son del 3 de septiembre de 2026 (“CodeQL code security fixes” y sus pruebas), y el 1 de octubre se añadió un CODEOWNERS.

Un parser de expresiones regulares dentro de la tool

El servidor acepta una sola tool, process-instruction, con un único argumento instruction de tipo string. Si el cliente llama a cualquier otro nombre, responde con McpError(MethodNotFound, "Unknown tool: …"). El resultado es el objeto interno pasado por JSON.stringify y devuelto como texto.

Dentro, un NLParser interpreta la frase con expresiones regulares en español y en inglés, agrupadas por familias de comandos (AppCommands, UICommands, SimulatorCommands, CaptureCommands, AccessibilityCommands, DebugCommands y MiscCommands). Este es un patrón literal de AppCommands.ts:

/instalar\s+(la\s+)?app(\s+en\s+la\s+ruta)?\s+(?<appPath>[^\s,]+)/i

Lo acompañan ejemplos como 'instalar app /ruta/a/la/app.ipa' y 'install app /path/to/app.ipa'. En total, src tiene unas 4.200 líneas de TypeScript, contando las pruebas.

Entendemos la idea: el anuncio lo presenta como un simulador que se controla en lenguaje natural. Pero en un servidor MCP el que escribe la llamada es el modelo, y el modelo ya es el parser. Si le das una tool por tarea, con su esquema (install_app con appPath, por ejemplo), el modelo rellena argumentos tipados y el SDK los valida. Con una tool de texto libre, el modelo convierte la intención en una frase y luego unas expresiones regulares intentan deshacer ese trabajo. Cada frase que no encaje con un patrón es un fallo que el esquema habría evitado, y el servidor tiene que mantener ese parser. Por eso en la lección de herramientas y en diseño de servidores recomendamos tools orientadas a tarea, con parámetros explícitos.

No responde a tools/list

El servidor se crea con el Server de bajo nivel del SDK:

this.server = new Server(
  {
    name: 'iOS Simulator MCP Server',
    version: '1.0.1',
  },
  {
    capabilities: {
      resources: {},
      tools: {},
    },
  }
);

Declara las capacidades tools y resources, pero solo registra un manejador, el de CallToolRequestSchema. No hay manejador de ListToolsRequestSchema en src. Buscamos en todo el historial del repositorio, 52 commits desde el primero del 18 de marzo de 2025, y nunca lo ha tenido. Tampoco hay manejador de resources.

Lo reproducimos con el mismo patrón, @modelcontextprotocol/sdk@1.30.0 (la versión que resuelve su lockfile) y el Client oficial por stdio. Primero pedimos la lista de tools y después llamamos a la tool por su nombre. La salida literal:

tools/list -> -32601 MCP error -32601: Method not found
{"content":[{"type":"text","text":"ok process-instruction"}]}

El servidor anuncia que tiene tools, pero un cliente que las descubre con tools/list recibe “Method not found” y no ve ninguna. La llamada directa funciona solo si el cliente ya conoce el nombre process-instruction. Un host normal descubre las tools con tools/list y pasa su descripción y su esquema al modelo, así que aquí el modelo se queda sin ninguna. El 9 de octubre había tres issues abiertos en el repositorio y ninguno trataba de esto.

La regla práctica es sencilla. No declares una capacidad que no implementas, y prueba tu servidor con un cliente real que liste antes de llamar.

SDK v1, era 2025 y solo stdio

La dependencia es "@modelcontextprotocol/sdk": "^1.25.2", la rama v1 del SDK de TypeScript. El servidor se conecta por stdio, con StdioServerTransport y server.connect, y con ese montaje el SDK negocia la era 2025 con el handshake initialize. Solo ofrece stdio.

Para un servidor local que lanza procesos en tu Mac, stdio es lo natural. Pero el servidor no habla 2026-07-28. El paso siguiente sería el SDK v2, que es el que usamos en la lección del servidor en TypeScript: con serveStdio(factory) el servidor atiende la versión nueva y sigue sirviendo a los clientes de 2025. Cómo convivir con las dos versiones mientras los clientes migran lo contamos en estado actual y evolución.

Lo que hace bien: execFile y –

La parte que lanza procesos está muy bien resuelta. src/idb/IDBManager.ts ejecuta xcrun e idb con execFile, sin shell, y pone -- antes de los valores posicionales que vienen del usuario:

['install', '--udid', udid, '--', appPath]
['launch', '--udid', udid, '--', bundleId]
['ui', 'text', '--udid', udid, '--', text]

Sin shell no hay inyección de comandos. Y el -- impide que un valor que empiece por - se lea como una opción del programa. Es justo lo que hay que hacer cuando una tool acaba en un proceso del sistema y parte de los argumentos los escribe un modelo.

Qué copiar y qué evitar

Los dos servidores dejan lecciones claras, y las buenas pesan más que las malas.

  • Copia los esquemas estrictos de Teams: cada parámetro con su tipo y su límite, y salida estructurada con modelos tipados.
  • Copia la idea de fijar el alcance en la configuración, y valida que ningún argumento permita salir de él.
  • Trata los cursores que te devuelve el cliente como entrada no confiable, aunque los hayas emitido tú.
  • Pide a la plataforma el permiso más estrecho que te deje trabajar, como ChannelMessage.Read.Group en lugar de ChannelMessage.Read.All.
  • No expongas un servidor Streamable HTTP fuera de tu máquina sin autorización delante, porque la protección de Host y Origin no identifica a nadie.
  • Ejecuta el contenedor sin root, como la imagen de Teams.
  • Lanza los procesos del sistema con execFile y --, como el servidor de iOS.
  • Evita una tool de texto libre con un parser dentro, y da al modelo una tool por tarea con su esquema.
  • Implementa tools/list y cualquier otra capacidad que declares, y compruébalo con un cliente real.

Si quieres practicar estas decisiones con un servidor propio, empieza por herramientas y diseño de servidores. La autorización está en APIs externas y autorización, la validación de Host y Origin en despliegue y el modelo de amenazas en seguridad avanzada.

Aprende esto en el curso

La lección 1 la puedes leer ya, sin registro. Las otras 13 del Básico son gratis con una cuenta.

Empieza gratis

Fuentes

  1. InditexTech/mcp-teams-server (repositorio) (2026-10-09)
  2. mcp-teams-server: releases (1.0.15, 2026-10-02) (2026-10-02)
  3. mcp-teams-server: imagen de contenedor (2026-10-09)
  4. InditexTech/mcp-server-simulator-ios-idb (repositorio) (2026-10-09)
  5. Organización InditexTech en GitHub (2026-10-09)
  6. InditexTech/foss: manifiesto y plantillas FOSS (2026-10-09)
  7. Anuncio de @midudev en X (2026-10-08)

Artículos relacionados