group_pairing
Find the best wine that pairs well with 2-10 different dishes at once, ideal for multi-course dinners. Input meal names to get top recommendations.
Instructions
Find the best wine that pairs well with multiple dishes at once. Provide 2-10 meal names and get wines that score well across all of them. Requires Pro tier. Best for: "What single wine works for a 3-course dinner?" | Auth: API key (Bearer sk_live_...) or x402 payment (USDC on Base) | Price: $0.03/call (PRO)
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| meal_names | Yes | List of meal/dish names (e.g. ["Caesar salad", "grilled lamb", "chocolate mousse"]) | |
| language | No | Language code for results (e.g. "en", "nl", "fr"). Defaults to "en". |
Implementation Reference
- src/tools/group-pairing.ts:74-127 (handler)The main handler for the group_pairing tool. It resolves meal names to database IDs, calls the /api/v1/pairing/group endpoint, and formats the response. Requires Pro tier API key.
export async function executeGroupPairing( client: SommelierXClient, config: ServerConfig, input: GroupPairingInput, ): Promise<string> { const language = input.language ?? config.defaultLanguage; // Step 1: Resolve all meal names const resolved = await Promise.all( input.meal_names.map((name) => resolveMeal(client, name, language)), ); const matched = resolved.filter((r): r is ResolvedMeal & { id: number } => r.matched && r.id !== null); const unmatched = resolved.filter((r) => !r.matched); if (matched.length < 2) { const foundCount = matched.length; return [ `Could only find ${foundCount} of ${input.meal_names.length} meals in the database.`, `Group pairing requires at least 2 matched meals.`, '', `Found: ${matched.map((m) => m.name).join(', ') || 'none'}`, `Not found: ${unmatched.map((u) => u.name).join(', ')}`, '', 'Use search_meals to find the correct meal names.', ].join('\n'); } // Step 2: Call group pairing const mealIds = matched.map((m) => m.id); let result: GroupPairingResult; try { result = await client.post<GroupPairingResult>('/api/v1/pairing/group', { mealIds, language, }); } catch (error: unknown) { if (error instanceof SommelierXApiError && error.statusCode === 403) { return [ 'Group pairing requires a Pro or Enterprise API key.', 'The current MCP server is running without an API key or with a Free tier key.', '', 'To use this feature:', '1. Get a Pro API key at https://api.sommelierx.com', '2. Set the SOMMELIERX_API_KEY environment variable in your MCP config', ].join('\n'); } const message = error instanceof Error ? error.message : 'Unknown error'; return `Error calculating group pairing: ${message}`; } return formatGroupPairingResponse(matched, unmatched, result.results); } - src/tools/group-pairing.ts:18-30 (schema)Zod schema defining the group_pairing input: meal_names (array of 2-10 strings) and optional language code.
export const groupPairingSchema = z.object({ meal_names: z .array(z.string().min(2)) .min(2, 'At least 2 meals are required for group pairing') .max(10, 'Maximum 10 meals allowed') .describe('List of meal/dish names (e.g. ["Caesar salad", "grilled lamb", "chocolate mousse"])'), language: z .string() .min(2) .max(10) .optional() .describe('Language code for results (e.g. "en", "nl", "fr"). Defaults to "en".'), }); - src/index.ts:147-158 (registration)Registers the 'group_pairing' tool with the MCP server using server.tool(), wiring the schema shape and async handler.
// ── Tool 7: group_pairing ── server.tool( 'group_pairing', 'Find the best wine that pairs well with multiple dishes at once. Provide 2-10 meal names and get wines that score well across all of them. Requires Pro tier. Best for: "What single wine works for a 3-course dinner?" | Auth: API key (Bearer sk_live_...) or x402 payment (USDC on Base) | Price: $0.03/call (PRO)', groupPairingSchema.shape, async (input) => { const parsed = groupPairingSchema.parse(input); const result = await executeGroupPairing(client, config, parsed); return { content: [{ type: 'text' as const, text: result }] }; }, ); - src/tools/group-pairing.ts:44-64 (helper)resolveMeal - helper that searches for a meal by name in the database and returns its ID or null if not found.
async function resolveMeal( client: SommelierXClient, name: string, language: string, ): Promise<ResolvedMeal> { try { const result = await client.get<MealListResult>('/api/v1/meals', { search: name, language, perPage: '1', }); if (result.data && result.data.length > 0) { return { name, id: result.data[0].id, matched: true }; } return { name, id: null, matched: false }; } catch { return { name, id: null, matched: false }; } } - src/tools/group-pairing.ts:132-184 (helper)formatGroupPairingResponse - formats the API results into a human-readable string showing top 5 wines with per-meal breakdown.
function formatGroupPairingResponse( matched: ResolvedMeal[], unmatched: ResolvedMeal[], wines: Array<WineMatch & { mealScores?: Array<{ mealId: number; mealName: string; match_percentage: number }> }>, ): string { const lines: string[] = []; lines.push(`Group wine pairing for: ${matched.map((m) => m.name).join(', ')}`); if (unmatched.length > 0) { lines.push(`Note: Could not find these meals: ${unmatched.map((u) => u.name).join(', ')}`); } lines.push(''); const topWines = wines.slice(0, 5); if (topWines.length === 0) { lines.push('No wines found that pair well with all of these dishes.'); return lines.join('\n'); } lines.push('Best wines across all dishes:'); lines.push(''); for (let i = 0; i < topWines.length; i++) { const wine = topWines[i]; const rank = i + 1; lines.push(`${rank}. ${wine.name} (${wine.color})`); lines.push(` Overall match: ${wine.score.match_percentage}%`); if (wine.region) { lines.push(` Region: ${wine.region}`); } if (wine.grapes && wine.grapes.length > 0) { lines.push(` Grapes: ${wine.grapes.join(', ')}`); } // Show per-meal breakdown if available if (wine.mealScores && wine.mealScores.length > 0) { lines.push(' Per-dish scores:'); for (const ms of wine.mealScores) { lines.push(` - ${ms.mealName}: ${ms.match_percentage}%`); } } lines.push(''); } return lines.join('\n'); }