@curless/clubmed-demo 0.1.4 → 0.2.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 +3 -2
- package/dist/index.js +35 -61
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# @curless/clubmed-demo
|
|
2
2
|
|
|
3
|
-
A demo "Club Med
|
|
4
|
-
|
|
3
|
+
A demo "Club Med" storefront MCP server. Browses Club Med resorts worldwide
|
|
4
|
+
(Alps & Japan ski; Mediterranean, Indian Ocean, Caribbean & Americas sun) and
|
|
5
|
+
books them through [agentbank](https://gitlab.com/robin-ruan/agentbank)
|
|
5
6
|
acquiring (ACP). For demos/testing only.
|
|
6
7
|
|
|
7
8
|
## Use (Claude Desktop / any MCP client)
|
package/dist/index.js
CHANGED
|
@@ -2,68 +2,49 @@
|
|
|
2
2
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
-
// @curless/clubmed-demo — a SIMULATED "Club Med
|
|
6
|
-
// browses
|
|
7
|
-
//
|
|
8
|
-
// the
|
|
9
|
-
//
|
|
5
|
+
// @curless/clubmed-demo — a SIMULATED "Club Med" storefront MCP for the BUYER.
|
|
6
|
+
// It browses Club Med resorts and starts a booking by calling Club Med's own
|
|
7
|
+
// merchant backend (mcp.curless.ai/clubmed) — NOT the agentbank gateway
|
|
8
|
+
// directly. The backend owns the catalog + prices the booking + opens a
|
|
9
|
+
// merchant-quoted checkout with its own key; this storefront holds NO secret.
|
|
10
|
+
// Payment is completed by the customer at the returned hosted checkout link.
|
|
10
11
|
//
|
|
11
12
|
// Run (Claude Desktop / any MCP client):
|
|
12
13
|
// npx -y @curless/clubmed-demo
|
|
13
|
-
// with env:
|
|
14
|
-
// AGENTBANK_API_URL (default https://mcp.curless.ai; set
|
|
15
|
-
//
|
|
16
|
-
//
|
|
14
|
+
// with env (all optional):
|
|
15
|
+
// AGENTBANK_API_URL base URL (default https://mcp.curless.ai; set
|
|
16
|
+
// http://localhost:3000 for local dev). The Club Med
|
|
17
|
+
// backend lives under <base>/clubmed.
|
|
17
18
|
//
|
|
18
19
|
// IMPORTANT: only JSON-RPC may be written to stdout (the MCP stdio transport).
|
|
19
20
|
// Never console.log here — diagnostics go to stderr.
|
|
20
|
-
const API_BASE = process.env.AGENTBANK_API_URL ?? 'https://mcp.curless.ai';
|
|
21
|
-
const
|
|
22
|
-
const MERCHANT = process.env.CLUBMED_MERCHANT_ID ?? '429488';
|
|
21
|
+
const API_BASE = (process.env.AGENTBANK_API_URL ?? 'https://mcp.curless.ai').replace(/\/$/, '');
|
|
22
|
+
const CLUBMED = `${API_BASE}/clubmed`;
|
|
23
23
|
const api = async (path, init = {}) => {
|
|
24
|
-
const res = await fetch(`${
|
|
24
|
+
const res = await fetch(`${CLUBMED}${path}`, {
|
|
25
25
|
...init,
|
|
26
|
-
headers: {
|
|
27
|
-
'content-type': 'application/json',
|
|
28
|
-
...(TOKEN ? { authorization: `Bearer ${TOKEN}` } : {}),
|
|
29
|
-
...(init.headers ?? {}),
|
|
30
|
-
},
|
|
26
|
+
headers: { 'content-type': 'application/json', ...(init.headers ?? {}) },
|
|
31
27
|
});
|
|
32
28
|
if (!res.ok) {
|
|
33
|
-
throw new Error(`
|
|
29
|
+
throw new Error(`clubmed-backend ${path} -> ${res.status}: ${await res.text().catch(() => '')}`);
|
|
34
30
|
}
|
|
35
31
|
return (await res.json());
|
|
36
32
|
};
|
|
37
|
-
const eur = (minor) => `€${(minor / 100).toLocaleString('en-IE', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
38
|
-
const view = (it) => ({
|
|
39
|
-
sku: it.sku,
|
|
40
|
-
name: it.name,
|
|
41
|
-
country: it.attributes.country,
|
|
42
|
-
kind: it.attributes.kind,
|
|
43
|
-
description: it.description,
|
|
44
|
-
currency: it.currency,
|
|
45
|
-
price: eur(it.price), // full per-person/week price
|
|
46
|
-
available: it.available,
|
|
47
|
-
});
|
|
48
|
-
const loadCatalog = async () => {
|
|
49
|
-
const { items } = await api(`/v1/merchants/${encodeURIComponent(MERCHANT)}/catalog`);
|
|
50
|
-
return items;
|
|
51
|
-
};
|
|
52
33
|
const TOOLS = [
|
|
53
34
|
{
|
|
54
35
|
name: 'list_clubmed_resorts',
|
|
55
|
-
description: 'Browse Club Med resorts
|
|
36
|
+
description: 'Browse Club Med resorts worldwide (Alps & Japan ski; Mediterranean, Indian Ocean, Caribbean & Americas sun) — shows the per-person/week price. Optional country (ISO-2, e.g. FR/IT/JP/MV/MQ/MX) or kind (ski|sun) filter.',
|
|
56
37
|
inputSchema: {
|
|
57
38
|
type: 'object',
|
|
58
39
|
properties: {
|
|
59
|
-
country: { type: 'string', description: 'ISO-2, e.g. FR / IT /
|
|
40
|
+
country: { type: 'string', description: 'ISO-2, e.g. FR / IT / JP / MV / MU / MQ / MX' },
|
|
60
41
|
kind: { type: 'string', enum: ['ski', 'sun'] },
|
|
61
42
|
},
|
|
62
43
|
},
|
|
63
44
|
},
|
|
64
45
|
{
|
|
65
46
|
name: 'get_clubmed_resort',
|
|
66
|
-
description: 'Get one Club Med
|
|
47
|
+
description: 'Get one Club Med resort by its sku (from list_clubmed_resorts).',
|
|
67
48
|
inputSchema: {
|
|
68
49
|
type: 'object',
|
|
69
50
|
required: ['sku'],
|
|
@@ -72,14 +53,13 @@ const TOOLS = [
|
|
|
72
53
|
},
|
|
73
54
|
{
|
|
74
55
|
name: 'book_clubmed_resort',
|
|
75
|
-
description: "
|
|
56
|
+
description: "Start a Club Med booking. Club Med's backend prices the resort and opens a secure checkout for the full per-person/week price (Stripe test mode). Returns the amount and a hosted payment link — the customer completes payment at that link. Does not charge a card directly.",
|
|
76
57
|
inputSchema: {
|
|
77
58
|
type: 'object',
|
|
78
59
|
required: ['sku'],
|
|
79
60
|
properties: {
|
|
80
61
|
sku: { type: 'string' },
|
|
81
62
|
quantity: { type: 'integer', description: 'number of guests/weeks (default 1)' },
|
|
82
|
-
paymentToken: { type: 'string', description: "delegated token; default 'pm_card_visa'" },
|
|
83
63
|
},
|
|
84
64
|
},
|
|
85
65
|
},
|
|
@@ -87,42 +67,36 @@ const TOOLS = [
|
|
|
87
67
|
const text = (value) => ({
|
|
88
68
|
content: [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
89
69
|
});
|
|
90
|
-
const server = new Server({ name: 'clubmed
|
|
70
|
+
const server = new Server({ name: 'clubmed', version: '0.2.0' }, { capabilities: { tools: {} } });
|
|
91
71
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
92
72
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
93
73
|
const args = (req.params.arguments ?? {});
|
|
94
74
|
try {
|
|
95
75
|
if (req.params.name === 'list_clubmed_resorts') {
|
|
96
76
|
const country = typeof args.country === 'string' ? args.country.toUpperCase() : undefined;
|
|
97
|
-
const kind = typeof args.kind === 'string' ? args.kind :
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
.map(view);
|
|
102
|
-
return text({ merchant: MERCHANT, currency: 'EUR', count: resorts.length, resorts });
|
|
77
|
+
const kind = typeof args.kind === 'string' ? `?kind=${encodeURIComponent(args.kind)}` : '';
|
|
78
|
+
const { items } = await api(`/catalog${kind}`);
|
|
79
|
+
const resorts = country ? items.filter((r) => r.country === country) : items;
|
|
80
|
+
return text({ currency: 'EUR', count: resorts.length, resorts });
|
|
103
81
|
}
|
|
104
82
|
if (req.params.name === 'get_clubmed_resort') {
|
|
105
83
|
const sku = String(args.sku ?? '');
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
return { content: [{ type: 'text', text: `no resort with sku ${sku}` }], isError: true };
|
|
109
|
-
}
|
|
110
|
-
return text(view(it));
|
|
84
|
+
const resort = await api(`/catalog/${encodeURIComponent(sku)}`);
|
|
85
|
+
return text(resort);
|
|
111
86
|
}
|
|
112
87
|
if (req.params.name === 'book_clubmed_resort') {
|
|
113
88
|
const sku = String(args.sku ?? '');
|
|
114
89
|
const quantity = Math.max(Number(args.quantity) || 1, 1);
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
90
|
+
const booking = await api('/checkout', { method: 'POST', body: JSON.stringify({ sku, quantity }) });
|
|
91
|
+
return text({
|
|
92
|
+
status: 'awaiting_payment',
|
|
93
|
+
resort: booking.name,
|
|
94
|
+
quantity,
|
|
95
|
+
total: booking.amountDisplay,
|
|
96
|
+
payUrl: booking.paymentLink.url,
|
|
97
|
+
message: `Open the payment link to confirm and pay for ${booking.name} (${booking.amountDisplay}).`,
|
|
98
|
+
booking,
|
|
124
99
|
});
|
|
125
|
-
return text({ booked: true, order });
|
|
126
100
|
}
|
|
127
101
|
return { content: [{ type: 'text', text: `unknown tool: ${req.params.name}` }], isError: true };
|
|
128
102
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAEnG,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@curless/clubmed-demo",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Demo Club Med storefront MCP — browse resorts
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Demo Club Med storefront MCP — browse resorts worldwide and start a booking via Club Med's merchant backend (mcp.curless.ai/clubmed); the customer pays at the returned hosted checkout link.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|