Skip to content

Cliente MCP (TypeScript + Express) que sincroniza catálogos ARDF, indexa en SQLite (sqlite-vec) y expone búsqueda híbrida (semántica+léxica) vía API REST ( /status, /sync, /refresh, /search, /resources/:id ).

Notifications You must be signed in to change notification settings

MauricioPerera/MCPARDF_CLIENT

Repository files navigation

MCP ARDF Client

Endpoints

  • GET /status: devuelve ok, dbPath, dims, uptimeSeconds y embedReady (true si EMBED_URL y EMBED_DIMS > 0).
  • GET /health: endpoint público de salud. Verifica conectividad a la base de datos y estado del servicio de embeddings cuando está configurado.
  • GET /metrics: habilitado cuando ADMIN_ENABLED=1. Si ADMIN_TOKEN está definido, requiere Authorization: Bearer .
  • POST /search: búsqueda híbrida (KNN + FTS) por defecto; enviar { hybrid: false } para modo KNN-only.
  • GET /resources/:id y GET /resources: acceso a recursos indexados.

Interfaces MCP

  • Streamable HTTP en /mcp: expone herramientas y recursos MCP reales (requiere la misma autenticaci��n que el resto del API) para hosts compatibles con el protocolo.
  • Herramientas registradas:
    • search_resources: reutiliza la b��squeda h��brida y devuelve items, score, sim, bm25 junto con limit/offset.
    • Recurso ardf://resource/{id}: descriptor ARDF completo (JSON).
    • Prompt ardf/howto: pautas para combinar b��squeda + lectura de descriptores.
  • STDIO: npm run mcp:stdio lanza el servidor MCP sobre STDIO; Ctrl+C realiza un cierre limpio del transporte.
  • Gating: la herramienta de b��squeda y resources/read respetan visibility == 'public' antes de devolver resultados.

Cuerpo de /search

Campos admitidos:

  • query: string (requerido)

  • allowedTypes: ["tool" | "prompt" | "workflow" | "resource"][] (opcional)

  • domain, tag, tier, neededScope, serverUrl: string (opcionales)

  • k: número entero positivo ≤ SEARCH_K_MAX (opcional; se aplica clamp; por defecto SEARCH_K)

  • hybrid: boolean (opcional, default true)

  • wSim, wBm25: números en [0,1] (opcionales). Si ambos se envían, se normalizan para que wSim + wBm25 = 1. Si ambos están presentes, su suma debe ser > 0; si la suma ≤ 0, se responde 400 con { error: "invalid_body", details }.

Notas:

  • En hybrid=false, se usa sólo KNN. Si el servicio de embeddings no está disponible por fallo de red/timeout, se devuelve 200 con items vacíos.
  • Backoff de embeddings: si el servicio responde 429, se respeta Retry-After (segundos o fecha). Si no está presente, se usa 100ms*intento y se aplica jitter 0.5–1.5x.
  • Si el k solicitado excede SEARCH_K_MAX, se ajusta automáticamente y se registra un log informativo con kRequested, kMaxEnv y kUsed.
  • Validación de pesos: cuando se envían ambos wSim y wBm25, se requiere que wSim + wBm25 > 0; en caso contrario, 400 con detalles de validación.

/health – payload de respuesta

Ejemplo de respuesta: { ok: true, db: { ok: true }, embeddings: { configured: true, ok: true, dims: 384, model: "dummy-embeddings", tookMs: 12 } } Notas:

  • Si embeddings no está configurado (falta EMBED_URL/EMBED_MODEL/EMBED_DIMS), embeddings.error = "missing_env" y ok global considera sólo DB.
  • Si está configurado, se realiza una vectorización mínima con input "ok" y se valida que las dimensiones coincidan.
  • /health es público (exento de API_TOKEN) al igual que /status.

