npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

oxidegate-lens

v0.7.1

Published

Read-only presentation layer for OxideGate: shows how many bytes each MCP server costs on every request. Targets eager harnesses (OpenCode and similar), where those bytes are sent whether the proxy is there or not. It reads the proxy's HTTP endpoints; it

Readme

Logo de OxideGate-Lens

oxidegate-lens

Muestra cuántos bytes pesa cada servidor MCP en cada petición. Lee esos datos de OxideGate (un proxy local en Rust); nunca mide nada por su cuenta. Tampoco concluye por qué falta un byte — sólo muestra los hechos que sí puede medir, por separado, sin mezclarlos.


Instalación

brew install pichu2707/tap/oxidegate-lens

Instala un comando: oxidegate-savings (el reporte). Cero dependencias: solo necesita Node.

Si usas OpenCode, instálalo por npm

Homebrew sirve los binarios de línea de comandos, pero no instala node_modules — y el plugin de OpenCode necesita @opencode-ai/plugin en tiempo de ejecución para exponer sus tools. Instalado por brew, el plugin arranca pero sus tres tools manuales de la válvula no existen, y te lo dice:

[oxidegate-lens] manual MCP valve tools unavailable: @opencode-ai/plugin could not be loaded

Por npm sí llega completo, y en un solo comando — sin tocar ningún JSON:

opencode plugin oxidegate-lens

opencode plugin es el instalador del propio OpenCode: descarga el paquete y actualiza tu configuración él mismo. OpenCode instala los plugins de npm en su propia caché (~/.cache/opencode/node_modules/), no en el node_modules de tu proyecto.

Si prefieres hacerlo a mano, o quieres fijar la versión, también vale instalar el paquete y apuntar a la ruta:

npm install oxidegate-lens
{
  "plugin": ["./node_modules/oxidegate-lens/opencode/oxidegate-lens.ts"]
}

Y necesita a OxideGate corriendo, que es quien mide:

brew install pichu2707/tap/oxidegate

Empieza aquí

Un solo camino, cuatro pasos:

| | Paso | Comando / acción | | ----- | ------------------------------------------ | ------------------------------------------------------ | | 1 | Enciende OxideGate | OXIDEGATE_PORT=8899 oxidegate | | 2 | Apunta tu agente al proxy | export ANTHROPIC_BASE_URL=http://127.0.0.1:8899 | | 3 | Úsalo un rato, y luego pide el reporte | oxidegate-savings | | 4 | Lee la tabla | ↓ es exactamente lo que vas a ver |

ANTHROPIC_BASE_URL va SIN /v1. El cliente añade la ruta él mismo. Si le pones el /v1, la petición sale a /v1/v1/messages y el proxy responde 404.

Al reporte no hace falta decirle el puerto. oxidegate-savings busca el proxy solo: mira OXIDEGATE_LENS_URL, luego OXIDEGATE_PORT, luego lo que el propio OxideGate declara en ~/.config/oxidegate/proxy.log, y por último los puertos habituales. En todos los casos comprueba que quien contesta es OxideGate de verdad antes de creerle — que algo responda en un puerto no lo convierte en el proxy.

A OxideGate sí tienes que decírselo (paso 1), y conviene: el 8080 lo suelen ocupar Apache, Tomcat o Jenkins. Si dejas un OXIDEGATE_PORT viejo apuntando a otro servicio, la lente sigue buscando y te avisa de que lo ha ignorado, en vez de escribirte un reporte a partir de las respuestas de un desconocido.

El paso 3 no es opcional. OxideGate solo puede medir lo que ha visto pasar. Si pides el reporte sin haber hecho ninguna petición a través del proxy, la tabla sale vacía — y eso no es un fallo, es que no hay nada que medir todavía.

