import type { McpUiHostContext } from "@modelcontextprotocol/ext-apps"; import { useApp } from "@modelcontextprotocol/ext-apps/react"; import { StrictMode, useState, useMemo, useCallback, useEffect } from "react"; import { createRoot } from "react-dom/client"; import { SliderRow } from "./components/SliderRow.tsx"; import { MetricCard } from "./components/MetricCard.tsx"; import { ProjectionChart } from "./components/ProjectionChart.tsx"; import { calculateProjections, calculateSummary } from "./lib/calculations.ts"; import { formatCurrency, formatPercent, formatCurrencySlider, } from "./lib/formatters.ts"; import type { ScenarioInputs, ScenarioTemplate, ScenarioSummary, } from "./types.ts"; import "./global.css"; import "./mcp-app.css"; interface CallToolResultData { templates?: ScenarioTemplate[]; defaultInputs?: ScenarioInputs; } const APP_INFO = { name: "SaaS Scenario Modeler", version: "1.0.0" }; function getSafeAreaPaddingStyle(hostContext?: McpUiHostContext) { return { paddingTop: hostContext?.safeAreaInsets?.top, paddingRight: hostContext?.safeAreaInsets?.right, paddingBottom: hostContext?.safeAreaInsets?.bottom, paddingLeft: hostContext?.safeAreaInsets?.left, }; } // Local defaults for immediate render (should match server's DEFAULT_INPUTS) const FALLBACK_INPUTS: ScenarioInputs = { startingMRR: 50000, monthlyGrowthRate: 5, monthlyChurnRate: 3, grossMargin: 80, fixedCosts: 30000, }; function ScenarioModeler() { const [templates, setTemplates] = useState([]); const [defaultInputs, setDefaultInputs] = useState(FALLBACK_INPUTS); const [hostContext, setHostContext] = useState< McpUiHostContext | undefined >(); const { app, error } = useApp({ appInfo: APP_INFO, capabilities: {}, onAppCreated: (app) => { app.ontoolresult = async (result) => { const { templates, defaultInputs } = result.structuredContent as unknown as CallToolResultData; if (templates) setTemplates(templates); if (defaultInputs) setDefaultInputs(defaultInputs); }; app.onhostcontextchanged = (params) => { setHostContext((prev) => ({ ...prev, ...params })); }; }, }); useEffect(() => { if (app) { setHostContext(app.getHostContext()); } }, [app]); if (error) { return (
Error: {error.message}
); } if (!app) { return (
Connecting...
); } return ( ); } interface ScenarioModelerInnerProps { templates: ScenarioTemplate[]; defaultInputs: ScenarioInputs; hostContext?: McpUiHostContext; } function ScenarioModelerInner({ templates, defaultInputs, hostContext, }: ScenarioModelerInnerProps) { const [inputs, setInputs] = useState(FALLBACK_INPUTS); const [selectedTemplateId, setSelectedTemplateId] = useState( null, ); // Derived state - recalculates when inputs change const projections = useMemo(() => calculateProjections(inputs), [inputs]); const summary = useMemo( () => calculateSummary(projections, inputs), [projections, inputs], ); // Selected template (if any) const selectedTemplate = useMemo( () => templates.find((t) => t.id === selectedTemplateId) ?? null, [templates, selectedTemplateId], ); // Handlers const handleInputChange = useCallback( (key: keyof ScenarioInputs, value: number) => { setInputs((prev) => ({ ...prev, [key]: value })); }, [], ); const handleReset = useCallback(() => { setInputs(defaultInputs); setSelectedTemplateId(null); }, [defaultInputs]); const handleTemplateSelect = useCallback( (e: React.ChangeEvent) => { const value = e.target.value; setSelectedTemplateId(value || null); }, [], ); const handleLoadTemplate = useCallback(() => { if (selectedTemplate) { setInputs(selectedTemplate.parameters); setSelectedTemplateId(null); } }, [selectedTemplate]); return (
{/* Header */}

SaaS Scenario Modeler

{selectedTemplate && ( )}
{/* Parameters */}

Parameters

handleInputChange("startingMRR", v)} /> formatPercent(v)} onChange={(v) => handleInputChange("monthlyGrowthRate", v)} /> formatPercent(v)} onChange={(v) => handleInputChange("monthlyChurnRate", v)} /> formatPercent(v)} onChange={(v) => handleInputChange("grossMargin", v)} /> handleInputChange("fixedCosts", v)} />
{/* Chart */} {/* Metrics */}
); } interface MetricsSectionProps { userSummary: ScenarioSummary; templateSummary: ScenarioSummary | null; templateName?: string; } function MetricsSection({ userSummary, templateSummary, templateName, }: MetricsSectionProps) { const profitVariant = userSummary.totalProfit >= 0 ? "positive" : "negative"; // Calculate comparison delta const profitDelta = templateSummary ? ((userSummary.totalProfit - templateSummary.totalProfit) / Math.abs(templateSummary.totalProfit)) * 100 : null; return (
{/* User's scenario */}

Your Scenario

{/* Template comparison (only when selected) */} {templateSummary && templateName && (

vs. {templateName}

= 0 ? "positive" : "negative" } />
)}
{/* Summary row */}
Break-even:{" "} {userSummary.breakEvenMonth ? `Month ${userSummary.breakEvenMonth}` : "Not achieved"} MRR Growth:{" "} = 0 ? "positive" : "negative"}`} > {formatPercent(userSummary.mrrGrowthPct, true)} {profitDelta !== null && ( vs. template:{" "} = 0 ? "positive" : "negative"}`} > {formatPercent(profitDelta, true)} )}
); } createRoot(document.getElementById("root")!).render( , );