Merge pull request #81 from jonathanhefner/examples-use-content-for-now

Use `content` instead of `structuredContent` in examples
This commit is contained in:
Jonathan Hefner 2025-12-05 16:36:32 -06:00 • committed by GitHub
commit 437b9185cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 65 additions and 150 deletions

View file

@ -5,7 +5,6 @@ import cors from "cors";
import express, { type Request, type Response } from "express"; import express, { type Request, type Response } from "express";
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { z } from "zod";
import { RESOURCE_URI_META_KEY } from "../../dist/src/app"; import { RESOURCE_URI_META_KEY } from "../../dist/src/app";
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3001; const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3001;
@ -30,14 +29,12 @@ const server = new McpServer({
title: "Get Time", title: "Get Time",
description: "Returns the current server time as an ISO 8601 string.", description: "Returns the current server time as an ISO 8601 string.",
inputSchema: {}, inputSchema: {},
outputSchema: { time: z.string() },
_meta: { [RESOURCE_URI_META_KEY]: resourceUri }, _meta: { [RESOURCE_URI_META_KEY]: resourceUri },
}, },
async (): Promise<CallToolResult> => { async (): Promise<CallToolResult> => {
const time = new Date().toISOString(); const time = new Date().toISOString();
return { return {
content: [{ type: "text", text: time }], content: [{ type: "text", text: JSON.stringify({ time }) }],
structuredContent: { time },
}; };
}, },
); );

View file

@ -20,8 +20,12 @@ const log = {
function extractTime(callToolResult: CallToolResult): string { function extractTime(callToolResult: CallToolResult): string {
const { time } = (callToolResult.structuredContent as { time?: string }) ?? {}; const text = callToolResult.content!
return time ?? "[ERROR]"; .filter((c): c is { type: "text"; text: string } => c.type === "text")
.map((c) => c.text)
.join("");
const { time } = JSON.parse(text) as { time: string };
return time;
} }

View file

@ -5,7 +5,6 @@ import cors from "cors";
import express, { type Request, type Response } from "express"; import express, { type Request, type Response } from "express";
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { z } from "zod";
import { RESOURCE_URI_META_KEY } from "../../dist/src/app"; import { RESOURCE_URI_META_KEY } from "../../dist/src/app";
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3001; const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3001;
@ -30,14 +29,12 @@ const server = new McpServer({
title: "Get Time", title: "Get Time",
description: "Returns the current server time as an ISO 8601 string.", description: "Returns the current server time as an ISO 8601 string.",
inputSchema: {}, inputSchema: {},
outputSchema: { time: z.string() },
_meta: { [RESOURCE_URI_META_KEY]: resourceUri }, _meta: { [RESOURCE_URI_META_KEY]: resourceUri },
}, },
async (): Promise<CallToolResult> => { async (): Promise<CallToolResult> => {
const time = new Date().toISOString(); const time = new Date().toISOString();
return { return {
content: [{ type: "text", text: time }], content: [{ type: "text", text: JSON.stringify({ time }) }],
structuredContent: { time },
}; };
}, },
); );

View file

@ -15,8 +15,12 @@ const log = {
function extractTime(result: CallToolResult): string { function extractTime(result: CallToolResult): string {
const { time } = (result.structuredContent as { time?: string }) ?? {}; const text = result.content!
return time ?? "[ERROR]"; .filter((c): c is { type: "text"; text: string } => c.type === "text")
.map((c) => c.text)
.join("");
const { time } = JSON.parse(text) as { time: string };
return time;
} }

View file

@ -221,30 +221,6 @@ function generateHistory(
return months; 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 // MCP Server Setup
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -263,7 +239,6 @@ server.registerTool(
description: description:
"Returns budget configuration with 24 months of historical allocations and industry benchmarks by company stage", "Returns budget configuration with 24 months of historical allocations and industry benchmarks by company stage",
inputSchema: {}, inputSchema: {},
outputSchema: BudgetDataResponseSchema.shape,
_meta: { [RESOURCE_URI_META_KEY]: resourceUri }, _meta: { [RESOURCE_URI_META_KEY]: resourceUri },
}, },
async (): Promise<CallToolResult> => { async (): Promise<CallToolResult> => {
@ -292,10 +267,9 @@ server.registerTool(
content: [ content: [
{ {
type: "text", type: "text",
text: formatBudgetSummary(response), text: JSON.stringify(response),
}, },
], ],
structuredContent: response,
}; };
}, },
); );

