---
date: 2026-08-10
updated: 2026-08-30
tags:
  - type/guide
  - domain/agents
  - domain/identity
status: active
aliases:
  - MCP describe.net
  - describe.net MCP server
related-files:
  - ../describenet/mcp_server.py
  - server.py
  - ../tests/test_mcp.py
  - ../tests/test_mcp_remote.py
  - ../describenet/aggregate.py
  - ../describenet/api.py
---

# describe.net desde un cliente MCP

Un agente que está por contratar, prestar, delegar o firmar con una contraparte
necesita una cosa: saber con quién trata. Este servidor pone el índice global de
reputación ERC-8004 adentro de su cliente MCP, para que la respuesta salga de un
tool call y no de escribir requests HTTP.

Catorce herramientas: seis cobran como las rutas que envuelven; las otras ocho no
cobran nunca — `describe_resolve` dice qué es el string que tenés en la mano y en
qué cadenas existe, `describe_pricing` explica las pagas, `describe_check_wallet`
dice si hay algo que comprar antes de gastar, y el agregado del índice entero
(cadenas, feed, tipos, salud, manifiesto) es gratis. Leaderboard y facets suman
variante gratis (primera página / sin wallet).

> El conteo se re-midió el **2026-08-30** contra el servidor VIVO, no contra el
> archivo:
>
> ```bash
> curl -s -X POST https://api.describe.net/mcp \
>   -H 'Content-Type: application/json' \
>   -H 'Accept: application/json, text/event-stream' \
>   -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
> ```
>
> y mirando cuáles de las herramientas devueltas declaran el argumento `payment`
> en su `inputSchema` — **14 tools, 6 con `payment`, 8 sin**. Las seis pagas:
> `describe_lookup_wallet`, `describe_rater_profile`, `describe_lookup_agent`,
> `describe_history`, `describe_facets`, `describe_leaderboard`. No se contó
> leyendo esta página ni el server card: los dos son copias, y una copia no
> verifica a la otra.
>
> Esta línea contaba **doce** hasta que entró `describe_resolve` el 2026-08-23, y
> **trece** hasta que entró `describe_rater_profile` el 2026-08-29 — corregida el
> 2026-08-30, siete días después de la medición anterior. Las dos correcciones se
> dejan escritas porque juntas dicen algo que ninguna dice sola: **un conteo en
> prosa no se pone rojo solo.** Ninguna de las dos veces falló un test; las dos
> veces se descubrió leyendo. El que necesite el número exacto que corra el curl
> de arriba, no que copie esta línea.

> **Sobre el nombre del server.** La convención para servidores MCP en Python
> es `{service}_mcp` (sería `describenet_mcp`). Éste se llama `describe.net` y
> se queda así a propósito: es el nombre que la configuración ya publicada usa
> (el server-card en `/.well-known/mcp/server-card.json` y todo cliente ya
> instalado lo conocen por ese `serverInfo.name`), y renombrar un identificador
> ya desplegado rompe a los clientes existentes a cambio de estética. Los
> nombres de las HERRAMIENTAS sí siguen la convención (`describe_*` como
> prefijo de servicio).

---

## Lo que se compra acá (y lo que no)

**No se vende el número.** La cadena es pública: cualquiera puede leer
`NewFeedback` y `Registered` y sacar sus propias cuentas. Se cobra el trabajo que
hay entre esos eventos y una respuesta accionable, que son cuatro cosas caras y
una que ni siquiera es cara — es imposible de saltear:

1. **El universo entero, que no se puede enumerar de otra forma.** El Identity
   Registry de mainnet **no expone `totalSupply()`** (verificado en base,
   ethereum y polygon el 2026-08-10 — ver [[HANDOFF]]). No hay `for id in
   1..N`: la única manera de saber qué agentes existen es escanear el evento
   `Registered` de cada cadena sobre rangos amplios de bloques, que es
   exactamente donde un RPC público corta.
2. **La propiedad REAL de cada agente.** `Registered` carga al dueño
   **inicial**, que casi siempre es el facilitator que minteó el id antes de
   transferirlo. Un índice que joinea por ahí archiva a casi todos los agentes
   bajo el facilitator y no vale nada. Acá el join es por el dueño **actual**,
   verificado on-chain.
