Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion examples/basic-server-vanillajs/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
import fs from "node:fs/promises";
import path from "node:path";
import { z } from "zod";
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "@modelcontextprotocol/ext-apps/server";
import { startServer } from "./server-utils.js";

Expand All @@ -28,12 +29,16 @@ export function createServer(): McpServer {
title: "Get Time",
description: "Returns the current server time as an ISO 8601 string.",
inputSchema: {},
outputSchema: z.object({
time: z.string(),
}),
_meta: { [RESOURCE_URI_META_KEY]: RESOURCE_URI },
},
async (): Promise<CallToolResult> => {
const time = new Date().toISOString();
return {
content: [{ type: "text", text: JSON.stringify({ time }) }],
content: [{ type: "text", text: time }],
structuredContent: { time },
};
},
);
Expand Down
4 changes: 2 additions & 2 deletions examples/basic-server-vanillajs/src/mcp-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ const log = {


function extractTime(result: CallToolResult): string {
const { text } = result.content?.find((c) => c.type === "text")!;
return text;
const { time } = (result.structuredContent as { time?: string }) ?? {};
return time ?? "[ERROR]";
}


Expand Down
28 changes: 27 additions & 1 deletion examples/budget-allocator-server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,30 @@ function generateHistory(
return months;
}

// ---------------------------------------------------------------------------
// Response Formatting
// ---------------------------------------------------------------------------

function formatBudgetSummary(data: BudgetDataResponse): string {
const lines: string[] = [
"Budget Allocator Configuration",
"==============================",
"",
`Default Budget: ${data.config.currencySymbol}${data.config.defaultBudget.toLocaleString()}`,
`Available Presets: ${data.config.presetBudgets.map((b) => `${data.config.currencySymbol}${b.toLocaleString()}`).join(", ")}`,
"",
"Categories:",
...data.config.categories.map(
(c) => ` - ${c.name}: ${c.defaultPercent}% default`,
),
"",
`Historical Data: ${data.analytics.history.length} months`,
`Benchmark Stages: ${data.analytics.stages.join(", ")}`,
`Default Stage: ${data.analytics.defaultStage}`,
];
return lines.join("\n");
}

// ---------------------------------------------------------------------------
// MCP Server Setup
// ---------------------------------------------------------------------------
Expand All @@ -248,6 +272,7 @@ export function createServer(): McpServer {
description:
"Returns budget configuration with 24 months of historical allocations and industry benchmarks by company stage",
inputSchema: {},
outputSchema: BudgetDataResponseSchema,
_meta: { [RESOURCE_URI_META_KEY]: resourceUri },
},
async (): Promise<CallToolResult> => {
Expand Down Expand Up @@ -276,9 +301,10 @@ export function createServer(): McpServer {
content: [
{
type: "text",
text: JSON.stringify(response),
text: formatBudgetSummary(response),
},
],
structuredContent: response,
};
},
);
Expand Down
8 changes: 1 addition & 7 deletions examples/budget-allocator-server/src/mcp-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -607,13 +607,7 @@ const app = new App({ name: "Budget Allocator", version: "1.0.0" });

app.ontoolresult = (result) => {
log.info("Received tool result:", result);
const text = result
.content!.filter(
(c): c is { type: "text"; text: string } => c.type === "text",
)
.map((c) => c.text)
.join("");
const data = JSON.parse(text) as BudgetDataResponse;
const data = result.structuredContent as unknown as BudgetDataResponse;
if (data?.config && data?.analytics) {
initializeUI(data.config, data.analytics);
}
Expand Down
15 changes: 14 additions & 1 deletion examples/cohort-heatmap-server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,17 @@ function generateCohortData(
};
}

function formatCohortSummary(data: CohortData): string {
const avgRetention = data.cohorts
.flatMap((c) => c.cells)
.filter((cell) => cell.periodIndex > 0)
.reduce((sum, cell, _, arr) => sum + cell.retention / arr.length, 0);

return `Cohort Analysis: ${data.cohorts.length} cohorts, ${data.periods.length} periods
Average retention: ${(avgRetention * 100).toFixed(1)}%
Metric: ${data.metric}, Period: ${data.periodType}`;
}

