distil-ai-rms-reporting 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 +56 -0
- package/dist/api.js +201 -0
- package/dist/index.js +471 -0
- package/dist/safety.js +22 -0
- package/dist/tools.js +66 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Distil AI - RMS Reporting
|
|
2
|
+
|
|
3
|
+
MCP server that connects Claude Desktop to your restaurant management analytics.
|
|
4
|
+
|
|
5
|
+
## Setup Instructions
|
|
6
|
+
|
|
7
|
+
### 1. Install Node.js
|
|
8
|
+
|
|
9
|
+
Download and install Node.js (version 18 or later) from [nodejs.org](https://nodejs.org/).
|
|
10
|
+
|
|
11
|
+
Choose the **LTS** version and follow the installer prompts.
|
|
12
|
+
|
|
13
|
+
### 2. Configure Claude Desktop
|
|
14
|
+
|
|
15
|
+
Open Claude Desktop, then open the MCP configuration file:
|
|
16
|
+
|
|
17
|
+
- **Mac**: Claude Desktop menu > Settings > Developer > Edit Config
|
|
18
|
+
- **Windows**: File > Settings > Developer > Edit Config
|
|
19
|
+
|
|
20
|
+
This opens your `claude_desktop_config.json` file. Replace its contents with the following (or add the `distil-ai-rms-reporting` entry to your existing `mcpServers` block):
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"mcpServers": {
|
|
25
|
+
"distil-ai-rms-reporting": {
|
|
26
|
+
"command": "npx",
|
|
27
|
+
"args": ["-y", "distil-ai-rms-reporting"],
|
|
28
|
+
"env": {
|
|
29
|
+
"API_BASE_URL": "YOUR_API_URL",
|
|
30
|
+
"API_KEY": "YOUR_API_KEY"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Replace `YOUR_API_URL` and `YOUR_API_KEY` with the values provided to you by your Distil contact.
|
|
38
|
+
|
|
39
|
+
### 3. Restart Claude Desktop
|
|
40
|
+
|
|
41
|
+
Quit Claude Desktop completely and reopen it. You should see a tools icon in the chat input area confirming the connection is active.
|
|
42
|
+
|
|
43
|
+
### 4. Start asking questions
|
|
44
|
+
|
|
45
|
+
Try prompts like:
|
|
46
|
+
|
|
47
|
+
- "Show me reservation trends for the last 3 months"
|
|
48
|
+
- "Which restaurant has the highest no-show rate?"
|
|
49
|
+
- "What does demand look like for next week?"
|
|
50
|
+
- "How are our review scores trending?"
|
|
51
|
+
|
|
52
|
+
## Troubleshooting
|
|
53
|
+
|
|
54
|
+
- **Tools icon not appearing**: Make sure Node.js is installed by opening a terminal and running `node --version`. You should see `v18` or higher.
|
|
55
|
+
- **Connection errors**: Double-check your `API_URL` and `API_KEY` values in the config file.
|
|
56
|
+
- **Still not working**: Quit and reopen Claude Desktop after any config changes.
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.setMcpContext = setMcpContext;
|
|
7
|
+
exports.testConnection = testConnection;
|
|
8
|
+
exports.apiGetSchema = apiGetSchema;
|
|
9
|
+
exports.apiQueryReservations = apiQueryReservations;
|
|
10
|
+
exports.apiGetSummaryStats = apiGetSummaryStats;
|
|
11
|
+
exports.apiGetLoyaltyBreakdown = apiGetLoyaltyBreakdown;
|
|
12
|
+
exports.apiGetGuestBehaviour = apiGetGuestBehaviour;
|
|
13
|
+
exports.apiGetRevenueStats = apiGetRevenueStats;
|
|
14
|
+
exports.apiGetTimePatternStats = apiGetTimePatternStats;
|
|
15
|
+
exports.apiGetDemandOutlook = apiGetDemandOutlook;
|
|
16
|
+
exports.apiGetReviewStats = apiGetReviewStats;
|
|
17
|
+
exports.apiGetForecastStats = apiGetForecastStats;
|
|
18
|
+
exports.apiGetDateStats = apiGetDateStats;
|
|
19
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
20
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
21
|
+
dotenv_1.default.config();
|
|
22
|
+
const requiredEnvVars = ["API_BASE_URL", "API_KEY"];
|
|
23
|
+
for (const key of requiredEnvVars) {
|
|
24
|
+
if (!process.env[key]) {
|
|
25
|
+
console.error(`Missing required environment variable: ${key}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const BASE_URL = process.env.API_BASE_URL.replace(/\/$/, "");
|
|
30
|
+
const API_KEY = process.env.API_KEY;
|
|
31
|
+
function authHeaders() {
|
|
32
|
+
return {
|
|
33
|
+
"Authorization": `Bearer ${API_KEY}`,
|
|
34
|
+
"Content-Type": "application/json",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
// Context headers for query logging — set per-request by tool handlers
|
|
38
|
+
let _mcpContext = {};
|
|
39
|
+
function setMcpContext(toolName, userPrompt) {
|
|
40
|
+
_mcpContext = { toolName, userPrompt };
|
|
41
|
+
}
|
|
42
|
+
function mcpHeaders() {
|
|
43
|
+
const headers = {};
|
|
44
|
+
headers["X-Request-Id"] = crypto_1.default.randomUUID();
|
|
45
|
+
if (_mcpContext.toolName)
|
|
46
|
+
headers["X-MCP-Tool"] = _mcpContext.toolName;
|
|
47
|
+
if (_mcpContext.userPrompt) {
|
|
48
|
+
headers["X-User-Prompt"] = Buffer.from(_mcpContext.userPrompt).toString("base64");
|
|
49
|
+
}
|
|
50
|
+
return headers;
|
|
51
|
+
}
|
|
52
|
+
async function request(path, options) {
|
|
53
|
+
const url = `${BASE_URL}${path}`;
|
|
54
|
+
const res = await fetch(url, {
|
|
55
|
+
...options,
|
|
56
|
+
headers: { ...authHeaders(), ...mcpHeaders(), ...(options?.headers ?? {}) },
|
|
57
|
+
});
|
|
58
|
+
// Clear context after each request
|
|
59
|
+
_mcpContext = {};
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
const body = await res.text().catch(() => "");
|
|
62
|
+
throw new Error(`API error ${res.status} on ${path}${body ? `: ${body}` : ""}`);
|
|
63
|
+
}
|
|
64
|
+
return res.json();
|
|
65
|
+
}
|
|
66
|
+
async function testConnection() {
|
|
67
|
+
try {
|
|
68
|
+
await request("/health");
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
72
|
+
console.error(`API connection failed: ${msg}`);
|
|
73
|
+
console.error(`Check that API_BASE_URL and API_KEY in .env are correct.`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function apiGetSchema() {
|
|
78
|
+
return request("/schema");
|
|
79
|
+
}
|
|
80
|
+
async function apiQueryReservations(whereClause, tenantCode) {
|
|
81
|
+
return request("/reservations/query", {
|
|
82
|
+
method: "POST",
|
|
83
|
+
body: JSON.stringify({ where_clause: whereClause, tenant_code: tenantCode }),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
async function apiGetSummaryStats(groupByField, tenantCode) {
|
|
87
|
+
const params = new URLSearchParams({ group_by: groupByField });
|
|
88
|
+
if (tenantCode)
|
|
89
|
+
params.set("tenant_code", tenantCode);
|
|
90
|
+
return request(`/summary-stats?${params}`);
|
|
91
|
+
}
|
|
92
|
+
async function apiGetLoyaltyBreakdown(tenantCode) {
|
|
93
|
+
const params = tenantCode ? `?tenant_code=${encodeURIComponent(tenantCode)}` : "";
|
|
94
|
+
return request(`/loyalty-breakdown${params}`);
|
|
95
|
+
}
|
|
96
|
+
async function apiGetGuestBehaviour(dimension, dateFrom, dateTo, tenantCode) {
|
|
97
|
+
const params = new URLSearchParams({ dimension });
|
|
98
|
+
if (dateFrom)
|
|
99
|
+
params.set("date_from", dateFrom);
|
|
100
|
+
if (dateTo)
|
|
101
|
+
params.set("date_to", dateTo);
|
|
102
|
+
if (tenantCode)
|
|
103
|
+
params.set("tenant_code", tenantCode);
|
|
104
|
+
return request(`/guest-behaviour?${params}`);
|
|
105
|
+
}
|
|
106
|
+
async function apiGetRevenueStats(dimension, period, dateFrom, dateTo, tenantCode) {
|
|
107
|
+
const params = new URLSearchParams({ dimension });
|
|
108
|
+
if (period)
|
|
109
|
+
params.set("period", period);
|
|
110
|
+
if (dateFrom)
|
|
111
|
+
params.set("date_from", dateFrom);
|
|
112
|
+
if (dateTo)
|
|
113
|
+
params.set("date_to", dateTo);
|
|
114
|
+
if (tenantCode)
|
|
115
|
+
params.set("tenant_code", tenantCode);
|
|
116
|
+
return request(`/revenue-stats?${params}`);
|
|
117
|
+
}
|
|
118
|
+
async function apiGetTimePatternStats(dimension, dateFrom, dateTo, tenantCode) {
|
|
119
|
+
const params = new URLSearchParams({ dimension });
|
|
120
|
+
if (dateFrom)
|
|
121
|
+
params.set("date_from", dateFrom);
|
|
122
|
+
if (dateTo)
|
|
123
|
+
params.set("date_to", dateTo);
|
|
124
|
+
if (tenantCode)
|
|
125
|
+
params.set("tenant_code", tenantCode);
|
|
126
|
+
return request(`/time-pattern-stats?${params}`);
|
|
127
|
+
}
|
|
128
|
+
async function apiGetDemandOutlook(dimension, snapshotDate, restaurantName, dateFrom, dateTo, partySize, tenantCode) {
|
|
129
|
+
const params = new URLSearchParams({ dimension });
|
|
130
|
+
if (snapshotDate)
|
|
131
|
+
params.set("snapshot_date", snapshotDate);
|
|
132
|
+
if (restaurantName)
|
|
133
|
+
params.set("restaurant_name", restaurantName);
|
|
134
|
+
if (dateFrom)
|
|
135
|
+
params.set("date_from", dateFrom);
|
|
136
|
+
if (dateTo)
|
|
137
|
+
params.set("date_to", dateTo);
|
|
138
|
+
if (partySize)
|
|
139
|
+
params.set("party_size", partySize);
|
|
140
|
+
if (tenantCode)
|
|
141
|
+
params.set("tenant_code", tenantCode);
|
|
142
|
+
return request(`/demand-outlook?${params}`);
|
|
143
|
+
}
|
|
144
|
+
async function apiGetReviewStats(dimension, restaurantName, restaurantCategory, source, dateFrom, dateTo, period, minRating, maxRating, sortBy, weeks, tenantCode) {
|
|
145
|
+
const params = new URLSearchParams({ dimension });
|
|
146
|
+
if (restaurantName)
|
|
147
|
+
params.set("restaurant_name", restaurantName);
|
|
148
|
+
if (restaurantCategory)
|
|
149
|
+
params.set("restaurant_category", restaurantCategory);
|
|
150
|
+
if (source)
|
|
151
|
+
params.set("source", source);
|
|
152
|
+
if (dateFrom)
|
|
153
|
+
params.set("date_from", dateFrom);
|
|
154
|
+
if (dateTo)
|
|
155
|
+
params.set("date_to", dateTo);
|
|
156
|
+
if (period)
|
|
157
|
+
params.set("period", period);
|
|
158
|
+
if (minRating)
|
|
159
|
+
params.set("min_rating", minRating);
|
|
160
|
+
if (maxRating)
|
|
161
|
+
params.set("max_rating", maxRating);
|
|
162
|
+
if (sortBy)
|
|
163
|
+
params.set("sort_by", sortBy);
|
|
164
|
+
if (weeks)
|
|
165
|
+
params.set("weeks", weeks);
|
|
166
|
+
if (tenantCode)
|
|
167
|
+
params.set("tenant_code", tenantCode);
|
|
168
|
+
return request(`/review-stats?${params}`);
|
|
169
|
+
}
|
|
170
|
+
async function apiGetForecastStats(dimension, snapshotDate, restaurantName, serviceDateFrom, serviceDateTo, service, snapshotDate2, tenantCode) {
|
|
171
|
+
const params = new URLSearchParams({ dimension });
|
|
172
|
+
if (snapshotDate)
|
|
173
|
+
params.set("snapshot_date", snapshotDate);
|
|
174
|
+
if (restaurantName)
|
|
175
|
+
params.set("restaurant_name", restaurantName);
|
|
176
|
+
if (serviceDateFrom)
|
|
177
|
+
params.set("service_date_from", serviceDateFrom);
|
|
178
|
+
if (serviceDateTo)
|
|
179
|
+
params.set("service_date_to", serviceDateTo);
|
|
180
|
+
if (service)
|
|
181
|
+
params.set("service", service);
|
|
182
|
+
if (snapshotDate2)
|
|
183
|
+
params.set("snapshot_date_2", snapshotDate2);
|
|
184
|
+
if (tenantCode)
|
|
185
|
+
params.set("tenant_code", tenantCode);
|
|
186
|
+
return request(`/forecast-stats?${params}`);
|
|
187
|
+
}
|
|
188
|
+
async function apiGetDateStats(period, groupBy, dateFrom, dateTo, tenantCode, restaurantName) {
|
|
189
|
+
const params = new URLSearchParams({ period });
|
|
190
|
+
if (groupBy)
|
|
191
|
+
params.set("group_by", groupBy);
|
|
192
|
+
if (dateFrom)
|
|
193
|
+
params.set("date_from", dateFrom);
|
|
194
|
+
if (dateTo)
|
|
195
|
+
params.set("date_to", dateTo);
|
|
196
|
+
if (tenantCode)
|
|
197
|
+
params.set("tenant_code", tenantCode);
|
|
198
|
+
if (restaurantName)
|
|
199
|
+
params.set("restaurant_name", restaurantName);
|
|
200
|
+
return request(`/date-stats?${params}`);
|
|
201
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
5
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
6
|
+
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
7
|
+
const api_js_1 = require("./api.js");
|
|
8
|
+
const tools_js_1 = require("./tools.js");
|
|
9
|
+
const server = new index_js_1.Server({ name: "distil-ai-rms-reporting", version: "1.0.0" }, { capabilities: { tools: {} } });
|
|
10
|
+
const TENANT_PARAM = {
|
|
11
|
+
tenant_code: {
|
|
12
|
+
type: "string",
|
|
13
|
+
description: "Optional. Restrict results to a specific tenant (brand). If omitted, all tenants your token has access to are included.",
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
const PROMPT_PARAM = {
|
|
17
|
+
_user_prompt: {
|
|
18
|
+
type: "string",
|
|
19
|
+
description: "Optional. Pass the user's original natural language question that triggered this tool call. Used for usage analytics and improving data quality.",
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
23
|
+
tools: [
|
|
24
|
+
{
|
|
25
|
+
name: "get_schema",
|
|
26
|
+
description: "Returns the column structure of the data_reservation_master table. Call this first to understand available fields before writing queries.",
|
|
27
|
+
inputSchema: {
|
|
28
|
+
type: "object",
|
|
29
|
+
properties: { ...PROMPT_PARAM },
|
|
30
|
+
required: [],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "query_reservations",
|
|
35
|
+
description: "Query data_reservation_master by providing a SQL WHERE clause. Returns up to 200 matching rows. Do not include the full SELECT statement — only the filter condition. Outcome values: Done, NoShow, Cancelled (past reservations), Confirmed, NotConfirmed (future reservations). Example: \"outcome = 'NoShow' AND restaurant_name = 'Harbour'\"",
|
|
36
|
+
inputSchema: {
|
|
37
|
+
type: "object",
|
|
38
|
+
properties: {
|
|
39
|
+
where_clause: {
|
|
40
|
+
type: "string",
|
|
41
|
+
description: "A SQL WHERE clause fragment (no SELECT, no semicolons). Example: \"restaurant_name = 'Harbour' AND covers > 4\"",
|
|
42
|
+
},
|
|
43
|
+
...TENANT_PARAM,
|
|
44
|
+
...PROMPT_PARAM,
|
|
45
|
+
},
|
|
46
|
+
required: ["where_clause"],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "get_summary_stats",
|
|
51
|
+
description: "Returns aggregate counts grouped by a specified column. Outcome values are: Done, NoShow, Cancelled (past reservations) and Confirmed, NotConfirmed (future reservations). Useful for quick breakdowns by outcome, restaurant_name, guest_loyalty_level_at_time, origin, etc.",
|
|
52
|
+
inputSchema: {
|
|
53
|
+
type: "object",
|
|
54
|
+
properties: {
|
|
55
|
+
group_by_field: {
|
|
56
|
+
type: "string",
|
|
57
|
+
enum: [
|
|
58
|
+
"outcome",
|
|
59
|
+
"restaurant_name",
|
|
60
|
+
"origin",
|
|
61
|
+
"guest_loyalty_level_at_time",
|
|
62
|
+
"covers",
|
|
63
|
+
"currency_code",
|
|
64
|
+
"tenant_code",
|
|
65
|
+
],
|
|
66
|
+
description: "Column to group by. Must be one of the listed values.",
|
|
67
|
+
},
|
|
68
|
+
...TENANT_PARAM,
|
|
69
|
+
...PROMPT_PARAM,
|
|
70
|
+
},
|
|
71
|
+
required: ["group_by_field"],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "get_revenue_stats",
|
|
76
|
+
description: "Revenue-focused analysis for CFO-level questions. Covers revenue per cover, no-show cost, channel performance, restaurant rankings, and forward-looking revenue at risk from upcoming bookings. Use this for any question involving money, revenue, financial performance, or cost of no-shows.",
|
|
77
|
+
inputSchema: {
|
|
78
|
+
type: "object",
|
|
79
|
+
properties: {
|
|
80
|
+
dimension: {
|
|
81
|
+
type: "string",
|
|
82
|
+
enum: ["by_period", "by_channel", "by_restaurant", "no_show_cost", "revenue_at_risk"],
|
|
83
|
+
description: "by_period: revenue trend over time (requires period param). by_channel: revenue and no-show rate by booking origin. by_restaurant: revenue per cover by venue. no_show_cost: estimated lost revenue per restaurant. revenue_at_risk: expected revenue from upcoming confirmed bookings.",
|
|
84
|
+
},
|
|
85
|
+
period: {
|
|
86
|
+
type: "string",
|
|
87
|
+
enum: ["day", "week", "month", "quarter", "year"],
|
|
88
|
+
description: "Required when dimension=by_period.",
|
|
89
|
+
},
|
|
90
|
+
date_from: { type: "string", description: "Optional start date filter (ISO 8601)." },
|
|
91
|
+
date_to: { type: "string", description: "Optional end date filter (ISO 8601)." },
|
|
92
|
+
...TENANT_PARAM,
|
|
93
|
+
...PROMPT_PARAM,
|
|
94
|
+
},
|
|
95
|
+
required: ["dimension"],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "get_time_pattern_stats",
|
|
100
|
+
description: "Analyses reservation patterns by time — day of week, hour of day, or a combined heatmap. Use this for questions about busy periods, peak times, when no-shows are highest, or operational planning.",
|
|
101
|
+
inputSchema: {
|
|
102
|
+
type: "object",
|
|
103
|
+
properties: {
|
|
104
|
+
dimension: {
|
|
105
|
+
type: "string",
|
|
106
|
+
enum: ["day_of_week", "hour_of_day", "day_and_hour", "party_size_anomaly"],
|
|
107
|
+
description: "day_of_week: covers, revenue, no-show rate per day. hour_of_day: same by hour. day_and_hour: full heatmap grid (day × hour). party_size_anomaly: weekly avg covers and % large party (7+) with z-scores — flags weeks that are statistical outliers, useful for detecting event-driven spikes.",
|
|
108
|
+
},
|
|
109
|
+
date_from: { type: "string", description: "Optional start date filter (ISO 8601)." },
|
|
110
|
+
date_to: { type: "string", description: "Optional end date filter (ISO 8601)." },
|
|
111
|
+
...TENANT_PARAM,
|
|
112
|
+
...PROMPT_PARAM,
|
|
113
|
+
},
|
|
114
|
+
required: ["dimension"],
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: "get_guest_behaviour_stats",
|
|
119
|
+
description: "Analyses guest behaviour patterns. Use this for questions about cross-venue visits, visit frequency, return rates, and loyalty tier breakdowns for single vs multi-venue guests. All analysis is based on guest_global_id. Dimensions: venue_spread (how many distinct venues each guest visits), visit_frequency (distribution of how many times guests dine), return_rate (what share of guests come back), cross_venue_loyalty (loyalty tier split between single and multi-venue guests).",
|
|
120
|
+
inputSchema: {
|
|
121
|
+
type: "object",
|
|
122
|
+
properties: {
|
|
123
|
+
dimension: {
|
|
124
|
+
type: "string",
|
|
125
|
+
enum: ["venue_spread", "visit_frequency", "return_rate", "cross_venue_loyalty", "loyalty_movement_by_venue", "ftd_conversion", "guest_lifetime_value", "churn_risk"],
|
|
126
|
+
description: "venue_spread: distinct venues per guest. visit_frequency: visit count buckets. return_rate: overall return rate summary. cross_venue_loyalty: loyalty tier split by single/multi-venue. loyalty_movement_by_venue: which restaurants see the most tier progression. ftd_conversion: what % of first-timers came back and how fast. guest_lifetime_value: revenue buckets across guests. churn_risk: loyal guests who haven't returned in 60/90/180+ days.",
|
|
127
|
+
},
|
|
128
|
+
date_from: {
|
|
129
|
+
type: "string",
|
|
130
|
+
description: "Optional start date filter (ISO 8601, e.g. 2024-01-01).",
|
|
131
|
+
},
|
|
132
|
+
date_to: {
|
|
133
|
+
type: "string",
|
|
134
|
+
description: "Optional end date filter (ISO 8601, e.g. 2024-12-31).",
|
|
135
|
+
},
|
|
136
|
+
...TENANT_PARAM,
|
|
137
|
+
...PROMPT_PARAM,
|
|
138
|
+
},
|
|
139
|
+
required: ["dimension"],
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: "get_date_stats",
|
|
144
|
+
description: "Returns reservation counts, covers, and revenue aggregated by a time period (day, week, month, quarter, year). Optionally broken down by a second dimension. Use this for any time-series or trend questions.",
|
|
145
|
+
inputSchema: {
|
|
146
|
+
type: "object",
|
|
147
|
+
properties: {
|
|
148
|
+
period: {
|
|
149
|
+
type: "string",
|
|
150
|
+
enum: ["day", "week", "month", "quarter", "year"],
|
|
151
|
+
description: "Time bucket to group by.",
|
|
152
|
+
},
|
|
153
|
+
group_by: {
|
|
154
|
+
type: "string",
|
|
155
|
+
enum: ["outcome", "restaurant_name", "origin", "guest_loyalty_level_at_time", "covers", "currency_code", "service_period"],
|
|
156
|
+
description: "Optional second dimension to break results down further. service_period groups by Breakfast/Lunch/Dinner inferred from reservation time — useful for cross-referencing against forecast by service.",
|
|
157
|
+
},
|
|
158
|
+
date_from: {
|
|
159
|
+
type: "string",
|
|
160
|
+
description: "Optional start date filter (ISO 8601, e.g. 2024-01-01).",
|
|
161
|
+
},
|
|
162
|
+
date_to: {
|
|
163
|
+
type: "string",
|
|
164
|
+
description: "Optional end date filter (ISO 8601, e.g. 2024-12-31).",
|
|
165
|
+
},
|
|
166
|
+
restaurant_name: {
|
|
167
|
+
type: "string",
|
|
168
|
+
description: "Optional. Filter results to a specific restaurant. Useful when cross-referencing reservation actuals against forecast for a single venue.",
|
|
169
|
+
},
|
|
170
|
+
...TENANT_PARAM,
|
|
171
|
+
...PROMPT_PARAM,
|
|
172
|
+
},
|
|
173
|
+
required: ["period"],
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: "get_demand_outlook",
|
|
178
|
+
description: "Queries the data_restaurant_demand_master table to analyse forward-looking demand. Use this for questions about upcoming reservation demand, booking pace (how far in advance guests book), party size distribution in the outlook period, or which restaurants have the highest expected demand. Always uses the latest snapshot by default.",
|
|
179
|
+
inputSchema: {
|
|
180
|
+
type: "object",
|
|
181
|
+
properties: {
|
|
182
|
+
dimension: {
|
|
183
|
+
type: "string",
|
|
184
|
+
enum: ["by_date", "by_restaurant", "by_party_size", "booking_pace", "pace_by_date", "by_date_restaurant", "pace_snapshot"],
|
|
185
|
+
description: "pace_snapshot: USE THIS FIRST for any demand outlook or booking pace question. Returns one row per restaurant × future date with: days_out_today, cum_demand_today, baseline_median (same-DOW historical median at the same days_out), pct_vs_baseline, flag (Well ahead/Ahead/On track/Behind/Well behind), party mix % by band (mix_small_pct/mix_standard_pct/mix_medium_pct/mix_large_pct/mix_event_pct), and mix_shift_flag highlighting the biggest mix shift vs baseline if >10pp. One call replaces 36+ multi-call pace patterns. by_date: total demand per upcoming date (all restaurants combined). by_restaurant: demand ranked by restaurant for the outlook period. by_party_size: demand distribution across party sizes. booking_pace: booking curve aggregated across all dates. pace_by_date: booking curve per restaurant × date × days_out — all restaurants included unless restaurant_name filter applied. by_date_restaurant: per-date per-restaurant demand from the latest snapshot.",
|
|
186
|
+
},
|
|
187
|
+
snapshot_date: {
|
|
188
|
+
type: "string",
|
|
189
|
+
description: "Optional. ISO date (YYYY-MM-DD) of a specific snapshot to use. Defaults to the latest available snapshot. Not applicable for booking_pace (which uses all snapshots).",
|
|
190
|
+
},
|
|
191
|
+
restaurant_name: {
|
|
192
|
+
type: "string",
|
|
193
|
+
description: "Optional. Filter to a specific restaurant.",
|
|
194
|
+
},
|
|
195
|
+
date_from: {
|
|
196
|
+
type: "string",
|
|
197
|
+
description: "Optional. Filter on the demand date (the_date) — start of range (ISO 8601).",
|
|
198
|
+
},
|
|
199
|
+
date_to: {
|
|
200
|
+
type: "string",
|
|
201
|
+
description: "Optional. Filter on the demand date (the_date) — end of range (ISO 8601).",
|
|
202
|
+
},
|
|
203
|
+
party_size: {
|
|
204
|
+
type: "string",
|
|
205
|
+
description: "Optional. Filter to a specific party size (integer as string). Most useful with booking_pace.",
|
|
206
|
+
},
|
|
207
|
+
...TENANT_PARAM,
|
|
208
|
+
...PROMPT_PARAM,
|
|
209
|
+
},
|
|
210
|
+
required: ["dimension"],
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: "get_loyalty_breakdown",
|
|
215
|
+
description: "Returns loyalty level distribution across all reservations. Shows total reservations, outcome breakdown (Done, NoShow, Cancelled for past; Confirmed, NotConfirmed for future), and revenue stats per loyalty level, plus a breakdown of booking origin by loyalty level.",
|
|
216
|
+
inputSchema: {
|
|
217
|
+
type: "object",
|
|
218
|
+
properties: { ...TENANT_PARAM },
|
|
219
|
+
required: [],
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: "get_review_stats",
|
|
224
|
+
description: "Queries the data_review_master table for guest review analytics. Use for questions about ratings, review trends, platform performance, sub-dimension scores (food/service/ambience/value), and recent negative feedback. Covers all review platforms (Google, TripAdvisor, OpenTable, etc.).",
|
|
225
|
+
inputSchema: {
|
|
226
|
+
type: "object",
|
|
227
|
+
properties: {
|
|
228
|
+
dimension: {
|
|
229
|
+
type: "string",
|
|
230
|
+
enum: ["by_restaurant", "by_category", "by_source", "by_period", "rating_distribution", "sub_ratings", "recent_low", "recent_all", "trend"],
|
|
231
|
+
description: "by_restaurant: avg overall + sub-ratings, review count, positive/negative split per restaurant. Use sort_by to rank by a specific dimension. " +
|
|
232
|
+
"by_category: same metrics rolled up by restaurant_category — use to compare brand tiers without listing every venue. " +
|
|
233
|
+
"by_source: metrics broken down by review platform (Google, TripAdvisor, OpenTable, etc.). " +
|
|
234
|
+
"by_period: rating trend over time — requires period param. " +
|
|
235
|
+
"rating_distribution: star band breakdown (1★–5★) per restaurant. " +
|
|
236
|
+
"sub_ratings: food/service/ambience/value side-by-side per restaurant with weakest dimension flag. " +
|
|
237
|
+
"recent_low: last 90 days of reviews rated below 3 (configurable via max_rating) with text excerpt — for operational alerts, up to 200 rows. " +
|
|
238
|
+
"recent_all: last 90 days of all reviews with text excerpt — use for word clouds and broad sentiment analysis; filter with min_rating/max_rating, up to 500 rows. " +
|
|
239
|
+
"trend: weekly (or monthly) ratings per restaurant across the last N periods — use to spot improving or declining sites in a single call. Control granularity with period (default week) and lookback with weeks param (default 4).",
|
|
240
|
+
},
|
|
241
|
+
period: {
|
|
242
|
+
type: "string",
|
|
243
|
+
enum: ["week", "month", "quarter", "year"],
|
|
244
|
+
description: "Required for by_period. Controls granularity for trend (default: week).",
|
|
245
|
+
},
|
|
246
|
+
sort_by: {
|
|
247
|
+
type: "string",
|
|
248
|
+
enum: ["overall", "food", "service", "ambience", "value", "reviews"],
|
|
249
|
+
description: "Optional. For by_restaurant: sort results by this dimension descending. Default: overall rating.",
|
|
250
|
+
},
|
|
251
|
+
weeks: {
|
|
252
|
+
type: "string",
|
|
253
|
+
description: "Optional. For trend: number of periods to look back (default 4, max 52). E.g. '6' returns the last 6 weeks.",
|
|
254
|
+
},
|
|
255
|
+
restaurant_name: {
|
|
256
|
+
type: "string",
|
|
257
|
+
description: "Optional. Filter to a specific restaurant.",
|
|
258
|
+
},
|
|
259
|
+
restaurant_category: {
|
|
260
|
+
type: "string",
|
|
261
|
+
description: "Optional. Filter to a restaurant category (e.g. 'Fine Dining', 'Casual'). Useful for querying all venues of the same brand tier in one call.",
|
|
262
|
+
},
|
|
263
|
+
source: {
|
|
264
|
+
type: "string",
|
|
265
|
+
description: "Optional. Filter to a specific review platform (e.g. 'Google', 'TripAdvisor', 'OpenTable').",
|
|
266
|
+
},
|
|
267
|
+
date_from: {
|
|
268
|
+
type: "string",
|
|
269
|
+
description: "Optional. Filter reviews on or after this date (ISO 8601).",
|
|
270
|
+
},
|
|
271
|
+
date_to: {
|
|
272
|
+
type: "string",
|
|
273
|
+
description: "Optional. Filter reviews on or before this date (ISO 8601).",
|
|
274
|
+
},
|
|
275
|
+
min_rating: {
|
|
276
|
+
type: "string",
|
|
277
|
+
description: "Optional. Only include reviews at or above this rating (e.g. '4' for positive reviews only). Most useful with recent_all.",
|
|
278
|
+
},
|
|
279
|
+
max_rating: {
|
|
280
|
+
type: "string",
|
|
281
|
+
description: "Optional. Only include reviews at or below this rating (e.g. '2' for very negative only). Overrides the default < 3 threshold in recent_low.",
|
|
282
|
+
},
|
|
283
|
+
...TENANT_PARAM,
|
|
284
|
+
...PROMPT_PARAM,
|
|
285
|
+
},
|
|
286
|
+
required: ["dimension"],
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
name: "get_forecast_stats",
|
|
291
|
+
description: "Queries the data_forecast_master table to analyse cover forecasts. Use this for questions about forecasted covers, no-show forecasts, walk-in forecasts, cancellation forecasts, and comparing how forecasts change between snapshot dates. Each snapshot represents a daily forecast of future service dates.",
|
|
292
|
+
inputSchema: {
|
|
293
|
+
type: "object",
|
|
294
|
+
properties: {
|
|
295
|
+
dimension: {
|
|
296
|
+
type: "string",
|
|
297
|
+
enum: ["by_date", "by_day_of_week", "by_restaurant", "by_restaurant_day_of_week", "by_service", "by_date_restaurant", "snapshot_comparison"],
|
|
298
|
+
description: "by_date: aggregated forecast by service_date — total covers by category per date. Defaults to next 90 days; use service_date_from/service_date_to to change the window. " +
|
|
299
|
+
"by_day_of_week: average covers per day of week (Mon–Sun) across ALL restaurants — use to identify which day is strongest/weakest portfolio-wide. Defaults to next 90 days. Results ordered by avg_total_completed_covers desc. " +
|
|
300
|
+
"by_restaurant: forecast totals per restaurant across the full date range — always returns one row per restaurant, no date windowing needed. " +
|
|
301
|
+
"by_restaurant_day_of_week: average covers per day of week broken down BY restaurant — USE THIS for staffing questions or comparing peak days across venues. Returns restaurant × weekday rows ordered by restaurant then avg covers desc. Defaults to next 90 days. " +
|
|
302
|
+
"by_service: forecast breakdown by service period (Breakfast, Lunch, Dinner) — always aggregated across full range, no date windowing needed. " +
|
|
303
|
+
"by_date_restaurant: detailed per-date, per-restaurant, per-service rows. REQUIRES at least one of: restaurant_name, service_date_from, service_date_to. Defaults to next 14 days for the date window. " +
|
|
304
|
+
"snapshot_comparison: compare two snapshot dates to see how the forecast has changed — requires both snapshot_date and snapshot_date_2.",
|
|
305
|
+
},
|
|
306
|
+
snapshot_date: {
|
|
307
|
+
type: "string",
|
|
308
|
+
description: "Optional. ISO date (YYYY-MM-DD) of a specific snapshot to use. Defaults to the latest available snapshot. Required for snapshot_comparison.",
|
|
309
|
+
},
|
|
310
|
+
snapshot_date_2: {
|
|
311
|
+
type: "string",
|
|
312
|
+
description: "Optional. ISO date (YYYY-MM-DD) of the second snapshot for snapshot_comparison dimension only.",
|
|
313
|
+
},
|
|
314
|
+
restaurant_name: {
|
|
315
|
+
type: "string",
|
|
316
|
+
description: "Optional. Filter to a specific restaurant.",
|
|
317
|
+
},
|
|
318
|
+
service_date_from: {
|
|
319
|
+
type: "string",
|
|
320
|
+
description: "Optional. Filter on the service date — start of range (ISO 8601).",
|
|
321
|
+
},
|
|
322
|
+
service_date_to: {
|
|
323
|
+
type: "string",
|
|
324
|
+
description: "Optional. Filter on the service date — end of range (ISO 8601).",
|
|
325
|
+
},
|
|
326
|
+
service: {
|
|
327
|
+
type: "string",
|
|
328
|
+
description: "Optional. Filter to a specific service period (e.g. 'breakfast', 'lunch', 'dinner').",
|
|
329
|
+
},
|
|
330
|
+
...TENANT_PARAM,
|
|
331
|
+
...PROMPT_PARAM,
|
|
332
|
+
},
|
|
333
|
+
required: ["dimension"],
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
],
|
|
337
|
+
}));
|
|
338
|
+
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
339
|
+
const { name, arguments: args } = request.params;
|
|
340
|
+
const a = (args ?? {});
|
|
341
|
+
// Pass tool name + optional user prompt to API for query logging
|
|
342
|
+
(0, api_js_1.setMcpContext)(name, a._user_prompt);
|
|
343
|
+
try {
|
|
344
|
+
switch (name) {
|
|
345
|
+
case "get_schema": {
|
|
346
|
+
const schema = await (0, tools_js_1.getSchema)();
|
|
347
|
+
return { content: [{ type: "text", text: schema }] };
|
|
348
|
+
}
|
|
349
|
+
case "query_reservations": {
|
|
350
|
+
if (!a.where_clause) {
|
|
351
|
+
return {
|
|
352
|
+
content: [{ type: "text", text: "Error: where_clause is required." }],
|
|
353
|
+
isError: true,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
const rows = await (0, tools_js_1.queryReservations)(a.where_clause, a.tenant_code);
|
|
357
|
+
return {
|
|
358
|
+
content: [
|
|
359
|
+
{
|
|
360
|
+
type: "text",
|
|
361
|
+
text: rows.length === 0
|
|
362
|
+
? "No rows matched the given WHERE clause."
|
|
363
|
+
: JSON.stringify(rows, null, 2),
|
|
364
|
+
},
|
|
365
|
+
],
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
case "get_summary_stats": {
|
|
369
|
+
if (!a.group_by_field) {
|
|
370
|
+
return {
|
|
371
|
+
content: [{ type: "text", text: "Error: group_by_field is required." }],
|
|
372
|
+
isError: true,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
const stats = await (0, tools_js_1.getSummaryStats)(a.group_by_field, a.tenant_code);
|
|
376
|
+
return {
|
|
377
|
+
content: [{ type: "text", text: JSON.stringify(stats, null, 2) }],
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
case "get_guest_behaviour_stats": {
|
|
381
|
+
if (!a.dimension) {
|
|
382
|
+
return {
|
|
383
|
+
content: [{ type: "text", text: "Error: dimension is required." }],
|
|
384
|
+
isError: true,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
const data = await (0, tools_js_1.getGuestBehaviour)(a.dimension, a.date_from, a.date_to, a.tenant_code);
|
|
388
|
+
return {
|
|
389
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
case "get_revenue_stats": {
|
|
393
|
+
if (!a.dimension) {
|
|
394
|
+
return { content: [{ type: "text", text: "Error: dimension is required." }], isError: true };
|
|
395
|
+
}
|
|
396
|
+
const data = await (0, tools_js_1.getRevenueStats)(a.dimension, a.period, a.date_from, a.date_to, a.tenant_code);
|
|
397
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
398
|
+
}
|
|
399
|
+
case "get_time_pattern_stats": {
|
|
400
|
+
if (!a.dimension) {
|
|
401
|
+
return { content: [{ type: "text", text: "Error: dimension is required." }], isError: true };
|
|
402
|
+
}
|
|
403
|
+
const data = await (0, tools_js_1.getTimePatternStats)(a.dimension, a.date_from, a.date_to, a.tenant_code);
|
|
404
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
405
|
+
}
|
|
406
|
+
case "get_date_stats": {
|
|
407
|
+
if (!a.period) {
|
|
408
|
+
return {
|
|
409
|
+
content: [{ type: "text", text: "Error: period is required." }],
|
|
410
|
+
isError: true,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
const stats = await (0, tools_js_1.getDateStats)(a.period, a.group_by, a.date_from, a.date_to, a.tenant_code, a.restaurant_name);
|
|
414
|
+
return {
|
|
415
|
+
content: [{ type: "text", text: JSON.stringify(stats, null, 2) }],
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
case "get_demand_outlook": {
|
|
419
|
+
if (!a.dimension) {
|
|
420
|
+
return { content: [{ type: "text", text: "Error: dimension is required." }], isError: true };
|
|
421
|
+
}
|
|
422
|
+
const data = await (0, tools_js_1.getDemandOutlook)(a.dimension, a.snapshot_date, a.restaurant_name, a.date_from, a.date_to, a.party_size, a.tenant_code);
|
|
423
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
424
|
+
}
|
|
425
|
+
case "get_loyalty_breakdown": {
|
|
426
|
+
const breakdown = await (0, tools_js_1.getLoyaltyBreakdown)(a.tenant_code);
|
|
427
|
+
return {
|
|
428
|
+
content: [{ type: "text", text: JSON.stringify(breakdown, null, 2) }],
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
case "get_review_stats": {
|
|
432
|
+
if (!a.dimension) {
|
|
433
|
+
return { content: [{ type: "text", text: "Error: dimension is required." }], isError: true };
|
|
434
|
+
}
|
|
435
|
+
const data = await (0, tools_js_1.getReviewStats)(a.dimension, a.restaurant_name, a.restaurant_category, a.source, a.date_from, a.date_to, a.period, a.min_rating, a.max_rating, a.sort_by, a.weeks, a.tenant_code);
|
|
436
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
437
|
+
}
|
|
438
|
+
case "get_forecast_stats": {
|
|
439
|
+
if (!a.dimension) {
|
|
440
|
+
return { content: [{ type: "text", text: "Error: dimension is required." }], isError: true };
|
|
441
|
+
}
|
|
442
|
+
const data = await (0, tools_js_1.getForecastStats)(a.dimension, a.snapshot_date, a.restaurant_name, a.service_date_from, a.service_date_to, a.service, a.snapshot_date_2, a.tenant_code);
|
|
443
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
444
|
+
}
|
|
445
|
+
default:
|
|
446
|
+
return {
|
|
447
|
+
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
448
|
+
isError: true,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
catch (err) {
|
|
453
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
454
|
+
return {
|
|
455
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
456
|
+
isError: true,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
async function main() {
|
|
461
|
+
await (0, api_js_1.testConnection)();
|
|
462
|
+
console.error("API connection established.");
|
|
463
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
464
|
+
await server.connect(transport);
|
|
465
|
+
console.error("MCP server running on stdio.");
|
|
466
|
+
}
|
|
467
|
+
main().catch((err) => {
|
|
468
|
+
console.error("Fatal error:", err);
|
|
469
|
+
process.exit(1);
|
|
470
|
+
});
|
|
471
|
+
process.on("SIGINT", () => process.exit(0));
|
package/dist/safety.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateWhereClause = validateWhereClause;
|
|
4
|
+
exports.validateGroupByField = validateGroupByField;
|
|
5
|
+
const BLOCKED_KEYWORDS = /\b(INSERT|UPDATE|DELETE|DROP|TRUNCATE|ALTER|CREATE|GRANT|REVOKE|EXEC|EXECUTE|CALL)\b/i;
|
|
6
|
+
const BLOCKED_CHARS = /;/;
|
|
7
|
+
function validateWhereClause(clause) {
|
|
8
|
+
if (BLOCKED_KEYWORDS.test(clause)) {
|
|
9
|
+
return { ok: false, reason: "WHERE clause contains a disallowed keyword (only read operations are permitted)." };
|
|
10
|
+
}
|
|
11
|
+
if (BLOCKED_CHARS.test(clause)) {
|
|
12
|
+
return { ok: false, reason: "WHERE clause must not contain semicolons." };
|
|
13
|
+
}
|
|
14
|
+
return { ok: true };
|
|
15
|
+
}
|
|
16
|
+
function validateGroupByField(field) {
|
|
17
|
+
// Only allow simple column name identifiers (letters, numbers, underscores)
|
|
18
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(field)) {
|
|
19
|
+
return { ok: false, reason: `Invalid field name: "${field}". Only alphanumeric characters and underscores are allowed.` };
|
|
20
|
+
}
|
|
21
|
+
return { ok: true };
|
|
22
|
+
}
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getSchema = getSchema;
|
|
4
|
+
exports.queryReservations = queryReservations;
|
|
5
|
+
exports.getSummaryStats = getSummaryStats;
|
|
6
|
+
exports.getLoyaltyBreakdown = getLoyaltyBreakdown;
|
|
7
|
+
exports.getGuestBehaviour = getGuestBehaviour;
|
|
8
|
+
exports.getRevenueStats = getRevenueStats;
|
|
9
|
+
exports.getTimePatternStats = getTimePatternStats;
|
|
10
|
+
exports.getDemandOutlook = getDemandOutlook;
|
|
11
|
+
exports.getReviewStats = getReviewStats;
|
|
12
|
+
exports.getForecastStats = getForecastStats;
|
|
13
|
+
exports.getDateStats = getDateStats;
|
|
14
|
+
const safety_js_1 = require("./safety.js");
|
|
15
|
+
const api_js_1 = require("./api.js");
|
|
16
|
+
async function getSchema() {
|
|
17
|
+
const { schema } = await (0, api_js_1.apiGetSchema)();
|
|
18
|
+
return schema;
|
|
19
|
+
}
|
|
20
|
+
async function queryReservations(whereClause, tenantCode) {
|
|
21
|
+
const check = (0, safety_js_1.validateWhereClause)(whereClause);
|
|
22
|
+
if (!check.ok) {
|
|
23
|
+
throw new Error(`Query blocked: ${check.reason}`);
|
|
24
|
+
}
|
|
25
|
+
const { rows } = await (0, api_js_1.apiQueryReservations)(whereClause, tenantCode);
|
|
26
|
+
return rows;
|
|
27
|
+
}
|
|
28
|
+
async function getSummaryStats(groupByField, tenantCode) {
|
|
29
|
+
const check = (0, safety_js_1.validateGroupByField)(groupByField);
|
|
30
|
+
if (!check.ok) {
|
|
31
|
+
throw new Error(`Invalid group-by field: ${check.reason}`);
|
|
32
|
+
}
|
|
33
|
+
const { stats } = await (0, api_js_1.apiGetSummaryStats)(groupByField, tenantCode);
|
|
34
|
+
return stats;
|
|
35
|
+
}
|
|
36
|
+
async function getLoyaltyBreakdown(tenantCode) {
|
|
37
|
+
return (0, api_js_1.apiGetLoyaltyBreakdown)(tenantCode);
|
|
38
|
+
}
|
|
39
|
+
async function getGuestBehaviour(dimension, dateFrom, dateTo, tenantCode) {
|
|
40
|
+
const { data } = await (0, api_js_1.apiGetGuestBehaviour)(dimension, dateFrom, dateTo, tenantCode);
|
|
41
|
+
return data;
|
|
42
|
+
}
|
|
43
|
+
async function getRevenueStats(dimension, period, dateFrom, dateTo, tenantCode) {
|
|
44
|
+
const { data } = await (0, api_js_1.apiGetRevenueStats)(dimension, period, dateFrom, dateTo, tenantCode);
|
|
45
|
+
return data;
|
|
46
|
+
}
|
|
47
|
+
async function getTimePatternStats(dimension, dateFrom, dateTo, tenantCode) {
|
|
48
|
+
const { data } = await (0, api_js_1.apiGetTimePatternStats)(dimension, dateFrom, dateTo, tenantCode);
|
|
49
|
+
return data;
|
|
50
|
+
}
|
|
51
|
+
async function getDemandOutlook(dimension, snapshotDate, restaurantName, dateFrom, dateTo, partySize, tenantCode) {
|
|
52
|
+
const { data } = await (0, api_js_1.apiGetDemandOutlook)(dimension, snapshotDate, restaurantName, dateFrom, dateTo, partySize, tenantCode);
|
|
53
|
+
return data;
|
|
54
|
+
}
|
|
55
|
+
async function getReviewStats(dimension, restaurantName, restaurantCategory, source, dateFrom, dateTo, period, minRating, maxRating, sortBy, weeks, tenantCode) {
|
|
56
|
+
const { data } = await (0, api_js_1.apiGetReviewStats)(dimension, restaurantName, restaurantCategory, source, dateFrom, dateTo, period, minRating, maxRating, sortBy, weeks, tenantCode);
|
|
57
|
+
return data;
|
|
58
|
+
}
|
|
59
|
+
async function getForecastStats(dimension, snapshotDate, restaurantName, serviceDateFrom, serviceDateTo, service, snapshotDate2, tenantCode) {
|
|
60
|
+
const { data } = await (0, api_js_1.apiGetForecastStats)(dimension, snapshotDate, restaurantName, serviceDateFrom, serviceDateTo, service, snapshotDate2, tenantCode);
|
|
61
|
+
return data;
|
|
62
|
+
}
|
|
63
|
+
async function getDateStats(period, groupBy, dateFrom, dateTo, tenantCode, restaurantName) {
|
|
64
|
+
const { stats } = await (0, api_js_1.apiGetDateStats)(period, groupBy, dateFrom, dateTo, tenantCode, restaurantName);
|
|
65
|
+
return stats;
|
|
66
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "distil-ai-rms-reporting",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for restaurant management system reporting and analytics",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"distil-ai-rms-reporting": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"start": "node dist/index.js",
|
|
15
|
+
"dev": "ts-node src/index.ts",
|
|
16
|
+
"prepublishOnly": "npm run build"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"mcp",
|
|
20
|
+
"restaurant",
|
|
21
|
+
"analytics",
|
|
22
|
+
"reporting",
|
|
23
|
+
"claude"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"license": "UNLICENSED",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
31
|
+
"dotenv": "^16.4.5"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^20.14.0",
|
|
35
|
+
"ts-node": "^10.9.2",
|
|
36
|
+
"typescript": "^5.5.3"
|
|
37
|
+
}
|
|
38
|
+
}
|