3. **Cada rating fechado.** El resumen que devuelve la cadena trae `client`,
   `feedbackIndex`, `value` y tags — **sin `txHash`, sin bloque, sin
   timestamp**. La fecha y la trazabilidad salen de leer los logs.
4. **Las revocaciones aplicadas.** Revocar **muta una fila existente sin mover
   el contador**: cualquier delta basado en contar sirve para siempre un score
   que incluye un rating revocado.
5. **Una política, versionada, la misma para todos.** Vive en
   [`describenet/aggregate.py`](../describenet/aggregate.py) y viaja en cada
   respuesta como `policy_version`. Si dos consumidores pudieran obtener números
   distintos, no habría política.

Un lookup cross-chain que a un tercero le cuesta horas de paginar un RPC de
archivo, acá es una llamada.

### Y desde el 2026-08-23, ni siquiera hace falta saber dónde mirar

Todo lo de arriba supone que ya sabés a **quién** estás mirando. Muchas veces no:
lo que se tiene en la mano es un string suelto — una dirección EVM, un base58 de
Solana, un número. `describe_resolve` toma eso y contesta **qué se interpretó** y
**en qué cadenas existe**, con la wallet dueña de cada identidad. Es gratis, y lo
es por la misma promesa que el resto de esta página: eso es el número y la
**ubicación**, no la descomposición.

Lo que cierra no es una comodidad, es un agujero. **Un `agent_id` no es un
identificador.** Los registries EVM mintean un rango denso desde 0 sin un solo
hueco, así que un id `k` existe en toda cadena que tenga más de `k` agentes — no
es una tendencia, es aritmética. Medido el 2026-08-23 sobre la copia local del
índice (93.696 agentes, transacción `read_only`):

```
agent_id distintos:                                  62.814
  … que existen en 2+ cadenas:                       26.736
  … de ésos, con el MISMO dueño en todas:                 0
  … de ésos, con dueños DISTINTOS:                   26.736
```

Cero excepciones. Preguntar *"¿de quién es el agente 25975?"* no tiene una
respuesta: tiene tantas como cadenas donde ese id exista, y son personas
distintas. Por eso `describe_resolve` **nunca contesta en singular** — devuelve
una fila por cadena, cada una etiquetada con la lectura que la produjo.

> 🔴 Esas cifras son de la **copia local**, no de producción: son dos índices de
> tamaño distinto. Antes de citarlas como números de producción hay que
> re-medirlas contra RDS. Lo que no cambia con el tamaño es el cero de la tercera
> línea, que es el hecho que importa.

Y en Solana el agujero es peor, porque ahí la ambigüedad es **sintáctica**: el
`agent_id` de un agente y la wallet de una persona son el mismo objeto — 32 bytes
en base58, sin prefijo, sin checksum. Se probaron las cuatro vías de separarlos y
ninguna sirve. Medido el 2026-08-23 sobre los 1.410 agent ids de Solana de la
copia local:

```
largo de los agent ids:      43 (75)  ·  44 (1.335)
largo de las 253 wallets:    43 (26)  ·  44 (227)      <- el mismo rango
on-curve  (son keypairs):    1.410 de 1.410
off-curve (serían PDAs):     0
```

Los assets de Metaplex Core son **keypairs, no PDAs**, así que el truco de mirar
si la clave cae en la curva —la única vía que prometía algo— no separa nada. Y no
hay prefijo vanity ni checksum donde agarrarse. **No existe el regex.** La
desambiguación tiene que ser por consulta, cuesta dos index scans, y por eso la
respuesta trae **las dos lecturas etiquetadas, sin elegir**: `interpretations`
dice qué permite la forma, y cada fila de `matches` dice bajo qué lectura la
encontró el índice.

¿Y puede un mismo string ser las dos cosas a la vez? Estructuralmente sí: el
layout de `AssetV1` guarda al dueño como un `Pubkey` pelado, sin exigir que sea
una cuenta de sistema. Medido el 2026-08-23 —el `INTERSECT` entre los `agent_id`
y los `current_owner` de solana—: **cero casos hoy**. Es raro, pero «raro» no es
una imposibilidad sobre la que se pueda apoyar código, y por eso la respuesta es
una lista y no una etiqueta.

