From e57ab57b4892142ee9ed06635b971a4f8b48cfcb Mon Sep 17 00:00:00 2001 From: root Date: Mon, 20 Apr 2026 15:27:44 -0300 Subject: [PATCH] Accept MCP flat payload in deck generator to stop silent-blank decks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit generate_deck on the MCP server passes a flat spec (customer_name, title, workload_type, services, cost_summary, ...) — but from_spec only understood the proposal-spec YAML shape (metadata/summary/architecture/...). With no matching keys, only the title and closing layouts rendered, both populated with empty strings, producing a deck that looked blank. Detect the flat shape in from_spec and adapt it into the proposal-spec fields that the existing slide builders already consume. Title, summary (target state + current-state bullets) and cost slides now render from whatever the MCP tool sends; the richer proposal-spec path is untouched. Co-Authored-By: Claude Opus 4.7 (1M context) --- tools/oci_deck_gen.py | 87 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/tools/oci_deck_gen.py b/tools/oci_deck_gen.py index bdbb3a0..b139d3a 100644 --- a/tools/oci_deck_gen.py +++ b/tools/oci_deck_gen.py @@ -29,6 +29,86 @@ from pptx.enum.shapes import MSO_SHAPE from oci_pptx_base import Colors, Layouts, OraclePresBase +# ============================================================ +# MCP flat-spec adapter +# ============================================================ + +def _is_flat_spec(spec: dict) -> bool: + """MCP payload shape: no 'metadata' key, but has customer_name/id/title.""" + if not isinstance(spec, dict): + return False + if "metadata" in spec: + return False + return any(k in spec for k in ("customer_name", "customer_id", "title")) + + +def _adapt_flat_spec(spec: dict) -> dict: + """Map the MCP flat payload to the proposal-spec structure from_spec expects.""" + arch = spec.get("architecture") if isinstance(spec.get("architecture"), dict) else {} + + customer = spec.get("customer_name") or spec.get("customer_id", "") + title = spec.get("title") or arch.get("workload", "") or "Architecture Proposal" + workload = spec.get("workload_type") or arch.get("workload", "") + deployment = spec.get("deployment_model") or arch.get("deployment", "") + region = spec.get("primary_region") or spec.get("region", "") + billing = spec.get("billing_model") or arch.get("license_model", "") + dr_raw = spec.get("disaster_recovery") + if dr_raw is None and "dr" in arch: + dr_raw = "Enabled" if arch.get("dr") else "Not in scope" + capacity = spec.get("capacity") or arch.get("capacity") or {} + + target_parts = [p for p in (workload, deployment and f"deployment: {deployment}", + region and f"region: {region}") if p] + target_state = " — ".join(target_parts) + + current_state = [] + if isinstance(capacity, dict): + for k, v in capacity.items(): + current_state.append(f"{k.replace('_', ' ').title()}: {v}") + if billing: + current_state.append(f"License model: {billing}") + if dr_raw: + current_state.append(f"Disaster recovery: {dr_raw}") + + line_items = [] + for svc in spec.get("services", []) or []: + if not isinstance(svc, dict): + continue + qty = svc.get("quantity", svc.get("qty")) + notes = svc.get("notes", "") + if qty is not None: + notes = f"Qty: {qty}" + (f" — {notes}" if notes else "") + line_items.append({"component": svc.get("name", ""), "monthly_payg": "—", "notes": notes}) + + cost_summary = spec.get("cost_summary") or {} + monthly = cost_summary.get("monthly_estimate") + if monthly is not None: + currency = cost_summary.get("currency", "USD") + annual = cost_summary.get("annual_estimate", monthly * 12) + line_items.append({ + "component": "Total (estimated)", + "monthly_payg": f"{currency} {monthly:,.2f}", + "notes": f"Annual: {currency} {annual:,.2f}", + }) + + adapted = { + "metadata": {"customer": customer, "subtitle": title}, + "summary": { + "why": f"{customer} — {title}" if customer and title else (customer or title), + "current_state": current_state, + "target_state": target_state, + "timeline": "", + }, + } + if line_items: + adapted["cost"] = { + "line_items": line_items, + "assumptions": spec.get("assumptions", []) or [], + "show_byol": False, + } + return adapted + + # ============================================================ # Slide Generator # ============================================================ @@ -1185,6 +1265,13 @@ class OCIDeckGenerator(OraclePresBase): @classmethod def from_spec(cls, spec: dict, template: Optional[str] = None) -> "OCIDeckGenerator": """Build a complete deck from a YAML specification.""" + # The MCP server passes a flat payload shape (customer_name, services, + # cost_summary, ...) that doesn't match the proposal-spec structure this + # method was originally written against. Adapt it so the deck renders + # with actual content instead of only blank title/closing slides. + if _is_flat_spec(spec): + spec = _adapt_flat_spec(spec) + meta = spec.get("metadata", {}) gen = cls( customer=meta.get("customer", ""),