* build app-with-deps
* make registerAppTool more interchangeable w/ server.registerTool
* feat: use structuredContent in all example servers
All servers now return structuredContent alongside JSON text content.
All mcp-apps now read structuredContent directly (no JSON.parse fallback).
Updated servers:
- basic-server-vanillajs
- budget-allocator-server
- cohort-heatmap-server
- customer-segmentation-server
- integration-server
- scenario-modeler-server
- system-monitor-server
- threejs-server
- video-resource-server
- wiki-explorer-server
* feat: add outputSchema to all example servers
Define output schemas for structuredContent validation:
- basic-server-vanillajs: { time: string }
- budget-allocator-server: BudgetDataResponseSchema
- cohort-heatmap-server: CohortDataSchema
- customer-segmentation-server: customers/segments schema
- integration-server: { time: string }
- scenario-modeler-server: templates/defaultInputs schema
- system-monitor-server: SystemStatsSchema
- threejs-server: { code, height }
- video-resource-server: { videoUri, description }
- wiki-explorer-server: { page, links, error }
* prettier:fix
* make registerAppTool more interchangeable w/ server.registerTool
* feat: use structuredContent in all example servers
All servers now return structuredContent alongside JSON text content.
All mcp-apps now read structuredContent directly (no JSON.parse fallback).
Updated servers:
- basic-server-vanillajs
- budget-allocator-server
- cohort-heatmap-server
- customer-segmentation-server
- integration-server
- scenario-modeler-server
- system-monitor-server
- threejs-server
- video-resource-server
- wiki-explorer-server
* feat: add outputSchema to all example servers
Define output schemas for structuredContent validation:
- basic-server-vanillajs: { time: string }
- budget-allocator-server: BudgetDataResponseSchema
- cohort-heatmap-server: CohortDataSchema
- customer-segmentation-server: customers/segments schema
- integration-server: { time: string }
- scenario-modeler-server: templates/defaultInputs schema
- system-monitor-server: SystemStatsSchema
- threejs-server: { code, height }
- video-resource-server: { videoUri, description }
- wiki-explorer-server: { page, links, error }
* prettier:fix
* revert: remove unrelated workflow, version, and arch changes
* chore: update package-lock.json
* refactor(basic-server-vanillajs): address PR review comments
- Simplify server.ts to return plain time string in text content (not JSON),
matching the quickstart guide pattern
- Add defensive coding in mcp-app.ts with optional chaining and fallback
to handle potential type mismatches gracefully
* refactor: restore rich text formatting for LLM-readable content
Restore the human-readable text formatting functions that were removed,
providing meaningful summaries in the text content while keeping
structuredContent for programmatic UI consumption:
- budget-allocator-server: formatBudgetSummary with config overview
- cohort-heatmap-server: formatCohortSummary with retention stats
- scenario-modeler-server: formatScenarioSummary with financial metrics
162 lines
4.2 KiB
TypeScript
162 lines
4.2 KiB
TypeScript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
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 {
|
|
RESOURCE_MIME_TYPE,
|
|
RESOURCE_URI_META_KEY,
|
|
registerAppResource,
|
|
registerAppTool,
|
|
} from "@modelcontextprotocol/ext-apps/server";
|
|
import { startServer } from "./server-utils.js";
|
|
import {
|
|
generateCustomers,
|
|
generateSegmentSummaries,
|
|
} from "./src/data-generator.ts";
|
|
import { SEGMENTS, type Customer, type SegmentSummary } from "./src/types.ts";
|
|
|
|
const DIST_DIR = path.join(import.meta.dirname, "dist");
|
|
|
|
// Schemas - types are derived from these using z.infer
|
|
const GetCustomerDataInputSchema = z.object({
|
|
segment: z
|
|
.enum(["All", ...SEGMENTS])
|
|
.optional()
|
|
.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;
|
|
|
|
function getCustomerData(segmentFilter?: string): {
|
|
customers: Customer[];
|
|
segments: SegmentSummary[];
|
|
} {
|
|
// Generate data on first call
|
|
if (!cachedCustomers) {
|
|
cachedCustomers = generateCustomers(250);
|
|
cachedSegments = generateSegmentSummaries(cachedCustomers);
|
|
}
|
|
|
|
// Filter by segment if specified
|
|
let customers = cachedCustomers;
|
|
if (segmentFilter && segmentFilter !== "All") {
|
|
customers = cachedCustomers.filter((c) => c.segment === segmentFilter);
|
|
}
|
|
|
|
return {
|
|
customers,
|
|
segments: cachedSegments!,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Creates a new MCP server instance with tools and resources registered.
|
|
* Each HTTP session needs its own server instance because McpServer only supports one transport.
|
|
*/
|
|
export function createServer(): McpServer {
|
|
const server = new McpServer({
|
|
name: "Customer Segmentation Server",
|
|
version: "1.0.0",
|
|
});
|
|
|
|
// Register the get-customer-data tool and its associated UI resource
|
|
{
|
|
const resourceUri = "ui://customer-segmentation/mcp-app.html";
|
|
|
|
registerAppTool(
|
|
server,
|
|
"get-customer-data",
|
|
{
|
|
title: "Get Customer Data",
|
|
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,
|
|
};
|
|
},
|
|
);
|
|
|
|
registerAppResource(
|
|
server,
|
|
resourceUri,
|
|
resourceUri,
|
|
{
|
|
mimeType: RESOURCE_MIME_TYPE,
|
|
description: "Customer Segmentation Explorer UI",
|
|
},
|
|
async (): Promise<ReadResourceResult> => {
|
|
const html = await fs.readFile(
|
|
path.join(DIST_DIR, "mcp-app.html"),
|
|
"utf-8",
|
|
);
|
|
|
|
return {
|
|
contents: [
|
|
{
|
|
uri: resourceUri,
|
|
mimeType: RESOURCE_MIME_TYPE,
|
|
text: html,
|
|
},
|
|
],
|
|
};
|
|
},
|
|
);
|
|
}
|
|
|
|
return server;
|
|
}
|
|
|
|
async function main() {
|
|
if (process.argv.includes("--stdio")) {
|
|
await createServer().connect(new StdioServerTransport());
|
|
} else {
|
|
const port = parseInt(process.env.PORT ?? "3105", 10);
|
|
await startServer(createServer, {
|
|
port,
|
|
name: "Customer Segmentation Server",
|
|
});
|
|
}
|
|
}
|
|
|
|
main().catch((e) => {
|
|
console.error(e);
|
|
process.exit(1);
|
|
});
|