Sin esta puerta, un agente que recibe `"25975"` y nada más no tenía ningún camino
gratis. Corrido el 2026-08-23 contra el servidor:

- `describe_check_wallet("25975")` → `status: error`, *"wallet inválida"*, y el
  hint enumera las dos formas de wallet **sin mencionar que existan agent ids**;
- `describe_lookup_agent` tiene `network` **sin default**: es imposible de
  llamar sin adivinarla;
- `describe_chains` sólo confirma cuántas veces hay que adivinar — once cadenas,
  leído de `GET /health` el 2026-08-23.

Le quedaba firmar hasta once veces. Y el paywall corre **antes** del ruteo, así
que **el 402 sale igual exista o no el agente**: hasta diez de esas once firmas
se pagan para recibir «no está», con un hint que dice *buscá por wallet* — el
consejo que el llamador no puede seguir, porque si tuviera la wallet no habría
preguntado.

---

## Dos transportes, un solo servidor

El código es uno: [`describenet/mcp_server.py`](../describenet/mcp_server.py).
Se sirve de dos formas, y todo lo que sigue en esta página vale para las dos.

### Remoto (Streamable HTTP) — sin instalar nada

Desde el 2026-08-23 el mismo servidor está montado dentro de la API, en
`https://api.describe.net/mcp`. Un cliente que hable Streamable HTTP se conecta
ahí y ve **exactamente el mismo juego de herramientas** — la afirmación de esta
sección es la PARIDAD entre los dos transportes, no un número: el conteo vive
arriba, medido una sola vez y con su fecha, para no tener dos renglones que haya
que acordarse de mover juntos.

```json
{
  "mcpServers": {
    "describe-net": {
      "type": "streamable-http",
      "url": "https://api.describe.net/mcp"
    }
  }
}
```

Tres cosas del remoto, las tres consecuencia de que corre en AWS Lambda:

- **Sin estado.** Cada mensaje JSON-RPC es un POST independiente: el servidor
  no emite `mcp-session-id` y acepta `tools/call` sin `initialize` previo. Nada
  vive en memoria entre dos llamadas, porque la siguiente puede caer en otro
  contenedor.
- **Sólo JSON, sólo POST.** Las respuestas son `application/json` (no SSE) y
  `GET /mcp` contesta 405: no hay mensajes iniciados por el servidor y Lambda
  no puede sostener un stream abierto.
- **La misma caseta de peaje.** Adentro de la API las tools llaman a los
  endpoints en el mismo proceso (transporte ASGI, sin red) y atraviesan el
  mismo paywall: una tool paga devuelve el 402 de su ruta y se paga con tu
  firma en `payment`, exactamente igual que por stdio. Cambia dónde corre el
  proceso; no cambia nada de cómo se cobra ni quién firma.

### Local (stdio)

```bash
pip install -r mcp/requirements.txt
```

`mcp/server.py` es el lanzador: importa `main` de `describenet/mcp_server.py` y
lo corre por stdio. Configuración del cliente MCP (Claude Desktop, Claude Code,
cualquiera que hable stdio):

```json
{
  "mcpServers": {
    "describe-net": {
      "command": "python",
      "args": ["/ruta/absoluta/al/repo/mcp/server.py"],
      "env": { "DESCRIBENET_API_URL": "https://api.describe.net" }
    }
  }
}
```

`DESCRIBENET_API_URL` es la única variable y se lee **en cada llamada**, no al
importar: apuntarla a `http://localhost:8088` es todo lo que hace falta para
trabajar contra el índice local (`make up && make schema && make api`).

**No hay claves.** Este servidor no firma, no custodia nada y no puede mover tus
fondos. Tampoco toca la base de datos: le pega a la API HTTP como cualquier otro
consumidor.

---

## El loop de decisión

No es "llamar a un endpoint y leer un número". Es esto:

