@maideo/mcp 0.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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -0
  3. package/package.json +43 -0
  4. package/src/index.js +360 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Maideo Services
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.md ADDED
@@ -0,0 +1,97 @@
1
+ # @maideo/mcp
2
+
3
+ Official MCP (Model Context Protocol) server for the Maideo Agent API.
4
+
5
+ Lets any MCP-compatible AI agent (Claude Desktop, Claude Code, ChatGPT desktop, Continue.dev, Cursor, etc.) book professional home cleaning services (ménage à domicile) anywhere in France, end-to-end, without human intervention.
6
+
7
+ ## What it does
8
+
9
+ - Checks coverage by postal code
10
+ - Returns a firm price quote
11
+ - Creates a booking in Maideo's back-office (visible to the ops team)
12
+ - Enrolls the end user with URSSAF for the 50% immediate tax credit advance (no card payment needed — SEPA direct debit after each intervention)
13
+ - Polls booking status
14
+
15
+ ## Tools exposed
16
+
17
+ | Tool | Purpose |
18
+ |------|---------|
19
+ | `search_coverage` | Check if Maideo serves a zip code |
20
+ | `get_quote` | Get a firm 72h-valid price quote |
21
+ | `create_booking` | Create the booking (returns a 72h `bookingToken`) |
22
+ | `enroll_avance_immediate` | Submit URSSAF enrollment (IBAN + identity) |
23
+ | `get_booking_status` | Poll status (order, worker, URSSAF) |
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ npm install -g @maideo/mcp
29
+ # or use npx without installing:
30
+ npx @maideo/mcp
31
+ ```
32
+
33
+ ## Configure Claude Desktop
34
+
35
+ Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
36
+
37
+ ```json
38
+ {
39
+ "mcpServers": {
40
+ "maideo": {
41
+ "command": "npx",
42
+ "args": ["-y", "@maideo/mcp"],
43
+ "env": {
44
+ "MAIDEO_AGENT_NAME": "claude-desktop"
45
+ }
46
+ }
47
+ }
48
+ }
49
+ ```
50
+
51
+ Restart Claude Desktop. You can now ask Claude:
52
+
53
+ > "Can you book me a weekly 3-hour cleaning at 12 rue de Rivoli, 75001 Paris, starting next Monday?"
54
+
55
+ Claude will chain the 5 tools automatically.
56
+
57
+ ## Configure Claude Code
58
+
59
+ ```bash
60
+ claude mcp add maideo npx -- -y @maideo/mcp
61
+ ```
62
+
63
+ ## Environment variables
64
+
65
+ | Variable | Default | Purpose |
66
+ |----------|---------|---------|
67
+ | `MAIDEO_API_BASE` | `https://api.maideo.fr/public/agent` | API base URL (override for staging/dev) |
68
+ | `MAIDEO_AGENT_NAME` | `maideo-mcp-client` | Identifier sent via `X-Agent-Name` header (used for rate limiting and analytics) |
69
+
70
+ ## Anti-fraud hold
71
+
72
+ All bookings created via this MCP server are held for 48h before worker dispatch. The Maideo ops team may phone/SMS the end user to verify. **Do not submit bookings with fake data in production.** Your `MAIDEO_AGENT_NAME` may be blocklisted.
73
+
74
+ You must also collect explicit consent from the end user before calling `create_booking`. Pass `agentConsent: true` as a binding attestation.
75
+
76
+ ## How payment works (there is no payment step)
77
+
78
+ Maideo uses the URSSAF **avance immédiate de crédit d'impôt** (immediate tax credit advance), a French government program for home services.
79
+
80
+ 1. End user enrolls via `enroll_avance_immediate` — provides identity, birth place, address, IBAN
81
+ 2. Worker does the cleaning
82
+ 3. Maideo declares the hours to URSSAF
83
+ 4. URSSAF pays Maideo directly for 50% of the amount (the tax credit portion)
84
+ 5. URSSAF collects the remaining 50% from the end user via SEPA direct debit
85
+
86
+ **No card, no Stripe, no upfront payment, no signed PDF.** The IBAN + identity are enough.
87
+
88
+ ## Links
89
+
90
+ - [Maideo website](https://www.maideo.fr)
91
+ - [OpenAPI spec](https://www.maideo.fr/openapi.json)
92
+ - [Agents policy](https://www.maideo.fr/agents.txt)
93
+ - [LLMs manifest](https://www.maideo.fr/llms.txt)
94
+
95
+ ## License
96
+
97
+ MIT
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@maideo/mcp",
3
+ "version": "0.1.0",
4
+ "description": "Official MCP server for the Maideo Agent API — book home cleaning services in France end-to-end",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "maideo-mcp": "src/index.js"
9
+ },
10
+ "files": [
11
+ "src/",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "start": "node src/index.js",
17
+ "dev": "MAIDEO_API_BASE=http://localhost:3000/public/agent node src/index.js"
18
+ },
19
+ "keywords": [
20
+ "mcp",
21
+ "model-context-protocol",
22
+ "maideo",
23
+ "cleaning",
24
+ "menage",
25
+ "france",
26
+ "claude",
27
+ "chatgpt",
28
+ "agent"
29
+ ],
30
+ "author": "Maideo <dev@maideo.fr>",
31
+ "license": "MIT",
32
+ "dependencies": {
33
+ "@modelcontextprotocol/sdk": "^1.0.0"
34
+ },
35
+ "engines": {
36
+ "node": ">=20"
37
+ },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/maideo/maideo-mcp"
41
+ },
42
+ "homepage": "https://www.maideo.fr/agents.txt"
43
+ }
package/src/index.js ADDED
@@ -0,0 +1,360 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @maideo/mcp — Official MCP server for the Maideo Agent API.
4
+ *
5
+ * Exposes 5 tools that map 1:1 on the public HTTP API:
6
+ * - search_coverage
7
+ * - get_quote
8
+ * - create_booking
9
+ * - enroll_avance_immediate
10
+ * - get_booking_status
11
+ *
12
+ * Runs over stdio. Install with `npm i -g @maideo/mcp` then add to your
13
+ * Claude Desktop / Claude Code / ChatGPT MCP client config.
14
+ */
15
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
16
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
17
+ import {
18
+ CallToolRequestSchema,
19
+ ListToolsRequestSchema,
20
+ } from "@modelcontextprotocol/sdk/types.js";
21
+
22
+ const API_BASE =
23
+ process.env.MAIDEO_API_BASE || "https://api.maideo.fr/public/agent";
24
+ const AGENT_NAME =
25
+ process.env.MAIDEO_AGENT_NAME || "maideo-mcp-client";
26
+
27
+ async function apiRequest(path, { method = "GET", body, bookingToken } = {}) {
28
+ const headers = {
29
+ "Content-Type": "application/json",
30
+ "X-Agent-Name": AGENT_NAME,
31
+ };
32
+ if (bookingToken) {
33
+ headers["Authorization"] = `Bearer ${bookingToken}`;
34
+ }
35
+ const res = await fetch(`${API_BASE}${path}`, {
36
+ method,
37
+ headers,
38
+ body: body ? JSON.stringify(body) : undefined,
39
+ });
40
+ const text = await res.text();
41
+ let data;
42
+ try {
43
+ data = text ? JSON.parse(text) : {};
44
+ } catch {
45
+ data = { raw: text };
46
+ }
47
+ if (!res.ok) {
48
+ const err = new Error(
49
+ `Maideo API ${res.status}: ${data.error || res.statusText}`
50
+ );
51
+ err.statusCode = res.status;
52
+ err.details = data.details;
53
+ throw err;
54
+ }
55
+ return data;
56
+ }
57
+
58
+ const server = new Server(
59
+ {
60
+ name: "@maideo/mcp",
61
+ version: "0.1.0",
62
+ },
63
+ {
64
+ capabilities: {
65
+ tools: {},
66
+ },
67
+ }
68
+ );
69
+
70
+ const TOOLS = [
71
+ {
72
+ name: "search_coverage",
73
+ description:
74
+ "Check whether Maideo serves a given French postal code and get the estimated hourly rate (gross, before the 50% tax credit advance). Use this BEFORE get_quote.",
75
+ inputSchema: {
76
+ type: "object",
77
+ required: ["zip"],
78
+ properties: {
79
+ zip: {
80
+ type: "string",
81
+ pattern: "^[0-9]{5}$",
82
+ description: "5-digit French postal code",
83
+ },
84
+ prestation: {
85
+ type: "string",
86
+ enum: ["MENAGE"],
87
+ default: "MENAGE",
88
+ },
89
+ },
90
+ },
91
+ },
92
+ {
93
+ name: "get_quote",
94
+ description:
95
+ "Get a firm price quote for a cleaning booking. Returns a quoteToken valid for 72h that must be passed to create_booking.",
96
+ inputSchema: {
97
+ type: "object",
98
+ required: ["zip", "nbHeuresSemaine", "frequency"],
99
+ properties: {
100
+ zip: { type: "string", pattern: "^[0-9]{5}$" },
101
+ city: { type: "string" },
102
+ nbHeuresSemaine: {
103
+ type: "number",
104
+ minimum: 2,
105
+ description: "Number of hours per intervention",
106
+ },
107
+ nbPrestaSemaine: {
108
+ type: "integer",
109
+ default: 1,
110
+ description: "Number of interventions per week",
111
+ },
112
+ frequency: {
113
+ type: "string",
114
+ enum: ["one_shot", "weekly", "bi_weekly", "monthly"],
115
+ },
116
+ prestation: {
117
+ type: "string",
118
+ enum: ["MENAGE"],
119
+ default: "MENAGE",
120
+ },
121
+ prestationInfo: {
122
+ type: "object",
123
+ properties: {
124
+ houseType: { type: "string" },
125
+ houseSize: { type: "integer" },
126
+ repassage: { type: "boolean" },
127
+ pets: {
128
+ type: "object",
129
+ properties: {
130
+ dog: { type: "boolean" },
131
+ cat: { type: "boolean" },
132
+ },
133
+ },
134
+ comments: { type: "string" },
135
+ },
136
+ },
137
+ },
138
+ },
139
+ },
140
+ {
141
+ name: "create_booking",
142
+ description:
143
+ "Create a booking. Requires a quoteToken from get_quote plus the end-user's identity, address, and explicit consent. Returns a bookingToken (72h) for subsequent calls and a bookingId. The booking is held for 48h before worker dispatch as anti-fraud protection.",
144
+ inputSchema: {
145
+ type: "object",
146
+ required: [
147
+ "quoteToken",
148
+ "client",
149
+ "address",
150
+ "dateDebut",
151
+ "agentConsent",
152
+ ],
153
+ properties: {
154
+ quoteToken: { type: "string" },
155
+ client: {
156
+ type: "object",
157
+ required: ["firstName", "lastName", "email", "phone"],
158
+ properties: {
159
+ firstName: { type: "string" },
160
+ lastName: { type: "string" },
161
+ email: { type: "string", format: "email" },
162
+ phone: { type: "string" },
163
+ },
164
+ },
165
+ address: {
166
+ type: "object",
167
+ required: ["street", "city", "zip"],
168
+ properties: {
169
+ street: { type: "string" },
170
+ city: { type: "string" },
171
+ zip: { type: "string", pattern: "^[0-9]{5}$" },
172
+ country: { type: "string", default: "France" },
173
+ },
174
+ },
175
+ dateDebut: {
176
+ type: "string",
177
+ format: "date-time",
178
+ description: "First intervention date (ISO 8601)",
179
+ },
180
+ frequency: {
181
+ type: "string",
182
+ enum: ["one_shot", "weekly", "bi_weekly", "monthly"],
183
+ },
184
+ nbHeuresSemaine: { type: "number" },
185
+ nbPrestaSemaine: { type: "integer", default: 1 },
186
+ comments: { type: "string" },
187
+ agentConsent: {
188
+ type: "boolean",
189
+ description:
190
+ "Attest that the end-user explicitly consented to share their data via your agent",
191
+ },
192
+ },
193
+ },
194
+ },
195
+ {
196
+ name: "enroll_avance_immediate",
197
+ description:
198
+ "Enroll the end user with URSSAF for the 50% immediate tax credit advance. After this succeeds, the user pays only half the gross hourly rate via SEPA. Required: bookingToken from create_booking, full identity, birth place, postal address, IBAN.",
199
+ inputSchema: {
200
+ type: "object",
201
+ required: [
202
+ "bookingId",
203
+ "bookingToken",
204
+ "civilite",
205
+ "nomNaissance",
206
+ "prenoms",
207
+ "dateNaissance",
208
+ "lieuNaissance",
209
+ "numeroTelephonePortable",
210
+ "adresseMail",
211
+ "adressePostale",
212
+ "coordonneeBancaire",
213
+ ],
214
+ properties: {
215
+ bookingId: { type: "string" },
216
+ bookingToken: { type: "string" },
217
+ civilite: {
218
+ type: "string",
219
+ enum: ["1", "2"],
220
+ description: "1=Monsieur, 2=Madame (NOT M/MME)",
221
+ },
222
+ nomNaissance: { type: "string" },
223
+ nomUsage: { type: "string" },
224
+ prenoms: { type: "string" },
225
+ dateNaissance: { type: "string", format: "date" },
226
+ lieuNaissance: {
227
+ type: "object",
228
+ properties: {
229
+ codePaysNaissance: { type: "string" },
230
+ departementNaissance: { type: "string" },
231
+ communeNaissance: {
232
+ type: "object",
233
+ properties: {
234
+ codeCommune: { type: "string" },
235
+ libelleCommune: { type: "string" },
236
+ },
237
+ },
238
+ },
239
+ },
240
+ numeroTelephonePortable: {
241
+ type: "string",
242
+ pattern: "^(0|\\+33)[6-7]([0-9]{2}){4}$",
243
+ },
244
+ adresseMail: { type: "string", format: "email" },
245
+ adressePostale: {
246
+ type: "object",
247
+ properties: {
248
+ libelleVoie: { type: "string" },
249
+ libelleCommune: { type: "string" },
250
+ codeCommune: { type: "string" },
251
+ codePostal: { type: "string" },
252
+ codePays: { type: "string" },
253
+ },
254
+ },
255
+ coordonneeBancaire: {
256
+ type: "object",
257
+ required: ["bic", "iban", "titulaire"],
258
+ properties: {
259
+ bic: { type: "string" },
260
+ iban: { type: "string" },
261
+ titulaire: { type: "string" },
262
+ },
263
+ },
264
+ },
265
+ },
266
+ },
267
+ {
268
+ name: "get_booking_status",
269
+ description:
270
+ "Poll the current status of a booking (order, mission, worker assignment, URSSAF enrollment). Requires bookingId + bookingToken from create_booking.",
271
+ inputSchema: {
272
+ type: "object",
273
+ required: ["bookingId", "bookingToken"],
274
+ properties: {
275
+ bookingId: { type: "string" },
276
+ bookingToken: { type: "string" },
277
+ },
278
+ },
279
+ },
280
+ ];
281
+
282
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
283
+ tools: TOOLS,
284
+ }));
285
+
286
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
287
+ const { name, arguments: args } = request.params;
288
+ try {
289
+ let result;
290
+ switch (name) {
291
+ case "search_coverage": {
292
+ const qs = new URLSearchParams({
293
+ zip: args.zip,
294
+ prestation: args.prestation || "MENAGE",
295
+ });
296
+ result = await apiRequest(`/coverage?${qs.toString()}`);
297
+ break;
298
+ }
299
+ case "get_quote": {
300
+ result = await apiRequest("/quote", {
301
+ method: "POST",
302
+ body: args,
303
+ });
304
+ break;
305
+ }
306
+ case "create_booking": {
307
+ result = await apiRequest("/book", {
308
+ method: "POST",
309
+ body: args,
310
+ });
311
+ break;
312
+ }
313
+ case "enroll_avance_immediate": {
314
+ const { bookingId, bookingToken, ...urssafPayload } = args;
315
+ result = await apiRequest(
316
+ `/booking/${bookingId}/enroll-urssaf`,
317
+ {
318
+ method: "POST",
319
+ body: urssafPayload,
320
+ bookingToken,
321
+ }
322
+ );
323
+ break;
324
+ }
325
+ case "get_booking_status": {
326
+ const { bookingId, bookingToken } = args;
327
+ result = await apiRequest(`/booking/${bookingId}`, {
328
+ bookingToken,
329
+ });
330
+ break;
331
+ }
332
+ default:
333
+ throw new Error(`Unknown tool: ${name}`);
334
+ }
335
+ return {
336
+ content: [
337
+ {
338
+ type: "text",
339
+ text: JSON.stringify(result, null, 2),
340
+ },
341
+ ],
342
+ };
343
+ } catch (err) {
344
+ return {
345
+ isError: true,
346
+ content: [
347
+ {
348
+ type: "text",
349
+ text: `Error calling ${name}: ${err.message}${
350
+ err.details ? `\nDetails: ${JSON.stringify(err.details)}` : ""
351
+ }`,
352
+ },
353
+ ],
354
+ };
355
+ }
356
+ });
357
+
358
+ const transport = new StdioServerTransport();
359
+ await server.connect(transport);
360
+ console.error("[@maideo/mcp] Server running on stdio");