Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
inspector — Inspecciona, depura y prueba visualmente servidores del Model Context Protocol (MCP) desde una interfaz web, CLI o TUI, con exploración de herramientas/recursos, registro de solicitudes y soporte de OAuth. | Kitploit
Herramientas/GitHubGitHub/modelcontextprotocol/inspector
Scripting y AutomatizaciónDepuradoresUtilidades y FrameworksAutenticación
GitHubmodelcontextprotocol/inspector

inspector

Inspecciona, depura y prueba visualmente servidores del Model Context Protocol (MCP) desde una interfaz web, CLI o TUI, con exploración de herramientas/recursos, registro de solicitudes y soporte de OAuth.

Ver Repositorio

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir
Sitio web
10.7k1.5khace 20h 29mRevisado por Kitploit

MCP Inspector

Una herramienta para desarrolladores para inspeccionar servidores Model Context Protocol (MCP). Se distribuye como un único paquete, @modelcontextprotocol/inspector, que ofrece tres formas de inspeccionar un servidor:

  • Web — una aplicación de una sola página Vite + React + Mantine con un backend Node.
  • CLI — un cliente de línea de comandos programable para automatización, CI y bucles rápidos de retroalimentación de agentes.
  • TUI — una interfaz de terminal interactiva construida con Ink.

