@tkawen/ambulancedz-mcp 1.1.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.
Files changed (3) hide show
  1. package/README.md +55 -0
  2. package/package.json +34 -0
  3. package/src/index.js +168 -0
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # @tkawen/ambulancedz-mcp
2
+
3
+ MCP server for **[Ambulance.dz](https://ambulancedz.com)** — Algeria's 69-wilaya
4
+ ambulance / medical-transport marketplace. Lets any MCP client (Claude Desktop,
5
+ Claude Code, etc.) find ambulances, locate the nearest available one by GPS,
6
+ estimate a trip's cost, and read live platform stats.
7
+
8
+ ## Tools
9
+
10
+ | Tool | Description |
11
+ |------|-------------|
12
+ | `list_wilayas` | All 69 wilayas with driver counts + how many are available now |
13
+ | `list_communes` | Communes of a wilaya (by id) |
14
+ | `search_ambulances` | Search drivers by wilaya / commune / service type / status (verified first) |
15
+ | `find_nearest_ambulance` | Nearest **available** ambulances to a GPS point, with distance + ETA |
16
+ | `get_ambulance` | Full details of one driver by id |
17
+ | `estimate_price` | Estimated DZD cost range for a trip between two wilayas |
18
+ | `platform_stats` | Drivers / verified / available / coverage / avg rating |
19
+
20
+ It talks to the public read-only JSON API at `{BASE}/api/v1`. No credentials needed.
21
+
22
+ ## Install & run
23
+
24
+ ```bash
25
+ npm install
26
+ node src/index.js # stdio MCP server
27
+ npm test # smoke test against the live API
28
+ ```
29
+
30
+ ## Configure
31
+
32
+ | Env | Default | Purpose |
33
+ |-----|---------|---------|
34
+ | `AMBULANCEDZ_BASE_URL` | `https://ambulancedz.com` | Target platform base URL |
35
+
36
+ ## Use with Claude Desktop / Claude Code
37
+
38
+ Add to your MCP config (e.g. `claude_desktop_config.json`):
39
+
40
+ ```json
41
+ {
42
+ "mcpServers": {
43
+ "ambulancedz": {
44
+ "command": "node",
45
+ "args": ["D:/PROJECTS/05-OTHER-PROJECTS/ambulancedz-mcp/src/index.js"]
46
+ }
47
+ }
48
+ }
49
+ ```
50
+
51
+ Or, once published: `"command": "npx", "args": ["-y", "@tkawen/ambulancedz-mcp"]`.
52
+
53
+ ## License
54
+
55
+ MIT © TKAWEN
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@tkawen/ambulancedz-mcp",
3
+ "version": "1.1.0",
4
+ "description": "MCP server for Ambulance.dz — find ambulances, locate the nearest available one, estimate trip cost, and query Algeria's 69-wilaya emergency-transport marketplace.",
5
+ "type": "module",
6
+ "bin": {
7
+ "ambulancedz-mcp": "src/index.js"
8
+ },
9
+ "main": "src/index.js",
10
+ "files": [
11
+ "src",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "scripts": {
18
+ "start": "node src/index.js",
19
+ "test": "node test/smoke.js"
20
+ },
21
+ "keywords": [
22
+ "mcp",
23
+ "model-context-protocol",
24
+ "ambulance",
25
+ "algeria",
26
+ "ambulancedz",
27
+ "tkawen"
28
+ ],
29
+ "license": "MIT",
30
+ "dependencies": {
31
+ "@modelcontextprotocol/sdk": "^1.12.0",
32
+ "zod": "^3.23.8"
33
+ }
34
+ }
package/src/index.js ADDED
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Ambulance.dz MCP server
4
+ * ------------------------
5
+ * Exposes Algeria's ambulance-transport marketplace (ambulancedz.com) as MCP
6
+ * tools: list wilayas/communes, search drivers, find the nearest available
7
+ * ambulance by GPS, estimate trip cost, and read platform stats.
8
+ *
9
+ * Talks to the public read-only JSON API at {BASE}/api/v1.
10
+ * Configure the target with AMBULANCEDZ_BASE_URL (default https://ambulancedz.com).
11
+ */
12
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
+ import { z } from "zod";
15
+
16
+ const BASE = (process.env.AMBULANCEDZ_BASE_URL || "https://ambulancedz.com").replace(/\/+$/, "");
17
+ const API = BASE + "/api/v1";
18
+
19
+ async function api(path, params) {
20
+ const url = new URL(API + path);
21
+ if (params) {
22
+ for (const [k, v] of Object.entries(params)) {
23
+ if (v !== undefined && v !== null && v !== "") url.searchParams.set(k, String(v));
24
+ }
25
+ }
26
+ const res = await fetch(url, { headers: { Accept: "application/json" } });
27
+ const text = await res.text();
28
+ let json;
29
+ try {
30
+ json = JSON.parse(text);
31
+ } catch {
32
+ json = { raw: text.slice(0, 500) };
33
+ }
34
+ if (!res.ok) {
35
+ const msg = json?.error || json?.message || JSON.stringify(json);
36
+ throw new Error(`HTTP ${res.status}: ${msg}`);
37
+ }
38
+ return json;
39
+ }
40
+
41
+ async function apiPost(path, body) {
42
+ const res = await fetch(API + path, {
43
+ method: "POST",
44
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
45
+ body: JSON.stringify(body),
46
+ });
47
+ const text = await res.text();
48
+ let json;
49
+ try {
50
+ json = JSON.parse(text);
51
+ } catch {
52
+ json = { raw: text.slice(0, 500) };
53
+ }
54
+ if (!res.ok) {
55
+ const msg = json?.error || json?.message || JSON.stringify(json);
56
+ throw new Error(`HTTP ${res.status}: ${msg}`);
57
+ }
58
+ return json;
59
+ }
60
+
61
+ const ok = (data) => ({ content: [{ type: "text", text: JSON.stringify(data, null, 2) }] });
62
+ const fail = (e) => ({ content: [{ type: "text", text: "Error: " + (e?.message || String(e)) }], isError: true });
63
+
64
+ const server = new McpServer({ name: "ambulancedz", version: "1.1.0" });
65
+
66
+ server.tool(
67
+ "list_wilayas",
68
+ "List all 69 Algerian wilayas (provinces) with the number of listed ambulance drivers and how many are available right now in each. Use this to discover wilaya ids.",
69
+ {},
70
+ async () => {
71
+ try { return ok(await api("/wilayas")); } catch (e) { return fail(e); }
72
+ }
73
+ );
74
+
75
+ server.tool(
76
+ "list_communes",
77
+ "List the communes (municipalities) of a wilaya, by the wilaya id from list_wilayas.",
78
+ { wilaya_id: z.number().int().describe("Wilaya id") },
79
+ async ({ wilaya_id }) => {
80
+ try { return ok(await api(`/wilayas/${wilaya_id}/communes`)); } catch (e) { return fail(e); }
81
+ }
82
+ );
83
+
84
+ server.tool(
85
+ "search_ambulances",
86
+ "Search listed ambulance drivers, optionally filtered by wilaya, commune, service type or availability. Verified drivers and higher ratings come first.",
87
+ {
88
+ wilaya: z.number().int().optional().describe("Wilaya id"),
89
+ commune: z.number().int().optional().describe("Commune id"),
90
+ service_type: z.enum(["emergency", "transport", "inter_wilaya"]).optional(),
91
+ status: z.enum(["available", "busy", "offline"]).optional(),
92
+ per_page: z.number().int().min(1).max(50).optional().describe("Results per page (max 50)"),
93
+ },
94
+ async (args) => {
95
+ try { return ok(await api("/ambulances", args)); } catch (e) { return fail(e); }
96
+ }
97
+ );
98
+
99
+ server.tool(
100
+ "find_nearest_ambulance",
101
+ "Find the nearest AVAILABLE ambulances to a GPS location inside Algeria, sorted by distance, with a rough ETA in minutes.",
102
+ {
103
+ lat: z.number().describe("Latitude (within Algeria, ~18..38)"),
104
+ lng: z.number().describe("Longitude (within Algeria, ~-9..12)"),
105
+ },
106
+ async ({ lat, lng }) => {
107
+ try { return ok(await api("/ambulances/nearest", { lat, lng })); } catch (e) { return fail(e); }
108
+ }
109
+ );
110
+
111
+ server.tool(
112
+ "get_ambulance",
113
+ "Get full details of a single ambulance/driver by id (phone, plate, location, rating, verification).",
114
+ { id: z.number().int().describe("Ambulance id") },
115
+ async ({ id }) => {
116
+ try { return ok(await api(`/ambulances/${id}`)); } catch (e) { return fail(e); }
117
+ }
118
+ );
119
+
120
+ server.tool(
121
+ "estimate_price",
122
+ "Estimate the approximate cost range (DZD) of an ambulance trip between two wilayas. Indicative only — the final price is agreed with the driver.",
123
+ {
124
+ from_wilaya: z.number().int().describe("Pickup wilaya id"),
125
+ to_wilaya: z.number().int().describe("Destination wilaya id"),
126
+ service: z.enum(["basic", "equipped"]).optional().describe("basic (default) or medically-equipped"),
127
+ night: z.boolean().optional().describe("Night-time pricing (×1.4)"),
128
+ },
129
+ async (args) => {
130
+ try { return ok(await api("/price-estimate", args)); } catch (e) { return fail(e); }
131
+ }
132
+ );
133
+
134
+ server.tool(
135
+ "platform_stats",
136
+ "Get Ambulance.dz platform stats: total / verified / available drivers, wilaya coverage, commune count and average rating.",
137
+ {},
138
+ async () => {
139
+ try { return ok(await api("/stats")); } catch (e) { return fail(e); }
140
+ }
141
+ );
142
+
143
+ server.tool(
144
+ "request_recurring_booking",
145
+ "Register a recurring (dialysis / chronic-care) transport request for a patient. Creates a PENDING booking that awaits a driver's acceptance. Find ids with list_wilayas / list_communes, and optionally pick a preferred driver with search_ambulances.",
146
+ {
147
+ patient_name: z.string().describe("Patient full name"),
148
+ patient_phone: z.string().describe("Algerian mobile, e.g. 0551234567"),
149
+ pickup_wilaya_id: z.number().int().describe("Pickup wilaya id"),
150
+ dest_wilaya_id: z.number().int().describe("Destination (dialysis centre/hospital) wilaya id"),
151
+ days: z.array(z.number().int().min(0).max(6)).min(1).describe("Weekdays: 0=Sunday .. 6=Saturday, e.g. [0,2,4]"),
152
+ time: z.string().describe("Pickup time HH:MM (24h), e.g. 07:00"),
153
+ pickup_commune_id: z.number().int().optional(),
154
+ dest_commune_id: z.number().int().optional(),
155
+ pickup_note: z.string().optional().describe("Exact pickup address / landmark"),
156
+ dest_note: z.string().optional().describe("Hospital / dialysis centre name"),
157
+ service_type: z.enum(["emergency", "transport", "inter_wilaya"]).optional(),
158
+ ambulance_id: z.number().int().optional().describe("Preferred driver id (from search_ambulances)"),
159
+ notes: z.string().optional().describe("Patient condition notes (e.g. needs a wheelchair)"),
160
+ },
161
+ async (args) => {
162
+ try { return ok(await apiPost("/recurring-bookings", args)); } catch (e) { return fail(e); }
163
+ }
164
+ );
165
+
166
+ const transport = new StdioServerTransport();
167
+ await server.connect(transport);
168
+ console.error(`ambulancedz MCP server ready — base: ${BASE}`);