```
PASO −1 — GRATIS, Y SÓLO SI NO SABÉS QUÉ TENÉS EN LA MANO
describe_resolve(query)            ← ¿qué ES este string y en qué cadenas
                                     existe? Devuelve una fila por cadena con
                                     la wallet dueña ahí. Con eso ya tenés la
                                     llave fuerte —la wallet— para el paso 0.
                                     Si devuelve dos o más filas, NO son el
                                     mismo sujeto: elegí antes de seguir.

PASO 0 — GRATIS, SIEMPRE PRIMERO
describe_check_wallet(wallet)      ← ¿esta wallet TIENE reputación, y en qué
                                     cadenas? Si no tiene, no hay nada que
                                     comprar: acá termina el loop sin gastar.
describe_pricing()                 ← qué cuesta cada cosa, qué tan grande es el
                                     índice hoy, con qué política se calcula.
        │
        ▼
describe_lookup_wallet(wallet)     ← el agregado cross-chain de la contraparte
        │
        ├─ ¿402?  → firmá y repetí la MISMA llamada con `payment=` (abajo)
        │
        ▼
LEER LA COMPOSICIÓN, NO EL NÚMERO
        │
        ├─ distinct_raters  ¿cuántas personas DISTINTAS opinaron?
        ├─ top_client_share ¿hay una que concentra?
        ├─ self_rated.gap   ¿cuánto se sobrevalora?
        └─ caveats[]        las trampas que ESTOS datos disparan, ya escritas
        │
        ▼
describe_facets(wallet)            ← ¿en qué ÁREA es bueno, contra la media del
                                     índice? "gran liveness, pobre activity" es
                                     una decisión; "68" no lo es.
        │
        ▼
describe_history(wallet)           ← ¿venía subiendo o bajando? Un 85 que sube y
                                     un 85 que baja son decisiones opuestas.
        │
        ▼
describe_lookup_agent(net, id)     ← verificación: cada rating con su txHash.
                                     Con esto comprobás que no te mentimos.
```

**La regla que rige todo: nunca decidas con `final_score` solo.** Un score sin
su composición es un oráculo; con su composición es evidencia. Por eso cada
respuesta sube `distinct_raters`, `top_client_share` y `self_rated` a primer
nivel, y por eso el `policy_version` viaja siempre — incluso cuando falta, donde
llega como advertencia explícita en vez de como silencio.

### `caveats[]`: ramificá sobre el `code`, nunca sobre el `text`

Desde el **2026-08-28** cada entrada de `caveats[]` es un objeto, no un string:

```json
{ "code": "burn-address", "text": "Esta wallet es una direccion de quema…" }
```

**El contrato, y es el motivo del campo: `text` puede cambiar sin aviso
—re-redactarse, re-medirse, hasta traducirse—; `code` jamás.** El texto YA
cambió una vez y rompió en silencio a todo el que hacía matching de prosa. Los
codes no llevan versión ni bumpean `policy_version`, porque un caveat es
advisory por construcción: nombra un CORTE, no mueve un score ni un precio.

Los ocho, y son todos —el set está congelado por test, agregar o renombrar uno
se pone rojo a propósito—: `no-score` · `concentration-degraded` ·
`single-rater` · `few-raters` · `top-client-share` · `campaign-per-rater` ·
`self-rated` · `burn-address`.

Dos cosas que muerden. **Una lista vacía no es «verificado limpio»**: es «no
disparó ninguna trampa sobre los campos de ESTA respuesta». Y en la puerta
gratis (`describe_check_wallet` / `GET /wallets/{wallet}/chains`) la lista es un
**subconjunto** — sólo lo computable del agregado público, hoy `burn-address` —
así que vacío ahí no promete que la descomposición paga venga callada.

`mcp_server.py` arma además caveats **propios de su presentación** (series
truncadas, colas recortadas) y salen por la misma puerta con la misma forma, a
propósito: dos formas en la misma lista obligarían al consumidor a ramificar
sobre la forma antes de poder ramificar sobre el code.

---

## Cómo se paga un 402

Pedir sin pagar **no cuesta nada** y es el camino previsto: la primera llamada
devuelve el desafío, no un error.

1. **Leé el desafío.** `challenge` trae `amount`, `token`, el destinatario y
   `supportedChains`. Los valores salen de ahí, nunca de una tabla cacheada.
