From 109403c5302598dee9f9cad9663faf529aaa1c9e Mon Sep 17 00:00:00 2001 From: Jd Date: Thu, 8 Jan 2026 00:37:34 -0500 Subject: [PATCH] feat(examples): add D3 graph and Recharts chart server examples Add two new MCP ext-app examples showcasing charting capabilities: - d3-graph-server: Vanilla JS force-directed graph visualization with D3.js. Includes 3 sample graphs (dependencies, org-chart, knowledge) with zoom, pan, drag, and click-to-recenter features. - recharts-chart-server: React-based multi-chart dashboard with Recharts. Supports bar, line, area, and pie charts with 3 datasets (monthly-revenue, quarterly-sales, product-mix). Both examples include E2E tests with golden screenshots. --- examples/d3-graph-server/README.md | 123 +++ examples/d3-graph-server/mcp-app.html | 26 + examples/d3-graph-server/package.json | 32 + examples/d3-graph-server/server.ts | 506 ++++++++++ examples/d3-graph-server/src/global.css | 12 + examples/d3-graph-server/src/mcp-app.css | 126 +++ examples/d3-graph-server/src/mcp-app.ts | 301 ++++++ examples/d3-graph-server/src/server-utils.ts | 110 +++ examples/d3-graph-server/tsconfig.json | 19 + examples/d3-graph-server/vite.config.ts | 24 + examples/recharts-chart-server/README.md | 116 +++ examples/recharts-chart-server/mcp-app.html | 13 + examples/recharts-chart-server/package.json | 36 + examples/recharts-chart-server/server.ts | 268 ++++++ .../src/components/ChartRenderer.tsx | 203 ++++ examples/recharts-chart-server/src/global.css | 15 + .../recharts-chart-server/src/mcp-app.css | 132 +++ .../recharts-chart-server/src/mcp-app.tsx | 196 ++++ .../recharts-chart-server/src/server-utils.ts | 110 +++ examples/recharts-chart-server/tsconfig.json | 20 + examples/recharts-chart-server/vite.config.ts | 25 + package-lock.json | 879 +++++++++++++++++- tests/e2e/servers.spec.ts | 4 + .../servers.spec.ts-snapshots/d3-graph.png | Bin 0 -> 24326 bytes .../recharts-chart.png | Bin 0 -> 31191 bytes 25 files changed, 3263 insertions(+), 33 deletions(-) create mode 100644 examples/d3-graph-server/README.md create mode 100644 examples/d3-graph-server/mcp-app.html create mode 100644 examples/d3-graph-server/package.json create mode 100644 examples/d3-graph-server/server.ts create mode 100644 examples/d3-graph-server/src/global.css create mode 100644 examples/d3-graph-server/src/mcp-app.css create mode 100644 examples/d3-graph-server/src/mcp-app.ts create mode 100644 examples/d3-graph-server/src/server-utils.ts create mode 100644 examples/d3-graph-server/tsconfig.json create mode 100644 examples/d3-graph-server/vite.config.ts create mode 100644 examples/recharts-chart-server/README.md create mode 100644 examples/recharts-chart-server/mcp-app.html create mode 100644 examples/recharts-chart-server/package.json create mode 100644 examples/recharts-chart-server/server.ts create mode 100644 examples/recharts-chart-server/src/components/ChartRenderer.tsx create mode 100644 examples/recharts-chart-server/src/global.css create mode 100644 examples/recharts-chart-server/src/mcp-app.css create mode 100644 examples/recharts-chart-server/src/mcp-app.tsx create mode 100644 examples/recharts-chart-server/src/server-utils.ts create mode 100644 examples/recharts-chart-server/tsconfig.json create mode 100644 examples/recharts-chart-server/vite.config.ts create mode 100644 tests/e2e/servers.spec.ts-snapshots/d3-graph.png create mode 100644 tests/e2e/servers.spec.ts-snapshots/recharts-chart.png diff --git a/examples/d3-graph-server/README.md b/examples/d3-graph-server/README.md new file mode 100644 index 00000000..2943d609 --- /dev/null +++ b/examples/d3-graph-server/README.md @@ -0,0 +1,123 @@ +# Example: D3 Graph Server + +Interactive force-directed graph visualization using D3.js. Explore entity relationships like package dependencies, org charts, or knowledge graphs with zoom, pan, and node interaction. + +## Features + +- **Force-directed layout**: Physics-based graph simulation with D3.js +- **Multiple graph datasets**: Package dependencies, org chart, and AI/ML knowledge graph +- **Interactive nodes**: Drag to reposition, click to recenter view +- **Zoom and pan**: Scroll to zoom, drag background to pan +- **Tooltips**: Hover over nodes to see descriptions +- **Node filtering**: Center on any node with configurable depth + +## Running + +1. Install dependencies: + + ```bash + npm install + ``` + +2. Build and start the server: + + ```bash + npm run start:http # for Streamable HTTP transport + # OR + npm run start:stdio # for stdio transport + ``` + +3. View using the [`basic-host`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host) example or another MCP Apps-compatible host. + +### Tool Input Examples + +**Default (package dependencies graph):** + +```json +{} +``` + +**Package dependencies - centered on React:** + +```json +{ + "graphId": "dependencies", + "centerNode": "react", + "depth": 2 +} +``` + +**Package dependencies - centered on D3:** + +```json +{ + "graphId": "dependencies", + "centerNode": "d3", + "depth": 3 +} +``` + +**Organization chart:** + +```json +{ + "graphId": "org-chart" +} +``` + +**Org chart - centered on VP of Engineering:** + +```json +{ + "graphId": "org-chart", + "centerNode": "vp-eng", + "depth": 2 +} +``` + +**AI/ML knowledge graph:** + +```json +{ + "graphId": "knowledge" +} +``` + +**Knowledge graph - centered on transformers:** + +```json +{ + "graphId": "knowledge", + "centerNode": "transformers", + "depth": 2 +} +``` + +**Knowledge graph - centered on PyTorch:** + +```json +{ + "graphId": "knowledge", + "centerNode": "pytorch", + "depth": 3 +} +``` + +## Architecture + +### Server (`server.ts`) + +MCP server with sample graph datasets representing different relationship types. + +Exposes one tool: + +- `get-graph-data` - Returns nodes and links for force-directed visualization + +### App (`src/mcp-app.ts`) + +Vanilla TypeScript app using D3.js that: + +- Receives graph data via the MCP App SDK +- Renders an interactive force-directed graph with `d3.forceSimulation()` +- Supports zoom/pan via `d3.zoom()` +- Enables node dragging and click-to-recenter diff --git a/examples/d3-graph-server/mcp-app.html b/examples/d3-graph-server/mcp-app.html new file mode 100644 index 00000000..378eb1d9 --- /dev/null +++ b/examples/d3-graph-server/mcp-app.html @@ -0,0 +1,26 @@ + + + + + + + D3 Graph Explorer + + +
+
+ + +
+
+ +
+
+
+ + + diff --git a/examples/d3-graph-server/package.json b/examples/d3-graph-server/package.json new file mode 100644 index 00000000..5dddec32 --- /dev/null +++ b/examples/d3-graph-server/package.json @@ -0,0 +1,32 @@ +{ + "name": "d3-graph-server", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build", + "watch": "cross-env INPUT=mcp-app.html vite build --watch", + "serve": "bun server.ts", + "start": "cross-env NODE_ENV=development npm run build && npm run serve", + "dev": "cross-env NODE_ENV=development concurrently 'npm run watch' 'npm run serve'" + }, + "dependencies": { + "@modelcontextprotocol/ext-apps": "../..", + "@modelcontextprotocol/sdk": "^1.24.0", + "d3": "^7.9.0", + "zod": "^4.1.13" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/d3": "^7.4.3", + "@types/express": "^5.0.0", + "@types/node": "^22.0.0", + "concurrently": "^9.2.1", + "cors": "^2.8.5", + "cross-env": "^7.0.3", + "express": "^5.1.0", + "typescript": "^5.9.3", + "vite": "^6.0.0", + "vite-plugin-singlefile": "^2.3.0" + } +} diff --git a/examples/d3-graph-server/server.ts b/examples/d3-graph-server/server.ts new file mode 100644 index 00000000..7fd8853b --- /dev/null +++ b/examples/d3-graph-server/server.ts @@ -0,0 +1,506 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.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 "./src/server-utils.js"; + +const DIST_DIR = path.join(import.meta.dirname, "dist"); + +// ============================================================================ +// Schemas +// ============================================================================ + +const GraphNodeSchema = z.object({ + id: z.string(), + group: z.string(), + size: z.number(), + description: z.string().optional(), +}); + +const GraphLinkSchema = z.object({ + source: z.string(), + target: z.string(), + strength: z.number().min(0).max(1), +}); + +const GraphDataSchema = z.object({ + nodes: z.array(GraphNodeSchema), + links: z.array(GraphLinkSchema), + metadata: z.object({ + title: z.string(), + description: z.string(), + }), +}); + +const GetGraphDataInputSchema = z.object({ + graphId: z + .enum(["dependencies", "org-chart", "knowledge"]) + .optional() + .describe("Which graph dataset to load"), + centerNode: z.string().optional().describe("Node ID to center the view on"), + depth: z + .number() + .min(1) + .max(5) + .optional() + .describe("Depth of nodes to include from center (1-5)"), +}); + +// Types +type GraphData = z.infer; + +// ============================================================================ +// Sample Graph Data +// ============================================================================ + +const SAMPLE_GRAPHS: Record = { + dependencies: { + nodes: [ + { + id: "react", + group: "core", + size: 40, + description: "React library for building UIs", + }, + { + id: "react-dom", + group: "core", + size: 30, + description: "React DOM renderer", + }, + { + id: "recharts", + group: "viz", + size: 25, + description: "Charting library for React", + }, + { + id: "d3", + group: "viz", + size: 35, + description: "Data visualization library", + }, + { + id: "typescript", + group: "tooling", + size: 30, + description: "TypeScript compiler", + }, + { + id: "vite", + group: "tooling", + size: 28, + description: "Next-generation frontend build tool", + }, + { + id: "eslint", + group: "tooling", + size: 22, + description: "JavaScript linter", + }, + { + id: "prettier", + group: "tooling", + size: 20, + description: "Code formatter", + }, + { + id: "zod", + group: "runtime", + size: 18, + description: "TypeScript-first schema validation", + }, + { + id: "express", + group: "runtime", + size: 26, + description: "Web framework for Node.js", + }, + { id: "lodash", group: "util", size: 24, description: "Utility library" }, + { id: "axios", group: "util", size: 20, description: "HTTP client" }, + { + id: "dayjs", + group: "util", + size: 16, + description: "Date utility library", + }, + { + id: "tailwindcss", + group: "styling", + size: 28, + description: "Utility-first CSS framework", + }, + { + id: "postcss", + group: "styling", + size: 18, + description: "CSS transformer", + }, + ], + links: [ + { source: "react-dom", target: "react", strength: 1.0 }, + { source: "recharts", target: "react", strength: 0.9 }, + { source: "recharts", target: "d3", strength: 0.7 }, + { source: "vite", target: "typescript", strength: 0.6 }, + { source: "eslint", target: "typescript", strength: 0.5 }, + { source: "prettier", target: "eslint", strength: 0.4 }, + { source: "axios", target: "lodash", strength: 0.3 }, + { source: "tailwindcss", target: "postcss", strength: 0.8 }, + { source: "zod", target: "typescript", strength: 0.6 }, + { source: "express", target: "zod", strength: 0.4 }, + { source: "vite", target: "postcss", strength: 0.5 }, + { source: "recharts", target: "lodash", strength: 0.3 }, + ], + metadata: { + title: "Package Dependencies", + description: "Common npm package relationships in a modern web project", + }, + }, + "org-chart": { + nodes: [ + { + id: "ceo", + group: "executive", + size: 45, + description: "Chief Executive Officer", + }, + { + id: "cto", + group: "executive", + size: 38, + description: "Chief Technology Officer", + }, + { + id: "cfo", + group: "executive", + size: 38, + description: "Chief Financial Officer", + }, + { + id: "vp-eng", + group: "management", + size: 32, + description: "VP of Engineering", + }, + { + id: "vp-product", + group: "management", + size: 32, + description: "VP of Product", + }, + { + id: "vp-sales", + group: "management", + size: 32, + description: "VP of Sales", + }, + { + id: "eng-lead-1", + group: "lead", + size: 26, + description: "Engineering Lead - Platform", + }, + { + id: "eng-lead-2", + group: "lead", + size: 26, + description: "Engineering Lead - Frontend", + }, + { + id: "pm-1", + group: "lead", + size: 24, + description: "Product Manager - Core", + }, + { + id: "pm-2", + group: "lead", + size: 24, + description: "Product Manager - Growth", + }, + { id: "dev-1", group: "ic", size: 18, description: "Senior Developer" }, + { id: "dev-2", group: "ic", size: 18, description: "Developer" }, + { id: "dev-3", group: "ic", size: 18, description: "Developer" }, + { id: "designer", group: "ic", size: 20, description: "UX Designer" }, + ], + links: [ + { source: "cto", target: "ceo", strength: 1.0 }, + { source: "cfo", target: "ceo", strength: 1.0 }, + { source: "vp-eng", target: "cto", strength: 0.9 }, + { source: "vp-product", target: "cto", strength: 0.9 }, + { source: "vp-sales", target: "cfo", strength: 0.8 }, + { source: "eng-lead-1", target: "vp-eng", strength: 0.8 }, + { source: "eng-lead-2", target: "vp-eng", strength: 0.8 }, + { source: "pm-1", target: "vp-product", strength: 0.8 }, + { source: "pm-2", target: "vp-product", strength: 0.8 }, + { source: "dev-1", target: "eng-lead-1", strength: 0.7 }, + { source: "dev-2", target: "eng-lead-2", strength: 0.7 }, + { source: "dev-3", target: "eng-lead-2", strength: 0.7 }, + { source: "designer", target: "pm-1", strength: 0.6 }, + { source: "designer", target: "eng-lead-2", strength: 0.4 }, + ], + metadata: { + title: "Organization Chart", + description: + "Company organizational structure and reporting relationships", + }, + }, + knowledge: { + nodes: [ + { id: "ml", group: "ai", size: 40, description: "Machine Learning" }, + { + id: "deep-learning", + group: "ai", + size: 35, + description: "Deep Learning", + }, + { + id: "nlp", + group: "ai", + size: 30, + description: "Natural Language Processing", + }, + { id: "cv", group: "ai", size: 28, description: "Computer Vision" }, + { + id: "transformers", + group: "architecture", + size: 32, + description: "Transformer architecture", + }, + { + id: "cnn", + group: "architecture", + size: 26, + description: "Convolutional Neural Networks", + }, + { + id: "rnn", + group: "architecture", + size: 22, + description: "Recurrent Neural Networks", + }, + { + id: "python", + group: "tools", + size: 38, + description: "Python programming language", + }, + { + id: "pytorch", + group: "tools", + size: 30, + description: "PyTorch framework", + }, + { + id: "tensorflow", + group: "tools", + size: 28, + description: "TensorFlow framework", + }, + { + id: "huggingface", + group: "tools", + size: 26, + description: "Hugging Face library", + }, + { + id: "llm", + group: "application", + size: 35, + description: "Large Language Models", + }, + { id: "chatgpt", group: "application", size: 30, description: "ChatGPT" }, + { + id: "stable-diffusion", + group: "application", + size: 28, + description: "Stable Diffusion", + }, + ], + links: [ + { source: "deep-learning", target: "ml", strength: 1.0 }, + { source: "nlp", target: "ml", strength: 0.9 }, + { source: "cv", target: "ml", strength: 0.9 }, + { source: "transformers", target: "deep-learning", strength: 0.8 }, + { source: "cnn", target: "deep-learning", strength: 0.8 }, + { source: "rnn", target: "deep-learning", strength: 0.7 }, + { source: "transformers", target: "nlp", strength: 0.9 }, + { source: "cnn", target: "cv", strength: 0.9 }, + { source: "pytorch", target: "python", strength: 0.8 }, + { source: "tensorflow", target: "python", strength: 0.8 }, + { source: "huggingface", target: "transformers", strength: 0.9 }, + { source: "huggingface", target: "pytorch", strength: 0.7 }, + { source: "llm", target: "transformers", strength: 1.0 }, + { source: "llm", target: "nlp", strength: 0.9 }, + { source: "chatgpt", target: "llm", strength: 1.0 }, + { source: "stable-diffusion", target: "cv", strength: 0.8 }, + { source: "stable-diffusion", target: "deep-learning", strength: 0.7 }, + ], + metadata: { + title: "AI/ML Knowledge Graph", + description: + "Concepts and relationships in artificial intelligence and machine learning", + }, + }, +}; + +// ============================================================================ +// Graph Filtering +// ============================================================================ + +function filterGraphByCenter( + graph: GraphData, + centerNode: string, + depth: number, +): GraphData { + // Find all nodes within `depth` hops from centerNode + const nodeSet = new Set([centerNode]); + const linkSet = new Map>(); + + // Build adjacency map + for (const link of graph.links) { + if (!linkSet.has(link.source)) linkSet.set(link.source, new Set()); + if (!linkSet.has(link.target)) linkSet.set(link.target, new Set()); + linkSet.get(link.source)!.add(link.target); + linkSet.get(link.target)!.add(link.source); + } + + // BFS to find nodes within depth + let frontier = [centerNode]; + for (let d = 0; d < depth && frontier.length > 0; d++) { + const next: string[] = []; + for (const node of frontier) { + const neighbors = linkSet.get(node) ?? []; + for (const neighbor of neighbors) { + if (!nodeSet.has(neighbor)) { + nodeSet.add(neighbor); + next.push(neighbor); + } + } + } + frontier = next; + } + + // Filter nodes and links + const filteredNodes = graph.nodes.filter((n) => nodeSet.has(n.id)); + const filteredLinks = graph.links.filter( + (l) => nodeSet.has(l.source) && nodeSet.has(l.target), + ); + + return { + nodes: filteredNodes, + links: filteredLinks, + metadata: { + ...graph.metadata, + title: `${graph.metadata.title} (centered on ${centerNode})`, + }, + }; +} + +// ============================================================================ +// MCP Server +// ============================================================================ + +function createServer(): McpServer { + const server = new McpServer({ + name: "D3 Graph Server", + version: "1.0.0", + }); + + const resourceUri = "ui://d3-graph/mcp-app.html"; + + registerAppTool( + server, + "get-graph-data", + { + title: "Get Graph Data", + description: + "Returns graph data (nodes and links) for force-directed visualization. Supports multiple graph datasets and optional filtering by center node.", + inputSchema: GetGraphDataInputSchema.shape, + _meta: { [RESOURCE_URI_META_KEY]: resourceUri }, + }, + async (args: { + graphId?: "dependencies" | "org-chart" | "knowledge"; + centerNode?: string; + depth?: number; + }): Promise => { + const graphId = args.graphId ?? "dependencies"; + let graphData = SAMPLE_GRAPHS[graphId]; + + if (!graphData) { + return { + isError: true, + content: [{ type: "text", text: `Unknown graph: ${graphId}` }], + }; + } + + // Filter if centerNode is specified + if (args.centerNode) { + const nodeExists = graphData.nodes.some( + (n) => n.id === args.centerNode, + ); + if (!nodeExists) { + return { + isError: true, + content: [ + { type: "text", text: `Node not found: ${args.centerNode}` }, + ], + }; + } + graphData = filterGraphByCenter( + graphData, + args.centerNode, + args.depth ?? 2, + ); + } + + return { + content: [ + { + type: "text", + text: JSON.stringify({ + graphId, + ...graphData, + }), + }, + ], + }; + }, + ); + + registerAppResource( + server, + resourceUri, + resourceUri, + { mimeType: RESOURCE_MIME_TYPE, description: "D3 Graph Explorer UI" }, + async (): Promise => { + 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; +} + +startServer(createServer); diff --git a/examples/d3-graph-server/src/global.css b/examples/d3-graph-server/src/global.css new file mode 100644 index 00000000..59c501f5 --- /dev/null +++ b/examples/d3-graph-server/src/global.css @@ -0,0 +1,12 @@ +* { + box-sizing: border-box; +} + +html, body { + margin: 0; + padding: 0; + font-family: system-ui, -apple-system, sans-serif; + font-size: 1rem; + height: 100%; + overflow: hidden; +} diff --git a/examples/d3-graph-server/src/mcp-app.css b/examples/d3-graph-server/src/mcp-app.css new file mode 100644 index 00000000..02a0b735 --- /dev/null +++ b/examples/d3-graph-server/src/mcp-app.css @@ -0,0 +1,126 @@ +#main { + width: 100%; + height: 100vh; + display: flex; + flex-direction: column; + background: #f8fafc; +} + +@media (prefers-color-scheme: dark) { + #main { + background: #0f172a; + } +} + +#controls { + display: flex; + gap: 0.5rem; + padding: 0.75rem; + background: white; + border-bottom: 1px solid #e2e8f0; +} + +@media (prefers-color-scheme: dark) { + #controls { + background: #1e293b; + border-bottom-color: #334155; + } +} + +#controls select, +#controls button { + padding: 0.5rem 1rem; + border-radius: 6px; + font-size: 0.875rem; + cursor: pointer; +} + +#controls select { + border: 1px solid #cbd5e1; + background: white; + flex: 1; + max-width: 250px; +} + +@media (prefers-color-scheme: dark) { + #controls select { + background: #334155; + border-color: #475569; + color: #f1f5f9; + } +} + +#controls button { + border: none; + background: #3b82f6; + color: white; + font-weight: 500; +} + +#controls button:hover { + background: #2563eb; +} + +#graph-container { + flex: 1; + overflow: hidden; + position: relative; +} + +#graph { + width: 100%; + height: 100%; +} + +.node { + cursor: pointer; + stroke: #fff; + stroke-width: 2px; +} + +.node:hover { + stroke-width: 3px; +} + +.link { + stroke: #94a3b8; + stroke-opacity: 0.6; +} + +@media (prefers-color-scheme: dark) { + .link { + stroke: #64748b; + } +} + +.node-label { + font-size: 10px; + pointer-events: none; + fill: #1e293b; + text-anchor: middle; + dominant-baseline: central; +} + +@media (prefers-color-scheme: dark) { + .node-label { + fill: #f1f5f9; + } +} + +#tooltip { + position: absolute; + padding: 8px 12px; + background: rgba(15, 23, 42, 0.9); + color: white; + border-radius: 6px; + font-size: 12px; + pointer-events: none; + opacity: 0; + transition: opacity 0.15s; + z-index: 100; + max-width: 200px; +} + +#tooltip.visible { + opacity: 1; +} diff --git a/examples/d3-graph-server/src/mcp-app.ts b/examples/d3-graph-server/src/mcp-app.ts new file mode 100644 index 00000000..080276db --- /dev/null +++ b/examples/d3-graph-server/src/mcp-app.ts @@ -0,0 +1,301 @@ +/** + * D3 Force-Directed Graph Visualization for MCP Apps + */ +import { App } from "@modelcontextprotocol/ext-apps"; +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import * as d3 from "d3"; +import "./global.css"; +import "./mcp-app.css"; + +// Types +interface GraphNode { + id: string; + group: string; + size: number; + description?: string; + x?: number; + y?: number; + fx?: number | null; + fy?: number | null; +} + +interface GraphLink { + source: string | GraphNode; + target: string | GraphNode; + strength: number; +} + +interface GraphData { + graphId: string; + nodes: GraphNode[]; + links: GraphLink[]; + metadata: { + title: string; + description: string; + }; +} + +// Color scheme for node groups +const GROUP_COLORS: Record = { + // dependencies graph + core: "#ef4444", + viz: "#8b5cf6", + tooling: "#3b82f6", + runtime: "#10b981", + util: "#f59e0b", + styling: "#ec4899", + // org-chart graph + executive: "#dc2626", + management: "#2563eb", + lead: "#7c3aed", + ic: "#059669", + // knowledge graph + ai: "#6366f1", + architecture: "#0891b2", + tools: "#65a30d", + application: "#ea580c", +}; + +// Logging +const log = { + info: console.log.bind(console, "[APP]"), + error: console.error.bind(console, "[APP]"), +}; + +// DOM Elements +const graphSelect = document.getElementById( + "graph-select", +) as HTMLSelectElement; +const resetBtn = document.getElementById("reset-btn") as HTMLButtonElement; +const graphContainer = document.getElementById("graph-container")!; +const svg = d3.select("#graph"); +const tooltip = document.getElementById("tooltip")!; + +// State +let currentGraphData: GraphData | null = null; +let simulation: d3.Simulation | null = null; + +// Create main group for zoom/pan +const g = svg.append("g"); + +// Create groups for links and nodes (in order) +const linksGroup = g.append("g").attr("class", "links"); +const nodesGroup = g.append("g").attr("class", "nodes"); + +// Zoom behavior +const zoom = d3 + .zoom() + .scaleExtent([0.1, 4]) + .on("zoom", (event) => { + g.attr("transform", event.transform.toString()); + }); + +svg.call(zoom); + +// Extract data from tool result +function extractGraphData(result: CallToolResult): GraphData | null { + const textContent = result.content?.find((c) => c.type === "text"); + if (!textContent || textContent.type !== "text") return null; + try { + return JSON.parse(textContent.text) as GraphData; + } catch { + log.error("Failed to parse graph data"); + return null; + } +} + +// Get node color by group +function getNodeColor(group: string): string { + return GROUP_COLORS[group] ?? "#64748b"; +} + +// Update tooltip content safely (no innerHTML) +function updateTooltip(node: GraphNode): void { + // Clear existing content + tooltip.textContent = ""; + + // Create title element + const title = document.createElement("strong"); + title.textContent = node.id; + tooltip.appendChild(title); + + // Add line break + tooltip.appendChild(document.createElement("br")); + + // Add description + const desc = document.createTextNode(node.description ?? node.group); + tooltip.appendChild(desc); +} + +// Render the graph +function renderGraph(data: GraphData): void { + currentGraphData = data; + + // Clear existing + linksGroup.selectAll("*").remove(); + nodesGroup.selectAll("*").remove(); + + // Stop previous simulation + if (simulation) { + simulation.stop(); + } + + const width = graphContainer.clientWidth; + const height = graphContainer.clientHeight; + + // Create simulation + simulation = d3 + .forceSimulation(data.nodes) + .force( + "link", + d3 + .forceLink(data.links) + .id((d) => d.id) + .strength((d) => (d.strength as number) * 0.5), + ) + .force("charge", d3.forceManyBody().strength(-300)) + .force("center", d3.forceCenter(width / 2, height / 2)) + .force( + "collision", + d3.forceCollide().radius((d) => d.size + 5), + ); + + // Draw links + const links = linksGroup + .selectAll("line") + .data(data.links) + .join("line") + .attr("class", "link") + .attr("stroke-width", (d) => Math.max(1, (d.strength as number) * 3)); + + // Draw nodes + const nodeGroups = nodesGroup + .selectAll("g") + .data(data.nodes) + .join("g") + .attr("cursor", "pointer"); + + // Node circles + nodeGroups + .append("circle") + .attr("class", "node") + .attr("r", (d) => d.size / 2) + .attr("fill", (d) => getNodeColor(d.group)) + .on("mouseover", (_event, d) => { + updateTooltip(d); + tooltip.classList.add("visible"); + }) + .on("mousemove", (event) => { + tooltip.style.left = `${event.pageX + 12}px`; + tooltip.style.top = `${event.pageY - 12}px`; + }) + .on("mouseout", () => { + tooltip.classList.remove("visible"); + }) + .on("click", async (_event, d) => { + // Re-center graph on clicked node + log.info(`Clicked node: ${d.id}, loading centered view...`); + try { + await app.callServerTool({ + name: "get-graph-data", + arguments: { + graphId: currentGraphData?.graphId, + centerNode: d.id, + depth: 2, + }, + }); + } catch (e) { + log.error("Failed to load centered graph:", e); + } + }); + + // Node labels (for larger nodes) + nodeGroups + .filter((d) => d.size >= 24) + .append("text") + .attr("class", "node-label") + .attr("dy", (d) => d.size / 2 + 14) + .text((d) => d.id); + + // Drag behavior + const drag = d3 + .drag() + .on("start", (event, d) => { + if (!event.active) simulation!.alphaTarget(0.3).restart(); + d.fx = d.x; + d.fy = d.y; + }) + .on("drag", (event, d) => { + d.fx = event.x; + d.fy = event.y; + }) + .on("end", (event, d) => { + if (!event.active) simulation!.alphaTarget(0); + d.fx = null; + d.fy = null; + }); + + nodeGroups.call(drag); + + // Update positions on tick + simulation.on("tick", () => { + links + .attr("x1", (d) => (d.source as GraphNode).x!) + .attr("y1", (d) => (d.source as GraphNode).y!) + .attr("x2", (d) => (d.target as GraphNode).x!) + .attr("y2", (d) => (d.target as GraphNode).y!); + + nodeGroups.attr("transform", (d) => `translate(${d.x},${d.y})`); + }); +} + +// Reset view to initial zoom +function resetView(): void { + svg.transition().duration(500).call(zoom.transform, d3.zoomIdentity); +} + +// Event listeners +graphSelect.addEventListener("change", async () => { + const graphId = graphSelect.value; + log.info(`Loading graph: ${graphId}`); + try { + await app.callServerTool({ + name: "get-graph-data", + arguments: { graphId }, + }); + } catch (e) { + log.error("Failed to load graph:", e); + } +}); + +resetBtn.addEventListener("click", resetView); + +// Handle window resize +window.addEventListener("resize", () => { + if (currentGraphData && simulation) { + const width = graphContainer.clientWidth; + const height = graphContainer.clientHeight; + simulation.force("center", d3.forceCenter(width / 2, height / 2)); + simulation.alpha(0.3).restart(); + } +}); + +// Create MCP App +const app = new App({ name: "D3 Graph Explorer", version: "1.0.0" }); + +app.ontoolresult = (result) => { + log.info("Received graph data"); + const data = extractGraphData(result); + if (data) { + renderGraph(data); + // Update select to match current graph + if (data.graphId && graphSelect.value !== data.graphId) { + graphSelect.value = data.graphId; + } + } +}; + +app.onerror = log.error; + +// Connect to host +app.connect(); diff --git a/examples/d3-graph-server/src/server-utils.ts b/examples/d3-graph-server/src/server-utils.ts new file mode 100644 index 00000000..40524237 --- /dev/null +++ b/examples/d3-graph-server/src/server-utils.ts @@ -0,0 +1,110 @@ +/** + * Shared utilities for running MCP servers with various transports. + */ + +import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import cors from "cors"; +import type { Request, Response } from "express"; + +/** + * Starts an MCP server using the appropriate transport based on command-line arguments. + * + * If `--stdio` is passed, uses stdio transport. Otherwise, uses Streamable HTTP transport. + * + * @param createServer - Factory function that creates a new McpServer instance. + */ +export async function startServer( + createServer: () => McpServer, +): Promise { + try { + if (process.argv.includes("--stdio")) { + await startStdioServer(createServer); + } else { + await startStreamableHttpServer(createServer); + } + } catch (e) { + console.error(e); + process.exit(1); + } +} + +/** + * Starts an MCP server with stdio transport. + * + * @param createServer - Factory function that creates a new McpServer instance. + */ +export async function startStdioServer( + createServer: () => McpServer, +): Promise { + await createServer().connect(new StdioServerTransport()); +} + +/** + * Starts an MCP server with Streamable HTTP transport in stateless mode. + * + * Each request creates a fresh server and transport instance, which are + * closed when the response ends (no session tracking). + * + * The server listens on the port specified by the PORT environment variable, + * defaulting to 3001 if not set. + * + * @param createServer - Factory function that creates a new McpServer instance per request. + */ +export async function startStreamableHttpServer( + createServer: () => McpServer, +): Promise { + const port = parseInt(process.env.PORT ?? "3001", 10); + + // Express app - bind to all interfaces for development/testing + const expressApp = createMcpExpressApp({ host: "0.0.0.0" }); + expressApp.use(cors()); + + expressApp.all("/mcp", async (req: Request, res: Response) => { + // Create fresh server and transport for each request (stateless mode) + const server = createServer(); + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + }); + + // Clean up when response ends + res.on("close", () => { + transport.close().catch(() => {}); + server.close().catch(() => {}); + }); + + try { + await server.connect(transport); + await transport.handleRequest(req, res, req.body); + } catch (error) { + console.error("MCP error:", error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: "2.0", + error: { code: -32603, message: "Internal server error" }, + id: null, + }); + } + } + }); + + const { promise, resolve, reject } = Promise.withResolvers(); + + const httpServer = expressApp.listen(port, (err?: Error) => { + if (err) return reject(err); + console.log(`Server listening on http://localhost:${port}/mcp`); + resolve(); + }); + + const shutdown = () => { + console.log("\nShutting down..."); + httpServer.close(() => process.exit(0)); + }; + + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); + + return promise; +} diff --git a/examples/d3-graph-server/tsconfig.json b/examples/d3-graph-server/tsconfig.json new file mode 100644 index 00000000..535267b2 --- /dev/null +++ b/examples/d3-graph-server/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "server.ts"] +} diff --git a/examples/d3-graph-server/vite.config.ts b/examples/d3-graph-server/vite.config.ts new file mode 100644 index 00000000..6ff6d997 --- /dev/null +++ b/examples/d3-graph-server/vite.config.ts @@ -0,0 +1,24 @@ +import { defineConfig } from "vite"; +import { viteSingleFile } from "vite-plugin-singlefile"; + +const INPUT = process.env.INPUT; +if (!INPUT) { + throw new Error("INPUT environment variable is not set"); +} + +const isDevelopment = process.env.NODE_ENV === "development"; + +export default defineConfig({ + plugins: [viteSingleFile()], + build: { + sourcemap: isDevelopment ? "inline" : undefined, + cssMinify: !isDevelopment, + minify: !isDevelopment, + + rollupOptions: { + input: INPUT, + }, + outDir: "dist", + emptyOutDir: false, + }, +}); diff --git a/examples/recharts-chart-server/README.md b/examples/recharts-chart-server/README.md new file mode 100644 index 00000000..6154ec37 --- /dev/null +++ b/examples/recharts-chart-server/README.md @@ -0,0 +1,116 @@ +# Example: Recharts Dashboard Server + +A React-based business metrics dashboard with switchable chart types. Visualize revenue, sales, and product data with bar, line, area, and pie charts. + +## Features + +- **Multiple chart types**: Bar, line, area, and pie charts +- **Dataset switching**: Toggle between monthly revenue, quarterly sales, and product mix +- **Responsive design**: Charts adapt to container size +- **Custom tooltips**: Formatted values with dark theme styling +- **Color-coded series**: Each data series has a distinct color +- **Theme support**: Adapts to light/dark mode preferences + +## Running + +1. Install dependencies: + + ```bash + npm install + ``` + +2. Build and start the server: + + ```bash + npm run start:http # for Streamable HTTP transport + # OR + npm run start:stdio # for stdio transport + ``` + +3. View using the [`basic-host`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host) example or another MCP Apps-compatible host. + +### Tool Input Examples + +**Default (monthly revenue as bar chart):** + +```json +{} +``` + +**Monthly revenue as line chart:** + +```json +{ + "datasetId": "monthly-revenue", + "chartType": "line" +} +``` + +**Monthly revenue as area chart:** + +```json +{ + "datasetId": "monthly-revenue", + "chartType": "area" +} +``` + +**Quarterly sales by region (bar chart):** + +```json +{ + "datasetId": "quarterly-sales" +} +``` + +**Quarterly sales as line chart:** + +```json +{ + "datasetId": "quarterly-sales", + "chartType": "line" +} +``` + +**Quarterly sales as area chart:** + +```json +{ + "datasetId": "quarterly-sales", + "chartType": "area" +} +``` + +**Product mix as pie chart:** + +```json +{ + "datasetId": "product-mix" +} +``` + +**Product mix as bar chart:** + +```json +{ + "datasetId": "product-mix", + "chartType": "bar" +} +``` + +## Architecture + +### Server (`server.ts`) + +MCP server with sample business datasets for different visualization scenarios. + +Exposes one tool: + +- `get-chart-data` - Returns chart data with metadata for rendering + +### App (`src/`) + +- Built with React for reactive state management +- Uses Recharts for chart visualization +- Components: `ChartRenderer` (renders bar/line/area/pie based on type) +- Chart type and dataset selection triggers tool calls for new data diff --git a/examples/recharts-chart-server/mcp-app.html b/examples/recharts-chart-server/mcp-app.html new file mode 100644 index 00000000..6b895ed8 --- /dev/null +++ b/examples/recharts-chart-server/mcp-app.html @@ -0,0 +1,13 @@ + + + + + + + Recharts Dashboard + + +
+ + + diff --git a/examples/recharts-chart-server/package.json b/examples/recharts-chart-server/package.json new file mode 100644 index 00000000..bc01040f --- /dev/null +++ b/examples/recharts-chart-server/package.json @@ -0,0 +1,36 @@ +{ + "name": "recharts-chart-server", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build", + "watch": "cross-env INPUT=mcp-app.html vite build --watch", + "serve": "bun server.ts", + "start": "cross-env NODE_ENV=development npm run build && npm run serve", + "dev": "cross-env NODE_ENV=development concurrently 'npm run watch' 'npm run serve'" + }, + "dependencies": { + "@modelcontextprotocol/ext-apps": "../..", + "@modelcontextprotocol/sdk": "^1.24.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "recharts": "^2.15.0", + "zod": "^4.1.13" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.0", + "@types/node": "^22.0.0", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "@vitejs/plugin-react": "^4.3.4", + "concurrently": "^9.2.1", + "cors": "^2.8.5", + "cross-env": "^7.0.3", + "express": "^5.1.0", + "typescript": "^5.9.3", + "vite": "^6.0.0", + "vite-plugin-singlefile": "^2.3.0" + } +} diff --git a/examples/recharts-chart-server/server.ts b/examples/recharts-chart-server/server.ts new file mode 100644 index 00000000..0d7ac19a --- /dev/null +++ b/examples/recharts-chart-server/server.ts @@ -0,0 +1,268 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.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 "./src/server-utils.js"; + +const DIST_DIR = path.join(import.meta.dirname, "dist"); + +// ============================================================================ +// Schemas +// ============================================================================ + +const ChartTypeSchema = z.enum(["bar", "line", "area", "pie"]); + +const GetChartDataInputSchema = z.object({ + datasetId: z + .enum(["monthly-revenue", "quarterly-sales", "product-mix"]) + .describe("Which dataset to load"), + chartType: ChartTypeSchema.optional().describe( + "Preferred chart type (bar, line, area, pie)", + ), +}); + +// Types +type ChartType = z.infer; + +interface DataPoint { + [key: string]: string | number; +} + +interface ChartMetadata { + title: string; + description: string; + xKey: string; + series: string[]; + colors: Record; + defaultChartType: ChartType; + supportedChartTypes: ChartType[]; +} + +interface ChartDataset { + data: DataPoint[]; + metadata: ChartMetadata; +} + +// ============================================================================ +// Sample Datasets +// ============================================================================ + +const DATASETS: Record = { + "monthly-revenue": { + data: [ + { month: "Jan", revenue: 42000, costs: 28000, profit: 14000 }, + { month: "Feb", revenue: 38000, costs: 25000, profit: 13000 }, + { month: "Mar", revenue: 45000, costs: 29000, profit: 16000 }, + { month: "Apr", revenue: 52000, costs: 32000, profit: 20000 }, + { month: "May", revenue: 48000, costs: 30000, profit: 18000 }, + { month: "Jun", revenue: 56000, costs: 35000, profit: 21000 }, + { month: "Jul", revenue: 61000, costs: 38000, profit: 23000 }, + { month: "Aug", revenue: 58000, costs: 36000, profit: 22000 }, + { month: "Sep", revenue: 64000, costs: 40000, profit: 24000 }, + { month: "Oct", revenue: 70000, costs: 43000, profit: 27000 }, + { month: "Nov", revenue: 75000, costs: 46000, profit: 29000 }, + { month: "Dec", revenue: 82000, costs: 50000, profit: 32000 }, + ], + metadata: { + title: "Monthly Revenue", + description: "Revenue, costs, and profit over the past 12 months", + xKey: "month", + series: ["revenue", "costs", "profit"], + colors: { + revenue: "#3b82f6", + costs: "#ef4444", + profit: "#10b981", + }, + defaultChartType: "bar", + supportedChartTypes: ["bar", "line", "area"], + }, + }, + "quarterly-sales": { + data: [ + { + quarter: "Q1 2023", + north: 120000, + south: 85000, + east: 95000, + west: 110000, + }, + { + quarter: "Q2 2023", + north: 135000, + south: 92000, + east: 105000, + west: 125000, + }, + { + quarter: "Q3 2023", + north: 142000, + south: 98000, + east: 112000, + west: 138000, + }, + { + quarter: "Q4 2023", + north: 168000, + south: 115000, + east: 128000, + west: 155000, + }, + { + quarter: "Q1 2024", + north: 155000, + south: 108000, + east: 118000, + west: 145000, + }, + { + quarter: "Q2 2024", + north: 178000, + south: 125000, + east: 135000, + west: 165000, + }, + ], + metadata: { + title: "Quarterly Sales by Region", + description: "Sales performance across different regions", + xKey: "quarter", + series: ["north", "south", "east", "west"], + colors: { + north: "#6366f1", + south: "#f59e0b", + east: "#14b8a6", + west: "#ec4899", + }, + defaultChartType: "bar", + supportedChartTypes: ["bar", "line", "area"], + }, + }, + "product-mix": { + data: [ + { name: "Enterprise", value: 42, color: "#3b82f6" }, + { name: "Professional", value: 28, color: "#8b5cf6" }, + { name: "Starter", value: 18, color: "#10b981" }, + { name: "Free", value: 8, color: "#f59e0b" }, + { name: "Legacy", value: 4, color: "#64748b" }, + ], + metadata: { + title: "Product Mix", + description: "Revenue distribution across product tiers", + xKey: "name", + series: ["value"], + colors: { + Enterprise: "#3b82f6", + Professional: "#8b5cf6", + Starter: "#10b981", + Free: "#f59e0b", + Legacy: "#64748b", + }, + defaultChartType: "pie", + supportedChartTypes: ["pie", "bar"], + }, + }, +}; + +// ============================================================================ +// MCP Server +// ============================================================================ + +function createServer(): McpServer { + const server = new McpServer({ + name: "Recharts Dashboard Server", + version: "1.0.0", + }); + + const resourceUri = "ui://recharts-dashboard/mcp-app.html"; + + registerAppTool( + server, + "get-chart-data", + { + title: "Get Chart Data", + description: + "Returns chart data for visualization. Supports multiple datasets and chart types.", + inputSchema: GetChartDataInputSchema.shape, + _meta: { [RESOURCE_URI_META_KEY]: resourceUri }, + }, + async (args: { + datasetId: "monthly-revenue" | "quarterly-sales" | "product-mix"; + chartType?: ChartType; + }): Promise => { + const dataset = DATASETS[args.datasetId]; + + if (!dataset) { + return { + isError: true, + content: [ + { type: "text", text: `Unknown dataset: ${args.datasetId}` }, + ], + }; + } + + // Validate chart type if provided + const chartType = args.chartType ?? dataset.metadata.defaultChartType; + if (!dataset.metadata.supportedChartTypes.includes(chartType)) { + return { + isError: true, + content: [ + { + type: "text", + text: `Chart type "${chartType}" not supported for this dataset. Supported: ${dataset.metadata.supportedChartTypes.join(", ")}`, + }, + ], + }; + } + + return { + content: [ + { + type: "text", + text: JSON.stringify({ + datasetId: args.datasetId, + chartType, + data: dataset.data, + metadata: dataset.metadata, + }), + }, + ], + }; + }, + ); + + registerAppResource( + server, + resourceUri, + resourceUri, + { mimeType: RESOURCE_MIME_TYPE, description: "Recharts Dashboard UI" }, + async (): Promise => { + 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; +} + +startServer(createServer); diff --git a/examples/recharts-chart-server/src/components/ChartRenderer.tsx b/examples/recharts-chart-server/src/components/ChartRenderer.tsx new file mode 100644 index 00000000..66521868 --- /dev/null +++ b/examples/recharts-chart-server/src/components/ChartRenderer.tsx @@ -0,0 +1,203 @@ +import { + ResponsiveContainer, + BarChart, + Bar, + LineChart, + Line, + AreaChart, + Area, + PieChart, + Pie, + Cell, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, +} from "recharts"; + +type ChartType = "bar" | "line" | "area" | "pie"; + +interface DataPoint { + [key: string]: string | number; +} + +interface ChartMetadata { + title: string; + description: string; + xKey: string; + series: string[]; + colors: Record; + defaultChartType: ChartType; + supportedChartTypes: ChartType[]; +} + +interface ChartRendererProps { + data: DataPoint[]; + metadata: ChartMetadata; + chartType: ChartType; +} + +// Format currency values +function formatValue(value: number): string { + if (value >= 1000000) { + return `$${(value / 1000000).toFixed(1)}M`; + } + if (value >= 1000) { + return `$${(value / 1000).toFixed(0)}K`; + } + return `$${value}`; +} + +// Format percentage values +function formatPercent(value: number): string { + return `${value}%`; +} + +export function ChartRenderer({ + data, + metadata, + chartType, +}: ChartRendererProps) { + const { xKey, series, colors } = metadata; + const isPie = chartType === "pie"; + + // For pie charts, use the name field and add colors from data if available + const pieColors = + data[0]?.color !== undefined + ? data.map((d) => d.color as string) + : series.map((s) => colors[s] ?? "#64748b"); + + if (isPie) { + return ( + + + + `${name}: ${(percent * 100).toFixed(0)}%` + } + labelLine={true} + > + {data.map((entry, index) => ( + + ))} + + formatPercent(value)} + contentStyle={{ + backgroundColor: "#1e293b", + border: "none", + borderRadius: "6px", + color: "#f1f5f9", + }} + /> + + + + ); + } + + // Common props for cartesian charts + const chartContent = ( + <> + + + + formatValue(value)} + contentStyle={{ + backgroundColor: "#1e293b", + border: "none", + borderRadius: "6px", + color: "#f1f5f9", + }} + labelStyle={{ color: "#94a3b8" }} + /> + + + ); + + if (chartType === "bar") { + return ( + + + {chartContent} + {series.map((key) => ( + + ))} + + + ); + } + + if (chartType === "line") { + return ( + + + {chartContent} + {series.map((key) => ( + + ))} + + + ); + } + + // Area chart + return ( + + + {chartContent} + {series.map((key) => ( + + ))} + + + ); +} diff --git a/examples/recharts-chart-server/src/global.css b/examples/recharts-chart-server/src/global.css new file mode 100644 index 00000000..0102ed9a --- /dev/null +++ b/examples/recharts-chart-server/src/global.css @@ -0,0 +1,15 @@ +* { + box-sizing: border-box; +} + +html, body { + margin: 0; + padding: 0; + font-family: system-ui, -apple-system, sans-serif; + font-size: 1rem; + min-height: 100%; +} + +#root { + min-height: 100vh; +} diff --git a/examples/recharts-chart-server/src/mcp-app.css b/examples/recharts-chart-server/src/mcp-app.css new file mode 100644 index 00000000..0dc3dfc0 --- /dev/null +++ b/examples/recharts-chart-server/src/mcp-app.css @@ -0,0 +1,132 @@ +.main { + min-height: 100vh; + padding: 1rem; + background: #f8fafc; +} + +@media (prefers-color-scheme: dark) { + .main { + background: #0f172a; + color: #f1f5f9; + } +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; + flex-wrap: wrap; + gap: 1rem; +} + +.header-title { + margin: 0; + font-size: 1.5rem; + font-weight: 600; +} + +.controls { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.controls select, +.controls button { + padding: 0.5rem 1rem; + border-radius: 6px; + font-size: 0.875rem; + cursor: pointer; +} + +.controls select { + border: 1px solid #cbd5e1; + background: white; + min-width: 180px; +} + +@media (prefers-color-scheme: dark) { + .controls select { + background: #334155; + border-color: #475569; + color: #f1f5f9; + } +} + +.controls button { + border: none; + font-weight: 500; + transition: background-color 0.15s; +} + +.controls button.active { + background: #3b82f6; + color: white; +} + +.controls button:not(.active) { + background: #e2e8f0; + color: #475569; +} + +@media (prefers-color-scheme: dark) { + .controls button:not(.active) { + background: #334155; + color: #94a3b8; + } +} + +.controls button:hover { + opacity: 0.9; +} + +.chart-container { + background: white; + border-radius: 12px; + padding: 1.5rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + margin-bottom: 1rem; +} + +@media (prefers-color-scheme: dark) { + .chart-container { + background: #1e293b; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + } +} + +.chart-title { + margin: 0 0 1rem 0; + font-size: 1.125rem; + font-weight: 600; + color: #1e293b; +} + +@media (prefers-color-scheme: dark) { + .chart-title { + color: #f1f5f9; + } +} + +.chart-description { + margin: 0 0 1rem 0; + font-size: 0.875rem; + color: #64748b; +} + +.loading { + display: flex; + justify-content: center; + align-items: center; + height: 300px; + color: #64748b; +} + +.error { + display: flex; + justify-content: center; + align-items: center; + height: 300px; + color: #ef4444; +} diff --git a/examples/recharts-chart-server/src/mcp-app.tsx b/examples/recharts-chart-server/src/mcp-app.tsx new file mode 100644 index 00000000..786c04c5 --- /dev/null +++ b/examples/recharts-chart-server/src/mcp-app.tsx @@ -0,0 +1,196 @@ +import { useApp } from "@modelcontextprotocol/ext-apps/react"; +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { StrictMode, useState, useCallback } from "react"; +import { createRoot } from "react-dom/client"; +import { ChartRenderer } from "./components/ChartRenderer.tsx"; +import "./global.css"; +import "./mcp-app.css"; + +// Types +type ChartType = "bar" | "line" | "area" | "pie"; +type DatasetId = "monthly-revenue" | "quarterly-sales" | "product-mix"; + +interface DataPoint { + [key: string]: string | number; +} + +interface ChartMetadata { + title: string; + description: string; + xKey: string; + series: string[]; + colors: Record; + defaultChartType: ChartType; + supportedChartTypes: ChartType[]; +} + +interface ChartData { + datasetId: DatasetId; + chartType: ChartType; + data: DataPoint[]; + metadata: ChartMetadata; +} + +const APP_INFO = { name: "Recharts Dashboard", version: "1.0.0" }; + +// Dataset options for the selector +const DATASET_OPTIONS: { id: DatasetId; label: string }[] = [ + { id: "monthly-revenue", label: "Monthly Revenue" }, + { id: "quarterly-sales", label: "Quarterly Sales by Region" }, + { id: "product-mix", label: "Product Mix" }, +]; + +// Extract chart data from tool result +function extractChartData(result: CallToolResult): ChartData | null { + const textContent = result.content?.find((c) => c.type === "text"); + if (!textContent || textContent.type !== "text") return null; + try { + return JSON.parse(textContent.text) as ChartData; + } catch { + console.error("[APP] Failed to parse chart data"); + return null; + } +} + +function RechartsDashboard() { + const [chartData, setChartData] = useState(null); + const [selectedDataset, setSelectedDataset] = + useState("monthly-revenue"); + const [selectedChartType, setSelectedChartType] = useState("bar"); + + const { app, error } = useApp({ + appInfo: APP_INFO, + capabilities: {}, + onAppCreated: (app) => { + app.ontoolresult = async (result) => { + const data = extractChartData(result); + if (data) { + setChartData(data); + setSelectedDataset(data.datasetId); + setSelectedChartType(data.chartType); + } + }; + }, + }); + + const handleDatasetChange = useCallback( + async (e: React.ChangeEvent) => { + const datasetId = e.target.value as DatasetId; + setSelectedDataset(datasetId); + if (app) { + try { + await app.callServerTool({ + name: "get-chart-data", + arguments: { datasetId }, + }); + } catch (err) { + console.error("[APP] Failed to load dataset:", err); + } + } + }, + [app], + ); + + const handleChartTypeChange = useCallback( + async (chartType: ChartType) => { + // Check if chart type is supported + if ( + chartData && + !chartData.metadata.supportedChartTypes.includes(chartType) + ) { + return; + } + setSelectedChartType(chartType); + if (app) { + try { + await app.callServerTool({ + name: "get-chart-data", + arguments: { datasetId: selectedDataset, chartType }, + }); + } catch (err) { + console.error("[APP] Failed to change chart type:", err); + } + } + }, + [app, selectedDataset, chartData], + ); + + if (error) { + return ( +
+
Error: {error.message}
+
+ ); + } + + if (!app) { + return ( +
+
Connecting...
+
+ ); + } + + const supportedTypes = chartData?.metadata.supportedChartTypes ?? [ + "bar", + "line", + "area", + "pie", + ]; + + return ( +
+
+

Recharts Dashboard

+
+ + {(["bar", "line", "area", "pie"] as ChartType[]).map((type) => ( + + ))} +
+
+ +
+ {chartData ? ( + <> +

{chartData.metadata.title}

+

+ {chartData.metadata.description} +

+ + + ) : ( +
Loading chart data...
+ )} +
+
+ ); +} + +createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/examples/recharts-chart-server/src/server-utils.ts b/examples/recharts-chart-server/src/server-utils.ts new file mode 100644 index 00000000..40524237 --- /dev/null +++ b/examples/recharts-chart-server/src/server-utils.ts @@ -0,0 +1,110 @@ +/** + * Shared utilities for running MCP servers with various transports. + */ + +import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import cors from "cors"; +import type { Request, Response } from "express"; + +/** + * Starts an MCP server using the appropriate transport based on command-line arguments. + * + * If `--stdio` is passed, uses stdio transport. Otherwise, uses Streamable HTTP transport. + * + * @param createServer - Factory function that creates a new McpServer instance. + */ +export async function startServer( + createServer: () => McpServer, +): Promise { + try { + if (process.argv.includes("--stdio")) { + await startStdioServer(createServer); + } else { + await startStreamableHttpServer(createServer); + } + } catch (e) { + console.error(e); + process.exit(1); + } +} + +/** + * Starts an MCP server with stdio transport. + * + * @param createServer - Factory function that creates a new McpServer instance. + */ +export async function startStdioServer( + createServer: () => McpServer, +): Promise { + await createServer().connect(new StdioServerTransport()); +} + +/** + * Starts an MCP server with Streamable HTTP transport in stateless mode. + * + * Each request creates a fresh server and transport instance, which are + * closed when the response ends (no session tracking). + * + * The server listens on the port specified by the PORT environment variable, + * defaulting to 3001 if not set. + * + * @param createServer - Factory function that creates a new McpServer instance per request. + */ +export async function startStreamableHttpServer( + createServer: () => McpServer, +): Promise { + const port = parseInt(process.env.PORT ?? "3001", 10); + + // Express app - bind to all interfaces for development/testing + const expressApp = createMcpExpressApp({ host: "0.0.0.0" }); + expressApp.use(cors()); + + expressApp.all("/mcp", async (req: Request, res: Response) => { + // Create fresh server and transport for each request (stateless mode) + const server = createServer(); + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + }); + + // Clean up when response ends + res.on("close", () => { + transport.close().catch(() => {}); + server.close().catch(() => {}); + }); + + try { + await server.connect(transport); + await transport.handleRequest(req, res, req.body); + } catch (error) { + console.error("MCP error:", error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: "2.0", + error: { code: -32603, message: "Internal server error" }, + id: null, + }); + } + } + }); + + const { promise, resolve, reject } = Promise.withResolvers(); + + const httpServer = expressApp.listen(port, (err?: Error) => { + if (err) return reject(err); + console.log(`Server listening on http://localhost:${port}/mcp`); + resolve(); + }); + + const shutdown = () => { + console.log("\nShutting down..."); + httpServer.close(() => process.exit(0)); + }; + + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); + + return promise; +} diff --git a/examples/recharts-chart-server/tsconfig.json b/examples/recharts-chart-server/tsconfig.json new file mode 100644 index 00000000..b9fb7fac --- /dev/null +++ b/examples/recharts-chart-server/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "server.ts"] +} diff --git a/examples/recharts-chart-server/vite.config.ts b/examples/recharts-chart-server/vite.config.ts new file mode 100644 index 00000000..da0af84e --- /dev/null +++ b/examples/recharts-chart-server/vite.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import { viteSingleFile } from "vite-plugin-singlefile"; + +const INPUT = process.env.INPUT; +if (!INPUT) { + throw new Error("INPUT environment variable is not set"); +} + +const isDevelopment = process.env.NODE_ENV === "development"; + +export default defineConfig({ + plugins: [react(), viteSingleFile()], + build: { + sourcemap: isDevelopment ? "inline" : undefined, + cssMinify: !isDevelopment, + minify: !isDevelopment, + + rollupOptions: { + input: INPUT, + }, + outDir: "dist", + emptyOutDir: false, + }, +}); diff --git a/package-lock.json b/package-lock.json index 6cbf63c5..a75ef2f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -302,6 +302,64 @@ "dev": true, "license": "MIT" }, + "examples/d3-graph-server": { + "version": "1.0.0", + "dependencies": { + "@modelcontextprotocol/ext-apps": "../..", + "@modelcontextprotocol/sdk": "^1.24.0", + "d3": "^7.9.0", + "zod": "^4.1.13" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/d3": "^7.4.3", + "@types/express": "^5.0.0", + "@types/node": "^22.0.0", + "concurrently": "^9.2.1", + "cors": "^2.8.5", + "cross-env": "^7.0.3", + "express": "^5.1.0", + "typescript": "^5.9.3", + "vite": "^6.0.0", + "vite-plugin-singlefile": "^2.3.0" + } + }, + "examples/d3-graph-server/node_modules/@types/node": { + "version": "22.19.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", + "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "examples/d3-graph-server/node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "examples/d3-graph-server/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "examples/integration-server": { "version": "1.0.0", "dependencies": { @@ -343,6 +401,68 @@ "dev": true, "license": "MIT" }, + "examples/recharts-chart-server": { + "version": "1.0.0", + "dependencies": { + "@modelcontextprotocol/ext-apps": "../..", + "@modelcontextprotocol/sdk": "^1.24.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "recharts": "^2.15.0", + "zod": "^4.1.13" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.0", + "@types/node": "^22.0.0", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "@vitejs/plugin-react": "^4.3.4", + "concurrently": "^9.2.1", + "cors": "^2.8.5", + "cross-env": "^7.0.3", + "express": "^5.1.0", + "typescript": "^5.9.3", + "vite": "^6.0.0", + "vite-plugin-singlefile": "^2.3.0" + } + }, + "examples/recharts-chart-server/node_modules/@types/node": { + "version": "22.19.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", + "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "examples/recharts-chart-server/node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "examples/recharts-chart-server/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "examples/scenario-modeler-server": { "version": "1.0.0", "dependencies": { @@ -646,7 +766,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -870,6 +989,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -2190,6 +2318,281 @@ "@types/node": "*" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -2229,6 +2632,13 @@ "@types/send": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/hast": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", @@ -2252,7 +2662,6 @@ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2277,7 +2686,6 @@ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2788,7 +3196,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3152,6 +3559,15 @@ "node": ">=8" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/cohort-heatmap-server": { "resolved": "examples/cohort-heatmap-server", "link": true @@ -3183,6 +3599,15 @@ "dev": true, "license": "MIT" }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3339,18 +3764,57 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, "node_modules/customer-segmentation-server": { "resolved": "examples/customer-segmentation-server", "link": true }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-array": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "dev": true, "license": "ISC", "dependencies": { "internmap": "1 - 2" @@ -3359,6 +3823,15 @@ "node": ">=12" } }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-binarytree": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz", @@ -3366,21 +3839,71 @@ "dev": true, "license": "MIT" }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-color": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "dev": true, "license": "ISC", "engines": { "node": ">=12" } }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-dispatch": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", - "dev": true, "license": "ISC", "engines": { "node": ">=12" @@ -3390,7 +3913,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", - "dev": true, "license": "ISC", "dependencies": { "d3-dispatch": "1 - 3", @@ -3400,16 +3922,78 @@ "node": ">=12" } }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/d3-ease": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=12" } }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-force-3d": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.6.tgz", @@ -3431,7 +4015,31 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", - "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-graph-server": { + "resolved": "examples/d3-graph-server", + "link": true + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", "license": "ISC", "engines": { "node": ">=12" @@ -3441,7 +4049,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "dev": true, "license": "ISC", "dependencies": { "d3-color": "1 - 3" @@ -3457,11 +4064,37 @@ "dev": true, "license": "MIT" }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-quadtree": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", - "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", "license": "ISC", "engines": { "node": ">=12" @@ -3471,7 +4104,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "dev": true, "license": "ISC", "dependencies": { "d3-array": "2.10.0 - 3", @@ -3488,7 +4120,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", - "dev": true, "license": "ISC", "dependencies": { "d3-color": "1 - 3", @@ -3502,9 +4133,19 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", - "dev": true, "license": "ISC", - "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, "engines": { "node": ">=12" } @@ -3513,7 +4154,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "dev": true, "license": "ISC", "dependencies": { "d3-array": "2 - 3" @@ -3526,7 +4166,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "dev": true, "license": "ISC", "dependencies": { "d3-time": "1 - 3" @@ -3539,7 +4178,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "dev": true, "license": "ISC", "engines": { "node": ">=12" @@ -3549,7 +4187,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", - "dev": true, "license": "ISC", "dependencies": { "d3-color": "1 - 3", @@ -3569,7 +4206,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", - "dev": true, "license": "ISC", "dependencies": { "d3-dispatch": "1 - 3", @@ -3599,6 +4235,12 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -3609,6 +4251,15 @@ "node": ">=6" } }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -3618,6 +4269,16 @@ "node": ">= 0.8" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -3952,7 +4613,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -4012,6 +4672,15 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", @@ -4462,7 +5131,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "dev": true, "license": "ISC", "engines": { "node": ">=12" @@ -4611,7 +5279,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/jsesc": { @@ -4756,6 +5423,12 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/lodash-es": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", @@ -4855,6 +5528,18 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/loupe": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", @@ -5415,6 +6100,23 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -5489,7 +6191,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -5506,6 +6207,12 @@ "react": "^19.2.1" } }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -5516,6 +6223,37 @@ "node": ">=0.10.0" } }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -5529,6 +6267,48 @@ "node": ">=8.10.0" } }, + "node_modules/recharts": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-chart-server": { + "resolved": "examples/recharts-chart-server", + "link": true + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -5582,13 +6362,18 @@ "dev": true, "license": "MIT" }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, "node_modules/rollup": { "version": "4.53.3", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -5641,6 +6426,12 @@ "node": ">= 18" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, "node_modules/rxjs": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", @@ -6301,6 +7092,12 @@ "resolved": "examples/threejs-server", "link": true }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -6363,7 +7160,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7113,7 +7909,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7201,6 +7996,28 @@ "node": ">= 0.8" } }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/video-resource-server": { "resolved": "examples/video-resource-server", "link": true @@ -7211,7 +8028,6 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -7345,7 +8161,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7598,7 +8413,6 @@ "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", "dev": true, "license": "ISC", - "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -7643,7 +8457,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/tests/e2e/servers.spec.ts b/tests/e2e/servers.spec.ts index e4b1bb52..38281f70 100644 --- a/tests/e2e/servers.spec.ts +++ b/tests/e2e/servers.spec.ts @@ -19,6 +19,8 @@ const DYNAMIC_MASKS: Record = { ], threejs: ["canvas"], // 3D render canvas (dynamic animation) "wiki-explorer": ["#graph"], // Force-directed graph (dynamic layout) + "d3-graph": ["#graph-container", "svg"], // Force simulation is dynamic + "recharts-chart": [".chart-container"], // Charts with dynamic rendering }; // Server configurations (key is used for screenshot filenames, name is the MCP server name) @@ -34,6 +36,8 @@ const SERVERS = [ { key: "system-monitor", name: "System Monitor Server" }, { key: "threejs", name: "Three.js Server" }, { key: "wiki-explorer", name: "Wiki Explorer" }, + { key: "d3-graph", name: "D3 Graph Server" }, + { key: "recharts-chart", name: "Recharts Dashboard Server" }, ]; /** diff --git a/tests/e2e/servers.spec.ts-snapshots/d3-graph.png b/tests/e2e/servers.spec.ts-snapshots/d3-graph.png new file mode 100644 index 0000000000000000000000000000000000000000..20f8eaace9a73211c93405015d6c8a22d1778cc6 GIT binary patch literal 24326 zcmeIacT|)4zb+a?!G;Jby(oxSC@Q@Mm7*driX%!@kfs#rJ;AYqfQSgvrHVA^ouG8- zy%VH|7D{M=B=__B+k5UlckOe}xohpU_E~q${4qKiCGYz!pYnX3=Xvvowg%IFuKg$! zis{-_)mtdk9{BC9w!ORH7Zv{NbSTte)HT&hx80-WhZ&=IT(;0l#kb$9948fM5U(EF z&3;?urV61sz&ZPm+t2we^;Gw8U)Q+pciR8>u8!aqvAEm&%5(GDSofaTSNwq%5_z7s zCdSS>^sHfVTdAVO($d1xa^uGcX6h5A#h1{TA`D;JHqUSp`DNGc`|ykYU#NrdAex7L z;YqCa?K%&iiC#i6pipr_dn@5T@}Bqa89k$$T(&h8IbIh;oS(k|pB~R*IK(4M+gTO= zR=JCwffV61n5+}4+_)zgW8z6jI&tM?^(h7>t`G1>RgAS_6y*jAj|giUIyMMxZxGRq zE}l%4W_5u9Y6ry2o**l+`q$N*WvA-bEV+gQS%sx)&VeKLzdwKdI-AA9%V1!xxIH>L zy2TqF>^|ScC!>}92Tje7TESoaU%$a_It~=d_v3LCJ-jyS|LbetXFH`GwOPw;@9gTT z9Vu;_x!9Lwf_qBOC{{H3*OiwCuZ^VLOi(v+s5v%l;p*(1SksE7I{Ay_)C<7Bz7(@F z@hQ5utFaG14AHt{>@k_>Ko&wR;ZNNNUs`Dt$5GY?ogy(;hJ+S5+=2p561&?zUpaz` zdr?#OW;zy2{c%9tdp^7FeJzVG-HhE(kwXJ*Udd)+v6RS%#&{pCdc~}RLcO0@AGFuu zO0gxA2)Tw!6Cpa8MPq*ZP#GzA($>~{AGGAdKZS*@-A0)0L45765viWLxSu_c>ZZC99W;eq#| z+I}!F^ObdKD+mSjjG`1hNi)61UKxW;u*V|B6Yrf?USuKN=S*mxi^bEX{A0(zd(LIZ6gI!ActTn04dPXh_S?gD_x@Z)q4r2f zuL+hy-6jUH3sZU1^LzA{$7IomVY@RWWl=Oz3R;Pf7-CUuwOqrFcf{bPxYKMcA$uqE*r}*Dh z9a0ea>+#X{7uc~k!taYo^@2ekt|yfeD@q4yFtoNA(^~Y*##L{*wpD{_# ze8Ple#w@`hSu9G@-TJ6*ql{QD8cMw@g&uuB$S!vA0T?xjt%1uY5WmS~iWE|g5yoy4RRrnc3XAIP*c+ePR zg}j?C)QMDVj`hcjW49W0l(!!0(r|}UhzD}OvM_6D{^Av~%D2r|6IccY6P%r$ zy9xtaWyjwl3(#*F>oXauN8Oo5TdoS8sFU^@tmn2bYtL2V;a?jwzDM=e%feAT$wqj& z<(<{mI>x&GEM2j%Tu<0(ZY59WGTB}uHzP&72t!??Y$Nqa*}M*#Lh4G@4Us$Fn1X(l ztQ}2;`}CGm!s1YG5WXb}cU>hjFF(KLd-Z97MRf-4PWv*cH6k9nkl(A#^_dj78x_Y) zdVpHQoA-Tt07q)8uwpF(_AR!(s_7$@po6{cUvJIr(r=v-jcl^b3tqRO3uz`kYu)#h zRTDe$H0nlwGu#s7(rAXP$NP)PJ5JQ2cYZPVcbL$~z1GXk_86x(V(kyzcb4nMpR(y& zeNGB9mOs#u-5lgIZ$=@n`5$}9%wN%Q{Tw`#@tW3@)phaZKse?U_+fr7>Tfx+mwp_u zFX*>G)3#cLZ-k$3HxSP)3DC;_x{_XPoEydChZF%dJiH3gpawQ7HHglavX&D7y@ge83 z$}SolWrmo$^qmp;M!3-TKbe2L_b^zk2hL}oW1++enV~0DBxATY+iSfh{QS{j5aJnP zzB8A#j+K*jW68HWLZ6P83u)mPd7h}0?SsTqyg@Z$u#R@=k=vfng|jkEvLG>GND2IE z4iNiV94W}1ND~Zfag0vOEIFH&aE2>j((YV!UCzv9jaL$cStC{6r=nxdA6;9Wpe(8}C}$w2bd3VHMR~PJ!fW0>heqA* zwTFW)9<=U$vakD&LB{mYwJUdHU3%_C+T=8het*^(87!R;Ii&Qo4^r*u{^1(+PwCSVjmAqJ<2U4CtG)41omz?ftSbSfxD@bYm?!1nV*G`4l z@{EJ(s38B=Re6a(u0%KQiC_%@(bkp6Bb8-B7oU`cjXbGhT7LP-xxBfSt}gM?qkMh+ zi3bO4Jz1$DE~0 zx-?|e>O&#g&x9v`-C z?wf}t!StJltqooENs*LxuQ{ft$}bhTCmly6!{$4-u3^`6W4lxE?w1zJH%25AVqm-4 zrxxUP?iQx-E3M8ypM@KHLEy2z#veNVSL>I{VzAUL*`aqy_+xs87o=l`FO*z?>pH0a zIO9*;q%)6A+&)OF%A~d{{mt(DcK1;&73B8z_Fxn$Bjc{fsetv0Lw`w`R=?h>+20&P zsl39hBuVyzNT>A?W2XJQY9#Z7A+5QaTzyQd<(#GYN0XU=JGLYVB92B?W4m`!GdGwN zUqh~>XI9;i_3R}yS#<~UDV0pK9g>@PmlET7wxnVWZ;;j+|A|Su?|8vhlSHdG(F@;b zp?<{uW0j!>Y6R9ZCvw>_+PULe@Ke2e=Aof?^a*uns)M3wf%A{&`|y-Sw@%OQKlOaz zf|}-<)xK>gcrsv}(wswQKqI-qQrb|ZdJRL^){1?ItNG_kArK$m2*(u7 zWtNWp>X2I&=#BM=c~kCpkiWH@YJjgSOQL$=+V^XZY$-!E)Mgb~E@&OK;dOL|oI6k( z#81nd(;)fT^bB^8RZ2-6nwD!g>ROhqWm1J%l%I~{_F}2MFx;wTDu1SLct5GJoO$U%+{oyfo=X?49jYclo#fRtbBI+pMmVZW)gy00{2ws zzP=kyI&ak$SMLXRQ|Dq@m{1@KYxL%^?qhF|lIfdGHfDT|sufB5E7M&XFxjw@1o`D_fg?+!fmsI|?&emdGM zFzQneRBl`Imgq>PBgZ_vpz>(#F}(kDPrOIuFa$^5Tq!T^$MP4K?pa2!L8T{K>y|DQ47&6h6;6IM=)rx9rIJ$S>bM(KzXt7%AW=zI%37=?gk7z>zTAYJwcFk6 zeDmS9yS#3ScKKS5L8n|H9^=JxynZoK$={tIu+zJ-Ah=L>;ji7{VNjxp=6Bu7DqFd} zUXIV($3wiVDOUV&<-5m1z;af(@`E3V4Z6wNn)gR!;;^{St4#XNpC23$m$f-WbnN!g zwfA^9OW&&H<+4M1F7MxhFX+jkNSkEPEPDvCsMUz;N$=U1Yih$DArCA)Zk321pD-Er6R;Mmr5Y!=?#zFo<4pgpd|PAs?S;=c1l z|7TYGw9UFXe1IT*9k-gj$!!mQ#nrKJLIX*N?cVWSRh4wgqa6q3Q*H#%%bV3{aiEtM z)`i$(C_fWSV%Jw26^QN~?Xma#1$W6PDN0e@*1lkP2bl~VKW}VLXpCZmeVQK`GgU%5 zpPf-ke#UC4_aYk7eEhSBo_5RE$6TSLTx~_8qgR)znC-uPXBd{gi)QPBYwKRkqEW3L zA2<^#r8j4iZXRYNO)!EQO#_Nca&~mVz@s#h|My(Z3R&)4$+h|1)|OvY z-ZVSB{5WB&wZE+`m3*lzVIO}`Bz6DCyQXh;Td6DM+R=B~fBqvpZB3A(o@Mi?hW&cn zo9fR|d#3qWXKvNl86S$_Eaz4+v?tH>+Pl7@ z*g<_GGVe2IfR~>NH|vmRH7acGDTQpxEI$)>b^fNts{)MSB$;hy$dAwc{P)#t6+@h zul&uFimT$&mrXFl+XT0k08dx_GSk6~>Dw$;);{ih^^jSPW-lOBS%YvaPx(-*&!SU* zyj4Ilo%`m}um1X~;FDN#XRLOF4;8s zuiU|?+@o*gxv8_?8Tp4`adJmiboqA_pu77Q7NDc#Qc1`7@Nx2kBC6q>Y}-4g^K6t~ zEQ0DH9g|42!K3JSix&Blt85Mby~Vg2vhZ@*YK!LFW(~wEI|4oQe>&0fQgkz#S>@E8 z%T=#Vwso=bDN9mQHws84jL9dPn6fz9Uh$4Xbj>YP3y#RS86hxb2Ean^`->M(@A+?NkKq|zN`iuFln=NO%0lE`^Hyra?lm2u1Y*%PmL-e+PrhTX)=NgYT^Cz<7HZk{4WRqbR7 zoG0dWYO@X06&t70 z;D=)7i~@7woNbZ&LQXM9+yAjkOOzU1hh7``sJ#AuojA1YM@QVaiej-tT9aOl<{26F zh_5Co2IPQ~9+0=!3chdpx-u z2)KLC1XS0`+;tSYC0Z3%nX#Ky^BQ;G1e#9_LKjvvt$8GSVFO2_z;!A^^0mZVWXQ0q z?D`T!yVhFEK#JU2mu|utrN-^#LkjcXbhYJ#23N0cnrsjB#vh2z_+)hQ%(+`@CRZ01 z8XHW=9bt=5b2zm&3v_p}lzGcxF}tI$I82*cy{9A^N|h*0 za4Ze5PH<-y3oh@lj_Va5H8?ZrSk@pCV4i}}JRIK)E#K5vy#&}q~5&HOA1IwIh4eboHea<$}lCa&gP72L=kTl%`1@(K!_H%U{5Wwy10Eq@&l+vFR0 zigbx7HBnKjPSelwnX|SQEw5LV*-6v7WE4D2e;9uC#`n>+?N}VK!1p#DZ+>$*Y@*zc z+4!96HL47Y&-5e8%wmgUcqW5>StC!TBHnv70}f)t_84|2lx|BhQF9k+i7xf_S?dc~ z#*6@Dp{7(_)ePu3d{0d{v!dd3_Ei#VkmL(}Ow!tWO|eg?w!KNW&Uo-EA%(UI$1?^d zeedNOd{w0pAZXB>^zA>F-qvgRw)as!iHitO$umY~W1H-nl7OJi3IGW#O&PgXringY zYxUj_lQ5JKPCBOn(!|=nPJXO4xl6f1y6vLizyP~->0GLHND8%gM=Ffai!-_CutK^- zKfvcbvd7PN;~8ql_#m&mvv%gqeJU{-(2xvIL2gt|iN%l&jVciU!02&CK9Wv?ye9;& zA&08Md97Qow{>e)4~Oi9IKXNR+mEx#*Y)ko*R*ASJ)gj?SCo4`#2C$Dl3u?4j)sy+ zDk1q)z_+%xiA0|%g|&f}Su+~;S*&3x?P)B>W?iIwYnjJ&X{VXE3Ab%#%vciP8RMw~ zO}*>=CfscY+|dAWPN+%Bb&mNc)c3WwFV~1;F8J*`a%#wR(AH$@r`{cX>m(wUwtY>^ zwqk3_GRi^SIl~0XXe9g(;;HLMc?u!2nQHpfgUx8ev3RIA;c7>-PpkHJLrq8mhrDdR zO+M9&oe_hryCkC|xx8?ufo8Xsv@MFWGAPt^4zrJTtgK_ixKilW=-ekX7}d{_bwXQC zJQFxrD1;*VGnl}ZkL0c>yx(Xo&z?r*Y#eBIS)sEURS?8{x!%YMD&rf#)jV@%|bt^5%$X&#UvDB^E`Tjps=w`ZL~8E4vK}iF zuOHr2omZUyRt1<)q`mhz_f1Q;pm6qJaq3l#mbb4ttfhO<0Gd{8loN-c_=0xdJzWB` zv&rk)cjDIEO%2}snRtJ7+E!-i;`Nf2-GOlkTpRP`$36rbviXJJ(Y?HZ-+0md9uvWv zTS)#`g%S-(WCk2Ec0+(-9v!?9+eM?`X==VZa60%(v!NZ;NDiSBL&Ao?(!D3&_n@~LbZXN+Nc!vQC`rW_Nv@kf zuyfWf^L21OZXAyXSngUZXCIpLf=w+=;JbRnuuV7BUi*5Cr@3bndc1N=AbDhGsjE(1 zXZw%O_%qur>~r?GOSHSwPp6Y9#TS#D9y!*hFo<2mUTh4l(}|#F30p*Mc~6IQ+F|E$ zEvd?@$t|13#5#-Qv14qH{R4(J+upheNQ+;jy}e&n+d;wPt;WspnYgxg^>qbuACk37 zd%B@0VOwAMFfHHWhEsy_IuE$)b{yTdlbPa!%a!Da|9nl7YH$bXGQ?3(Mf(Ac6we1Hf{@5+F;KRjAwZ0q6LtpP~M6`2I7d_nAdV zL2a3l-{U85fq{yB02WYCQoXb2P8w9avD<)ZB7kpQN0HgU-HNG*$B@i*xg2CJTkIf% z2vdxt(YWRA9|*c4&1x;N@@S&6pXj~&2hsxB&eodM7*i(2v2WzprR4`xqFwcy-bS_$ zX-Cd3C&v48>zs(WU)^#pvq<1Cs}89Mv+8e1cSo=g2ZWBaua;VUA@)DY9PG0@RSW%^Gn&?9MWRrxp%5YVz!PEnxg*X0;CYG_f@uXNc3pZP7G%Bl|w_ zcoE)8#A~oIs+R3g0-`PP7spj6XUOV{0k2DcvPzJJrbnccR%W{9_#<-k97bWH5IY!$ zZ4G_JY+bsUdFG~}XX}U3*sZKf8DkdJ^P)X7*!d(0JHDctwvgYTR$NjkvGBwrP0X@{mK7tv z_md8(mv?X&iTEE}kJBA~o=H)C|F&nwws4qD6o2*W1uVc6EF(P-%$*NF%V$fvnotB) zSV-ojjE*g4CDI=y{cdZJP1Jxz9X{14nF124gp?g#{?;*F{plS-D^ExnIFfsdEeY5 z-hp@Yqtj}$3~ijB_L}+dwO+Th`0RJ2r<*%0j4R_`ygi-T;w~Y*;Vn(kP?TiqvqSQy zmE?+zC#oO^cedw>viY-U3gq%SHC?!Xo$t#oU*j9M^&ri(n^0H664K_L*Ib@$q5#q*c1N_4aAzdcPbPBkdRr>Z*K54hg1Xe3U zddT~NzwCN*QslxRUU|uN^m}iv$AU>IgF&H%Fc+9lHGg(L<$*GR>%@G3%t$?-OuYS0 z6y=%4xT6|cQPg5}_65nSM#;^ZkQ7!i9plvk10e(-@tkirt{4wg$kDLb<**xy0=v!4 zI&=)5@9W;&tWNva6P(e5r^U*07v9oOZ~tk%6o`wKwHs>+{-Takww-OdD?d&%#y%#S zC8Y2#y}D*H;@IZ7K#{(eir2Wq<-KV#vS>SbiQda+NWyM$$b6h`+eUdQ;-)Ucnodh; zdG6r;_FI7#ME|VTAyfT>^RYvuwk{vE^!;rni=DyO7HjwlY5cS_$G5&+w$rz{FW9iZ zGwXgNC8{rps$(HJwJOO_=aU!*41NOQ!N#B1L52b1LIR4XJn(>6Q5>d47?KI;VC63`1ZAQ218GD!mxTaas@>+X33&yW3UKNxD`;_A4 zF@N#Ey-M38Zj5k`=-9tw0n7d-Cyn|Q1l%U30X9RvOq-puUWn7;mnX8MLAVUS#B{9vtHp72P+~2~8{(dL{%7i1&K_3&HF4b8@k5F|fZ$nwG_y*{cro zaS56yf4RRv3Pg+jmDhUF+wKuiOA$zfPWuuaXFVx?|BlQx=Uvo^=mHiDWswFZo&xt4-M$@~tBZyz)iK!p?J#+kROPVZ?@{H^}& z{UQ51&*U{~kwTnXq}TrG7K5|4mRNb|c7LPUNQ5!R(0T^0LMe03@2bg&>FC7+g*6fE z!`+;8vhSUmg4Wj}L*5Td$1I!F;faTRpB77tGyQc8i_=kDuD)_e?xaH1SC=kYHc@)U zie73x!SWof!gnK?6gWYDtXPb|W%y`kbGdND^Kk^9uvWzH`o!}+GM74;eYGAbBXV;6 zLoz>bG*3W_uYm+ul4wZg>QliL3M5KD#VFT51aH={ja7~zK^Gw6x&Eg}>8*g_bM1Su z90z0Z1#N|eqjmCLi+A(91-VU}+tnZ~4!{5(({W9h*K_7Gq(f5?Kbb*5!O5HBL0R38 zSX9k)ue@y9c|Q%)Ku_qt)W70kE#cH&>x++n*#Dq__e})l9Ol^H)X0O2oBguQ!6nL$3=d(bFiT z!88SYczAmL4cygl(L@d3DHXYtIH+k1 zQqGSjjE{?J5p5^08QBgaB}-C`CCgtl!-cof_KHJLOD}9$PyNVL@tUnzwaqidbtKcT z57<;*uClP1$X}NDvASzjh`Idy`24d?IeaCjld(r`mNE;SmW}cLO28^^=szrP>sf{& zxrDOiNuT*J+>W{T{A=8IZri+@E9l3Qc=q!;2GDdR_;Z&Jy?z+G)nwUm2$21eV#qaW z+ZRTJ6PSx3B&l!mKm$YJ9XDo)!R<_n6T5^2@Jqj{5+=EaTlT*DjTD?1F-y=ZgLpOq z9bUF}1P9W%(4ZqZ7uMJ=5SUjCqfGVLxni?+_JGpr{n;eICuA3k#$g7CPxKc^s-r@g zjeouS7$d*gbf^97OLt8?mPSG5pfeS~_z2BrW)Vqveab2O?%S77oK2v2#24o0n@=mI zkX_y&J=N=Bg}LUdY45S>YXZ_<%*8E3ULEj@>wxI(JTxcGcpl_abUOJG=1NIAC#Zbl zB{T5^nIhoR9Oi#B)=+CQ1GQ-)CKoQuI^0C=e(^wZU1v}<-nmmNmw9(;(RPu;xakxu zI~zLIb2fGJN$%)h2WrpttVR|tvz%SHJP*JW@E1(RCVdd_Cw%#pGU;>Q>I7EWWwxvD z6X|FXLYa_fxYm0(Zdm(iOvrR-L>j)gFWYRqIEYi?6j}7jt3yUmct_43l)101DtJM3 zU~mxcGO56y?&IFr)6D>V zC$MlxZby){lpWjCIki5|@4Rk7n{9Rs8*^hBCT`}g#F%{Y7_gzH+Pjjf6PfeGJEZN# zNWH>4y-k-WA>Cn%8%qZ|PwDt?cwy%tbkVyq54JXhC=21L`+ z?Rm;raS~uf;m0FAkyX=Dg}AJ}4G6&}A*szaH(P&4pd-z2m#9FwzRtxB>@@lE!;F{* z;!>{?=+_mexHWDv!*JQXDoA~C*bUE%-8+_{Y{12comNzDxs~|yp52K+z4aTo6O^+n zJbuHGlLtBmpRHT0XN`!sox!jZ!k&9zyNn~%+)l#GvsF8?Dzub|sHm5$rNigZUff)H zu*UR)bshubC^AgqdOxh~;gh53Dp4Mz+TjxX3>X)<>8!x|3-;L3!6p&PDWfVEWgxJ?&2u2>=aRSuTGSLvA$ZObHzV;o zrfSU$s5Vy=z?1H$(N0GuPa-m-z ztLGq#1+YkII?5?WZdtOlyzG;lx3SaqwjaF!kfS=fWyC_Sx7)U;U)vMjTk~tQ5pV${ zhXUh^pK((gN~;+LVV^U~@;gDFxlqcdnRs^OI!B2p4PD6tdDe_XcsL8n{^c;)<|x2F z`k|QX3aC%Q=S3R&?$_?ATrtL6!_Q^L}RH~ zdLh2E5{65U-IcHN6b9Tj(Rb#yFyOL#0**rK@FL?&_DPV^sCmV4Fu*bZw#*dv?} zu=9+6XB62Sh=&~xS|OQEd&Uo|>}pdT%h*PG08R07XPEtwT*z7_+RlEs0wCKZ1@n$N zjMqYP^RVVEzQB5utkw@$n~;F6Jo=*LKz+q(i$bmhX8f+ZXa6(Nf<6HOy!>4H5v`gc z&VG?wlhEE7W!y;+^@fpql5Jmoy!E5~EY}mJ4RPF^C8UCoY)!&eVSnaJWZDdKh@dgR zm+b8Ab<8{zUs$oEoP%&HJIf?9bY4H25Cy~Iov&$`euvhf5+uUCa<4|_3WWF>4xRAx zCrpiD2nuyDhk?J=@43^lmpK;hiZ9FpW$0TrOr<*k7)okrGNXe-{HOh znRP8yR^nePyBt9T6uG?u17N&*5TBCkd|weKqiV-??ds1!1%Ssh5^kj?s%dA@8)EY# zfpICWg*$QCjLlbKjknQGvr{$9xuz`fTn;8r^ZB&oQMI7F!b_n7pPMn~gZ5?;)|lbY zFFI>K#HACsF3GGbZs)kzkBPYryXXyw1jr6&0OGI&JUPEbw{_Loq^iejADCWpat{F> zoSStch)e3L0V+^?UPE~$IM3KDK&nH76R4X7KT^;X0x~AJMjVsLC(ov76g0KWQODSE z;h6GAmA-Tc&q#!RiAOoBnCv5DG#*_oja0SdkS2-c1Hg&!?FlgwKc7M;%S}4HMLZz} zL@~ZM*#>3TbggFLfg>jw@KvL=FvEzT?C<9s=pU)g?1}xLAj}>YM3Rl{4qSZSlmz)# zDs7HYB~zy$)k@X!+12J1p|4n5%QK#9%emC~F&&>--Fv*eo3c0BR!jSd3m(nJl2Hh& zh1`zwz0=c|7EF(F?yN`gWS>ui0vMMpc=^R#7Ouk5NnAhd9`A5V485!v->r{+w+e!*$tn3hXQaDewKQf9L!U^GRsCh=gijt znI^xH2Yd>_Y7CmX%U-TP!n9lq*5E7d(SOV|a}k>Ne*7|02oDq^BR*}Ywrnu1;wW5N z{Zg*So(|13r)azxtw4L5=b3_W(K+kt)rb(ho~wXo#~Njcl%qr0-oW;T9k@T^{p{3@ z72``G{@96So$|t@G?Dhv_8XBgdIzl@G8ScIh&tYOy)=;JLa?1gZg;yt1>mj;fo!JE zZ{7MHyt2|(FO@BAYK7~+{i)UaRB2;QqgW(n8ySU{jQO1t@G&=04_GI_QpoG+(T|t~ zDIrrpQfUs0r_6v=LVvn9u%&=GgKicp=k^Mp>Zl)VFQD`kR)l8U!v<6f2FePQaKgzg zfmO>@wk%Uc=+r^L2()8n8AKT4<9jbtgc8P%|9H(J7%vMrUru(gpi*u7Qy6Sek@00_ z;SQ1=p6+3jg>iF;92=cfpjP#jlSWtW+5(ZIeZXh)*DC~ew29ti0#^3|K%YZi?#50l z+h5Vc4#Pa0l^X5j#A^PSQ?lr7&`T1J0%5$0l8UjBDik&7A$P=LKzQKE0 z$ehG=IVRhz4hej@JU7p{z_bf0lb-%&rxrO&m?*_eMg&JA+2o;E0H?$hH<0&4&h#y7 z{yP@H=zYCJ{b>apyN;3y>;BFUXLNH|a3kKUkVOn78tu#9OjkflM43j@Qi;#qRztJA zg*vP!Y7^(2C+)6G6KhE=8xZM0nqLhS`iAf_RI6d79?qn04^7p z-l#dI#qu}zAx->69Un-CVjFqBEJiBx)Bq5tKV`^TrAUj_~$U^7jm?;m}>rCs3CZy6^ zN7x!#o%S=X$LjVbK`*B5+B{*S^S)RT>VdjWV$YYLJKmB}xhY371~%5qrY1J>spL82 zctEmqr&urpE{ilSycQyhi~B#S*bjsmx)0 zB<>0-;VHcj3+B*4&Twz1$fx_x>-5h9vsis&)iwpmxA{Ppthq$xqT1ATxA{M}sp zD%TJfUud?vWgorsegYHg1ZE!F5m?C5Y2SxPRW@8UM|=#NUxyD50dH&+$-S|oo67%z z+v<$dAf0xA$9RUG3HI@C4%+Oq_Ja&0qKtFJCI3K+ z^~VkUbj^#FQ;#P5yq9Ps9zKoKw z8{Yd#Hj>IH6R7!NATn)PjF8@m3#wzxsw9boW&`xh?Q$p;l?W0LQQTcGB_C$$Aj#iA z$Pa;N>gH|`!{nQ@t2eu#VxUtMnYITt0AwG-_Y%n3wW4^=P~b>KPo@q8bQsKL;sy?@ zq^0t6@}c5>aq;`&pm==CabXM!Pfti_3JMBg`j1lge&XNmtCI%Y;H2fizyK8zDiww) z$o#z)3Q%Z-VannPoZKO(ZR54cx5|#!@p;)4{tA1=%x`=)l-2jnq|#y3j4rY6_+2P_ z&g+c~&V)B?EAC}OmC5Ami;60LVWFe*ojQf0f5#42NOWplMI6=UO%0n{A*mw@pW|*^ zvYSr7fBSD@p9$@xTi5&zk)bmRMb$vCQ>-epC>Cx&sze@F-gFmA-yr+$4X%t}y1q}XsE0c@ z;i1@(#=z{b2t44wDSsAkpAF4>!h0V+iMW@quXO<7M%+Gq6rNC75PA8eTh?3;GC?awu!|p_((-f6EUhlIXo*Y_kJ+`UoHzJ-|n`nL>0!izRbN+~k3& zcJ=6P-{d=DVV&`Ur{Gi`^RG#4Ut#ryHK^7yw2Kf$qzfcF9=25?ExltePSh8r5K`fk&-cfYwcHxn@YOAj zFGOJwhe&on5fupXo87)sf4XY`$(d1kQ#TI5ZJ;`GLZBmUqKF=cviByCYB;n;kzyGc z-X1|k+)lk6aW4Q)3D@E?A}XwL5_AEx(9q^tp!dEJM*f6o6JVPokzFzU+Wkmh@&S+pE`NT9LXkLs zAL7vK5^2G37_0VGA`b_;H{Y@^-H)u>`HrAe65zuLwPHhU|G;qQC+>)_g{U7vRDbo+ z42P&uaDM+502U*XrynlAKe70q1c6W8^&pw~^|zW%#klKHex6m(K2%&ge7F*CD3+q8 zaRa6D_#B)6HRQ`ts0nT46#h$A_%Bi35JGJFC6D}1$=pZz5o~bXw7&dt8iXP~5SWgO zUr*|2dh`&^%agw^;&E|ACy`gT8<_I&p~ubt@izQ^BFB`c&(M~CKO8LQ|G`lLw3qe% z`WYYD7B0}CBqM%D+5h3_L5PBLJ!CyP+6o&Dzx|h%@tpzy`=W+Ou=cgMlX0i>GKwK? z-?9BKU}IW;Gn@XtIof|KIw#aJ?r8e2hr`j=LKgVvq@%ub^==5vKs!dh99b*LCri^1 z?culo5{A}CBb@R~dgK#{v^#0mCs6cl&-cH$3>$;2@=E;ev(4%1r$FQ?{I|+-CI0X5 z{&x=gzk@n_`TsNn9q6Tl{~7lQu4x`Dj@9KCmru~4W{_Cyar11b#T~&TDBqXA5AScu zs>jXW_xTj0B>1r2-?2D~jThGPU$PSTaun*F0+Kf%9sDf;bxZ-A;KkhUPnwYH1S=-_ z`Je0b&vp9eI{kB<{uOcm56e!nPud;+9Sitxsb~MmR7hU>modWcUg@7q1ufw}h2L+( zjQ{WL;Xl{upX>BbAN5~`w)}IQ{<%*74cAF!7fR*aKWFcsv-jU{_5e3S+`9gO#QuTA z{{Ib$UB>N3tvmp~f(@YCg1yZ0dNNFtRL*TzHxbprbxqcq2PQx(V<3PVkF&<&QOhzK~6nq*d>?_#0k}UfBud_9l|2sf&URv z|KGB?`@4xnW(0rZ$!)CUwSxPjl~0k^fbY7`*WTaH`JkXMshnXH#0Oa5&+miJn*;@F z)x#HTt+VLdfpifL{!1|7U3-em5{h4EvfM|!sla_^q{li^*~inqG5N-)i{NJm-1~D8 z^~g_{=&pQ9|B8v5jo?=4Yg|_nVT7Q;zR3^%>mB|6vsYbx|Nk&F+#Ef~Bm0E9HEjZ8 zB8G!Y9<1S(d9bTWbm8o(WFL^gygbOs%3u83(s1efCq)lOp1>%0Zzpr>EmPsb5% z`4;T#MsnDIqQBeMd#UAMlL46jt-iLBQYFgJ9hZ5@#JvvmJosqubQ)^)ZA*)`r851* z^)gZJ4|L(L{>%38KMpf3W3ocmx{rv=PCIygW3DpgjI2^wNK)^$r0(VSPk zWEup!IAAUjcB6dR04 L?rIu(MAJ$lX^MOeQH|~BeC~Oz^jJisQ(6aP_h`&Aug|W zf;|ol4cEZaWC%q0TZTAF;$9s(m+Unpx~1b zAT19>6hTTsetx&O571r&f-8VMf{;9hZulc&EBF~ZFoeyAX7%9x5R%xsQDy~sHXJc#fFWnXO+&EF8MG}lYF$tjl&7;|BQ7*^j?#ao zht)y|PXzJ^5p2AE34vc4m|_H#MFi}jloNf#FWaZy_JnaM^45q~m%}QMpMNg$rLKYJ z9Iu2$6J-OBYd94tQaYb=VHE_BH%j#Df-wnwpk@H-($G~Li;r0|sLm1%#p-t84`V+L zUPGZakMFHi2^j=N0V3hXfc8_fgsUtVBS^1t#T_Uh7p6g9_%^l?hP4nQF>v0+fT`iI z3cKF2$Y&c67#N6f@i{Lhn&2?)jRBv4UGnG7+veI<#E}7*D(#Yio?r%&e*0{8G|73+ zy$D#upQl~nh$!!+XZ5w%Xb^+(vO4K0Lg0ypRb*D)97UW16M~fGFFs0t0(OF3Su}{6 z+F%x%ftYWR)xdqpU}z59|Iojl9}$?Tje-}Hz>EOb@|s>OVxy5|f>vTLnRZ2?+-Y#0 zCA`4=Qi5o`wC)*H-7q4jOFV}k0oOG~Fp~t2mMGg4ERBkssKui!cb#u*il6fuead*s2)}y-{2me8!B9P8 zIm(EA)0&|RK)JFBKW>AI=mGp&GcSRBbR*`R-c$i+eLk|7F)kcbFh-U$?LrNCN1tk-7v z(7?*OFns`wyTEKK%;mk89V1NXKf&m2^(}4kRtOko`H%@ICLp5#szn8T@ zT4Y5{sgyma9kkfh2J?S7I@TR0A@nzWcCV5&+|@SppGEBd>>Xe#8m<6~6}=joq7}Ig zXYEFm?1hF`>>zoER{@%^{(IEH^Ht!t*L}wlOj7#b2qSe1*(DRaB7oNY1gOR@F8>1S zn8@bti-H?eU@Q2GisZFjMnWC9(IWP6Fg=y*LX_Ek-Qa{#HW!iI(p#~f|Eua1Lf--f z(_O_<(3#~xVFa8SA?>FO;SYX(I0FH{67i5&gVYcMS{r|{l9>*mxC5;d;gN=KffkVn zd~mQz?7Ai#O#)F64RnkFJ&VZoiHVF*_8|i`#sdi>;1+`49@#)FbnmW$u=`x-b%(m& zJ{-@g>CMrWL^w>B6{8W>Iha-YS#q_`A!OP4CXo$(RNOrv8%U^`Y#>I7uB%W1;*oO? zenxx!kl=oD1d6a#;N0BPSpns3aTU%WXj;mE;|7SPl*V`LiXBLtD}|#nS=%T2J9XGlRRf+KB^neyM}==HApzt zYD^u##)nWaBVp7*hTOji!fK8hP&Y_Gs0ERSlHZJkMwJKDr%;MwB&{J)ZGqv*+6#8$ zqLN0$uGPJ0=7a`28K}q+;K9_X|+2YFW9vcn!0de2hc`InXjCbsi zIy-{M{6K)D7}!>WE#9YcppA}UrW;}p7n&b*qBbpXi|zo06EPpXnG)S<7Xww<)%V;s z-!4fDv)Y`j!r}z&Ed0kT5~~q4Dw2{r={&F?hgy80NgognNW3}zrJzAab+SDeDW!6G6wX)Kp0@Njw} z0qG-72lg$|A?CSb-}I}8_m(G=CKB907egJIRt|%^LdOxON04o$kSVmnpH{8vwf-ay z+isw=Wmyi~Xf@E*d8bW*(gl290?;dgB6r2WcO21&9tHm*pe$Qfw{IRKBQ*WP0k5u> zytHePexDyGSXX-m4@#l_c*Lw%G@inZHanmcTMx!2JdwOokGi19^tbB%?JH6fV5=Sz zl!Ta#b-2iH;~tPOTQ3mUkzv6cBPi zvaR}M8#GIZKCTBZq^zZ$lc5vD@>uWggBOX9htd%~bR-VsB9?2=LSoi)W7W20$Z5lT z7ml&Wr-cD)m^{jxEg=f6#YelZgFY!bvA|T$3gWn6x*!G-iM9b{9S?Z)1e+oBcVHWy z)Bo9^Fzsa-1NT1N7>d+RglCL69f1ZTZw8ZM;t~i6`?prIfA#3kd2U=kn=kVpJ7FAk zKgRCgBjat#Oom|-2!uA2=ok%JJ{nDX_d)P*MKK_)C_Sq8p!2uq4LrBu6Vx>|ZPm=n I_y7980P_dRQ~&?~ literal 0 HcmV?d00001 diff --git a/tests/e2e/servers.spec.ts-snapshots/recharts-chart.png b/tests/e2e/servers.spec.ts-snapshots/recharts-chart.png new file mode 100644 index 0000000000000000000000000000000000000000..4fac0ff2c2daf7e525c9060bb5f33c25e41218f0 GIT binary patch literal 31191 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 Ul`GyhLoSKb)pS(zRBVF(2iXMJO#lD@ literal 0 HcmV?d00001