export function createServer(): McpServer {
const server = new McpServer({
name: "Cohort Heatmap Server",
Expand All @@ -169,6 +180,7 @@ export function createServer(): McpServer {
description:
"Returns cohort retention heatmap data showing customer retention over time by signup month",
inputSchema: GetCohortDataInputSchema.shape,
outputSchema: CohortDataSchema.shape,
_meta: { [RESOURCE_URI_META_KEY]: resourceUri },
},
async ({ metric, periodType, cohortCount, maxPeriods }) => {
Expand All @@ -180,7 +192,8 @@ export function createServer(): McpServer {
);

return {
content: [{ type: "text", text: JSON.stringify(data) }],
content: [{ type: "text", text: formatCohortSummary(data) }],
structuredContent: data,
};
},
);
Expand Down
8 changes: 1 addition & 7 deletions examples/cohort-heatmap-server/src/mcp-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,7 @@ function CohortHeatmapInner({
maxPeriods: 12,
},
});
const text = result
.content!.filter(
(c): c is { type: "text"; text: string } => c.type === "text",
)
.map((c) => c.text)
.join("");
setData(JSON.parse(text) as CohortData);
setData(result.structuredContent as unknown as CohortData);
} catch (e) {
console.error("Failed to fetch cohort data:", e);
} finally {
Expand Down
25 changes: 25 additions & 0 deletions examples/customer-segmentation-server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,29 @@ const GetCustomerDataInputSchema = z.object({
.describe("Filter by segment (default: All)"),
});

const CustomerSchema = z.object({
id: z.string(),
name: z.string(),
segment: z.string(),
annualRevenue: z.number(),
employeeCount: z.number(),
accountAge: z.number(),
engagementScore: z.number(),
supportTickets: z.number(),
nps: z.number(),
});

const SegmentSummarySchema = z.object({
name: z.string(),
count: z.number(),
color: z.string(),
});

const GetCustomerDataOutputSchema = z.object({
customers: z.array(CustomerSchema),
segments: z.array(SegmentSummarySchema),
});

// Cache generated data for session consistency
let cachedCustomers: Customer[] | null = null;
let cachedSegments: SegmentSummary[] | null = null;
Expand Down Expand Up @@ -78,13 +101,15 @@ export function createServer(): McpServer {
description:
"Returns customer data with segment information for visualization. Optionally filter by segment.",
inputSchema: GetCustomerDataInputSchema.shape,
outputSchema: GetCustomerDataOutputSchema.shape,
_meta: { [RESOURCE_URI_META_KEY]: resourceUri },
},
async ({ segment }): Promise<CallToolResult> => {
const data = getCustomerData(segment);

return {
content: [{ type: "text", text: JSON.stringify(data) }],
structuredContent: data,
};
},
);
Expand Down
8 changes: 7 additions & 1 deletion examples/integration-server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
} from "@modelcontextprotocol/sdk/types.js";
import fs from "node:fs/promises";
import path from "node:path";
import { z } from "zod";
import {
registerAppTool,
registerAppResource,
Expand Down Expand Up @@ -33,16 +34,21 @@ export function createServer(): McpServer {
title: "Get Time",
description: "Returns the current server time.",
inputSchema: {},
outputSchema: z.object({
time: z.string(),
}),
_meta: { [RESOURCE_URI_META_KEY]: RESOURCE_URI },
},
async (): Promise<CallToolResult> => {
const time = new Date().toISOString();
return {
content: [
{
type: "text",
text: JSON.stringify({ time: new Date().toISOString() }),
text: JSON.stringify({ time }),
},
],
structuredContent: { time },
};
},
);
Expand Down
8 changes: 1 addition & 7 deletions examples/integration-server/src/mcp-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,7 @@ const log = {
};

function extractTime(callToolResult: CallToolResult): string {
const text = callToolResult
.content!.filter(
(c): c is { type: "text"; text: string } => c.type === "text",
)
.map((c) => c.text)
.join("");
const { time } = JSON.parse(text) as { time: string };
const { time } = callToolResult.structuredContent as { time: string };
return time;
}

Expand Down
71 changes: 60 additions & 11 deletions examples/scenario-modeler-server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ const GetScenarioDataInputSchema = z.object({
),
});

const GetScenarioDataOutputSchema = z.object({
templates: z.array(ScenarioTemplateSchema),
defaultInputs: ScenarioInputsSchema,
customProjections: z.array(MonthlyProjectionSchema).optional(),
customSummary: ScenarioSummarySchema.optional(),
});

// Types derived from schemas
type ScenarioInputs = z.infer<typeof ScenarioInputsSchema>;
type MonthlyProjection = z.infer<typeof MonthlyProjectionSchema>;
Expand Down Expand Up @@ -243,6 +250,37 @@ const DEFAULT_INPUTS: ScenarioInputs = {
fixedCosts: 30000,
};

// ============================================================================
// Formatters for text output
// ============================================================================