2. **Verificá el destinatario.** `recipient_check.verdict`. La única dirección a
   la que este servicio pide que le paguen es
   `0xe4dc963c56979E0260fc146b87eE24F18220e545`, y está pinneada en el servidor
   justamente para poder comparar. Si dice `DO_NOT_PAY`, **no pagues**: o el
   desafío no vino de describe.net, o la tesorería cambió y el servidor no lo
   sabe. Los dos casos se resuelven preguntando, no firmando.
3. **Firmá.** Una autorización EIP-3009 `TransferWithAuthorization` por ese
   monto, a ese destinatario, en una de esas cadenas, codificada en base64.
   - desde el MCP de tu wallet: `ows_sign_eip3009` (OWS) o `pay_x402` (PayBox);
   - o en Python, con el SDK:
     ```python
     from uvd_x402_sdk import X402Client, X402Config

     client = X402Client(config=X402Config(recipient_evm=recipient))
     client.connect_with_signer(...)          # tu clave nunca sale de tu lado
     header = client.create_authorization(     # → el string base64
         pay_to=recipient, amount_usd=Decimal(amount), chain_name="base",
     )
     ```
4. **Repetí la misma llamada con `payment=<base64>`.** Mismos argumentos, un
   campo más.

El **gas lo paga el facilitator** (`https://facilitator.ultravioletadao.xyz`):
vos sólo firmás. Y el nonce de la autorización se consume cuando se liquida, así
que **una credencial que ya pagó no paga dos veces** — una firma por llamada
paga.

> Si un 4xx llega *después* de mandar `payment`, casi siempre es una credencial
> gastada o firmada por otro monto. Reintentar con la misma falla igual: pedí sin
> `payment`, leé el desafío nuevo y firmá contra ése.

---

## Las herramientas

Las ocho gratis — no aceptan `payment` y no pueden devolver 402:

| Herramienta | Qué contesta |
|---|---|
| `describe_pricing` | Qué cuesta cada cosa (sondeado del 402 **vivo**, nunca de una constante), tamaño vivo del índice, políticas vigentes. Empezá acá. |
| `describe_resolve` | ¿Qué **es** este string y dónde existe? Una dirección EVM, un base58 de Solana o un agent id: contesta qué se interpretó (`interpretations`) y una fila por cadena (`matches`), cada una con la lectura que la produjo y la wallet dueña **ahí**. Match exacto: sin prefijo, sin comodín, sin listado y sin búsqueda por nombre. Nunca contesta en singular. |
| `describe_check_wallet` | ¿Esta wallet tiene reputación, y en qué cadenas? El paso 0 antes de pagar: si acá no hay nada, no hay nada que comprar. |
| `describe_chains` | Cada cadena indexada lado a lado — o una en detalle (`network=`), con su top puntuado sólo con los ratings de esa red. `stale_hours` incluida: la señal de frescura que `blocks_behind` no puede dar. |
| `describe_feed` | Las últimas calificaciones del índice entero —o de una sola cadena con `network`, desde 2026-08-23—, ordenadas por el reloj de la **cadena** (`block_time`), con red, valor, faceta y `tx_hash`. |
| `describe_index_status` | `GET /health` servido solo: totales, las cuatro políticas —con los `confidence_thresholds` que reproducen la banda— y el estado de escaneo por cadena. Para un monitor que no necesita los **trece** sondeos de precio que dispara `describe_pricing` (una entrada de `_PROBES` por cada herramienta que no sea ella misma — contadas el 2026-08-30). *(Historia de esta celda, y vale más escrita: decía **trece** cuando eran doce, se corrigió a **doce** el 2026-08-23, y volvió a ser trece el 2026-08-29 con `describe_rater_profile`. El número no es un dato independiente: es `tools − 1`, y esta celda lo tipeó tres veces a mano en lugar de decir la regla. Ahora dice las dos.)* |
| `describe_types` | Cobertura por tipo **autodeclarado**, con `unknown_share` calculado sobre los números vivos. |
| `describe_manifesto` | El manifiesto como datos: cada principio con el código que lo hace cumplir y cada afirmación con el comando que la verifica. |

Las seis pagas — sin `payment` devuelven el desafío 402, nunca un cobro:

