@gera-services/mcp-gera-clinic 0.1.0 → 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/dist/server.js CHANGED
@@ -1,325 +1,257 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
- var server_exports = {};
20
- __export(server_exports, {
21
- server: () => server
1
+ /**
2
+ * GeraClinic MCP Server (stdio)
3
+ *
4
+ * Exposes GeraClinic's real UK healthcare-provider directory (Care Quality
5
+ * Commission registered locations) and its non-diagnostic health calculators to
6
+ * AI agents over the Model Context Protocol. Everything is computed locally from
7
+ * a bundled snapshot of real CQC data and pure reference math — no backend, no
8
+ * network, no auth required — so an agent (Claude, ChatGPT with tools, any MCP
9
+ * client) can find a care provider, read area care statistics, and run a health
10
+ * calculator entirely offline.
11
+ *
12
+ * Product: GeraClinic — https://geraclinic.com (a Gera Systems product)
13
+ * Data: Care Quality Commission www.cqc.org.uk, Open Government Licence v3.0.
14
+ */
15
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
16
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
17
+ import { z } from 'zod';
18
+ import { CQC_META, CQC_AUTHORITIES, CQC_ATTRIBUTION, SERVICE_TYPES, ALL_PROVIDERS, searchProviders, findArea, } from './cqc.js';
19
+ import { CALCULATORS, bmi, bmrTdee, idealWeightKg, bloodPressureCategory, heartRateZones, a1c, waterIntakeLitres, waistToHeight, } from './calculators.js';
20
+ export const server = new McpServer({
21
+ name: 'gera-clinic',
22
+ version: '1.0.0',
22
23
  });
23
- module.exports = __toCommonJS(server_exports);
24
- var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
25
- var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
26
- var import_zod = require("zod");
27
- const API_BASE = process.env.GERACLINIC_API_URL || "https://api.geraclinic.com";
28
- async function apiCall(path, options = {}) {
29
- const res = await fetch(`${API_BASE}${path}`, {
30
- ...options,
31
- headers: {
32
- "Content-Type": "application/json",
33
- "X-MCP-Client": "mcp-gera-clinic/1.0.0",
34
- ...options.headers
35
- }
36
- });
37
- if (!res.ok) {
38
- throw new Error(`API error ${res.status}: ${await res.text()}`);
39
- }
40
- return res.json();
24
+ function asText(payload) {
25
+ return {
26
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
27
+ };
41
28
  }
42
- const server = new import_mcp.McpServer({
43
- name: "gera-clinic",
44
- version: "1.0.0"
29
+ const CQC_DISCLAIMER = 'Directory of CQC-registered locations. CQC ratings are categorical (Outstanding/Good/Requires improvement/Inadequate), never numeric. Verify details on cqc.org.uk before relying on them. Source: Care Quality Commission, Open Government Licence v3.0.';
30
+ // ── Tool 1: find_care_provider ───────────────────────────────────────────────
31
+ // Search the real CQC dataset by name / postcode / service type / area.
32
+ server.registerTool('find_care_provider', {
33
+ title: 'Find a CQC-registered care or health provider',
34
+ description: "Search GeraClinic's directory of real Care Quality Commission (CQC) registered UK providers — GP surgeries, dentists, hospitals, clinics, care/nursing homes, hospices, urgent-care centres and more. Filter by name, postcode (full or outward, e.g. 'TN12'), service type (e.g. 'Dentist', 'Doctors/GPs', 'Hospital'), and/or local authority (e.g. 'kent'). Returns provider name, address, postcode, phone, website, service types and last-inspected date. All filters are AND-combined; omit any. Offline — real CQC records only.",
35
+ inputSchema: {
36
+ query: z.string().optional().describe('Provider or organisation name substring.'),
37
+ postcode: z
38
+ .string()
39
+ .optional()
40
+ .describe('Full postcode or outward code, e.g. "TN12 6AX" or "TN12".'),
41
+ serviceType: z
42
+ .string()
43
+ .optional()
44
+ .describe('Service type, e.g. "Dentist", "Doctors/GPs", "Hospital", "Nursing homes".'),
45
+ authority: z
46
+ .string()
47
+ .optional()
48
+ .describe('Local authority slug or name, e.g. "kent", "Essex".'),
49
+ limit: z.number().int().min(1).max(50).optional().describe('Max results (default 10).'),
50
+ },
51
+ }, async ({ query, postcode, serviceType, authority, limit }) => {
52
+ const results = searchProviders({
53
+ query,
54
+ postcode,
55
+ serviceType,
56
+ authority,
57
+ limit: limit ?? 10,
58
+ });
59
+ return asText({
60
+ query: {
61
+ query: query ?? null,
62
+ postcode: postcode ?? null,
63
+ serviceType: serviceType ?? null,
64
+ authority: authority ?? null,
65
+ },
66
+ count: results.length,
67
+ providers: results.map((r) => ({
68
+ cqc_location_id: r.provider.cqcLocationId,
69
+ name: r.provider.name,
70
+ provider_name: r.provider.providerName,
71
+ address: r.provider.address,
72
+ postcode: r.provider.postcode,
73
+ phone: r.provider.phone,
74
+ website: r.provider.website,
75
+ service_types: r.provider.serviceTypes,
76
+ last_inspected: r.provider.lastInspected,
77
+ cqc_profile_url: `https://www.cqc.org.uk/location/${r.provider.cqcLocationId}`,
78
+ authority: r.authorityName,
79
+ region: r.region,
80
+ })),
81
+ disclaimer: CQC_DISCLAIMER,
82
+ });
45
83
  });
46
- server.tool(
47
- "search_doctors",
48
- "Search for doctors by specialty, location, and availability. Returns a list of verified doctors with their ratings, languages, consultation fees, and appointment types (video, in-person, chat).",
49
- {
50
- specialty: import_zod.z.string().optional().describe('Medical specialty (e.g., "cardiology", "dermatology", "general-practice")'),
51
- country: import_zod.z.string().optional().describe('ISO 3166-1 alpha-2 country code (e.g., "GE", "UG", "GH")'),
52
- language: import_zod.z.string().optional().describe('Preferred language (e.g., "en", "ka", "fr")'),
53
- appointment_type: import_zod.z.enum(["VIDEO", "IN_PERSON", "CHAT"]).optional().describe("Type of consultation"),
54
- min_rating: import_zod.z.number().min(0).max(5).optional().describe("Minimum doctor rating (0-5)"),
55
- page: import_zod.z.number().int().positive().optional().describe("Page number for pagination"),
56
- limit: import_zod.z.number().int().min(1).max(50).optional().describe("Results per page (max 50)")
57
- },
58
- async (params) => {
59
- const query = new URLSearchParams();
60
- if (params.specialty) query.set("specialty", params.specialty);
61
- if (params.language) query.set("language", params.language);
62
- if (params.appointment_type) query.set("appointment_type", params.appointment_type);
63
- if (params.min_rating !== void 0) query.set("min_rating", String(params.min_rating));
64
- if (params.page) query.set("page", String(params.page));
65
- if (params.limit) query.set("limit", String(params.limit));
66
- const headers = {};
67
- if (params.country) headers["X-Tenant-ID"] = params.country;
68
- const result = await apiCall(`/doctors?${query}`, { headers });
69
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
70
- }
71
- );
72
- server.tool(
73
- "get_doctor_profile",
74
- "Get detailed profile for a specific doctor including bio, qualifications, languages, consultation fees, and supported appointment types.",
75
- {
76
- doctor_id: import_zod.z.string().uuid().describe("Doctor UUID"),
77
- country: import_zod.z.string().optional().describe("ISO 3166-1 alpha-2 country code")
78
- },
79
- async (params) => {
80
- const headers = {};
81
- if (params.country) headers["X-Tenant-ID"] = params.country;
82
- const result = await apiCall(`/doctors/${params.doctor_id}`, { headers });
83
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
84
- }
85
- );
86
- server.tool(
87
- "get_available_slots",
88
- "Get available appointment time slots for a specific doctor on a given date. Returns a list of open slots with start/end times.",
89
- {
90
- doctor_id: import_zod.z.string().uuid().describe("Doctor UUID"),
91
- date: import_zod.z.string().describe("Date in ISO format YYYY-MM-DD"),
92
- country: import_zod.z.string().optional().describe("ISO 3166-1 alpha-2 country code")
93
- },
94
- async (params) => {
95
- const headers = {};
96
- if (params.country) headers["X-Tenant-ID"] = params.country;
97
- const result = await apiCall(`/doctors/${params.doctor_id}/slots?date=${params.date}`, { headers });
98
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
99
- }
100
- );
101
- server.tool(
102
- "list_specialties",
103
- "List all medical specialties available in a given country. Useful for discovering what types of doctors are available before searching.",
104
- {
105
- country: import_zod.z.string().optional().describe("ISO 3166-1 alpha-2 country code")
106
- },
107
- async (params) => {
108
- const headers = {};
109
- if (params.country) headers["X-Tenant-ID"] = params.country;
110
- const result = await apiCall("/doctors/specialties", { headers });
111
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
112
- }
113
- );
114
- server.tool(
115
- "get_featured_doctors",
116
- "Get top-rated, verified doctors. Useful for recommending the best available doctors to users.",
117
- {
118
- country: import_zod.z.string().optional().describe("ISO 3166-1 alpha-2 country code"),
119
- limit: import_zod.z.number().int().min(1).max(20).optional().describe("Number of featured doctors to return")
120
- },
121
- async (params) => {
122
- const headers = {};
123
- if (params.country) headers["X-Tenant-ID"] = params.country;
124
- const query = params.limit ? `?limit=${params.limit}` : "";
125
- const result = await apiCall(`/doctors/featured${query}`, { headers });
126
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
127
- }
128
- );
129
- server.tool(
130
- "get_health_services",
131
- "Get all health service types available in a specific country on GeraClinic. Returns services such as telemedicine video consultations, in-person appointments, pharmacy delivery, lab test booking, and home nursing.",
132
- {
133
- country: import_zod.z.string().describe('ISO 3166-1 alpha-2 country code (e.g., "GB", "US", "GE", "KE")')
134
- },
135
- async (params) => {
136
- const headers = {
137
- "X-Tenant-ID": params.country
138
- };
139
- const result = await apiCall("/health-services", { headers });
140
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
141
- }
142
- );
143
- server.tool(
144
- "book_appointment",
145
- "Book a medical appointment with a doctor. Requires authentication. Returns booking confirmation with payment URL if applicable.",
146
- {
147
- doctor_id: import_zod.z.string().uuid().describe("Doctor UUID"),
148
- date: import_zod.z.string().describe("Appointment date in ISO format YYYY-MM-DD"),
149
- start_time: import_zod.z.string().describe("Start time in HH:MM format"),
150
- end_time: import_zod.z.string().describe("End time in HH:MM format"),
151
- appointment_type: import_zod.z.enum(["VIDEO", "IN_PERSON", "CHAT"]).describe("Type of consultation"),
152
- reason: import_zod.z.string().optional().describe("Reason for visit / symptoms"),
153
- country: import_zod.z.string().optional().describe("ISO 3166-1 alpha-2 country code"),
154
- auth_token: import_zod.z.string().describe("User bearer token for authentication")
155
- },
156
- async (params) => {
157
- const headers = {
158
- Authorization: `Bearer ${params.auth_token}`
159
- };
160
- if (params.country) headers["X-Tenant-ID"] = params.country;
161
- const result = await apiCall("/appointments", {
162
- method: "POST",
163
- headers,
164
- body: JSON.stringify({
165
- doctor_id: params.doctor_id,
166
- date: params.date,
167
- start_time: params.start_time,
168
- end_time: params.end_time,
169
- type: params.appointment_type,
170
- reason: params.reason
171
- })
84
+ // ── Tool 2: get_cqc_area_stats ───────────────────────────────────────────────
85
+ // Aggregated, real CQC statistics for an authority or locality.
86
+ server.registerTool('get_cqc_area_stats', {
87
+ title: 'Get CQC care statistics for a UK area',
88
+ description: "Return GeraClinic's aggregated Care Quality Commission statistics for a UK local authority or locality (e.g. 'kent', 'essex'). Includes total registered providers, how many publish a phone number / website, the breakdown by service type (GPs, dentists, care homes, hospitals, etc.) with counts and percentages, the most common service type, and the latest inspection date in the area. Use list_care_authorities first to discover valid area names. Real CQC data, offline.",
89
+ inputSchema: {
90
+ area: z
91
+ .string()
92
+ .describe('Authority or locality slug/name, e.g. "kent", "essex", "surrey".'),
93
+ },
94
+ }, async ({ area }) => {
95
+ const found = findArea(area);
96
+ if (!found) {
97
+ return asText({
98
+ error: `No CQC area found for "${area}". Call list_care_authorities to see valid areas.`,
99
+ });
100
+ }
101
+ const a = found.area;
102
+ return asText({
103
+ area: 'name' in a ? a.name : a.slug,
104
+ kind: found.kind,
105
+ authority: found.authorityName,
106
+ region: 'region' in a ? a.region : null,
107
+ stats: {
108
+ total_providers: a.stats.total,
109
+ with_phone: a.stats.withPhone,
110
+ with_phone_pct: a.stats.withPhonePct,
111
+ with_website: a.stats.withWebsite,
112
+ top_service_type: a.stats.topServiceType,
113
+ latest_inspection: a.stats.latestCheck,
114
+ service_type_breakdown: a.stats.serviceTypes.map((s) => ({
115
+ type: s.type,
116
+ count: s.count,
117
+ pct: s.pct,
118
+ })),
119
+ },
120
+ disclaimer: CQC_DISCLAIMER,
172
121
  });
173
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
174
- }
175
- );
176
- server.tool(
177
- "get_my_appointments",
178
- "List upcoming and past appointments for the authenticated patient. Returns appointments with doctor info, date/time, status, and consultation type. Use this to check a patient's appointment history or upcoming schedule.",
179
- {
180
- status: import_zod.z.enum(["upcoming", "completed", "cancelled"]).optional().describe("Filter by appointment status. Omit to return all."),
181
- from_date: import_zod.z.string().optional().describe("Filter appointments from this date (ISO format YYYY-MM-DD)"),
182
- to_date: import_zod.z.string().optional().describe("Filter appointments up to this date (ISO format YYYY-MM-DD)"),
183
- page: import_zod.z.number().int().positive().optional().describe("Page number for pagination"),
184
- limit: import_zod.z.number().int().min(1).max(50).optional().describe("Results per page (max 50)"),
185
- country: import_zod.z.string().optional().describe("ISO 3166-1 alpha-2 country code"),
186
- auth_token: import_zod.z.string().describe("User bearer token for authentication")
187
- },
188
- async (params) => {
189
- const headers = {
190
- Authorization: `Bearer ${params.auth_token}`
191
- };
192
- if (params.country) headers["X-Tenant-ID"] = params.country;
193
- const query = new URLSearchParams();
194
- if (params.status) query.set("status", params.status);
195
- if (params.from_date) query.set("from_date", params.from_date);
196
- if (params.to_date) query.set("to_date", params.to_date);
197
- if (params.page) query.set("page", String(params.page));
198
- if (params.limit) query.set("limit", String(params.limit));
199
- const result = await apiCall(`/appointments?${query}`, { headers });
200
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
201
- }
202
- );
203
- server.tool(
204
- "cancel_appointment",
205
- "Cancel an existing appointment. Requires authentication. Returns cancellation confirmation and refund details if applicable. Must be called at least 1 hour before the scheduled appointment time.",
206
- {
207
- appointment_id: import_zod.z.string().uuid().describe("Appointment UUID to cancel"),
208
- reason: import_zod.z.string().optional().describe("Reason for cancellation (helps improve the platform)"),
209
- country: import_zod.z.string().optional().describe("ISO 3166-1 alpha-2 country code"),
210
- auth_token: import_zod.z.string().describe("User bearer token for authentication")
211
- },
212
- async (params) => {
213
- const headers = {
214
- Authorization: `Bearer ${params.auth_token}`
215
- };
216
- if (params.country) headers["X-Tenant-ID"] = params.country;
217
- const result = await apiCall(`/appointments/${params.appointment_id}/cancel`, {
218
- method: "POST",
219
- headers,
220
- body: JSON.stringify({ reason: params.reason })
122
+ });
123
+ // ── Tool 3: list_care_authorities ────────────────────────────────────────────
124
+ // Discover the areas available, optionally filtered by region.
125
+ server.registerTool('list_care_authorities', {
126
+ title: 'List UK areas covered by the CQC directory',
127
+ description: "List the UK local authorities GeraClinic's CQC directory covers, with the number of registered providers and the top service type in each. Optionally filter by region (e.g. 'London', 'South East', 'North West'). Use this to find a valid `area` value for get_cqc_area_stats or an `authority` for find_care_provider.",
128
+ inputSchema: {
129
+ region: z.string().optional().describe('Filter by region, e.g. "London", "South East".'),
130
+ },
131
+ }, async ({ region }) => {
132
+ const rNorm = region ? region.toLowerCase().trim() : null;
133
+ const authorities = CQC_AUTHORITIES.filter((a) => !rNorm || (a.region ?? '').toLowerCase() === rNorm).map((a) => ({
134
+ slug: a.slug,
135
+ name: a.name,
136
+ region: a.region,
137
+ total_providers: a.stats.total,
138
+ top_service_type: a.stats.topServiceType,
139
+ localities: a.localities.length,
140
+ }));
141
+ return asText({
142
+ dataset: {
143
+ total_providers_indexed: CQC_META.totalProviders,
144
+ total_localities: CQC_META.totalLocalities,
145
+ generated_at: CQC_META.generatedAt,
146
+ attribution: CQC_ATTRIBUTION,
147
+ attribution_url: CQC_META.attributionUrl,
148
+ },
149
+ region_filter: region ?? null,
150
+ count: authorities.length,
151
+ service_types_available: SERVICE_TYPES,
152
+ authorities,
221
153
  });
222
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
223
- }
224
- );
225
- server.tool(
226
- "get_lab_results",
227
- "Retrieve lab test results for the authenticated patient. Returns digitally signed diagnostic reports including blood work, imaging, and other diagnostic results. Results are returned as structured data with normal ranges and doctor notes.",
228
- {
229
- result_id: import_zod.z.string().uuid().optional().describe("Specific lab result UUID. Omit to list all results."),
230
- from_date: import_zod.z.string().optional().describe("Filter results from this date (ISO format YYYY-MM-DD)"),
231
- to_date: import_zod.z.string().optional().describe("Filter results up to this date (ISO format YYYY-MM-DD)"),
232
- test_type: import_zod.z.string().optional().describe('Filter by test type (e.g., "blood-panel", "urine", "imaging", "ecg")'),
233
- page: import_zod.z.number().int().positive().optional().describe("Page number for pagination (when listing all results)"),
234
- limit: import_zod.z.number().int().min(1).max(50).optional().describe("Results per page (max 50)"),
235
- country: import_zod.z.string().optional().describe("ISO 3166-1 alpha-2 country code"),
236
- auth_token: import_zod.z.string().describe("User bearer token for authentication")
237
- },
238
- async (params) => {
239
- const headers = {
240
- Authorization: `Bearer ${params.auth_token}`
154
+ });
155
+ // ── Tool 4: list_health_calculators ──────────────────────────────────────────
156
+ server.registerTool('list_health_calculators', {
157
+ title: 'List GeraClinic health calculators',
158
+ description: 'List the health calculators available via run_health_calculator — BMI, BMR/TDEE calories, ideal weight, blood-pressure category, heart-rate zones, A1C↔glucose, water intake, and waist-to-height ratio — each with its id, required inputs, and the public reference standard it uses. All are objective and non-diagnostic.',
159
+ inputSchema: {},
160
+ }, async () => asText({
161
+ count: CALCULATORS.length,
162
+ calculators: CALCULATORS,
163
+ note: 'Objective reference math only educational, not a diagnosis or prescription.',
164
+ }));
165
+ // ── Tool 5: run_health_calculator ────────────────────────────────────────────
166
+ server.registerTool('run_health_calculator', {
167
+ title: 'Run a GeraClinic health calculator',
168
+ description: "Run one of GeraClinic's non-diagnostic health calculators and return the result. Pick `calculator` (see list_health_calculators) and pass the metric inputs it needs (heights in cm, weights in kg). Examples: bmi {weightKg, heightCm}; bmr_tdee {weightKg, heightCm, ageYears, sex, activity}; ideal_weight {heightCm, sex}; blood_pressure {systolic, diastolic}; heart_rate_zones {ageYears}; a1c {a1cPercent} or {avgGlucoseMgDl}; water_intake {weightKg, activityMinutes?}; waist_to_height {waistCm, heightCm}. Results are educational reference values, not medical advice.",
169
+ inputSchema: {
170
+ calculator: z
171
+ .enum([
172
+ 'bmi',
173
+ 'bmr_tdee',
174
+ 'ideal_weight',
175
+ 'blood_pressure',
176
+ 'heart_rate_zones',
177
+ 'a1c',
178
+ 'water_intake',
179
+ 'waist_to_height',
180
+ ])
181
+ .describe('Which calculator to run.'),
182
+ weightKg: z.number().positive().optional(),
183
+ heightCm: z.number().positive().optional(),
184
+ ageYears: z.number().int().positive().optional(),
185
+ sex: z.enum(['male', 'female']).optional(),
186
+ activity: z.enum(['sedentary', 'light', 'moderate', 'active', 'very_active']).optional(),
187
+ systolic: z.number().positive().optional(),
188
+ diastolic: z.number().positive().optional(),
189
+ a1cPercent: z.number().positive().optional(),
190
+ avgGlucoseMgDl: z.number().positive().optional(),
191
+ activityMinutes: z.number().min(0).optional(),
192
+ waistCm: z.number().positive().optional(),
193
+ },
194
+ }, async (args) => {
195
+ const need = (cond, msg) => {
196
+ if (!cond)
197
+ throw new Error(msg);
241
198
  };
242
- if (params.country) headers["X-Tenant-ID"] = params.country;
243
- if (params.result_id) {
244
- const result2 = await apiCall(`/lab-results/${params.result_id}`, { headers });
245
- return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
199
+ let result;
200
+ switch (args.calculator) {
201
+ case 'bmi':
202
+ need(args.weightKg != null && args.heightCm != null, 'bmi requires weightKg and heightCm');
203
+ result = bmi(args.weightKg, args.heightCm);
204
+ break;
205
+ case 'bmr_tdee':
206
+ need(args.weightKg != null &&
207
+ args.heightCm != null &&
208
+ args.ageYears != null &&
209
+ args.sex != null &&
210
+ args.activity != null, 'bmr_tdee requires weightKg, heightCm, ageYears, sex, activity');
211
+ result = bmrTdee(args.weightKg, args.heightCm, args.ageYears, args.sex, args.activity);
212
+ break;
213
+ case 'ideal_weight':
214
+ need(args.heightCm != null && args.sex != null, 'ideal_weight requires heightCm and sex');
215
+ result = idealWeightKg(args.heightCm, args.sex);
216
+ break;
217
+ case 'blood_pressure':
218
+ need(args.systolic != null && args.diastolic != null, 'blood_pressure requires systolic and diastolic');
219
+ result = bloodPressureCategory(args.systolic, args.diastolic);
220
+ break;
221
+ case 'heart_rate_zones':
222
+ need(args.ageYears != null, 'heart_rate_zones requires ageYears');
223
+ result = heartRateZones(args.ageYears);
224
+ break;
225
+ case 'a1c':
226
+ need(args.a1cPercent != null || args.avgGlucoseMgDl != null, 'a1c requires a1cPercent or avgGlucoseMgDl');
227
+ result = a1c(args.a1cPercent, args.avgGlucoseMgDl);
228
+ break;
229
+ case 'water_intake':
230
+ need(args.weightKg != null, 'water_intake requires weightKg');
231
+ result = waterIntakeLitres(args.weightKg, args.activityMinutes ?? 0);
232
+ break;
233
+ case 'waist_to_height':
234
+ need(args.waistCm != null && args.heightCm != null, 'waist_to_height requires waistCm and heightCm');
235
+ result = waistToHeight(args.waistCm, args.heightCm);
236
+ break;
246
237
  }
247
- const query = new URLSearchParams();
248
- if (params.from_date) query.set("from_date", params.from_date);
249
- if (params.to_date) query.set("to_date", params.to_date);
250
- if (params.test_type) query.set("test_type", params.test_type);
251
- if (params.page) query.set("page", String(params.page));
252
- if (params.limit) query.set("limit", String(params.limit));
253
- const result = await apiCall(`/lab-results?${query}`, { headers });
254
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
255
- }
256
- );
257
- server.tool(
258
- "order_medication",
259
- "Order medication from a pharmacy via GeraClinic. Can be linked to a doctor's prescription or ordered as OTC (over-the-counter). Requires authentication. Returns order confirmation with estimated delivery time and pharmacy details.",
260
- {
261
- prescription_id: import_zod.z.string().uuid().optional().describe("Prescription UUID from a GeraClinic doctor \u2014 required for prescription-only medications"),
262
- items: import_zod.z.array(import_zod.z.object({
263
- medication_name: import_zod.z.string().describe('Medication name (e.g., "Paracetamol 500mg", "Amoxicillin 250mg")'),
264
- quantity: import_zod.z.number().int().positive().describe("Number of units/packs to order"),
265
- notes: import_zod.z.string().optional().describe('Additional instructions (e.g., "brand preferred: X")')
266
- })).min(1).describe("List of medications to order"),
267
- delivery_address: import_zod.z.string().describe("Full delivery address for the medication"),
268
- pharmacy_id: import_zod.z.string().uuid().optional().describe("Preferred pharmacy UUID. If omitted, the nearest available pharmacy is selected automatically."),
269
- country: import_zod.z.string().optional().describe("ISO 3166-1 alpha-2 country code"),
270
- auth_token: import_zod.z.string().describe("User bearer token for authentication")
271
- },
272
- async (params) => {
273
- const headers = {
274
- Authorization: `Bearer ${params.auth_token}`
275
- };
276
- if (params.country) headers["X-Tenant-ID"] = params.country;
277
- const result = await apiCall("/pharmacy/orders", {
278
- method: "POST",
279
- headers,
280
- body: JSON.stringify({
281
- prescription_id: params.prescription_id,
282
- items: params.items,
283
- delivery_address: params.delivery_address,
284
- pharmacy_id: params.pharmacy_id
285
- })
238
+ return asText({
239
+ calculator: args.calculator,
240
+ result,
241
+ disclaimer: 'Educational reference value only — not a medical diagnosis, treatment, or prescription. Consult a clinician for personal advice.',
286
242
  });
287
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
288
- }
289
- );
290
- server.tool(
291
- "search_pharmacies",
292
- "Find pharmacies near a location that deliver medications via GeraClinic. Returns pharmacies with their operating hours, delivery radius, and available payment methods.",
293
- {
294
- lat: import_zod.z.number().optional().describe("Latitude for proximity search"),
295
- lng: import_zod.z.number().optional().describe("Longitude for proximity search"),
296
- city: import_zod.z.string().optional().describe("City name to search within"),
297
- country: import_zod.z.string().optional().describe("ISO 3166-1 alpha-2 country code"),
298
- open_now: import_zod.z.boolean().optional().describe("Filter to only show pharmacies currently open"),
299
- page: import_zod.z.number().int().positive().optional().describe("Page number"),
300
- limit: import_zod.z.number().int().min(1).max(50).optional().describe("Results per page (max 50)")
301
- },
302
- async (params) => {
303
- const headers = {};
304
- if (params.country) headers["X-Tenant-ID"] = params.country;
305
- const query = new URLSearchParams();
306
- if (params.lat !== void 0) query.set("lat", String(params.lat));
307
- if (params.lng !== void 0) query.set("lng", String(params.lng));
308
- if (params.city) query.set("city", params.city);
309
- if (params.open_now !== void 0) query.set("open_now", String(params.open_now));
310
- if (params.page) query.set("page", String(params.page));
311
- if (params.limit) query.set("limit", String(params.limit));
312
- const result = await apiCall(`/pharmacies?${query}`, { headers });
313
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
314
- }
315
- );
316
- async function main() {
317
- const transport = new import_stdio.StdioServerTransport();
318
- await server.connect(transport);
319
- console.error("GeraClinic MCP server running on stdio");
320
- }
321
- main().catch(console.error);
322
- // Annotate the CommonJS export names for ESM import in node:
323
- 0 && (module.exports = {
324
- server
325
243
  });
244
+ // ── Start ────────────────────────────────────────────────────────────────────
245
+ export async function main() {
246
+ const transport = new StdioServerTransport();
247
+ await server.connect(transport);
248
+ // stderr only — stdout is the MCP transport.
249
+ console.error(`GeraClinic MCP server running on stdio (gera-clinic v1.0.0) — ${ALL_PROVIDERS.length} CQC providers indexed`);
250
+ }
251
+ const isMain = typeof process !== 'undefined' && process.argv[1] && /server\.js$/.test(process.argv[1]);
252
+ if (isMain) {
253
+ main().catch((err) => {
254
+ console.error('Fatal:', err);
255
+ process.exit(1);
256
+ });
257
+ }