devkick presents age:AI Bulk Barn MCP zero-shot speedrun
Goal
You are starting from an empty directory. Build the entire BulkBarnMCP project from scratch, install it, stand it up, and write the HTML instructions so it can be demonstrated live. The MCP name must be BulkBarnMCP (FastMCP("BulkBarnMCP")).
When this prompt finishes, ALL of the following must be true:
- Python dependencies are installed from
requirements.txt. - The
mcp-server/server.pyfile exists and implements the full tool set described below. mcp-server/densities.jsonandmcp-server/aliases.jsonexist and are loaded by the server. These must be built from scratch, not copied from a pre-filled list.- A Bulk Barn product catalog exists under
data/and was built by scraping the Bulk Barn category URLs you discover. mcp-server/test_server.pyandmcp-server/test_mcp_client.pyexist and pass against the catalog you built.build-catalog.pyexists as a cross-platform Python scraper.site/index.htmlandsite/guide.htmlexist.index.htmllinks toguide.html;guide.htmlcontains the connection instructions and tool list.README.mdandAGENTS.mdexist with project documentation and agent notes..gitignoreexists.- The MCP server is running in SSE mode on port 8000.
- The static site is being served on port 8080 and
guide.htmlis reachable. - Smoke tests pass, the SSE endpoint responds, and the guide page displays correctly in a browser.
Environment assumptions
- Windows with PowerShell 7 or macOS with
bash/zsh. - Python 3.11+ is installed. On Windows use
python; on macOS usepython3ifpythonis not available. pip(orpip3on macOS) can install fromrequirements.txt.- Internet access is available.
Project layout
Create this directory tree in the project root:
<project-root>
data/ # one JSON file per category + _index.json
mcp-server/
server.py # FastMCP server named BulkBarnMCP
densities.json # unit + density table (built from web sources)
aliases.json # ingredient aliases / not-available flags (built from scraped products)
test_server.py # module smoke test
test_mcp_client.py # stdio MCP client smoke test
site/
index.html # landing page with link to guide
guide.html # connection guide
build-catalog.py # Python scraper for the Bulk Barn catalog
requirements.txt
README.md
AGENTS.md
.gitignore
Step 0 - Create directory structure
Use a Python one-liner so it works on both Windows and macOS:
python -c "import os; [os.makedirs(d, exist_ok=True) for d in ('mcp-server', 'site', 'data')]"
On macOS, if python is not available, use python3.
Step 1 - requirements.txt
Create requirements.txt with these dependencies:
mcp>=1.0.0
thefuzz>=0.22.1
python-Levenshtein>=0.27.4
Add requests and beautifulsoup4 if you use the HTTP scraper, or playwright / selenium if you use a browser-based approach.
Step 2 - .gitignore
Create .gitignore ignoring Python build artifacts, virtual environments, the nutrition cache, and IDE folders:
# Python
__pycache__/
*.pyc
*.pyo
*.egg-info/
.venv/
# Generated / cached data
nutrition-cache.json
# IDE
.vscode/
.idea/
Step 3 - Scrape the Bulk Barn catalog
The catalog is a directory data/ with one JSON file per category plus _index.json.
Discover the categories
Start by fetching the category list from:
https://www.bulkbarn.ca/cmspages/bulkbarn/get_categories.aspx?lang=en
Parse the XML to discover every category. Each category has an id, a name, and a URL path. Build the category page URL from the path (prepend https://www.bulkbarn.ca/en/ to the path as needed).
Exclude non-food categories like:
- Cleaning and Personal Care
- Cake Decorating & Candy Making
- Glassware & Gadgets
- Health
- Petfood & Birdseed
- Pinatas
- Vitamins & Supplements
Scrape the remaining food categories.
What to extract
For each category page, extract every product and build objects with:
code- the bin code / PLU (a numeric string, typically 1 to 7 digits).name- the product name, HTML-decoded and trimmed.product_url- the absolute product detail URL. If the href is relative, prependhttps://www.bulkbarn.ca.
Deduplicate by code and sort products by code ascending within each category file.
Per-category file schema
Each per-category JSON file:
{
"category": "Category Name",
"catid": 430,
"source_url": "https://www.bulkbarn.ca/en/Products/Categories/Category-Slug",
"scraped_at": "2026-08-14T22:57:49+00:00",
"count": 123,
"products": [
{
"code": "<bin code>",
"name": "<product name>",
"product_url": "https://www.bulkbarn.ca/en/Products/All/<slug>-<code>"
}
]
}
_index.json schema
{
"source": "https://www.bulkbarn.ca/en/Products",
"generated_at": "2026-08-14T22:57:49+00:00",
"categories": [
{ "catid": 430, "name": "Category Name", "file": "category-file-stem.json", "count": 123 }
],
"total": 123
}
How to scrape
The simplest approach is a plain HTTP GET of each category page. Use a realistic User-Agent header. The product grid is present in the returned HTML, inside <li class="prod-thumbnail"> elements. Each thumbnail contains an <a> element with:
data-prod-bbplu- the bin code / PLU.data-prod-name- the product name (HTML-encoded).href- the product detail URL, typically relative (e.g.,/en/Products/All/<slug>-<code>).
Parse the HTML, extract those fields for every thumbnail, HTML-decode the name, build absolute product URLs, deduplicate by code, and sort.
If a category page uses pagination or lazy/infinite scroll, discover and fetch the additional pages. Inspect the page for pagination links or XHR endpoints and replicate the requests.
If the grid does not appear in a plain GET, you may also use Playwright, Selenium, or manual network capture to retrieve the rendered HTML or the underlying product API calls. Do not rely on any machine-specific credential store or paid scraping service.
Write the scraper as build-catalog.py. It must accept --out-dir, --force, and optionally --category-id / --include-category-ids arguments. If a category returns zero products, retry once with a longer delay.
Step 4 - mcp-server/densities.json
Create mcp-server/densities.json from scratch. Do not copy a pre-made table. Research standard culinary densities and build the file.
units
Each key is a unit string. Each value has:
type:weight,volume, orcount.- For
weight:gramsper unit. - For
volume:mlper unit. - For
count: no extra fields.
Include common weight, volume, and count units. Use standard metric and US customary conversion values.
ingredients
Each ingredient object has:
patterns: list of strings to match against a recipe ingredient name.density_g_per_cup: float grams per US cup.
Build this from web sources. Search for "grams per cup" for the ingredients you find in the Bulk Barn catalog. Cover the major categories found in your scraped data: flours, sugars, fats, nuts, seeds, dried fruit, grains, beans, pasta, spices, baking supplies, sweeteners, spreads, and dairy products.
If a density cannot be found, omit the ingredient from the density table. The server will then report that it cannot determine the purchase weight.
Step 5 - mcp-server/aliases.json
Create mcp-server/aliases.json from scratch. Do not copy a pre-made list.
The aliases object maps a normalized recipe ingredient name to a bin code and optional note:
{
"aliases": {
"<recipe term>": { "code": "<bin code or null>", "note": "<optional note>" }
}
}
Build it by inspecting the products you scraped. For common ambiguous recipe terms (salt, sugar, brown sugar, flour, baking powder, walnuts, oats, rice, cinnamon, honey, peanut butter, etc.), choose the most representative Bulk Barn product.
For items Bulk Barn does not sell or only sells in a form that is not what a typical recipe means (fresh butter, eggs, dairy milk, olive oil, water), set code to null and add a helpful note.
Step 6 - mcp-server/server.py
Create mcp-server/server.py. It must be a FastMCP server named BulkBarnMCP and expose exactly these tools:
list_categories()-> str (JSON array of categories from_index.json).search_products(query: str, category: Optional[str] = None, limit: int = 10)-> str (JSON array of matching products withmatch_score).get_product(code: str)-> str (JSON product or error).get_nutrition(code: str, force_refresh: bool = False)-> str (JSON nutrition payload or error).get_recipe_nutrition(ingredients: str, force_refresh: bool = False)-> str (JSON withingredientsarray andtotals).convert_recipe_to_shopping_list(ingredients: str)-> str (JSON array of matched recipe items).list_densities()-> str (JSON of densities table).
Server behavior
-
Load
_index.jsonfromdata/_index.json. For each category, load its file and build:_catalog: list of all products as{code, name, category, catid, product_url?}._products_by_code: dict mapping code to product._index: the parsed index.
-
Load
densities.jsonandaliases.jsoninto_densitiesand_aliases. -
Ingredient parsing:
- Normalize text: lowercase, NFKC, remove parentheses, collapse whitespace.
- Replace Unicode fractions with decimal equivalents.
- Strip common prep and size words.
- Parse quantities: mixed numbers, simple fractions, decimals, integers. Handle ranges by taking the first quantity.
- Handle qualitative amounts: pinch, dash, to taste.
- Return a dict with
raw,quantity,unit,name,original_name.
-
Product matching:
- Check aliases first. If an alias has a
codeand it exists in the catalog, return that product withmatch_score100 and optionalalias_note. - If no alias, use
thefuzz.process.extractOnewith a custom scorer that tokenizes and stems ingredient and product names, rewards full query token overlap, and penalizes extra product words. Match threshold is 50. - For
search_products, also useprocess.extractwith the same scorer, respectinglimitand optionalcategoryfilter.
- Check aliases first. If an alias has a
-
Weight calculation:
- Look up unit in
_densities["units"]. - If unit is weight, multiply quantity by
grams. - If unit is volume, look up ingredient density (
_find_density). If found, computequantity * ml_per_unit * density / 236.588. - If unit is count, return
Noneand add a note that the item is sold by count. - Round purchase weight: >= 1000 g -> kg with 2 decimals; >= 1 g -> ceil to integer grams; else 2 decimals.
- Look up unit in
-
convert_recipe_to_shopping_list:- Split
ingredientsby lines. - For each line, parse it, find product, calculate grams, build alternatives list (top 3 minus the matched product, limit to 2), and return a JSON array.
- Each result includes:
ingredient,parsed(quantity, unit, ingredient_name),product,code,category,purchase_grams,purchase_weight,match_score,alternatives, and optionalnotes. - If an alias has
code: null, return statusnot_availablewith the alias note. - If no product match, return status
no_match. - Notes to include: alias note, weight-unknown note, low-confidence note (match score < 75).
- Split
-
Nutrition fetching (
get_nutrition):- Maintain a JSON cache in
nutrition-cache.jsonat the project root. - If
force_refreshis false and code is cached, return cached value. - Use
urllib.requestwith a browser User-Agent to fetchproduct_url. - Parse the HTML: split on "Nutrition Facts", take the last section, strip HTML tags and
*characters, collapse whitespace. - Extract:
serving_size(prefer parenthesized(X g|ml), else firstX g|ml),calories,fat_g,saturated_g,trans_g,cholesterol_mg,sodium_mg,carbohydrate_g,fibre_g,sugars_g,sugar_alcohol_g,protein_g,vitamin_a_percent,vitamin_c_percent,calcium_percent,iron_percent,potassium_mg. - Return a payload with
code,product,product_url,fetched_at(ISO 8601 UTC), andnutrition. - On error, return
{"error": ...}.
- Maintain a JSON cache in
-
get_recipe_nutrition:- Call
convert_recipe_to_shopping_list, parse JSON. - For each matched ingredient with
purchase_grams, callget_nutrition(respectingforce_refresh). - Scale nutrition to the purchased grams using the serving size. Parse serving size as
amount unit(g or ml). Usescale = grams / serving_amount. For ml, approximate 1 ml = 1 g. - Sum numeric fields into
totals(excludeserving_size_used,scaled_to_grams). - Return
{"ingredients": [...], "totals": {...}}.
- Call
-
CLI:
argparsewith--transport(stdioorsse, defaultstdio) and--port(default 8000).- When
--transport sse, setmcp.settings.port = args.portthen callmcp.run(transport="sse"). - When
stdio, callmcp.run(). - Load catalog, densities, and aliases before starting.
Implementation notes
- Use
from mcp.server.fastmcp import FastMCPandthefuzz. - Paths: derive project root from
Path(__file__).parent.parent. Data dir isPROJECT_ROOT / "data". - Density file is
Path(__file__).parent / "densities.json". - Aliases file is
Path(__file__).parent / "aliases.json". - Nutrition cache is
PROJECT_ROOT / "nutrition-cache.json". - All tool outputs are JSON strings.
- Do not pass
porttomcp.run(). Usemcp.settings.port.
Step 7 - mcp-server/test_server.py
Create mcp-server/test_server.py that:
- Adds
mcp-server/tosys.path. - Imports
server. - Calls
server.load_catalog(),server.load_densities(),server.load_aliases(). - Prints
catalog size: <len(_catalog)>. - Prints
--- search 'salt' ---andserver.search_products("salt", limit=5). If "salt" returns nothing, search for another common term from your catalog. - Prints
--- search 'flour' ---andserver.search_products("flour", limit=3)(or another common term). - Prints
--- recipe ---andserver.convert_recipe_to_shopping_list(...)with this recipe:- 2 cups all-purpose flour
- 100 g walnuts
- 1 tsp salt
- 1/2 cup brown sugar
- 1 tsp baking powder
- 3 tbsp olive oil
- 2 tbsp butter
- Prints
--- nutrition for a known product ---. Pick the first product returned bysearch_products("flour", limit=1), extract itscode, and callserver.get_nutrition(code)[:1200].
The script exits 0 on success.
Step 8 - mcp-server/test_mcp_client.py
Create mcp-server/test_mcp_client.py that:
- Uses
mcp.ClientSessionandmcp.client.stdio.stdio_client. - Starts the server with
StdioServerParameters(command=sys.executable, args=[str(SERVER)]). - Lists tools and prints tool names.
- Calls
convert_recipe_to_shopping_listwith ingredients2 cups all-purpose flour\n1 tsp salt\n100 g walnutsand prints the result. - Calls
get_nutritionwith the code of the first flour product returned by the previous call, and prints the first 1000 characters. - Calls
get_recipe_nutritionwith the same ingredients plus1/2 cup brown sugarand prints the first 1000 characters.
The script exits 0 on success.
Step 9 - site/guide.html
Create site/guide.html as a clean, responsive HTML page. Requirements:
- Use Bulk Barn brand colors:
#7e2d26(dark red) and#a8453c(lighter red) for headings; a warm off-white background#f7f3eeand white cards with a light border. - Header: "Bulk Barn MCP" and "Connection guide for the live demonstration".
- Sections:
- What is running - short description and endpoint badges for stdio and SSE, with
http://localhost:8000/sseandhttp://localhost:8000/messages. - Available MCP tools - list all seven tools with their signatures.
- Connect Claude Desktop (stdio) - show both config paths:
- Windows:
%APPDATA%\Claude\claude_desktop_config.json - macOS:
~/Library/Application Support/Claude/claude_desktop_config.jsonInclude a JSON block withmcpServers.bulkbarnpointing to the absolute path ofmcp-server/server.pyand tell the user to adjust the path and command (pythonon Windows,python3on macOS).
- Windows:
- Connect Claude Code (stdio) - show the
claude config add mcpServers ...command. - Connect an SSE client - show
python mcp-server/server.py --transport sse --port 8000(usepython3on macOS) andhttp://localhost:8000/sse. - ChatGPT / GPTs - note that ChatGPT does not natively connect to local MCP servers; use Claude Desktop, Claude Code, or an SSE client.
- Try a live prompt - include the example recipe and the example
What are the nutrition facts for Bulk Barn code <code>?prompt. Use a real code from your catalog (e.g., the code for all-purpose flour or table salt). - Health checks - list
python mcp-server/test_server.py(orpython3on macOS),python mcp-server/test_mcp_client.py(orpython3on macOS), andcurl http://localhost:8000/sse.
- What is running - short description and endpoint badges for stdio and SSE, with
- Include a "Back to landing page" link to
index.html. - Footer: "Bulk Barn MCP demonstration - generated by Devin".
- Style should be mobile-friendly.
Step 10 - site/index.html
Create site/index.html as a simple marketing landing page. Requirements:
- Use Bulk Barn brand red
#e12a28and white/neutral tones. - A top menu bar with the project name/logo and a "Connect guide" link to
guide.html. - Hero headline: "bake with bulk barn MCP" (or similar), tagline about converting recipes to Bulk Barn shopping lists.
- Feature list with checkmarks:
- Recipe to Bulk Barn shopping list conversion
- Smart ingredient matching with curated aliases
- Auto weight conversion from cups, spoons, and counts
- Per-product nutrition facts pulled from bulkbarn.ca
- Full recipe nutrition totals at a glance
- Search the catalog by name or category
- Works inside Claude Desktop as an MCP server
- A "Copy Claude Desktop config" button that copies a JSON config block to the clipboard using
navigator.clipboard.writeText. Include microcopy "Paste it into your Claude Desktop config file" and show both Windows and macOS paths below the button. - A link "View full connection guide" to
guide.html. - Mobile-friendly.
- Do not depend on external video or image assets. If you want media, use CSS-only design or data URIs. Keep the page self-contained.
Step 11 - README.md
Create README.md with:
- Title "# Bulk Barn MCP".
- One-paragraph description of what the server does.
- "What's included" section listing
data/,mcp-server/server.py,mcp-server/densities.json,mcp-server/aliases.json,build-catalog.py,nutrition-cache.json. - Prerequisites (Python 3.11+, Windows or macOS, internet access for the HTTP scraper).
- Install command with
python -m pip install -r requirements.txt(usepython3 -m pipon macOS if needed). - Refresh catalog command
python build-catalog.py --force(usepython3on macOS). - Run stdio and SSE server commands.
- Claude Desktop config block with the correct absolute path for the current machine.
- Available tools list.
- Example recipe prompt.
- Data accuracy and nutrition caching notes.
Step 12 - AGENTS.md
Create AGENTS.md with:
- Project layout.
- Common commands (install, refresh catalog, module smoke test, stdio smoke test, syntax check).
- Claude Desktop config block.
- Notes about the HTTP scraper, nutrition cache, and purchase-weight estimates.
- Landing page notes.
Step 13 - Install and test
Run these commands from the project root, using python3 on macOS where needed:
python -m pip install -r requirements.txt
python -m py_compile mcp-server/server.py
python mcp-server/test_server.py
python mcp-server/test_mcp_client.py
All must complete with exit code 0.
If the catalog is missing, first run the catalog build before testing.
Step 14 - Start services
From one terminal session, start the MCP server in SSE mode:
python mcp-server/server.py --transport sse --port 8000
From a second session, start the static site server:
python -m http.server 8080 --directory site
Use python3 on macOS if python is not available.
Step 15 - Verify
Run these checks and confirm output:
# SSE endpoint should stream an event: endpoint
curl -N http://localhost:8000/sse
# Guide page should return HTML
curl http://localhost:8080/guide.html
For a port check, use the appropriate command:
- Windows PowerShell:
Test-NetConnection -ComputerName localhost -Port 8000andTest-NetConnection -ComputerName localhost -Port 8080. BothTcpTestSucceededshould beTrue. - macOS:
nc -zv localhost 8000andnc -zv localhost 8080. Both should report success.
Definition of done
Before declaring complete, confirm ALL of these:
requirements.txt,mcp-server/server.py,mcp-server/densities.json,mcp-server/aliases.json,mcp-server/test_server.py,mcp-server/test_mcp_client.py,build-catalog.py,site/index.html,site/guide.html,README.md,AGENTS.md, and.gitignoreall exist.FastMCPis instantiated with the name"BulkBarnMCP".python -m pip install -r requirements.txt(orpython3 -m pip) completed successfully.python mcp-server/test_server.py(orpython3) exits 0.python mcp-server/test_mcp_client.py(orpython3) exits 0.http://localhost:8000/ssereturns a Server-Sent Events stream.http://localhost:8080/guide.htmlreturns the connection guide HTML.site/index.htmllinks toguide.html.- The guide lists the MCP tools, endpoint URLs, and Claude Desktop config.
densities.jsonandaliases.jsonwere built from scratch, not pasted from a pre-filled list.
Common pitfalls
- Do not pass
porttomcp.run(). The correct SSE pattern is:mcp.settings.port = args.port mcp.run(transport="sse") - Use
Path(__file__).parent.parentfor the project root, not a hard-coded string. - Ensure
data/_index.jsonexists and itscategoriesarray references files that exist. - Nutrition fetching requires
product_urlin the catalog. - The
search_productsandconvert_recipe_to_shopping_listtools must return JSON strings, not Python objects. - Keep all tool names in snake_case.
- Do not use em dashes in documentation; use hyphens.
- The category list endpoint returns XML; the category pages return HTML with the product grid. Parse both correctly.
- Use a realistic
User-Agentwhen making HTTP requests; some endpoints may block default library headers.