| Herramienta | Qué contesta |
|---|---|
| `describe_lookup_wallet` | La reputación de una wallet sumando **todas** las cadenas, con su composición. `snapshot=True` deja un recibo replayable. |
| `describe_rater_profile` | **El otro lado del grano**: la wallet como CALIFICADORA, no como sujeto. Cuánto emitió, a cuántos sujetos distintos, con qué dispersión (`value_stddev` cerca de cero = una máquina de estampar, y eso lo leés vos: acá no hay corte ni veredicto), el reparto por cadena, la parte de su sujeto favorito y los roles leídos de `tag1`. **No emite score y no va a emitirlo** — rankear calificadores sería un veredicto nuevo encima del que este índice ya se niega a dar. Es el seguimiento natural de un caveat `top-client-share` o `campaign-per-rater`: dice si la voz que concentra es una contraparte prolífica o un sello de goma. Envuelve `GET /reputation/rater/{wallet}` ($0.01), la única ruta paga cuyo 402 **no** trae `free_preview`, a propósito: el preview gratis por wallet contesta otra pregunta. |
| `describe_lookup_agent` | Un agente y **cada** rating con su transacción — incluidos los revocados, marcados. La herramienta de verificación. Con `confidence` y `caveats[]` arriba, los mismos que sirve la puerta HTTP: `GET /reputation/agent/{net}/{id}` los trae desde el 2026-08-25 y esta tool no los subía, así que quedaban enterrados bajo la lista de ratings — el mismo pago comprando menos contexto por esta puerta. El eco se cura con `max_ratings` (default 200, `0` = todo) y el recorte **siempre se declara**. |
| `describe_history` | Cómo se movió el score, fechado por tiempo **on-chain**. |
| `describe_facets` | Reputación por área, comparada contra la media del índice. Sin `wallet`, la tabla de referencia global (esa variante es gratis). |
| `describe_leaderboard` | El top ordenado por **evidencia**, con el número que decidió el orden a la vista y `pagination` honesta (la API no publica el total; acá viaja `null`, no inventado). La primera página sale del `/leaderboard` gratis; paginar va a `/leaderboard/page`, que cobra. |

La descripción completa vive **en cada herramienta**: qué devuelve, qué cuesta,
por qué vale y cómo se lee. Es el producto, no documentación de apoyo — un
agente decide si paga leyendo eso.

### Salida estructurada (`outputSchema`): la decisión, no la omisión

El SDK (`mcp>=1.23`) ya genera `outputSchema` y `structuredContent` para todas
las herramientas de este servidor: el retorno `dict[str, Any]` produce
`{"type": "object", "additionalProperties": true}` (medido contra el SDK
1.23.3, no leído del changelog). **No se declara un esquema más estricto a
propósito.** El contrato real de cada herramienta es una unión discriminada por
`status` — `ok`, `payment_required`, `nothing_to_buy`, `not_found`,
`unreachable`, `error` — y FastMCP **valida cada retorno contra el esquema
declarado**: tipar sólo la rama `ok` convertiría el desafío 402 (la respuesta
más importante del producto) en un error de protocolo justo cuando el agente
necesita leerlo. Un esquema que prohíbe la respuesta correcta es peor que un
esquema laxo.

---

## Trampas medidas (no hipotéticas)

- **Un `agent_id` no identifica a nadie por sí solo.** El mismo número es un
  agente distinto —y de otra persona— en cada cadena, y en Solana un agent id y
  una wallet son sintácticamente indistinguibles. Un integrador que trate un id
  suelto como llave está agregando reputación de desconocidos. La llave fuerte es
  la **wallet**; `describe_resolve` es la que convierte un id en esa wallet. Las
  mediciones, arriba.
- **Un 100 de un solo calificador no es un agente mejor: es uno menos
  observado.** Medido el 2026-08-10: de 15.763 wallets con ratings, **7.577
  tienen exactamente un calificador**, y el 22% de ésas promedia 100 perfecto —
  contra el 1% entre las de tres calificadores. Por eso el leaderboard **no**
  ordena por promedio.