El reporte imprime tres bloques independientes, nunca mezclados en un solo veredicto, más un aviso:

  1. La tabla — bytes. Cada servidor mcp que aparece ahí ya está en el body: sus bytes viajaron, punto. sí, desconectándolo sin excepción, sin importar deferred_tools ni quién dice ser el cliente.
  2. Disponibles vs. llegados — una resta, sin causa. Cuántos servidores MCP tienes disponibles (claude mcp list, en tu propia máquina) contra cuántos llegaron al cable en esta petición. Si faltan, los nombra y ahí se detiene: no elige entre "tu harness los retiene" y "todavía no conectaron" — las dos son causas reales y una sola petición no alcanza para distinguirlas.
  3. Tokens de contexto — otra moneda, aparte. Si el esquema, además de viajar en el body, también ocupa el contexto del modelo por adelantado (deferred_tools). Nunca cambia un byte de la tabla.

Ejemplo real, Claude Code hablando con Anthropic a través de OxideGate, con los 4 servidores MCP disponibles llegando los 4:

fuente: 2026-07-12T15:24:29.525525732+00:00  claude-opus-4-8-lens-case1  (anthropic)  cliente: claude-cli/2.1.207 (external, sdk-cli)

SERVIDOR                      KIND     TOOLS       BYTES   % TOOLS  ¿SE PUEDE QUITAR?
claude_ai_Google_Drive        mcp          1       161 B     25.2%  sí, desconectándolo
(native)                      native       2       158 B     24.7%  no, sólo con --tools
claude_ai_Google_Calendar     mcp          1       111 B     17.4%  sí, desconectándolo
plugin_engram_engram          mcp          1       111 B     17.4%  sí, desconectándolo
claude_ai_Gmail               mcp          1        91 B     14.2%  sí, desconectándolo
overhead (corchetes/comas)    -            -         7 B         -

ahorro por petición desconectando los 4 servidores MCP: 474 B (74.2% de los tools)

Tienes 4 servidor(es) MCP disponibles. En esta petición llegaron los 4.

tokens de contexto (otra moneda — NO bytes, no cambia nada de la tabla de arriba):
  - claude_ai_Google_Drive: ocupa el contexto completo por adelantado (0 tools diferidas)
  - claude_ai_Google_Calendar: ocupa el contexto completo por adelantado (0 tools diferidas)
  - plugin_engram_engram: ocupa el contexto completo por adelantado (0 tools diferidas)
  - claude_ai_Gmail: ocupa el contexto completo por adelantado (0 tools diferidas)

aviso: algunos harnesses (p. ej. Claude Code) difieren esquemas MCP por defecto, pero ese
diferido se cae a carga completa detrás de un ANTHROPIC_BASE_URL que no sea de Anthropic —
y OxideGate es exactamente eso. Si tu harness es de ese tipo, una parte de los bytes de
la tabla de arriba podría ser un artefacto de tener el proxy en el medio, no un costo que
exista sin él. Esta ejecución no lo puede decidir por ti: para comprobarlo, repite la misma
petición apuntando directo a Anthropic (sin pasar por OxideGate) y compara los bytes.
Detalle medido: docs/optimizer-tool-search.md §3 en el repo de OxideGate.

Y este es el caso donde algunos de los servidores disponibles no llegaron al cable en esta petición puntual — el reporte lo dice como un hecho, sin adivinar la causa:

SERVIDOR                      KIND     TOOLS       BYTES   % TOOLS  ¿SE PUEDE QUITAR?
(native)                      native       2       158 B     62.5%  no, sólo con --tools
claude_ai_Gmail               mcp          1        91 B     36.0%  sí, desconectándolo
overhead (corchetes/comas)    -            -         4 B         -

ahorro por petición desconectando el servidor MCP: 91 B (36.0% de los tools)

Tienes 4 servidores MCP disponibles. En esta petición llegaron 1.
Los otros 3 (claude_ai_Google_Drive, claude_ai_Google_Calendar, plugin_engram_engram) no viajan ahora mismo.
Puede ser que tu harness los esté reteniendo, o que todavía no hayan conectado —
ninguna de las dos causas se puede confirmar desde esta sola petición (medido:
docs/optimizer-tool-search.md §3.1.4 en OxideGate).

