Skip to main content

MCP server de legislacion laboral argentina para agentes de IA

Project description

ley-ar

Infraestructura open source de legislación laboral argentina para agentes de IA.

npm version license: MIT MCP compatible


Quick Start

Instalá y conectá con Claude Desktop en 30 segundos:

npx ley-ar
// ~/Library/Application Support/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "ley-ar": {
      "command": "npx",
      "args": ["ley-ar"]
    }
  }
}

Reiniciá Claude Desktop. Ahora podés preguntar:

Vos:    "Me despidieron sin causa después de 3 años. Cobraba $1.200.000
         brutos. ¿Cuánto me corresponde?"

Claude: Usando ley-ar, calculé tu indemnización:

        Indemnización por antigüedad (Art. 245 LCT):  $3.600.000
        Preaviso - 2 meses (Arts. 231-232 LCT):       $2.400.000
        Integración mes de despido (Art. 233 LCT):       $840.000
        SAC proporcional (Art. 123 LCT):                 $198.333
        Vacaciones proporcionales (Art. 156 LCT):        $276.164
        SAC sobre preaviso (Art. 121 LCT):               $200.000
        ─────────────────────────────────────────────────────────
        TOTAL:                                         $7.514.497

        ⚠️ No se aplicó tope del CCT — verificar manualmente.
        ⚠️ No incluye intereses — aplicar tasa activa BNA desde la fecha de egreso.

Cada monto es determinístico. No hay alucinación. Las fórmulas son las de la LCT.


¿Qué es esto?

ley-ar es un servidor MCP (Model Context Protocol) que expone legislación laboral argentina como herramientas que cualquier LLM puede usar directamente.

Cuando conectás ley-ar a un agente de IA, el modelo puede:

  • Buscar artículos de la LCT, LRT y leyes complementarias por tema en lenguaje natural
  • Consultar jurisprudencia laboral relevante a un caso específico
  • Calcular con precisión determinística todos los rubros indemnizatorios de un despido
  • Verificar si una acción laboral está prescripta
  • Recuperar el texto exacto de cualquier artículo

No existe infraestructura equivalente para legislación latinoamericana. Este proyecto es el primero.


El problema que resuelve

Los LLMs tienen conocimiento general sobre derecho laboral argentino, pero ese conocimiento es:

  • Impreciso en los montos: las fórmulas del art. 245 LCT requieren cálculo exacto, no estimación
  • No actualizado: la legislación cambia (el art. 275 LCT fue sustituido por el Decreto 70/2023)
  • No verificable: cuando un modelo cita un artículo, no hay forma de saber si es correcto

ley-ar resuelve esto separando responsabilidades:

LLM               →  entiende el caso, conversa, redacta
ley-ar (MCP)      →  busca artículos reales, calcula montos, verifica plazos

El LLM nunca inventa datos legales. Los datos legales vienen de una fuente determinística y verificable.


Tools disponibles

Tool Qué hace Usa IA Determinístico
buscar_articulos Búsqueda híbrida en legislación laboral Sí (embeddings locales) Parcial
jurisprudencia Búsqueda de fallos por descriptores del caso No
calcular_indemnizacion Calcula todos los rubros de un despido No
verificar_prescripcion Verifica si una acción laboral está prescripta No
norma_vigente Recupera el texto exacto de un artículo No

4 de 5 tools son 100% locales, sin API externa, sin modelo de IA. buscar_articulos usa embeddings pre-generados localmente — el único costo de IA es el embedding de la query entrante.


buscar_articulos

Búsqueda híbrida que combina keyword matching (descriptores SAIJ con stemming en español) y búsqueda semántica (embeddings con modelo legal fine-tuned). Cada método cubre las debilidades del otro.

Input:

{
  tema: string,           // "despido sin causa", "embarazo", etc.
  ley?: "LCT" | "LRT",   // opcional — filtra por ley específica
  max_resultados?: number // default: 5
}

Output:

{
  articulos: [
    {
      ley: "Ley 20.744 - Ley de Contrato de Trabajo",
      articulo: "245",
      titulo: "Indemnización por antigüedad o despido",
      texto: "En los casos de despido dispuesto por el empleador...",
      capitulo: "Título XII: De la extinción del contrato de trabajo",
      relevancia: 0.91
    }
  ],
  total_encontrados: 3
}

Cómo funciona internamente — la búsqueda tiene dos capas que corren en paralelo:

