commit c5d6ec4a369fcf3ea8decde1c7417be72800cb3f Author: Isaac Johnson Date: Wed Jan 28 07:10:08 2026 -0600 first diff --git a/.gemini/settings.json.old b/.gemini/settings.json.old new file mode 100644 index 0000000..055799e --- /dev/null +++ b/.gemini/settings.json.old @@ -0,0 +1,10 @@ +{ + "mcpServers": { + "recipeMaker": { + "command": "python", + "args": [ + "/home/builder/Workspaces/recipeMakerMCP/server.py" + ] + } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..600a4e9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,191 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python,linux +# Edit at https://www.toptal.com/developers/gitignore?templates=python,linux + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +# End of https://www.toptal.com/developers/gitignore/api/python,linux diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e74b299 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.13-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir --default-timeout=100 -r requirements.txt + +COPY server.py . + +# Ensure the recipes directory exists so permissions can be managed if mounted +RUN mkdir -p recipes/images + +ENTRYPOINT ["python", "server.py"] diff --git a/gemini-extension.json b/gemini-extension.json new file mode 100644 index 0000000..b300972 --- /dev/null +++ b/gemini-extension.json @@ -0,0 +1,20 @@ +{ + "extensionId": "recipemaker", + "name": "RecipeMaker", + "version": "1.0.1", + "description": "An MCP server for creating recipes with images, running in a Docker container.", + "mcpServers": [ + { + "id": "recipe-maker", + "name": "Recipe Maker Server", + "transportType": "stdio", + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "idjohnson/recipemakermcp:latest" + ] + } + ] +} diff --git a/recipes/cornbread/images/hero.png b/recipes/cornbread/images/hero.png new file mode 100644 index 0000000..51a6307 Binary files /dev/null and b/recipes/cornbread/images/hero.png differ diff --git a/recipes/cornbread/images/prep.png b/recipes/cornbread/images/prep.png new file mode 100644 index 0000000..4af502a Binary files /dev/null and b/recipes/cornbread/images/prep.png differ diff --git a/recipes/cornbread/traditional_rustic_cornbread.md b/recipes/cornbread/traditional_rustic_cornbread.md new file mode 100644 index 0000000..aa1dfa7 --- /dev/null +++ b/recipes/cornbread/traditional_rustic_cornbread.md @@ -0,0 +1,39 @@ +# Traditional Rustic Cornbread + +**Servings:** 8 servings + +## Description +A classic Southern-style cornbread with a golden, crispy crust and a moist, tender crumb. Baked in a cast-iron skillet for that perfect rustic touch. + +![Hero Image of Traditional Rustic Cornbread](images/hero.png) + + +## Ingredients +- 2 cups stone-ground yellow cornmeal +- 1 cup all-purpose flour +- 2 teaspoons baking powder +- 1 teaspoon baking soda +- 1 teaspoon salt +- 2 large eggs +- 1 1/2 cups buttermilk +- 1/2 cup unsalted butter, melted +- 2 tablespoons honey + +## Steps +1. Preheat oven to 400°F (200°C) and heat a 10-inch cast-iron skillet. +2. Whisk dry ingredients (cornmeal, flour, powders, salt) in a large bowl. +3. Whisk wet ingredients (eggs, buttermilk, honey) in a separate bowl. +4. Combine wet and dry ingredients. Add melted butter. Stir until just mixed. +5. Grease the hot skillet with butter. +6. Pour batter into skillet. +7. Bake for 20-25 minutes until golden brown. +8. Cool slightly before serving. + +![Ingredients and Prep for Traditional Rustic Cornbread](images/prep.png) + diff --git a/recipes/images/spaghetti_carbonara_hero.png b/recipes/images/spaghetti_carbonara_hero.png new file mode 100644 index 0000000..602b946 Binary files /dev/null and b/recipes/images/spaghetti_carbonara_hero.png differ diff --git a/recipes/images/spaghetti_carbonara_prep.png b/recipes/images/spaghetti_carbonara_prep.png new file mode 100644 index 0000000..2bc43d6 Binary files /dev/null and b/recipes/images/spaghetti_carbonara_prep.png differ diff --git a/recipes/spaghetti_carbonara.md b/recipes/spaghetti_carbonara.md new file mode 100644 index 0000000..38761b1 --- /dev/null +++ b/recipes/spaghetti_carbonara.md @@ -0,0 +1,32 @@ +# Spaghetti Carbonara + +**Servings:** 4 people + +## Description +A classic Italian pasta dish made with eggs, cheese, pancetta, and pepper. + +![Hero Image of Spaghetti Carbonara](images/spaghetti_carbonara_hero.png) + + +## Ingredients +- 1 lb Spaghetti +- 4 Large Eggs +- 1 cup Pecorino Romano +- 0.5 lb Pancetta +- Black Pepper + +## Steps +1. Boil water +2. Cook pasta +3. Fry pancetta +4. Mix eggs and cheese +5. Combine all + +![Ingredients and Prep for Spaghetti Carbonara](images/spaghetti_carbonara_prep.png) + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..cf16ce2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +mcp[cli] +Pillow diff --git a/server.py b/server.py new file mode 100644 index 0000000..efabc25 --- /dev/null +++ b/server.py @@ -0,0 +1,144 @@ +from mcp.server.fastmcp import FastMCP +from PIL import Image, ImageDraw, ImageFont +import os +import textwrap + +# Initialize the FastMCP server +mcp = FastMCP("recipe-maker") + +# Constants +RECIPE_DIR = "recipes" +IMAGE_DIR = "recipes/images" + +# Ensure directories exist +os.makedirs(RECIPE_DIR, exist_ok=True) +os.makedirs(IMAGE_DIR, exist_ok=True) + +def create_placeholder(text: str, file_path: str, color: str = "lightgray"): + """Creates a simple placeholder image with text.""" + width, height = 1024, 1024 + img = Image.new('RGB', (width, height), color=color) + d = ImageDraw.Draw(img) + + # Simple text centering logic + try: + # Try to load a font, otherwise fallback to default + font = ImageFont.load_default() + # Scale up text if possible (rudimentary) + # In a real app we'd load a TTF + except: + font = None + + text_lines = textwrap.wrap(text, width=40) + y_text = height // 2 - (len(text_lines) * 15) + + for line in text_lines: + d.text((width // 2, y_text), line, fill="black", anchor="mm", font=font) + y_text += 30 + + img.save(file_path) + +def generate_nanobanana_hero_prompt(title: str, description: str) -> str: + return ( + f"Photorealistic hero shot of {title}, {description}. " + "Professional food photography, high detailed, appetizing, 4k resolution, soft natural lighting." + ) + +def generate_nanobanana_prep_prompt(title: str, ingredients: list[str]) -> str: + ing_list = ", ".join(ingredients[:5]) # Top 5 ingredients + return ( + f"Photorealistic overhead shot of ingredients for {title}, including {ing_list}. " + "Kitchen workspace with bowls, mixers, and utensils. Clean, organized, bright lighting." + ) + +@mcp.tool() +def create_recipe(title: str, description: str, ingredients: list[str], steps: list[str], servings: str) -> str: + """ + Creates a recipe as a Markdown file with placeholder images. + + Args: + title: The title of the recipe. + description: A brief description. + ingredients: List of ingredient strings (e.g. "1 cup flour"). + steps: List of cooking steps. + servings: Serving size information. + """ + safe_title = "".join(c for c in title if c.isalnum() or c in (' ', '_')).replace(' ', '_').lower() + recipe_filename = os.path.join(RECIPE_DIR, f"{safe_title}.md") + + hero_img_name = f"{safe_title}_hero.png" + prep_img_name = f"{safe_title}_prep.png" + + hero_img_path = os.path.join(IMAGE_DIR, hero_img_name) + prep_img_path = os.path.join(IMAGE_DIR, prep_img_name) + + # Generate prompts + hero_prompt = generate_nanobanana_hero_prompt(title, description) + prep_prompt = generate_nanobanana_prep_prompt(title, ingredients) + + # Create placeholders + create_placeholder(f"HERO: {title}\nPrompt: {hero_prompt}", hero_img_path, color="lightblue") + create_placeholder(f"PREP: {title}\nPrompt: {prep_prompt}", prep_img_path, color="lightgreen") + + # Create Markdown content + md_content = f"""# {title} + +**Servings:** {servings} + +## Description +{description} + +![Hero Image of {title}](images/{hero_img_name}) + + +## Ingredients +""" + for ing in ingredients: + md_content += f"- {ing}\n" + + md_content += "\n## Steps\n" + for i, step in enumerate(steps, 1): + md_content += f"{i}. {step}\n" + + md_content += f""" +![Ingredients and Prep for {title}](images/{prep_img_name}) + +""" + + with open(recipe_filename, "w") as f: + f.write(md_content) + + return f"Recipe created at {recipe_filename}" + +@mcp.tool() +def create_hero_image(title: str, description: str) -> str: + """Generates a hero image placeholder and prompt.""" + safe_title = "".join(c for c in title if c.isalnum() or c in (' ', '_')).replace(' ', '_').lower() + img_name = f"{safe_title}_hero_custom.png" + img_path = os.path.join(IMAGE_DIR, img_name) + + prompt = generate_nanobanana_hero_prompt(title, description) + create_placeholder(f"HERO: {title}\nPrompt: {prompt}", img_path, color="lightblue") + + return f"Created placeholder at {img_path}. Prompt: {prompt}" + +@mcp.tool() +def create_prep_image(title: str, ingredients: list[str]) -> str: + """Generates a prep image placeholder and prompt.""" + safe_title = "".join(c for c in title if c.isalnum() or c in (' ', '_')).replace(' ', '_').lower() + img_name = f"{safe_title}_prep_custom.png" + img_path = os.path.join(IMAGE_DIR, img_name) + + prompt = generate_nanobanana_prep_prompt(title, ingredients) + create_placeholder(f"PREP: {title}\nPrompt: {prompt}", img_path, color="lightgreen") + + return f"Created placeholder at {img_path}. Prompt: {prompt}" + +if __name__ == "__main__": + mcp.run()