¿SE PUEDE QUITAR? nunca lee deferred_tools ni el header client. Un tool marcado con defer_loading: true sigue viajando ENTERO en el body — es un flag sobre una definición que Anthropic necesita completa para poder buscarla, no un recorte de esa definición. Diferido ahorra contexto, nunca cable (detalle: docs/optimizer-tool-search.md §2.2 en el repo de OxideGate). Por eso toda fila mcp en la tabla es siempre sí, desconectándolo, sin excepción: si tiene fila, llegó al cable completa. Lo que puede haber, además, son servidores disponibles que no llegaron a tener fila — eso es el bloque "disponibles vs. llegados" de abajo de la tabla, nunca la columna.

Con un harness que carga los esquemas eager de verdad (OpenCode, por ejemplo), la misma tabla y el mismo resumen de ahorro son igual de directos, sin ningún bloque extra — ese dialecto no tiene primitivo de diferido, así que no hay nada que restar:

fuente: 2026-07-12T15:24:18.316600407+00:00  gpt-4o  (openai)  cliente: curl/8.21.0

SERVIDOR                      KIND     TOOLS       BYTES   % TOOLS  ¿SE PUEDE QUITAR?
claude_ai_Gmail               mcp          1       120 B     51.3%  sí, desconectándolo
(native)                      native       1       111 B     47.4%  no, sólo con --tools
overhead (corchetes/comas)    -            -         3 B         -

ahorro por petición desconectando el servidor MCP: 120 B (51.3% de los tools)
ya re-enviados en 1 petición observada: 120 B
Este dialecto (openai) no tiene primitivo de diferido: no existe una versión
donde estos bytes sean opcionales, para ningún harness. El costo de arriba es real,
sin ambigüedad — nada que decidir aquí.

Si la tabla sale vacía, es por una de tres razones, en este orden: (1) OxideGate está apagado, (2) el puerto no es el que pusiste en OXIDEGATE_PORT, o (3) todavía no pasó ninguna petición con MCP por el proxy. No hay una cuarta.

Alternativa cómoda: ejecuta npm link una vez y después llama a oxidegate-savings desde cualquier lado (en vez de node bin/oxidegate-savings.mjs).

Qué significa cada columna

| Columna | Qué es | | ------------------- | ----------------------------------------------------------------------------- | | SERVIDOR | El servidor que aporta esas herramientas ((native) es el propio agente). | | KIND | mcp = servidor MCP conectado; native = superficie del agente. | | TOOLS | Cuántas herramientas declara ese servidor. | | BYTES | Cuánto pesan sus esquemas en el cuerpo de cada petición. | | % TOOLS | Qué porción del total de herramientas representa. | | ¿SE PUEDE QUITAR? | mcp → siempre sí, desconectándolo, sin excepción. native → siempre no, sólo con --tools. |

Por qué ¿SE PUEDE QUITAR? nunca depende de deferred_tools ni del client

Es tentador pensar que un servidor MCP con sus tools marcadas defer_loading: true "ya pesa poco" en el body. No es así, y confundir esas dos cosas es el error que este reporte existe para no cometer. defer_loading es un flag SOBRE una definición de tool que Anthropic sigue necesitando completa en el body — tool_search corre en el servidor de Anthropic y busca sobre las tools declaradas en el request; si no están ahí, no hay nada que buscar. Diferido ahorra contexto (lo que el modelo carga por adelantado), nunca cable (lo que viaja en cada petición). Detalle medido: docs/optimizer-tool-search.md §2.2 y §3.2 en el repo de OxideGate.

Tampoco depende del header User-Agent (client): es contenido que manda el propio cliente, sin forma de verificarlo desde el servidor — cualquier proceso puede mandar claude-cli/... sin serlo. Una versión anterior de este reporte usaba ese header para decidir si hedgear una fila o una línea entera; siete rondas de revisión adversarial encontraron, cada una, un caso real donde esa inferencia se equivocaba. La corrección no fue una regla mejor: fue dejar de inferir. Hoy el header sólo se imprime, informativo, en la línea fuente: de arriba — nunca decide nada.

Qué significa el bloque "disponibles vs. llegados"

