From 76188bf0f2392ce00947560108918d55c943c0d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Coutada?= Date: Sun, 9 Aug 2026 02:33:21 +0100 Subject: [PATCH] =?UTF-8?q?feat(mcp):=20Aur=C3=A9lio=20Embroidery=20MCP=20?= =?UTF-8?q?server=20=E2=80=94=206=20tools?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tools: - embroidery_formats — list 47 readable, 19 writable formats - embroidery_machines — list all 15 vendors from registry - embroidery_match_machine — match model name to vendor entry - embroidery_convert — convert between any supported formats - embroidery_preview — render PNG stitch visualization - embroidery_send — convert + write to Gotek USB stick - embroidery_info — get stitch count, colors, dimensions, time Built on FastMCP 3.4 + pyembroidery 1.5.1. Loads vendor-registry/registry.yaml at startup. --- mcp/__pycache__/server.cpython-314.pyc | Bin 0 -> 17676 bytes mcp/requirements.txt | 6 + mcp/server.py | 367 +++++++++++++++++++++++++ 3 files changed, 373 insertions(+) create mode 100644 mcp/__pycache__/server.cpython-314.pyc create mode 100644 mcp/requirements.txt create mode 100644 mcp/server.py diff --git a/mcp/__pycache__/server.cpython-314.pyc b/mcp/__pycache__/server.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9547344b873dd75216f0a70b3d78e8c7c4db128b GIT binary patch literal 17676 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 3.0, pyembroidery >= 1.5 + +fastmcp>=3.0 +pyembroidery>=1.5 +pyyaml>=6.0 diff --git a/mcp/server.py b/mcp/server.py new file mode 100644 index 0000000..73ca03b --- /dev/null +++ b/mcp/server.py @@ -0,0 +1,367 @@ +"""Aurélio Embroidery MCP Server — the bridge between design and machine. + +Tools: convert, preview, send, formats, machines, match-machine. +Loads vendor registry at startup. Uses pyembroidery for all format operations. +""" +from pathlib import Path + +import yaml +from fastmcp import FastMCP + +mcp = FastMCP("Aurélio Embroidery") + +# ── Load vendor registry ───────────────────────────────────────────── +REGISTRY_PATH = Path(__file__).resolve().parent.parent / "vendor-registry" / "registry.yaml" +with open(REGISTRY_PATH) as f: + VENDOR_REGISTRY = yaml.safe_load(f) +VENDORS: list[dict] = VENDOR_REGISTRY["vendors"] + + +# ── Tools ───────────────────────────────────────────────────────────── + +@mcp.tool() +def embroidery_formats() -> dict: + """List all supported embroidery formats with read/write capabilities.""" + import pyembroidery as pe + + # pyembroidery 1.5.1: supported_formats() returns a generator of dicts with + # keys: 'description', 'extension', 'extensions', 'mimetype', 'category', 'reader', 'writer' + sf = list(pe.supported_formats()) + readable = sorted( + [f["extension"] for f in sf if f.get("reader")], + key=str.lower, + ) + writable = sorted( + [f["extension"] for f in sf if f.get("writer")], + key=str.lower, + ) + + return { + "readable": readable, + "writable": writable, + "total_readable": len(readable), + "total_writable": len(writable), + } + + +@mcp.tool() +def embroidery_machines(vendor: str | None = None) -> list[dict]: + """List known embroidery machines. Filter by vendor name if provided.""" + vendors = VENDORS + if vendor: + vendors = [v for v in vendors if vendor.lower() in v["vendor"].lower()] + return [ + { + "vendor": v["vendor"], + "models": v["models"], + "formats": v["formats"], + "media": v["media"], + "connectivity": v.get("connectivity", []), + } + for v in vendors + ] + + +@mcp.tool() +def embroidery_match_machine(model: str) -> dict: + """Match a machine model name to its vendor entry. + + Returns format, media type, jumper config, and connectivity options. + Example: embroidery_match_machine('Tajima TME-612') + """ + for v in VENDORS: + for pattern in v.get("models", []): + clean = pattern.rstrip("*").lower() + if clean and clean in model.lower(): + return { + "matched": True, + "model_query": model, + "matched_pattern": pattern, + **{k: v[k] for k in ["vendor", "formats", "media", "floppy", "jumpers", "connectivity", "notes"] if k in v}, + } + return { + "matched": False, + "model_query": model, + "suggestion": "Try a broader search or add this machine to the vendor registry.", + } + + +@mcp.tool() +def embroidery_convert( + input_path: str, + output_path: str, + output_format: str | None = None, +) -> dict: + """Convert between embroidery file formats. + + Reads any supported format, writes to the target format. + Format is auto-detected from output_path extension if not specified. + + Supported: DST, PES, EXP, JEF, VP3, HUS, VIP, SHV, SEW, XXX, + PEC, PCS, EMB, TBF, U01, DSB, DSZ, 10o, CSV, JSON, SVG. + + Args: + input_path: Absolute path to input embroidery file + output_path: Absolute path for output file + output_format: Optional format extension (e.g. 'dst', 'pes') + + Returns: + dict with success, input/output formats, stitch count, thread count + """ + import pyembroidery as pe + + inp = Path(input_path).expanduser().resolve() + out = Path(output_path).expanduser().resolve() + + if not inp.exists(): + return {"error": f"Input file not found: {inp}"} + + fmt = output_format or out.suffix.lstrip(".").lower() + + try: + pattern = pe.read(str(inp)) + except Exception as e: + return {"error": f"Failed to read {inp}: {e}"} + + try: + pe.write(pattern, str(out)) + except Exception as e: + return {"error": f"Failed to write {out} as {fmt}: {e}"} + + stitch_count = ( + pattern.count_stitches() + if hasattr(pattern, "count_stitches") + else len(getattr(pattern, "stitches", [])) + ) + thread_count = ( + len(pattern.threadlist) + if hasattr(pattern, "threadlist") + else 0 + ) + + return { + "success": True, + "input": str(inp), + "input_format": inp.suffix.lstrip("."), + "output": str(out), + "output_format": fmt, + "stitch_count": stitch_count, + "thread_count": thread_count, + } + + +@mcp.tool() +def embroidery_preview( + input_path: str, + output_path: str | None = None, + width: int = 800, + height: int = 600, + background: str = "#1A1A2E", +) -> dict: + """Generate a PNG preview of an embroidery design. + + Renders all stitches as colored lines on a dark background. + If output_path is omitted, saves next to the input as _preview.png. + + Args: + input_path: Path to embroidery file + output_path: Optional output PNG path + width: Image width in pixels (default 800) + height: Image height in pixels (default 600) + background: Background color as hex (default dark navy) + + Returns: + dict with output path, stitch count, bounds in mm + """ + import pyembroidery as pe + from PIL import Image, ImageDraw + + inp = Path(input_path).expanduser().resolve() + if not inp.exists(): + return {"error": f"Input file not found: {inp}"} + + out = Path(output_path).expanduser().resolve() if output_path else inp.with_suffix(".preview.png") + + try: + pattern = pe.read(str(inp)) + except Exception as e: + return {"error": f"Failed to read {inp}: {e}"} + + # Collect stitches + stitchblocks = getattr(pattern, "get_as_stitchblocks", None) + stitches = stitchblocks() if stitchblocks else getattr(pattern, "stitches", []) + if not stitches: + return {"error": "No stitches found in pattern"} + + # Bounding box + xs = [s[0] for s in stitches if isinstance(s, (list, tuple)) and len(s) >= 2] + ys = [s[1] for s in stitches if isinstance(s, (list, tuple)) and len(s) >= 2] + if not xs: + return {"error": "No stitch coordinates found"} + + min_x, max_x = min(xs), max(xs) + min_y, max_y = min(ys), max(ys) + rw, rh = max_x - min_x + 1, max_y - min_y + 1 + pad = 30 + scale = min((width - pad * 2) / rw, (height - pad * 2) / rh) + + # Draw + img = Image.new("RGB", (width, height), background) + draw = ImageDraw.Draw(img) + colors = [ + "#E74C3C", "#E67E22", "#F1C40F", "#2ECC71", "#3498DB", + "#9B59B6", "#1ABC9C", "#E84393", "#00B894", "#6C5CE7", + ] + + prev_x = prev_y = None + color_idx = 0 + + for s in stitches: + if len(s) < 2: + continue + cmd = s[2] if len(s) >= 3 else 0 + import pyembroidery as pe + + if cmd == pe.COLOR_CHANGE: + color_idx = (color_idx + 1) % len(colors) + prev_x = prev_y = None + continue + if cmd == pe.END: + break + + x = pad + int((s[0] - min_x) * scale) + y = pad + int((s[1] - min_y) * scale) + + if cmd == pe.JUMP: + prev_x = prev_y = None + continue + + if prev_x is not None and prev_y is not None: + draw.line([(prev_x, prev_y), (x, y)], fill=colors[color_idx], width=1) + + prev_x, prev_y = x, y + + img.save(str(out)) + + return { + "success": True, + "output": str(out), + "stitch_count": len(stitches), + "bounds_mm": { + "width": round(rw / 10, 1), + "height": round(rh / 10, 1), + }, + } + + +@mcp.tool() +def embroidery_send( + input_path: str, + machine_model: str, + usb_mount: str = "/media/usb", + disk_number: int = 0, +) -> dict: + """Convert a design and write it to a USB stick for a specific embroidery machine. + + Auto-detects the right format, writes the file to the Gotek USB stick + in the format the machine expects (e.g., DSKA0000.DST for Tajima). + + Args: + input_path: Path to the design file (any supported format) + machine_model: Machine model to target (e.g., 'Tajima TME-612') + usb_mount: Mount point of the Gotek USB stick (default /media/usb) + disk_number: Disk image number 0-999, becomes DSKAXXXX + + Returns: + dict with success, output file path, Gotek filename, stitch count + """ + import pyembroidery as pe + + inp = Path(input_path).expanduser().resolve() + usb = Path(usb_mount).expanduser().resolve() + + if not inp.exists(): + return {"error": f"Input file not found: {inp}"} + if not usb.exists(): + return {"error": f"USB mount not found: {usb}. Is the Gotek stick plugged in?"} + + # Match machine + match = embroidery_match_machine(machine_model) + if not match.get("matched"): + return {"error": f"Unknown machine: {machine_model}. Add it to the vendor registry."} + + fmt = match["formats"][0] + filename = f"DSKA{disk_number:04d}.{fmt.upper()}" + output_path = usb / filename + + try: + pattern = pe.read(str(inp)) + pe.write(pattern, str(output_path)) + except Exception as e: + return {"error": f"Failed: {e}"} + + stitch_count = ( + pattern.count_stitches() + if hasattr(pattern, "count_stitches") + else 0 + ) + + return { + "success": True, + "machine": machine_model, + "format": fmt, + "output_file": str(output_path), + "gotek_filename": filename, + "stitch_count": stitch_count, + "disk_number": disk_number, + "instructions": ( + f"Insert USB stick into Gotek on {machine_model}. " + f"Select disk {disk_number:04d}. Load design. Press START." + ), + } + + +@mcp.tool() +def embroidery_info(input_path: str) -> dict: + """Get detailed info about an embroidery file without converting it. + + Returns format, stitch count, color count, dimensions, estimated time. + Works on any supported format. + """ + import pyembroidery as pe + + inp = Path(input_path).expanduser().resolve() + if not inp.exists(): + return {"error": f"Input file not found: {inp}"} + + try: + pattern = pe.read(str(inp)) + except Exception as e: + return {"error": f"Failed to read {inp}: {e}"} + + stitches = getattr(pattern, "stitches", []) + stitch_count = len(stitches) + + # Count color changes + import pyembroidery as pe + color_count = 1 + sum(1 for s in stitches if len(s) >= 3 and s[2] == pe.COLOR_CHANGE) + + # Bounds + xs = [s[0] for s in stitches if isinstance(s, (list, tuple)) and len(s) >= 2] + ys = [s[1] for s in stitches if isinstance(s, (list, tuple)) and len(s) >= 2] + + return { + "success": True, + "path": str(inp), + "format": inp.suffix.upper().lstrip("."), + "stitch_count": stitch_count, + "color_count": color_count, + "width_mm": round((max(xs) - min(xs)) / 10, 1) if xs else 0, + "height_mm": round((max(ys) - min(ys)) / 10, 1) if ys else 0, + "estimated_time_min": round(stitch_count / 500, 1) if stitch_count else 0, + } + + +# ── Main ────────────────────────────────────────────────────────────── +if __name__ == "__main__": + mcp.run()