function formatCurrency(value: number): string {
const absValue = Math.abs(value);
const sign = value < 0 ? "-" : "";
if (absValue >= 1_000_000) {
return `${sign}$${(absValue / 1_000_000).toFixed(2)}M`;
}
if (absValue >= 1_000) {
return `${sign}$${(absValue / 1_000).toFixed(1)}K`;
}
return `${sign}$${Math.round(absValue)}`;
}

function formatScenarioSummary(
summary: ScenarioSummary,
label: string,
): string {
return [
`${label}:`,
` Ending MRR: ${formatCurrency(summary.endingMRR)}`,
` ARR: ${formatCurrency(summary.arr)}`,
` Total Revenue: ${formatCurrency(summary.totalRevenue)}`,
` Total Profit: ${formatCurrency(summary.totalProfit)}`,
` MRR Growth: ${summary.mrrGrowthPct.toFixed(1)}%`,
` Break-even: ${summary.breakEvenMonth ? `Month ${summary.breakEvenMonth}` : "Not achieved"}`,
].join("\n");
}

// ============================================================================
// MCP Server
// ============================================================================
Expand All @@ -269,6 +307,7 @@ export function createServer(): McpServer {
description:
"Returns SaaS scenario templates and optionally computes custom projections for given inputs",
inputSchema: GetScenarioDataInputSchema.shape,
outputSchema: GetScenarioDataOutputSchema.shape,
_meta: { [RESOURCE_URI_META_KEY]: resourceUri },
},
async (args: {
Expand All @@ -278,18 +317,28 @@ export function createServer(): McpServer {
? calculateScenario(args.customInputs)
: undefined;

const text = [
"SaaS Scenario Modeler",
"=".repeat(40),
"",
"Available Templates:",
...SCENARIO_TEMPLATES.map(
(t) => ` ${t.icon} ${t.name}: ${t.description}`,
),
"",
customScenario
? formatScenarioSummary(customScenario.summary, "Custom Scenario")
: "Use customInputs parameter to compute projections for a specific scenario.",
].join("\n");

return {
content: [
{
type: "text",
text: JSON.stringify({
templates: SCENARIO_TEMPLATES,
defaultInputs: DEFAULT_INPUTS,
customProjections: customScenario?.projections,
customSummary: customScenario?.summary,
}),
},
],
content: [{ type: "text", text }],
structuredContent: {
templates: SCENARIO_TEMPLATES,
defaultInputs: DEFAULT_INPUTS,
customProjections: customScenario?.projections,
customSummary: customScenario?.summary,
},
};
},
);
Expand Down
16 changes: 2 additions & 14 deletions examples/scenario-modeler-server/src/mcp-app.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { McpUiHostContext } from "@modelcontextprotocol/ext-apps";
import { useApp } from "@modelcontextprotocol/ext-apps/react";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { StrictMode, useState, useMemo, useCallback, useEffect } from "react";
import { createRoot } from "react-dom/client";
import { SliderRow } from "./components/SliderRow.tsx";
Expand All @@ -25,18 +24,6 @@ interface CallToolResultData {
defaultInputs?: ScenarioInputs;
}

/** Extract templates and defaultInputs from tool result content */
function extractResultData(result: CallToolResult): CallToolResultData {
const text = result
.content!.filter(
(c): c is { type: "text"; text: string } => c.type === "text",
)
.map((c) => c.text)
.join("");
const { templates, defaultInputs } = JSON.parse(text) as CallToolResultData;
return { templates, defaultInputs };
}

const APP_INFO = { name: "SaaS Scenario Modeler", version: "1.0.0" };

function getSafeAreaPaddingStyle(hostContext?: McpUiHostContext) {
Expand Down Expand Up @@ -70,7 +57,8 @@ function ScenarioModeler() {
capabilities: {},
onAppCreated: (app) => {
app.ontoolresult = async (result) => {
const { templates, defaultInputs } = extractResultData(result);
const { templates, defaultInputs } =
result.structuredContent as unknown as CallToolResultData;
if (templates) setTemplates(templates);
if (defaultInputs) setDefaultInputs(defaultInputs);
};
Expand Down
2 changes: 2 additions & 0 deletions examples/system-monitor-server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export function createServer(): McpServer {
description:
"Returns current system statistics including per-core CPU usage, memory, and system info.",
inputSchema: {},
outputSchema: SystemStatsSchema.shape,
_meta: { [RESOURCE_URI_META_KEY]: resourceUri },
},
async (): Promise<CallToolResult> => {
Expand Down Expand Up @@ -151,6 +152,7 @@ export function createServer(): McpServer {

return {
content: [{ type: "text", text: JSON.stringify(stats) }],
structuredContent: stats,
};
},
);
Expand Down
Loading
Loading