Query del usuario
       │
       ├──────────────────────────────┐
       ▼                              ▼
┌──────────────┐              ┌────────────────┐
│   KEYWORD    │              │   SEMÁNTICO    │
│              │              │                │
│ Tokeniza     │              │ Embedding con  │
│ + stemming   │              │ bge-m3-es-     │
│ español      │              │ legal          │
│ + match vs   │              │ + FAISS        │
│ descriptores │              │ similitud      │
│ SAIJ +       │              │ coseno vs      │
│ sinónimos    │              │ 5,490 vectores │
└──────┬───────┘              └───────┬────────┘
       │                              │
       │  {descriptor: score}         │  {descriptor: score}
       │                              │
       └──────────┬───────────────────┘
                  ▼
         ┌───────────────┐
         │    MERGE      │
         │               │
         │ Por cada       │
         │ descriptor:   │
         │ max(kw, sem)  │
         └───────┬───────┘
                 ▼
         ┌───────────────┐
         │  PONDERACIÓN  │
         │  POR RAMA     │
         │               │
         │ Cada rama     │
         │ SAIJ tiene    │
         │ su propia     │
         │ ancla (no     │
         │ global)       │
         └───────┬───────┘
                 ▼
         Top N artículos
         con texto completo

¿Por qué híbrido y no solo uno?

El keyword matching es preciso cuando hay coincidencia exacta de frases pero no entiende semántica. El semántico conecta conceptos sin match exacto pero con queries cortas produce resultados absurdos. La combinación de ambos resuelve los dos problemas.

Detalle de la ponderación por rama: antes de la versión híbrida, el sistema tomaba el descriptor con mayor score como "ancla" global y penalizaba todo lo que no estuviera en su misma rama jerárquica SAIJ. Eso provocaba que queries multi-concepto como "despido durante embarazo" perdieran los artículos de embarazo (rama distinta a la de despido). Ahora agrupamos por rama y cada rama tiene su propia ancla, lo que permite capturar artículos de múltiples áreas del derecho laboral.

Resultados medidos en 10 casos de prueba:

Caso Keyword solo Semántico solo Híbrido
"me despidieron estando embarazada" ✅ LCT_178 top ✅ LCT_178 top ✅ LCT_178 top
"despido sin causa durante embarazo" ❌ pierde embarazo ✅ LCT_178 top ✅ LCT_245 + LCT_178 + LCT_182
"despido sin causa" ✅ LCT_245 top ❌ "mensajería en moto" ✅ LCT_245 top
"trabajo en negro" ✅ LdE_8 top ✅ LdE_8 top ✅ LdE_8 top
"horas extras" ✅ LCT_201 top ✅ LCT_201 top ✅ LCT_201 top
"accidente de trabajo" ❌ solo "trabajo" genérico ❌ "mensajería en moto" ✅ LRT_39, LRT_46, LRT_6
"sindicato" ⚠️ solo reincorporación ❌ "despido por riña" ⚠️ agrega discriminatorio + afiliado
"licencia por enfermedad" ⚠️ 1 descriptor ❌ "estatuto bancario" ✅ despido + licencia combinados
"aguinaldo" ✅ SAC top ❌ "falta de aumento" ✅ SAC top
"acoso laboral" ✅ mobbing top ❌ "bonificación emergencia" ✅ mobbing top

Resumen: keyword solo 6/10 — semántico solo 4/10 — híbrido 9/10.

El caso que motivó todo el rediseño ("despido sin causa durante embarazo") ahora devuelve los 3 artículos clave: LCT_245 (indemnización por despido), LCT_178 (presunción por embarazo), LCT_182 (indemnización agravada).


jurisprudencia

Busca fallos por overlap de descriptores SAIJ extraídos del caso. 100% local.

Input:

{
  caso: string,              // descripción del caso en lenguaje natural
  jurisdiccion?: string,     // "CABA" | "PBA" | "nacional"
  max_resultados?: number    // default: 3
}

Output:

{
  fallos: [
    {
      tribunal: "Cámara Nacional de Apelaciones del Trabajo, Sala VII",
      caratula: "González, María c/ Transportes SA s/ despido",
      fecha: "2023-05-15",
      descriptores: ["DESPIDO INJUSTIFICADO"],
      sintesis: "Se confirmó la condena al pago de indemnización...",
      relevancia: 0.87
    }
  ]
}

calcular_indemnizacion

