generateFinancialReport
Generates a single unified HTML and Markdown financial report from metrics JSON data and saves it to disk.
Instructions
Generates a single unified HTML + Markdown financial report and saves them to disk.
Args:
metrics_json: JSON string. Two accepted shapes:
1. Single-month: the direct output from computeFinancialMetrics.
2. Multi-month (preferred when user supplies multiple months of data):
{
"source": "...",
"business_type": "saas",
"industry_confidence": "high|medium|low",
"industry_reasoning": "Why this industry was chosen, or why uncertain.",
"period_label": "March 2026 – May 2026",
"months": [
{"period": "March 2026", ...computeFinancialMetrics output for March},
{"period": "April 2026", ...computeFinancialMetrics output for April},
{"period": "May 2026", ...computeFinancialMetrics output for May}
]
}
Always produce ONE unified report covering all months the user supplied.
Do NOT generate one report per month.
output_dir: Directory to save reports to. Default is current directory.
Returns:
JSON with paths to both report files and the markdown content inline.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| metrics_json | Yes | ||
| output_dir | No | . |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| result | Yes |
Implementation Reference
- src/startup_finance_mcp/server.py:163-164 (registration)The `@mcp.tool()` decorator registers `generateFinancialReport` as an MCP tool.
@mcp.tool() def generateFinancialReport(metrics_json: str, output_dir: str = ".") -> str: - The `generateFinancialReport` function: parses JSON input, creates output directory, calls `render_markdown` and `render_html` from render_report.py, saves both .md and .html files, and returns paths + markdown content as JSON.
def generateFinancialReport(metrics_json: str, output_dir: str = ".") -> str: """ Generates a single unified HTML + Markdown financial report and saves them to disk. Args: metrics_json: JSON string. Two accepted shapes: 1. Single-month: the direct output from `computeFinancialMetrics`. 2. Multi-month (preferred when user supplies multiple months of data): { "source": "...", "business_type": "saas", "industry_confidence": "high|medium|low", "industry_reasoning": "Why this industry was chosen, or why uncertain.", "period_label": "March 2026 – May 2026", "months": [ {"period": "March 2026", ...computeFinancialMetrics output for March}, {"period": "April 2026", ...computeFinancialMetrics output for April}, {"period": "May 2026", ...computeFinancialMetrics output for May} ] } Always produce ONE unified report covering all months the user supplied. Do NOT generate one report per month. output_dir: Directory to save reports to. Default is current directory. Returns: JSON with paths to both report files and the markdown content inline. """ try: payload = json.loads(metrics_json) except json.JSONDecodeError as e: logger.error(f"Failed to parse metrics_json: {e}") return json.dumps({"error": f"Invalid JSON input: {e}"}) out = Path(output_dir) out.mkdir(parents=True, exist_ok=True) md_content = render_markdown(payload) html_content = render_html(payload) md_path = out / "financial_report.md" html_path = out / "financial_report.html" md_path.write_text(md_content, encoding="utf-8") html_path.write_text(html_content, encoding="utf-8") logger.info(f"Reports saved: {md_path.resolve()}, {html_path.resolve()}") return json.dumps({ "md_path": str(md_path.resolve()), "html_path": str(html_path.resolve()), "markdown_content": md_content, }) - Docstring defines the accepted JSON schemas: single-month (direct computeFinancialMetrics output) or multi-month (with months array, period_label, business_type, industry_confidence, industry_reasoning).
""" Generates a single unified HTML + Markdown financial report and saves them to disk. Args: metrics_json: JSON string. Two accepted shapes: 1. Single-month: the direct output from `computeFinancialMetrics`. 2. Multi-month (preferred when user supplies multiple months of data): { "source": "...", "business_type": "saas", "industry_confidence": "high|medium|low", "industry_reasoning": "Why this industry was chosen, or why uncertain.", "period_label": "March 2026 – May 2026", "months": [ {"period": "March 2026", ...computeFinancialMetrics output for March}, {"period": "April 2026", ...computeFinancialMetrics output for April}, {"period": "May 2026", ...computeFinancialMetrics output for May} ] } Always produce ONE unified report covering all months the user supplied. Do NOT generate one report per month. output_dir: Directory to save reports to. Default is current directory. Returns: JSON with paths to both report files and the markdown content inline. """ - `render_markdown(payload)` — dispatches to `render_markdown_multi` if multi-month, otherwise generates a single-month Markdown table with core + industry metrics, disclaimer, and metadata.
def render_markdown(payload): if _is_multi_month(payload): return render_markdown_multi(payload) source = payload.get("source", "manual") business_type = payload.get("business_type", "other") industry_name = INDUSTRY_DISPLAY_NAMES.get(business_type, business_type.title()) rows = _build_rows(payload.get("metrics", {})) industry_rows = _build_industry_rows(payload.get("industry_metrics", {})) lines = [] lines.append("# Financial Report") lines.append("") lines.append(f"- Generated: {datetime.now(UTC).strftime('%Y-%m-%d %H:%M UTC')}") lines.append(f"- Source: {source}") lines.append(f"- Industry: {industry_name}") lines.append("") lines.append(AI_DISCLAIMER) lines.append("") # Common metrics table lines.append("## Core Metrics") lines.append("") lines.append("| Metric | Value | Label | Reason | Missing Inputs |") lines.append("|---|---:|---|---|---|") for row in rows: lines.append( f"| {row['metric_name']} | {row['value']} | {row['label']} | {row['reason']} | {row['missing']} |" ) lines.append("") # Industry-specific metrics table if industry_rows: lines.append(f"## {industry_name} Industry Metrics") lines.append("") lines.append("| Metric | Value | Label | Reason | Missing Inputs |") lines.append("|---|---:|---|---|---|") for row in industry_rows: lines.append( f"| {row['metric_name']} | {row['value']} | {row['label']} | {row['reason']} | {row['missing']} |" ) lines.append("") return "\n".join(lines) - `render_html(payload)` — dispatches to `render_html_multi` if multi-month, otherwise generates a styled single-month HTML page with core + industry metrics tables, AI disclaimer, and dark-themed CSS.
def render_html(payload): if _is_multi_month(payload): return render_html_multi(payload) source = escape(str(payload.get("source", "manual"))) business_type = payload.get("business_type", "other") industry_name = escape(INDUSTRY_DISPLAY_NAMES.get(business_type, business_type.title())) rows = _build_rows(payload.get("metrics", {})) industry_rows = _build_industry_rows(payload.get("industry_metrics", {})) def _rows_to_html(row_list): html_parts = [] for row in row_list: label = escape(row["label"]) cls = f"label {label}" if label else "label" html_parts.append( "<tr>" f"<td>{escape(row['metric_name'])}</td>" f"<td class='num'>{escape(row['value'])}</td>" f"<td><span class='{cls}'>{label}</span></td>" f"<td>{escape(row['reason'])}</td>" f"<td>{escape(row['missing'])}</td>" "</tr>" ) return "".join(html_parts) row_html = _rows_to_html(rows) industry_html = _rows_to_html(industry_rows) industry_section = "" if industry_rows: industry_section = f""" <div class="section-head"><h2>{industry_name} Industry Metrics</h2></div> <table> <thead> <tr> <th>Metric</th> <th style="text-align:right">Value</th> <th>Label</th> <th>Reason</th> <th>Missing Inputs</th> </tr> </thead> <tbody> {industry_html} </tbody> </table>""" generated = datetime.now(UTC).strftime("%Y-%m-%d %H:%M UTC") return f"""<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Financial Report</title> <style> :root {{ --bg: #0b1220; --panel: #111a2b; --panel-2: #0f1726; --text: #e5edf8; --muted: #9fb1c9; --line: #243247; --strong: #2dd4bf; --adequate: #60a5fa; --weak: #f59e0b; --critical: #ef4444; --insufficient_data: #a3a3a3; --not_applicable: #a3a3a3; --accent: #818cf8; }} body {{ margin: 0; font-family: "Segoe UI", Arial, sans-serif; background: var(--bg); color: var(--text); }} .wrap {{ max-width: 1100px; margin: 24px auto; padding: 0 16px; }} .panel {{ background: var(--panel); border: 1px solid var(--line); border-radius: 8px; overflow: hidden; box-shadow: 0 14px 36px rgba(0, 0, 0, 0.45); margin-bottom: 20px; }} .head {{ padding: 16px; border-bottom: 1px solid var(--line); }} .section-head {{ padding: 12px 16px; border-bottom: 1px solid var(--line); background: var(--panel-2); }} .section-head h2 {{ margin: 0; font-size: 16px; color: var(--accent); }} h1 {{ margin: 0 0 4px; font-size: 24px; }} .meta {{ color: var(--muted); font-size: 13px; }} .disclaimer {{ padding: 12px 16px; background: rgba(245, 158, 11, 0.08); border-left: 3px solid var(--weak); font-size: 12px; color: var(--muted); line-height: 1.5; }} table {{ width: 100%; border-collapse: collapse; }} th, td {{ padding: 10px 12px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; font-size: 13px; }} th {{ background: var(--panel-2); font-weight: 600; }} td.num {{ text-align: right; font-variant-numeric: tabular-nums; }} .label {{ display: inline-block; border-radius: 999px; padding: 2px 8px; font-size: 12px; border: 1px solid currentColor; line-height: 1.4; }} .strong {{ color: var(--strong); }} .adequate {{ color: var(--adequate); }} .weak {{ color: var(--weak); }} .critical {{ color: var(--critical); }} .insufficient_data {{ color: var(--insufficient_data); }} .not_applicable {{ color: var(--not_applicable); }} </style> </head> <body> <div class="wrap"> <div class="panel"> <div class="head"> <h1>Financial Report</h1> <div class="meta">Generated: {escape(generated)} | Source: {source} | Industry: {industry_name}</div> </div> <div class="disclaimer"> ⚠️ <strong>AI Categorization Notice:</strong> Some metrics may have been derived from AI-categorized bank transactions. Only explicitly identified line items were summed — no percentage-based estimates or hardcoded assumptions were used. Please review categorizations for accuracy. </div> <table> <thead> <tr> <th>Metric</th> <th style="text-align:right">Value</th> <th>Label</th> <th>Reason</th> <th>Missing Inputs</th> </tr> </thead> <tbody> {row_html} </tbody> </table> {industry_section} </div> </div> </body> </html>"""