knownfy-mcp
v1.0.5
Published
MCP server for the Knownfy compliance intelligence API (KYC/KYB). Investigate entities, query relationship graphs, monitor watchlists and run bulk batches from Claude Desktop, Cursor or Claude Code.
Maintainers
Readme
knownfy-mcp
MCP (Model Context Protocol) server para la API de Knownfy. Expone la capa de inteligencia de compliance (KYC/KYB) como tools para que un agente —Claude Desktop, Cursor, Claude Code, etc.— pueda investigar entidades, consultar grafos de relaciones, monitorear watchlists y correr lotes masivos.
Framing: Knownfy expone información pública para apoyar el criterio del analista. No es una plataforma de decisión; los reportes son inteligencia, no veredictos.
Instalación
No hay que instalar nada: se corre con npx.
{
"mcpServers": {
"knownfy": {
"command": "npx",
"args": ["knownfy-mcp"],
"env": {
"KNOWNFY_API_TOKEN": "knfx_live_...",
"KNOWNFY_API_URL": "https://api.knownfy.app"
}
}
}
}- Claude Desktop:
~/Library/Application Support/Claude/claude_desktop_config.json - Cursor:
.cursor/mcp.jsonen el workspace - Claude Code:
claude mcp add knownfy -- env KNOWNFY_API_TOKEN=knfx_live_... npx knownfy-mcp
Autenticación (dual)
Knownfy acepta dos tipos de credencial — configurá una:
| Var | Cuándo | Header | Notas |
|---|---|---|---|
| KNOWNFY_BEARER_TOKEN | usuario con cuenta / subscription (JWT de Auth0) | Authorization: Bearer | Precede a la API key si están ambas. |
| KNOWNFY_API_KEY | partner / máquina | X-API-Key | Generala en Settings → API keys. |
| KNOWNFY_API_TOKEN | una sola var (lo que muestra /developers) | autodetecta | Si empieza con eyJ → Bearer; si no → X-API-Key. |
| KNOWNFY_API_URL | opcional | — | Default https://api.knownfy.app. Sandbox: https://api.dev.knownfy.app. |
Cómo obtener la credencial
- Usuario con cuenta (JWT): logueá en el front, DevTools → Network → cualquier
request a la API → copiá el header
Authorization: Bearer eyJ...(sin el prefijoBearer). - Partner (API key): https://knownfy.app/settings/api-keys → Generar clave. Se muestra una sola vez; guardala. Se revoca desde la misma pantalla.
Mandá una sola cabecera. El middleware evalúa el camino Bearer de Auth0 antes que el de
X-API-Key, así que una key válida enviada también como Bearer muere como JWT inválido (401). El cliente ya resuelve esto solo — no lo pises a mano.
Tools (13)
| Tool | Qué hace |
|---|---|
| investigate_entity | Resuelve persona/empresa a entidad canónica y devuelve inteligencia + cobertura. Dice si investigó o si devolvió una corrida anterior; force: true encola una nueva. |
| get_entity_intelligence | Composite de inteligencia de una entidad (tier, PEP/sanciones, señales, riesgo relacional). |
| search_entities | Busca entidades ya investigadas en el workspace. |
| get_entity_graph | Grafo de relaciones de una entidad, resumido: vecinos, histogramas y cautelas. Todo recorte se declara en warnings. |
| get_tenant_graph | Grafo del workspace, resumido y ordenado por señalamiento. Todo recorte se declara en warnings. |
| get_report | Reporte CDR completo. Acepta report_id (entidad ya investigada) o job_id (corrida recién encolada). |
| watch_entity | Suscribe webhooks ante cambios del perfil de una entidad. |
| list_watchlist | Lista las entidades en monitoreo. |
| list_alerts | Lista alertas de watchlist. Cada alerta trae metadata.alert_type (direct / relational_risk): filtrá sobre ese campo, la API no tiene filtro server-side. |
| link_entities | Crea una relación verificada por analista entre dos entidades. |
| submit_bulk_batch | Encola hasta 500 entidades para investigación en paralelo. |
| get_bulk_status | Progreso de un lote. |
| get_bulk_intelligence | Inteligencia completa de todas las entradas terminadas de un lote. |
Flujo típico: investigate_entity → get_entity_intelligence → get_entity_graph /
watch_entity. Para volumen: submit_bulk_batch → get_bulk_status → get_bulk_intelligence.
Desarrollo
npm install
npm run build # compila TS → dist/
npm test # tsc + guardas (node:test, sin deps nuevas)
npm start # corre dist/index.js (stdio)npm test corre también como prepublishOnly: un publish con las guardas en
rojo no sale.
Probar con MCP Inspector:
npm run build && npx @modelcontextprotocol/inspector node dist/index.jsChangelog
1.0.5
El aviso de identidad que 1.0.4 estrenó podía mentir. Medido contra dev el mismo día que salió, sobre una entidad real, estas dos frases volvieron en el mismo objeto:
note: "NO se investigó nada en esta llamada."
identity_warning: "Se investigó sin CUIT/CUIL."y tres campos más abajo, identifiers: [{type: "cuit", value: "20-…"}], con la
fuente ARCA en status: "found". La entidad tenía el identificador guardado y la
investigación lo había usado.
La causa: el aviso se calculaba con los argumentos de la llamada, antes de
mirar la respuesta. Como el tax_id no viajaba en esa llamada, disparaba — sin
enterarse de que el backend ya lo tenía. Es la misma familia que este cliente
persigue, apuntando al otro lado: no vuelve limpio lo sucio, pero un aviso que
aparece siempre deja de leerse, y el día que salga el verdadero va a estar
tapado por el ruido que generó el falso.
- Se calcula después del fetch y mirándolo. Si la ficha ya trae un ancla
fiscal, no se advierte. Los tipos que cuentan como ancla se enumeran
(
cuit,cpf,rut,ruc,rfc…): la ficha también trae identificadores que no desambiguan a nadie, y contarlos apagaría la guarda justo cuando hace falta. - Se leen los dos niveles del payload.
identifierscuelga de la raíz, pero el mismo objeto trae un sub-objetointelligenceanidado. Leer el nivel equivocado devuelvefalsey el aviso vuelve a mentir — el mismo error una capa más adentro. - El tiempo verbal sigue a los hechos. Sobre una llamada que no investigó nada, ahora dice "la investigación previa se corrió sin …, y en la ficha tampoco hay uno guardado", en vez de afirmar una corrida que no ocurrió.
La guarda no se desactiva sola: sin ficha, con identifiers: [], o con un
identificador que no ancla, la advertencia sigue saliendo. Hay un test para cada
uno de esos tres casos, porque la diferencia entre un arreglo y un silenciador
es exactamente esa.
1.0.4
El resumidor de grafo que salió en 1.0.3 leía el endpoint equivocado. Knownfy
tiene dos grafos y hablan dos idiomas: /api/v2/tenant/graph manda source /
target / relation_type / confidence, y /api/v1/entities/{id}/graph —el que
usa get_entity_graph— manda source_node_id / target_node_id / relation /
weight. El cliente pedía el segundo y leía los campos del primero.
Medido contra una entidad real: los vecinos salían sin id, sin nombre y sin tipo
de vínculo (sólo source_type), y el histograma decía relation_types:
{undeclared: 5}. Un agente recibía cinco relaciones anónimas y las narraba como
"no se declara el tipo" — cuando el tipo estaba en la respuesta, intacto, bajo otra
clave. Los 17 tests pasaban en verde, porque los fixtures los escribí desde la
forma que supuse en vez de capturar una respuesta. Un test contra un esquema
inventado no es una guarda: es una segunda copia de la misma suposición.
- Se leen los dos vocabularios. Los nodos se indexan por
node_idy porid(las aristas referencian el primero, la identidad viaja en el segundo), y la raíz se resuelve portarget_node_id,entity_idois_target, así que la arista que apunta hacia la entidad también resuelve su otro extremo. weightno se publica como confianza. En el v1 está cableado en 3.0 para toda arista. Presentarlo comoconfidencefabricaba un orden con apariencia de medición. Sin confianza real, los vecinos se ordenan por respaldo (grounding_status) y se dice con qué criterio se ordenó.topological_inferenceahora cuenta como inferencia. Es "los conecto porque aparecen afiliados al mismo hub", la evidencia más débil del set — y era el 60% de las aristas del caso medido. 1.0.3 no emitía una sola cautela.grounding_status: unverifiedsale nombrado, con sugrounding_reason. De dónde salió una arista y si alguien la corroboró son dos ejes distintos.- Se declara cuándo las aristas exceden a los vecinos. El mismo par puede entrar dos veces con respaldo distinto en cada observación: contar filas dice "5 vínculos" sobre 4 contrapartes.
is_canonicalno existe en el v1; se deriva deentity_id/raw_entity_id. Antes dabaundefinedpara todos y ningún nodo contaba como crudo: nombres que nadie investigó se leían como entidades investigadas y limpias.- El tope de 200 del endpoint se nombra. El backend pide las relaciones con
limit=200cableado y no declara si topeó. No poder saber si cortó no es lo mismo que saber que no cortó.
La raíz: los fixtures de los tests pasan a ser respuestas capturadas de la API
(src/fixtures.ts), copiadas verbatim en claves y estructura. Las 10 guardas nuevas
fallan las 8 que corresponden contra el código de 1.0.3 — verificado corriéndolas
contra el commit publicado, no contra la intención.
El contexto se aceptaba y se tiraba
investigate_entity declaraba extra_context en el schema y lo mandaba sólo en
la rama force, que es la que va por /api/v1/investigations/. El body de
resolve no lo llevaba — y resolve es el único camino por el que se encola la
investigación de una entidad nueva. O sea: justo en el caso donde la
investigación de verdad corre, el "para qué" se descartaba en silencio. Del otro
lado había dos pérdidas más, arregladas en el mismo release (apply_extra_context
en el backend): resolve no leía extra_context del body, y
/api/v1/investigations/ lo escribía siempre bajo la clave del perfil, mientras la
rama company del flow lee company_extra_context y nunca mira la otra.
La guarda de identidad que la UI tiene y el agente no
En la UI, si el país es LATAM y no cargaste identificador fiscal, un modal te frena
y te hace elegir "Continuar sin identificador". Esa protección no llega al canal del
agente por dos razones: un agente no puede clickear un modal, y volver tax_id
obligatorio es peor que no tenerlo — un LLM frente a un campo requerido no pregunta,
rellena, y un CUIT inventado no es una omisión sino una afirmación falsa que ancla
la investigación a otra persona.
Así que la protección cambia de forma, no de umbral (src/identity.ts, espejo de
LATAM_COUNTRIES y de la copia taxid_warning.* del front):
investigate_entitydevuelveidentity_warningcuando el país es LATAM y no haytax_id, nombrando el documento que correspondía (CUIT/CUIL, CPF/CNPJ, RUT…) y diciendo qué hacer: pedirle el identificador al usuario y volver a llamar.submit_bulk_batchlo resume por lote: cuántas filas de cuántas van sin ancla, nombrando hasta cinco. 500 avisos iguales no son 500 avisos, son ruido que tapa el único que importaba.- Las descripciones de tool piden preguntar ANTES, que es la única manera de que la guarda sea previa al gasto del crédito, como el modal.
1.0.3
Tres defectos medidos contra dev con una cuenta real. Los tres comparten forma: un resultado recortado o filtrado que no decía que lo estaba.
status: "all"devolvía vacío. El centinela lo inventó este cliente; la API nunca lo tuvo, y del otro lado entraba comoWHERE status = 'all'. Medido:list_watchlist(status:"all")devolvíaentries: []en el mismo objeto quetotal_monitored: 9y un histograma que sumaba 9. Nada fallaba: un 200 con lista vacía es indistinguible de un workspace sin nada monitoreado. Ahora"all"omite el parámetro.- Los grafos no entraban en una ventana de razonamiento. 139 KB para una entidad
y 763 KB / 419 nodos para el workspace — pasando
limit: 5, porque el backend aplica ese tope a la semilla de canónicas y cada punta de arista vuelve a entrar como nodo. Ahora se devuelve un resumen, y todo recorte —del backend o de este cliente— sale nombrado enwarnings. alert_typeno existía del lado del servidor. Se declaraba en el schema y viajaba en la query string; Flask lo ignoraba. Un agente que pedía sólo las relacionales recibía todas y las narraba como relacionales. Se saca del schema; el campo viaja enmetadata.alert_typede cada alerta. En su lugar se exponenseverityyentity_id, que el endpoint sí filtra.
Primeras guardas del paquete (17 casos) y primer job de CI: hasta esta versión el cliente MCP no tenía tests, ni typecheck en CI, ni gate de publicación.
1.0.2
Un veredicto limpio y un veredicto sin datos se veían igual. Esta versión los separa.
get_reportaceptareport_id, no sólojob_id. Para una entidad ya investigadainvestigation_job_idesnull, así que el informe CDR completo (~170 KB: qué fuentes se consultaron, cuáles fallaron, qué datasets se screenearon) era inalcanzable desde el MCP y la única lectura posible eran los ~1,5 KB de síntesis.- Bloque
coveragenuevo eninvestigate_entityyget_entity_intelligence: fuentes consultadas con su estado, datasets screeneados, consultas ejecutadas, frescura — máswarningsen prosa. La prosa importa: unnullestructural no sobrevive a un LLM resumiendo su propia salida. - Las advertencias marcan calidad o confianza < 60, fuentes en error/timeout, crawls
inalcanzables, cobertura parcial declarada, y el caso peor:
screening: "clear"sin registro de ninguna consulta ejecutada. investigate_entityya no miente sobre lo que hizo. Antes decía "investigación completada" cuando sólo había resuelto una entidad preexistente. Ahora devuelveinvestigation_performedy unnoteque dice si los datos son de esta corrida o de una anterior (con fecha). El nuevo flagforceencola una corrida real.- Con una clave de sandbox (
environment: test) la API devuelve un job simulado (mock_…). El MCP lo detecta y lo dice, en vez de reportar una investigación que nunca corrió.
1.0.1
list_watchlistpegaba a/api/v2/watchlistsin barra final. La ruta canónica es/api/v2/watchlist/, así que Flask contestaba 308 y la credencial se perdía en el salto: el síntoma era un 401 que parecía "key mala" cuando en realidad era la URL.- El cliente ahora sigue los 307/308 a mano re-enviando las cabeceras, para que un redirect nunca vuelva a leerse como un problema de credencial.
- El source del paquete pasa a estar versionado (
clients/knownfy-mcp/). Hasta 1.0.0 sólo existía eldist/publicado en npm y no se podía reconstruir.
Notas
- Disponible en planes Pro y Teams.
- La superficie cubre el camino curado v1 de developers. Para todos los endpoints,
usá la colección de Postman
Knownfy API (Partners).