Variables de entorno relevantes

  • EMBED_URL: URL del servicio de embeddings
  • EMBED_MODEL: nombre de modelo
  • EMBED_DIMS: dimensiones esperadas del vector
  • EMBED_TOKEN: Bearer opcional para el servicio de embeddings
  • LOG_LEVEL: nivel de log (pino)
  • ADMIN_ENABLED=1: habilita /metrics
  • ADMIN_TOKEN: protege /metrics con Bearer
  • SEARCH_K: K por defecto en KNN
  • SEARCH_K_MAX: límite máximo para k (clamp del valor solicitado; por defecto 100)
  • HYBRID_WEIGHT_SIM / HYBRID_WEIGHT_BM25: pesos por defecto de fusión (si no se envían en el body)
  • DB_PATH: ruta a la base de datos SQLite
  • MCP_SERVER_URL: URL por defecto para sincronización MCP
  • RATE_LIMIT: límite de peticiones por ventana para rate limiting opcional (ej., 60)
  • RATE_WINDOW_MS: tamaño de la ventana de rate limiting en milisegundos (ej., 60000)

Métricas expuestas (Prometheus)

  • embedding_latency_seconds: histograma de latencia de llamadas al webhook de embeddings
  • embedding_retries_total: contador de reintentos de embeddings
  • embedding_429_total: contador de códigos 429 del servicio de embeddings
  • knn_failures_total: contador de fallos en KNN
  • fts_failures_total: contador de fallos en FTS
  • search_latency_seconds: latencia de POST /search
  • search_requests_total{outcome}: total de POST /search por outcome
  • resources_latency_seconds: latencia de GET /resources
  • resources_requests_total{outcome}: total de GET /resources por outcome
  • sync_latency_seconds{route}: latencia de operaciones de sincronización (route ∈ {refresh, sync, import})
  • sync_requests_total{route,outcome}: total de operaciones de sincronización por ruta y outcome

Quickstart

npm i
cp .env.example .env
# PORT=8888 por defecto

npm run dev
curl -s http://127.0.0.1:8888/status | jq .

Modo laboratorio (todo local)

Lanza embeddings, servidor MCP de prueba y el cliente a la vez.

# terminal 1 — embeddings dummy (puerto 8788)
npm run mock:embed

# terminal 2 — MCP ARDF stub (puerto 8001)
npm run mock:mcp

# terminal 3 — cliente (puerto 8888)
# Bash/macOS:
export MCP_SERVER_URL=http://127.0.0.1:8001
export EMBED_URL=http://127.0.0.1:8788/embed
npm run dev

# Windows PowerShell:
$env:MCP_SERVER_URL = 'http://127.0.0.1:8001'
$env:EMBED_URL = 'http://127.0.0.1:8788/embed'
npm run dev

Un solo comando (requiere concurrently):

MCP_SERVER_URL=http://127.0.0.1:8001 \
EMBED_URL=http://127.0.0.1:8788/embed \
npm run lab

Windows PowerShell:

$env:MCP_SERVER_URL = 'http://127.0.0.1:8001'
$env:EMBED_URL = 'http://127.0.0.1:8788/embed'
npm run lab

Scripts de laboratorio unificados (cross‑platform)

  • npm run lab:start:9001: arranca mock‑embed en :4001 (dims=4, model=my-embeddings) y el cliente en :9001 (usa tsx src/server.ts). Variables: API_TOKEN=labtoken, EMBED_URL=http://127.0.0.1:4001/embed, EMBED_MODEL=my-embeddings, EMBED_DIMS=4.
  • npm run lab:start:9003: arranca mock‑embed en :8788, mock MCP en :8001 y el servidor compilado en :9003 con métricas admin (ADMIN_ENABLED=1, ADMIN_TOKEN=s3cret). Variables: PORT=9003, API_TOKEN=labtoken, ADMIN_ENABLED=1, ADMIN_TOKEN=s3cret, EMBED_URL=http://127.0.0.1:8788/embed, MCP_SERVER_URL=http://127.0.0.1:8001.

Notas:

  • Requiere tener instalados concurrently y cross-env.
  • Asegúrate de liberar los puertos correspondientes (4001/9001 o 8788/8001/9003) antes de ejecutar.
  • Para habilitar /metrics también en 9001, añade ADMIN_ENABLED=1 y opcionalmente ADMIN_TOKEN.
  • Verificaciones rápidas:
    • curl -s -H "Authorization: Bearer labtoken" http://127.0.0.1:9001/status | jq .
    • curl -s -H "Authorization: Bearer labtoken" http://127.0.0.1:9003/status | jq .
    • Métricas admin en 9003: curl -s -H "Authorization: Bearer s3cret" http://127.0.0.1:9003/metrics | head -n 20

