Clean up examples (#182)

* Centralize example server transport logic in shared utils

Move stdio/HTTP transport handling into `server-utils.ts` so example
servers only need to call `startServer(createServer)`. Switch HTTP
transport to stateless mode and remove legacy SSE endpoints.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Make example servers self-contained with local server-utils

Copy `server-utils.ts` into each example's `src/` directory so examples
can be used standalone without the shared directory. Updates imports
from `"../shared/server-utils.js"` to `"./src/server-utils.js"`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add `integration-server` example for E2E testing

Add dedicated `integration-server` example (duplicated from
`basic-server-react`) to preserve E2E testing capabilities while
allowing `basic-server-react` to be simplified as a minimal example.

Changes:
- Add `examples/integration-server` exercising SDK communication APIs
- Update E2E tests to use `integration-server` instead of
  `basic-server-react` and `basic-server-vanillajs`
- Use server name labels instead of indices for more robust test
  selection
- Remove `basic-react.png` and `basic-vanillajs.png` golden snapshots

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Simplify `basic-server-*` examples

- Clarify comments
- Return plain text from `get-time` tool instead of JSON
- Remove simulated cleanup delay from `onteardown`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jonathan Hefner 2025-12-19 05:44:27 -06:00 • committed by GitHub
commit 9e9f5f0210
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 1748 additions and 475 deletions

View file

@ -4,7 +4,8 @@ import { test, expect, type Page, type ConsoleMessage } from "@playwright/test";
// Note: CSS modules generate unique class names, so we use attribute selectors
// with partial matches (e.g., [class*="heatmapWrapper"]) for those components
const DYNAMIC_MASKS: Record<string, string[]> = {
"basic-react": ["code"], // Server time display
integration: ["#server-time"], // Server time display
"basic-react": ["#server-time"], // Server time display
"basic-vanillajs": ["#server-time"], // Server time display
"cohort-heatmap": ['[class*="heatmapWrapper"]'], // Heatmap grid (random data)
"customer-segmentation": [".chart-container"], // Scatter plot (random data)
@ -16,29 +17,22 @@ const DYNAMIC_MASKS: Record<string, string[]> = {
"#memory-bar-fill", // Memory bar fill level
"#info-uptime", // System uptime
],
"wiki-explorer": ["#graph"], // Force-directed graph (dynamic layout)
threejs: ["canvas"], // 3D render canvas (dynamic animation)
"wiki-explorer": ["#graph"], // Force-directed graph (dynamic layout)
};
// Server configurations
// Server configurations (key is used for screenshot filenames, name is the MCP server name)
const SERVERS = [
{ key: "basic-react", index: 0, name: "Basic MCP App Server (React-based)" },
{
key: "basic-vanillajs",
index: 1,
name: "Basic MCP App Server (Vanilla JS)",
},
{ key: "budget-allocator", index: 2, name: "Budget Allocator Server" },
{ key: "cohort-heatmap", index: 3, name: "Cohort Heatmap Server" },
{
key: "customer-segmentation",
index: 4,
name: "Customer Segmentation Server",
},
{ key: "scenario-modeler", index: 5, name: "SaaS Scenario Modeler" },
{ key: "system-monitor", index: 6, name: "System Monitor Server" },
{ key: "threejs", index: 7, name: "Three.js Server" },
{ key: "wiki-explorer", index: 8, name: "Wiki Explorer" },
{ key: "integration", name: "Integration Test Server" },
{ key: "basic-react", name: "Basic MCP App Server (React)" },
{ key: "basic-vanillajs", name: "Basic MCP App Server (Vanilla JS)" },
{ key: "budget-allocator", name: "Budget Allocator Server" },
{ key: "cohort-heatmap", name: "Cohort Heatmap Server" },
{ key: "customer-segmentation", name: "Customer Segmentation Server" },
{ key: "scenario-modeler", name: "SaaS Scenario Modeler" },
{ key: "system-monitor", name: "System Monitor Server" },
{ key: "threejs", name: "Three.js Server" },
{ key: "wiki-explorer", name: "Wiki Explorer" },
];
/**
@ -72,13 +66,13 @@ async function waitForAppLoad(page: Page) {
}
/**
* Load a server by selecting it and clicking Call Tool
* Load a server by selecting it by name and clicking Call Tool
*/
async function loadServer(page: Page, serverIndex: number) {
async function loadServer(page: Page, serverName: string) {
await page.goto("/");
// Wait for servers to connect (select becomes enabled when servers are ready)
await expect(page.locator("select").first()).toBeEnabled({ timeout: 30000 });
await page.locator("select").first().selectOption({ index: serverIndex });
await page.locator("select").first().selectOption({ label: serverName });
await page.click('button:has-text("Call Tool")');
await waitForAppLoad(page);
}
@ -113,11 +107,11 @@ test.describe("Host UI", () => {
SERVERS.forEach((server) => {
test.describe(server.name, () => {
test("loads app UI", async ({ page }) => {
await loadServer(page, server.index);
await loadServer(page, server.name);
});
test("screenshot matches golden", async ({ page }) => {
await loadServer(page, server.index);
await loadServer(page, server.name);
await page.waitForTimeout(500); // Brief stabilization
// Get mask locators for dynamic content (timestamps, charts, etc.)
@ -131,52 +125,46 @@ SERVERS.forEach((server) => {
});
});
// Interaction tests for basic servers (React and Vanilla JS have identical UIs)
const BASIC_SERVERS = SERVERS.filter(
(s) => s.key === "basic-react" || s.key === "basic-vanillajs",
);
// Interaction tests for integration server (tests all SDK communication APIs)
const integrationServer = SERVERS.find((s) => s.key === "integration")!;
BASIC_SERVERS.forEach((server) => {
test.describe(`${server.name} - Interactions`, () => {
test("Send Message button triggers host callback", async ({ page }) => {
const logs = captureHostLogs(page);
await loadServer(page, server.index);
test.describe(`${integrationServer.name} - Interactions`, () => {
test("Send Message button triggers host callback", async ({ page }) => {
const logs = captureHostLogs(page);
await loadServer(page, integrationServer.name);
const appFrame = getAppFrame(page);
await appFrame.locator('button:has-text("Send Message")').click();
const appFrame = getAppFrame(page);
await appFrame.locator('button:has-text("Send Message")').click();
// Wait for the async message to be processed
await page.waitForTimeout(500);
// Wait for the async message to be processed
await page.waitForTimeout(500);
expect(logs.some((log) => log.includes("Message from MCP App"))).toBe(
true,
);
});
expect(logs.some((log) => log.includes("Message from MCP App"))).toBe(true);
});
test("Send Log button triggers host callback", async ({ page }) => {
const logs = captureHostLogs(page);
await loadServer(page, server.index);
test("Send Log button triggers host callback", async ({ page }) => {
const logs = captureHostLogs(page);
await loadServer(page, integrationServer.name);
const appFrame = getAppFrame(page);
await appFrame.locator('button:has-text("Send Log")').click();
const appFrame = getAppFrame(page);
await appFrame.locator('button:has-text("Send Log")').click();
await page.waitForTimeout(500);
await page.waitForTimeout(500);
expect(logs.some((log) => log.includes("Log message from MCP App"))).toBe(
true,
);
});
expect(logs.some((log) => log.includes("Log message from MCP App"))).toBe(
true,
);
});
test("Open Link button triggers host callback", async ({ page }) => {
const logs = captureHostLogs(page);
await loadServer(page, server.index);
test("Open Link button triggers host callback", async ({ page }) => {
const logs = captureHostLogs(page);
await loadServer(page, integrationServer.name);
const appFrame = getAppFrame(page);
await appFrame.locator('button:has-text("Open Link")').click();
const appFrame = getAppFrame(page);
await appFrame.locator('button:has-text("Open Link")').click();
await page.waitForTimeout(500);
await page.waitForTimeout(500);
expect(logs.some((log) => log.includes("Open link request"))).toBe(true);
});
expect(logs.some((log) => log.includes("Open link request"))).toBe(true);
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB