france-data-mcp 0.7.2
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/LICENSE +21 -0
- package/README.en.md +100 -0
- package/README.md +137 -0
- package/dist/cli.js +127 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1674 -0
- package/dist/index.js.map +1 -0
- package/dist/sante/index.d.ts +1053 -0
- package/dist/sante/index.js +1566 -0
- package/dist/sante/index.js.map +1 -0
- package/dist/territoire/index.d.ts +153 -0
- package/dist/territoire/index.js +249 -0
- package/dist/territoire/index.js.map +1 -0
- package/dist/types-6cvLQmuz.d.ts +68 -0
- package/package.json +105 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cyril Turkieh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.en.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# france-data-mcp
|
|
2
|
+
|
|
3
|
+
> A TypeScript MCP server that **cross-references and reconciles** 6 French public registries (INSEE SIRENE, FINESS DREES, RPPS / ANS Health Directory, Ameli Health Directory, IGN, DINUM). Detects closed SIRETs not yet propagated by DREES, distinguishes site vs group, exposes data freshness per source.
|
|
4
|
+
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://github.com/cturkieh/france-data-mcp/actions)
|
|
7
|
+
[](https://france-data-mcp.vercel.app/mcp)
|
|
8
|
+
[](https://www.npmjs.com/package/france-data-mcp)
|
|
9
|
+
|
|
10
|
+
🇬🇧 Short English overview. [Full documentation in French →](README.md)
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
### Option 1 — Remote URL (claude.ai, Claude Code, Cursor)
|
|
17
|
+
|
|
18
|
+
`https://france-data-mcp.vercel.app/mcp`
|
|
19
|
+
|
|
20
|
+
| Client | Config |
|
|
21
|
+
|---|---|
|
|
22
|
+
| **claude.ai** | Settings → Connectors → Add custom connector → URL above |
|
|
23
|
+
| **Claude Code** | `~/.claude.json` → `mcpServers` → `{ "type": "http", "url": "..." }` |
|
|
24
|
+
| **Cursor** | `~/.cursor/mcp.json` → same configuration |
|
|
25
|
+
|
|
26
|
+
### Option 2 — npm stdio wrapper (native Claude Desktop, other clients)
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"mcpServers": {
|
|
31
|
+
"france-data": {
|
|
32
|
+
"command": "npx",
|
|
33
|
+
"args": ["-y", "france-data-mcp"]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The wrapper forwards stdio → remote HTTPS endpoint. No local DB to provision. Override possible: `FRANCE_DATA_MCP_URL=https://my-mirror.example/mcp`.
|
|
40
|
+
|
|
41
|
+
Client-by-client setup + self-hosting: [docs/installation-claude.md](docs/installation-claude.md).
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## What it does
|
|
46
|
+
|
|
47
|
+
Brings together the most useful French government data sources under a uniform typed API and a single MCP server endpoint, with:
|
|
48
|
+
|
|
49
|
+
- Typed API (zero `any`), uniform rate-limit handling (exponential retry, `retry-after` aware)
|
|
50
|
+
- Hardened ingestion pipeline (SHA-256 short-circuit, atomic swap, post-swap canary)
|
|
51
|
+
- Cross-source reconciliation FINESS ↔ RPPS ↔ SIRENE — detects closed SIRETs still listed active, M&A renamings not yet propagated, inconsistent company names across registries
|
|
52
|
+
- Honest docs about what each source contains and its gotchas
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Tools (25)
|
|
57
|
+
|
|
58
|
+
- **Territory (4)**: `autocomplete_commune`, `get_commune_by_code`, `geocode_adresse`, `reverse_geocode`
|
|
59
|
+
- **Companies (3)**: `entreprises_in_radius`, `entreprise_by_siren` (+ INSEE SIRENE V3.11 fallback), `etablissement_by_siret`
|
|
60
|
+
- **FINESS healthcare facilities (3)**: `etablissements_finess_in_radius`, `etablissements_finess_by_categorie`, `etablissement_by_finess`
|
|
61
|
+
- **Ameli licensed practitioners (4)**: `professionnels_in_radius`, `professionnels_par_specialite_dept`, `lister_specialites_ameli`, `lister_types_ps_ameli`
|
|
62
|
+
- **RPPS / ANS — all active practitioners (5)**: `professionnels_rpps_in_radius`, `professionnels_rpps_par_dept`, `rpps_dans_etablissement`, `rpps_search_by_name` (fuzzy trigram), `professionnel_by_rpps` (+ live FHIR ANS fallback)
|
|
63
|
+
- **Multi-source cross-checks (6)**: `data_freshness`, `verifier_site_actif`, `compare_raison_sociale_finess_vs_rpps`, `historique_etablissement`, `reconcilier_finess_sirene`, `finess_sirene_coverage_in_radius`
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Use cases
|
|
68
|
+
|
|
69
|
+
- Healthcare territorial analysis (supply mapping, access to care)
|
|
70
|
+
- Market research and competitive intelligence
|
|
71
|
+
- Local journalism with data
|
|
72
|
+
- Civic tech applications
|
|
73
|
+
- Any *"what's around this point?"* query on French open data
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Status
|
|
78
|
+
|
|
79
|
+
✅ **v0.7.2 — in production.** MCP server live at `https://france-data-mcp.vercel.app/mcp`, exposing **25 tools**. ~95K FINESS facilities, ~462K Ameli practitioners, ~2.2M active RPPS practitioners. TypeScript strict, Biome clean, **580 tests passing**. Sentry error monitoring live. See [CHANGELOG](CHANGELOG.md) for the full history.
|
|
80
|
+
|
|
81
|
+
### Roadmap
|
|
82
|
+
|
|
83
|
+
- **v0.8** — Composite health tools (`panorama_sante_territoire`, density analytics), INSEE Melodi (commune-level macro series)
|
|
84
|
+
- **v0.9+** — DOM-COM support, INSEE IRIS (infra-communal demographics)
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Public limits
|
|
89
|
+
|
|
90
|
+
- **Rate limit**: 60 req/min per IP on `tools/call` (handshake methods stay free). Over the limit: JSON-RPC error `-32000` with `data.retryAfterSeconds`.
|
|
91
|
+
- **Structured JSON logs** per request: `ts`, `method`, `tool`, `ip_hash` (SHA-256), `duration_ms`, `outcome`. No raw IPs, no tool args persisted (GDPR-friendly).
|
|
92
|
+
- **Sentry error monitoring** on internal 500s (tags `mcp.method`, `mcp.tool`, `mcp.outcome`).
|
|
93
|
+
|
|
94
|
+
For heavy use, throttle client-side or self-host.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
MIT for the code. Each data source keeps its own license (mostly Etalab Open License). See [main README](README.md#licence) for attribution requirements.
|
package/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# france-data-mcp
|
|
2
|
+
|
|
3
|
+
> MCP TypeScript qui **croise et réconcilie** 6 référentiels publics français (INSEE SIRENE, FINESS DREES, RPPS / Annuaire Santé ANS, Annuaire Santé Ameli, IGN, DINUM). Détecte les SIRET fermés invisibles côté DREES, distingue site vs groupe, expose la fraîcheur de chaque source.
|
|
4
|
+
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://github.com/cturkieh/france-data-mcp/actions)
|
|
7
|
+
[](https://france-data-mcp.vercel.app/mcp)
|
|
8
|
+
[](https://www.npmjs.com/package/france-data-mcp)
|
|
9
|
+
|
|
10
|
+
🇫🇷 Documentation principale en français. [English version →](README.en.md)
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
### Option 1 — URL distante (claude.ai, Claude Code, Cursor)
|
|
17
|
+
|
|
18
|
+
`https://france-data-mcp.vercel.app/mcp`
|
|
19
|
+
|
|
20
|
+
| Client | Config |
|
|
21
|
+
|---|---|
|
|
22
|
+
| **claude.ai** | Settings → Connectors → Add custom connector → URL ci-dessus |
|
|
23
|
+
| **Claude Code** | `~/.claude.json` → `mcpServers` → `{ "type": "http", "url": "..." }` |
|
|
24
|
+
| **Cursor** | `~/.cursor/mcp.json` → même configuration |
|
|
25
|
+
|
|
26
|
+
### Option 2 — Wrapper npm stdio (Claude Desktop natif, autres clients)
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"mcpServers": {
|
|
31
|
+
"france-data": {
|
|
32
|
+
"command": "npx",
|
|
33
|
+
"args": ["-y", "france-data-mcp"]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Le wrapper forwarde stdio → endpoint HTTPS distant. Aucune DB locale à provisionner. Override possible : `FRANCE_DATA_MCP_URL=https://mon-miroir.example/mcp`.
|
|
40
|
+
|
|
41
|
+
Détails par client + self-hosting : [docs/installation-claude.md](docs/installation-claude.md).
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Pourquoi ce projet
|
|
46
|
+
|
|
47
|
+
Les APIs officielles (INSEE, FINESS DREES, RPPS ANS, Annuaire Ameli, IGN, DINUM) existent mais sont **éclatées, sous-documentées et pleines de pièges** : rate limits, formats CSV propriétaires, latence DREES de 1-2 mois, diffusion partielle INSEE, mappings inconsistants Ameli ↔ RPPS.
|
|
48
|
+
|
|
49
|
+
`france-data-mcp` est **le premier MCP qui croise factuellement ces sources** pour répondre à des questions concrètes — cartographie d'offre de soins, étude de marché territoriale, journalisme local, civic-tech.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Périmètre — 6 sources publiques croisées
|
|
54
|
+
|
|
55
|
+
- 🗺️ **Territoire** : geo.api.gouv.fr (DINUM, communes), IGN Géoplateforme (géocodage)
|
|
56
|
+
- 🏥 **Santé** : FINESS / DREES (~95 K établissements), Annuaire Santé Ameli (~462 K libéraux), RPPS / ANS (~2,2 M PS actifs)
|
|
57
|
+
- 🏢 **Entreprises** : DINUM Recherche Entreprises + INSEE SIRENE V3.11
|
|
58
|
+
|
|
59
|
+
**Cross-source** : réconciliation FINESS ↔ RPPS ↔ SIRENE pour détecter SIRET fermés, rebrandings, raisons sociales périmées.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Outils MCP (25 tools)
|
|
64
|
+
|
|
65
|
+
### 🗺️ Territoire (4)
|
|
66
|
+
`autocomplete_commune` · `get_commune_by_code` · `geocode_adresse` · `reverse_geocode`
|
|
67
|
+
|
|
68
|
+
### 🏢 Entreprises (3)
|
|
69
|
+
`entreprises_in_radius` · `entreprise_by_siren` (+ fallback INSEE SIRENE V3.11) · `etablissement_by_siret`
|
|
70
|
+
|
|
71
|
+
### 🏥 Établissements santé FINESS (3)
|
|
72
|
+
`etablissements_finess_in_radius` · `etablissements_finess_by_categorie` · `etablissement_by_finess`
|
|
73
|
+
|
|
74
|
+
> 24 familles couvrant ~92 % du volume. Source DREES rafraîchie bimestriellement.
|
|
75
|
+
|
|
76
|
+
### 👨⚕️ Professionnels libéraux Ameli (4)
|
|
77
|
+
`professionnels_in_radius` · `professionnels_par_specialite_dept` · `lister_specialites_ameli` · `lister_types_ps_ameli`
|
|
78
|
+
|
|
79
|
+
> Libéraux **conventionnés uniquement** (~462 K).
|
|
80
|
+
|
|
81
|
+
### 🩺 Tous les PS — RPPS / Annuaire Santé ANS (5)
|
|
82
|
+
`professionnels_rpps_in_radius` · `professionnels_rpps_par_dept` · `rpps_dans_etablissement` · `rpps_search_by_name` (fuzzy) · `professionnel_by_rpps` (+ fallback FHIR ANS)
|
|
83
|
+
|
|
84
|
+
> ~2,2 M PS actifs (libéraux + salariés privés + hospitaliers contractuels + agents publics). Par défaut : Civils uniquement.
|
|
85
|
+
|
|
86
|
+
### 🔀 Croisement multi-source (6)
|
|
87
|
+
Réconciliation FINESS ↔ RPPS ↔ SIRENE — faits bruts sans interprétation métier.
|
|
88
|
+
|
|
89
|
+
`data_freshness` · `verifier_site_actif` · `compare_raison_sociale_finess_vs_rpps` · `historique_etablissement` · `reconcilier_finess_sirene` · `finess_sirene_coverage_in_radius`
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Garde-fous publics
|
|
94
|
+
|
|
95
|
+
- **Rate limit** : 60 req/min par IP sur `tools/call` (les méthodes meta restent libres). Au-delà : erreur `-32000` avec `data.retryAfterSeconds`.
|
|
96
|
+
- **Logs JSON structurés** par requête : `ts`, `method`, `tool`, `ip_hash` (SHA-256), `duration_ms`, `outcome`. Aucune IP en clair, aucun argument tool persisté (RGPD-friendly).
|
|
97
|
+
- **Sentry error monitoring** sur les 500 internes (tags `mcp.method`, `mcp.tool`, `mcp.outcome`).
|
|
98
|
+
|
|
99
|
+
Usage intensif : throttler côté client ou self-héberger.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## État du projet
|
|
104
|
+
|
|
105
|
+
✅ **V0.7.2 — en production.** 25 tools, ~95 K FINESS, ~462 K Ameli, ~2,2 M RPPS actifs. **580 tests verts**, TypeScript strict, Biome clean. Crons GitHub Actions actifs (FINESS bimensuel, Ameli hebdo, RPPS mensuel). Sentry monitoring live. Voir [CHANGELOG](CHANGELOG.md) pour l'historique.
|
|
106
|
+
|
|
107
|
+
### Roadmap
|
|
108
|
+
|
|
109
|
+
- [ ] **V0.8** — Tools composites santé (`panorama_sante_territoire`, densités), INSEE Melodi (séries macro communales)
|
|
110
|
+
- [ ] **V0.9+** — Support DOM-COM, INSEE IRIS (démographie infra-communale)
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Contribuer
|
|
115
|
+
|
|
116
|
+
Ouvrir une issue pour discuter avant d'envoyer une PR.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Licence
|
|
121
|
+
|
|
122
|
+
MIT — voir [LICENSE](LICENSE). Les **données** restent sous leurs licences respectives :
|
|
123
|
+
|
|
124
|
+
| Source | Licence | Mention obligatoire |
|
|
125
|
+
|---|---|---|
|
|
126
|
+
| FINESS | Licence Ouverte (Etalab) | « Source : FINESS, ANS/DREES » |
|
|
127
|
+
| Annuaire Santé Ameli | Art. L.1461-2 CSP | « Source : Annuaire santé Ameli, Assurance Maladie » |
|
|
128
|
+
| DINUM Recherche Entreprises | Licence Ouverte | « Source : Annuaire des Entreprises, DINUM » |
|
|
129
|
+
| INSEE | Licence Ouverte | « Source : Insee » |
|
|
130
|
+
| IGN Géoplateforme | Licence Ouverte | « © IGN/Géoplateforme » |
|
|
131
|
+
| geo.api.gouv.fr | Licence Ouverte | « Source : geo.api.gouv.fr (Etalab) » |
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Remerciements
|
|
136
|
+
|
|
137
|
+
DINUM, Etalab, Atlasanté, ANS, INSEE, IGN pour la qualité de leurs APIs. data.gouv.fr pour l'animation civic-tech. Anthropic pour le protocole MCP.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { stdin, stdout } from 'process';
|
|
3
|
+
import { createInterface } from 'readline';
|
|
4
|
+
import { pathToFileURL } from 'url';
|
|
5
|
+
|
|
6
|
+
// src/core/version.ts
|
|
7
|
+
var VERSION = "0.7.2";
|
|
8
|
+
|
|
9
|
+
// bin/cli.ts
|
|
10
|
+
var DEFAULT_ENDPOINT = "https://france-data-mcp.vercel.app/mcp";
|
|
11
|
+
var ENDPOINT = process.env.FRANCE_DATA_MCP_URL || DEFAULT_ENDPOINT;
|
|
12
|
+
var USER_AGENT = `france-data-mcp-npm/${VERSION}`;
|
|
13
|
+
var JSON_RPC_INTERNAL_ERROR = -32603;
|
|
14
|
+
var rawTimeoutEnv = process.env.FRANCE_DATA_MCP_TIMEOUT_MS;
|
|
15
|
+
var parsedTimeout = Number(rawTimeoutEnv);
|
|
16
|
+
var isValidTimeout = Number.isFinite(parsedTimeout) && parsedTimeout > 0;
|
|
17
|
+
var REQUEST_TIMEOUT_MS = isValidTimeout ? parsedTimeout : 6e4;
|
|
18
|
+
if (rawTimeoutEnv !== void 0 && !isValidTimeout) {
|
|
19
|
+
console.error(
|
|
20
|
+
`[france-data-mcp-npm] FRANCE_DATA_MCP_TIMEOUT_MS="${rawTimeoutEnv}" invalide, fallback ${REQUEST_TIMEOUT_MS}ms`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
function parseId(line) {
|
|
24
|
+
try {
|
|
25
|
+
const msg = JSON.parse(line);
|
|
26
|
+
if (msg && typeof msg === "object" && "id" in msg) {
|
|
27
|
+
const id = msg.id;
|
|
28
|
+
if (typeof id === "string" || typeof id === "number" || id === null) return id;
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
var defaultWriteOut = (s) => {
|
|
35
|
+
stdout.write(s);
|
|
36
|
+
};
|
|
37
|
+
function emitJsonRpcError(id, code, message, writeOut) {
|
|
38
|
+
const payload = { jsonrpc: "2.0", id, error: { code, message } };
|
|
39
|
+
writeOut(`${JSON.stringify(payload)}
|
|
40
|
+
`);
|
|
41
|
+
}
|
|
42
|
+
async function forwardLine(line, fetchFn = fetch, writeOut = defaultWriteOut) {
|
|
43
|
+
const trimmed = line.trim();
|
|
44
|
+
if (!trimmed) return;
|
|
45
|
+
const id = parseId(trimmed);
|
|
46
|
+
let response;
|
|
47
|
+
try {
|
|
48
|
+
response = await fetchFn(ENDPOINT, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: {
|
|
51
|
+
"content-type": "application/json",
|
|
52
|
+
"user-agent": USER_AGENT,
|
|
53
|
+
accept: "application/json"
|
|
54
|
+
},
|
|
55
|
+
body: trimmed,
|
|
56
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
57
|
+
});
|
|
58
|
+
} catch (err) {
|
|
59
|
+
const reason = sanitizeReason(err instanceof Error ? err.message : String(err));
|
|
60
|
+
console.error(`[france-data-mcp-npm] forward failed: ${reason}`);
|
|
61
|
+
emitJsonRpcError(
|
|
62
|
+
id,
|
|
63
|
+
JSON_RPC_INTERNAL_ERROR,
|
|
64
|
+
`Network error forwarding to ${SAFE_ENDPOINT}: ${reason}`,
|
|
65
|
+
writeOut
|
|
66
|
+
);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
let text;
|
|
70
|
+
try {
|
|
71
|
+
text = await response.text();
|
|
72
|
+
} catch (err) {
|
|
73
|
+
const reason = sanitizeReason(err instanceof Error ? err.message : String(err));
|
|
74
|
+
console.error(`[france-data-mcp-npm] body stream interrupted: ${reason}`);
|
|
75
|
+
emitJsonRpcError(
|
|
76
|
+
id,
|
|
77
|
+
JSON_RPC_INTERNAL_ERROR,
|
|
78
|
+
`Body stream interrupted from ${SAFE_ENDPOINT}: ${reason}`,
|
|
79
|
+
writeOut
|
|
80
|
+
);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
emitJsonRpcError(
|
|
85
|
+
id,
|
|
86
|
+
JSON_RPC_INTERNAL_ERROR,
|
|
87
|
+
`Upstream HTTP ${response.status} from ${SAFE_ENDPOINT}: ${sanitizeReason(text.slice(0, 200))}`,
|
|
88
|
+
writeOut
|
|
89
|
+
);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (text.length > 0) writeOut(`${text.replace(/\n+$/u, "")}
|
|
93
|
+
`);
|
|
94
|
+
}
|
|
95
|
+
function safeEndpointForLog(endpoint) {
|
|
96
|
+
try {
|
|
97
|
+
const u = new URL(endpoint);
|
|
98
|
+
u.username = "";
|
|
99
|
+
u.password = "";
|
|
100
|
+
return u.toString();
|
|
101
|
+
} catch {
|
|
102
|
+
return endpoint;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
var SAFE_ENDPOINT = safeEndpointForLog(ENDPOINT);
|
|
106
|
+
function sanitizeReason(reason) {
|
|
107
|
+
return reason.replace(/(https?:\/\/)[^/\s@]+@/giu, "$1[redacted]@");
|
|
108
|
+
}
|
|
109
|
+
async function main() {
|
|
110
|
+
console.error(`[france-data-mcp-npm] v${VERSION} \u2192 ${SAFE_ENDPOINT}`);
|
|
111
|
+
const rl = createInterface({ input: stdin, crlfDelay: Number.POSITIVE_INFINITY });
|
|
112
|
+
for await (const line of rl) {
|
|
113
|
+
await forwardLine(line);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
var isMain = typeof process.argv[1] === "string" && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
117
|
+
if (isMain) {
|
|
118
|
+
main().catch((err) => {
|
|
119
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
120
|
+
console.error(`[france-data-mcp-npm] fatal: ${reason}`);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export { ENDPOINT, USER_AGENT, forwardLine, parseId, safeEndpointForLog };
|
|
126
|
+
//# sourceMappingURL=cli.js.map
|
|
127
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +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.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"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { Commune, GeocodeOptions, GeocodeResult, SearchCommunesOptions, TERRITOIRE_VERSION, geocode, geocodeMany, getCommuneByCode, reverseGeocode, searchCommunes } from './territoire/index.js';
|
|
2
|
+
export { AnsFhirPractitioner, ByCategorieInput, Dirigeant, Entreprise, Etablissement, EtablissementFiness, FINESS_CATEGORIES, FINESS_EHPAD, FINESS_FAMILY_CODES, FINESS_HOPITAUX, FINESS_LABOS, FINESS_MSP_CPTS, FINESS_PHARMACIES, FilterAnnuaireOptions, Finance, FinessCategorieCode, FinessFamille, FinessFamilleQuery, FinessQueryResult, FinessResult, InRadiusInput, LoadFinessOptions, NAF_EHPAD, NAF_LABOS, NAF_MEDECINE_VILLE, NAF_PHARMACIES, NAF_SANTE, NafCodeSante, ProfessionnelSante, RPPS_CGU_NOTICE, RPPS_MODE_EXERCICE, RppsDansEtablissementInput, RppsInRadiusInput, RppsLookupResult, RppsParSpecialiteDeptInput, RppsQueryResult, RppsResult, SANTE_VERSION, SearchEntreprisesOptions, SearchEntreprisesResult, SearchFinessOptions, StreamAnnuaireOptions, ensureAnnuaireAmeli, finessFamille, getAnsFhirApiKey, getAnsFhirBaseUrl, getEntrepriseBySiren, getFinessByCategorie, getFinessByNumFiness, getFinessInRadius, getInseeApiKey, getRppsById, getRppsDansEtablissement, getRppsInRadius, getRppsParSpecialiteDept, haversineDistance, libelleCategorieFiness, libelleNaf, loadFiness, loadProfessionnels, lookupPractitionerByRpps, lookupSirenViaInsee, searchEntreprises, searchEtablissementsFiness, streamProfessionnels } from './sante/index.js';
|
|
3
|
+
export { C as Coordinates, R as RateLimitOptions } from './types-6cvLQmuz.js';
|