# LMU Harness for agents > Studio de Le Mans Ultimate para você e o seu agente de IA, feito com os logs reais do jogo: resultados (.xml), telemetria (.duckdb) e replay (.Vcr/.lmur). Mostra sessões, voltas, comparação de voltas, replay 3D da corrida (Racing Line) e relatório de coaching. Este servidor: https://lmu.ferlabs.tech ## Páginas - [Início](https://lmu.ferlabs.tech/): visão geral do app. - [Sessões](https://lmu.ferlabs.tech/sessoes): lista de sessões com filtros. - [Sessão](https://lmu.ferlabs.tech/sessions/): resultados, voltas, telemetria e coaching de uma sessão ( é o nome do .xml sem extensão). - [Racing Line](https://lmu.ferlabs.tech/sessions//race): replay 3D da corrida. - [Novidades](https://lmu.ferlabs.tech/novidades): o que mudou em cada versão. - [Importar](https://lmu.ferlabs.tech/import): enviar logs pelo navegador (arrastar arquivos) ou pelo terminal. ## API v1 (leitura, JSON) Descoberta: GET https://lmu.ferlabs.tech/api/v1 devolve a lista de endpoints. Contrato completo no repositório (apps/ios/API.md). - GET https://lmu.ferlabs.tech/api/v1/sessions: lista de sessões. - GET https://lmu.ferlabs.tech/api/v1/sessions/:id: detalhe da sessão (pilotos, resultado). - GET https://lmu.ferlabs.tech/api/v1/sessions/:id/laps?driver=: voltas de um piloto. - GET https://lmu.ferlabs.tech/api/v1/sessions/:id/telemetry-laps?driver=: voltas com telemetria. - GET https://lmu.ferlabs.tech/api/v1/sessions/:id/best-laps: melhor volta de cada piloto. - GET https://lmu.ferlabs.tech/api/v1/sessions/:id/track-laps: voltas da mesma pista em outras sessões. - GET https://lmu.ferlabs.tech/api/v1/sessions/:id/coach?driver=: relatório de coaching. - GET https://lmu.ferlabs.tech/api/v1/sessions/:id/track?driver=: geometria da pista. - GET|POST https://lmu.ferlabs.tech/api/v1/telemetry: telemetria de voltas em colunas (também de sessões só com .lmur, `source: lmur`). GET: `?ref=~~` (repita `ref` até 12 vezes; piloto codificado na URL; `max=` reduz amostras); POST: `{ refs: [{ sessionId, driver, lap }], max? }`. - GET|POST https://lmu.ferlabs.tech/api/v1/compare: comparação de voltas. - GET|POST https://lmu.ferlabs.tech/api/v1/saved e PATCH|DELETE https://lmu.ferlabs.tech/api/v1/saved/:id: voltas salvas. - GET https://lmu.ferlabs.tech/api/v1/race/:id?drivers=&hz=: linha do tempo da corrida (Racing Line). - GET https://lmu.ferlabs.tech/api/v1/race/:id/events?from=&to=&kinds=: eventos da corrida. - GET|PUT https://lmu.ferlabs.tech/api/v1/profile: perfil do piloto. - POST https://lmu.ferlabs.tech/api/v1/ingest: mesmo envio de /api/ingest, sem CORS de navegador. ## MCP Este app expõe um servidor MCP remoto e público em `https://lmu.ferlabs.tech/mcp` (Streamable HTTP, protocolo 2025-11-25, stateless, respostas JSON via POST). Ele é somente leitura e oferece 14 ferramentas (lista gerada de `tools/list`, veja abaixo). Comece por `list_sessions` para obter os ids. No Claude Code: `claude mcp add --transport http lmu https://lmu.ferlabs.tech/mcp`. Descoberta sem configuração: GET https://lmu.ferlabs.tech/.well-known/ai-catalog.json (AI Catalog) aponta para o cartão do servidor em https://lmu.ferlabs.tech/mcp/server-card (MCP Server Card); as páginas HTML trazem o cabeçalho `Link: ; rel="mcp"`. Clientes sem cabeçalho Origin são aceitos; navegadores, só a partir da própria origem. Em navegadores com WebMCP (`document.modelContext`), a página registra as mesmas ferramentas e mais as de navegação (open_session, open_race_replay, open_lap_comparison, filter_sessions_by_driver e, na página de uma sessão, get_current_session, focus_driver e compare_with_leader). ### Ferramentas do MCP - `list_sessions`: List all recorded LMU sessions (id, track, type, date). - `get_session`: Get full detail of one session (metadata, results, laps) by id from list_sessions. - `get_results`: Get classification of a session, optionally filtered by car class (e.g. - `get_laps`: Get lap times and sectors of a session, optionally for one driver (exact name as in results). - `get_telemetry_laps`: List lap numbers that have telemetry for a session. - `get_telemetry_lap`: Get telemetry samples (t, distM, speedKmh, throttle, brake, steer, gear, rpm) of one lap. - `compare_laps`: Compare two laps per 100 m segment: min speed delta, braking point delta, full-throttle point delta, cumulative time delta. - `list_track_laps`: List every lap WITH telemetry from all sessions on the same track as the given session, for all drivers (own .duckdb, the .Vcr replay or, without it, the .lmur replay lite), fastest first. - `get_track_surface`: Track geometry built from every recorded lap on the session's track (all sessions and drivers): centerline every ~2 m, left/right edges, width per point, corners (same 'Curva N' labels as compare_laps), officialCorners (official turn number/name with startM/endM/apexM, e.g. - `get_race_snapshot`: Race order at an instant: position, lap, distance on the lap, speed, pit flag and gap to the leader (seconds, or laps behind), from the race timeline (replay .Vcr; owner only via .duckdb). - `get_race_events`: Race event feed in session time (same clock as the XML `et`), sorted: flags (green at the start, white when time is up / last lap, chequered for the winner), each driver's chequered flag, overtakes, fastest laps, pit in/out, incidents, damage, penalties, track limits, retirements, chat. - `get_coach_report`: Build the coaching report for a session (pace, consistency, sector weaknesses, tips). - `get_profile`: Get the driver who owns the logs (playerName), where it came from (env, config, telemetry auto-detection or sessions), the human drivers seen in the logs and knownDrivers [{name, sessions, hasTelemetry}] (drivers with their own .duckdb first). - `list_saved_laps`: List saved laps (newest first), optionally for one track. ## Enviar sessões (para agentes) Para enviar logs a este servidor, leia GET https://lmu.ferlabs.tech/api/ingest: o JSON traz `auth` (login ligado ou exigido), `accepts` (extensões aceitas) e `howTo` (passo a passo e exemplos curl prontos). Resumo: Envie os logs do Le Mans Ultimate (resultados .xml, telemetria .duckdb e replay já processado .lmur) com POST em /api/ingest. Um arquivo por requisição é o jeito mais simples e confiável. Endpoint: POST https://lmu.ferlabs.tech/api/ingest ### Métodos - octet-stream (recomendado): POST com content-type application/octet-stream, o arquivo como corpo cru e o header x-file-name com o nome do arquivo (pode vir URL-encoded). Um arquivo por requisição. - multipart/form-data: POST com vários campos "files" (um por arquivo) e, opcional, o campo "owner". Bom para poucos arquivos pequenos (o corpo é lido na memória). Dentro de uma mesma requisição o .xml precisa vir antes do .lmur. ### Headers - content-type: application/octet-stream (ou multipart/form-data) - x-file-name: obrigatório no modo octet-stream; nome do arquivo com extensão - x-owner: opcional; quem está enviando (só metadado). Com login, vale o nome da conta - x-mtime: opcional; mtime do arquivo em ms (só serve para casar .Vcr, que você não deve enviar) - authorization: Bearer (obrigatório neste servidor) ### Aceitos - .xml: resultado da sessão (rFactor XML com ). A pasta no servidor sai do conteúdo (data + pista), não do caminho local - .duckdb: telemetria da sessão (mantenha o nome original do jogo, ex.: "_R_2026-07-19T10_06_31Z.duckdb"; a pasta sai do nome) - .lmur: replay lite já processado no seu computador (gerado do .Vcr por `lmu lite` ou pelo LMU Uploader). O nome do arquivo precisa ser ".lmur" ### Recusados - .Vcr bruto (50 a 275 MB, decodificado no servidor): não envie; gere o .lmur local (este servidor o recusa) - qualquer outra extensão (ex.: .json, .cache, .promo): volta em "skipped" com reason "unsupported" - XML que não é resultado do rFactor/LMU, ou .lmur sem sessão correspondente ou com formato inválido: "skipped" com reason "invalid" ### Casamento .xml e .lmur (e ordem) - O id da sessão é o nome do .xml sem a extensão (ex.: 2026_07_19_10_06_31-70R1.xml tem id 2026_07_19_10_06_31-70R1). - O .lmur deve se chamar exatamente ".lmur" (mesmo nome base do .xml, ex.: 2026_07_19_10_06_31-70R1.lmur). A pasta local não importa. - O .xml da sessão precisa já estar no servidor: envie todos os .xml primeiro e os .lmur depois. Um .lmur sem o .xml volta em skipped com reason "invalid". - .duckdb não depende de ordem: vai para a pasta da corrida pelo nome, e pilotos diferentes da mesma corrida se juntam na mesma sessão. ### Idempotência Reenviar é seguro. Arquivo com o mesmo nome, tamanho e sha256 de um que já está no servidor volta em skipped com reason "duplicate" e não é gravado de novo. Mesmo nome com conteúdo diferente é guardado como cópia (-1, -2). Um .lmur de uma sessão que já tem replay também vira "duplicate". ### Resposta JSON 200: { imported: [{ name, path, sessionId?, owner?, sha256, bytes }], skipped: [{ name, reason: "duplicate"|"unsupported"|"invalid", sessionId?, sha256?, bytes? }] }. Erros: 400 { error } (nenhum arquivo ou x-file-name ausente) e 401 { error } (login exigido e sem token válido). ### Login - Este servidor EXIGE login para enviar: sem "Authorization: Bearer " o POST volta 401. - 1) POST https://lmu.ferlabs.tech/api/auth/device/code com JSON {"client_id":"lmu-uploader"} devolve device_code, user_code, verification_uri_complete e interval (segundos). - 2) Imprima para uma pessoa o link https://lmu.ferlabs.tech/device?user_code= (o mesmo que verification_uri_complete): ela abre no navegador, entra na conta e aprova. - 3) A cada interval segundos, POST https://lmu.ferlabs.tech/api/auth/device/token com JSON {"grant_type":"urn:ietf:params:oauth:grant-type:device_code","device_code":"","client_id":"lmu-uploader"}. Enquanto o erro for authorization_pending, continue (slow_down: aumente o intervalo em 5 s). Ao aprovar vem access_token. - 4) Use o access_token em todos os envios: header "Authorization: Bearer " (nos exemplos, a variável LMU_TOKEN). O token vale 90 dias e renova com o uso; o script pronto está em curl.login. ### Exemplos Login por dispositivo (exporta LMU_TOKEN): ```bash LMU_URL=https://lmu.ferlabs.tech # 1) pede um código de dispositivo CODE=$(curl -sS -X POST "$LMU_URL/api/auth/device/code" \ -H "content-type: application/json" -d '{"client_id":"lmu-uploader"}') DEVICE_CODE=$(printf '%s' "$CODE" | sed -n 's/.*"device_code":"\([^"]*\)".*/\1/p') USER_CODE=$(printf '%s' "$CODE" | sed -n 's/.*"user_code":"\([^"]*\)".*/\1/p') # 2) uma pessoa abre o link, entra na conta e aprova echo "Abra e aprove: $LMU_URL/device?user_code=$USER_CODE" # 3) espera a aprovação (authorization_pending = ainda não aprovou) while :; do sleep 5 R=$(curl -sS -X POST "$LMU_URL/api/auth/device/token" -H "content-type: application/json" \ -d "{\"grant_type\":\"urn:ietf:params:oauth:grant-type:device_code\",\"device_code\":\"$DEVICE_CODE\",\"client_id\":\"lmu-uploader\"}") LMU_TOKEN=$(printf '%s' "$R" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p') [ -n "$LMU_TOKEN" ] && break case "$R" in *authorization_pending*|*slow_down*) ;; *) echo "$R"; break ;; esac done export LMU_TOKEN # use em "Authorization: Bearer $LMU_TOKEN" ``` Um arquivo: ```bash # LMU_TOKEN vem do login por dispositivo (exemplo "login"); sem ele o envio volta 401 curl -sS -X POST https://lmu.ferlabs.tech/api/ingest \ -H "content-type: application/octet-stream" \ -H "x-file-name: 2026_07_19_10_06_31-70R1.xml" \ -H "x-owner: meu-nome" \ -H "authorization: Bearer $LMU_TOKEN" \ --data-binary @2026_07_19_10_06_31-70R1.xml ``` Vários de uma vez (multipart): ```bash # LMU_TOKEN vem do login por dispositivo (exemplo "login"); sem ele o envio volta 401 curl -sS -X POST https://lmu.ferlabs.tech/api/ingest \ -F "owner=meu-nome" \ -F "files=@2026_07_19_10_06_31-70R1.xml" \ -F "files=@2026_07_19_10_06_31-70R1.lmur" \ -H "authorization: Bearer $LMU_TOKEN" ``` Pasta inteira de logs (xml, depois duckdb, depois lmur; ignora .Vcr, .cache e .promo): ```bash # LMU_TOKEN vem do login por dispositivo (exemplo "login"); sem ele o envio volta 401 LMU_LOGS=/data/logs # pasta com os logs do jogo LMU_URL=https://lmu.ferlabs.tech # a ordem importa: .xml primeiro (o .lmur só entra se o .xml da mesma sessão já estiver no servidor) for ext in xml duckdb lmur; do find "$LMU_LOGS" -type f -iname "*.$ext" -not -path "*/.cache/*" -not -path "*/.promo/*" -print0 | sort -z | while IFS= read -r -d '' f; do curl -sS -X POST "$LMU_URL/api/ingest" \ -H "content-type: application/octet-stream" \ -H "x-file-name: $(basename "$f")" \ -H "x-owner: meu-nome" \ -H "authorization: Bearer $LMU_TOKEN" \ --data-binary @"$f"; echo done done ```