france-data-mcp 0.10.5 → 0.10.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -79,16 +79,16 @@ Brings together the most useful French government data sources under a uniform t
79
79
 
80
80
  ## Status
81
81
 
82
- ✅ **v0.10.5 — in production.** 35 tools, 4 ingested health sources (FINESS, Ameli, RPPS, CDS) + live INSEE / DINUM / IGN. 933 tests, TypeScript strict. Listed on the [official MCP Registry](https://registry.modelcontextprotocol.io/v0.1/servers?search=france-data-mcp). Observability: Sentry + Axiom + `/healthz`.
82
+ ✅ **v0.10.6 — in production.** 35 tools, 4 ingested health sources (FINESS, Ameli, RPPS, CDS) + live INSEE / DINUM / IGN. 954 tests, TypeScript strict. Listed on the [official MCP Registry](https://registry.modelcontextprotocol.io/v0.1/servers?search=france-data-mcp). Observability: Sentry + Axiom + `/healthz`.
83
83
 
84
84
  Full history: [CHANGELOG](CHANGELOG.md).
85
85
 
86
86
  ### Roadmap
87
87
 
88
- > Shipped versions in the [CHANGELOG](CHANGELOG.md) (quality audits B1→B10 then P1→P4 handled through v0.10.5).
88
+ > Shipped versions in the [CHANGELOG](CHANGELOG.md) (quality audits B1→B10 then P1→P4 handled, Robustness backlog closed in v0.10.6).
89
89
 
90
- - **Robustness** — drop redundant `rpps_insee_idx`, validate `savoir_faire_code` at boundary, validate IGN coords
91
- - **v1.0+** — DOM-COM support (`code_insee CHAR(5)`), INSEE IRIS (infra-communal demographics)
90
+ - [x] **Robustness** — drop redundant `rpps_insee_idx`, validate ANS nomenclature at boundary, validate IGN coords _(shipped v0.10.6)_
91
+ - [ ] **v1.0+** — DOM-COM support (`code_insee CHAR(5)`), INSEE IRIS (infra-communal demographics)
92
92
 
93
93
  ---
94
94
 
package/README.md CHANGED
@@ -112,15 +112,15 @@ Usage intensif : throttler côté client ou self-héberger.
112
112
 
113
113
  ## État du projet
114
114
 
115
- ✅ **V0.10.5 — en production.** 35 tools, 4 sources santé ingérées (FINESS, Ameli, RPPS, CDS) + INSEE / DINUM / IGN en live. 933 tests, TypeScript strict. Sur le [registry MCP officiel](https://registry.modelcontextprotocol.io/v0.1/servers?search=france-data-mcp). Observabilité Sentry + Axiom + `/healthz`.
115
+ ✅ **V0.10.6 — en production.** 35 tools, 4 sources santé ingérées (FINESS, Ameli, RPPS, CDS) + INSEE / DINUM / IGN en live. 954 tests, TypeScript strict. Sur le [registry MCP officiel](https://registry.modelcontextprotocol.io/v0.1/servers?search=france-data-mcp). Observabilité Sentry + Axiom + `/healthz`.
116
116
 
117
117
  Historique complet : [CHANGELOG](CHANGELOG.md).
118
118
 
119
119
  ### Roadmap
120
120
 
121
- > Historique des versions livrées dans le [CHANGELOG](CHANGELOG.md) (audits qualité B1→B10 puis P1→P4 traités jusqu'à V0.10.5).
121
+ > Historique des versions livrées dans le [CHANGELOG](CHANGELOG.md) (audits qualité B1→B10 puis P1→P4 traités, backlog Robustesse clos en V0.10.6).
122
122
 
123
- - [ ] **Robustesse** — DROP `rpps_insee_idx` redondant, validation `savoir_faire_code` au boundary, validation coords IGN
123
+ - [x] **Robustesse** — DROP `rpps_insee_idx` redondant, validation nomenclature ANS au boundary, validation coords IGN _(livré V0.10.6)_
124
124
  - [ ] **V1.0+** — DOM-COM (`code_insee CHAR(5)`), INSEE IRIS (démographie infra-communale)
125
125
 
126
126
  ---
package/dist/cli.js CHANGED
@@ -5,7 +5,7 @@ import { createInterface } from 'readline';
5
5
  import { fileURLToPath } from 'url';
6
6
 
7
7
  // src/core/version.ts
8
- var VERSION = "0.10.5";
8
+ var VERSION = "0.10.7";
9
9
 
10
10
  // bin/cli.ts
11
11
  var DEFAULT_ENDPOINT = "https://france-data-mcp.vercel.app/mcp";
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/core/version.ts","../bin/cli.ts"],"names":[],"mappings":"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","file":"cli.js","sourcesContent":["/**\n * Version courante du serveur MCP + wrapper npm. Source de vérité partagée :\n * - `api/mcp.ts` → expose via `initialize.serverInfo.version` au client MCP\n * - `bin/cli.ts` → expose dans le User-Agent HTTP + le banner stderr\n *\n * Synchronisée manuellement avec `package.json.version` à chaque release.\n * Une déclaration en TS pur évite la friction des import attributes JSON\n * (instables entre tsup/esbuild/@vercel/node).\n */\nexport const VERSION = \"0.10.5\";\n","#!/usr/bin/env node\n/**\n * france-data-mcp — wrapper npm.\n *\n * Forwarde le protocole MCP stdio (NDJSON sur stdin/stdout) vers l'endpoint\n * HTTP `france-data-mcp.vercel.app/mcp`. Permet aux clients MCP qui ne savent\n * pas appeler un endpoint HTTP distant (Claude Desktop natif, certains IDE)\n * d'utiliser le serveur via `npx france-data-mcp`.\n *\n * Architecture :\n * - Lit stdin ligne par ligne (NDJSON, spec MCP stdio transport). Trim le\n * whitespace périphérique avant forward — transformation volontaire et\n * inoffensive (n'altère pas le JSON-RPC payload).\n * - Pour chaque ligne non vide, POST vers `ENDPOINT` et écrit la réponse\n * sur stdout (NDJSON).\n * - En cas d'erreur réseau, HTTP >= 400, ou body stream interrompu, émet\n * une réponse JSON-RPC error (-32603) pour ne JAMAIS faire hang le client.\n *\n * stdout doit rester pur JSON-RPC (NDJSON) — tout log interne va sur stderr\n * via `console.error` (jamais `stdout.write` pour autre chose qu'une réponse\n * JSON-RPC). Pas d'état stateful : le serveur HTTP est stateless lui aussi.\n */\n\nimport { realpathSync } from \"node:fs\";\nimport { stdin, stdout } from \"node:process\";\nimport { createInterface } from \"node:readline\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { VERSION } from \"../src/core/version.js\";\n\nconst DEFAULT_ENDPOINT = \"https://france-data-mcp.vercel.app/mcp\";\nconst ENDPOINT = process.env.FRANCE_DATA_MCP_URL || DEFAULT_ENDPOINT;\nconst USER_AGENT = `france-data-mcp-npm/${VERSION}`;\nconst JSON_RPC_INTERNAL_ERROR = -32603;\n\nconst rawTimeoutEnv = process.env.FRANCE_DATA_MCP_TIMEOUT_MS;\nconst parsedTimeout = Number(rawTimeoutEnv);\nconst isValidTimeout = Number.isFinite(parsedTimeout) && parsedTimeout > 0;\nconst REQUEST_TIMEOUT_MS = isValidTimeout ? parsedTimeout : 60_000;\n// Signaler le fallback côté stderr (jamais stdout — réservé au JSON-RPC) pour\n// éviter un silent failure si l'utilisateur a tapé une valeur invalide.\nif (rawTimeoutEnv !== undefined && !isValidTimeout) {\n console.error(\n `[france-data-mcp-npm] FRANCE_DATA_MCP_TIMEOUT_MS=\"${rawTimeoutEnv}\" invalide, fallback ${REQUEST_TIMEOUT_MS}ms`,\n );\n}\n\ntype JsonRpcId = string | number | null;\ntype JsonRpcMessage = { id?: JsonRpcId; method?: string };\n\n/**\n * Extrait l'id JSON-RPC d'une ligne (best-effort). Utilisé uniquement pour\n * construire une réponse error propre quand le forward réseau échoue.\n */\nfunction parseId(line: string): JsonRpcId {\n try {\n const msg = JSON.parse(line) as unknown;\n if (msg && typeof msg === \"object\" && \"id\" in msg) {\n const id = (msg as JsonRpcMessage).id;\n if (typeof id === \"string\" || typeof id === \"number\" || id === null) return id;\n }\n } catch {\n // Best-effort : si la ligne n'est pas du JSON valide, forwardLine émettra\n // quand même une réponse JSON-RPC error sur stdout avec id=null. Le\n // diagnostic texte va sur stderr via console.error.\n }\n return null;\n}\n\nconst defaultWriteOut = (s: string): void => {\n stdout.write(s);\n};\n\n/**\n * Émet une réponse JSON-RPC error sur stdout. NDJSON appliqué de façon\n * uniforme (1 message = 1 ligne).\n */\nfunction emitJsonRpcError(\n id: JsonRpcId,\n code: number,\n message: string,\n writeOut: (s: string) => void,\n): void {\n const payload = { jsonrpc: \"2.0\", id, error: { code, message } };\n writeOut(`${JSON.stringify(payload)}\\n`);\n}\n\n/**\n * POST une ligne JSON-RPC vers l'endpoint HTTP et écrit la réponse sur stdout.\n * Catche toutes les erreurs (réseau, timeout, HTTP >=400, stream interrompu)\n * en émettant une réponse JSON-RPC error — le client MCP voit toujours une\n * réponse pour chaque request, jamais de hang.\n */\nexport async function forwardLine(\n line: string,\n fetchFn: typeof fetch = fetch,\n writeOut: (s: string) => void = defaultWriteOut,\n): Promise<void> {\n const trimmed = line.trim();\n if (!trimmed) return;\n\n // Parsing d'id fait UNE seule fois, réutilisé sur tous les chemins d'erreur.\n const id = parseId(trimmed);\n\n let response: Response;\n try {\n response = await fetchFn(ENDPOINT, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n \"user-agent\": USER_AGENT,\n accept: \"application/json\",\n },\n body: trimmed,\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n });\n } catch (err) {\n const reason = sanitizeReason(err instanceof Error ? err.message : String(err));\n console.error(`[france-data-mcp-npm] forward failed: ${reason}`);\n emitJsonRpcError(\n id,\n JSON_RPC_INTERNAL_ERROR,\n `Network error forwarding to ${SAFE_ENDPOINT}: ${reason}`,\n writeOut,\n );\n return;\n }\n\n let text: string;\n try {\n text = await response.text();\n } catch (err) {\n // Stream interrompu après les headers (gateway timeout, réseau coupé) :\n // sans ce catch, la promise rejette → main() crash → client hang sur l'id.\n // console.error pour le diagnostic local, capture Sentry inapplicable\n // côté wrapper client (par design : pas de telemetry).\n const reason = sanitizeReason(err instanceof Error ? err.message : String(err));\n console.error(`[france-data-mcp-npm] body stream interrupted: ${reason}`);\n emitJsonRpcError(\n id,\n JSON_RPC_INTERNAL_ERROR,\n `Body stream interrupted from ${SAFE_ENDPOINT}: ${reason}`,\n writeOut,\n );\n return;\n }\n\n if (!response.ok) {\n emitJsonRpcError(\n id,\n JSON_RPC_INTERNAL_ERROR,\n `Upstream HTTP ${response.status} from ${SAFE_ENDPOINT}: ${sanitizeReason(text.slice(0, 200))}`,\n writeOut,\n );\n return;\n }\n\n // L'endpoint Vercel renvoie un seul objet JSON-RPC (status 200) ou rien\n // (204 pour les notifications). Passthrough verbatim — pas de re-sérialisation\n // pour préserver la précision (ordre des clés, formats numériques).\n if (text.length > 0) writeOut(`${text.replace(/\\n+$/u, \"\")}\\n`);\n}\n\n/**\n * Masque les credentials userinfo d'une URL connue (notre ENDPOINT) avant\n * inclusion dans un log stderr ou un message error JSON-RPC stdout.\n *\n * Surfaces couvertes :\n * 1. Banner stderr au démarrage (`SAFE_ENDPOINT`)\n * 2. Messages error JSON-RPC qui interpolent `${SAFE_ENDPOINT}`\n * 3. Messages error des exceptions `fetch` Node 22+ — leur `err.message`\n * embarque l'URL fautive ENTIÈRE incluant userinfo (\"Request cannot be\n * constructed from a URL that includes credentials: https://user:pass@…\").\n * Cette surface est couverte par `sanitizeReason()` complémentaire — ne PAS\n * se reposer sur cette fonction seule.\n */\nfunction safeEndpointForLog(endpoint: string): string {\n try {\n const u = new URL(endpoint);\n u.username = \"\";\n u.password = \"\";\n return u.toString();\n } catch {\n return endpoint;\n }\n}\n\n// Calculé une seule fois au module load — réutilisé dans tous les messages\n// (banner stderr + 3 chemins d'erreur stdout JSON-RPC).\nconst SAFE_ENDPOINT = safeEndpointForLog(ENDPOINT);\n\n/**\n * Strip toute URL `scheme://user:pass@host` d'une string libre. Le runtime\n * `fetch` Node 22+ throw un `TypeError` dont le message contient verbatim\n * l'URL fautive, incluant userinfo. Sans sanitization, le `reason` d'une\n * erreur réseau fuiterait les credentials côté stdout (JSON-RPC error) ET\n * stderr (diagnostic). Defense-in-depth obligatoire — la même URL pourrait\n * arriver via un message d'erreur de proxy, DNS, gateway, etc.\n */\nfunction sanitizeReason(reason: string): string {\n return reason.replace(/(https?:\\/\\/)[^/\\s@]+@/giu, \"$1[redacted]@\");\n}\n\nasync function main(): Promise<void> {\n console.error(`[france-data-mcp-npm] v${VERSION} → ${SAFE_ENDPOINT}`);\n const rl = createInterface({ input: stdin, crlfDelay: Number.POSITIVE_INFINITY });\n for await (const line of rl) {\n await forwardLine(line);\n }\n}\n\n/**\n * Détecte si le module est exécuté directement (vs importé par un test).\n * Évite de démarrer la boucle stdin pendant les tests.\n *\n * V0.7.6 fix : la garde précédente comparait `pathToFileURL(argv1).href` à\n * `import.meta.url`. Quand npm/npx exécutent le bin via un symlink dans\n * `node_modules/.bin/`, `process.argv[1]` reste le chemin du symlink mais\n * Node ESM résout `import.meta.url` vers la cible réelle. Les deux divergent\n * → `main()` jamais appelé → process exit silencieux. Régression silencieuse\n * en prod depuis V0.7.2 (1er wrapper npm). Fix : comparer les `realpath` des\n * deux côtés pour matcher quel que soit le routage symlink.\n */\nexport function isMainModule(importMetaUrl: string, argv1: string | undefined): boolean {\n if (typeof argv1 !== \"string\") return false;\n try {\n return realpathSync(fileURLToPath(importMetaUrl)) === realpathSync(argv1);\n } catch {\n // Catch volontairement silencieux (PAS un silent failure métier) :\n // c'est une décision booléenne sans effet utilisateur — on ne hand off\n // aucune donnée, on ne masque aucune erreur API. Si realpath ou\n // fileURLToPath throw (URL malformée, fichier inexistant — typique en\n // contexte test/import abstrait), la bonne réponse est \"ne PAS démarrer\n // main()\", pas \"logger une erreur\" qui polluerait stderr de tous les\n // tests qui importent le module.\n return false;\n }\n}\n\nif (isMainModule(import.meta.url, process.argv[1])) {\n main().catch((err: unknown) => {\n const reason = err instanceof Error ? err.message : String(err);\n console.error(`[france-data-mcp-npm] fatal: ${reason}`);\n process.exit(1);\n });\n}\n\nexport { ENDPOINT, USER_AGENT, parseId, safeEndpointForLog };\n"]}
1
+ {"version":3,"sources":["../src/core/version.ts","../bin/cli.ts"],"names":[],"mappings":"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","file":"cli.js","sourcesContent":["/**\n * Version courante du serveur MCP + wrapper npm. Source de vérité partagée :\n * - `api/mcp.ts` → expose via `initialize.serverInfo.version` au client MCP\n * - `bin/cli.ts` → expose dans le User-Agent HTTP + le banner stderr\n *\n * Synchronisée manuellement avec `package.json.version` à chaque release.\n * Une déclaration en TS pur évite la friction des import attributes JSON\n * (instables entre tsup/esbuild/@vercel/node).\n */\nexport const VERSION = \"0.10.7\";\n","#!/usr/bin/env node\n/**\n * france-data-mcp — wrapper npm.\n *\n * Forwarde le protocole MCP stdio (NDJSON sur stdin/stdout) vers l'endpoint\n * HTTP `france-data-mcp.vercel.app/mcp`. Permet aux clients MCP qui ne savent\n * pas appeler un endpoint HTTP distant (Claude Desktop natif, certains IDE)\n * d'utiliser le serveur via `npx france-data-mcp`.\n *\n * Architecture :\n * - Lit stdin ligne par ligne (NDJSON, spec MCP stdio transport). Trim le\n * whitespace périphérique avant forward — transformation volontaire et\n * inoffensive (n'altère pas le JSON-RPC payload).\n * - Pour chaque ligne non vide, POST vers `ENDPOINT` et écrit la réponse\n * sur stdout (NDJSON).\n * - En cas d'erreur réseau, HTTP >= 400, ou body stream interrompu, émet\n * une réponse JSON-RPC error (-32603) pour ne JAMAIS faire hang le client.\n *\n * stdout doit rester pur JSON-RPC (NDJSON) — tout log interne va sur stderr\n * via `console.error` (jamais `stdout.write` pour autre chose qu'une réponse\n * JSON-RPC). Pas d'état stateful : le serveur HTTP est stateless lui aussi.\n */\n\nimport { realpathSync } from \"node:fs\";\nimport { stdin, stdout } from \"node:process\";\nimport { createInterface } from \"node:readline\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { VERSION } from \"../src/core/version.js\";\n\nconst DEFAULT_ENDPOINT = \"https://france-data-mcp.vercel.app/mcp\";\nconst ENDPOINT = process.env.FRANCE_DATA_MCP_URL || DEFAULT_ENDPOINT;\nconst USER_AGENT = `france-data-mcp-npm/${VERSION}`;\nconst JSON_RPC_INTERNAL_ERROR = -32603;\n\nconst rawTimeoutEnv = process.env.FRANCE_DATA_MCP_TIMEOUT_MS;\nconst parsedTimeout = Number(rawTimeoutEnv);\nconst isValidTimeout = Number.isFinite(parsedTimeout) && parsedTimeout > 0;\nconst REQUEST_TIMEOUT_MS = isValidTimeout ? parsedTimeout : 60_000;\n// Signaler le fallback côté stderr (jamais stdout — réservé au JSON-RPC) pour\n// éviter un silent failure si l'utilisateur a tapé une valeur invalide.\nif (rawTimeoutEnv !== undefined && !isValidTimeout) {\n console.error(\n `[france-data-mcp-npm] FRANCE_DATA_MCP_TIMEOUT_MS=\"${rawTimeoutEnv}\" invalide, fallback ${REQUEST_TIMEOUT_MS}ms`,\n );\n}\n\ntype JsonRpcId = string | number | null;\ntype JsonRpcMessage = { id?: JsonRpcId; method?: string };\n\n/**\n * Extrait l'id JSON-RPC d'une ligne (best-effort). Utilisé uniquement pour\n * construire une réponse error propre quand le forward réseau échoue.\n */\nfunction parseId(line: string): JsonRpcId {\n try {\n const msg = JSON.parse(line) as unknown;\n if (msg && typeof msg === \"object\" && \"id\" in msg) {\n const id = (msg as JsonRpcMessage).id;\n if (typeof id === \"string\" || typeof id === \"number\" || id === null) return id;\n }\n } catch {\n // Best-effort : si la ligne n'est pas du JSON valide, forwardLine émettra\n // quand même une réponse JSON-RPC error sur stdout avec id=null. Le\n // diagnostic texte va sur stderr via console.error.\n }\n return null;\n}\n\nconst defaultWriteOut = (s: string): void => {\n stdout.write(s);\n};\n\n/**\n * Émet une réponse JSON-RPC error sur stdout. NDJSON appliqué de façon\n * uniforme (1 message = 1 ligne).\n */\nfunction emitJsonRpcError(\n id: JsonRpcId,\n code: number,\n message: string,\n writeOut: (s: string) => void,\n): void {\n const payload = { jsonrpc: \"2.0\", id, error: { code, message } };\n writeOut(`${JSON.stringify(payload)}\\n`);\n}\n\n/**\n * POST une ligne JSON-RPC vers l'endpoint HTTP et écrit la réponse sur stdout.\n * Catche toutes les erreurs (réseau, timeout, HTTP >=400, stream interrompu)\n * en émettant une réponse JSON-RPC error — le client MCP voit toujours une\n * réponse pour chaque request, jamais de hang.\n */\nexport async function forwardLine(\n line: string,\n fetchFn: typeof fetch = fetch,\n writeOut: (s: string) => void = defaultWriteOut,\n): Promise<void> {\n const trimmed = line.trim();\n if (!trimmed) return;\n\n // Parsing d'id fait UNE seule fois, réutilisé sur tous les chemins d'erreur.\n const id = parseId(trimmed);\n\n let response: Response;\n try {\n response = await fetchFn(ENDPOINT, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n \"user-agent\": USER_AGENT,\n accept: \"application/json\",\n },\n body: trimmed,\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n });\n } catch (err) {\n const reason = sanitizeReason(err instanceof Error ? err.message : String(err));\n console.error(`[france-data-mcp-npm] forward failed: ${reason}`);\n emitJsonRpcError(\n id,\n JSON_RPC_INTERNAL_ERROR,\n `Network error forwarding to ${SAFE_ENDPOINT}: ${reason}`,\n writeOut,\n );\n return;\n }\n\n let text: string;\n try {\n text = await response.text();\n } catch (err) {\n // Stream interrompu après les headers (gateway timeout, réseau coupé) :\n // sans ce catch, la promise rejette → main() crash → client hang sur l'id.\n // console.error pour le diagnostic local, capture Sentry inapplicable\n // côté wrapper client (par design : pas de telemetry).\n const reason = sanitizeReason(err instanceof Error ? err.message : String(err));\n console.error(`[france-data-mcp-npm] body stream interrupted: ${reason}`);\n emitJsonRpcError(\n id,\n JSON_RPC_INTERNAL_ERROR,\n `Body stream interrupted from ${SAFE_ENDPOINT}: ${reason}`,\n writeOut,\n );\n return;\n }\n\n if (!response.ok) {\n emitJsonRpcError(\n id,\n JSON_RPC_INTERNAL_ERROR,\n `Upstream HTTP ${response.status} from ${SAFE_ENDPOINT}: ${sanitizeReason(text.slice(0, 200))}`,\n writeOut,\n );\n return;\n }\n\n // L'endpoint Vercel renvoie un seul objet JSON-RPC (status 200) ou rien\n // (204 pour les notifications). Passthrough verbatim — pas de re-sérialisation\n // pour préserver la précision (ordre des clés, formats numériques).\n if (text.length > 0) writeOut(`${text.replace(/\\n+$/u, \"\")}\\n`);\n}\n\n/**\n * Masque les credentials userinfo d'une URL connue (notre ENDPOINT) avant\n * inclusion dans un log stderr ou un message error JSON-RPC stdout.\n *\n * Surfaces couvertes :\n * 1. Banner stderr au démarrage (`SAFE_ENDPOINT`)\n * 2. Messages error JSON-RPC qui interpolent `${SAFE_ENDPOINT}`\n * 3. Messages error des exceptions `fetch` Node 22+ — leur `err.message`\n * embarque l'URL fautive ENTIÈRE incluant userinfo (\"Request cannot be\n * constructed from a URL that includes credentials: https://user:pass@…\").\n * Cette surface est couverte par `sanitizeReason()` complémentaire — ne PAS\n * se reposer sur cette fonction seule.\n */\nfunction safeEndpointForLog(endpoint: string): string {\n try {\n const u = new URL(endpoint);\n u.username = \"\";\n u.password = \"\";\n return u.toString();\n } catch {\n return endpoint;\n }\n}\n\n// Calculé une seule fois au module load — réutilisé dans tous les messages\n// (banner stderr + 3 chemins d'erreur stdout JSON-RPC).\nconst SAFE_ENDPOINT = safeEndpointForLog(ENDPOINT);\n\n/**\n * Strip toute URL `scheme://user:pass@host` d'une string libre. Le runtime\n * `fetch` Node 22+ throw un `TypeError` dont le message contient verbatim\n * l'URL fautive, incluant userinfo. Sans sanitization, le `reason` d'une\n * erreur réseau fuiterait les credentials côté stdout (JSON-RPC error) ET\n * stderr (diagnostic). Defense-in-depth obligatoire — la même URL pourrait\n * arriver via un message d'erreur de proxy, DNS, gateway, etc.\n */\nfunction sanitizeReason(reason: string): string {\n return reason.replace(/(https?:\\/\\/)[^/\\s@]+@/giu, \"$1[redacted]@\");\n}\n\nasync function main(): Promise<void> {\n console.error(`[france-data-mcp-npm] v${VERSION} → ${SAFE_ENDPOINT}`);\n const rl = createInterface({ input: stdin, crlfDelay: Number.POSITIVE_INFINITY });\n for await (const line of rl) {\n await forwardLine(line);\n }\n}\n\n/**\n * Détecte si le module est exécuté directement (vs importé par un test).\n * Évite de démarrer la boucle stdin pendant les tests.\n *\n * V0.7.6 fix : la garde précédente comparait `pathToFileURL(argv1).href` à\n * `import.meta.url`. Quand npm/npx exécutent le bin via un symlink dans\n * `node_modules/.bin/`, `process.argv[1]` reste le chemin du symlink mais\n * Node ESM résout `import.meta.url` vers la cible réelle. Les deux divergent\n * → `main()` jamais appelé → process exit silencieux. Régression silencieuse\n * en prod depuis V0.7.2 (1er wrapper npm). Fix : comparer les `realpath` des\n * deux côtés pour matcher quel que soit le routage symlink.\n */\nexport function isMainModule(importMetaUrl: string, argv1: string | undefined): boolean {\n if (typeof argv1 !== \"string\") return false;\n try {\n return realpathSync(fileURLToPath(importMetaUrl)) === realpathSync(argv1);\n } catch {\n // Catch volontairement silencieux (PAS un silent failure métier) :\n // c'est une décision booléenne sans effet utilisateur — on ne hand off\n // aucune donnée, on ne masque aucune erreur API. Si realpath ou\n // fileURLToPath throw (URL malformée, fichier inexistant — typique en\n // contexte test/import abstrait), la bonne réponse est \"ne PAS démarrer\n // main()\", pas \"logger une erreur\" qui polluerait stderr de tous les\n // tests qui importent le module.\n return false;\n }\n}\n\nif (isMainModule(import.meta.url, process.argv[1])) {\n main().catch((err: unknown) => {\n const reason = err instanceof Error ? err.message : String(err);\n console.error(`[france-data-mcp-npm] fatal: ${reason}`);\n process.exit(1);\n });\n}\n\nexport { ENDPOINT, USER_AGENT, parseId, safeEndpointForLog };\n"]}
package/dist/index.js CHANGED
@@ -74,8 +74,14 @@ async function fetchJson(url, options = {}) {
74
74
  if (err instanceof HttpError) throw err;
75
75
  lastError = err;
76
76
  if (lastError instanceof SyntaxError) {
77
- console.error(`[france-data-mcp] invalid JSON response from ${url}: ${lastError.message}`);
78
- throw lastError;
77
+ const isFinalAttempt2 = attempt === maxRetries;
78
+ const log2 = isFinalAttempt2 ? console.error : console.warn;
79
+ log2(
80
+ `[france-data-mcp] invalid JSON response from ${url} (attempt ${attempt + 1}/${maxRetries + 1}): ${lastError.message}`
81
+ );
82
+ if (isFinalAttempt2) break;
83
+ await sleep(baseDelayMs * 2 ** attempt + jitter());
84
+ continue;
79
85
  }
80
86
  if (lastError.name === "AbortError") {
81
87
  console.warn(`[france-data-mcp] fetch aborted (caller signal) on ${url}`);
@@ -201,16 +207,70 @@ function toCommune(api) {
201
207
  };
202
208
  }
203
209
 
210
+ // src/core/coords.ts
211
+ function parseCoordinates(lon, lat) {
212
+ const lonNum = parseLooseNumber(lon);
213
+ const latNum = parseLooseNumber(lat);
214
+ if (lonNum === void 0 || latNum === void 0) return void 0;
215
+ return { lon: lonNum, lat: latNum };
216
+ }
217
+ function parseLooseNumber(value) {
218
+ if (value == null) return void 0;
219
+ if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
220
+ const normalized = value.replace(",", ".");
221
+ const num = Number.parseFloat(normalized);
222
+ return Number.isFinite(num) ? num : void 0;
223
+ }
224
+
225
+ // src/core/text-match.ts
226
+ function normalizeForCompare(value) {
227
+ return value.normalize("NFD").replace(new RegExp("\\p{M}", "gu"), "").toLowerCase().replace(/[.,'’\-\s]+/g, " ").trim();
228
+ }
229
+ function diceCoefficient(a, b) {
230
+ if (a === b) return 1;
231
+ if (a.length < 2 || b.length < 2) return a === b ? 1 : 0;
232
+ const bigramsA = /* @__PURE__ */ new Map();
233
+ for (let i = 0; i < a.length - 1; i++) {
234
+ const bg = a.slice(i, i + 2);
235
+ bigramsA.set(bg, (bigramsA.get(bg) ?? 0) + 1);
236
+ }
237
+ let intersection = 0;
238
+ let totalB = 0;
239
+ for (let i = 0; i < b.length - 1; i++) {
240
+ const bg = b.slice(i, i + 2);
241
+ totalB++;
242
+ const inA = bigramsA.get(bg);
243
+ if (inA !== void 0 && inA > 0) {
244
+ intersection++;
245
+ bigramsA.set(bg, inA - 1);
246
+ }
247
+ }
248
+ const totalA = a.length - 1;
249
+ if (totalA + totalB === 0) return 0;
250
+ return 2 * intersection / (totalA + totalB);
251
+ }
252
+
204
253
  // src/territoire/geocode.ts
205
254
  var BASE_URL2 = "https://data.geopf.fr/geocodage";
206
- var LOW_SCORE_THRESHOLD = 0.5;
255
+ var LOW_SCORE_THRESHOLD_BY_TYPE = {
256
+ housenumber: 0.7,
257
+ street: 0.6,
258
+ locality: 0.6,
259
+ municipality: 0.5
260
+ };
261
+ var DEFAULT_LOW_SCORE_THRESHOLD = 0.5;
262
+ function lowScoreThreshold(type) {
263
+ return LOW_SCORE_THRESHOLD_BY_TYPE[type] ?? DEFAULT_LOW_SCORE_THRESHOLD;
264
+ }
265
+ var PARTIAL_MATCH_DICE_THRESHOLD = 0.7;
207
266
  async function geocode(address, options = {}) {
208
267
  const results = await geocodeMany(address, { ...options, limit: 1 });
209
268
  const top = results[0];
210
269
  if (!top) return null;
211
- if (top.score < LOW_SCORE_THRESHOLD) {
270
+ if (top.confidence_low || top.match_partial) {
271
+ const reason = top.confidence_low ? `score ${top.score.toFixed(2)} < seuil ${lowScoreThreshold(top.type)} (type "${top.type}")` : `libell\xE9 IGN divergent de l'adresse demand\xE9e (match_partial)`;
212
272
  console.warn(
213
- `[france-data-mcp] geocode("${address}"): score ${top.score.toFixed(2)} < ${LOW_SCORE_THRESHOLD} \u2014 r\xE9sultat tr\xE8s incertain (label retourn\xE9: "${top.label}").`
273
+ `[france-data-mcp] geocode("${address}"): ${reason} \u2014 r\xE9sultat incertain (label retourn\xE9: "${top.label}").`
214
274
  );
215
275
  }
216
276
  return top;
@@ -224,7 +284,16 @@ async function geocodeMany(address, options = {}) {
224
284
  if (type) params.set("type", type);
225
285
  const url = `${BASE_URL2}/search/?${params.toString()}`;
226
286
  const data = await fetchJson(url, { signal });
227
- return data.features.map(toGeocodeResult);
287
+ return usableGeocodeResults(data.features, `q="${address}"`, address);
288
+ }
289
+ function usableGeocodeResults(features, context, requestedAddress) {
290
+ const results = features.map((f) => toGeocodeResult(f, requestedAddress)).filter((r) => r !== null);
291
+ if (features.length > 0 && results.length === 0) {
292
+ console.warn(
293
+ `[france-data-mcp] geocode (${context}): IGN a renvoy\xE9 ${features.length} feature(s) mais toutes inexploitables \u2014 r\xE9sultat vide \u2260 \xAB adresse introuvable \xBB, anomalie payload IGN.`
294
+ );
295
+ }
296
+ return results;
228
297
  }
229
298
  async function reverseGeocode(point, signal) {
230
299
  const params = new URLSearchParams({
@@ -233,11 +302,18 @@ async function reverseGeocode(point, signal) {
233
302
  });
234
303
  const url = `${BASE_URL2}/reverse/?${params.toString()}`;
235
304
  const data = await fetchJson(url, { signal });
236
- const feature = data.features[0];
237
- return feature ? toGeocodeResult(feature) : null;
305
+ const results = usableGeocodeResults(data.features, `reverse ${point.lon},${point.lat}`);
306
+ return results[0] ?? null;
238
307
  }
239
- function toGeocodeResult(feature) {
240
- const [lon, lat] = feature.geometry.coordinates;
308
+ function toGeocodeResult(feature, requestedAddress) {
309
+ const coords = feature.geometry?.coordinates;
310
+ const point = Array.isArray(coords) ? parseCoordinates(coords[0], coords[1]) : void 0;
311
+ if (!point) {
312
+ console.warn(
313
+ `[france-data-mcp] geocode: feature sans coordonn\xE9es exploitables (label: "${feature.properties?.label ?? "<absent>"}", type: "${feature.properties?.type ?? "<absent>"}") \u2014 feature ignor\xE9e.`
314
+ );
315
+ return null;
316
+ }
241
317
  const rawScore = feature.properties.score;
242
318
  const scoreValid = typeof rawScore === "number" && Number.isFinite(rawScore);
243
319
  if (!scoreValid) {
@@ -245,12 +321,20 @@ function toGeocodeResult(feature) {
245
321
  `[france-data-mcp] geocode: feature sans score num\xE9rique exploitable (label: "${feature.properties.label}", type: "${feature.properties.type}") \u2014 confidence_low forc\xE9 \xE0 true par prudence.`
246
322
  );
247
323
  }
324
+ const type = feature.properties.type;
325
+ const label = feature.properties.label;
326
+ const matchPartial = requestedAddress !== void 0 ? diceCoefficient(normalizeForCompare(requestedAddress), normalizeForCompare(label)) < PARTIAL_MATCH_DICE_THRESHOLD : void 0;
248
327
  return {
249
- point: { lon, lat },
250
- label: feature.properties.label,
328
+ point,
329
+ label,
251
330
  score: scoreValid ? rawScore : 0,
252
- confidence_low: scoreValid ? rawScore < LOW_SCORE_THRESHOLD : true,
253
- type: feature.properties.type,
331
+ confidence_low: scoreValid ? rawScore < lowScoreThreshold(type) : true,
332
+ type,
333
+ // Spread conditionnel (pas `pickDefined`, typé string-only) : on expose
334
+ // `match_partial:false` quand une adresse a été demandée ET matche bien
335
+ // — l'absence du champ signifie "non évalué" (géocodage inverse), pas
336
+ // "bon match". Distinction utile au caller.
337
+ ...matchPartial !== void 0 ? { match_partial: matchPartial } : {},
254
338
  ...pickDefined({
255
339
  codePostal: feature.properties.postcode,
256
340
  codeCommune: feature.properties.citycode,
@@ -403,21 +487,6 @@ async function getPopulationByDept(codeDept, options = {}) {
403
487
  // src/territoire/index.ts
404
488
  var TERRITOIRE_VERSION = "0.1.0";
405
489
 
406
- // src/core/coords.ts
407
- function parseCoordinates(lon, lat) {
408
- const lonNum = parseLooseNumber(lon);
409
- const latNum = parseLooseNumber(lat);
410
- if (lonNum === void 0 || latNum === void 0) return void 0;
411
- return { lon: lonNum, lat: latNum };
412
- }
413
- function parseLooseNumber(value) {
414
- if (value == null) return void 0;
415
- if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
416
- const normalized = value.replace(",", ".");
417
- const num = Number.parseFloat(normalized);
418
- return Number.isFinite(num) ? num : void 0;
419
- }
420
-
421
490
  // src/core/env.ts
422
491
  function readApiKeyEnv(name) {
423
492
  const raw = process.env[name];
@@ -1435,6 +1504,17 @@ function expectRpcRows(rpc, data) {
1435
1504
  }
1436
1505
  return data;
1437
1506
  }
1507
+ function buildListQueryResult(rpc, data, limit, metadata, mapRow) {
1508
+ const rows = expectRpcRows(rpc, data);
1509
+ const truncated = rows.length > limit;
1510
+ const sliced = truncated ? rows.slice(0, limit) : rows;
1511
+ return {
1512
+ count: sliced.length,
1513
+ truncated,
1514
+ results: sliced.map(mapRow),
1515
+ query_metadata: metadata
1516
+ };
1517
+ }
1438
1518
 
1439
1519
  // src/sante/finess-db.ts
1440
1520
  function familiesToCodes(familles) {
@@ -1498,15 +1578,13 @@ async function getFinessByNumFiness(numFiness) {
1498
1578
  return lookupFound(toFinessResult(first));
1499
1579
  }
1500
1580
  function buildFinessQueryResult(rpc, data, limit, metadata) {
1501
- const rows = expectRpcRows(rpc, data);
1502
- const truncated = rows.length > limit;
1503
- const sliced = truncated ? rows.slice(0, limit) : rows;
1504
- return {
1505
- count: sliced.length,
1506
- truncated,
1507
- results: sliced.map(toFinessResult),
1508
- query_metadata: metadata
1509
- };
1581
+ return buildListQueryResult(
1582
+ rpc,
1583
+ data,
1584
+ limit,
1585
+ metadata,
1586
+ toFinessResult
1587
+ );
1510
1588
  }
1511
1589
  function toFinessResult(row) {
1512
1590
  const lat = row.geom?.coordinates[1];
@@ -1598,15 +1676,13 @@ async function getRppsDansEtablissement(input) {
1598
1676
  p_limit: limit + 1
1599
1677
  });
1600
1678
  if (error) throw new Error(formatRpcError("rpps_dans_etablissement", error));
1601
- const rows = expectRpcRows("rpps_dans_etablissement", data);
1602
- const truncated = rows.length > limit;
1603
- const sliced = truncated ? rows.slice(0, limit) : rows;
1604
- return {
1605
- count: sliced.length,
1606
- truncated,
1607
- results: sliced.map(toCompactResult),
1608
- query_metadata: rppsEtablissementMetadata()
1609
- };
1679
+ return buildListQueryResult(
1680
+ "rpps_dans_etablissement",
1681
+ data,
1682
+ limit,
1683
+ rppsEtablissementMetadata(),
1684
+ toCompactResult
1685
+ );
1610
1686
  }
1611
1687
  async function getRppsById(rppsId) {
1612
1688
  const trimmed = rppsId.trim();
@@ -1624,15 +1700,13 @@ async function getRppsById(rppsId) {
1624
1700
  return rows.map(toLookupResult);
1625
1701
  }
1626
1702
  function buildQueryResult(rpc, data, limit, metadata) {
1627
- const rows = expectRpcRows(rpc, data);
1628
- const truncated = rows.length > limit;
1629
- const sliced = truncated ? rows.slice(0, limit) : rows;
1630
- return {
1631
- count: sliced.length,
1632
- truncated,
1633
- results: sliced.map(toResult),
1634
- query_metadata: metadata
1635
- };
1703
+ return buildListQueryResult(
1704
+ rpc,
1705
+ data,
1706
+ limit,
1707
+ metadata,
1708
+ toResult
1709
+ );
1636
1710
  }
1637
1711
  function toResult(row) {
1638
1712
  const lat = row.geom?.coordinates[1];