Skip to content

Commit 98d8fcd

Browse files
Add customer-segmentation-server example
Demo MCP App for customer data visualization featuring: - Interactive bubble chart (Chart.js) with configurable X/Y axes - 250 customers across 4 segments (Enterprise, Mid-Market, SMB, Startup) - 6 metrics: Revenue, Employees, Account Age, Engagement, Tickets, NPS - Optional bubble sizing by a third metric - Clickable legend for segment filtering - Detail panel on hover/click - Light/dark theme support 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent fa2d413 commit 98d8fcd

File tree

12 files changed

+1300
-109
lines changed

12 files changed

+1300
-109
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Example: Customer Segmentation Explorer
2+
3+
A demo MCP App that displays customer data as an interactive scatter/bubble chart with segment-based clustering. Users can explore different metrics, filter by segment, and click to see detailed customer information.
4+
5+
## Features
6+
7+
- **Interactive Scatter Plot**: Bubble chart visualization using Chart.js with configurable X/Y axes
8+
- **Segment Clustering**: 250 customers grouped into 4 segments (Enterprise, Mid-Market, SMB, Startup)
9+
- **Axis Selection**: Choose from 6 metrics for each axis (Revenue, Employees, Account Age, Engagement, Tickets, NPS)
10+
- **Size Mapping**: Optional bubble sizing by a third metric for additional data dimension
11+
- **Legend Filtering**: Click segment pills to show/hide customer groups
12+
- **Detail Panel**: Hover or click customers to see name, segment, revenue, engagement, and NPS
13+
- **Theme Support**: Adapts to light/dark mode preferences
14+
15+
## Running
16+
17+
1. Install dependencies:
18+
19+
```bash
20+
npm install
21+
```
22+
23+
2. Build and start the server:
24+
25+
```bash
26+
npm start
27+
```
28+
29+
The server will listen on `http://localhost:3001/mcp`.
30+
31+
3. View using the [`basic-host`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host) example or another MCP Apps-compatible host.
32+
33+
## Architecture
34+
35+
### Server (`server.ts`)
36+
37+
Exposes a single `get-customer-data` tool that returns:
38+
39+
- Array of 250 generated customer records with segment assignments
40+
- Segment summary with counts and colors for each group
41+
- Optional segment filter parameter
42+
43+
The tool is linked to a UI resource via `_meta[RESOURCE_URI_META_KEY]`.
44+
45+
### App (`src/mcp-app.ts`)
46+
47+
- Uses Chart.js bubble chart for the visualization
48+
- Fetches data once on connection
49+
- Dropdown controls update chart axes and bubble sizing
50+
- Custom legend with clickable segment toggles
51+
- Detail panel updates on hover/click interactions
52+
53+
### Data Generator (`src/data-generator.ts`)
54+
55+
- Generates realistic customer data with Gaussian clustering around segment centers
56+
- Each segment has characteristic ranges for revenue, employees, engagement, etc.
57+
- Company names generated from word-list combinations (e.g., "Apex Data Corp")
58+
- Data cached in memory for session consistency
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Customer Segmentation Explorer</title>
7+
</head>
8+
<body>
9+
<main class="main">
10+
<header class="header">
11+
<h1 class="title">Customer Segmentation</h1>
12+
</header>
13+
14+
<section class="controls-section">
15+
<label class="select-label">
16+
X:
17+
<select id="x-axis" class="select">
18+
<option value="annualRevenue">Annual Revenue</option>
19+
<option value="employeeCount">Employees</option>
20+
<option value="accountAge">Account Age (mo)</option>
21+
<option value="engagementScore">Engagement</option>
22+
<option value="supportTickets">Support Tickets</option>
23+
<option value="nps">NPS</option>
24+
</select>
25+
</label>
26+
<label class="select-label">
27+
Y:
28+
<select id="y-axis" class="select">
29+
<option value="annualRevenue">Annual Revenue</option>
30+
<option value="employeeCount">Employees</option>
31+
<option value="accountAge">Account Age (mo)</option>
32+
<option value="engagementScore" selected>Engagement</option>
33+
<option value="supportTickets">Support Tickets</option>
34+
<option value="nps">NPS</option>
35+
</select>
36+
</label>
37+
<label class="select-label">
38+
Size:
39+
<select id="size-metric" class="select">
40+
<option value="off">Off</option>
41+
<option value="annualRevenue">Annual Revenue</option>
42+
<option value="employeeCount">Employees</option>
43+
<option value="accountAge">Account Age (mo)</option>
44+
<option value="engagementScore">Engagement</option>
45+
<option value="supportTickets">Support Tickets</option>
46+
<option value="nps">NPS</option>
47+
</select>
48+
</label>
49+
</section>
50+
51+
<section class="chart-section">
52+
<div class="chart-container">
53+
<canvas id="scatter-chart"></canvas>
54+
</div>
55+
</section>
56+
57+
<section class="legend-section">
58+
<div id="legend" class="legend"></div>
59+
</section>
60+
61+
<section class="detail-section">
62+
<div id="detail-panel" class="detail-panel">
63+
<span class="detail-placeholder">Hover over a point to see details</span>
64+
</div>
65+
</section>
66+
</main>
67+
68+
<script type="module" src="./src/mcp-app.ts"></script>
69+
</body>
70+
</html>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "customer-segmentation-server",
3+
"version": "1.0.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"build": "INPUT=mcp-app.html vite build",
8+
"watch": "INPUT=mcp-app.html vite build --watch",
9+
"serve": "bun server.ts",
10+
"start": "NODE_ENV=development npm run build && npm run serve",
11+
"dev": "NODE_ENV=development concurrently 'npm run watch' 'npm run serve'"
12+
},
13+
"dependencies": {
14+
"@modelcontextprotocol/ext-apps": "../..",
15+
"@modelcontextprotocol/sdk": "^1.22.0",
16+
"chart.js": "^4.4.0",
17+
"zod": "^3.25.0"
18+
},
19+
"devDependencies": {
20+
"@types/cors": "^2.8.19",
21+
"@types/express": "^5.0.0",
22+
"@types/node": "^22.0.0",
23+
"concurrently": "^9.2.1",
24+
"cors": "^2.8.5",
25+
"express": "^5.1.0",
26+
"typescript": "^5.9.3",
27+
"vite": "^6.0.0",
28+
"vite-plugin-singlefile": "^2.3.0"
29+
}
30+
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
3+
import type {
4+
CallToolResult,
5+
ReadResourceResult,
6+
} from "@modelcontextprotocol/sdk/types.js";
7+
import cors from "cors";
8+
import express, { type Request, type Response } from "express";
9+
import fs from "node:fs/promises";
10+
import path from "node:path";
11+
import { z } from "zod";
12+
import { RESOURCE_URI_META_KEY } from "../../dist/src/app";
13+
import {
14+
generateCustomers,
15+
generateSegmentSummaries,
16+
} from "./src/data-generator.ts";
17+
import { SEGMENTS, type Customer, type SegmentSummary } from "./src/types.ts";
18+
19+
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3001;
20+
const DIST_DIR = path.join(import.meta.dirname, "dist");
21+
22+
// Schemas - types are derived from these using z.infer
23+
const GetCustomerDataInputSchema = z.object({
24+
segment: z
25+
.enum(["All", ...SEGMENTS])
26+
.optional()
27+
.describe("Filter by segment (default: All)"),
28+
});
29+
30+
const CustomerSchema = z.object({
31+
id: z.string(),
32+
name: z.string(),
33+
segment: z.string(),
34+
annualRevenue: z.number(),
35+
employeeCount: z.number(),
36+
accountAge: z.number(),
37+
engagementScore: z.number(),
38+
supportTickets: z.number(),
39+
nps: z.number(),
40+
});
41+
42+
const SegmentSummarySchema = z.object({
43+
name: z.string(),
44+
count: z.number(),
45+
color: z.string(),
46+
});
47+
48+
const GetCustomerDataOutputSchema = z.object({
49+
customers: z.array(CustomerSchema),
50+
segments: z.array(SegmentSummarySchema),
51+
});
52+
53+
// Cache generated data for session consistency
54+
let cachedCustomers: Customer[] | null = null;
55+
let cachedSegments: SegmentSummary[] | null = null;
56+
57+
function getCustomerData(segmentFilter?: string): {
58+
customers: Customer[];
59+
segments: SegmentSummary[];
60+
} {
61+
// Generate data on first call
62+
if (!cachedCustomers) {
63+
cachedCustomers = generateCustomers(250);
64+
cachedSegments = generateSegmentSummaries(cachedCustomers);
65+
}
66+
67+
// Filter by segment if specified
68+
let customers = cachedCustomers;
69+
if (segmentFilter && segmentFilter !== "All") {
70+
customers = cachedCustomers.filter((c) => c.segment === segmentFilter);
71+
}
72+
73+
return {
74+
customers,
75+
segments: cachedSegments!,
76+
};
77+
}
78+
79+
const server = new McpServer({
80+
name: "Customer Segmentation Server",
81+
version: "1.0.0",
82+
});
83+
84+
// Register the get-customer-data tool and its associated UI resource
85+
{
86+
const resourceUri = "ui://customer-segmentation/mcp-app.html";
87+
88+
server.registerTool(
89+
"get-customer-data",
90+
{
91+
title: "Get Customer Data",
92+
description:
93+
"Returns customer data with segment information for visualization. Optionally filter by segment.",
94+
inputSchema: GetCustomerDataInputSchema.shape,
95+
outputSchema: GetCustomerDataOutputSchema.shape,
96+
_meta: { [RESOURCE_URI_META_KEY]: resourceUri },
97+
},
98+
async ({ segment }): Promise<CallToolResult> => {
99+
const data = getCustomerData(segment);
100+
101+
return {
102+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
103+
structuredContent: data,
104+
};
105+
},
106+
);
107+
108+
server.registerResource(
109+
resourceUri,
110+
resourceUri,
111+
{ description: "Customer Segmentation Explorer UI" },
112+
async (): Promise<ReadResourceResult> => {
113+
const html = await fs.readFile(
114+
path.join(DIST_DIR, "mcp-app.html"),
115+
"utf-8",
116+
);
117+
118+
return {
119+
contents: [{ uri: resourceUri, mimeType: "text/html+mcp", text: html }],
120+
};
121+
},
122+
);
123+
}
124+
125+
const app = express();
126+
app.use(cors());
127+
app.use(express.json());
128+
129+
app.post("/mcp", async (req: Request, res: Response) => {
130+
try {
131+
const transport = new StreamableHTTPServerTransport({
132+
sessionIdGenerator: undefined,
133+
enableJsonResponse: true,
134+
});
135+
res.on("close", () => {
136+
transport.close();
137+
});
138+
139+
await server.connect(transport);
140+
141+
await transport.handleRequest(req, res, req.body);
142+
} catch (error) {
143+
console.error("Error handling MCP request:", error);
144+
if (!res.headersSent) {
145+
res.status(500).json({
146+
jsonrpc: "2.0",
147+
error: { code: -32603, message: "Internal server error" },
148+
id: null,
149+
});
150+
}
151+
}
152+
});
153+
154+
const httpServer = app.listen(PORT, () => {
155+
console.log(
156+
`Customer Segmentation Server listening on http://localhost:${PORT}/mcp`,
157+
);
158+
});
159+
160+
function shutdown() {
161+
console.log("\nShutting down...");
162+
httpServer.close(() => {
163+
console.log("Server closed");
164+
process.exit(0);
165+
});
166+
}
167+
168+
process.on("SIGINT", shutdown);
169+
process.on("SIGTERM", shutdown);

0 commit comments

Comments
 (0)