Skip to content
Draft
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
6 changes: 2 additions & 4 deletions examples/basic-server-react/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/s
import fs from "node:fs/promises";
import path from "node:path";
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "@modelcontextprotocol/ext-apps/server";
import { startServer } from "../shared/server-utils.js";
import { makeToolResult, startServer } from "../shared/server-utils.js";

const DIST_DIR = path.join(import.meta.dirname, "dist");
const RESOURCE_URI = "ui://get-time/mcp-app.html";
Expand Down Expand Up @@ -32,9 +32,7 @@ function createServer(): McpServer {
},
async (): Promise<CallToolResult> => {
const time = new Date().toISOString();
return {
content: [{ type: "text", text: JSON.stringify({ time }) }],
};
return makeToolResult({ time });
},
);

Expand Down
6 changes: 2 additions & 4 deletions examples/basic-server-vanillajs/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/s
import fs from "node:fs/promises";
import path from "node:path";
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "@modelcontextprotocol/ext-apps/server";
import { startServer } from "../shared/server-utils.js";
import { makeToolResult, startServer } from "../shared/server-utils.js";

const DIST_DIR = path.join(import.meta.dirname, "dist");
const RESOURCE_URI = "ui://get-time/mcp-app.html";
Expand Down Expand Up @@ -32,9 +32,7 @@ function createServer(): McpServer {
},
async (): Promise<CallToolResult> => {
const time = new Date().toISOString();
return {
content: [{ type: "text", text: JSON.stringify({ time }) }],
};
return makeToolResult({ time });
},
);

Expand Down
23 changes: 17 additions & 6 deletions examples/basic-server-vanillajs/src/mcp-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,23 @@ const log = {


function extractTime(result: CallToolResult): string {
const text = result.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 };
return time;
// Prefer structuredContent (STRUCTURED_CONTENT_ONLY mode), fall back to parsing text
let data: { time: string };
if (result.structuredContent) {
data = result.structuredContent as { time: string };
} else {
const text = result.content!
.filter((c): c is { type: "text"; text: string } => c.type === "text")
.map((c) => c.text)
.join("");
try {
data = JSON.parse(text) as { time: string };
} catch (e) {
log.error("Failed to parse tool result:", text, e);
return "[PARSE ERROR]";
}
}
return data.time;
}


Expand Down
13 changes: 4 additions & 9 deletions examples/budget-allocator-server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ 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 "../shared/server-utils.js";
import { makeToolResult, startServer } from "../shared/server-utils.js";

const DIST_DIR = path.join(import.meta.dirname, "dist");

Expand Down Expand Up @@ -270,14 +272,7 @@ function createServer(): McpServer {
},
};

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

Expand Down
34 changes: 27 additions & 7 deletions examples/budget-allocator-server/src/mcp-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -606,13 +606,33 @@ 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;

if (result.isError) {
log.error("Tool returned error:", result);
return;
}

// Prefer structuredContent (STRUCTURED_CONTENT_ONLY mode), fall back to parsing text
let data: BudgetDataResponse | undefined;
if (result.structuredContent) {
data = result.structuredContent as BudgetDataResponse;
} else {
const text = result
.content!.filter(
(c): c is { type: "text"; text: string } => c.type === "text",
)
.map((c) => c.text)
.join("");
if (text) {
try {
data = JSON.parse(text) as BudgetDataResponse;
} catch (e) {
log.error("Failed to parse tool result:", text, e);
return;
}
}
}

if (data?.config && data?.analytics) {
initializeUI(data.config, data.analytics);
}
Expand Down
8 changes: 4 additions & 4 deletions examples/cohort-heatmap-server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ 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 "../shared/server-utils.js";
import { makeToolResult, startServer } from "../shared/server-utils.js";

const DIST_DIR = path.join(import.meta.dirname, "dist");

Expand Down Expand Up @@ -177,9 +179,7 @@ function createServer(): McpServer {
maxPeriods,
);

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

