MealPlanner MCP Multi-Agent
by vijayaselvam
README.md
# π½οΈ MealPlanner MCP Multi-Agent
A **multimodal meal planning agent** built using [FastMCP](https://gofastmcp.com/) (Model Context Protocol). It demonstrates how to build an MCP server with **image analysis** (via Google Gemini Vision), **tools**, **resources**, and **prompts**.
## ποΈ Architecture
```
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MCP Client (client.py) β
β Connects to server, calls tools, reads resources β
βββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββ
β MCP Protocol (stdio)
βββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββ
β MCP Server (server.py) β
β β
β π§ Tools: β
β β’ identify_ingredients(image) β Gemini Vision API β
β β’ suggest_recipes(ingredients, diet) β
β β’ get_nutrition(recipe_name) β
β β’ get_meal_plan(ingredients, meals, diet) β
β β
β π¦ Resources: β
β β’ recipes://all β Full recipe database β
β β’ recipes://{id} β Single recipe details β
β β’ dietary://profiles β Dietary restrictions β
β β
β π¬ Prompts: β
β β’ analyze_fridge β Image analysis workflow β
β β’ weekly_meal_prep β Meal prep planning β
β β
β π§ External API: β
β β’ Google Gemini 2.0 Flash (Vision/Multimodal) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
```
---
## π End-to-End Workflow
### High-Level Sequence
```
ββββββββββ ββββββββββββββ ββββββββββββββ ββββββββββββββ
β User β β MCP Client β β MCP Server β β Gemini β
β β β (client.py)β β (server.py)β β Vision β
βββββ¬βββββ βββββββ¬βββββββ βββββββ¬βββββββ βββββββ¬βββββββ
β β β β
β 1. Run client β β β
β with --image β β β
βββββββββββββββββββ>β β β
β β β β
β β 2. Connect via β β
β β stdio transport β β
β ββββββββββββββββββββ>β β
β β β β
β β 3. list_tools() β β
β β list_resources()β β
β β list_prompts() β β
β ββββββββββββββββββββ>β β
β β<ββββββββββββββββββ-β β
β β (capabilities) β β
β β β β
β β 4. read_resource β β
β β ("recipes://all") β β
β ββββββββββββββββββββ>β β
β β<ββββββββββββββββββ-β β
β β (8 recipes) β β
β β β β
β β 5. call_tool β β
β β identify_ β β
β β ingredients(img) β β
β ββββββββββββββββββββ>β 6. Send image β
β β β + prompt β
β β βββββββββββββββββββββ>β
β β β<βββββββββββββββββββββ
β β β 7. Ingredients JSONβ
β β<ββββββββββββββββββ-β β
β β (ingredients list)β β
β β β β
β β 8. call_tool β β
β β suggest_recipes() β β
β ββββββββββββββββββββ>β β
β β<ββββββββββββββββββ-β β
β β (matched recipes) β β
β β β β
β β 9. call_tool β β
β β get_nutrition() β β
β ββββββββββββββββββββ>β β
β β<ββββββββββββββββββ-β β
β β (nutrition info) β β
β β β β
β β 10. call_tool β β
β β get_meal_plan() β β
β ββββββββββββββββββββ>β β
β β<ββββββββββββββββββ-β β
β β (daily plan) β β
β β β β
β 11. Print resultsβ β β
β<βββββββββββββββββββ β β
β (formatted output) β β
```
### Step-by-Step Data Flow
---
#### Step 1: Client Connects to Server
```python
# client.py β The client starts the server as a subprocess via stdio
client = Client("server.py")
async with client:
# Connection is established using MCP protocol over stdio
# FastMCP auto-handles: handshake, capability negotiation, lifecycle
```
**What happens internally:**
- `Client("server.py")` tells FastMCP to launch `server.py` as a **subprocess**
- Communication happens over **stdin/stdout** (stdio transport)
- MCP protocol handshake occurs automatically
- Client discovers server name: `"MealPlanner"`
---
#### Step 2: Discover Server Capabilities
```python
# client.py β Ask the server what it can do
tools = await client.list_tools() # β 4 tools
resources = await client.list_resources() # β 2 static resources
templates = await client.list_resource_templates() # β 1 template
prompts = await client.list_prompts() # β 2 prompts
```
**Server responds with:**
| Type | Name | Description |
|------|------|-------------|
| π§ Tool | `identify_ingredients` | Analyze fridge photo β ingredients (multimodal) |
| π§ Tool | `suggest_recipes` | Match ingredients β recipes |
| π§ Tool | `get_nutrition` | Recipe β nutritional info |
| π§ Tool | `get_meal_plan` | Ingredients β daily meal plan |
| π¦ Resource | `recipes://all` | Full recipe database (8 recipes) |
| π¦ Resource | `dietary://profiles` | 4 dietary profiles |
| π Template | `recipes://{recipe_id}` | Lookup single recipe by ID |
| π¬ Prompt | `analyze_fridge` | Guided fridge analysis workflow |
| π¬ Prompt | `weekly_meal_prep` | Weekly planning template |
---
#### Step 3: Read Resources (Recipe Database & Dietary Profiles)
```python
# client.py β Read the recipe database
recipes = await client.read_resource("recipes://all")
```
**Server processing (server.py):**
```python
@mcp.resource("recipes://all")
def get_all_recipes() -> str:
# Iterates RECIPE_DATABASE dict β returns JSON summary of 8 recipes
# Each recipe has: id, name, cuisine, prep_time, difficulty, ingredients
```
**Data returned:**
```json
[
{"id": "pasta_primavera", "name": "Pasta Primavera", "cuisine": "Italian", ...},
{"id": "chicken_stir_fry", "name": "Chicken Stir Fry", "cuisine": "Asian", ...},
{"id": "vegetable_omelette", "name": "Vegetable Omelette", ...},
// ... 8 recipes total
]
```
---
#### Step 4: Multimodal β Identify Ingredients from Fridge Photo πΈ
This is the **core multimodal step** where an image is analyzed.
```python
# client.py β Send fridge image for analysis
result = await client.call_tool(
"identify_ingredients",
{"image_path": "/path/to/fridge_photo.jpg"}
)
```
**Server processing (server.py) β 4 sub-steps:**
```
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β identify_ingredients(image_path) β
β β
β Step A: Read image file from disk β
β image_bytes = Path(image_path).read_bytes() β
β β
β Step B: Encode to base64 β
β image_base64 = base64.b64encode(image_bytes) β
β Detect MIME type: .jpg β "image/jpeg" β
β β
β Step C: Send to Google Gemini Vision API β
β gemini_client.models.generate_content( β
β model="gemini-2.0-flash", β
β contents=[text_prompt + inline_image_data] β
β ) β
β β
β Step D: Parse & return structured JSON β
β Clean markdown code blocks from response β
β Return ingredients list as JSON string β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
```
**The prompt sent to Gemini:**
```
Analyze this image of a fridge or pantry. Identify ALL visible food ingredients.
Return a JSON object with this structure:
{
"ingredients": [
{"name": "...", "category": "produce|dairy|protein|...", "quantity": "..."}
],
"total_count": <number>,
"freshness_notes": "..."
}
```
**Example response from Gemini:**
```json
{
"ingredients": [
{"name": "eggs", "category": "protein", "quantity": "6 eggs"},
{"name": "milk", "category": "dairy", "quantity": "1 carton"},
{"name": "tomato", "category": "produce", "quantity": "3 pieces"},
{"name": "cheese", "category": "dairy", "quantity": "1 block"},
{"name": "butter", "category": "dairy", "quantity": "1 stick"}
],
"total_count": 5,
"freshness_notes": "All items appear fresh and well-stored"
}
```
---
#### Step 5: Suggest Recipes Based on Ingredients
```python
# client.py β Pass identified ingredients to recipe matcher
result = await client.call_tool(
"suggest_recipes",
{"ingredients": ["eggs", "milk", "tomato", "cheese", "butter"],
"dietary_preference": "none"}
)
```
**Server processing (server.py):**
```
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β suggest_recipes(ingredients, dietary_preference) β
β β
β 1. Normalize ingredients β lowercase β
β β
β 2. Load dietary profile β
β "none" β no excluded ingredients β
β "vegan" β excludes eggs, cheese, milk, butter, etc. β
β β
β 3. For EACH recipe in RECIPE_DATABASE (8 recipes): β
β a. Skip if recipe has excluded ingredients β
β b. Count how many recipe ingredients are available β
β c. Calculate match % = available / total Γ 100 β
β d. Include if match β₯ 40% β
β β
β 4. Sort by match_percentage (highest first) β
β β
β 5. Return: recipe name, match %, available & missing items β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
```
**Example output:**
```json
{
"dietary_preference": "No Restrictions",
"total_matches": 4,
"recipes": [
{
"name": "Vegetable Omelette",
"match_percentage": 66.7,
"available_ingredients": ["eggs", "cheese", "butter"],
"missing_ingredients": ["bell pepper", "onion", "mushroom"]
},
{
"name": "Grilled Cheese Sandwich",
"match_percentage": 66.7,
"available_ingredients": ["cheese", "butter"],
"missing_ingredients": ["bread"]
}
]
}
```
---
#### Step 6: Get Nutrition Info for Top Recipe
```python
# client.py β Get nutrition for the best match
result = await client.call_tool(
"get_nutrition",
{"recipe_name": "vegetable_omelette"}
)
```
**Server processing (server.py):**
```
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β get_nutrition(recipe_name) β
β β
β 1. Search RECIPE_DATABASE by ID or name (case-insensitive) β
β 2. If not found β fuzzy match (partial string match) β
β 3. Return full recipe details: β
β - Ingredients list β
β - Step-by-step cooking instructions β
β - Nutrition: calories, protein, carbs, fat, fiber β
β - Auto-generated health tips based on values β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
```
**Example output:**
```json
{
"recipe": "Vegetable Omelette",
"nutrition": {
"calories": 320,
"protein": "22g",
"carbs": "8g",
"fat": "24g",
"fiber": "2g"
},
"instructions": [
"Whisk eggs with a splash of milk.",
"Melt butter in a non-stick pan over medium heat.",
"SautΓ© diced vegetables for 2-3 minutes.",
"Pour in egg mixture and cook until edges set.",
"Add cheese, fold, and serve."
],
"health_tips": [
"πͺ Good protein content β great for muscle recovery!",
"π§ Remember to stay hydrated throughout the day!"
]
}
```
---
#### Step 7: Generate Daily Meal Plan
```python
# client.py β Create a full day's meal plan
result = await client.call_tool(
"get_meal_plan",
{"ingredients": ingredient_names, "meals_per_day": 3, "dietary_preference": "none"}
)
```
**Server processing (server.py):**
```
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β get_meal_plan(ingredients, meals_per_day, diet) β
β β
β 1. Internally calls suggest_recipes() to get matched recipes β
β 2. Assigns recipes to meal slots: β
β - Meal 1 β "Breakfast" (best match recipe) β
β - Meal 2 β "Lunch" (2nd best match) β
β - Meal 3 β "Dinner" (3rd best match) β
β 3. Calculates total estimated calories β
β 4. Returns structured meal plan β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
```
**Example output:**
```json
{
"meal_plan": [
{"meal": "Breakfast", "recipe": "Vegetable Omelette", "match": "66.7%"},
{"meal": "Lunch", "recipe": "Grilled Cheese Sandwich", "match": "66.7%"},
{"meal": "Dinner", "recipe": "Classic Tomato Soup", "match": "50.0%"}
],
"estimated_total_calories": 920,
"note": "This is a suggestion based on available ingredients. Adjust portions as needed!"
}
```
---
#### Step 8: Client Prints Formatted Results
The client formats all JSON responses into a **user-friendly terminal output** with emojis, colors, and clear sections.
---
### Complete Data Flow Diagram
```
πΈ Fridge Photo
β
βΌ
ββββββββββββββββ base64 + prompt ββββββββββββββββ
β MCP Server β βββββββββββββββββββββββ β Google Gemini β
β Tool: β β Vision API β
β identify_ β <βββ JSON ingredients ββ β (2.0 Flash) β
β ingredients β ββββββββββββββββ
ββββββββ¬ββββββββ
β ["eggs", "tomato", "cheese", ...]
βΌ
ββββββββββββββββ
β MCP Server β Compares against
β Tool: β βββΆ RECIPE_DATABASE (8 recipes)
β suggest_ β βββΆ DIETARY_PROFILES (4 profiles)
β recipes β
ββββββββ¬ββββββββ
β [{name: "Omelette", match: 83%}, ...]
βΌ
ββββββββββββββββ
β MCP Server β Looks up recipe details
β Tool: β βββΆ Full instructions
β get_ β βββΆ Nutrition data
β nutrition β βββΆ Health tips (auto-generated)
ββββββββ¬ββββββββ
β {calories: 320, protein: "22g", ...}
βΌ
ββββββββββββββββ
β MCP Server β Combines top recipes into
β Tool: β βββΆ Breakfast / Lunch / Dinner
β get_ β βββΆ Calculates total calories
β meal_plan β
ββββββββ¬ββββββββ
β
βΌ
π Complete Meal Plan
with Nutrition Info
```
### Demo Mode vs Image Mode
| Aspect | Demo Mode (`python3 client.py`) | Image Mode (`python3 client.py --image photo.jpg`) |
|--------|------|------|
| Image analysis | β Skipped | β
Calls Gemini Vision API |
| Ingredients | Hardcoded sample list (10 items) | Extracted from image by AI |
| API key needed | β No (server loads but tool not called) | β
Yes (GOOGLE_API_KEY in .env) |
| Recipe matching | β
Works | β
Works |
| Nutrition info | β
Works | β
Works |
| Meal plan | β
Works | β
Works |
---
## π Project Structure
```
mealplanner_mcp_multi_agent/
βββ server.py # MCP Server β tools, resources, prompts
βββ client.py # MCP Client β demo script (connects via HTTP)
βββ pyproject.toml # Project config & dependencies (uv)
βββ uv.lock # Locked dependency versions
βββ .env.example # Environment variable template
βββ .python-version # Python version pin
βββ README.md # This file
```
## π Setup
### 1. Install uv (if not already installed)
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```
### 2. Install dependencies
```bash
uv sync
```
This creates a `.venv` virtual environment and installs all dependencies from `pyproject.toml`.
### 3. Get a Google Gemini API key
- Visit [Google AI Studio](https://aistudio.google.com/apikey)
- Create a free API key
- Copy `.env.example` to `.env` and paste your key:
```bash
cp .env.example .env
# Edit .env and add your GOOGLE_API_KEY
```
### 4. Start the MCP Server (Terminal 1)
```bash
uv run python3 server.py
```
The server starts at `http://127.0.0.1:8000/mcp` using **Streamable HTTP** transport.
### 5. Run the Client (Terminal 2)
```bash
# Demo mode (uses sample ingredients)
uv run python3 client.py
# With a real fridge photo (multimodal!)
uv run python3 client.py --image path/to/fridge_photo.jpg
# Connect to a custom server URL
uv run python3 client.py --url http://your-server:8000/mcp
```
### 6. Test with MCP Inspector
```bash
uv run fastmcp dev server.py
```
This opens a web UI where you can interactively test all tools, resources, and prompts.
## π§ MCP Concepts Demonstrated
| Concept | What It Does | Example in This Project |
|---------|-------------|------------------------|
| **Tools** | Functions the LLM can call | `identify_ingredients()` β sends image to Gemini |
| **Resources** | Read-only data for the LLM | `recipes://all` β browse recipe database |
| **Resource Templates** | Dynamic resources with parameters | `recipes://{recipe_id}` β get specific recipe |
| **Prompts** | Reusable prompt templates | `analyze_fridge` β guided fridge analysis workflow |
## πΌοΈ Multimodal Feature
The `identify_ingredients` tool is the **multimodal** component:
1. Accepts an **image file path** (JPG, PNG, WebP)
2. Encodes the image to **base64**
3. Sends it to **Google Gemini 2.0 Flash** with a structured prompt
4. Returns a **JSON list** of identified ingredients with categories
This demonstrates how MCP tools can handle non-text inputs (images) alongside text.
## π Example Output
```
============================================================
π½οΈ Meal Planner MCP Client
============================================================
π Discovering server capabilities...
π§ Available Tools:
β’ identify_ingredients: Analyze a photo of a fridge or pantry...
β’ suggest_recipes: Suggest recipes based on available ingredients...
β’ get_nutrition: Get detailed nutritional information...
β’ get_meal_plan: Generate a simple daily meal plan...
π¦ Available Resources:
β’ recipes://all: Recipe Database
β’ dietary://profiles: Dietary Profiles
π¨βπ³ Suggesting Recipes:
π’ Vegetable Omelette (83.3% match)
π’ Caprese Salad (80.0% match)
π‘ Classic Tomato Soup (66.7% match)
π‘ Pasta Primavera (57.1% match)
π‘ Grilled Cheese Sandwich (66.7% match)
```
## π Key Learning Points
1. **FastMCP simplifies MCP server creation** β just use `@mcp.tool()`, `@mcp.resource()`, `@mcp.prompt()` decorators
2. **Multimodal = image + text** β the server handles image encoding and sends to a vision API
3. **Tools vs Resources** β Tools perform actions (with side effects); Resources provide read-only data
4. **Prompts** β Reusable templates that guide the LLM through multi-step workflows
5. **Client connects via stdio** β FastMCP handles all the MCP protocol details automatically
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues