* 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>
231 lines
5.8 KiB
TypeScript
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],
|
|
}));
|
|
}
|