food402 1.0.2 → 1.0.4-beta.1

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.
@@ -0,0 +1,455 @@
1
+ #!/usr/bin/env node
2
+ // src/index.ts - TGO Yemek MCP Server
3
+ import "dotenv/config";
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { z } from "zod";
7
+ import { getAddresses, getRestaurants, getRestaurantMenu, getProductDetails, getProductRecommendations, setShippingAddress, addToBasket, getBasket, removeFromBasket, clearBasket, searchRestaurants, getCities, getDistricts, getNeighborhoods, addAddress, getSavedCards, getCheckoutReady, placeOrder, getOrders, getOrderDetail, updateCustomerNote, } from "./api.js";
8
+ const server = new McpServer({
9
+ name: "tgo-yemek",
10
+ version: "1.0.0",
11
+ });
12
+ // Prompt: order_food - Main entry point for food ordering
13
+ server.prompt("order_food", "Start a food ordering session. Always begin by asking user to select a delivery address.", async () => {
14
+ // Fetch addresses to include in the prompt
15
+ try {
16
+ const addressesResult = await getAddresses();
17
+ const addressList = addressesResult.addresses
18
+ .map((a, i) => `${i + 1}. ${a.addressName} - ${a.addressLine}, ${a.neighborhoodName}, ${a.districtName} (ID: ${a.id})`)
19
+ .join("\n");
20
+ return {
21
+ messages: [
22
+ {
23
+ role: "user",
24
+ content: {
25
+ type: "text",
26
+ text: `I want to order food. Here are my saved addresses:\n\n${addressList}\n\nWhich address should I deliver to?`
27
+ }
28
+ }
29
+ ]
30
+ };
31
+ }
32
+ catch (error) {
33
+ return {
34
+ messages: [
35
+ {
36
+ role: "user",
37
+ content: {
38
+ type: "text",
39
+ text: "I want to order food. Please fetch my addresses first using get_addresses and ask me which one to use for delivery."
40
+ }
41
+ }
42
+ ]
43
+ };
44
+ }
45
+ });
46
+ // Helper to format successful responses
47
+ function formatResponse(data) {
48
+ return {
49
+ content: [
50
+ {
51
+ type: "text",
52
+ text: JSON.stringify(data, null, 2),
53
+ },
54
+ ],
55
+ };
56
+ }
57
+ // Helper to format error responses
58
+ function formatError(error) {
59
+ const message = error instanceof Error ? error.message : String(error);
60
+ return {
61
+ content: [
62
+ {
63
+ type: "text",
64
+ text: `Error: ${message}`,
65
+ },
66
+ ],
67
+ isError: true,
68
+ };
69
+ }
70
+ // Tool: get_addresses
71
+ server.tool("get_addresses", "Get user's saved delivery addresses. User must select an address with select_address before browsing restaurants.", {}, async () => {
72
+ try {
73
+ const result = await getAddresses();
74
+ return formatResponse(result);
75
+ }
76
+ catch (error) {
77
+ return formatError(error);
78
+ }
79
+ });
80
+ // Tool: select_address
81
+ server.tool("select_address", "Select a delivery address. MUST be called before get_restaurants or add_to_basket. Sets the shipping address for the cart.", {
82
+ addressId: z.number().describe("Address ID from get_addresses"),
83
+ }, async (args) => {
84
+ try {
85
+ // Set shipping address for cart
86
+ await setShippingAddress({
87
+ shippingAddressId: args.addressId,
88
+ invoiceAddressId: args.addressId,
89
+ });
90
+ // Get address details to return to user
91
+ const addressesResult = await getAddresses();
92
+ const selectedAddress = addressesResult.addresses.find(a => a.id === args.addressId);
93
+ if (!selectedAddress) {
94
+ return formatResponse({
95
+ success: true,
96
+ message: "Shipping address set successfully",
97
+ addressId: args.addressId
98
+ });
99
+ }
100
+ return formatResponse({
101
+ success: true,
102
+ message: "Delivery address selected",
103
+ address: {
104
+ id: selectedAddress.id,
105
+ name: selectedAddress.addressName,
106
+ addressLine: selectedAddress.addressLine,
107
+ neighborhood: selectedAddress.neighborhoodName,
108
+ district: selectedAddress.districtName,
109
+ city: selectedAddress.cityName,
110
+ latitude: selectedAddress.latitude,
111
+ longitude: selectedAddress.longitude
112
+ }
113
+ });
114
+ }
115
+ catch (error) {
116
+ return formatError(error);
117
+ }
118
+ });
119
+ // Tool: get_restaurants
120
+ server.tool("get_restaurants", "Search restaurants near a location. Requires select_address to be called first.", {
121
+ latitude: z.string().describe("Latitude coordinate from selected address"),
122
+ longitude: z.string().describe("Longitude coordinate from selected address"),
123
+ page: z.number().optional().describe("Page number for pagination (default: 1)"),
124
+ }, async (args) => {
125
+ try {
126
+ const result = await getRestaurants(args.latitude, args.longitude, args.page ?? 1);
127
+ return formatResponse(result);
128
+ }
129
+ catch (error) {
130
+ return formatError(error);
131
+ }
132
+ });
133
+ // Tool: get_restaurant_menu
134
+ server.tool("get_restaurant_menu", "Get a restaurant's full menu with categories and items", {
135
+ restaurantId: z.number().describe("Restaurant ID"),
136
+ latitude: z.string().describe("Latitude coordinate"),
137
+ longitude: z.string().describe("Longitude coordinate"),
138
+ }, async (args) => {
139
+ try {
140
+ const result = await getRestaurantMenu(args.restaurantId, args.latitude, args.longitude);
141
+ return formatResponse(result);
142
+ }
143
+ catch (error) {
144
+ return formatError(error);
145
+ }
146
+ });
147
+ // Tool: get_product_details
148
+ server.tool("get_product_details", "Get product customization options (ingredients, modifiers)", {
149
+ restaurantId: z.number().describe("Restaurant ID"),
150
+ productId: z.number().describe("Product ID"),
151
+ latitude: z.string().describe("Latitude coordinate"),
152
+ longitude: z.string().describe("Longitude coordinate"),
153
+ }, async (args) => {
154
+ try {
155
+ const result = await getProductDetails(args.restaurantId, args.productId, args.latitude, args.longitude);
156
+ return formatResponse(result);
157
+ }
158
+ catch (error) {
159
+ return formatError(error);
160
+ }
161
+ });
162
+ // Tool: get_product_recommendations
163
+ server.tool("get_product_recommendations", "Get 'goes well with' suggestions for products", {
164
+ restaurantId: z.number().describe("Restaurant ID"),
165
+ productIds: z.array(z.number()).describe("Array of product IDs to get recommendations for"),
166
+ }, async (args) => {
167
+ try {
168
+ const result = await getProductRecommendations(args.restaurantId, args.productIds);
169
+ return formatResponse(result);
170
+ }
171
+ catch (error) {
172
+ return formatError(error);
173
+ }
174
+ });
175
+ // Simplified schemas for add_to_basket (avoiding recursive $ref which breaks some MCP clients)
176
+ const ModifierProductSchema = z.object({
177
+ productId: z.number().describe("Selected option's product ID"),
178
+ modifierGroupId: z.number().describe("The modifier group this belongs to"),
179
+ });
180
+ const BasketItemSchema = z.object({
181
+ productId: z.number().describe("Product ID to add"),
182
+ quantity: z.number().describe("Quantity to add"),
183
+ modifierProducts: z.array(ModifierProductSchema).optional().describe("Selected modifiers (optional)"),
184
+ excludeIngredientIds: z.array(z.number()).optional().describe("IDs of ingredients to exclude (optional)"),
185
+ });
186
+ // Tool: add_to_basket
187
+ server.tool("add_to_basket", "Add items to the shopping cart. Requires select_address to be called first.", {
188
+ storeId: z.number().describe("Restaurant ID"),
189
+ items: z.array(BasketItemSchema).describe("Items to add to basket"),
190
+ latitude: z.number().describe("Latitude coordinate (number)"),
191
+ longitude: z.number().describe("Longitude coordinate (number)"),
192
+ }, async (args) => {
193
+ try {
194
+ // Transform simplified schema to full API format
195
+ const items = args.items.map((item) => ({
196
+ productId: item.productId,
197
+ quantity: item.quantity,
198
+ modifierProducts: (item.modifierProducts || []).map((mod) => ({
199
+ productId: mod.productId,
200
+ modifierGroupId: mod.modifierGroupId,
201
+ modifierProducts: [],
202
+ ingredientOptions: { excludes: [], includes: [] },
203
+ })),
204
+ ingredientOptions: {
205
+ excludes: (item.excludeIngredientIds || []).map((id) => ({ id })),
206
+ includes: [],
207
+ },
208
+ }));
209
+ const result = await addToBasket({
210
+ storeId: args.storeId,
211
+ items,
212
+ latitude: args.latitude,
213
+ longitude: args.longitude,
214
+ isFlashSale: false,
215
+ storePickup: false,
216
+ });
217
+ return formatResponse(result);
218
+ }
219
+ catch (error) {
220
+ return formatError(error);
221
+ }
222
+ });
223
+ // Tool: get_basket
224
+ server.tool("get_basket", "Get current cart contents", {}, async () => {
225
+ try {
226
+ const result = await getBasket();
227
+ return formatResponse(result);
228
+ }
229
+ catch (error) {
230
+ return formatError(error);
231
+ }
232
+ });
233
+ // Tool: remove_from_basket
234
+ server.tool("remove_from_basket", "Remove an item from the cart", {
235
+ itemId: z.string().describe("Item UUID from the cart (from get_basket response)"),
236
+ }, async (args) => {
237
+ try {
238
+ const result = await removeFromBasket(args.itemId);
239
+ return formatResponse(result);
240
+ }
241
+ catch (error) {
242
+ return formatError(error);
243
+ }
244
+ });
245
+ // Tool: clear_basket
246
+ server.tool("clear_basket", "Clear the entire cart", {}, async () => {
247
+ try {
248
+ await clearBasket();
249
+ return formatResponse({ success: true, message: "Basket cleared successfully" });
250
+ }
251
+ catch (error) {
252
+ return formatError(error);
253
+ }
254
+ });
255
+ // Tool: search_restaurants
256
+ server.tool("search_restaurants", "Search restaurants and products by keyword", {
257
+ searchQuery: z.string().describe("Search keyword (e.g., 'dürüm', 'pizza', 'burger')"),
258
+ latitude: z.string().describe("Latitude coordinate"),
259
+ longitude: z.string().describe("Longitude coordinate"),
260
+ page: z.number().optional().describe("Page number for pagination (default: 1)"),
261
+ }, async (args) => {
262
+ try {
263
+ const result = await searchRestaurants(args.searchQuery, args.latitude, args.longitude, args.page ?? 1);
264
+ return formatResponse(result);
265
+ }
266
+ catch (error) {
267
+ return formatError(error);
268
+ }
269
+ });
270
+ // Tool: get_cities
271
+ server.tool("get_cities", "Get list of all cities for address selection", {}, async () => {
272
+ try {
273
+ const result = await getCities();
274
+ return formatResponse(result);
275
+ }
276
+ catch (error) {
277
+ return formatError(error);
278
+ }
279
+ });
280
+ // Tool: get_districts
281
+ server.tool("get_districts", "Get districts for a city", {
282
+ cityId: z.number().describe("City ID"),
283
+ }, async (args) => {
284
+ try {
285
+ const result = await getDistricts(args.cityId);
286
+ return formatResponse(result);
287
+ }
288
+ catch (error) {
289
+ return formatError(error);
290
+ }
291
+ });
292
+ // Tool: get_neighborhoods
293
+ server.tool("get_neighborhoods", "Get neighborhoods for a district", {
294
+ districtId: z.number().describe("District ID"),
295
+ }, async (args) => {
296
+ try {
297
+ const result = await getNeighborhoods(args.districtId);
298
+ return formatResponse(result);
299
+ }
300
+ catch (error) {
301
+ return formatError(error);
302
+ }
303
+ });
304
+ // Tool: add_address
305
+ server.tool("add_address", "Add a new delivery address. Use get_cities, get_districts, get_neighborhoods to find location IDs first.", {
306
+ name: z.string().describe("First name"),
307
+ surname: z.string().describe("Last name"),
308
+ phone: z.string().describe("Phone number without country code (e.g., '5356437070')"),
309
+ addressName: z.string().describe("Name for this address (e.g., 'Home', 'Work')"),
310
+ addressLine: z.string().describe("Street address"),
311
+ cityId: z.number().describe("City ID (from get_cities)"),
312
+ districtId: z.number().describe("District ID (from get_districts)"),
313
+ neighborhoodId: z.number().describe("Neighborhood ID (from get_neighborhoods)"),
314
+ latitude: z.string().describe("Latitude coordinate"),
315
+ longitude: z.string().describe("Longitude coordinate"),
316
+ apartmentNumber: z.string().optional().describe("Apartment/building number"),
317
+ floor: z.string().optional().describe("Floor number"),
318
+ doorNumber: z.string().optional().describe("Door number"),
319
+ addressDescription: z.string().optional().describe("Additional details/directions"),
320
+ elevatorAvailable: z.boolean().optional().describe("Whether elevator is available"),
321
+ }, async (args) => {
322
+ try {
323
+ const result = await addAddress({
324
+ name: args.name,
325
+ surname: args.surname,
326
+ phone: args.phone,
327
+ addressName: args.addressName,
328
+ addressLine: args.addressLine,
329
+ cityId: args.cityId,
330
+ districtId: args.districtId,
331
+ neighborhoodId: args.neighborhoodId,
332
+ latitude: args.latitude,
333
+ longitude: args.longitude,
334
+ apartmentNumber: args.apartmentNumber,
335
+ floor: args.floor,
336
+ doorNumber: args.doorNumber,
337
+ addressDescription: args.addressDescription,
338
+ elevatorAvailable: args.elevatorAvailable,
339
+ });
340
+ return formatResponse(result);
341
+ }
342
+ catch (error) {
343
+ return formatError(error);
344
+ }
345
+ });
346
+ // Tool: get_saved_cards
347
+ server.tool("get_saved_cards", "Get user's saved payment cards (masked). If no cards, user must add one on the website.", {}, async () => {
348
+ try {
349
+ const result = await getSavedCards();
350
+ return formatResponse(result);
351
+ }
352
+ catch (error) {
353
+ return formatError(error);
354
+ }
355
+ });
356
+ // Tool: checkout_ready
357
+ server.tool("checkout_ready", "Get basket ready for checkout with payment context. Call this before placing an order.", {}, async () => {
358
+ try {
359
+ const result = await getCheckoutReady();
360
+ return formatResponse(result);
361
+ }
362
+ catch (error) {
363
+ return formatError(error);
364
+ }
365
+ });
366
+ // Tool: set_order_note
367
+ server.tool("set_order_note", "Set order note and service preferences. Call before place_order.", {
368
+ note: z.string().optional().describe("Note for courier/restaurant"),
369
+ noServiceWare: z.boolean().optional().describe("Don't include plastic/cutlery (default: false)"),
370
+ contactlessDelivery: z.boolean().optional().describe("Leave at door (default: false)"),
371
+ dontRingBell: z.boolean().optional().describe("Don't ring doorbell (default: false)"),
372
+ }, async (args) => {
373
+ try {
374
+ await updateCustomerNote({
375
+ customerNote: args.note ?? "",
376
+ noServiceWare: args.noServiceWare ?? false,
377
+ contactlessDelivery: args.contactlessDelivery ?? false,
378
+ dontRingBell: args.dontRingBell ?? false,
379
+ });
380
+ return formatResponse({
381
+ success: true,
382
+ message: "Order note and preferences saved"
383
+ });
384
+ }
385
+ catch (error) {
386
+ return formatError(error);
387
+ }
388
+ });
389
+ // Tool: place_order
390
+ server.tool("place_order", "Place the order using a saved card with 3D Secure. Opens browser for bank verification if needed.", {
391
+ cardId: z.number().describe("Card ID from get_saved_cards"),
392
+ }, async (args) => {
393
+ try {
394
+ const result = await placeOrder(args.cardId);
395
+ // If 3D Secure is required and we have HTML content, open it in browser
396
+ if (result.requires3DSecure && result.htmlContent) {
397
+ const { writeFileSync } = await import("fs");
398
+ const { execSync } = await import("child_process");
399
+ const { tmpdir } = await import("os");
400
+ const { join } = await import("path");
401
+ const tempFile = join(tmpdir(), `3dsecure_${Date.now()}.html`);
402
+ writeFileSync(tempFile, result.htmlContent);
403
+ // Open in default browser (works on macOS, Linux, Windows)
404
+ const platform = process.platform;
405
+ if (platform === "darwin") {
406
+ execSync(`open "${tempFile}"`);
407
+ }
408
+ else if (platform === "win32") {
409
+ execSync(`start "" "${tempFile}"`);
410
+ }
411
+ else {
412
+ execSync(`xdg-open "${tempFile}"`);
413
+ }
414
+ return formatResponse({
415
+ ...result,
416
+ htmlContent: undefined, // Don't return the full HTML in response
417
+ browserOpened: true,
418
+ message: "3D Secure verification page opened in browser. Complete the payment there."
419
+ });
420
+ }
421
+ return formatResponse(result);
422
+ }
423
+ catch (error) {
424
+ return formatError(error);
425
+ }
426
+ });
427
+ // Tool: get_orders
428
+ server.tool("get_orders", "Get user's order history with status", {
429
+ page: z.number().optional().describe("Page number (default: 1)"),
430
+ }, async (args) => {
431
+ try {
432
+ const result = await getOrders(args.page ?? 1);
433
+ return formatResponse(result);
434
+ }
435
+ catch (error) {
436
+ return formatError(error);
437
+ }
438
+ });
439
+ // Tool: get_order_detail
440
+ server.tool("get_order_detail", "Get detailed information about a specific order including delivery status", {
441
+ orderId: z.string().describe("Order ID from get_orders"),
442
+ }, async (args) => {
443
+ try {
444
+ const result = await getOrderDetail(args.orderId);
445
+ return formatResponse(result);
446
+ }
447
+ catch (error) {
448
+ return formatError(error);
449
+ }
450
+ });
451
+ async function main() {
452
+ const transport = new StdioServerTransport();
453
+ await server.connect(transport);
454
+ }
455
+ main().catch(console.error);
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
3
+ import { join } from 'path';
4
+ // Find project root (where npm install was run)
5
+ const projectRoot = process.env.INIT_CWD || process.cwd();
6
+ const mcpJsonPath = join(projectRoot, '.mcp.json');
7
+ const food402Config = {
8
+ command: "node",
9
+ args: ["./node_modules/food402/dist/index.js"],
10
+ env: {
11
+ TGO_EMAIL: "your-email@example.com",
12
+ TGO_PASSWORD: "your-password"
13
+ }
14
+ };
15
+ let config = { mcpServers: {} };
16
+ if (existsSync(mcpJsonPath)) {
17
+ try {
18
+ config = JSON.parse(readFileSync(mcpJsonPath, 'utf-8'));
19
+ if (!config.mcpServers)
20
+ config.mcpServers = {};
21
+ }
22
+ catch {
23
+ // Invalid JSON, start fresh
24
+ }
25
+ }
26
+ config.mcpServers.food402 = food402Config;
27
+ writeFileSync(mcpJsonPath, JSON.stringify(config, null, 2) + '\n');
28
+ console.log('✓ Added food402 to .mcp.json');
29
+ console.log('→ Update TGO_EMAIL and TGO_PASSWORD in .mcp.json with your credentials');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "food402",
3
- "version": "1.0.2",
3
+ "version": "1.0.4-beta.1",
4
4
  "description": "MCP server for ordering food from TGO Yemek",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,6 +12,7 @@
12
12
  "scripts": {
13
13
  "start": "tsx src/index.ts",
14
14
  "build": "tsc",
15
+ "postinstall": "node dist/postinstall.js",
15
16
  "prepublishOnly": "npm run build"
16
17
  },
17
18
  "keywords": [