pair_wine_with_ingredients
Discover the top five wine matches for your ingredients. Just provide a list like 'salmon, lemon, dill' and get scores.
Instructions
Find the best wine pairings for a list of ingredients. Provide ingredient names in natural language (e.g. "salmon", "lemon", "dill") and get the top 5 wine matches with scores. The server automatically resolves ingredient names to the database. Best for: "What wine goes with these ingredients?" | Auth: API key (Bearer sk_live_...) or x402 payment (USDC on Base) | Price: $0.02/call (PRO)
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| ingredients | Yes | List of ingredient names (e.g. ["salmon", "lemon", "dill"]) | |
| language | No | Language code for results (e.g. "en", "nl", "fr"). Defaults to "en". |
Implementation Reference
- Main execution function for the pair_wine_with_ingredients tool. Resolves ingredient names to DB IDs via search API, then calls /api/v1/pairing/calculate with the matched IDs and formats the response.
export async function executePairWineWithIngredients( client: SommelierXClient, config: ServerConfig, input: PairWineWithIngredientsInput, ): Promise<string> { const language = input.language ?? config.defaultLanguage; // Step 1: Resolve all ingredients in parallel const resolved = await Promise.all( input.ingredients.map((name) => resolveIngredient(client, name, language)), ); const matched = resolved.filter((r): r is ResolvedIngredient & { id: number } => r.matched && r.id !== null); const unmatched = resolved.filter((r) => !r.matched); if (matched.length === 0) { return formatNoMatchResponse(input.ingredients, language); } // Step 2: Calculate pairings with matched ingredients const pairingInput = matched.map((r) => ({ id: r.id, amount: 'medium' as const })); try { const result = await client.post<PairingResult>('/api/v1/pairing/calculate', { ingredients: pairingInput, language, }); return formatPairingResponse(result.results, matched, unmatched); } catch (error: unknown) { const message = error instanceof Error ? error.message : 'Unknown error'; return `Error calculating wine pairing: ${message}`; } } - Zod schema for tool input validation. Accepts an array of 1-20 ingredient names (strings) and an optional language code.
export const pairWineWithIngredientsSchema = z.object({ ingredients: z .array(z.string().min(1)) .min(1, 'At least one ingredient is required') .max(20, 'Maximum 20 ingredients allowed') .describe('List of ingredient names (e.g. ["salmon", "lemon", "dill"])'), language: z .string() .min(2) .max(10) .optional() .describe('Language code for results (e.g. "en", "nl", "fr"). Defaults to "en".'), }); - src/index.ts:69-80 (registration)Registers the tool with the MCP server using server.tool('pair_wine_with_ingredients', ...). Wires the schema and the async handler that parses input and calls executePairWineWithIngredients.
// ── Tool 1: pair_wine_with_ingredients ── server.tool( 'pair_wine_with_ingredients', 'Find the best wine pairings for a list of ingredients. Provide ingredient names in natural language (e.g. "salmon", "lemon", "dill") and get the top 5 wine matches with scores. The server automatically resolves ingredient names to the database. Best for: "What wine goes with these ingredients?" | Auth: API key (Bearer sk_live_...) or x402 payment (USDC on Base) | Price: $0.02/call (PRO)', pairWineWithIngredientsSchema.shape, async (input) => { const parsed = pairWineWithIngredientsSchema.parse(input); const result = await executePairWineWithIngredients(client, config, parsed); return { content: [{ type: 'text' as const, text: result }] }; }, ); - Helper function resolveIngredient that queries /api/v1/ingredients?search= to resolve a single ingredient name to a DB ID.
async function resolveIngredient( client: SommelierXClient, name: string, language: string, ): Promise<ResolvedIngredient> { try { const result = await client.get<IngredientListResult>('/api/v1/ingredients', { 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 }; } } - Helper function formatPairingResponse that formats the pairing results into a human-readable string for the LLM, showing top 5 wine matches with scores, region, grapes, and score breakdown.
function formatPairingResponse( wines: WineMatch[], matched: ResolvedIngredient[], unmatched: ResolvedIngredient[], ): string { const lines: string[] = []; lines.push(`Wine pairing for: ${matched.map((m) => m.name).join(', ')}`); if (unmatched.length > 0) { lines.push(`Note: Could not find these ingredients in the database: ${unmatched.map((u) => u.name).join(', ')}`); } lines.push(''); const topWines = wines.slice(0, 5); if (topWines.length === 0) { lines.push('No wine matches found for these ingredients.'); return lines.join('\n'); } lines.push('Top wine matches:'); 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(` 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(', ')}`); } if (wine.score.basic_score !== undefined) { const parts: string[] = []; parts.push(`basic ${wine.score.basic_score}`); parts.push(`balance ${wine.score.balance_score ?? 0}`); if (wine.score.aromatic_score != null) { parts.push(`aromatic ${wine.score.aromatic_score}`); } lines.push(` Score breakdown: ${parts.join(' | ')}`); } if (wine.description) { lines.push(` ${wine.description}`); } lines.push(''); } return lines.join('\n'); }