Expand Down
8 changes: 4 additions & 4 deletions examples/customer-segmentation-server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ 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 "../shared/server-utils.js";
import { makeToolResult, startServer } from "../shared/server-utils.js";
import {
generateCustomers,
generateSegmentSummaries,
Expand Down Expand Up @@ -81,9 +83,7 @@ function createServer(): McpServer {
async ({ segment }): Promise<CallToolResult> => {
const data = getCustomerData(segment);

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

Expand Down
28 changes: 18 additions & 10 deletions examples/customer-segmentation-server/src/mcp-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,16 +375,24 @@ async function fetchData(): Promise<void> {
arguments: {},
});

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[];
segments: SegmentSummary[];
};
// Prefer structuredContent (STRUCTURED_CONTENT_ONLY mode), fall back to parsing text
let data: { customers: Customer[]; segments: SegmentSummary[] };
if (result.structuredContent) {
data = result.structuredContent as typeof data;
} else {
const text = result
.content!.filter(
(c): c is { type: "text"; text: string } => c.type === "text",
)
.map((c) => c.text)
.join("");
try {
data = JSON.parse(text);
} catch (e) {
log.error("Failed to parse tool result:", text, e);
return;
}
}

state.customers = data.customers;
state.segments = data.segments;
Expand Down
23 changes: 9 additions & 14 deletions examples/scenario-modeler-server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ 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 "../shared/server-utils.js";
import { makeToolResult, startServer } from "../shared/server-utils.js";

const DIST_DIR = path.join(import.meta.dirname, "dist");

Expand Down Expand Up @@ -276,19 +278,12 @@ function createServer(): McpServer {
? calculateScenario(args.customInputs)
: undefined;

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

Expand Down
27 changes: 27 additions & 0 deletions examples/shared/server-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
Expand All @@ -15,6 +16,32 @@ import cors from "cors";
import { randomUUID } from "node:crypto";
import type { Request, Response } from "express";

/**
* When true, tool results use structuredContent field instead of JSON-stringified text.
* Set via STRUCTURED_CONTENT_ONLY=true environment variable.
*/
export const STRUCTURED_CONTENT_ONLY =
process.env.STRUCTURED_CONTENT_ONLY === "true";

/**
* Helper to create a tool result that optionally uses structuredContent.
* When STRUCTURED_CONTENT_ONLY is true, returns data in structuredContent field.
* Otherwise returns JSON-stringified data in content[0].text (legacy format).
*/
export function makeToolResult(data: Record<string, unknown>): CallToolResult {
if (STRUCTURED_CONTENT_ONLY) {
console.log("[makeToolResult] STRUCTURED_CONTENT_ONLY mode:", { structuredContent: data });
return {
content: [],
structuredContent: data,
};
}
console.log("[makeToolResult] Legacy mode:", { text: JSON.stringify(data).slice(0, 100) + "..." });
return {
content: [{ type: "text", text: JSON.stringify(data) }],
};
}

export interface ServerOptions {
/** Port to listen on (required). */
port: number;
Expand Down
8 changes: 4 additions & 4 deletions examples/system-monitor-server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import path from "node:path";
import si from "systeminformation";
import { z } from "zod";
import {
registerAppTool,
registerAppResource,
RESOURCE_MIME_TYPE,
RESOURCE_URI_META_KEY,
} from "@modelcontextprotocol/ext-apps/server";
import { startServer } from "../shared/server-utils.js";
import { makeToolResult, startServer } from "../shared/server-utils.js";

// Schemas - types are derived from these using z.infer
const CpuCoreSchema = z.object({
Expand Down Expand Up @@ -147,9 +149,7 @@ function createServer(): McpServer {
timestamp: new Date().toISOString(),
};

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

Expand Down
25 changes: 18 additions & 7 deletions examples/system-monitor-server/src/mcp-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,13 +264,24 @@ 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;
// Prefer structuredContent (STRUCTURED_CONTENT_ONLY mode), fall back to parsing text
let stats: SystemStats;
if (result.structuredContent) {
stats = result.structuredContent as SystemStats;
} else {
const text = result
.content!.filter(
(c): c is { type: "text"; text: string } => c.type === "text",
)
.map((c) => c.text)
.join("");
try {
stats = JSON.parse(text);
} catch (e) {
log.error("Failed to parse tool result:", text, e);
return;
}
}

// Initialize chart on first data if needed
if (!state.chart && stats.cpu.count > 0) {
Expand Down
Loading
Loading