Debajo de la tabla, cada ejecución de tráfico anthropic compara los servidores MCP que tienes disponibles (claude mcp list, vía lib/mcp-config.mjs, corrido en tu propia máquina) contra los que llegaron al cable (tools_by_server) para la petición de la tabla de arriba. Es una resta, no una conclusión:

  • Coinciden → lo dice y no agrega nada más.
  • Faltan algunos → los nombra y explica que la ausencia tiene más de una causa posible (tu harness los está reteniendo, o todavía no terminaron de conectar) sin elegir cuál aplica — una sola petición no alcanza para saberlo (medido: docs/optimizer-tool-search.md §3.1.4, donde un conector remoto estuvo ausente en la petición #1 y presente, sin marcar, en la #3 siete segundos después, sin que nadie lo pidiera).
  • No se pudo leer tu configuración disponible (claude no está en el PATH, el comando falló, se colgó, o su salida no tuvo el formato esperado) → lo dice explícitamente. Esto NUNCA se muestra igual que "0 servidores disponibles": son cosas distintas, y confundirlas fue exactamente el defecto que este reporte existe para no repetir (ver FAILURE POLICY en lib/mcp-config.mjs).
  • Dos nombres declarados colisionan al sanitizarsanitizeServerName() ([^A-Za-z0-9_] → _) NO es inyectiva: "foo bar" y "foo_bar" sanitizan ambos a "foo_bar". Cuando eso pasa entre dos servidores conectados, no hay forma de saber, desde tools_by_server, a cuál de los dos corresponde una fila foo_bar que llegó — o si corresponde a ambos. El reporte nombra la colisión y saca esos dos nombres del conteo de disponibles/llegados en vez de adivinar o fusionarlos en silencio (medido: extrayendo la función real con ese par de nombres se obtenía antes { available: 1, missing: [] } — un servidor entero desaparecía sin aviso).
  • La petición trae una fila (others) → OxideGate solo trackea hasta 32 servidores MCP distintos de forma individual por petición (MAX_TOOL_SERVERS en src/provider/mod.rs, OxideGate); el resto se sigue contando pero se funde en un único bucket sin nombre, (others), en la tabla de arriba. Si algún servidor disponible no tiene fila propia Y la petición trae esa fila (others), el reporte NO dice "no viajó": dice que no se puede confirmar si está adentro del bucket o si de verdad faltó — medido en vivo con 33 servidores donde el 33.º SÍ viajó (sus bytes están en (others)) y una versión anterior de este reporte igual decía que "no viaja ahora mismo".

Para tráfico de dialectos sin primitivo de diferido (upstream !== "anthropic") este bloque no aparece: no hay nada que restar, porque no existe una versión donde esos bytes fueran opcionales para ningún harness.

El aviso, y por qué no es una columna ni una línea de veredicto

En tráfico anthropic, el reporte siempre imprime un aviso corto: algunos harnesses (Claude Code, documentado) difieren esquemas MCP por defecto, pero ese diferido se cae a carga completa detrás de un ANTHROPIC_BASE_URL que no sea de Anthropic — y OxideGate es exactamente eso. Puede que una parte de los bytes de la tabla sea un artefacto de tener el proxy en el medio. El reporte no lo decide por ti — te dice cómo comprobarlo (repetir la petición sin el proxy y comparar). No es una columna hedgeada ni depende de reconocer el cliente: se imprime igual para cualquier petición anthropic, precisamente porque el client no es verificable y no debe decidir nada.


Cómo funciona en 30 segundos

OxideGate (proxy en Rust)            oxidegate-lens (este repo)
  ve el tráfico real         GET      solo LEE y MUESTRA
  entre cliente y proveedor  ───────▶   oxidegate-savings → la tabla de arriba
  mide bytes por servidor  /stats
                           /requests

OxideGate mide; este repo muestra. Son dos capas con visibilidad distinta: el proxy ve los bytes exactos en la red; un plugin dentro del agente solo vería la intención del agente, no el tráfico real. Por eso el lens nunca duplica la medición — la perdería sin ganar nada.


Las advertencias que la salida repite

Ignorarlas lleva a conclusiones falsas:

  • Son bytes medidos en el cable, no tokens ni dólares. Cada proveedor tokeniza distinto, así que convertirlos exigiría una constante que no se tiene. Un byte medido es un hecho; un token inferido, una conjetura.
  • defer_loading no saca ni un byte del cable. Es un flag sobre una definición de tool que Anthropic sigue necesitando completa en el body para poder buscarla (tool_search es server-side y busca sobre lo declarado en el request). Diferido ahorra contexto del modelo, no tráfico de red — así que ¿SE PUEDE QUITAR? nunca lo consulta: una fila mcp con todas sus tools diferidas pesa, en bytes, exactamente lo mismo que una sin diferir ninguna. Lo que deferred_tools sí decide es el bloque tokens de contexto al final del reporte — otra unidad, nunca la tabla de arriba. Detalle medido: docs/optimizer-tool-search.md §2.2 y §3.2 en OxideGate.
  • Las filas native no se quitan desconectando nada. Son la superficie de herramientas del propio agente. Sólo se reducen con --tools <lista>, que cambia lo que el agente puede hacer, no sólo lo que carga. --disallowedTools no sirve — es una puerta de permisos, no de carga; el esquema viaja igual.
  • Ausente nunca es cero. Ni en la configuración MCP disponible (si no se pudo leer, el reporte lo dice — nunca lo muestra como "0 servidores"), ni en deferred_tools (si el campo falta, es "desconocido", nunca "0 diferidas"), ni en la causa de por qué falta un servidor en el cable (puede estar retenido o puede estar todavía conectando — el reporte nombra ambas posibilidades y no elige, porque una sola petición no permite elegir).

Requisitos

  • OxideGate en ejecución y accesible por HTTP (puerto por defecto 8080; en desarrollo suele ser otro, ej. 8899, vía la variable OXIDEGATE_PORT).

Compatibilidad de versiones

| oxidegate-lens | OxideGate | Qué pasa | |---|---|---| | 0.2.x | >= 0.2.0 | Todo. Incluye el bloque «tokens de contexto» (deferred_tools, por servidor). | | 0.2.x | < 0.2.0 | No se rompe. Los campos nuevos no existen en ese proxy, así que el reporte los da por DESCONOCIDOS y lo dice. Un dato ausente NUNCA se muestra como un cero. |

Esa degradación no es una promesa: está cubierta por tests (test/oxidegate-savings.test.mjs, defectos 5 y 6). Si alguien la rompe, la suite se pone roja.

  • Node 24 o superior (usa fetch y AbortSignal.timeout globales, sin dependencias externas).
  • El comando claude en el PATH, sólo si quieres el bloque "disponibles vs. llegados" en tráfico anthropic (lib/mcp-config.mjs corre claude mcp list). Sin él, el reporte sigue funcionando: ese bloque dice que no pudo leer tu configuración y muestra sólo lo que ve directamente en el cable — nunca lo esconde ni lo confunde con un cero.

Tests

npm test

Corre la suite completa (node --test, sin dependencias nuevas) en menos de dos segundos. Es hermética: levanta un servidor node:http propio en un puerto efímero para simular a OxideGate y un claude falso en un PATH propio para simular claude mcp list — nunca toca un proxy real ni el claude real de esta máquina. Ver test/helpers/ para cómo.

Por qué existe esta suite

Este reporte pasó por nueve rondas de revisión adversarial. Se encontraron nueve defectos. Los nueve los encontró una persona o un agente leyendo y midiendo la salida a mano — ninguno lo encontró una máquina, porque hasta ahora no había ninguna.

Los invariantes que sobrevivieron están escritos en comentarios de código (ver el header de bin/oxidegate-savings.mjs). Un comentario no detiene a nadie. La próxima persona que toque ese archivo puede romper cualquiera de esos invariantes y nada se lo va a decir — salvo esta suite.

test/oxidegate-savings.test.mjs convierte cada uno de los nueve defectos en un test que falla si vuelve a aparecer: no es cobertura por cobertura, es protección de regresión para una lista específica, conocida y cara de bugs. Si en algún momento piensas en borrar uno de esos tests porque "ya no hace falta" o "molesta" — no lo hace falta, y sí molesta: es la puerta que le costó una ronda de revisión completa cerrar. Borrarlo la vuelve a abrir sin que nadie se entere hasta la próxima revisión adversarial, si la hay.

test/mcp-config.test.mjs cubre en unidad la FAILURE POLICY de lib/mcp-config.mjs (ausente ≠ cero) que sostiene los defectos #6 y #7.


Superficies avanzadas (experimentales)

El reporte de ahorro de arriba es el camino principal y está verificado. Esta otra superficie existe, pero es secundaria — instalarla y su paso a paso están en docs/GUIA-INSTALACION.md.

  • Plugin de OpenCode (opencode/oxidegate-lens.ts): por sí solo no enruta nada — sólo lee lo que OxideGate ya midió. Para que el tráfico pase por OxideGate hace falta configurarlo aparte: con un bloque provider en opencode.json (ver examples/opencode.json), o parcheando fetch desde otro plugin. El bloque provider es una forma de enrutar, no la única.

    Qué está verificado y qué no, con esa distinción a propósito:

    • Verificado contra un OpenCode 1.18.4 real: client.mcp.status, client.mcp.connect y client.mcp.disconnect. Un detalle que sólo aparece ejecutándolo: tras un disconnect el SDK devuelve el estado "disabled", no "disconnected". El plugin sobrevive a eso porque pasa el estado del SDK verbatim y nunca lo compara contra una cadena fija. No introduzcas una.
    • Verificado también contra un OpenCode real: el aviso de arranque (con un servidor protegido preservado y el resto desconectado), y el sondeo sobre session.idle — se conectó un MCP a mano y el aviso de transición salió en el siguiente reposo, sin que OpenCode emita ningún evento de MCP.
    • No verificado contra un OpenCode real: el hook tool.execute.after. Se ha ejercitado con un cliente falso, lo que prueba que el cableado corre, no que OpenCode lo dispare cuando creemos.

Avisos de MCP y servidores protegidos

Configuración por proyecto

Un proyecto puede declarar la suya en .oxidegate-lens.json, en su raíz:

{
  "disableByDefault": true,
  "protectedMcpServers": ["engram"]
}

Reemplaza a la global en lo que declara — no se fusiona. Un fichero, una respuesta. Callar sobre una clave no es declararla vacía: un proyecto que solo toca el interruptor no borra la lista de nadie.

La precedencia completa, de más fuerte a más débil:

variables de entorno  >  proyecto (si está aprobado)  >  global  >  por defecto

Y no se aplica hasta que lo apruebes

Un fichero de configuración dentro de un repo que clonaste es código ajeno. Puede desconectar servidores MCP que querías conservar, o —peor, porque es silencioso— marcar como protegido uno que querías apagar. Así que no se aplica solo:

oxidegate-mcp --approve

Te enseña el fichero antes de aprobarlo, porque aprobar a ciegas no es consentir.

La aprobación es del CONTENIDO, no de la ruta — el modelo de direnv. Si el fichero cambia, por una edición tuya o por un git pull, vuelve a pedirse. Aprobar una ruta para siempre dejaría que el repositorio cambiara el fichero mañana y se aplicara sin que te enteres.

Mientras esté pendiente no es silencioso: lo dicen el plugin al arrancar y oxidegate-savings --doctor, con el hash y qué hacer.

Comprobar que todo está conectado

oxidegate-savings --doctor

Recorre la cadena entera y dice qué eslabón falla y qué hacer. Cada comprobación que hace es un fallo que costó tiempo real de encontrar y que no era diagnosticable desde fuera:

  ✔ El proxy responde
  ✔ Lo que responde ES OxideGate
  ✔ /health responde 200
  ✔ 41 peticiones observadas
  ! Las tools llegan APLANADAS (tools_flattened)
      → La mitad PRECIO sigue siendo válida; la mitad USO no puede funcionar
        en esta ruta, y acumular más tráfico NO lo cambia.
  ✔ Precio disponible para 2 servidor(es)
  ✔ Desconectar al arrancar: ACTIVADO (1 protegido/s)

  DEGRADED — Funciona, pero hay algo que limita lo que se puede reportar.

Una comprobación que no se pudo hacer sale como ?, nunca como , y el veredicto no puede ser OK mientras quede alguna sin saber. Decir «todo bien» sobre algo que no se miró cierra la investigación que habría encontrado el problema.

Y si algo está roto, diagnostica igual: apuntar a un puerto ocupado por otro servicio, o a un proxy caído, produce el informe completo en vez de un fetch failed a secas.

Elegirlo sin editar JSON

oxidegate-mcp

Abre un selector con tus servidores MCP, su precio medido al lado, y marcas cuáles se preservan:

  Servidores MCP — elige cuáles se preservan al arrancar

  > ● engram        17.2 kB   se preserva
    ○ context7       4.6 kB   se desconecta al arrancar

  Desconectar al arrancar: ACTIVADO

  ↑↓ mover · espacio preservar/desconectar · d interruptor · enter guardar · q salir

Funciona sin OpenCode, y a propósito: la configuración es tuya, no del harness, así que sirve igual con OpenCode, con pi o con lo que venga. Por una tubería o sin terminal imprime el estado y sale, en vez de intentar abrir una pantalla imposible.

Lo único que no puede hacer es conectar o desconectar una sesión en marcha — eso necesita el SDK del harness y vive en el plugin (oxidegate_lens_mcp_connect / _disconnect). Aquí decides qué pasará la próxima vez que arranques.

O editando el fichero a mano

Todo vive en un solo sitio, ~/.config/oxidegate-lens/config.json:

{
  "disableByDefault": true,
  "protectedMcpServers": ["engram", "context7"]
}

Con eso, al abrir OpenCode el plugin desconecta los MCP que no hayas protegido y te lo dice con las cifras delante:

empiezas con 1 MCP sin conectar: context7 (4.6 kB). Siguen activos: engram (17.2 kB).
Para volver a abrir alguno: oxidegate_lens_mcp_connect. Detalle completo: oxidegate_lens_mcp_valve.

Ver el estado, medir el coste y saber cómo revertirlo, sin ejecutar ningún comando. Después avisa cuando alguno se conecta.

Por qué el interruptor está en el fichero y no en una variable de entorno. Estuvo en OXIDEGATE_MCP_DISABLE_BY_DEFAULT y falló una prueba real: el propio autor abrió OpenCode sin exportarla —con las instrucciones delante— y la función simplemente no corrió, sin ninguna pista de que estaba apagada. Un interruptor que hay que recordar exportar antes de arrancar un proceso está apagado la mayor parte del tiempo. Además dejaba la configuración incoherente: la lista en un fichero y el interruptor en el entorno, dos mecanismos para una sola función.

Las dos variables (OXIDEGATE_MCP_DISABLE_BY_DEFAULT y OXIDEGATE_MCP_ALLOWLIST) siguen funcionando y ganan cuando están definidas, así que nada de lo que ya tengas configurado se rompe. El override va en las dos direcciones: OXIDEGATE_MCP_DISABLE_BY_DEFAULT=0 apaga la función aunque el fichero la encienda. Y el allowlist definido y vacío significa "esta vez no protejas nada", que es como se ignora el fichero desde la línea de comandos sin editarlo.

Dos comportamientos que conviene conocer antes de que te sorprendan:

  • Si el fichero de config no se puede leer, no se desconecta NADA, y se avisa con la razón. Aquí una lista vacía no es un dato, es una orden: un lector que degradara un JSON roto a [] desconectaría exactamente los servidores que ese fichero existía para proteger. Ante la duda, no se toca.
  • El aviso de conexión llega en el siguiente momento de reposo, no al instante. OpenCode no emite ningún evento de MCP — su unión Event tiene 32 miembros y ninguno lo es — así que no hay nada a lo que suscribirse y la única forma de enterarse es leer el estado dos veces y comparar. El sondeo cuelga de session.idle.

En qué harnesses sirve

Medir el costo de MCP no aplica igual en todos los asistentes de código. Algunos ya difieren los esquemas (Claude Code), otros llaman desde su nube y no se pueden medir en local (Warp), y solo uno expone un slot de UI para un panel propio (OpenCode). El mapa completo — con la trampa de que «acepta base_url» no significa «medible» — está en docs/COMPATIBILIDAD-HARNESSES.md. oxidegate-savings no lee ese archivo: la lógica vive escrita a mano en bin/oxidegate-savings.mjs y lib/mcp-config.mjs. La matriz es la explicación para humanos de por qué existe el bloque "disponibles vs. llegados"; mantener el código y el doc en sync es trabajo de quien los edita, no algo automático.


Por qué es un repo aparte

oxidegate-lens vive separado de OxideGate a propósito:

  • OxideGate es la fuente de verdad: un proxy en Rust con su propio ciclo de vida, pruebas y versionado.
  • Esta capa de presentación cambia por otras razones (scripts, plugins de editores) y depende de otras cosas (Node). Mezclarlas acoplaría el versionado de un medidor con el de scripts de visualización, sin necesidad real.

Qué está verificado y qué no

Verificado (contra fuentes vivas):

  • La forma de GET /stats y GET /requests de OxideGate, probada en vivo contra una instancia en el puerto 8899 — incluidos los cuatro casos que el reporte distingue (todos los disponibles llegaron, algunos faltan, configuración ilegible, dialecto sin primitivo de diferido), generados con peticiones reales vía curl contra /v1/messages y /v1/chat/completions.
  • El formato de línea de claude mcp list (lib/mcp-config.mjs), probado en vivo contra Claude Code 2.1.207 — incluidos nombres con espacios (claude.ai Gmail) y con : (plugin:engram:engram), y la transformación a mcp__<server>__<tool> que hace el propio Claude Code al armar el nombre de la tool en el body saliente.
  • El fallback de Claude Code a carga completa detrás de un ANTHROPIC_BASE_URL no-first-party — documentado por Anthropic, y confirmado en OxideGate con un A/B con grupo de control y servidor sonda (docs/optimizer-tool-search.md §3.1 en OxideGate).
  • La colisión de sanitizeServerName(), probada extrayendo la función real con "foo bar"/"foo_bar"; el desborde a (others) con 33 servidores MCP distintos contra una instancia real en el puerto 8903 (uno de ellos un nombre declarado real, para confirmar que sus bytes SÍ estaban en (others)); un nombre de servidor con ': ' adentro ("evil: server - trap", vía un .mcp.json de proyecto escrito a mano) contra claude mcp list real; y la nota de native ausente cuando la tabla no trae ninguna fila native.

No verificado:

  • La API de hooks de plugins de OpenCode (opencode/oxidegate-lens.ts), tomada de documentación pública sin probar contra una instancia real.
  • El formato de línea de claude mcp list no es un contrato documentado por Anthropic — es salida de CLI para humanos, verificada contra una única versión (2.1.207). Un cambio de formato en otra versión haría que lib/mcp-config.mjs lo reporte como "no se pudo leer" (nunca como cero) — ver FAILURE POLICY en ese archivo — pero sí puede dejar de funcionar hasta que se actualice el parser.

Límite conocido, no un bug:

  • La comparación "disponibles vs. llegados" asume que la configuración MCP no cambió entre el momento en que se generó la petición que estás viendo y el momento en que corriste el reporte — claude mcp list lee el estado de AHORA, no el de esa petición. Para la petición más reciente esto casi nunca importa.
  • Absencia de un servidor en el cable tiene más de una causa posible (retención del harness, o conexión todavía en curso — medido en docs/optimizer-tool-search.md §3.1.4 en OxideGate) y este reporte no elige entre ellas a propósito: una sola petición no trae evidencia suficiente para hacerlo con honestidad.
  • sanitizeServerName() es NECESARIAMENTE lossy, no un bug a arreglar: Claude Code colapsa cualquier carácter fuera de [A-Za-z0-9_] a _ al construir el nombre de servidor que viaja en el body, así que oxidegate-lens no puede reconstruir el nombre original a partir del nombre en el cable — solo puede detectar cuándo dos nombres declarados distintos colisionaron y decirlo, nunca adivinar cuál llegó.
  • OxideGate trackea como máximo 32 servidores MCP distintos de forma individual por petición (MAX_TOOL_SERVERS, src/provider/mod.rs en OxideGate); el servidor 33.º en adelante se sigue contando en bytes, pero se funde en la fila (others) sin nombre. Mientras esa fila exista en la tabla, el bloque "disponibles vs. llegados" no puede afirmar que un servidor sin fila propia no llegó — solo que no tiene fila propia.

Enlace a OxideGate

https://github.com/pichu2707/OxideGate