View file

@ -606,7 +606,13 @@ const app = new App({ name: "Budget Allocator", version: "1.0.0" });
app.ontoolresult = (result) => { app.ontoolresult = (result) => {
log.info("Received tool result:", result); log.info("Received tool result:", result);
const data = result.structuredContent as unknown as BudgetDataResponse; 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;
if (data?.config && data?.analytics) { if (data?.config && data?.analytics) {
initializeUI(data.config, data.analytics); initializeUI(data.config, data.analytics);
} }

View file

@ -149,17 +149,6 @@ 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}`;
}
const server = new McpServer({ const server = new McpServer({
name: "Cohort Heatmap Server", name: "Cohort Heatmap Server",
version: "1.0.0", version: "1.0.0",
@ -176,7 +165,6 @@ const server = new McpServer({
description: description:
"Returns cohort retention heatmap data showing customer retention over time by signup month", "Returns cohort retention heatmap data showing customer retention over time by signup month",
inputSchema: GetCohortDataInputSchema.shape, inputSchema: GetCohortDataInputSchema.shape,
outputSchema: CohortDataSchema.shape,
_meta: { [RESOURCE_URI_META_KEY]: resourceUri }, _meta: { [RESOURCE_URI_META_KEY]: resourceUri },
}, },
async ({ metric, periodType, cohortCount, maxPeriods }) => { async ({ metric, periodType, cohortCount, maxPeriods }) => {
@ -188,8 +176,7 @@ const server = new McpServer({
); );
return { return {
content: [{ type: "text", text: formatCohortSummary(data) }], content: [{ type: "text", text: JSON.stringify(data) }],
structuredContent: data,
}; };
}, },
); );

View file

@ -103,7 +103,13 @@ function CohortHeatmapInner({ app }: { app: App }) {
maxPeriods: 12, maxPeriods: 12,
}, },
}); });
setData(result.structuredContent as unknown as CohortData); 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);
} catch (e) { } catch (e) {
console.error("Failed to fetch cohort data:", e); console.error("Failed to fetch cohort data:", e);
} finally { } finally {

View file

@ -27,29 +27,6 @@ const GetCustomerDataInputSchema = z.object({
.describe("Filter by segment (default: All)"), .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 // Cache generated data for session consistency
let cachedCustomers: Customer[] | null = null; let cachedCustomers: Customer[] | null = null;
let cachedSegments: SegmentSummary[] | null = null; let cachedSegments: SegmentSummary[] | null = null;
@ -92,15 +69,13 @@ const server = new McpServer({
description: description:
"Returns customer data with segment information for visualization. Optionally filter by segment.", "Returns customer data with segment information for visualization. Optionally filter by segment.",
inputSchema: GetCustomerDataInputSchema.shape, inputSchema: GetCustomerDataInputSchema.shape,
outputSchema: GetCustomerDataOutputSchema.shape,
_meta: { [RESOURCE_URI_META_KEY]: resourceUri }, _meta: { [RESOURCE_URI_META_KEY]: resourceUri },
}, },
async ({ segment }): Promise<CallToolResult> => { async ({ segment }): Promise<CallToolResult> => {
const data = getCustomerData(segment); const data = getCustomerData(segment);
return { return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }], content: [{ type: "text", text: JSON.stringify(data) }],
structuredContent: data,
}; };
}, },
); );

View file

@ -336,7 +336,13 @@ async function fetchData(): Promise<void> {
arguments: {}, arguments: {},
}); });
const data = result.structuredContent as unknown as { 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 {
customers: Customer[]; customers: Customer[];
segments: SegmentSummary[]; segments: SegmentSummary[];
}; };

View file

@ -61,13 +61,6 @@ 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 // Types derived from schemas
type ScenarioInputs = z.infer<typeof ScenarioInputsSchema>; type ScenarioInputs = z.infer<typeof ScenarioInputsSchema>;
type MonthlyProjection = z.infer<typeof MonthlyProjectionSchema>; type MonthlyProjection = z.infer<typeof MonthlyProjectionSchema>;
@ -247,37 +240,6 @@ const DEFAULT_INPUTS: ScenarioInputs = {
fixedCosts: 30000, 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 // MCP Server
// ============================================================================ // ============================================================================
@ -298,7 +260,6 @@ const server = new McpServer({
description: description:
"Returns SaaS scenario templates and optionally computes custom projections for given inputs", "Returns SaaS scenario templates and optionally computes custom projections for given inputs",
inputSchema: GetScenarioDataInputSchema.shape, inputSchema: GetScenarioDataInputSchema.shape,
outputSchema: GetScenarioDataOutputSchema.shape,
_meta: { [RESOURCE_URI_META_KEY]: resourceUri }, _meta: { [RESOURCE_URI_META_KEY]: resourceUri },
}, },
async (args: { async (args: {
@ -308,28 +269,18 @@ const server = new McpServer({
? calculateScenario(args.customInputs) ? calculateScenario(args.customInputs)
: undefined; : 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 { return {
content: [{ type: "text", text }], content: [
structuredContent: { {
templates: SCENARIO_TEMPLATES, type: "text",
defaultInputs: DEFAULT_INPUTS, text: JSON.stringify({
customProjections: customScenario?.projections, templates: SCENARIO_TEMPLATES,
customSummary: customScenario?.summary, defaultInputs: DEFAULT_INPUTS,
}, customProjections: customScenario?.projections,
customSummary: customScenario?.summary,
}),
},
],
}; };
}, },
); );

View file

@ -24,11 +24,15 @@ interface CallToolResultData {
defaultInputs?: ScenarioInputs; defaultInputs?: ScenarioInputs;
} }
/** Extract templates and defaultInputs from tool result structuredContent */ /** Extract templates and defaultInputs from tool result content */
function extractResultData(result: CallToolResult): CallToolResultData { function extractResultData(result: CallToolResult): CallToolResultData {
if (!result.structuredContent) return {}; const text = result
const { templates, defaultInputs } = .content!.filter(
result.structuredContent as CallToolResultData; (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 }; return { templates, defaultInputs };
} }

View file

@ -121,7 +121,6 @@ const server = new McpServer({
description: description:
"Returns current system statistics including per-core CPU usage, memory, and system info.", "Returns current system statistics including per-core CPU usage, memory, and system info.",
inputSchema: {}, inputSchema: {},
outputSchema: SystemStatsSchema.shape,
_meta: { [RESOURCE_URI_META_KEY]: resourceUri }, _meta: { [RESOURCE_URI_META_KEY]: resourceUri },
}, },
async (): Promise<CallToolResult> => { async (): Promise<CallToolResult> => {
@ -148,8 +147,7 @@ const server = new McpServer({
}; };
return { return {
content: [{ type: "text", text: JSON.stringify(stats, null, 2) }], content: [{ type: "text", text: JSON.stringify(stats) }],
structuredContent: stats,
}; };
}, },
); );

View file

@ -264,7 +264,13 @@ async function fetchStats(): Promise<void> {
arguments: {}, arguments: {},
}); });
const stats = result.structuredContent as unknown as SystemStats; const text = result
.content!.filter(
(c): c is { type: "text"; text: string } => c.type === "text",
)
.map((c) => c.text)
.join("");
const stats = JSON.parse(text) as SystemStats;
// Initialize chart on first data if needed // Initialize chart on first data if needed
if (!state.chart && stats.cpu.count > 0) { if (!state.chart && stats.cpu.count > 0) {