Troubleshooting — puertos ocupados

  • Detectar proceso que ocupa un puerto (Windows/Git Bash):
netstat -aon | findstr ":4001"
netstat -aon | findstr ":9001"
netstat -aon | findstr ":8788"
netstat -aon | findstr ":8001"
netstat -aon | findstr ":9003"
  • Detener proceso por PID con PowerShell desde Git Bash:
powershell.exe -Command "Stop-Process -Id <PID> -Force"
  • Reintentar el arranque:
npm run lab:start:9001
# o
npm run lab:start:9003

Autenticación (opcional)

  • Algunas rutas pueden requerir token. Define API_TOKEN antes de arrancar el cliente:
    • Bash/macOS: export API_TOKEN=labtoken
    • Windows PowerShell: $env:API_TOKEN = 'labtoken'
  • /metrics requiere ADMIN_ENABLED=1 y, si existe ADMIN_TOKEN, usar Authorization: Bearer <token> o x-admin-token: <token>.

Probar /metrics

Más ejemplos: consulta la sección GET /metrics en API.md (API.md → GET /metrics).

  • Con ADMIN_TOKEN definido (ej. s3cret):
# respuesta 401 sin token o con token incorrecto
curl -i http://localhost:8888/metrics
curl -i -H 'Authorization: Bearer labtoken' http://127.0.0.1:8888/metrics
# respuesta 200 con token correcto
curl -i -H 'Authorization: Bearer s3cret' http://127.0.0.1:8888/metrics | head -n 20
  • Sin ADMIN_TOKEN (sólo ADMIN_ENABLED=1):
# respuesta 200
curl -i http://localhost:8888/metrics | head -n 20

Ejemplos rápidos en laboratorio (puertos 9001/9003): Índice rápido:

  • Métricas (/metrics) en 9001 y 9003
  • Status (/status) en 9001 y 9003
  • Search (KNN-only) en 9001 y 9003
  • Resources — listado
  • Resources — por id
  • Resources — filtros y orden
  • Sync/Refresh — POST /refresh y POST /sync/export
  • Sync — POST /sync
  • Resources — include=payload
# 9001 con ADMIN_TOKEN=s3cret
curl -s -H 'Authorization: Bearer s3cret' http://127.0.0.1:9001/metrics | head -n 20
# sin token -> 401/unauthorized
curl -i http://127.0.0.1:9001/metrics

# 9003 con ADMIN_TOKEN=s3cret
curl -s -H 'Authorization: Bearer s3cret' http://127.0.0.1:9003/metrics | head -n 20
# sin token -> 401/unauthorized
curl -i http://127.0.0.1:9003/metrics

Status:

curl -s -H 'Authorization: Bearer labtoken' http://127.0.0.1:9001/status | jq .
curl -s -H 'Authorization: Bearer labtoken' http://127.0.0.1:9003/status | jq .

Search (KNN-only):

# /search KNN-only en 9001
curl -s -H 'Authorization: Bearer labtoken' -H 'content-type: application/json' \
  -d '{"query":"demo","k":5,"allowedTypes":["tool","prompt","workflow","resource"],"hybrid":false}' \
  http://127.0.0.1:9001/search | jq .

# /search KNN-only en 9003
curl -s -H 'Authorization: Bearer labtoken' -H 'content-type: application/json' \
  -d '{"query":"demo","k":5,"allowedTypes":["tool","prompt","workflow","resource"],"hybrid":false}' \
  http://127.0.0.1:9003/search | jq .

Resources:

# Listar recursos en 9001
curl -s -H 'Authorization: Bearer labtoken' http://127.0.0.1:9001/resources | jq .

# Listar recursos en 9003
curl -s -H 'Authorization: Bearer labtoken' http://127.0.0.1:9003/resources | jq .

Resources por id:

# Obtener el primer recurso devuelto en 9001
rid=$(curl -s -H 'Authorization: Bearer labtoken' http://127.0.0.1:9001/resources | jq -r '.items[0].id')
curl -s -H 'Authorization: Bearer labtoken' http://127.0.0.1:9001/resources/$rid | jq .

# Obtener el primer recurso devuelto en 9003
rid=$(curl -s -H 'Authorization: Bearer labtoken' http://127.0.0.1:9003/resources | jq -r '.items[0].id')
curl -s -H 'Authorization: Bearer labtoken' http://127.0.0.1:9003/resources/$rid | jq .

Resources con filtros y orden:

# 9001 — incluir payload, filtrar por tipo y tag, ordenar por updatedAt desc
curl -s -H 'Authorization: Bearer labtoken' \
  'http://127.0.0.1:9001/resources?limit=50&offset=0&type=tool&tag=beta&sort=updatedAt&order=desc&include=payload' | jq .

# 9003 — filtrar por dominio y scope requerido
curl -s -H 'Authorization: Bearer labtoken' \
  'http://127.0.0.1:9003/resources?domain=demo&neededScope=mcp:read' | jq .

# 9003 — filtrar por serverUrl y ordenar por título asc
curl -s -H 'Authorization: Bearer labtoken' \
  'http://127.0.0.1:9003/resources?serverUrl=http://127.0.0.1:8001&sort=title&order=asc' | jq .

Sync/Refresh en laboratorio:

# POST /refresh — 9001 (requiere serverUrl)
curl -s -X POST \
  -H 'content-type: application/json' \
  -H 'Authorization: Bearer labtoken' \
  -d '{"serverUrl":"http://127.0.0.1:8001"}' \
  http://127.0.0.1:9001/refresh | jq .

# POST /refresh — 9003
curl -s -X POST \
  -H 'content-type: application/json' \
  -H 'Authorization: Bearer labtoken' \
  -d '{"serverUrl":"http://127.0.0.1:8001"}' \
  http://127.0.0.1:9003/refresh | jq .

# POST /sync/export — 9001 (usando el primer id de /resources)
rid=$(curl -s -H 'Authorization: Bearer labtoken' http://127.0.0.1:9001/resources | jq -r '.items[0].resource_id // .items[0].id')
curl -s -X POST \
  -H 'content-type: application/json' \
  -H 'Authorization: Bearer labtoken' \
  -d '{"ids":["'"$rid"'"]}' \
  http://127.0.0.1:9001/sync/export | jq .

# POST /sync/export — 9003
rid=$(curl -s -H 'Authorization: Bearer labtoken' http://127.0.0.1:9003/resources | jq -r '.items[0].resource_id // .items[0].id')
curl -s -X POST \
  -H 'content-type: application/json' \
  -H 'Authorization: Bearer labtoken' \
  -d '{"ids":["'"$rid"'"]}' \
  http://127.0.0.1:9003/sync/export | jq .

Sync (POST /sync) en laboratorio:

# 9001 — sincronizar contra MCP stub (puerto 8001)
curl -s -X POST \
  -H 'content-type: application/json' \
  -H 'Authorization: Bearer labtoken' \
  -d '{"serverUrl":"http://127.0.0.1:8001"}' \
  http://127.0.0.1:9001/sync | jq .

# 9003 — sincronizar contra MCP stub (puerto 8001)
curl -s -X POST \
  -H 'content-type: application/json' \
  -H 'Authorization: Bearer labtoken' \
  -d '{"serverUrl":"http://127.0.0.1:8001"}' \
  http://127.0.0.1:9003/sync | jq .

Resources con include=payload:

# 9001 — incluir payload y limitar resultados
curl -s -H 'Authorization: Bearer labtoken' \
  'http://127.0.0.1:9001/resources?include=payload&limit=5' | jq .

# 9003 — incluir payload
curl -s -H 'Authorization: Bearer labtoken' \
  'http://127.0.0.1:9003/resources?include=payload' | jq .

Opción rápida alternativa (dos terminales)

  • Terminal 1 (embeddings con configuración fija): npm run stub:normal
  • Terminal 2 (cliente con configuración fija): npm run start:normal

Probar

# sync
curl -s -XPOST http://127.0.0.1:8888/sync -H 'content-type: application/json' -d '{"serverUrl":"http://127.0.0.1:8001"}' | jq .
# search
curl -s -XPOST http://127.0.0.1:8888/search -H 'content-type: application/json' -d '{"query":"demo","allowedTypes":["tool","prompt","workflow"],"k":5}' | jq .
# descriptor desde DB
curl -s http://127.0.0.1:8888/resources/demo_tool | jq .

API Endpoints (Quick Reference)

Método Ruta Descripción Body (ejemplo) Response (shape)
GET /status Salud del cliente y metadatos { ok, dbPath, dims, uptimeSeconds }
POST /sync Sincroniza catálogo desde un MCP { "serverUrl": "http://127.0.0.1:8001" } { embedded, updated, skipped, total, serverUrl }
POST /refresh Refresca catálogo y reindexa { "serverUrl": "http://127.0.0.1:8001" } { embedded, updated, skipped, total, serverUrl }
POST /search Búsqueda híbrida (KNN + FTS) { "query": "demo", "k": 5 } { items: [...] }
GET /resources/:id Descriptor ARDF por ID { payload_json: {...} }
GET /resources Listado paginado y filtrado ?limit=50&offset=0&domain=demo&tag=beta&sort=updatedAt&order=desc&include=payload { items, limit, offset, total }
POST /sync/export Exportar IDs por filtros { "domain": "demo" } { ids: [...] }

Artifacts CI

  • status.json (respuesta de /status)
  • search.json (resultado de /search)
  • lab.log (logs del modo laboratorio)

Para verlos: Actions → CI → última ejecución → Job lab-smoke → Artifacts.

Notas

  • fetchWithTimeout endurece llamadas al webhook (timeouts + retries 429/502/503/504).
  • Esquema FTS consolidado (resource_key, server_url, text).
  • Gating por visibility/scopes antes del ranking y filtros (domain, tag).
  • Preferir 127.0.0.1 en ejemplos de curl en lugar de localhost para evitar resoluciones mixtas IPv6/IPv4 y asegurar consistencia entre shells/entornos.

Tras el merge a main puedes retirar el badge de hardening/new-round si lo prefieres; el badge de main reflejará el estado del pipeline principal.

Release notes — v0.4.0 (2025-10-10)

  • Nuevo POST /refresh con instrumentación de métricas (sync_requests_total, sync_latency_seconds, label route="refresh").
  • /resources/:id devuelve el descriptor ARDF completo (payload_json).
  • Puerto unificado 8888; fetchWithTimeout con reintentos (429/502/503/504); gating por visibility/scopes.
  • Búsqueda híbrida (KNN + BM25) con pesos configurables; FTS consolidado.
  • CI (lab-smoke) publica artefactos; nuevo job refresh-check valida /refresh y métricas.
  • Documentación y topics actualizados; sección de troubleshooting para npm/registro.

GET /resources — filtros y ordenamiento

  • Paginación

    • limit: entero 1–200 (por defecto 20; máximo 200)
    • offset: entero ≥ 0 (por defecto 0)
    • include: si es "payload" devuelve el descriptor completo; de lo contrario sólo { resource_id }
  • Filtros

    • type: coincide exactamente con resource_type | type | kind en el descriptor
    • domain: coincide con domain | namespace
    • tag: coincide si tags incluye el valor o si tag == valor
    • tier: coincide exactamente con tier
    • neededScope: requiere que visibility.scopes o scopes contenga el valor
    • serverUrl: coincide con server_url | serverUrl
  • Ordenamiento

    • sort: resource_id | title | updatedAt/updated_at/updatedat | createdAt/created_at/createdat | serverUrl/server_url/serverurl
    • order: asc | desc
  • Notas

    • Cuando hay filtros, se obtiene un mayor número de filas y se pagina en memoria para no depender de columnas adicionales; límite interno hasta 5000
    • Se usa resources_index con fallback ardf_index
    • El payload proviene de payload_json; si include=payload se expande en cada item
  • Ejemplos

    • GET /resources?limit=50&offset=0&type=tool&tag=beta&sort=updatedAt&order=desc&include=payload
    • GET /resources?domain=demo&neededScope=mcp:read
    • GET /resources?serverUrl=http://127.0.0.1:8001&sort=title
  • Documentación detallada de la API: ver API.md

  • Ejemplos de éxito y errores (curl): ver API.md, secciones “Ejemplos de éxito (curl)” y “Ejemplos de errores (curl)”.

About

Cliente MCP (TypeScript + Express) que sincroniza catálogos ARDF, indexa en SQLite (sqlite-vec) y expone búsqueda híbrida (semántica+léxica) vía API REST ( /status, /sync, /refresh, /search, /resources/:id ).

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published