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 780bf796..e6e832ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -442,6 +442,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": { @@ -479,6 +537,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": { "name": "@modelcontextprotocol/server-scenario-modeler", "version": "0.1.0", @@ -1023,6 +1143,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", @@ -2578,6 +2707,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", @@ -2616,6 +3020,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", @@ -3758,6 +4169,10 @@ "node": ">=6" } }, + "node_modules/cohort-heatmap-server": { + "resolved": "examples/cohort-heatmap-server", + "link": true + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3785,6 +4200,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", @@ -3943,11 +4367,55 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "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" @@ -3956,6 +4424,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", @@ -3963,21 +4440,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" @@ -3987,7 +4514,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", @@ -3997,16 +4523,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", @@ -4028,7 +4616,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" @@ -4038,7 +4650,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" @@ -4054,11 +4665,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" @@ -4068,7 +4705,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", @@ -4085,7 +4721,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", @@ -4099,17 +4734,27 @@ "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", "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" + } + }, "node_modules/d3-time": { "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" @@ -4122,7 +4767,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" @@ -4135,7 +4779,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" @@ -4145,7 +4788,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", @@ -4165,7 +4807,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", @@ -4195,6 +4836,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", @@ -4205,6 +4852,13 @@ "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/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -4224,6 +4878,15 @@ "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/devalue": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.1.tgz", @@ -4634,6 +5297,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", @@ -5111,7 +5783,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" @@ -5282,7 +5953,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": { @@ -5450,6 +6120,10 @@ "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==", "node_modules/locate-character": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", @@ -5555,6 +6229,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", @@ -6137,6 +6823,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", @@ -6227,6 +6930,12 @@ "react": "^19.2.3" } }, + "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", @@ -6237,6 +6946,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", @@ -6250,6 +6990,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", @@ -6303,6 +7085,12 @@ "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.55.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", @@ -6364,6 +7152,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", @@ -7118,6 +7912,16 @@ "integrity": "sha512-k/CjiZ80bYss6Qs7/ex1TBlPD11whT9oKfT8oTGiHa34W4JRd1NiH/Tr1DbHWQ2/vMUypxksLnF2CfmlmM5XFQ==", "license": "MIT" }, + "node_modules/threejs-server": { + "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", @@ -8016,6 +8820,32 @@ "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 + }, "node_modules/vite": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", diff --git a/tests/e2e/servers.spec.ts b/tests/e2e/servers.spec.ts index e554bc3b..fe3cf2c3 100644 --- a/tests/e2e/servers.spec.ts +++ b/tests/e2e/servers.spec.ts @@ -23,6 +23,8 @@ const DYNAMIC_MASKS: Record = { ], threejs: ["#threejs-canvas", ".threejs-container"], // 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) @@ -42,6 +44,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 00000000..20f8eaac Binary files /dev/null and b/tests/e2e/servers.spec.ts-snapshots/d3-graph.png differ 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 00000000..4fac0ff2 Binary files /dev/null and b/tests/e2e/servers.spec.ts-snapshots/recharts-chart.png differ