@gera-services/mcp-gera-clinic 0.1.1 → 1.0.0
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.md +70 -37
- package/bin/cli.js +11 -1
- package/dist/calculators.d.ts +108 -0
- package/dist/calculators.js +227 -0
- package/dist/cqc.d.ts +102 -0
- package/dist/cqc.js +102 -0
- package/dist/data/cqc-cluster.json +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/server.d.ts +17 -0
- package/dist/server.js +257 -0
- package/llms.txt +13 -22
- package/package.json +21 -20
- package/server.json +8 -13
package/README.md
CHANGED
|
@@ -1,80 +1,113 @@
|
|
|
1
|
-
#
|
|
1
|
+
# GeraClinic MCP Server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://geraclinic.com)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
An [MCP](https://modelcontextprotocol.io) server that lets AI agents — Claude
|
|
6
|
+
Desktop, ChatGPT with tools, Cursor, Windsurf, or any MCP client — **find
|
|
7
|
+
CQC-registered UK care and health providers**, read **area care statistics**,
|
|
8
|
+
and run **non-diagnostic health calculators**, fully offline.
|
|
9
|
+
|
|
10
|
+
It bundles a snapshot of GeraClinic's real **Care Quality Commission (CQC)**
|
|
11
|
+
provider directory — GP surgeries, dentists, hospitals, clinics, care/nursing
|
|
12
|
+
homes, hospices and more — plus GeraClinic's published health-calculator math.
|
|
13
|
+
**No backend, no network, no auth** — everything is computed locally, so it is
|
|
14
|
+
safe to run anywhere and gives the same answers the
|
|
15
|
+
[GeraClinic](https://geraclinic.com) product gives.
|
|
16
|
+
|
|
17
|
+
> Source: Care Quality Commission `www.cqc.org.uk`, licensed under the
|
|
18
|
+
> [Open Government Licence v3.0](https://www.cqc.org.uk/about-us/transparency/using-cqc-data).
|
|
19
|
+
> CQC ratings are categorical (Outstanding / Good / Requires improvement /
|
|
20
|
+
> Inadequate), never numeric. Health calculators are educational reference math,
|
|
21
|
+
> not medical advice.
|
|
6
22
|
|
|
7
23
|
## Tools
|
|
8
24
|
|
|
9
|
-
| Tool |
|
|
10
|
-
|
|
11
|
-
| `
|
|
12
|
-
| `
|
|
13
|
-
| `
|
|
14
|
-
| `
|
|
15
|
-
| `
|
|
16
|
-
|
|
17
|
-
|
|
25
|
+
| Tool | What it does |
|
|
26
|
+
|------|--------------|
|
|
27
|
+
| `find_care_provider` | Search real CQC-registered providers by name, postcode (full or outward), service type, and/or local authority. Returns address, phone, website, service types, last-inspected date, and the CQC profile URL. |
|
|
28
|
+
| `get_cqc_area_stats` | Aggregated CQC statistics for a UK authority or locality: total providers, phone/website coverage, service-type breakdown (GPs, dentists, care homes, hospitals…), top service type, latest inspection date. |
|
|
29
|
+
| `list_care_authorities` | List the UK areas the directory covers (optionally filtered by region) with provider counts and the available service types — use to discover valid `area` / `authority` values. |
|
|
30
|
+
| `list_health_calculators` | List the available health calculators with their inputs and the public reference standard each uses. |
|
|
31
|
+
| `run_health_calculator` | Run a calculator: BMI, BMR/TDEE calories, ideal weight, blood-pressure category, heart-rate zones, A1C↔glucose, water intake, or waist-to-height ratio. |
|
|
32
|
+
|
|
33
|
+
A typical agent flow: `list_care_authorities` → `find_care_provider`
|
|
34
|
+
(by area + service type) or `get_cqc_area_stats`; and separately
|
|
35
|
+
`list_health_calculators` → `run_health_calculator`.
|
|
18
36
|
|
|
19
|
-
##
|
|
37
|
+
## Install & run
|
|
20
38
|
|
|
21
39
|
```bash
|
|
22
|
-
|
|
23
|
-
|
|
40
|
+
# Run directly (no global install) once published to npm:
|
|
41
|
+
npx -y @gera-services/mcp-gera-clinic
|
|
24
42
|
|
|
25
|
-
|
|
43
|
+
# Or from this repo:
|
|
44
|
+
cd packages/mcp-gera-clinic
|
|
45
|
+
npm run build # tsc --noCheck -> dist/ (+ copies the CQC data file)
|
|
46
|
+
node bin/cli.js # starts on stdio
|
|
47
|
+
```
|
|
26
48
|
|
|
27
|
-
|
|
49
|
+
## Client configuration
|
|
28
50
|
|
|
29
|
-
|
|
51
|
+
### Claude Desktop / Claude Code (`claude_desktop_config.json`)
|
|
30
52
|
|
|
31
53
|
```json
|
|
32
54
|
{
|
|
33
55
|
"mcpServers": {
|
|
34
56
|
"gera-clinic": {
|
|
35
57
|
"command": "npx",
|
|
36
|
-
"args": ["@gera-services/mcp-gera-clinic"]
|
|
58
|
+
"args": ["-y", "@gera-services/mcp-gera-clinic"]
|
|
37
59
|
}
|
|
38
60
|
}
|
|
39
61
|
}
|
|
40
62
|
```
|
|
41
63
|
|
|
42
|
-
|
|
64
|
+
Local (unpublished) variant — point at the built CLI:
|
|
43
65
|
|
|
44
66
|
```json
|
|
45
67
|
{
|
|
46
68
|
"mcpServers": {
|
|
47
69
|
"gera-clinic": {
|
|
48
|
-
"command": "
|
|
49
|
-
"args": ["
|
|
50
|
-
"env": {
|
|
51
|
-
"GERACLINIC_API_URL": "https://your-api-url.example.com"
|
|
52
|
-
}
|
|
70
|
+
"command": "node",
|
|
71
|
+
"args": ["/Users/armen/Gera/packages/mcp-gera-clinic/bin/cli.js"]
|
|
53
72
|
}
|
|
54
73
|
}
|
|
55
74
|
}
|
|
56
75
|
```
|
|
57
76
|
|
|
58
|
-
###
|
|
77
|
+
### Cursor / Windsurf (`.cursor/mcp.json` etc.)
|
|
59
78
|
|
|
60
|
-
```
|
|
61
|
-
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"mcpServers": {
|
|
82
|
+
"gera-clinic": {
|
|
83
|
+
"command": "npx",
|
|
84
|
+
"args": ["-y", "@gera-services/mcp-gera-clinic"]
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
62
88
|
```
|
|
63
89
|
|
|
64
|
-
##
|
|
90
|
+
## Verify it works
|
|
65
91
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
92
|
+
```bash
|
|
93
|
+
npm run build
|
|
94
|
+
node scripts/smoke.mjs
|
|
95
|
+
```
|
|
69
96
|
|
|
70
|
-
|
|
97
|
+
The smoke test speaks raw MCP JSON-RPC over stdio (`initialize` → `tools/list`
|
|
98
|
+
→ several `tools/call`) and asserts the results. Expected output ends with
|
|
99
|
+
`ALL SMOKE CHECKS PASSED`.
|
|
71
100
|
|
|
72
|
-
|
|
101
|
+
## Example
|
|
73
102
|
|
|
74
|
-
|
|
103
|
+
Ask your agent: *"Find me a dentist in Kent, and what's the BMI for someone
|
|
104
|
+
82 kg and 178 cm?"*
|
|
75
105
|
|
|
76
|
-
|
|
106
|
+
The agent calls `find_care_provider` with
|
|
107
|
+
`{ authority: "kent", serviceType: "Dentist" }` and `run_health_calculator`
|
|
108
|
+
with `{ calculator: "bmi", weightKg: 82, heightCm: 178 }`, returning real CQC
|
|
109
|
+
provider records and a BMI of `25.9` (`Overweight`, WHO category).
|
|
77
110
|
|
|
78
111
|
## License
|
|
79
112
|
|
|
80
|
-
MIT
|
|
113
|
+
MIT © Gera Systems Ltd
|
package/bin/cli.js
CHANGED
|
@@ -1,2 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* CLI entry point for the GeraClinic MCP server.
|
|
4
|
+
* Starts the server on stdio. Intended to be launched by an MCP client
|
|
5
|
+
* (Claude Desktop, ChatGPT-with-tools, Cursor, etc.) — not run interactively.
|
|
6
|
+
*/
|
|
7
|
+
import { main } from '../dist/server.js';
|
|
8
|
+
|
|
9
|
+
main().catch((err) => {
|
|
10
|
+
console.error('Fatal:', err);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GeraClinic health calculators — pure, non-diagnostic reference math.
|
|
3
|
+
*
|
|
4
|
+
* These mirror GeraClinic's published health-tools library
|
|
5
|
+
* (apps/gera-clinic-web/src/lib/health-tools/calc.ts) exactly: same formulas,
|
|
6
|
+
* same WHO/NIH/ACC-AHA/ADAG reference ranges. Every function returns an
|
|
7
|
+
* OBJECTIVE number plus, where applicable, a public reference CATEGORY label.
|
|
8
|
+
* Nothing here diagnoses a condition or prescribes a dose — orientation only.
|
|
9
|
+
*/
|
|
10
|
+
export declare const LB_PER_KG = 2.2046226218;
|
|
11
|
+
export declare const CM_PER_INCH = 2.54;
|
|
12
|
+
export declare function cmToIn(cm: number): number;
|
|
13
|
+
export type BmiCategory = 'Underweight' | 'Healthy weight' | 'Overweight' | 'Obesity';
|
|
14
|
+
export declare function bmiCategory(value: number): BmiCategory;
|
|
15
|
+
export declare function bmi(weightKg: number, heightCm: number): {
|
|
16
|
+
bmi: number;
|
|
17
|
+
category: BmiCategory;
|
|
18
|
+
healthyWeightRangeKg: {
|
|
19
|
+
minKg: number;
|
|
20
|
+
maxKg: number;
|
|
21
|
+
};
|
|
22
|
+
reference: string;
|
|
23
|
+
};
|
|
24
|
+
export declare function healthyWeightRangeKg(heightCm: number): {
|
|
25
|
+
minKg: number;
|
|
26
|
+
maxKg: number;
|
|
27
|
+
};
|
|
28
|
+
export type Sex = 'male' | 'female';
|
|
29
|
+
export type ActivityLevel = 'sedentary' | 'light' | 'moderate' | 'active' | 'very_active';
|
|
30
|
+
export declare const ACTIVITY_FACTORS: Record<ActivityLevel, number>;
|
|
31
|
+
export declare function bmrValue(weightKg: number, heightCm: number, ageYears: number, sex: Sex): number;
|
|
32
|
+
export declare function bmrTdee(weightKg: number, heightCm: number, ageYears: number, sex: Sex, activity: ActivityLevel): {
|
|
33
|
+
bmrKcalPerDay: number;
|
|
34
|
+
tdeeKcalPerDay: number;
|
|
35
|
+
activityFactor: number;
|
|
36
|
+
reference: string;
|
|
37
|
+
};
|
|
38
|
+
export declare function idealWeightKg(heightCm: number, sex: Sex): {
|
|
39
|
+
reference: string;
|
|
40
|
+
devineKg: number;
|
|
41
|
+
robinsonKg: number;
|
|
42
|
+
millerKg: number;
|
|
43
|
+
hamwiKg: number;
|
|
44
|
+
};
|
|
45
|
+
export type BpCategory = 'Normal' | 'Elevated' | 'Hypertension stage 1' | 'Hypertension stage 2' | 'Hypertensive crisis';
|
|
46
|
+
export declare function bloodPressureCategory(systolic: number, diastolic: number): {
|
|
47
|
+
systolic: number;
|
|
48
|
+
diastolic: number;
|
|
49
|
+
category: BpCategory;
|
|
50
|
+
reference: string;
|
|
51
|
+
};
|
|
52
|
+
export declare function heartRateZones(ageYears: number): {
|
|
53
|
+
estimatedMaxHr: number;
|
|
54
|
+
zones: {
|
|
55
|
+
description: string;
|
|
56
|
+
lowBpm: number;
|
|
57
|
+
highBpm: number;
|
|
58
|
+
name: string;
|
|
59
|
+
}[];
|
|
60
|
+
reference: string;
|
|
61
|
+
};
|
|
62
|
+
export declare const MGDL_PER_MMOL = 18.0182;
|
|
63
|
+
export declare function a1c(a1cPercent?: number, avgGlucoseMgDl?: number): {
|
|
64
|
+
a1cPercent: number;
|
|
65
|
+
estAvgGlucoseMgDl: number;
|
|
66
|
+
estAvgGlucoseMmolL: number;
|
|
67
|
+
reference: string;
|
|
68
|
+
avgGlucoseMgDl?: undefined;
|
|
69
|
+
estA1cPercent?: undefined;
|
|
70
|
+
avgGlucoseMmolL?: undefined;
|
|
71
|
+
error?: undefined;
|
|
72
|
+
} | {
|
|
73
|
+
avgGlucoseMgDl: number;
|
|
74
|
+
estA1cPercent: number;
|
|
75
|
+
avgGlucoseMmolL: number;
|
|
76
|
+
reference: string;
|
|
77
|
+
a1cPercent?: undefined;
|
|
78
|
+
estAvgGlucoseMgDl?: undefined;
|
|
79
|
+
estAvgGlucoseMmolL?: undefined;
|
|
80
|
+
error?: undefined;
|
|
81
|
+
} | {
|
|
82
|
+
error: string;
|
|
83
|
+
a1cPercent?: undefined;
|
|
84
|
+
estAvgGlucoseMgDl?: undefined;
|
|
85
|
+
estAvgGlucoseMmolL?: undefined;
|
|
86
|
+
reference?: undefined;
|
|
87
|
+
avgGlucoseMgDl?: undefined;
|
|
88
|
+
estA1cPercent?: undefined;
|
|
89
|
+
avgGlucoseMmolL?: undefined;
|
|
90
|
+
};
|
|
91
|
+
export declare function waterIntakeLitres(weightKg: number, activityMinutes?: number): {
|
|
92
|
+
litresPerDay: number;
|
|
93
|
+
reference: string;
|
|
94
|
+
};
|
|
95
|
+
export type WaistHeightCategory = 'Low' | 'Healthy' | 'Increased' | 'High';
|
|
96
|
+
export declare function waistToHeight(waistCm: number, heightCm: number): {
|
|
97
|
+
ratio: number;
|
|
98
|
+
category: WaistHeightCategory;
|
|
99
|
+
reference: string;
|
|
100
|
+
};
|
|
101
|
+
export interface CalculatorInfo {
|
|
102
|
+
id: string;
|
|
103
|
+
title: string;
|
|
104
|
+
description: string;
|
|
105
|
+
inputs: string[];
|
|
106
|
+
reference: string;
|
|
107
|
+
}
|
|
108
|
+
export declare const CALCULATORS: CalculatorInfo[];
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GeraClinic health calculators — pure, non-diagnostic reference math.
|
|
3
|
+
*
|
|
4
|
+
* These mirror GeraClinic's published health-tools library
|
|
5
|
+
* (apps/gera-clinic-web/src/lib/health-tools/calc.ts) exactly: same formulas,
|
|
6
|
+
* same WHO/NIH/ACC-AHA/ADAG reference ranges. Every function returns an
|
|
7
|
+
* OBJECTIVE number plus, where applicable, a public reference CATEGORY label.
|
|
8
|
+
* Nothing here diagnoses a condition or prescribes a dose — orientation only.
|
|
9
|
+
*/
|
|
10
|
+
export const LB_PER_KG = 2.2046226218;
|
|
11
|
+
export const CM_PER_INCH = 2.54;
|
|
12
|
+
export function cmToIn(cm) {
|
|
13
|
+
return cm / CM_PER_INCH;
|
|
14
|
+
}
|
|
15
|
+
export function bmiCategory(value) {
|
|
16
|
+
if (value < 18.5)
|
|
17
|
+
return 'Underweight';
|
|
18
|
+
if (value < 25)
|
|
19
|
+
return 'Healthy weight';
|
|
20
|
+
if (value < 30)
|
|
21
|
+
return 'Overweight';
|
|
22
|
+
return 'Obesity';
|
|
23
|
+
}
|
|
24
|
+
export function bmi(weightKg, heightCm) {
|
|
25
|
+
const m = heightCm / 100;
|
|
26
|
+
const value = Math.round((weightKg / (m * m)) * 10) / 10;
|
|
27
|
+
const range = healthyWeightRangeKg(heightCm);
|
|
28
|
+
return {
|
|
29
|
+
bmi: value,
|
|
30
|
+
category: bmiCategory(value),
|
|
31
|
+
healthyWeightRangeKg: range,
|
|
32
|
+
reference: 'WHO adult BMI categories (educational).',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function healthyWeightRangeKg(heightCm) {
|
|
36
|
+
const m = heightCm / 100;
|
|
37
|
+
return {
|
|
38
|
+
minKg: Math.round(18.5 * m * m * 10) / 10,
|
|
39
|
+
maxKg: Math.round(24.9 * m * m * 10) / 10,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export const ACTIVITY_FACTORS = {
|
|
43
|
+
sedentary: 1.2,
|
|
44
|
+
light: 1.375,
|
|
45
|
+
moderate: 1.55,
|
|
46
|
+
active: 1.725,
|
|
47
|
+
very_active: 1.9,
|
|
48
|
+
};
|
|
49
|
+
export function bmrValue(weightKg, heightCm, ageYears, sex) {
|
|
50
|
+
const base = 10 * weightKg + 6.25 * heightCm - 5 * ageYears;
|
|
51
|
+
return Math.round(sex === 'male' ? base + 5 : base - 161);
|
|
52
|
+
}
|
|
53
|
+
export function bmrTdee(weightKg, heightCm, ageYears, sex, activity) {
|
|
54
|
+
const basal = bmrValue(weightKg, heightCm, ageYears, sex);
|
|
55
|
+
return {
|
|
56
|
+
bmrKcalPerDay: basal,
|
|
57
|
+
tdeeKcalPerDay: Math.round(basal * ACTIVITY_FACTORS[activity]),
|
|
58
|
+
activityFactor: ACTIVITY_FACTORS[activity],
|
|
59
|
+
reference: 'Mifflin-St Jeor equation (educational estimate).',
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
// ── Ideal weight (Devine / Robinson / Miller / Hamwi) ────────────────────────
|
|
63
|
+
export function idealWeightKg(heightCm, sex) {
|
|
64
|
+
const inchesOver5ft = Math.max(0, cmToIn(heightCm) - 60);
|
|
65
|
+
const r = (n) => Math.round(n * 10) / 10;
|
|
66
|
+
const result = sex === 'male'
|
|
67
|
+
? {
|
|
68
|
+
devineKg: r(50 + 2.3 * inchesOver5ft),
|
|
69
|
+
robinsonKg: r(52 + 1.9 * inchesOver5ft),
|
|
70
|
+
millerKg: r(56.2 + 1.41 * inchesOver5ft),
|
|
71
|
+
hamwiKg: r(48 + 2.7 * inchesOver5ft),
|
|
72
|
+
}
|
|
73
|
+
: {
|
|
74
|
+
devineKg: r(45.5 + 2.3 * inchesOver5ft),
|
|
75
|
+
robinsonKg: r(49 + 1.7 * inchesOver5ft),
|
|
76
|
+
millerKg: r(53.1 + 1.36 * inchesOver5ft),
|
|
77
|
+
hamwiKg: r(45.5 + 2.2 * inchesOver5ft),
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
...result,
|
|
81
|
+
reference: 'Classic clinical reference formulas (Devine, Robinson, Miller, Hamwi). Population estimate, not a personal target.',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export function bloodPressureCategory(systolic, diastolic) {
|
|
85
|
+
let category;
|
|
86
|
+
if (systolic > 180 || diastolic > 120)
|
|
87
|
+
category = 'Hypertensive crisis';
|
|
88
|
+
else if (systolic >= 140 || diastolic >= 90)
|
|
89
|
+
category = 'Hypertension stage 2';
|
|
90
|
+
else if (systolic >= 130 || diastolic >= 80)
|
|
91
|
+
category = 'Hypertension stage 1';
|
|
92
|
+
else if (systolic >= 120 && diastolic < 80)
|
|
93
|
+
category = 'Elevated';
|
|
94
|
+
else
|
|
95
|
+
category = 'Normal';
|
|
96
|
+
return {
|
|
97
|
+
systolic,
|
|
98
|
+
diastolic,
|
|
99
|
+
category,
|
|
100
|
+
reference: 'ACC/AHA 2017 reference categories (educational). Does not diagnose hypertension — that needs repeated clinical measurement.',
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// ── Heart-rate training zones (220 - age) ────────────────────────────────────
|
|
104
|
+
export function heartRateZones(ageYears) {
|
|
105
|
+
const maxHr = 220 - ageYears;
|
|
106
|
+
const z = (lo, hi) => ({
|
|
107
|
+
lowBpm: Math.round(maxHr * lo),
|
|
108
|
+
highBpm: Math.round(maxHr * hi),
|
|
109
|
+
});
|
|
110
|
+
return {
|
|
111
|
+
estimatedMaxHr: maxHr,
|
|
112
|
+
zones: [
|
|
113
|
+
{ name: 'Zone 1 — Very light', ...z(0.5, 0.6), description: 'Warm-up and recovery' },
|
|
114
|
+
{ name: 'Zone 2 — Light', ...z(0.6, 0.7), description: 'Fat burning, base endurance' },
|
|
115
|
+
{ name: 'Zone 3 — Moderate', ...z(0.7, 0.8), description: 'Aerobic fitness' },
|
|
116
|
+
{ name: 'Zone 4 — Hard', ...z(0.8, 0.9), description: 'Increased performance' },
|
|
117
|
+
{ name: 'Zone 5 — Maximum', ...z(0.9, 1.0), description: 'Maximal effort, short bursts' },
|
|
118
|
+
],
|
|
119
|
+
reference: 'Estimated max HR = 220 − age, with the 5 standard training zones (educational).',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// ── A1C <-> average glucose (ADAG) ───────────────────────────────────────────
|
|
123
|
+
export const MGDL_PER_MMOL = 18.0182;
|
|
124
|
+
export function a1c(a1cPercent, avgGlucoseMgDl) {
|
|
125
|
+
if (a1cPercent != null) {
|
|
126
|
+
const mgDl = Math.round(28.7 * a1cPercent - 46.7);
|
|
127
|
+
return {
|
|
128
|
+
a1cPercent,
|
|
129
|
+
estAvgGlucoseMgDl: mgDl,
|
|
130
|
+
estAvgGlucoseMmolL: Math.round((mgDl / MGDL_PER_MMOL) * 10) / 10,
|
|
131
|
+
reference: 'ADAG formula: eAG(mg/dL) = 28.7 × A1C − 46.7 (educational).',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (avgGlucoseMgDl != null) {
|
|
135
|
+
return {
|
|
136
|
+
avgGlucoseMgDl,
|
|
137
|
+
estA1cPercent: Math.round(((avgGlucoseMgDl + 46.7) / 28.7) * 10) / 10,
|
|
138
|
+
avgGlucoseMmolL: Math.round((avgGlucoseMgDl / MGDL_PER_MMOL) * 10) / 10,
|
|
139
|
+
reference: 'ADAG formula inverted (educational).',
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return { error: 'Provide either a1cPercent or avgGlucoseMgDl.' };
|
|
143
|
+
}
|
|
144
|
+
// ── Water intake ─────────────────────────────────────────────────────────────
|
|
145
|
+
export function waterIntakeLitres(weightKg, activityMinutes = 0) {
|
|
146
|
+
const base = weightKg * 0.035;
|
|
147
|
+
const activityBump = (activityMinutes / 30) * 0.35;
|
|
148
|
+
return {
|
|
149
|
+
litresPerDay: Math.round((base + activityBump) * 100) / 100,
|
|
150
|
+
reference: '~35 ml per kg body weight plus a small activity bump (educational rule of thumb).',
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
export function waistToHeight(waistCm, heightCm) {
|
|
154
|
+
const ratio = Math.round((waistCm / heightCm) * 100) / 100;
|
|
155
|
+
let category;
|
|
156
|
+
if (ratio < 0.4)
|
|
157
|
+
category = 'Low';
|
|
158
|
+
else if (ratio < 0.5)
|
|
159
|
+
category = 'Healthy';
|
|
160
|
+
else if (ratio < 0.6)
|
|
161
|
+
category = 'Increased';
|
|
162
|
+
else
|
|
163
|
+
category = 'High';
|
|
164
|
+
return {
|
|
165
|
+
ratio,
|
|
166
|
+
category,
|
|
167
|
+
reference: 'Waist-to-height ratio reference bands (educational).',
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
export const CALCULATORS = [
|
|
171
|
+
{
|
|
172
|
+
id: 'bmi',
|
|
173
|
+
title: 'Body Mass Index',
|
|
174
|
+
description: 'BMI value, WHO category, and the healthy-weight range for the height.',
|
|
175
|
+
inputs: ['weightKg', 'heightCm'],
|
|
176
|
+
reference: 'WHO adult BMI ranges',
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
id: 'bmr_tdee',
|
|
180
|
+
title: 'BMR & TDEE (calories)',
|
|
181
|
+
description: 'Basal metabolic rate and total daily energy expenditure.',
|
|
182
|
+
inputs: ['weightKg', 'heightCm', 'ageYears', 'sex', 'activity'],
|
|
183
|
+
reference: 'Mifflin-St Jeor',
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
id: 'ideal_weight',
|
|
187
|
+
title: 'Ideal body weight',
|
|
188
|
+
description: 'Devine / Robinson / Miller / Hamwi reference estimates (kg).',
|
|
189
|
+
inputs: ['heightCm', 'sex'],
|
|
190
|
+
reference: 'Devine, Robinson, Miller, Hamwi formulas',
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
id: 'blood_pressure',
|
|
194
|
+
title: 'Blood-pressure category',
|
|
195
|
+
description: 'Maps a systolic/diastolic reading to its ACC/AHA reference category.',
|
|
196
|
+
inputs: ['systolic', 'diastolic'],
|
|
197
|
+
reference: 'ACC/AHA 2017 categories',
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
id: 'heart_rate_zones',
|
|
201
|
+
title: 'Heart-rate training zones',
|
|
202
|
+
description: 'Estimated max HR (220 − age) and the 5 standard training zones.',
|
|
203
|
+
inputs: ['ageYears'],
|
|
204
|
+
reference: '220 − age',
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
id: 'a1c',
|
|
208
|
+
title: 'A1C ↔ average glucose',
|
|
209
|
+
description: 'Convert A1C% to estimated average glucose, or the reverse.',
|
|
210
|
+
inputs: ['a1cPercent OR avgGlucoseMgDl'],
|
|
211
|
+
reference: 'ADAG study formula',
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
id: 'water_intake',
|
|
215
|
+
title: 'Daily water intake',
|
|
216
|
+
description: 'General daily water guidance from body weight and activity minutes.',
|
|
217
|
+
inputs: ['weightKg', 'activityMinutes?'],
|
|
218
|
+
reference: '~35 ml/kg rule of thumb',
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
id: 'waist_to_height',
|
|
222
|
+
title: 'Waist-to-height ratio',
|
|
223
|
+
description: 'Ratio plus its reference band.',
|
|
224
|
+
inputs: ['waistCm', 'heightCm'],
|
|
225
|
+
reference: 'Waist-to-height ratio bands',
|
|
226
|
+
},
|
|
227
|
+
];
|
package/dist/cqc.d.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real Care Quality Commission (CQC) provider dataset, bundled as JSON.
|
|
3
|
+
*
|
|
4
|
+
* Source: Care Quality Commission www.cqc.org.uk, crawled into GeraClinic's
|
|
5
|
+
* `crawled_listings` (vertical='geraclinic', country_code='GB') and snapshotted
|
|
6
|
+
* from apps/gera-clinic-web/src/data/cqc-cluster.generated.ts. Every record
|
|
7
|
+
* traces to a real registered provider. Public sector information licensed
|
|
8
|
+
* under the Open Government Licence v3.0.
|
|
9
|
+
*
|
|
10
|
+
* CQC publishes a CATEGORICAL rating scheme
|
|
11
|
+
* (Outstanding / Good / Requires improvement / Inadequate), never a 0–5 numeric
|
|
12
|
+
* scale, so this dataset carries NO numeric ratings. The `lastInspected` date
|
|
13
|
+
* is the real last-checked date for the provider record.
|
|
14
|
+
*/
|
|
15
|
+
export interface CqcServiceTypeBucket {
|
|
16
|
+
type: string;
|
|
17
|
+
count: number;
|
|
18
|
+
pct: number;
|
|
19
|
+
}
|
|
20
|
+
export interface CqcStats {
|
|
21
|
+
total: number;
|
|
22
|
+
withPhone: number;
|
|
23
|
+
withPhonePct: number;
|
|
24
|
+
withWebsite: number;
|
|
25
|
+
serviceTypes: CqcServiceTypeBucket[];
|
|
26
|
+
topServiceType: string | null;
|
|
27
|
+
latestCheck: string | null;
|
|
28
|
+
}
|
|
29
|
+
export interface CqcProvider {
|
|
30
|
+
cqcLocationId: string;
|
|
31
|
+
name: string;
|
|
32
|
+
providerName: string | null;
|
|
33
|
+
address: string | null;
|
|
34
|
+
postcode: string | null;
|
|
35
|
+
phone: string | null;
|
|
36
|
+
website: string | null;
|
|
37
|
+
serviceTypes: string[];
|
|
38
|
+
lastInspected: string | null;
|
|
39
|
+
}
|
|
40
|
+
export interface CqcLocality {
|
|
41
|
+
slug: string;
|
|
42
|
+
outwardCode: string;
|
|
43
|
+
stats: CqcStats;
|
|
44
|
+
providers: CqcProvider[];
|
|
45
|
+
}
|
|
46
|
+
export interface CqcAuthority {
|
|
47
|
+
slug: string;
|
|
48
|
+
name: string;
|
|
49
|
+
region: string | null;
|
|
50
|
+
stats: CqcStats;
|
|
51
|
+
providers: CqcProvider[];
|
|
52
|
+
localities: CqcLocality[];
|
|
53
|
+
}
|
|
54
|
+
export interface CqcDataset {
|
|
55
|
+
meta: {
|
|
56
|
+
generatedAt: string;
|
|
57
|
+
totalProviders: number;
|
|
58
|
+
totalLocalities: number;
|
|
59
|
+
attribution: string;
|
|
60
|
+
url: string;
|
|
61
|
+
attributionUrl: string;
|
|
62
|
+
};
|
|
63
|
+
authorities: CqcAuthority[];
|
|
64
|
+
}
|
|
65
|
+
export declare const CQC_META: {
|
|
66
|
+
generatedAt: string;
|
|
67
|
+
totalProviders: number;
|
|
68
|
+
totalLocalities: number;
|
|
69
|
+
attribution: string;
|
|
70
|
+
url: string;
|
|
71
|
+
attributionUrl: string;
|
|
72
|
+
};
|
|
73
|
+
export declare const CQC_AUTHORITIES: CqcAuthority[];
|
|
74
|
+
export declare const CQC_ATTRIBUTION: string;
|
|
75
|
+
export declare const ALL_PROVIDERS: CqcProvider[];
|
|
76
|
+
/** All distinct service-type labels present in the dataset, sorted. */
|
|
77
|
+
export declare const SERVICE_TYPES: string[];
|
|
78
|
+
export interface ProviderSearchParams {
|
|
79
|
+
query?: string;
|
|
80
|
+
postcode?: string;
|
|
81
|
+
serviceType?: string;
|
|
82
|
+
authority?: string;
|
|
83
|
+
limit: number;
|
|
84
|
+
}
|
|
85
|
+
export interface ProviderSearchResult {
|
|
86
|
+
provider: CqcProvider;
|
|
87
|
+
authoritySlug: string;
|
|
88
|
+
authorityName: string;
|
|
89
|
+
region: string | null;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Search the real CQC dataset. All filters are AND-combined; any omitted.
|
|
93
|
+
* Matching is case-insensitive substring for name/serviceType, and outward- or
|
|
94
|
+
* full-postcode prefix for postcode.
|
|
95
|
+
*/
|
|
96
|
+
export declare function searchProviders(params: ProviderSearchParams): ProviderSearchResult[];
|
|
97
|
+
/** Look up one authority or locality area by slug or name. */
|
|
98
|
+
export declare function findArea(slugOrName: string): {
|
|
99
|
+
kind: 'authority' | 'locality';
|
|
100
|
+
area: CqcAuthority | CqcLocality;
|
|
101
|
+
authorityName: string;
|
|
102
|
+
} | null;
|