Skip to main content

MCP Server

The MCP server exposes our food search functionality as tools that AI assistants like Claude can use.

Why Build an MCP Server?​

With MCP, you can:

  • Use your food search in Claude Desktop
  • Integrate with VS Code AI extensions
  • Build multi-agent workflows
  • Create standardized tool interfaces

Step 1: Create the Server​

Create app/mcp/server.py:

app/mcp/server.py
import json
from mcp.server import Server
from mcp.types import Tool, TextContent

from app.rag import retriever

# Create the MCP server
server = Server("food-companion")


@server.list_tools()
async def list_tools() -> list[Tool]:
"""List available tools."""
return [
Tool(
name="search_food",
description="Search for Indian vegetarian dishes by query. Supports filtering by cuisine, spice level, and allergens.",
inputSchema={
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Natural language search query (e.g., 'spicy breakfast', 'high protein dinner')"
},
"cuisine": {
"type": "string",
"description": "Filter by cuisine type",
"enum": ["south_indian", "north_indian", "gujarati", "bengali", "maharashtrian", "rajasthani"]
},
"spice_level": {
"type": "string",
"description": "Filter by spice level",
"enum": ["mild", "medium", "hot"]
},
"exclude_allergens": {
"type": "array",
"items": {"type": "string"},
"description": "Allergens to exclude (e.g., ['dairy', 'nuts', 'gluten'])"
}
},
"required": ["query"]
}
),
Tool(
name="get_food_details",
description="Get detailed information about a specific dish by name.",
inputSchema={
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Exact name of the dish (e.g., 'Masala Dosa', 'Idli')"
}
},
"required": ["name"]
}
),
Tool(
name="list_cuisines",
description="List all available cuisine types.",
inputSchema={
"type": "object",
"properties": {}
}
)
]


@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
"""Handle tool calls."""

if name == "search_food":
results = retriever.search(
query=arguments["query"],
cuisine=arguments.get("cuisine"),
spice_level=arguments.get("spice_level"),
exclude_allergens=arguments.get("exclude_allergens"),
top_k=5
)

if not results:
return [TextContent(
type="text",
text="No dishes found matching your criteria."
)]

formatted = []
for dish in results:
formatted.append(f"**{dish['name']}** ({dish['cuisine']})")
formatted.append(f"Spice: {dish['spice_level']}")
formatted.append(f"{dish['description']}")
formatted.append("")

return [TextContent(type="text", text="\n".join(formatted))]

elif name == "get_food_details":
dish = retriever.get_by_name(arguments["name"])

if not dish:
return [TextContent(
type="text",
text=f"Dish '{arguments['name']}' not found."
)]

return [TextContent(
type="text",
text=json.dumps(dish, indent=2)
)]

elif name == "list_cuisines":
cuisines = [
"south_indian - Dishes from Tamil Nadu, Karnataka, Kerala, Andhra Pradesh",
"north_indian - Dishes from Punjab, Uttar Pradesh, Delhi",
"gujarati - Dishes from Gujarat, known for sweet-savory balance",
"bengali - Dishes from West Bengal, known for fish and sweets",
"maharashtrian - Dishes from Maharashtra, varied from coastal to inland",
"rajasthani - Dishes from Rajasthan, known for dry preparations"
]
return [TextContent(type="text", text="\n".join(cuisines))]

else:
return [TextContent(type="text", text=f"Unknown tool: {name}")]


async def main():
"""Run the MCP server."""
from mcp.server.stdio import stdio_server

async with stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
server.create_initialization_options()
)


if __name__ == "__main__":
import asyncio
asyncio.run(main())

Understanding the Code​

Tool Definition​

Each tool has:

  • name - Identifier for the tool
  • description - What the tool does (AI reads this!)
  • inputSchema - JSON Schema for parameters
Tool(
name="search_food",
description="Search for Indian dishes...", # Be descriptive!
inputSchema={
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Natural language query"
}
},
"required": ["query"]
}
)

Tool Handler​

The call_tool function routes requests to the right logic:

@server.call_tool()
async def call_tool(name: str, arguments: dict):
if name == "search_food":
# Handle search
elif name == "get_food_details":
# Handle details
...

Response Format​

Tools return TextContent objects:

return [TextContent(type="text", text="Results here...")]

Step 2: Export the Server​

Create app/mcp/__init__.py:

app/mcp/__init__.py
from .server import server

__all__ = ["server"]

Step 3: Configure Claude Desktop​

Add to ~/Library/Application Support/Claude/claude_desktop_config.json:

claude_desktop_config.json
{
"mcpServers": {
"food-companion": {
"command": "uv",
"args": ["run", "python", "-m", "app.mcp.server"],
"cwd": "/absolute/path/to/food-companion/backend",
"env": {
"CHROMA_HOST": "localhost",
"CHROMA_PORT": "8000"
}
}
}
}
Path Must Be Absolute

Replace /absolute/path/to/food-companion/backend with the actual full path to your backend directory.

Step 4: Test the Server​

Restart Claude Desktop, then try:

"What South Indian dishes are good for breakfast?"

Claude will:

  1. See the search_food tool is available
  2. Call it with appropriate parameters
  3. Use the results to answer your question

MCP Server Architecture​

┌────────────────────┐     stdio      ┌────────────────────┐
│ Claude Desktop │◄──────────────►│ MCP Server │
│ (or other AI) │ │ (food-companion) │
└────────────────────┘ └─────────┬──────────┘
│
┌─────────▼──────────┐
│ ChromaDB │
│ (Food Search) │
└────────────────────┘

Tool Best Practices​

Good Descriptions​

# Good - tells AI when and how to use it
description="Search for dishes by natural language query. Use when user asks for food recommendations. Supports filtering by cuisine, spice level, and dietary restrictions."

# Bad - too vague
description="Search foods"

Clear Parameter Descriptions​

"exclude_allergens": {
"type": "array",
"items": {"type": "string"},
"description": "List of allergens to exclude. Common values: 'dairy', 'nuts', 'gluten', 'soy'"
# Include examples!
}

Helpful Error Messages​

if not dish:
return [TextContent(
type="text",
text=f"Dish '{name}' not found. Try searching first with search_food."
)]

Testing Without Claude​

You can test the MCP server directly:

cd backend
echo '{"method": "tools/list"}' | uv run python -m app.mcp.server

That's the complete backend! Next, let's build the frontend.