100% determinístico. Calcula los rubros de un despido aplicando las fórmulas exactas de la LCT. No usa IA. Cada monto es trazable a un artículo y una fórmula.

Input:

{
  fecha_ingreso: string,                          // "2020-03-15"
  fecha_egreso: string,                           // "2025-01-20"
  mejor_remuneracion: number,                     // remuneración mensual bruta
  causa: "sin_causa" | "con_causa" | "indirecto",
  registrado: boolean,
  preaviso_otorgado?: boolean
}

Output:

{
  rubros: {
    indemnizacion_antiguedad: {
      monto: 5000000,
      calculo: "5 períodos × $1.000.000",
      fundamento: "Art. 245 LCT"
    },
    preaviso: {
      monto: 2000000,
      calculo: "2 meses × $1.000.000 (antigüedad ≥ 5 años)",
      fundamento: "Arts. 231-232 LCT"
    },
    integracion_mes: {
      monto: 400000,
      calculo: "12 días × ($1.000.000 / 30)",
      fundamento: "Art. 233 LCT"
    },
    sac_proporcional: { ... },
    vacaciones_proporcionales: { ... },
    sac_sobre_preaviso: { ... }
  },
  total: 8122223,
  total_formateado: "$8.122.223",
  antiguedad: {
    años: 4,
    meses_restantes: 10,
    periodos_indemnizatorios: 5
  },
  advertencias: [
    "No se aplicó tope del CCT — verificar manualmente",
    "No incluye intereses — aplicar tasa activa BNA desde fecha de egreso"
  ]
}

Fórmulas implementadas:

Rubro Fórmula Artículo
Indemnización por antigüedad mejor_rem × períodos (fracción > 3 meses = 1 período) Art. 245 LCT
Preaviso 15 días (<3 meses), 1 mes (<5 años), 2 meses (≥5 años) Arts. 231-232 LCT
Integración mes (mejor_rem / 30) × días_restantes_del_mes Art. 233 LCT
SAC proporcional (mejor_rem / 2) × (días_semestre / 180) Art. 123 LCT
Vacaciones proporcionales según antigüedad: 14/21/28/35 días Art. 156 LCT
SAC sobre preaviso preaviso / 12 Art. 121 LCT

verificar_prescripcion

Input:

{
  tipo_reclamo: "despido" | "diferencias_salariales" | "accidente" | "multas_registro",
  fecha_hecho: string,     // "2023-06-15"
  fecha_consulta?: string  // default: hoy
}

Output:

{
  prescripto: false,
  plazo_total: "2 años",
  fecha_limite: "2025-06-15",
  dias_restantes: 120,
  fundamento: "Art. 256 LCT",
  advertencia: "⚠️ Quedan menos de 6 meses. Considerar iniciar acción judicial."
}

Plazos implementados:

Tipo de reclamo Plazo Artículo Nota
Despido 2 años desde extinción del vínculo Art. 256 LCT
Diferencias salariales 2 años desde que el crédito es exigible Art. 256 LCT
Accidente / enfermedad profesional 2 años desde conocimiento de la incapacidad Art. 44 LRT Plazo con matices jurisprudenciales; el cómputo del inicio puede variar según el caso. Verificar con abogado.
Multas por no registro 2 años desde extinción del vínculo Art. 256 LCT

norma_vigente

Lookup directo al texto de un artículo. Sin procesamiento, sin IA.

Input:

{
  ley: "LCT" | "LRT" | "24013" | "25323",
  articulo: string   // "245", "231", etc.
}

Output:

{
  ley: "Ley 20.744 - Ley de Contrato de Trabajo",
  articulo: "245",
  titulo: "Indemnización por antigüedad o despido",
  texto: "En los casos de despido dispuesto por el empleador sin justa causa...",
  capitulo: "Título XII: De la extinción del contrato de trabajo",
  vigente: true,
  ultima_modificacion: "Decreto 70/2023 B.O. 21/12/2023"
}

Arquitectura interna

Estructura del repositorio

ley-ar/
├── src/
│   ├── index.ts                   # Entry point — inicia el servidor MCP
│   ├── server.ts                  # Registro de las 5 tools con Zod schemas
│   ├── tools/
│   │   ├── buscar-articulos.ts    # Búsqueda híbrida (keyword + semántica)
│   │   ├── jurisprudencia.ts      # Búsqueda por descriptores SAIJ
│   │   ├── calcular-indem.ts      # Fórmulas LCT — 100% determinístico
│   │   ├── verificar-prescrip.ts  # Lógica de plazos por tipo de reclamo
│   │   └── norma-vigente.ts       # Lookup directo por ley + artículo
│   ├── services/
│   │   ├── hybrid-retriever.ts    # Orquesta keyword + semántico + merge
│   │   ├── keyword-matcher.ts     # Stemming español + match vs descriptores
│   │   ├── semantic-search.ts     # FAISS + embeddings bge-m3-es-legal
│   │   ├── legislation-store.ts   # Acceso a los JSONs de artículos
│   │   └── juris-search.ts        # Búsqueda en JSONL de jurisprudencia
│   └── data/
│       ├── legislacion/           # JSONs de leyes
│       ├── jurisprudencia/        # JSONL con fallos indexados por descriptores
│       ├── descriptores/          # Índice SAIJ (955 descriptores, 5,490 sinónimos)
│       └── embeddings/            # Vectores pre-generados (FAISS index)
├── scripts/
│   └── generate-embeddings.ts     # Genera vectores offline (corre una vez)
├── package.json
└── tsconfig.json

Flujo de datos en runtime

Cuando un cliente MCP llama una tool, esto es lo que pasa:

Cliente MCP (Claude Desktop, tu app, etc.)
       │
       │  tool_call: "calcular_indemnizacion"
       │  args: { fecha_ingreso: "2020-03-15", ... }
       ▼
┌──────────────────────────────────────────┐
│  index.ts → server.ts                    │
│  Routing: identifica la tool y valida    │
│  el input contra el Zod schema          │
└──────────────┬───────────────────────────┘
               ▼
┌──────────────────────────────────────────┐
│  tools/calcular-indem.ts                 │
│  Tool handler: ejecuta la lógica         │
│  específica de la tool                   │
│                                          │
│  Para calcular_indemnizacion:            │
│  → Aplica fórmulas del art. 245,        │
│    231, 232, 233, 123, 156 LCT          │
│  → Pura aritmética, cero IA             │
│                                          │
│  Para buscar_articulos:                  │
│  → Llama a hybrid-retriever.ts          │
│  → Keyword + semántico en paralelo      │
│  → Merge + ponderación por rama         │
│  → Enriquece con texto desde            │
│    legislation-store.ts                  │
└──────────────┬───────────────────────────┘
               ▼
┌──────────────────────────────────────────┐
│  services/ (capa de datos)               │
│                                          │
│  legislation-store.ts                    │
│    └── Lee JSONs de data/legislacion/    │
│                                          │
│  juris-search.ts                         │
│    └── Lee JSONL de data/jurisprudencia/ │
│                                          │
│  Datos cargados en memoria al iniciar    │
│  el servidor. Sin DB, sin filesystem     │
│  reads en cada request.                  │
└──────────────┬───────────────────────────┘
               ▼
       Respuesta JSON estructurada
       devuelta al cliente MCP

El sistema de búsqueda híbrido

El componente más complejo del servidor. Explicado en detalle:

Capa 1 — Keyword matching: Tokeniza el input, aplica stemming en español, y busca coincidencias contra 955 descriptores SAIJ y sus 5,490 sinónimos. Preciso con frases exactas ("despido sin causa" → match directo), pero pierde conceptos cuando las palabras no coinciden literalmente.

Capa 2 — Búsqueda semántica: Usa el modelo dariolopez/bge-m3-es-legal-tmp-6, un modelo de embeddings fine-tuned en texto legal español. Los 5,490 textos de descriptores + sinónimos están pre-vectorizados en un índice FAISS. En cada query, solo se genera el embedding de la query entrante y se buscan los 15 vectores más cercanos por similitud coseno. Conecta conceptos sin match exacto ("accidente laboral" ≈ "riesgo de trabajo"), pero produce ruido con queries cortas.

Capa 3 — Merge y ponderación: Combina ambos resultados tomando el score máximo por descriptor. Después aplica ponderación jerárquica por rama (no global), lo que permite que queries multi-concepto como "despido durante embarazo" devuelvan artículos de ambas ramas sin que una aplaste a la otra.


Datos incluidos

Ley Nombre Cobertura
Ley 20.744 Ley de Contrato de Trabajo (LCT) Artículos completos
Ley 24.557 Ley de Riesgos del Trabajo (LRT) Artículos completos
Ley 24.013 Ley Nacional de Empleo Arts. 8, 9, 10 (multas por no registro)
Ley 25.323 Incremento indemnizatorio Arts. 1, 2
Ley 11.544 Ley de Jornada de Trabajo Artículos selectos

