Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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
8 changes: 7 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,17 @@ 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();
const data = { time };
return {
content: [{ type: "text", text: JSON.stringify({ time }) }],
content: [{ type: "text", text: JSON.stringify(data) }],
structuredContent: data,
};
Comment on lines 38 to 42

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm inclined to write this as either:

      const data = { time: new Date().toISOString() };
      return {
        content: [{ type: "text", text: JSON.stringify(data) }],
        structuredContent: data,
      };  

Or as:

      const time = new Date().toISOString();
      return {
        content: [{ type: "text", text: time }],
        structuredContent: { time },
      };  

depending on how strict we want to be about content vs structuredContent equivalence.

I also think we should update the Quickstart guide to match whatever basic-server-vanillajs does, because it links to it as the full example.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! Updated to match the quickstart pattern:

const time = new Date().toISOString();
return {
  content: [{ type: "text", text: time }],
  structuredContent: { time },
};

The quickstart guide already uses this pattern (lines 127-132), so no changes needed there.

},
);
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 data = result.structuredContent as { time: string };
return data.time;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Originally, I wrote this as:

  const { time } = (result.structuredContent as { time?: string }) ?? {};
  return time ?? "[ERROR]";

to handle potential type desync, though I don't feel too strongly about it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! Updated to use the defensive pattern:

const { time } = (result.structuredContent as { time?: string }) ?? {};
return time ?? "[ERROR]";

This also matches what the quickstart guide shows (lines 223-224).

}


Expand Down
2 changes: 2 additions & 0 deletions examples/budget-allocator-server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,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 @@ -279,6 +280,7 @@ export function createServer(): McpServer {
text: JSON.stringify(response),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Originally, this was richly formatted text — see 61cd4ed#diff-4360a472ed891e48c204f1cf177aec8fb637d96bff5047cba6a1e4a899971b1aL224-R270.

Not sure whether we care about restoring that for these examples.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restored! Added formatBudgetSummary that produces human-readable output like:

Budget Allocator Configuration
==============================

Default Budget: $100,000
Available Presets: $50,000, $100,000, $250,000, $500,000

Categories:
  - Marketing: 25% default
  - Engineering: 35% default
  ...

The structuredContent still contains the full data for the UI.

},
],
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
2 changes: 2 additions & 0 deletions examples/cohort-heatmap-server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,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 @@ -181,6 +182,7 @@ export function createServer(): McpServer {

return {
content: [{ type: "text", text: JSON.stringify(data) }],

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restored! Added formatCohortSummary that produces:

Cohort Analysis: 12 cohorts, 12 periods
Average retention: 45.2%
Metric: retention, Period: monthly

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
23 changes: 17 additions & 6 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 @@ -269,6 +276,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 +286,21 @@ export function createServer(): McpServer {
? calculateScenario(args.customInputs)
: undefined;

const data = {
templates: SCENARIO_TEMPLATES,
defaultInputs: DEFAULT_INPUTS,
customProjections: customScenario?.projections,
customSummary: customScenario?.summary,
};

return {
content: [
{
type: "text",
text: JSON.stringify({
templates: SCENARIO_TEMPLATES,
defaultInputs: DEFAULT_INPUTS,
customProjections: customScenario?.projections,
customSummary: customScenario?.summary,
}),
text: JSON.stringify(data),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restored! Added formatCurrency and formatScenarioSummary that produce:

SaaS Scenario Modeler
========================================

Available Templates:
  🌱 Bootstrapped Growth: Low burn, steady growth, path to profitability
  🚀 VC Rocketship: High burn, explosive growth, raise more later
  ...

Custom Scenario:
  Ending MRR: $62.5K
  ARR: $750.0K
  Total Revenue: $678.2K
  ...

},
],
structuredContent: data,
};
},
);
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
8 changes: 1 addition & 7 deletions examples/system-monitor-server/src/mcp-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,13 +265,7 @@ async function fetchStats(): Promise<void> {
arguments: {},
});

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;
const stats = result.structuredContent as unknown as SystemStats;

// Initialize chart on first data if needed
if (!state.chart && stats.cpu.count > 0) {
Expand Down
13 changes: 7 additions & 6 deletions examples/threejs-server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,16 +166,17 @@ export function createServer(): McpServer {
.default(400)
.describe("Height in pixels"),
},
outputSchema: z.object({
code: z.string(),
height: z.number(),
}),
_meta: { [RESOURCE_URI_META_KEY]: resourceUri },
},
async ({ code, height }) => {
const data = { code, height };
return {
content: [
{
type: "text",
text: JSON.stringify({ code, height }),
},
],
content: [{ type: "text", text: JSON.stringify(data) }],
structuredContent: data,
};
},
);
Expand Down
19 changes: 10 additions & 9 deletions examples/video-resource-server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,20 +129,21 @@ ${Object.entries(VIDEO_LIBRARY)
`Video ID to play. Available: ${Object.keys(VIDEO_LIBRARY).join(", ")}`,
),
},
outputSchema: z.object({
videoUri: z.string(),
description: z.string(),
}),
_meta: { [RESOURCE_URI_META_KEY]: RESOURCE_URI },
},
async ({ videoId }): Promise<CallToolResult> => {
const video = VIDEO_LIBRARY[videoId];
const data = {
videoUri: `videos://${videoId}`,
description: video.description,
};
return {
content: [
{
type: "text",
text: JSON.stringify({
videoUri: `videos://${videoId}`,
description: video.description,
}),
},
],
content: [{ type: "text", text: JSON.stringify(data) }],
structuredContent: data,
};
},
);
Expand Down
16 changes: 4 additions & 12 deletions examples/video-resource-server/src/mcp-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,10 @@ const videoInfoEl = document.getElementById("video-info")!;
function parseToolResult(
result: CallToolResult,
): { videoUri: string; description: string } | null {
const text = result.content
?.filter((c): c is { type: "text"; text: string } => c.type === "text")
.map((c) => c.text)
.join("");

if (!text) return null;

try {
return JSON.parse(text) as { videoUri: string; description: string };
} catch {
return null;
}
return result.structuredContent as {
videoUri: string;
description: string;
} | null;
}

// Show states
Expand Down
Loading
Loading