Skip to main content
Glama

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

TableJSON Schema
NameRequiredDescriptionDefault
metrics_jsonYes
output_dirNo.

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
resultYes

Implementation Reference

  • 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>"""
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations provided, so description carries full burden. It discloses that reports are saved to disk and returns paths with inline content. However, it doesn't mention what happens if the output directory doesn't exist or if overwrite behavior, slightly reducing completeness.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Well-structured with clear sections (main action, args, returns). However, the description is somewhat lengthy and could be more concise by moving some parameter details into the schema description. Still, the front-loaded summary of the main purpose is effective.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity and the presence of an output schema (signaled), the description covers all necessary aspects: what it does, input format, output format, and usage constraints. No critical information is missing for an AI agent to use it correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With 0% schema description coverage, the description fully compensates by explaining the `metrics_json` parameter in great detail, including two accepted shapes and references to `computeFinancialMetrics`. It also clarifies the `output_dir` default. Adds significant meaning beyond the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool generates a unified HTML + Markdown financial report and saves to disk. It distinguishes itself from the sibling tool 'computeFinancialMetrics' by describing the input as its output, and emphasizes producing one report covering all months.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides explicit guidance on when to use: after `computeFinancialMetrics`. It explains the two accepted input shapes (single-month vs multi-month) and explicitly warns against generating one report per month, which gives clear usage context.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/MayankTalwar0/startup-finance-metrics'

If you have feedback or need assistance with the MCP directory API, please join our Discord server