ext-apps/examples/customer-segmentation-server/src/data-generator.ts
Jerome 65a62a9fae
feat: compile example servers to JavaScript for npm consumption (#259)
* feat: compile example servers to JavaScript for npm consumption

Add TypeScript compilation for server code so the example packages can be
imported and used by consumers without requiring TypeScript compilation.

Changes:
- Add tsconfig.server.json to each example for compiling server code
- Update package.json exports to point to compiled dist/server/server.js
- Add types field pointing to dist/server/server.d.ts
- Update build scripts to include server compilation step
- Fix .ts import extensions to .js in customer-segmentation-server

This enables importing the createServer function from published packages:
  import { createServer } from '@modelcontextprotocol/server-basic-react';

* fix: add type assertion for Nominatim API response

* chore: regenerate schemas

* refactor: split server examples into library and binary entry points

- server.ts: exports createServer() only (library entry point)
- main.ts: entry point with main() that runs the server (binary)
- package.json: adds bin field for npx support, uses inline bun build
- tsconfig.server.json: emitDeclarationOnly, outputs to dist/

This allows:
1. Importing the server as a library: import { createServer } from '@modelcontextprotocol/server-basic-react'
2. Running with npx: npx @modelcontextprotocol/server-basic-react [--stdio]

Added migration script at scripts/migrate-server-examples.ts

* refactor: migrate remaining server examples to library/binary structure

Apply the same refactoring to all 13 remaining server examples:
- scenario-modeler-server, shadertoy-server, map-server
- cohort-heatmap-server, customer-segmentation-server, video-resource-server
- system-monitor-server, budget-allocator-server, sheet-music-server
- threejs-server, integration-server, transcript-server, wiki-explorer-server

Each now has:
- server.ts: exports createServer() (library)
- main.ts: runs the server (binary with shebang)
- bin field in package.json for npx support

* chore: remove unused server-utils.ts from video-resource-server/src

* update

* fix(build): make Windows-compatible by removing chmod and fixing bun externals

- Remove 'chmod +x dist/index.js' which doesn't exist on Windows
- Add '--external ./server.js' to prevent bun from treating the import as a second entry point

* fix(build): use .ts import for bun compatibility

- Change import from './server.js' to './server.ts' in main.ts files
- Remove --external flag since bun should inline everything
- Fixes Windows build where bun couldn't resolve .js to .ts properly

* fix(build): use --external for Windows bun compatibility

Bun on Windows incorrectly treats imported .ts files as entry points.
Use --external './server.js' to work around this, creating a runtime
dependency between index.js and server.js.

* style: fix prettier formatting in pdf-server/main.ts

* fix(build): remove quotes from --external for Windows cmd.exe compatibility

* fix(build): use glob pattern for --external to work on all platforms

The literal path --external ./server.js was not matching the import.
Using glob pattern --external "./server*" correctly externalizes the server module.

* fix(build): use */server.js pattern for --external

The "./server*" pattern was not matching on Windows.
Using "*/server.js" matches any path ending in /server.js which works cross-platform.

* fix(build): use *server.js pattern without slash for Windows compatibility

* update package-lock

* chore: regenerate package-lock.json for cross-platform compatibility

* fix(build): use ./server.js instead of glob pattern for Windows compatibility

* fix(build): use double quotes for banner to fix Windows cmd.exe compatibility

---------

Co-authored-by: Olivier Chafik <ochafik@anthropic.com>
2026-01-14 23:46:17 +00:00

231 lines
5.8 KiB
TypeScript

import type { Customer, SegmentSummary, SegmentName } from "./types.js";
import { SEGMENT_COLORS, SEGMENTS } from "./types.js";
// Company name generation
const PREFIXES = [
"Apex",
"Nova",
"Prime",
"Vertex",
"Atlas",
"Quantum",
"Summit",
"Nexus",
"Titan",
"Pinnacle",
"Zenith",
"Vanguard",
"Horizon",
"Stellar",
"Onyx",
"Cobalt",
"Vector",
"Pulse",
"Forge",
"Spark",
];
const CORES = [
"Tech",
"Data",
"Cloud",
"Logic",
"Sync",
"Flow",
"Core",
"Net",
"Soft",
"Wave",
"Link",
"Mind",
"Byte",
"Grid",
"Hub",
];
const SUFFIXES = [
"Corp",
"Inc",
"Solutions",
"Systems",
"Labs",
"Group",
"Industries",
"Dynamics",
"Partners",
"Ventures",
"Global",
"Digital",
];
// Cluster centers for each segment
interface ClusterCenter {
annualRevenue: { min: number; max: number };
employeeCount: { min: number; max: number };
accountAge: { min: number; max: number };
engagementScore: { min: number; max: number };
supportTickets: { min: number; max: number };
nps: { min: number; max: number };
}
const CLUSTER_CENTERS: Record<SegmentName, ClusterCenter> = {
Enterprise: {
annualRevenue: { min: 2_000_000, max: 10_000_000 },
employeeCount: { min: 500, max: 5000 },
accountAge: { min: 60, max: 120 },
engagementScore: { min: 70, max: 95 },
supportTickets: { min: 5, max: 20 },
nps: { min: 40, max: 80 },
},
"Mid-Market": {
annualRevenue: { min: 500_000, max: 2_000_000 },
employeeCount: { min: 100, max: 500 },
accountAge: { min: 36, max: 84 },
engagementScore: { min: 60, max: 85 },
supportTickets: { min: 10, max: 30 },
nps: { min: 20, max: 60 },
},
SMB: {
annualRevenue: { min: 50_000, max: 500_000 },
employeeCount: { min: 10, max: 100 },
accountAge: { min: 12, max: 48 },
engagementScore: { min: 40, max: 70 },
supportTickets: { min: 15, max: 40 },
nps: { min: 0, max: 40 },
},
Startup: {
annualRevenue: { min: 10_000, max: 200_000 },
employeeCount: { min: 1, max: 50 },
accountAge: { min: 1, max: 24 },
engagementScore: { min: 50, max: 90 },
supportTickets: { min: 5, max: 25 },
nps: { min: 10, max: 70 },
},
};
// Segment distribution weights
const SEGMENT_WEIGHTS: Record<SegmentName, number> = {
Enterprise: 0.15,
"Mid-Market": 0.25,
SMB: 0.35,
Startup: 0.25,
};
// Box-Muller transform for Gaussian random numbers
function gaussianRandom(mean: number, stdDev: number): number {
const u1 = Math.random();
const u2 = Math.random();
const z0 = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2);
return z0 * stdDev + mean;
}
// Generate a value within range with Gaussian distribution centered in range
function generateClusteredValue(min: number, max: number): number {
const mean = (min + max) / 2;
const stdDev = (max - min) / 4; // 95% of values within range
const value = gaussianRandom(mean, stdDev);
return Math.max(min * 0.8, Math.min(max * 1.2, value)); // Allow slight overflow
}
// Generate unique company name
function generateCompanyName(usedNames: Set<string>): string {
let attempts = 0;
while (attempts < 100) {
const prefix = PREFIXES[Math.floor(Math.random() * PREFIXES.length)];
const core = CORES[Math.floor(Math.random() * CORES.length)];
const suffix = SUFFIXES[Math.floor(Math.random() * SUFFIXES.length)];
const name = `${prefix} ${core} ${suffix}`;
if (!usedNames.has(name)) {
usedNames.add(name);
return name;
}
attempts++;
}
// Fallback: add a number
const prefix = PREFIXES[Math.floor(Math.random() * PREFIXES.length)];
const core = CORES[Math.floor(Math.random() * CORES.length)];
const num = Math.floor(Math.random() * 1000);
return `${prefix} ${core} ${num}`;
}
// Select segment based on weights
function selectSegment(): SegmentName {
const rand = Math.random();
let cumulative = 0;
for (const segment of SEGMENTS) {
cumulative += SEGMENT_WEIGHTS[segment];
if (rand < cumulative) {
return segment;
}
}
return "SMB";
}
// Generate a single customer
function generateCustomer(id: number, usedNames: Set<string>): Customer {
const segment = selectSegment();
const center = CLUSTER_CENTERS[segment];
return {
id: `cust-${id.toString().padStart(4, "0")}`,
name: generateCompanyName(usedNames),
segment,
annualRevenue: Math.round(
generateClusteredValue(
center.annualRevenue.min,
center.annualRevenue.max,
),
),
employeeCount: Math.round(
generateClusteredValue(
center.employeeCount.min,
center.employeeCount.max,
),
),
accountAge: Math.round(
generateClusteredValue(center.accountAge.min, center.accountAge.max),
),
engagementScore: Math.round(
generateClusteredValue(
center.engagementScore.min,
center.engagementScore.max,
),
),
supportTickets: Math.round(
generateClusteredValue(
center.supportTickets.min,
center.supportTickets.max,
),
),
nps: Math.round(generateClusteredValue(center.nps.min, center.nps.max)),
};
}
// Generate all customers
export function generateCustomers(count: number = 250): Customer[] {
const usedNames = new Set<string>();
const customers: Customer[] = [];
for (let i = 0; i < count; i++) {
customers.push(generateCustomer(i + 1, usedNames));
}
return customers;
}
// Generate segment summaries from customers
export function generateSegmentSummaries(
customers: Customer[],
): SegmentSummary[] {
const counts = new Map<string, number>();
for (const customer of customers) {
counts.set(customer.segment, (counts.get(customer.segment) || 0) + 1);
}
return SEGMENTS.map((segment) => ({
name: segment,
count: counts.get(segment) || 0,
color: SEGMENT_COLORS[segment],
}));
}