UNPKG

@tomaspavlin/rohlik-mcp

Version:

MCP server for controlling Rohlik.cz grocery shopping website

329 lines 12.4 kB
import fetch from 'node-fetch'; const BASE_URL = process.env.ROHLIK_BASE_URL || 'https://www.rohlik.cz'; export class RohlikAPIError extends Error { status; constructor(message, status) { super(message); this.status = status; this.name = 'RohlikAPIError'; } } export class RohlikAPI { credentials; userId; addressId; sessionCookies = ''; constructor(credentials) { this.credentials = credentials; } async makeRequest(url, options = {}) { const headers = { 'Content-Type': 'application/json', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', ...(this.sessionCookies && { Cookie: this.sessionCookies }), ...(options.headers || {}) }; const response = await fetch(`${BASE_URL}${url}`, { ...options, headers }); // Store cookies for session management const setCookieHeader = response.headers.get('set-cookie'); if (setCookieHeader) { this.sessionCookies = setCookieHeader; } if (!response.ok) { throw new RohlikAPIError(`HTTP ${response.status}: ${response.statusText}`, response.status); } return await response.json(); } async login() { const loginData = { email: this.credentials.username, password: this.credentials.password, name: '' }; const response = await this.makeRequest('/services/frontend-service/login', { method: 'POST', body: JSON.stringify(loginData) }); if (response.status !== 200) { if (response.status === 401) { throw new RohlikAPIError('Invalid credentials', 401); } throw new RohlikAPIError(`Login failed: ${response.messages?.[0]?.content || 'Unknown error'}`); } this.userId = response.data?.user?.id; this.addressId = response.data?.address?.id; } async logout() { await this.makeRequest('/services/frontend-service/logout', { method: 'POST' }); this.sessionCookies = ''; } async searchProducts(productName, limit = 10, favouriteOnly = false) { await this.login(); try { const searchParams = new URLSearchParams({ search: productName, offset: '0', limit: String(limit + 5), companyId: '1', filterData: JSON.stringify({ filters: [] }), canCorrect: 'true' }); const response = await this.makeRequest(`/services/frontend-service/search-metadata?${searchParams}`); let products = response.data?.productList || []; // Remove sponsored content products = products.filter((p) => !p.badge?.some((badge) => badge.slug === 'promoted')); // Filter favourites if requested if (favouriteOnly) { products = products.filter((p) => p.favourite); } // Limit results products = products.slice(0, limit); return products.map((p) => ({ id: p.productId, name: p.productName, price: `${p.price.full} ${p.price.currency}`, brand: p.brand, amount: p.textualAmount })); } finally { await this.logout(); } } async addToCart(products) { await this.login(); try { const addedProducts = []; for (const product of products) { try { const payload = { actionId: null, productId: product.product_id, quantity: product.quantity, recipeId: null, source: 'true:Shopping Lists' }; await this.makeRequest('/services/frontend-service/v2/cart', { method: 'POST', body: JSON.stringify(payload) }); addedProducts.push(product.product_id); } catch (error) { console.error(`Failed to add product ${product.product_id}:`, error); } } return addedProducts; } finally { await this.logout(); } } async getCartContent() { await this.login(); try { const response = await this.makeRequest('/services/frontend-service/v2/cart'); const data = response.data || {}; return { total_price: data.totalPrice || 0, total_items: Object.keys(data.items || {}).length, can_make_order: data.submitConditionPassed || false, products: Object.entries(data.items || {}).map(([productId, productData]) => ({ id: productId, cart_item_id: productData.orderFieldId || '', name: productData.productName || '', quantity: productData.quantity || 0, price: productData.price || 0, category_name: productData.primaryCategoryName || '', brand: productData.brand || '' })) }; } finally { await this.logout(); } } async removeFromCart(orderFieldId) { await this.login(); try { await this.makeRequest(`/services/frontend-service/v2/cart?orderFieldId=${orderFieldId}`, { method: 'DELETE' }); return true; } catch (error) { console.error(`Failed to remove item ${orderFieldId}:`, error); return false; } finally { await this.logout(); } } async getShoppingList(shoppingListId) { await this.login(); try { const response = await this.makeRequest(`/api/v1/shopping-lists/id/${shoppingListId}`); // Handle both wrapped and direct responses const listData = response.data || response; return { name: listData?.name || 'Unknown List', products: listData?.products || [] }; } finally { await this.logout(); } } async getAccountData() { await this.login(); try { const result = {}; // Define endpoints similar to the Python implementation const endpoints = { delivery: '/services/frontend-service/first-delivery?reasonableDeliveryTime=true', next_order: '/api/v3/orders/upcoming', announcements: '/services/frontend-service/announcements/top', bags: '/api/v1/reusable-bags/user-info', timeslot: '/services/frontend-service/v1/timeslot-reservation', last_order: '/api/v3/orders/delivered?offset=0&limit=1', premium_profile: '/services/frontend-service/premium/profile', delivery_announcements: '/services/frontend-service/announcements/delivery', delivered_orders: '/api/v3/orders/delivered?offset=0&limit=50' }; // Fetch data from all endpoints for (const [endpoint, path] of Object.entries(endpoints)) { try { const response = await this.makeRequest(path); result[endpoint] = response.data || response; } catch (error) { console.error(`Error fetching ${endpoint}:`, error); result[endpoint] = null; } } // Handle next delivery slot endpoint (requires userId and addressId) if (this.userId && this.addressId) { try { const nextDeliveryPath = `/services/frontend-service/timeslots-api/0?userId=${this.userId}&addressId=${this.addressId}&reasonableDeliveryTime=true`; const response = await this.makeRequest(nextDeliveryPath); result.next_delivery_slot = response.data || response; } catch (error) { console.error('Error fetching next_delivery_slot:', error); result.next_delivery_slot = null; } } else { result.next_delivery_slot = null; } // Get cart content (call internal method to avoid double login) try { const response = await this.makeRequest('/services/frontend-service/v2/cart'); const data = response.data || {}; result.cart = { total_price: data.totalPrice || 0, total_items: Object.keys(data.items || {}).length, can_make_order: data.submitConditionPassed || false, products: Object.entries(data.items || {}).map(([productId, productData]) => ({ id: productId, cart_item_id: productData.orderFieldId || '', name: productData.productName || '', quantity: productData.quantity || 0, price: productData.price || 0, category_name: productData.primaryCategoryName || '', brand: productData.brand || '' })) }; } catch (error) { console.error('Error fetching cart:', error); result.cart = undefined; } return result; } finally { await this.logout(); } } async getOrderHistory(limit = 50) { await this.login(); try { const response = await this.makeRequest(`/api/v3/orders/delivered?offset=0&limit=${limit}`); return response.data || response; } finally { await this.logout(); } } async getDeliveryInfo() { await this.login(); try { const response = await this.makeRequest('/services/frontend-service/first-delivery?reasonableDeliveryTime=true'); return response.data || response; } finally { await this.logout(); } } async getUpcomingOrders() { await this.login(); try { const response = await this.makeRequest('/api/v3/orders/upcoming'); return response.data || response; } finally { await this.logout(); } } async getPremiumInfo() { await this.login(); try { const response = await this.makeRequest('/services/frontend-service/premium/profile'); return response.data || response; } finally { await this.logout(); } } async getDeliverySlots() { await this.login(); try { if (this.userId && this.addressId) { const response = await this.makeRequest(`/services/frontend-service/timeslots-api/0?userId=${this.userId}&addressId=${this.addressId}&reasonableDeliveryTime=true`); return response.data || response; } else { throw new RohlikAPIError('User ID or Address ID not available'); } } finally { await this.logout(); } } async getAnnouncements() { await this.login(); try { const response = await this.makeRequest('/services/frontend-service/announcements/top'); return response.data || response; } finally { await this.logout(); } } async getReusableBagsInfo() { await this.login(); try { const response = await this.makeRequest('/api/v1/reusable-bags/user-info'); return response.data || response; } finally { await this.logout(); } } } //# sourceMappingURL=rohlik-api.js.map