Add scenario-modeler-server example

Demo MCP App for SaaS financial planning with:
- Interactive sliders for MRR, growth, churn, margin, and costs
- 12-month projection charts (MRR, Gross Profit, Net Profit)
- 5 pre-built scenario templates (Bootstrapped, VC Rocketship, etc.)
- Template comparison overlay for strategy analysis
- Light/dark theme support

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jonathan Hefner 2025-12-04 13:46:38 -06:00
commit 79fa10097b
2 changed files with 1743 additions and 0 deletions

View file

@ -0,0 +1,72 @@
import type {
ScenarioInputs,
MonthlyProjection,
ScenarioSummary,
} from "../types.ts";
export function calculateProjections(
inputs: ScenarioInputs,
): MonthlyProjection[] {
const {
startingMRR,
monthlyGrowthRate,
monthlyChurnRate,
grossMargin,
fixedCosts,
} = inputs;
// Net growth rate (can be negative if churn > growth)
const netGrowthRate = (monthlyGrowthRate - monthlyChurnRate) / 100;
const projections: MonthlyProjection[] = [];
let cumulativeRevenue = 0;
for (let month = 1; month <= 12; month++) {
// MRR with compound growth
const mrr = startingMRR * Math.pow(1 + netGrowthRate, month);
// Profit calculations
const grossProfit = mrr * (grossMargin / 100);
const netProfit = grossProfit - fixedCosts;
// Cumulative revenue
cumulativeRevenue += mrr;
projections.push({
month,
mrr,
grossProfit,
netProfit,
cumulativeRevenue,
});
}
return projections;
}
export function calculateSummary(
projections: MonthlyProjection[],
inputs: ScenarioInputs,
): ScenarioSummary {
const endingMRR = projections[11].mrr;
const arr = endingMRR * 12;
const totalRevenue = projections.reduce((sum, p) => sum + p.mrr, 0);
const totalProfit = projections.reduce((sum, p) => sum + p.netProfit, 0);
const mrrGrowthPct =
((endingMRR - inputs.startingMRR) / inputs.startingMRR) * 100;
const avgMargin = (totalProfit / totalRevenue) * 100;
// Find first month where netProfit >= 0
const breakEvenProjection = projections.find((p) => p.netProfit >= 0);
const breakEvenMonth = breakEvenProjection?.month ?? null;
return {
endingMRR,
arr,
totalRevenue,
totalProfit,
mrrGrowthPct,
avgMargin,
breakEvenMonth,
};
}

View file

@ -0,0 +1,44 @@
/**
* Format a number as currency with abbreviated suffixes.
* Examples: $50K, $1.2M, -$30K
*/
export function formatCurrency(value: number): string {
const absValue = Math.abs(value);
const sign = value < 0 ? "-" : "";
if (absValue >= 1_000_000) {
const formatted = (absValue / 1_000_000).toFixed(
absValue >= 10_000_000 ? 1 : 2,
);
// Remove trailing zeros after decimal
const cleaned = formatted.replace(/\.?0+$/, "");
return `${sign}$${cleaned}M`;
}
if (absValue >= 1_000) {
const formatted = (absValue / 1_000).toFixed(absValue >= 100_000 ? 0 : 1);
const cleaned = formatted.replace(/\.?0+$/, "");
return `${sign}$${cleaned}K`;
}
return `${sign}$${Math.round(absValue)}`;
}
/**
* Format a number as a percentage.
* Examples: 5%, -2.5%, +12%
*/
export function formatPercent(value: number, showSign = false): string {
const sign = showSign && value > 0 ? "+" : "";
return `${sign}${value.toFixed(1).replace(/\.0$/, "")}%`;
}
/**
* Format currency for slider display (always show K suffix for consistency).
*/
export function formatCurrencySlider(value: number): string {
if (value >= 1_000) {
return `$${(value / 1_000).toFixed(0)}K`;
}
return `$${value}`;
}