Las tres se ejecutan a través de un único binario global mcp-inspector:```bash npx @modelcontextprotocol/inspector # web UI (default) npx @modelcontextprotocol/inspector --cli # CLI npx @modelcontextprotocol/inspector --tui # TUI

root@kitploit:~
> **¿Actualizando desde v1?** Lee la [guía de migración v1 → v2](https://github.com/modelcontextprotocol/inspector/blob/HEAD/docs/v1-to-v2-migration.md) — las opciones de CLI, la nueva separación entre `--config` y `--catalog`, la subida de la versión del motor de Node y lo que ya no se incluye.

> **Estado del repositorio.** Esta es la línea **v2** del Inspector. El desarrollo activo ocurre en **`v2/main`** (la rama de desarrollo — todos los PRs de v2 apuntan a ella), que se fusiona en **`main`** en las versiones hito; `main` es la rama predeterminada y contiene la última v2 publicada, publicada con la etiqueta `latest` de npm. La línea heredada **v1** vive en **`v1/main`** — solo correcciones de seguridad, publicada directamente desde esa rama con la etiqueta `v1-latest` de npm (`npx @modelcontextprotocol/inspector@v1-latest`). Consulta [`AGENTS.md`](https://github.com/modelcontextprotocol/inspector/blob/HEAD/AGENTS.md) para conocer las convenciones de ramas/tablero.

## Estructura del proyecto

v2 **no** es un espacio de trabajo npm. Cada cliente bajo `clients/*` mantiene su propio `package.json` y `node_modules`; el código compartido vive en `core/` y se consume mediante un alias `@inspector/core` en tiempo de compilación (sin `package.json` propio). Un único `npm install` en la raíz instala en cascada en cada cliente (consulta [Configuración](#setup)).```
inspector/
├── clients/
│   ├── web/          # Web client (Vite + React + Mantine). src/ = browser app; server/ = Node dev/prod backend
│   ├── cli/          # CLI client (tsup bundle, @inspector/core alias)
│   ├── tui/          # TUI client (Ink + React, tsup bundle)
│   └── launcher/     # Shared launcher — provides the `mcp-inspector` bin, dispatches to web/cli/tui
├── core/             # Shared code consumed via the `@inspector/core` alias (no package.json)
│   ├── auth/         # OAuth: providers, discovery, storage, endpoint overrides, mid-session recovery (browser/node/remote backends)
│   ├── client/       # Install-level client config (`client.json`): browser-safe parse/validate + Node load/save, remote backend, secrets
│   ├── json/         # JSON + parameter/argument conversion utilities, and the nullable-union
│   │                 #   schema collapse shared by the web and TUI form builders
│   ├── logging/      # Silent pino logger singleton
│   ├── mcp/          # InspectorClient runtime, state stores, transports, config import,
│   │                 #   and the RFC 6570 URI-template helpers the web form and TUI expand through
│   ├── node/         # Node-only shared helpers: version reader, hostUrl (host normalize/canonicalize + all-interfaces/loopback detection)
│   ├── react/        # React hooks over the state stores
│   └── storage/      # File I/O helpers for the OAuth persist backends
├── test-servers/     # Composable MCP test servers + fixtures used by integration tests
├── scripts/          # Root build/verify tooling (install cascade, smokes, verify-build-gate, verify-format-coverage, verify-dep-lockstep, pack:verify)
├── docs/             # Task-oriented guides (v1→v2 migration, server configuration, MCP App review, launcher/config plan)
├── specification/    # Design/build specifications
├── AGENTS.md         # Contribution rules for agents AND humans (see below)
└── README.md         # You are here

Cada cliente tiene su propio README con detalles específicos del cliente: web · cli · tui · launcher.

Las guías orientadas a tareas se encuentran en docs/:

  • Migración de v1 a v2 — el mapa v1 → v2: correspondencia de banderas de CLI, semántica de --config vs. --catalog con ejemplos de antes/después, el cambio de versión mínima de Node (>=22.7.5 → >=22.19.0), cambios de nombre de variables de entorno y los subpaquetes que ya no se distribuyen.
  • Configuración del servidor MCP — a qué servidor(es) se conecta el Inspector: --catalog vs. --config, objetivos ad-hoc, el separador --, el formato de archivo y sus campos específicos del Inspector por servidor. Compartida por los tres clientes; los README de cli y tui delegan en ella sus secciones de opciones de servidor.
  • Revisión de una App MCP — la receta CLI-first → web de un solo uso para la revisión automatizada de Apps: sonda --app-info → navegación por deep-link → widget renderizado, además de transferencia OAuth y soporte de proxy.
  • Consolidación del launcher y la configuración — por qué el launcher ejecuta un cliente en proceso en lugar de lanzarlo, y cómo encaja el procesador de configuración compartido.

Configuración

Requiere Node >=22.19.0.```bash npm install # root install; postinstall cascades into every client

root@kitploit:~
- **Fresh clone:** ejecuta `npm install` en la raíz del repositorio.
- **Después de un pull que cambie las dependencias de un cliente:** vuelve a ejecutar `npm install` en la raíz para resincronizar todos los clientes.

La cascada (`scripts/install-clients.mjs`) es solo para desarrollo: sale antes si el paquete se instala como dependencia, y el tarball publicado solo incluye el `build/` de cada cliente, por lo que los usuarios finales no se ven afectados. Establece `INSPECTOR_SKIP_CLIENT_INSTALL=1` para omitirla.

**Dónde se declara una dependencia.** Los paquetes del MCP SDK (`@modelcontextprotocol/client`, `core`, `server`, `server-legacy`, `ext-apps`) residen únicamente en el `package.json` de la **raíz**, nunca en el de un cliente. La resolución de Node asciende, por lo que la instalación de la raíz está en la cadena de todos los clientes, y el manifiesto raíz es contra el que el tarball publicado ya resuelve. Declararlos por cliente instala una segunda copia que puede desviarse de la de la raíz; así es como dos versiones de `ext-apps` (y de la v1 transitiva de `@modelcontextprotocol/sdk`) acabaron en el árbol antes de [#1970](https://github.com/modelcontextprotocol/inspector/issues/1970), y una segunda copia de `client`/`core` es el fallo para el que `vitest.shared.mts` lleva un workaround de `dedupe`. La misma ubicación solo en la raíz se aplica a cualquier cosa alcanzada únicamente a través de código propiedad de la raíz sin manifiesto propio (`test-servers/src`, `core/`), y `vitest.shared.mts` les hace alias a la raíz del repositorio: `express` y `yaml`, ambos alcanzados a través de `test-servers/src`, son los dos actuales. **Que un paquete así sea una `dependency` o una `devDependency` se deduce de quién lo consume en tiempo de ejecución, no de dónde se declara:** todo lo que `core/` importa en tiempo de ejecución debe ser una **`dependency`** de la raíz, porque los builds de los clientes externalizan los paquetes npm y una instalación publicada los resuelve desde el manifiesto raíz, donde las devDependencies no están. `express` es solo de pruebas y es una devDependency; `yaml` actualmente está en `dependencies`. **`vite` y `@vitejs/plugin-react` son `dependencies` de la raíz por la misma razón, no por error** — parecen herramientas de build, pero `clients/web/server/start-vite-dev-server.ts` los importa en tiempo de ejecución para `mcp-inspector --web --dev`, y `clients/web/tsup.runner.config.ts` los lista a ambos como `external`, por lo que una instalación publicada los resuelve desde el manifiesto raíz. Moverlos a `devDependencies` rompería `--web --dev` para los consumidores (y el `vite build` bajo demanda en `ensure-web-build.ts`) a la vez que pasaría todas las comprobaciones locales. Eso sí significa que aparecen bajo `npm audit --omit=dev`, lo cual es una ventaja: realmente están en el árbol de producción.

## Ejecución durante el desarrollo

Para la iteración web del día a día, ejecuta Vite directamente desde el cliente web (HMR rápido, sin necesidad de compilar el lanzador):```bash
cd clients/web && npm run dev

Los scripts controlados por el launcher que se muestran a continuación ejecutan el launcher compilado, así que compila primero (npm run build):```bash npm run web # prod web launcher against clients/web/dist npm run web:dev # web launcher in --dev mode (Vite)

root@kitploit:~
## El paquete compartido `@inspector/core`

![Arquitectura de código compartido: los cuatro clientes sobre el paquete compartido @inspector/core](https://assets.kitploit.com/production/public/readmes/50997/12951c8cb492b9d753ea13b98a8ea2475ef2e250a9e849227b05c3cfc7c5d31c/962dd293aaae81c94af868f086686d56189155e63449bfc6b537818e8725c812-display-v1.webp)

`core/` contiene la lógica compartida por los tres clientes para que web, CLI y TUI se comporten de forma idéntica. Su punto de entrada es la clase **`InspectorClient`** (`core/mcp/`), que posee la conexión con un servidor MCP, el ciclo de vida de petición/respuesta y un conjunto de almacenes de estado; `core/react/` expone hooks de React sobre esos almacenes que consumen tanto el árbol React de la web como el de la TUI (Ink). OAuth (`core/auth/`) está dividido en lógica isomórfica más backends de navegador/node/remoto, de modo que los mismos flujos funcionan en el navegador, en Node y contra un backend remoto.

`core/` carece intencionadamente de **`package.json`** — no se publica por sí solo. Cada cliente lo incluye mediante un alias `@inspector/core`:

- **CLI / TUI:** `esbuildOptions.alias` en su `tsup.config.ts` asigna `@inspector/core` → el directorio `core/` del repositorio, y `noExternal: [/^@inspector\/core/]` lo incorpora al bundle.
- **Web:** el mismo alias en `clients/web/vite.config.ts` para la aplicación de navegador y el runner backend de Node.

Publicar `core/` como paquete propio (p. ej., para que terceros puedan construir sobre él) se difiere deliberadamente — consulta el issue [#1636](https://github.com/modelcontextprotocol/inspector/issues/1636).

## Cliente web: componentes "tontos" + Storybook

El cliente web v2 se construye a partir de **componentes de presentación ("tontos")** — aceptan datos y callbacks como props y contienen solo lógica de visualización, sin recuperación directa de datos ni estado de cliente. El estado proviene de los hooks de `@inspector/core`, conectados cerca de la parte superior del árbol. Esto mantiene los componentes aislados, testeables y documentables.

Ese enfoque es lo que hace que **Storybook** sea de primera clase aquí: cada componente de pantalla y de elemento tiene un archivo `*.stories.tsx` (96+ historias) que lo renderiza con props de fixture. Las **funciones play** de Storybook actúan también como pruebas de interacción, ejecutadas sin interfaz (headless) en CI (`npm run ci:storybook`, Chromium vía Playwright).

El estilo sigue una estricta convención Mantine-first (variantes de tema y props de componentes por encima de clases CSS, propiedades personalizadas CSS `--inspector-*` por encima de literales de color crudos). Las reglas completas están en [`AGENTS.md`](https://github.com/modelcontextprotocol/inspector/blob/HEAD/AGENTS.md) en **React instructions** — léelas antes de tocar la UI web. Los componentes de elemento viven en `clients/web/src/components/elements/`; las variantes de tema en `clients/web/src/theme/`.

## Servidores de prueba

`test-servers/` proporciona **servidores MCP componibles** utilizados por las suites de integración y smoke, de modo que las pruebas ejercitan un servidor real sobre un transporte real en lugar de mocks. Un servidor se ensambla a partir de **presets** (fábricas de fixtures en `test-servers/src/preset-registry.ts` — herramientas, recursos, prompts, tareas, elicitación, sampling, OAuth, …) y se puede manejar de dos formas:

- **En proceso** — importa las fábricas (`createTestServerHttp`, `createEchoTool`, …) y ejecuta el servidor dentro del bucle de eventos de la prueba (se usa en las rutas de integración HTTP).
- **Como subproceso** — se lanza `test-servers/build/test-server-stdio.js` como un hijo stdio real (se usa en las pruebas smoke de CLI y en las pruebas de integración stdio).

Configura un servidor de forma declarativa con una configuración JSON (consulta `test-servers/configs/*.json`) que seleccione presets, y luego cárgalo con `--config`. Dado que los servidores se lanzan como subprocesos reales, la salida de compilación debe existir primero:```bash
npm run test-servers:build   # (from clients/web) → tsc -p test-servers, emits test-servers/build/

El alias de Vite @modelcontextprotocol/inspector-test-server (en clients/web/vite.config.ts) apunta a test-servers/build/index.js, de modo que getTestMcpServerPath() se resuelve a una ruta .js real.

Sirviendo la era del protocolo moderno

Un servidor streamable-HTTP también puede servir la era del protocolo moderno (2026-07-28) a través del createMcpHandler del SDK:

  • Establece transport.modern en la configuración JSON — true para servir sin estado en dos eras, o { "legacy": "reject" } para estricto solo moderno.
  • O pasa modern en el ServerConfig para un createTestServerHttp en proceso.

Esto es lo que permite que una conexión del Inspector que negocia protocolEra: "auto" | "modern" llegue a la rama moderna (server/discover poblado, sin sesión). Consulta test-servers/configs/modern-http.json.

Configuraciones de demostración

Cada configuración a continuación es un servidor listo para ejercitar una función manualmente. Cárgala con --config y, salvo que se indique lo contrario, conéctate con Era del protocolo = Modern.

MCP Apps

mcp-app-http.json sirve la herramienta mcp_app_demo (_meta.ui.resourceUri) junto con su recurso de UI mcp_app_demo_widget, de modo que la pestaña Apps tiene una app real que renderizar. Es un servidor streamable-HTTP normal: conéctate con la era del protocolo por defecto (legacy), no Modern.

Abre la pestaña Apps, selecciona mcp_app_demo, ponle un título y haz clic en Abrir app: el widget se renderiza dentro del iframe de la sandbox y ejercita la superficie del protocolo de UI del lado del host: renderizado en contexto host, size-changed, ui/message y una línea de log en el panel Registros de la app. Como el widget se sirve a través de la página proxy de la sandbox, esta configuración también es la que reproduce #1859 (la ausencia de clients/web/static/sandbox_proxy.html aparece aquí como un mensaje de "Sandbox no cargada" en lugar del widget), una falla que solo aparecía en un paquete instalado, nunca en el repositorio.

Para la versión automatizada del mismo flujo (--app-info probe → deep link → widget renderizado), consulta Revisando una app MCP.

MRTR

modern-mrtr-http.json sirve la herramienta mrtr_confirm (preajuste mrtr_confirm, createMrtrTool) sobre la rama moderna. Su manejador devuelve inputRequired(...) incrustando una elicitación de formulario, de modo que invocarla produce una ronda real: input_required → el cliente satisface la elicitación incrustada y reintenta con una nueva id → complete.

El Inspector maneja MRTR manualmente (inputRequired: { autoFulfill: false }), de modo que la elicitación incrustada se detiene en el modal de solicitud pendiente (etiquetado "input_required") para que respondas, y luego el reintento se completa. Útil para inspeccionar tanto esa UX de solicitud pendiente como la agrupación de conversaciones MRTR de la vista Protocol.

mrtr-showcase-http.json agrupa todos los preajustes de MRTR en un servidor:

Ejecuta mrtr_empty y responde a su única elicitación: la pestaña Protocol agrupa el intercambio como una conversación MRTR que termina en COMPLETE, y el panel de resultados dice "Resultado vacío — La llamada a la herramienta se completó correctamente y no devolvió contenido." En la compilación rota, ese mismo resultado se mostraba como "Aún no hay resultados", el placeholder previo a la ejecución del panel (#1860) — así que una llamada que el usuario acababa de ver tener éxito se leía como una que nunca se ejecutó. Un array content vacío sin structuredContent es un CallToolResult legal, y el panel solo se monta una vez que existe un resultado, por lo que el texto del placeholder no podía ser verdad ahí. (La otra mitad de esa misma brecha — un resultado cuyo payload vive solo en structuredContent — se cerró con #1908).

El preajuste legacy collect_elicitation llama a server.elicitInput, que falla en la rama 2026-07-28 — las solicitudes servidor→cliente no están permitidas ahí. MRTR es el reemplazo moderno.

Pestaña Network — cabeceras estandarizadas y taxonomía de errores

modern-network-http.json cubre SEP-2243 / SEP-2575. Sirve una herramienta get_weather cuyo argumento city lleva una anotación x-mcp-header: "City", de modo que un cliente moderno la refleja como Mcp-Param-City.

También sirve cuatro herramientas trigger_* que el inyector de errores de especificación de la rama moderna (transport.modern.injectSpecErrors: true) responde con un estado HTTP real y un cuerpo de error JSON-RPC:

Abre la pestaña Network para ver las cabeceras Mcp-* reflejadas resaltadas, los valores centinela decodificados y cada error renderizado de forma distinta.

El reflejo de Mcp-Param-* lo construye el Inspector, no el SDK. El SDK solo refleja dentro de client.callTool() y lo omite en el navegador (detectProbeEnvironment() !== "browser"). El Inspector enruta tools/call a través de client.request() para manejar MRTR manualmente, por lo que construye las cabeceras reflejadas por sí mismo (#1846) — en todos los clientes, incluido el web, ya que la solicitud upstream del cliente web la emite el backend de Node, no el navegador. Así que get_weather se puede invocar desde web, CLI y TUI por igual, tanto en la forma normal como en la de "Ejecutar como tarea".

x-mcp-header en la pestaña Tools

xmcpheader-modern-http.json sirve:

  • echo — herramienta simple.
  • get_weather — una anotación válida x-mcp-header: "City" en su argumento city.
  • invalid_header_tool — una anotación que usa el nombre de cabecera "Bad Header". El espacio la convierte en un token RFC 9110 no válido, por lo que toda la definición de la herramienta es no válida.
  • trigger_invalid_params — respondida con un error real -32602 Invalid params cuyo mensaje no trata sobre una herramienta faltante.

Abre la pestaña Tools: el panel de detalle de get_weather muestra una sección "Cabeceras de solicitud reflejadas (SEP-2243)" (city → Mcp-Param-City), y invalid_header_tool aparece tachado bajo un divisor "Excluido (SEP-2243)" con el motivo al pasar el cursor. Un cliente Streamable HTTP conforme DEBE eliminarlo de tools/list; el Inspector revela por qué.

Con el SDK v2, un tools/call que se rechaza con -32602 se renderiza como un panel de error distinto en lugar de un resultado isError — con el encabezado "Herramienta desconocida" cuando el mensaje menciona una herramienta faltante, o "Parámetros no válidos" en caso contrario (ejecuta trigger_invalid_params).

Obtención página a página

pagination-http.json sirve 12 herramientas, 12 recursos y 12 prompts (preajustes numbered_tools / numbered_resources / numbered_prompts, count: 12) con un maxPageSize de 4 cada uno, de modo que cada lista se pagina en tres páginas.

Activa "Obtener listas una página a la vez" (Ajustes del servidor — el ajuste paginatedLists, o el interruptor Paginado en una barra lateral de lista) y las listas cargan solo la página 1 (4 elementos) con un control Cargar siguiente página y un estado N páginas cargadas. Cada clic obtiene las siguientes 4 y las añade; Actualizar restablece a la página 1. Con el interruptor desactivado (por defecto), las mismas listas agregan automáticamente las tres páginas al conectar.

Salida estructurada

structured-output-http.json sirve list_items (structuredContent anidado — objetos dentro de arrays dentro de un objeto, la forma de #1908), get_temp (un payload plano de tres claves) y echo (sin outputSchema en absoluto). Es un servidor streamable-HTTP normal: conéctate con la era del protocolo por defecto (legacy).

Ejecuta list_items desde la pestaña Tools: el panel de resultados muestra el resumen de texto content[] ("Se encontraron 2 elementos.") y una sección plegable Salida estructurada que renderiza el payload validado por esquema como JSON formateado y copiable. Esa sección es lo que v2 estaba omitiendo: una herramienta que declara un outputSchema devuelve ahí sus datos reales, y el bloque de texto normalmente solo los resume. Ejecuta echo para confirmar que la sección está ausente cuando un resultado no lleva structuredContent.

Nombres de herramientas duplicados

duplicate-tool-names-http.json sirve get_weather, get_temp, echo y add, y luego repite get_weather y echo al final de tools/list con el mismo name y un título (duplicate) (duplicateToolNames). Ningún preajuste puede producir esta forma — el registerTool del SDK rechaza un nombre repetido — pero un servidor real puede hacerlo y lo hace, y el Inspector debe renderizarlo fielmente.

Conéctate (era legacy por defecto), abre la pestaña Tools y escribe get en Buscar herramientas: la lista debe reducirse exactamente a las tres filas get_*. En la compilación rota mantenía una fila obsoleta echo, porque la barra lateral usaba solo tool.name como clave de las filas y las claves en colisión dejaban huérfano a un hijo durante la reconciliación (#1957).

Las copias duplicadas se añaden al final en lugar de colocarse junto a su gemela a propósito. React empareja primero una secuencia inicial de hijos con la misma clave, así que un duplicado adyacente a la cabeza coincide por casualidad y el defecto se oculta; separar el par es lo que lo hace observable — y también es la forma realista, dos fuentes de herramientas concatenadas.

Argumentos anulables

nullable-fields-http.json sirve record_shipment, cuyos cuatro argumentos están declarados con .nullish() de Zod — "opcional y explícitamente anulable". Eso compila a anyOf: [<branch>, { "type": "null" }], de modo que el tipo real (y, para el enum, su lista enum) está en una rama, no en el nivel superior. get_temp está junto a él con un enum units simple y no anulable para comparar. Streamable-HTTP normal: conéctate con la era del protocolo por defecto (legacy).

Abre la pestaña Tools y selecciona record_shipment: direction debe renderizarse como un Select (envio / recebimento) con un botón de limpiar que lo devuelva a null, reference como un campo de texto, quantity como un campo numérico y express como una casilla de verificación. En la compilación rota, todos caían en el textarea de JSON crudo, que volvía a escapar su propio contenido en cada pulsación hasta que el valor era inservible (#1928). La herramienta devuelve en eco los argumentos que recibió, por lo que el panel de resultados muestra exactamente lo que se envió.

La TUI tenía la misma carencia y merece la pena probarla contra el mismo servidor (--tui, luego prueba record_shipment): direction es un select, quantity un campo entero, express un booleano. Ambos clientes comparten ahora un único paso de reducción — normalizeNullableUnion en core/json/nullableUnion.ts — precisamente para que no puedan divergir sobre qué esquemas pueden renderizar.

Plantillas de recurso RFC 6570

rfc6570-templates-http.json sirve dos plantillas de recurso tomadas directamente de #1919 — events_by_topic (foobar://events/{topic}) y events_by_query (foobar://events{?topic}) — y cada una devuelve en eco la URI contra la que fue emparejada, además de un recurso foobar://events simple (ver más abajo). Streamable-HTTP normal; conéctate con la era del protocolo por defecto (legacy).

Abre la pestaña Resources y elige events_by_topic, luego introduce foo/bar. La solicitud debe salir como foobar://events/foo%2Fbar, y el resultado devuelve en eco la URI que el servidor emparejó. En la compilación rota, el valor se insertaba en crudo, por lo que la barra creaba un segundo segmento de ruta y el matcher del SDK respondía -32602 Resource not found: foobar://events/foo/bar — exactamente el fallo del issue. Lo mismo ocurre con ?, #, %, espacios y texto no ASCII.

events_by_query es la mitad que era invisible: el escaneo antiguo /\{(\w+)\}/g no podía ver una expresión que llevara un operador, así que no se renderizaba ningún campo topic. Ahora aparece, marcado como Opcional — RFC 6570 elimina toda la expresión cuando la variable no está definida, así que leer con el campo vacío solicita foobar://events, y rellenarlo solicita foobar://events?topic=foo%2Fbar. La vista previa de la URI junto al título muestra la forma parcialmente expandida mientras escribes, dejando las expresiones sin rellenar tal como están escritas.

El recurso foobar://events simple se registra deliberadamente, no como relleno. El UriTemplate.match() del SDK compila {?topic} a un obligatorio \?topic=([^&]+), por lo que una plantilla sola no puede atender la lectura vacía — match("foobar://events") devuelve null. Un servidor real expone la colección sin filtrar como recurso propio; la demostración hace lo mismo para que ese paso realmente se resuelva.

El cliente web y la TUI expanden mediante un helper compartido, core/mcp/uriTemplate.ts — el formulario de Resources web directamente, la TUI a través de InspectorClient.readResourceFromTemplate — y ambos derivan también sus campos de formulario de su parser, que es la mitad que hace real el compartir: un formulario envía valores bajo los nombres que renderizó, así que un parser que mutila un nombre pierde silenciosamente el valor en el momento de la expansión. (La CLI no es consumidora: no tiene formulario de plantilla, y su resources/read pasa el --uri ya expandido directamente).

El UriTemplate del SDK todavía se usa, pero solo para validar una plantilla (construirlo es lo que rechaza una expresión sin cerrar). Su expansor no se usa, porque es incompleto en cinco aspectos — cada uno medido contra el SDK anclado, no inferido:

Las filas ; y :3 son las que un usuario ve directamente: con el parseo del SDK, el formulario renderiza campos etiquetados literalmente ;id e id:3. La fila +/# es corrupción silenciosa, no un exceso de escape: una literal IPv6 o una ruta ya codificada llega al servidor alterada.

Una plantilla que no se puede expandir en absoluto — un modificador fuera de la gramática ({id:abc}), o una expresión que no declara ninguna variable ({}, {a,}, {?}) — retiene la lectura en lugar de enviar algo. Elige events_malformed (foobar://events/{topic:abc}) para verlo: Leer recurso está deshabilitado, el motivo se imprime bajo el formulario y la vista previa muestra la plantilla tal como la declaró el servidor. La alternativa es peor de lo que parece: x://{} de otro modo se expandiría a x:// sin renderizar entradas, así que la comprobación de "todo lo requerido está relleno" del formulario pasa vacuamente y lee una URI que no es la plantilla que el servidor publicó.

Los literales también se codifican en pct en la expansión (RFC 6570 §3.1): café/{var} envía caf%C3%A9/value, no UTF-8 crudo en la ruta — algo que el expansor del SDK tampoco hace. Y los nombres que puede usar una plantilla son el varchar de RFC 6570 más una tolerancia etiquetada para - y ~: la suite de conformidad rechaza {default-graph-uri}, pero los servidores reales publican esos nombres y el matcher del SDK los soporta, así que el Inspector los expande y marca la variable conforming: false en lugar de rechazar un recurso que demostrablemente funciona.

Una variable indefinida es lo que omite su expresión: una variable definida como cadena vacía se expande (x{?q} da x?q=, x{;q} da x;q, según RFC 6570 §3.2.7). El expansor respeta esa distinción, por lo que un llamador como readResourceFromTemplate puede solicitar cualquiera de las dos URIs. Colapsar ambas es una cuestión de formulario, no de plantilla: ambos clientes inicializan cada variable declarada con "" y un campo de texto no puede expresar "definida pero vacía", así que cada formulario descarta sus vacíos (definedValues) al entrar.

La obligatoriedad es una propiedad de la expresión, no de la variable: RFC 6570 elimina los nombres indefinidos de una expresión con varios nombres, así que {a,b} con solo a relleno es expandible y un formulario no debe bloquearlo. requiredGroups devuelve una entrada por expresión no omitible y hasRequiredValues exige que cada una sea satisfecha por cualquiera de sus nombres — algo que ninguna bandera por variable puede expresar cuando un nombre se repite entre expresiones ({a,b}{a,c} se satisface rellenando b y c).#### Extensiones anunciadas

advertised-extensions-http.json sirve echo (siempre) y una herramienta get_weather controlada por la extensión io.modelcontextprotocol/tasks (extensionGatedTools): la herramienta se registra pero comienza deshabilitada, y el servidor la habilita en notifications/initialized solo cuando el cliente declaró esa extensión en sus capabilities.extensions.

  1. Conéctate: el Inspector anuncia la extensión Tasks por defecto, así que la lista de Herramientas muestra tanto echo como get_weather.
  2. Abre Server Settings → Advertised Extensions, desmarca Tasks (io.modelcontextprotocol/tasks) y vuelve a conectarte.
  3. El cliente ya no anuncia extensiones, el servidor nunca habilita get_weather y la lista de Herramientas muestra solo echo.

Este es el mando de depuración para un servidor que cambia legítimamente el registro de herramientas según lo que anuncia el cliente. Solo en la variante heredada con estado: la variante moderna por solicitud no tiene un oninitialized persistente.

Registro de logs, ambas eras

Tanto logging-legacy-http.json como logging-modern-http.json sirven logging: true junto con una herramienta send_notification que emite un notifications/message en un nivel elegido. El heredado es un servidor HTTP streamable simple; el moderno establece transport.modern: true.

  • Legacy — la pestaña Logs ofrece un selector Set Active Level con alcance de sesión y un botón Set. Llamar a send_notification transmite el log al panel.
  • Modern — la misma pestaña muestra en su lugar Log Level per Request. Elige un nivel para optar y el cliente sella _meta["io.modelcontextprotocol/logLevel"] en cada solicitud posterior (verifícalo en el cuerpo de la solicitud de la pestaña Network). Llamar a send_notification transmite el log a través de la respuesta SSE de la solicitud. Vuelve a ponerlo en Off y la misma llamada se bloquea silenciosamente: la solicitud omite la clave logLevel, por lo que el log nunca llega.

Ese bloqueo es fiel a la especificación («un servidor NO DEBE emitir notifications/message para una solicitud que no haya optado por ello»), porque send_notification emite a través del extra.log del SDK, con alcance por solicitud y consciente del umbral (ctx.mcpReq.log). En la variante moderna, lee la opción logLevel por solicitud del sobre de la solicitud y descarta el mensaje cuando el cliente no optó o el nivel está por debajo de la severidad solicitada; en la heredada, respeta el nivel de sesión de logging/setLevel. Dado que emite a través del notify de la solicitud, la respuesta moderna se actualiza a SSE y el log viaja en el flujo de la solicitud originante.

Suscripciones a recursos, ambas eras

Tanto subscriptions-legacy-http.json como subscriptions-modern-http.json sirven tres numbered_resources con subscriptions: true. El heredado también sirve una herramienta update_resource; el moderno establece transport.modern: true.

  • Legacy — abre un recurso en la pestaña Resources y haz clic en Subscribe. El cliente envía resources/subscribe y la sección Subscriptions lista la URI sin adornos de stream. Llama a update_resource con esa URI y el servidor actualiza el contenido y emite notifications/resources/updated, marcando la hora de última actualización del mosaico suscrito.
  • Modern — el mismo Subscribe envía en su lugar subscriptions/listen (su filtro lleva resourceSubscriptions más la opción resourcesListChanged) y se resuelve con notifications/subscriptions/acknowledged. La sección Subscriptions muestra entonces una insignia de estado del stream (Connecting… → Listening) en su cabecera, y se reconecta volviendo a listar si el stream de larga duración se cae.

La configuración moderna omite deliberadamente update_resource. La variante moderna del SDK no tiene estado y es por solicitud (createMcpHandler(() => createMcpServer(config))), por lo que la herramienta se ejecutaría contra una instancia de servidor desechable: el cambio de contenido no persistiría para el siguiente resources/read, y su resources/updated no llegaría al stream de escucha separado. Más confuso que útil.

Por lo tanto, el ciclo completo de notificación de actualización en vivo se demuestra en el servidor heredado (sesión con estado), y el servidor moderno es para el comportamiento de suscripción/escucha/insignia. La ruta de recepción del Inspector es transparente respecto a la era, por lo que un servidor moderno real con estado que enrute resources/updated hacia el stream de escucha actualiza el mosaico suscrito de la misma manera.

Tareas, ambas eras

Legacy (tasks-legacy-http.json) anuncia capabilities.tasks (tasks: { list, cancel }) con los ajustes predefinidos simple_task / progress_task / elicitation_task. Ejecuta una de esas herramientas con Run as task activado, y la pestaña Tasks la lista (rellenada vía tasks/list), consulta tasks/get, obtiene la carga útil con el bloqueante tasks/result y la cancela con tasks/cancel.

Modern (tasks-modern-http.json) establece transport.modern: true y tasksExtension: true, anunciando la extensión io.modelcontextprotocol/tasks (SEP-2663) y sirviendo modern_task / modern_input_task. La pestaña Tasks depende de la extensión negociada, no de capabilities.tasks.

  • Ejecuta modern_task como tarea: el tools/call devuelve un CreateTaskResult (resultType: "task", visible en las pestañas Protocol/Network), el cliente consulta tasks/get (sin tasks/list) y la tarea completada incluye su resultado en línea (sin el tasks/result bloqueante).
  • Ejecuta modern_input_task: la tarea pasa a input_required, mostrando una elicitación incrustada a través del modal de solicitud pendiente. Responderla envía tasks/update con los inputResponses, y la siguiente consulta se completa.

El SDK v2 eliminó todo el soporte de tareas y excluye por era los métodos tasks/* de la especificación en la era moderna en ambos lados. Así que el Inspector impulsa la extensión por sí mismo: el marco resultType: "task" se reescribe en el transporte como un CallToolResult que lleva el identificador, y tasks/get / update / cancel viajan por un canal de solicitud en bruto con el sobre moderno completo. El servidor de prueba sirve tasks/* desde un interceptor Express antes del manejador del SDK, ya que la variante moderna del SDK respondería -32601.

El Refresh de la pestaña Tasks vuelve a consultar los identificadores ya conocidos por el cliente; la era moderna no tiene lista de tareas en el servidor.

Compilación```bash

npm run build # builds all clients: web → cli → tui → launcher

root@kitploit:~
Individual clients: `build:web`, `build:cli`, `build:tui`, `build:launcher`. La compilación web produce tanto la SPA de navegador (`clients/web/dist`, Vite) como el runner del servidor de producción Node (`clients/web/build`, tsup).

## Pruebas y la puerta de calidad

Cada cliente se autovalida desde su propia carpeta; los scripts raíz los encadenan. **No** existe un script raíz agregado `test` — usa `validate` (rápido) o `coverage` (la puerta).

| Script                              | Qué hace                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `npm run validate`                  | Ejecuta primero las tres salvaguardas duraderas — `verify:format-coverage` (cada archivo de código fuente rastreado está sujeto a la puerta de formato), `verify:typecheck-coverage` (todos caen en un proyecto tsconfig), `verify:dep-lockstep` (ninguna dependencia que llega a un programa `tsc` desde dos instalaciones queda desviada entre ellas) — después `test:scripts` (las pruebas unitarias de los parsers propios de las salvaguardas), luego `validate:core` (la puerta `format:check` + `lint` del `core/` compartido), y por cliente: `format:check` + `lint` + **`typecheck`** (cli/tui/launcher; web verifica tipos vía `tsc -b` dentro de su `build`) + `build` + pruebas unitarias rápidas. La comprobación rápida del bucle interno. |
| `npm run coverage`                  | La **puerta de ≥90 % por archivo** (líneas/sentencias/funciones/ramas) bajo instrumentación v8, por cliente. Impuesta por el CI. Para web esto también ejecuta el proyecto de integración y cubre el runtime compartido de `core/` (incluidos `core/json` y `core/client`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `npm run smoke`                     | Smokes de extremo a extremo a través del launcher compilado (despacho `--help` + cli/tui/web de producción), además de dos smokes headless-Chromium: un smoke de arranque que ejecuta el bundle web de producción y comprueba un primer renderizado limpio (sin errores no capturados — excepción síncrona o rechazo no manejado, que es como se manifiesta un built-in de Node que llega al bundle del navegador), y un smoke de **MCP Apps** (`smoke:web:app`) que ejecuta la secuencia conectar → abrir app → `data-app-status="ready"` contra un servidor de Apps componible, cubriendo el proxy de sandbox y el puente de protocolo UI.                                                                                                                                                                                                                                                                                                                                       |
| `npm run verify:build-gate`         | Ejecuta un `vite build` real con un built-in de Node forzado en el grafo del navegador y comprueba que la compilación **falla** mediante la puerta #1769 (que convierte la advertencia de externalización de navegador de Vite en un error duro). Protege contra que la redacción de la advertencia se desvíe en una actualización de Vite y desactive silenciosamente la puerta. Parte de `npm run ci`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `npm run verify:format-coverage`    | Extrae los globs de `format:check` de cada `package.json` (solo los alcanzables desde `validate`), enumera todos los archivos de código fuente rastreados, y **falla** enumerando cualquier archivo no cubierto por un glob — la salvaguarda duradera para el invariante "cada archivo de código fuente propio está sujeto a la puerta de formato" (#1792). Se ejecuta primero en `validate`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `npm run test:scripts`              | Pruebas unitarias basadas en tablas (`node --test`) para los parsers puros de la propia salvaguarda (`scripts/lib/npm-scripts.mjs`, `scripts/lib/tsc-program.mjs` + los helpers exportados de `verify-typecheck-coverage.mjs` y `verify-dep-lockstep.mjs`), un caso por cada regla que codifican, más `scripts/lib/resolve-node-bin.test.mjs` — el resolvedor de bins multiplataforma (#1939), fijado contra las formas reales de `bin`/`exports` de los paquetes que los scripts realmente lanzan. Se ejecuta en `validate` — y `verify:typecheck-coverage` protege a su vez *esta* puerta (alcanzable desde `validate`, conjunto de pruebas no vacío, cada archivo de prueba cubierto por el glob de `test:scripts`), ya que `node --test` omite silenciosamente un archivo que su glob no encuentra y aun así sale con 0. |
| `npm run verify:typecheck-coverage` | El análogo de typecheck-coverage del anterior (#1791): para cada cliente Node (auto-descubierto desde disco — inscrito vía los proyectos de su script `typecheck`, o para un cliente `tsc -b` como `clients/web` vía sus `references` de `tsconfig.json`) ejecuta esos proyectos con `tsc --listFilesOnly`, los une, y **falla** enumerando cualquier `.ts`/`.tsx`/`.mts`/`.cts` rastreado bajo el cliente que no caiga en ningún proyecto (para que un nuevo config/helper de nivel superior no quede silenciosamente sin verificación de tipos). También exige, con denegación por defecto, que el TS de primera parte que ningún cliente posee (`test-servers/src`, el `vitest.shared.mts` raíz, todo `core/`, y cualquier nueva ubicación de nivel superior) caiga en el pase tsc de algún proyecto de cliente — así también se detecta un `*.tsx` de `core` al que los proyectos de web no llegan. Además verifica que la puerta esté cableada (el pase typecheck de cada cliente — su script `typecheck`, o el `tsc -b` de web — es alcanzable desde su `validate`, y la cadena raíz ejecuta el `validate` de cada cliente). Se ejecuta en `validate`. |
| `npm run verify:dep-lockstep`       | Protege el invariante "una versión por dependencia que cruza instalaciones" (#1896). v2 no es un workspace, así que el proyecto de pruebas de un cliente compila el TypeScript de primera parte compartido — `core/`, `test-servers/src`, y el `vitest.shared.mts` propiedad de la raíz, todos los cuales resuelven sus dependencias desde la instalación **raíz** — junto con las fuentes del propio cliente, poniendo el mismo paquete dos veces en un programa `tsc`. A la misma versión eso es inofensivo; desviadas, TypeScript debe relacionar dos copias estructuralmente distintas de cada tipo, lo que para una superficie de genéricos recursivos es exponencial (zod `4.3.6` vs `4.4.3` agotó el heap de 4GB de tsc en `clients/web`). Deriva su conjunto de candidatos de **lo que realmente entra en cada programa** (#1965) — cada proyecto tsconfig de cliente listado con `tsc --listFilesOnly` vía el compartido `scripts/lib/tsc-program.mjs`, cada archivo `node_modules` resuelto mapeado a su instalación propietaria, conservando los paquetes que llegan a un programa desde dos instalaciones (un paquete cuyas declaraciones llegan solo a través del `.d.ts` de otro paquete, como las de `@modelcontextprotocol/sdk`, es invisible para un escaneo de imports de primera parte). Precia cada copia desde la entrada del lockfile para la ruta de instalación exacta que el programa resolvió, compara solo las instalaciones que se encontraron en un programa, y **falla con denegación por defecto** ante cualquier discrepancia que no esté en la lista de permitidos anotada `TOLERATED_SKEW` — vacía hoy — tolerándose un paquete permitido solo *dentro de una versión mayor*. Se ejecuta en `validate`. |
| `npm run ci`                        | **Comando obligatorio antes del push.** `validate` → `coverage` → `verify:build-gate` → `smoke` → Storybook. Un verdadero superconjunto del CI de GitHub.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `npm run pack:verify`               | Smoke de publicación — ver [Publicación](#publishing).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |

También existen scripts por cliente (`validate:web`, `coverage:cli`, `smoke:tui`, …), además de `validate:core` / `format:core` raíz para el paquete compartido `core/`, `format:scripts` para las herramientas raíz de `scripts/`, y `format:shared` / `lint:shared` para la superficie "compartida" raíz (`test-servers/src/**`, `vitest.shared.mts`, el `eslint.config.js` raíz). Ejecuta `npm run format` antes de hacer commit — el `format` raíz corrige `core/`, `scripts/` raíz, la superficie compartida y cada cliente; `validate` ejecuta el `format:check` que no corrige y hace fallar el CI ante cualquier archivo sin formatear.

**El linting tiene conocimiento de tipos.** Los cinco ámbitos de ESLint (`clients/{web,cli,tui,launcher}` más el `core/` raíz + la puerta compartida) habilitan `@typescript-eslint/no-floating-promises` en `error`, de modo que una promesa que no es esperada, retornada, terminada con `.catch(…)`, ni descartada explícitamente con `void` hace fallar `lint` — y por tanto `validate` ([#1959](https://github.com/modelcontextprotocol/inspector/issues/1959)). La regla necesita información de tipos, así que el config de cada ámbito nombra un proyecto de parser; el del ámbito raíz es **`tsconfig.lint.json`**, un proyecto solo de lint que cubre `core/**`, `test-servers/src/**`, y `vitest.shared.mts`, que no tienen tsconfig propio. No emite nada y no cambia ningún typecheck — pero una nueva ubicación de TS de primera parte añadida al ámbito de lint raíz debe añadirse a su `include`. Ver **Instrucciones de TypeScript** en [`AGENTS.md`](https://github.com/modelcontextprotocol/inspector/blob/HEAD/AGENTS.md) para saber cuándo `void` es aceptable.

Para las reglas de pruebas completas — la puerta de ≥90 % por archivo, dónde viven los archivos de prueba, los proyectos unit vs. integración vs. storybook, y la política de `v8 ignore` — ver [`AGENTS.md`](https://github.com/modelcontextprotocol/inspector/blob/HEAD/AGENTS.md).

## Publicación

El paquete raíz `@modelcontextprotocol/inspector` se distribuye como **un solo tarball con un único número de versión** — sin paquetes `-web` / `-cli` / `-tui` / `-core` separados. `npm run build` compila cada cliente, y luego `prepack` se ejecuta antes de `npm publish`. Las dependencias de runtime se declaran en el `package.json` raíz; las compilaciones de cliente incluyen `@inspector/core` en el bundle y externalizan los paquetes npm resueltos desde la instalación raíz.

### Qué se distribuye, y los invariantes de empaquetado

La lista de permitidos `"files"` del `package.json` raíz es la fuente de verdad para el tarball. Existen algunas entradas poco obvias porque se leen **en runtime** o fueron descartadas silenciosamente por el packlist de npm — no las elimines sin volver a ejecutar `npm run pack:verify`:

- **Sin source maps.** Los bundlers de cliente establecen `sourcemap: false` (`clients/{cli,tui}/tsup.config.ts`, `clients/web/tsup.runner.config.ts`); Vite y el `tsc` del launcher ya no emiten ninguno. Los maps son ~la mitad del tamaño descomprimido y no se necesitan en runtime — depura vía `npm run dev` sobre el código fuente.
- **`clients/web/build` se distribuye vía `clients/web/.npmignore`.** `clients/web/.gitignore` lista `build/`, y el packlist de npm respeta ese `.gitignore` anidado por encima de la lista de permitidos `"files"` raíz — así que el runner del servidor web de producción faltaba silenciosamente del tarball mientras `clients/web/dist` se colaba (su `.gitignore` solo lista `dist-ssr`). `clients/web/.npmignore` anula el `.gitignore` para la publicación, de modo que tanto `build/` (runner) como `dist/` (SPA) se distribuyen. Los otros clientes no necesitan esto — ninguno distribuye un `.gitignore` anidado.
- **`clients/web/static` distribuye el proxy de sandbox de MCP Apps.** `clients/web/static/sandbox_proxy.html` es un archivo fuente versionado (no un artefacto de compilación), leído desde disco en runtime por `clients/web/server/sandbox-controller.ts` como `<runner dir>/../static/sandbox_proxy.html`. Faltaba por completo de la lista de permitidos `"files"` raíz, así que cada compilación publicada fallaba la pestaña Apps con **"Sandbox not loaded"** ([#1859](https://github.com/modelcontextprotocol/inspector/issues/1859)) mientras funcionaba bien en el repo. Debido a que la ruta se resuelve _relativa a_ `clients/web/build`, el directorio debe distribuirse en esa ubicación exacta — `pack:verify` verifica tanto la entrada del tarball como la ruta instalada en disco.
- **Una dependencia que renderiza React se incluye en el bundle, no se externaliza.** Un paquete externalizado resuelve su propio `react` desde donde npm haya colocado **al paquete** en el árbol del consumidor, que no es necesariamente donde el bundle resuelve el nuestro — npm coloca un paquete junto a un React que satisface *su* rango peer, y esos rangos son más laxos que los nuestros. `ink-form` y `ink-scroll-view` declaran `">=18"`, así que un proyecto con React 18 los satisface y consigue que se hoisteen mientras el React 19 del Inspector se anida debajo: dos copias de React, y la TUI muere con `TypeError: Cannot read properties of null (reading 'useState')` en el momento en que se monta un formulario de prueba de herramienta o una scroll view ([#1952](https://github.com/modelcontextprotocol/inspector/issues/1952)). Ambos se incluyen por tanto en línea mediante `clients/tui/tsup.config.ts` y **no** son dependencias raíz: el tarball distribuye su código dentro de `clients/tui/build/index.js` en lugar de que los consumidores los instalen. El bundling también fija sus dependencias transitivas a lo que resolvió la instalación de este repo (notablemente `ink-select-input@6` vía `overrides`, que npm ignora para un paquete instalado como dependencia). **`ink` es la única excepción, por coste:** incluirla en el bundle funciona pero añade ~1.4 MB (`react-reconciler` y `yoga-layout` vienen incluidos, además de un banner `createRequire` para el CJS inline), así que permanece externalizada — *no* porque su peer `">=19"` la haga segura, que no lo hace. Lo que hace tolerable eso es el rango raíz de `react`: `"^19.0.0"` está deliberadamente abierto a toda la versión mayor para que npm pueda deduplicar nuestro React con cualquier React 19 que fije un consumidor, dejando una `ink` externalizada sobre la misma copia que usa el bundle. **Estrechar ese rango reabre el bug para el propio renderizador** — `clients/tui/__tests__/tsupConfig.test.ts` lo fija al mínimo de la versión peer de `ink`, y protege el resto de la división; ver el [README de TUI](https://github.com/modelcontextprotocol/inspector/blob/HEAD/clients/tui/README.md#bundling-react-rendering-dependencies-must-be-inlined-1952).
- **Un único número de versión, leído desde el `package.json` raíz.** El Inspector se distribuye como un solo paquete con una versión, así que solo el `package.json` **raíz** lleva un `version` — los cuatro `clients/*/package.json` deliberadamente no tienen ninguno. Cada cliente Node (CLI, TUI y el backend web) resuelve la versión a través del lector compartido `readInspectorVersion()` en `core/node/version.ts`, que asciende hasta el manifiesto raíz (siempre presente en el tarball). Ningún `package.json` de cliente se lee en runtime, así que ninguno necesita distribuirse. El **navegador** web no puede leer el sistema de archivos; obtiene su versión desde el backend vía `GET /api/config` (ver [#1639](https://github.com/modelcontextprotocol/inspector/issues/1639)).

### `npm run pack:verify` — smoke de publicación contra el tarball realLos scripts `smoke:*` se ejecutan contra el árbol de compilación dentro del repositorio, que **no** es el paquete publicado. `npm run pack:verify` (`scripts/pack-and-verify.mjs`) cierra esa brecha: compila, ejecuta `npm pack` sobre el tarball publicable (verificando que no se envíen source maps y que los archivos requeridos en tiempo de ejecución estén presentes), instala el tarball en un **consumidor desechable limpio** — un directorio temporal nuevo donde ejecuta un `npm install <tgz>` real (descarga dependencias de ejecución, ejecuta `postinstall`), exactamente como haría `npx @modelcontextprotocol/inspector` — y ejecuta el bin `mcp-inspector` instalado de extremo a extremo: el despacho de `--help`, un `--cli tools/list` real sobre stdio, y un arranque `--web` de producción que debe servir `/` desde el `dist` incluido. Detecta fallos de rutas/empaquetado del tipo "funciona en `--dev`, se rompe bajo `npx …`". Requiere acceso a la red (la instalación descarga dependencias), por lo que es una comprobación local / de lanzamiento, **no** parte del bucle rápido de `validate`/`ci`.

### Lanzamiento de una versión

La publicación está automatizada por dos trabajos controlados por lanzamientos en [`.github/workflows/main.yml`](https://github.com/modelcontextprotocol/inspector/blob/HEAD/.github/workflows/main.yml) (`github.event_name == 'release'`, ambos `needs: build`):

- **`publish`** — el paquete npm. Ejecuta `npm run pack:verify` como compuerta previa a la publicación, verifica que la etiqueta del release coincida con la versión de `package.json` raíz, y luego `npm publish --access public --provenance` — un único `npm publish` (v2 no es un workspace de npm, por lo que no hay un `publish-all`/`--workspaces` estilo v1), con una atestación de procedencia firmada mediante GitHub OIDC (`id-token: write`, `environment: release`, `NPM_TOKEN`).
- **`publish-github-container-registry`** — la imagen de contenedor (ver [Docker](#docker)).

Una versión v2 se publica desde **`main`**, después de que el trabajo del hito se haya fusionado allí desde `v2/main` — no desde `v2/main` en sí. (La línea v1 se publica de forma independiente desde `v1/main` hacia la etiqueta `v1-latest` y nunca toca `main`; ver [Estado del repositorio](#mcp-inspector).)

Como hay **un solo número de versión** (solo el `package.json` raíz tiene uno — los clientes no llevan ninguno, así que no hay nada que mantener sincronizado ni paso `check-version`), el flujo de lanzamiento consta de tres pasos.

**1. Bump en `v2/main`, antes de la fusión del hito.** El bump es parte del trabajo del hito, por lo que pertenece a la rama de desarrollo y fluye hacia `main` con todo lo demás:

Sustituye el número de issue real y la versión a continuación — los comandos están escritos para que se puedan copiar y pegar tal cual (un bump menor de `2.2.0` → `2.3.0`):```bash
git checkout -b v2/chore/2010-bump-2-3-0 v2/main
npm version minor --no-git-tag-version   # or major / patch; bump only, no tag
# PR → v2/main

