france-data-mcp 0.7.2 → 0.7.5
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 +2 -1
- package/README.md +2 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.js +20 -11
- package/dist/index.js.map +1 -1
- package/dist/sante/index.js +17 -10
- package/dist/sante/index.js.map +1 -1
- package/dist/territoire/index.js +3 -1
- package/dist/territoire/index.js.map +1 -1
- package/package.json +25 -27
package/README.en.md
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
[](https://github.com/cturkieh/france-data-mcp/actions)
|
|
7
7
|
[](https://france-data-mcp.vercel.app/mcp)
|
|
8
8
|
[](https://www.npmjs.com/package/france-data-mcp)
|
|
9
|
+
[](https://smithery.ai/servers/cturkieh/france-data)
|
|
9
10
|
|
|
10
11
|
🇬🇧 Short English overview. [Full documentation in French →](README.md)
|
|
11
12
|
|
|
@@ -76,7 +77,7 @@ Brings together the most useful French government data sources under a uniform t
|
|
|
76
77
|
|
|
77
78
|
## Status
|
|
78
79
|
|
|
79
|
-
✅ **v0.7.
|
|
80
|
+
✅ **v0.7.5 — in production.** MCP server live at `https://france-data-mcp.vercel.app/mcp`, exposing **25 tools** with official MCP annotations and outputSchema (spec 2025-06-18, structuredContent emitted). ~95K FINESS facilities, ~462K Ameli practitioners, ~2.2M active RPPS practitioners. TypeScript strict, Biome clean, **606 tests passing**. Sentry error monitoring live (bot-noise filter active). See [CHANGELOG](CHANGELOG.md) for the full history.
|
|
80
81
|
|
|
81
82
|
### Roadmap
|
|
82
83
|
|
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
[](https://github.com/cturkieh/france-data-mcp/actions)
|
|
7
7
|
[](https://france-data-mcp.vercel.app/mcp)
|
|
8
8
|
[](https://www.npmjs.com/package/france-data-mcp)
|
|
9
|
+
[](https://smithery.ai/servers/cturkieh/france-data)
|
|
9
10
|
|
|
10
11
|
🇫🇷 Documentation principale en français. [English version →](README.en.md)
|
|
11
12
|
|
|
@@ -102,7 +103,7 @@ Usage intensif : throttler côté client ou self-héberger.
|
|
|
102
103
|
|
|
103
104
|
## État du projet
|
|
104
105
|
|
|
105
|
-
✅ **V0.7.
|
|
106
|
+
✅ **V0.7.5 — en production.** 25 tools avec annotations MCP + outputSchema (spec 2025-06-18, structuredContent émis), ~95 K FINESS, ~462 K Ameli, ~2,2 M RPPS actifs. **606 tests verts**, TypeScript strict, Biome clean. Crons GitHub Actions actifs (FINESS bimensuel, Ameli hebdo, RPPS mensuel). Sentry monitoring live (filtre bot-noise actif). Voir [CHANGELOG](CHANGELOG.md) pour l'historique.
|
|
106
107
|
|
|
107
108
|
### Roadmap
|
|
108
109
|
|
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":";;;;;;AASO,IAAM,OAAA,GAAU,OAAA;;;ACoBvB,IAAM,gBAAA,GAAmB,wCAAA;AACzB,IAAM,QAAA,GAAW,OAAA,CAAQ,GAAA,CAAI,mBAAA,IAAuB;AACpD,IAAM,UAAA,GAAa,uBAAuB,OAAO,CAAA;AACjD,IAAM,uBAAA,GAA0B,MAAA;AAEhC,IAAM,aAAA,GAAgB,QAAQ,GAAA,CAAI,0BAAA;AAClC,IAAM,aAAA,GAAgB,OAAO,aAAa,CAAA;AAC1C,IAAM,cAAA,GAAiB,MAAA,CAAO,QAAA,CAAS,aAAa,KAAK,aAAA,GAAgB,CAAA;AACzE,IAAM,kBAAA,GAAqB,iBAAiB,aAAA,GAAgB,GAAA;AAG5D,IAAI,aAAA,KAAkB,MAAA,IAAa,CAAC,cAAA,EAAgB;AAClD,EAAA,OAAA,CAAQ,KAAA;AAAA,IACN,CAAA,kDAAA,EAAqD,aAAa,CAAA,qBAAA,EAAwB,kBAAkB,CAAA,EAAA;AAAA,GAC9G;AACF;AASA,SAAS,QAAQ,IAAA,EAAyB;AACxC,EAAA,IAAI;AACF,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AAC3B,IAAA,IAAI,GAAA,IAAO,OAAO,GAAA,KAAQ,QAAA,IAAY,QAAQ,GAAA,EAAK;AACjD,MAAA,MAAM,KAAM,GAAA,CAAuB,EAAA;AACnC,MAAA,IAAI,OAAO,OAAO,QAAA,IAAY,OAAO,OAAO,QAAA,IAAY,EAAA,KAAO,MAAM,OAAO,EAAA;AAAA,IAC9E;AAAA,EACF,CAAA,CAAA,MAAQ;AAAA,EAIR;AACA,EAAA,OAAO,IAAA;AACT;AAEA,IAAM,eAAA,GAAkB,CAAC,CAAA,KAAoB;AAC3C,EAAA,MAAA,CAAO,MAAM,CAAC,CAAA;AAChB,CAAA;AAMA,SAAS,gBAAA,CACP,EAAA,EACA,IAAA,EACA,OAAA,EACA,QAAA,EACM;AACN,EAAA,MAAM,OAAA,GAAU,EAAE,OAAA,EAAS,KAAA,EAAO,IAAI,KAAA,EAAO,EAAE,IAAA,EAAM,OAAA,EAAQ,EAAE;AAC/D,EAAA,QAAA,CAAS,CAAA,EAAG,IAAA,CAAK,SAAA,CAAU,OAAO,CAAC;AAAA,CAAI,CAAA;AACzC;AAQA,eAAsB,WAAA,CACpB,IAAA,EACA,OAAA,GAAwB,KAAA,EACxB,WAAgC,eAAA,EACjB;AACf,EAAA,MAAM,OAAA,GAAU,KAAK,IAAA,EAAK;AAC1B,EAAA,IAAI,CAAC,OAAA,EAAS;AAGd,EAAA,MAAM,EAAA,GAAK,QAAQ,OAAO,CAAA;AAE1B,EAAA,IAAI,QAAA;AACJ,EAAA,IAAI;AACF,IAAA,QAAA,GAAW,MAAM,QAAQ,QAAA,EAAU;AAAA,MACjC,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS;AAAA,QACP,cAAA,EAAgB,kBAAA;AAAA,QAChB,YAAA,EAAc,UAAA;AAAA,QACd,MAAA,EAAQ;AAAA,OACV;AAAA,MACA,IAAA,EAAM,OAAA;AAAA,MACN,MAAA,EAAQ,WAAA,CAAY,OAAA,CAAQ,kBAAkB;AAAA,KAC/C,CAAA;AAAA,EACH,SAAS,GAAA,EAAK;AACZ,IAAA,MAAM,MAAA,GAAS,eAAe,GAAA,YAAe,KAAA,GAAQ,IAAI,OAAA,GAAU,MAAA,CAAO,GAAG,CAAC,CAAA;AAC9E,IAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,sCAAA,EAAyC,MAAM,CAAA,CAAE,CAAA;AAC/D,IAAA,gBAAA;AAAA,MACE,EAAA;AAAA,MACA,uBAAA;AAAA,MACA,CAAA,4BAAA,EAA+B,aAAa,CAAA,EAAA,EAAK,MAAM,CAAA,CAAA;AAAA,MACvD;AAAA,KACF;AACA,IAAA;AAAA,EACF;AAEA,EAAA,IAAI,IAAA;AACJ,EAAA,IAAI;AACF,IAAA,IAAA,GAAO,MAAM,SAAS,IAAA,EAAK;AAAA,EAC7B,SAAS,GAAA,EAAK;AAKZ,IAAA,MAAM,MAAA,GAAS,eAAe,GAAA,YAAe,KAAA,GAAQ,IAAI,OAAA,GAAU,MAAA,CAAO,GAAG,CAAC,CAAA;AAC9E,IAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,+CAAA,EAAkD,MAAM,CAAA,CAAE,CAAA;AACxE,IAAA,gBAAA;AAAA,MACE,EAAA;AAAA,MACA,uBAAA;AAAA,MACA,CAAA,6BAAA,EAAgC,aAAa,CAAA,EAAA,EAAK,MAAM,CAAA,CAAA;AAAA,MACxD;AAAA,KACF;AACA,IAAA;AAAA,EACF;AAEA,EAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,IAAA,gBAAA;AAAA,MACE,EAAA;AAAA,MACA,uBAAA;AAAA,MACA,CAAA,cAAA,EAAiB,QAAA,CAAS,MAAM,CAAA,MAAA,EAAS,aAAa,CAAA,EAAA,EAAK,cAAA,CAAe,IAAA,CAAK,KAAA,CAAM,CAAA,EAAG,GAAG,CAAC,CAAC,CAAA,CAAA;AAAA,MAC7F;AAAA,KACF;AACA,IAAA;AAAA,EACF;AAKA,EAAA,IAAI,IAAA,CAAK,SAAS,CAAA,EAAG,QAAA,CAAS,GAAG,IAAA,CAAK,OAAA,CAAQ,OAAA,EAAS,EAAE,CAAC;AAAA,CAAI,CAAA;AAChE;AAeA,SAAS,mBAAmB,QAAA,EAA0B;AACpD,EAAA,IAAI;AACF,IAAA,MAAM,CAAA,GAAI,IAAI,GAAA,CAAI,QAAQ,CAAA;AAC1B,IAAA,CAAA,CAAE,QAAA,GAAW,EAAA;AACb,IAAA,CAAA,CAAE,QAAA,GAAW,EAAA;AACb,IAAA,OAAO,EAAE,QAAA,EAAS;AAAA,EACpB,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,QAAA;AAAA,EACT;AACF;AAIA,IAAM,aAAA,GAAgB,mBAAmB,QAAQ,CAAA;AAUjD,SAAS,eAAe,MAAA,EAAwB;AAC9C,EAAA,OAAO,MAAA,CAAO,OAAA,CAAQ,2BAAA,EAA6B,eAAe,CAAA;AACpE;AAEA,eAAe,IAAA,GAAsB;AACnC,EAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,uBAAA,EAA0B,OAAO,CAAA,QAAA,EAAM,aAAa,CAAA,CAAE,CAAA;AACpE,EAAA,MAAM,EAAA,GAAK,gBAAgB,EAAE,KAAA,EAAO,OAAO,SAAA,EAAW,MAAA,CAAO,mBAAmB,CAAA;AAChF,EAAA,WAAA,MAAiB,QAAQ,EAAA,EAAI;AAC3B,IAAA,MAAM,YAAY,IAAI,CAAA;AAAA,EACxB;AACF;AAIA,IAAM,MAAA,GACJ,OAAO,OAAA,CAAQ,IAAA,CAAK,CAAC,CAAA,KAAM,QAAA,IAAY,MAAA,CAAA,IAAA,CAAY,GAAA,KAAQ,aAAA,CAAc,OAAA,CAAQ,IAAA,CAAK,CAAC,CAAC,CAAA,CAAE,IAAA;AAC5F,IAAI,MAAA,EAAQ;AACV,EAAA,IAAA,EAAK,CAAE,KAAA,CAAM,CAAC,GAAA,KAAiB;AAC7B,IAAA,MAAM,SAAS,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,OAAO,GAAG,CAAA;AAC9D,IAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,6BAAA,EAAgC,MAAM,CAAA,CAAE,CAAA;AACtD,IAAA,OAAA,CAAQ,KAAK,CAAC,CAAA;AAAA,EAChB,CAAC,CAAA;AACH","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.7.2\";\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 { stdin, stdout } from \"node:process\";\nimport { createInterface } from \"node:readline\";\nimport { pathToFileURL } 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// `import.meta.url === pathToFileURL(process.argv[1])` détecte qu'on est lancé\n// directement (vs importé par un test). Évite de boucler stdin pendant les tests.\nconst isMain =\n typeof process.argv[1] === \"string\" && import.meta.url === pathToFileURL(process.argv[1]).href;\nif (isMain) {\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.7.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 { stdin, stdout } from \"node:process\";\nimport { createInterface } from \"node:readline\";\nimport { pathToFileURL } 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// `import.meta.url === pathToFileURL(process.argv[1])` détecte qu'on est lancé\n// directement (vs importé par un test). Évite de boucler stdin pendant les tests.\nconst isMain =\n typeof process.argv[1] === \"string\" && import.meta.url === pathToFileURL(process.argv[1]).href;\nif (isMain) {\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
|
@@ -161,7 +161,9 @@ var DEFAULT_FIELDS = [
|
|
|
161
161
|
async function searchCommunes(options) {
|
|
162
162
|
const { nom, codePostal, code, limit = 10, boostPopulation = false, signal } = options;
|
|
163
163
|
if (!nom && !codePostal && !code) {
|
|
164
|
-
throw new
|
|
164
|
+
throw new RangeError(
|
|
165
|
+
"searchCommunes: au moins un crit\xE8re (nom, codePostal, code) est requis"
|
|
166
|
+
);
|
|
165
167
|
}
|
|
166
168
|
const params = new URLSearchParams();
|
|
167
169
|
if (nom) params.set("nom", nom);
|
|
@@ -356,20 +358,20 @@ async function searchEntreprises(options) {
|
|
|
356
358
|
signal
|
|
357
359
|
} = options;
|
|
358
360
|
if (!q && !naf && !codePostal && !departement && !codeCommune && !center) {
|
|
359
|
-
throw new
|
|
361
|
+
throw new RangeError(
|
|
360
362
|
"searchEntreprises: au moins un crit\xE8re est requis (q, naf, codePostal, departement, codeCommune ou center+radiusKm)"
|
|
361
363
|
);
|
|
362
364
|
}
|
|
363
365
|
if (center && (radiusKm === void 0 || radiusKm <= 0)) {
|
|
364
|
-
throw new
|
|
366
|
+
throw new RangeError("searchEntreprises: radiusKm > 0 requis quand center est fourni");
|
|
365
367
|
}
|
|
366
368
|
if (center && !q) {
|
|
367
369
|
if (naf) {
|
|
368
|
-
throw new
|
|
370
|
+
throw new RangeError(
|
|
369
371
|
"searchEntreprises: l'API DINUM n'accepte pas `naf` + `center+radiusKm` directement. Options : (1) `q='<terme>'` + center+radiusKm (recherche textuelle g\xE9olocalis\xE9e), (2) `naf` + `codePostal`/`departement`/`codeCommune` (filtrage administratif), (3) faire un reverseGeocode du center pour obtenir codeCommune puis filtrer."
|
|
370
372
|
);
|
|
371
373
|
}
|
|
372
|
-
throw new
|
|
374
|
+
throw new RangeError(
|
|
373
375
|
"searchEntreprises: `center+radiusKm` requiert un param\xE8tre `q` (recherche textuelle). L'API DINUM ne supporte pas la recherche g\xE9ographique pure."
|
|
374
376
|
);
|
|
375
377
|
}
|
|
@@ -399,7 +401,7 @@ async function searchEntreprises(options) {
|
|
|
399
401
|
}
|
|
400
402
|
async function getEntrepriseBySiren(siren, signal) {
|
|
401
403
|
if (!/^\d{9}$/.test(siren)) {
|
|
402
|
-
throw new
|
|
404
|
+
throw new RangeError(`getEntrepriseBySiren: SIREN invalide "${siren}" (attendu 9 chiffres)`);
|
|
403
405
|
}
|
|
404
406
|
const result = await searchEntreprises({ q: siren, perPage: 5, onlyActive: false, signal });
|
|
405
407
|
const match = result.entreprises.find((e) => e.siren === siren);
|
|
@@ -934,7 +936,7 @@ async function loadFiness(options = {}) {
|
|
|
934
936
|
function searchEtablissementsFiness(index, options) {
|
|
935
937
|
const { categories, codePostal, departement, codeCommune, center, radiusKm, limit } = options;
|
|
936
938
|
if (center && (radiusKm === void 0 || radiusKm <= 0)) {
|
|
937
|
-
throw new
|
|
939
|
+
throw new RangeError("searchEtablissementsFiness: radiusKm > 0 requis quand center est fourni");
|
|
938
940
|
}
|
|
939
941
|
const radiusMeters = center && radiusKm !== void 0 ? radiusKm * 1e3 : null;
|
|
940
942
|
const categoriesSet = categories ? new Set(categories) : null;
|
|
@@ -1250,6 +1252,15 @@ function trimOrNull(s) {
|
|
|
1250
1252
|
const trimmed = s.trim();
|
|
1251
1253
|
return trimmed === "" ? null : trimmed;
|
|
1252
1254
|
}
|
|
1255
|
+
function assertValidNumFiness(numFiness) {
|
|
1256
|
+
const trimmed = numFiness.trim();
|
|
1257
|
+
if (!/^\d{9}$/.test(trimmed)) {
|
|
1258
|
+
throw new RangeError(
|
|
1259
|
+
`[france-data-mcp] num_finess invalide "${numFiness}" \u2014 attendu 9 chiffres (FINESS site).`
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
return trimmed;
|
|
1263
|
+
}
|
|
1253
1264
|
function expectRpcRows(rpc, data) {
|
|
1254
1265
|
if (data === null || data === void 0) {
|
|
1255
1266
|
throw new Error(
|
|
@@ -1302,12 +1313,10 @@ async function getFinessByCategorie(input) {
|
|
|
1302
1313
|
return buildFinessQueryResult("finess_by_categorie", data, limit, finessByCategorieMetadata());
|
|
1303
1314
|
}
|
|
1304
1315
|
async function getFinessByNumFiness(numFiness) {
|
|
1305
|
-
|
|
1306
|
-
throw new Error(`[france-data-mcp] num_finess must be 9 digits, got "${numFiness}"`);
|
|
1307
|
-
}
|
|
1316
|
+
const trimmed = assertValidNumFiness(numFiness);
|
|
1308
1317
|
const supabase = getAnonClient();
|
|
1309
1318
|
const { data, error } = await supabase.rpc("finess_by_num_finess", {
|
|
1310
|
-
p_num_finess:
|
|
1319
|
+
p_num_finess: trimmed
|
|
1311
1320
|
});
|
|
1312
1321
|
if (error) {
|
|
1313
1322
|
throw new Error(formatRpcError("finess_by_num_finess", error));
|