- **`top_client_share` sola no detecta una campaña.** El caso extremo del índice
  es un agente con **300.001 ratings de 57 calificadores** (medido 2026-08-11;
  una edición anterior de esta página decía 62, que era imposible contra esa
  medición), todos con el mismo valor y el mismo tag: su share es ~0,14 y un
  umbral lo dejaría pasar. Lo que lo delata es ratings **por** calificador.
- **`declared_type` es autodeclarado.** Sale del JSON del `agentURI`, que es
  libre. ERC-8004 **no tiene campo de tipo**. Nunca lo trates como una
  verificación.
- **`tag1` es texto libre on-chain** y en este índice hay párrafos de 300
  caracteres usados como tag. La vista de facetas está curada por eso, dice
  cuántas dejó afuera, y el desglose completo sigue en `data`.
- **`concentration: null` no es "sin concentración"**: es la señal caída. La API
  la degrada antes que fallar la respuesta, y acá eso llega como advertencia.
- **Sin ratings ≠ cero.** "No hay evidencia" y "lo calificaron mal" son hechos
  distintos, y el índice se niega a colapsarlos.
- **`undated_reviews > 0`** significa que el último punto de la serie puede estar
  legítimamente por debajo del `final_score` del perfil. No es un bug de ninguno
  de los dos.

---

## Enlaces

Todo lo que un integrador (humano o agente) puede querer al lado de este server:

| Qué | Dónde |
|---|---|
| OpenAPI (el esquema vivo de la API) | <https://api.describe.net/openapi.json> |
| Swagger UI | <https://api.describe.net/docs> |
| ReDoc | <https://api.describe.net/redoc> |
| Hub de documentación | <https://docs.describe.net> |
| Skill paraguas para agentes | <https://describe.net/skill.md> |
| Workflows con curls reales | <https://describe.net/workflows.md> |
| llms.txt del sitio | <https://describe.net/llms.txt> |
| Server card MCP | <https://describe.net/.well-known/mcp/server-card.json> |
| Cómo se paga (x402), en detalle | <https://describe.net/auth.md> |
| La puerta A2A (`message/send`) | <https://describe.net/a2a.md> |
| El badge embebible (gratis) | <https://describe.net/badge.md> |

---

## Lo que este servidor NO hace

- **No calcula.** Ni una regla de score vive acá; toda herramienta llama al
  endpoint canónico. Si un consumidor pudiera producir un número distinto
  llamando por otra puerta, la política habría dejado de ser una.
- **No cobra por su cuenta.** Una sola caseta de peaje, en la capa HTTP, con el
  SDK x402. Este proceso relaya el 402 y reenvía el header firmado.
- **No firma ni guarda claves.**
- **No habla con las cadenas** ni con Postgres.

---

## Tests

```bash
python -m pytest tests/test_mcp.py -q          # las tools, sin red y sin base de datos
python -m pytest tests/test_mcp_remote.py -q   # el remoto (POST /mcp), contra el Postgres local
```

En `test_mcp.py` todo entra por un `httpx.MockTransport`: un test de esta capa
que dependiera de la API viva mediría el humor del entorno, y encima cambiaría
de resultado según si el paywall está prendido en ese momento.
`test_mcp_remote.py` levanta la app entera con `TestClient` y prueba el
handshake, `tools/list` contra el server card, una tool gratis con datos reales
de la base local —con `DESCRIBENET_API_URL` apuntando a un host inexistente,
para que sólo pase si el transporte in-process está cableado— y que la puerta
no cobra pero la tool paga sigue relayando su 402.

La suite está verificada **por mutación**, no por color: se rompió el
desenvoltorio del 402 de FastAPI, el chequeo de destinatario, el envío del
header `X-PAYMENT`, el filtro de tags de prosa, la advertencia de
`policy_version` faltante, la clasificación del 402, el aviso de ratings sin
fechar y el precio por defecto — **las ocho fallaron un test**. Y el 2026-08-21,
tres mutaciones más sobre lo nuevo: el `has_more` de la paginación forzado a
`False`, la trazabilidad del eco contada DESPUÉS del recorte, y la regex de
wallet aceptando todo — **las tres fallaron el test que las cubre**. Un check
verde que mira el objeto equivocado es peor que no tener check.