⚠️ --no-git-tag-version es fundamental. Un npm version a secas también etiqueta, y la etiqueta caería en un commit de v2/main — pero el lanzamiento debe cortarse desde main, así que la etiqueta tiene que apuntar al commit de fusión allí (paso 3). Etiquetar aquí crea una etiqueta en un commit que nunca se publica.

2. Fusiona v2/main → main mediante la rama habitual de fusión de hitos. Ahora lleva el incremento de versión, por lo que el lanzamiento llega a main con la versión ya correcta.

Entre los pasos 1 y 2 las dos ramas sí difieren, y eso es esperado, no una desviación: v2/main lee la versión que se está construyendo mientras que main todavía lee la que está actualmente publicada. Lo que este orden elimina es la desviación post-publicación: una vez que la fusión del hito llega, vuelven a coincidir, y v2/main nunca queda detrás de main. Si ves v2/main por delante de main, hay un lanzamiento en curso; si lo ves por detrás, algo salió mal.

3. Etiqueta el commit de main y redacta el lanzamiento:```bash git fetch origin main git tag 2.3.0 origin/main && git push origin 2.3.0

then draft & publish a GitHub Release for that tag → triggers publish

root@kitploit:~
⚠️ **Etiqueta `origin/main`, no tu `HEAD` local.** `git checkout main && git pull` se resuelve mediante cualquier estrategia de merge o rebase que tengas configurada, por lo que un `main` local divergente puede producir o reproducir commits locales silenciosamente. Etiquetar `HEAD` allí etiqueta un commit que no está en `origin/main`, y `git push origin <tag>` solo empuja la etiqueta — dejando un lanzamiento cuyo commit nunca se publicó. Nombrar `origin/main` explícitamente hace que el commit etiquetado sea exactamente a lo que apunta la rama remota, independientemente del estado local.

⚠️ **Sin prefijo `v`.** Las etiquetas de lanzamiento de este repo son `x.y.z` simples — `2.2.0`, `2.1.0`, `2.0.0` — así que etiqueta `2.3.0`, no `v2.3.0`. Ten en cuenta que el `tag-version-prefix` predeterminado de npm es `v` y el repo no establece ningún `.npmrc`, por lo que un simple `npm version` habría producido una etiqueta con prefijo `v` que no coincide con la convención. Etiquetar a mano (paso 3) es lo que lo mantiene correcto. El paso de comprobación del flujo de trabajo elimina una `v` inicial antes de comparar, por lo que una etiqueta con prefijo `v` aún se publicaría — solo sería inconsistente con todos los lanzamientos anteriores.

El commit objetivo del lanzamiento selecciona qué flujo de trabajo se ejecuta, por lo que esto solo publica cuando se corta un lanzamiento desde un commit que lleva este flujo de trabajo (v2).

**Por qué el incremento de versión va primero en `v2/main` ([#2010](https://github.com/modelcontextprotocol/inspector/issues/2010)).** Solía ocurrir en la rama de fusión del hito, que se corta desde `main` — por lo que el incremento existía solo *aguas abajo* de `v2/main` y nada lo devolvía. `v2/main` permaneció en `2.0.0` durante los lanzamientos 2.1.0 y 2.2.0. Eso no es cosmético: una rama cortada desde una rama de fusión de hito lleva silenciosamente el incremento a un PR no relacionado (esto ocurrió en [#2009](https://github.com/modelcontextprotocol/inspector/issues/2009), donde una corrección de errores del contenedor llegó con un diff `2.0.0 → 2.2.0`), y cualquier cosa que leyera la versión en desarrollo — `readInspectorVersion()`, `--version`, `GET /api/config` — reportaba una versión dos lanzamientos por detrás.

**No** "arregles" una deriva futura fusionando `main` de vuelta en `v2/main`. `main` lleva todo el historial v1 previo a v2 (retenido mediante `ec5d8e13 chore: replace main's tree with v2` — ~230 commits que `v2/main` no tiene), por lo que una fusión inversa injerta todo eso en el log de la rama de desarrollo de forma permanente para entregar un cambio de dos archivos. Incrementar la versión primero significa que no hay nada que fusionar de vuelta.