Además: índice de 955 descriptores SAIJ con 5,490 sinónimos, y corpus de jurisprudencia laboral en formato JSONL con fallos taggeados por descriptores (prioridad: CABA y Provincia de Buenos Aires).


Instalación y uso

Con Claude Desktop

{
  "mcpServers": {
    "ley-ar": {
      "command": "npx",
      "args": ["ley-ar"]
    }
  }
}

Como cliente MCP programático

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

const transport = new StdioClientTransport({
  command: "npx",
  args: ["ley-ar"]
});

const client = new Client({ name: "mi-app", version: "1.0.0" });
await client.connect(transport);

// Ejemplo: calcular indemnización
const resultado = await client.callTool("calcular_indemnizacion", {
  fecha_ingreso: "2020-03-15",
  fecha_egreso: "2025-01-20",
  mejor_remuneracion: 1200000,
  causa: "sin_causa",
  registrado: true
});

Desarrollo local

git clone https://github.com/tuusuario/ley-ar
cd ley-ar
npm install

# Generar embeddings (solo la primera vez, requiere el modelo descargado)
npm run generate-embeddings

# Iniciar en modo desarrollo
npm run dev

# Build
npm run build

Agregar nueva legislación

  1. Agregá el JSON de la ley en src/data/legislacion/:
{
  "content": "texto del artículo",
  "metadata": {
    "article": "número",
    "chapter": "capítulo",
    "tags": ["descriptor1", "descriptor2"],
    "code": "nombre de la ley"
  }
}
  1. Regenerá los embeddings:
npm run generate-embeddings

Limitaciones conocidas

  • Tope CCT (art. 245): No se calcula el tope indemnizatorio del convenio colectivo aplicable. El output incluye una advertencia explícita.
  • Intereses: No se calculan intereses. El abogado debe aplicar la tasa activa del BNA desde la fecha de egreso.
  • Cobertura geográfica: La jurisprudencia incluida prioriza CABA y Provincia de Buenos Aires.
  • Actualización de leyes: El corpus refleja la legislación vigente al momento de la última actualización del paquete. Verificar artículos modificados por decreto.
  • Búsqueda semántica con queries cortas: Con inputs de 1-2 palabras genéricas, la capa semántica puede producir ruido. La capa keyword compensa en esos casos.

Roadmap

  • v1.0 — Las 5 tools con LCT, LRT y leyes complementarias
  • v1.1 — Cálculo de tope CCT por rama de actividad (convenios colectivos)
  • v1.2 — Ampliación de corpus de jurisprudencia a otras jurisdicciones provinciales
  • v1.3 — Soporte para despido con causa (validación de causales del art. 242 LCT)

Proyectos que usan ley-ar

demanda-lab — Generador de borradores de demandas laborales. El primer producto construido sobre esta infraestructura.


Licencia

MIT — Libre para uso comercial y no comercial.


Autor


¿Lo estás usando en producción? Abrí un issue o mandá un PR para aparecer en esta lista.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ley_ar-0.1.0.tar.gz (16.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ley_ar-0.1.0-py3-none-any.whl (235.8 kB view details)

Uploaded Python 3

File details

Details for the file ley_ar-0.1.0.tar.gz.

File metadata

  • Download URL: ley_ar-0.1.0.tar.gz
  • Upload date:
  • Size: 16.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.14

File hashes

Hashes for ley_ar-0.1.0.tar.gz
Algorithm Hash digest
SHA256 2e0bb6a409719e559278447677f4795442d5152df0259e73578ee3f6faf4281e
MD5 b4e2b3a36e4911d7eee70b88cc86b488
BLAKE2b-256 c388a539dd74f6462d36d735f1bf47913ca06f7eed97bab2a049ab43fa34e7aa

See more details on using hashes here.

File details

Details for the file ley_ar-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: ley_ar-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 235.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.14

File hashes

Hashes for ley_ar-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9cf6b6054467ea0bfddf3071d2c54f908c31ee1e89c6e63ede3dc391e5ae9f72
MD5 62f8f10f395f65c7587187f413e97bf1
BLAKE2b-256 ef9a3866a4ef18465402fc95c35e04dbdbc9877087f25c3a9b195243f3fe2798

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page