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 +4 -4
- package/README.md +3 -3
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.js +130 -56
- package/dist/index.js.map +1 -1
- package/dist/sante/index.js +40 -29
- package/dist/sante/index.js.map +1 -1
- package/dist/territoire/index.d.ts +25 -6
- package/dist/territoire/index.js +98 -14
- package/dist/territoire/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
- [
|
|
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
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
|
-
|
|
78
|
-
|
|
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
|
|
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.
|
|
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}"):
|
|
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
|
|
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
|
|
237
|
-
return
|
|
305
|
+
const results = usableGeocodeResults(data.features, `reverse ${point.lon},${point.lat}`);
|
|
306
|
+
return results[0] ?? null;
|
|
238
307
|
}
|
|
239
|
-
function toGeocodeResult(feature) {
|
|
240
|
-
const
|
|
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
|
|
250
|
-
label
|
|
328
|
+
point,
|
|
329
|
+
label,
|
|
251
330
|
score: scoreValid ? rawScore : 0,
|
|
252
|
-
confidence_low: scoreValid ? rawScore <
|
|
253
|
-
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
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
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
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
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
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
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];
|