### Docker

El flujo de trabajo de lanzamiento publica una imagen de contenedor en GHCR (`ghcr.io/modelcontextprotocol/inspector`, `linux/amd64` + `linux/arm64`). El [`Dockerfile`](https://github.com/modelcontextprotocol/inspector/blob/HEAD/Dockerfile) es una compilación de dos etapas: la primera etapa instala y empaqueta con `npm pack` el tarball publicable; la segunda etapa instala ese tarball globalmente con `npm install -g`, por lo que la imagen distribuye exactamente el mismo artefacto que npm, con un binario `mcp-inspector` limpio.```bash
# run the web UI (reads the auth token from the container logs)
docker run --rm -p 127.0.0.1:6274:6274 ghcr.io/modelcontextprotocol/inspector

# or build the image locally
docker build -t mcp-inspector .
docker run --rm -p 127.0.0.1:6274:6274 mcp-inspector

¿Usas la pestaña Apps? Publica también 6275. El sandbox de MCP Apps es un segundo listener al que el navegador accede directamente, en MCP_SANDBOX_PORT (por defecto 6275). Nada más lo necesita, así que los comandos de un solo puerto anteriores son suficientes para una inspección normal — pero la pestaña Apps muestra un widget vacío sin él:```bash docker run --rm -p 127.0.0.1:6274:6274 -p 127.0.0.1:6275:6275
ghcr.io/modelcontextprotocol/inspector

root@kitploit:~
Publícalo en el **mismo número de puerto** dentro y fuera. La URL del sandbox se entrega al navegador a través de `/api/config` como `http://localhost:<container port>/sandbox`, por lo que reasignarla (`-p 9000:6275`) anuncia un puerto al que el navegador no puede acceder; usa `-e MCP_SANDBOX_PORT=9000 -p 127.0.0.1:9000:9000` en su lugar.

**Conserva el prefijo `127.0.0.1:` en el puerto publicado.** Un `-p 6274:6274` sin más publica en **cada interfaz de host**, poniendo el Inspector en tu red local. El `HOST=0.0.0.0` del contenedor es una preocupación aparte: gobierna las interfaces _del contenedor_, no las del host, por lo que la opción `DANGEROUSLY_BIND_ALL_INTERFACES` que protege un enlace comodín fuera de un contenedor no cubre este caso. Importa más aquí que en una aplicación web normal: el backend lanza procesos a petición, `GET /` incrusta el token de la API en el HTML servido, y una solicitud que llega **sin** cabecera `Origin` se salta por completo la lista de orígenes permitidos; por lo que para cualquier cliente que no sea un navegador, el token de la API es la única protección. Publicar de forma más amplia requiere una barrera real de control de acceso delante del Inspector: un proxy inverso que autentique, un túnel SSH, una red privada. Configurar tu propio `MCP_INSPECTOR_API_TOKEN` **no** sustituye esto: `GET /` revela cualquier token que esté en uso, así que uno personalizado se obtiene con la misma facilidad que uno generado.

**Conservando los servidores que añades.** El Inspector guarda tu lista de servidores en `$HOME/.mcp-inspector/mcp.json`, que en la imagen es `/home/node/.mcp-inspector/mcp.json`, dentro de la capa escribible del contenedor, por lo que `--rm` la descarta y cada ejecución comienza con una lista vacía. Monta un volumen ahí para conservarla:```bash
docker run --rm -p 127.0.0.1:6274:6274 \
  -v mcp-inspector-data:/home/node/.mcp-inspector \
  ghcr.io/modelcontextprotocol/inspector

El mismo volumen también conserva los tokens OAuth y el estado almacenado, de modo que un servidor autorizado permanece autorizado entre ejecuciones. Usa -e MCP_CATALOG_PATH=/some/other/path.json para colocar el catálogo en otro lugar: monta un volumen que cubra el directorio al que lo apuntes. Si haces un bind-mount de un directorio del host en lugar de un volumen con nombre (-v "$PWD/inspector-data:/home/node/.mcp-inspector"), el directorio conserva la propiedad del host, así que en Linux añade --user "$(id -u):$(id -g)" o haz chown a uid 1000; de lo contrario, el usuario node no root no puede escribir y añadir un servidor falla con EACCES.

¿Actualizando desde una imagen anterior a esta corrección? Las imágenes anteriores no creaban /home/node/.mcp-inspector, por lo que Docker creaba el punto de montaje del volumen como root y el usuario node no root no podía escribir en él. Un volumen vacío se repara solo en la primera ejecución de una imagen actual (Docker aplica la propiedad del directorio de la imagen a un volumen vacío), pero uno que ya contiene archivos conserva su antigua propiedad root y sigue fallando con EACCES. Corrígelo una vez:```bash docker run --rm -u 0 --entrypoint chown
-v mcp-inspector-data:/data ghcr.io/modelcontextprotocol/inspector
-R node:node /data

root@kitploit:~
La imagen usa por defecto `--web` vinculado a `0.0.0.0:6274` con la apertura automática del navegador deshabilitada; sobreescribe los argumentos para ejecutar otro modo (`docker run --rm ghcr.io/modelcontextprotocol/inspector --cli …`). Pasa `-e MCP_INSPECTOR_API_TOKEN=…` para establecer un token conocido (de lo contrario, se genera uno y se imprime en los registros), o `-e DANGEROUSLY_OMIT_AUTH=true` para deshabilitar la autenticación. Vincular `0.0.0.0` (todas las interfaces de red) se rechaza por defecto fuera de un contenedor — expone el backend que lanza procesos a la red local —, por lo que la imagen opta explícitamente por `DANGEROUSLY_BIND_ALL_INTERFACES=true` (ya establecido en el `Dockerfile`); un `HOST=0.0.0.0` sin esa opción finaliza con un error. Si **reasignas el puerto publicado** (`-p 127.0.0.1:8080:6274`), el origen del navegador (`http://localhost:8080`) ya no coincide con el puerto dentro del contenedor, así que establece `-e ALLOWED_ORIGINS=http://localhost:8080,http://127.0.0.1:8080` (o ejecuta `-e CLIENT_PORT=8080 -p 127.0.0.1:8080:8080`) o las conexiones devolverán 403. `ALLOWED_ORIGINS` **reemplaza** la lista predeterminada en lugar de fusionarse, así que enumera cada variante de loopback desde la que vayas a navegar (consulta el [README web](https://github.com/modelcontextprotocol/inspector/blob/HEAD/clients/web/README.md#host-binding--the-origin-allow-list)). La imagen se ejecuta como el usuario no root `node` y tiene un `HEALTHCHECK` que comprueba la interfaz web — asume el modo `--web` predeterminado, así que añade `--no-healthcheck` al ejecutar `--cli`/`--tui` (que no tienen servidor web).

## Contribución — `AGENTS.md` y `CLAUDE.md`

**[`AGENTS.md`](https://github.com/modelcontextprotocol/inspector/blob/HEAD/AGENTS.md) es el contrato para modificar este código base, y aplica por igual a humanos y agentes de IA.** No es una plantilla exclusiva para agentes: contiene las convenciones reales del proyecto: el flujo de trabajo de issues y tablero, las reglas de ramas/etiquetas, los estándares de TypeScript y Mantine/React, los requisitos de pruebas y cobertura, y el control obligatorio antes del push. Léelo antes de hacer cambios y mantenlo actualizado cuando cambies la estructura, las herramientas o las reglas.

`CLAUDE.md` es el punto de entrada que el agente [Claude Code](https://claude.com/claude-code) carga automáticamente; simplemente incluye `AGENTS.md` y este README, de modo que tanto agentes como humanos trabajan a partir de la misma fuente de verdad. Si usas otro agente que lea `AGENTS.md`, obtienes las mismas reglas.

Una regla clave que merece la pena destacar: **todo el trabajo se basa en issues.** Antes de empezar, busca o crea un issue de seguimiento en el tablero del proyecto v2; abre los PRs contra `v2/main` con `Closes #<issue>`. Las recetas exactas (etiquetas, IDs del tablero, estados) están en `AGENTS.md`.

## Licencia

MIT.
Descargar herramienta
ConfigDemuestraIssue
mcp-app-http.json (era legacy)Una app MCP (recurso de UI + herramienta de app) en la pestaña Apps#1859
modern-mrtr-http.jsonUna sola ronda MRTR—
mrtr-showcase-http.jsonTodos los preajustes de MRTR en un servidor#1860
modern-network-http.jsonPestaña Network: cabeceras Mcp-* + taxonomía de errores#1628
xmcpheader-modern-http.jsonPestaña Tools: reflejo y exclusiones de x-mcp-header#1632
pagination-http.jsonObtención de listas página a página#1721
structured-output-http.jsonPestaña Tools: la sección structuredContent de un resultado#1908
duplicate-tool-names-http.jsonUn tools/list que repite un nombre de herramienta#1957
nullable-fields-http.jsonPestaña Tools: argumentos anulables (anyOf + null)#1928
rfc6570-templates-http.jsonPestaña Resources: expansión de plantillas de recurso RFC 6570#1919
advertised-extensions-http.jsonRegistro de herramientas condicionado por extensiones anunciadas#1739
logging-{legacy,modern}-http.jsonRegistro de logs, en ambas eras#1629
subscriptions-{legacy,modern}-http.jsonSuscripciones de recursos, en ambas eras#1630
tasks-{legacy,modern}-http.jsonTareas, en ambas eras#1631
PreajusteComportamiento
mrtr_confirmRonda única
mrtr_two_stepDos rondas de elicitación mediante requestState
mrtr_sampleMuestreo incrustado → el panel Sampling
mrtr_rootsroots/list incrustado, respondido automáticamente en silencio desde las raíces configuradas (sin modal)
mrtr_edgeUna ronda solo de inputRequests, luego una ronda solo de requestState
mrtr_emptySe completa con un resultado vacío: sin content, sin structuredContent
mrtr_loopNunca se completa → dispara el límite MRTR_MAX_ROUNDS
ToolRespuesta
trigger_header_mismatch400 / -32020
trigger_missing_capability400 / -32021
trigger_unsupported_version400 / -32022 (con data.supported)
trigger_method_not_found404 / -32601
ShapeComportamiento del SDK
{a,b}une los valores en crudo — sin codificación, se pierde el prefijo de operador
{;id}; falta en su lista de operadores, por lo que la variable se parsea como ;id
{id:3}el modificador de prefijo se pliega en el nombre, dando id:3
{+v} / {#v}encodeURI daña los reservados [/] ([::1] → %5B::1%5D) y doble codifica los pct-triplets (%2F → %252F)
{v}encodeURIComponent deja los sub-delimitadores !'()* sin codificar, algo que RFC 6570 exige codificar