Skip to content

Commit 3a07525

Browse files
ochafikclaude
andcommitted
Merge main into ochafik/fix-issue-142-bun-optional-deps
Resolve conflicts: - .github/workflows/ci.yml: Keep both WSL/git-install tests and new e2e tests - package-lock.json: Accept theirs and regenerate 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2 parents 1471d29 + 8550545 commit 3a07525

53 files changed

Lines changed: 1900 additions & 940 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,32 @@ jobs:
119119
npm install "git+https://github.com/${{ github.repository }}#${{ github.head_ref || github.ref_name }}"
120120
# Verify the package is usable (ESM import)
121121
node --input-type=module -e "import { App } from '@modelcontextprotocol/ext-apps'; console.log('Import successful:', typeof App)"
122+
123+
e2e:
124+
runs-on: ubuntu-latest
125+
steps:
126+
- uses: actions/checkout@v4
127+
128+
- uses: oven-sh/setup-bun@v2
129+
with:
130+
bun-version: latest
131+
132+
- uses: actions/setup-node@v4
133+
with:
134+
node-version: "20"
135+
136+
- run: npm ci
137+
138+
- name: Install Playwright browsers
139+
run: npx playwright install --with-deps chromium
140+
141+
- name: Run E2E tests
142+
run: npx playwright test --reporter=list
143+
144+
- name: Upload test results
145+
uses: actions/upload-artifact@v4
146+
if: failure()
147+
with:
148+
name: test-results
149+
path: test-results/
150+
retention-days: 7

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,8 @@ bun.lockb
66
.vscode/
77
docs/api/
88
tmp/
9+
intermediate-findings/
10+
11+
# Playwright
12+
playwright-report/
13+
test-results/

.prettierrc.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"semi": true,
3+
"trailingComma": "all",
4+
"singleQuote": false,
5+
"printWidth": 80,
6+
"tabWidth": 2
7+
}

CONTRIBUTING.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,45 @@ Or build and run examples:
4040
npm run examples:start
4141
```
4242

43+
## Testing
44+
45+
### Unit Tests
46+
47+
Run unit tests with Bun:
48+
49+
```bash
50+
npm test
51+
```
52+
53+
### E2E Tests
54+
55+
E2E tests use Playwright to verify all example servers work correctly with screenshot comparisons.
56+
57+
```bash
58+
# Run all E2E tests
59+
npm run test:e2e
60+
61+
# Run a specific server's tests
62+
npm run test:e2e -- --grep "Budget Allocator"
63+
64+
# Run tests in interactive UI mode
65+
npm run test:e2e:ui
66+
```
67+
68+
### Updating Golden Screenshots
69+
70+
When UI changes are intentional, update the golden screenshots:
71+
72+
```bash
73+
# Update all screenshots
74+
npm run test:e2e:update
75+
76+
# Update screenshots for a specific server
77+
npm run test:e2e:update -- --grep "Three.js"
78+
```
79+
80+
**Note**: Golden screenshots are platform-agnostic. Tests use canvas masking and tolerance thresholds to handle minor cross-platform rendering differences.
81+
4382
## Code of Conduct
4483

4584
This project follows our [Code of Conduct](CODE_OF_CONDUCT.md). Please review it before contributing.

docs/quickstart.md

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ import cors from "cors";
102102
import express from "express";
103103
import fs from "node:fs/promises";
104104
import path from "node:path";
105-
import { z } from "zod";
105+
import * as z from "zod";
106106

107107
const server = new McpServer({
108108
name: "My MCP App Server",
@@ -130,17 +130,22 @@ server.registerTool(
130130
},
131131
);
132132

133-
server.registerResource(resourceUri, resourceUri, {}, async () => {
134-
const html = await fs.readFile(
135-
path.join(import.meta.dirname, "dist", "mcp-app.html"),
136-
"utf-8",
137-
);
138-
return {
139-
contents: [
140-
{ uri: resourceUri, mimeType: "text/html;profile=mcp-app", text: html },
141-
],
142-
};
143-
});
133+
server.registerResource(
134+
resourceUri,
135+
resourceUri,
136+
{ mimeType: "text/html;profile=mcp-app" },
137+
async () => {
138+
const html = await fs.readFile(
139+
path.join(import.meta.dirname, "dist", "mcp-app.html"),
140+
"utf-8",
141+
);
142+
return {
143+
contents: [
144+
{ uri: resourceUri, mimeType: "text/html;profile=mcp-app", text: html },
145+
],
146+
};
147+
},
148+
);
144149

145150
// Express server for MCP endpoint
146151
const app = express();

examples/basic-host/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"@modelcontextprotocol/sdk": "^1.22.0",
1616
"react": "^19.2.0",
1717
"react-dom": "^19.2.0",
18-
"zod": "^3.25.0"
18+
"zod": "^4.1.13"
1919
},
2020
"devDependencies": {
2121
"@types/express": "^5.0.0",

examples/basic-host/src/index.tsx

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,30 @@
1+
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
12
import { Component, type ErrorInfo, type ReactNode, StrictMode, Suspense, use, useEffect, useMemo, useRef, useState } from "react";
23
import { createRoot } from "react-dom/client";
34
import { callTool, connectToServer, hasAppHtml, initializeApp, loadSandboxProxy, log, newAppBridge, type ServerInfo, type ToolCallInfo } from "./implementation";
45
import styles from "./index.module.css";
56

67

8+
/**
9+
* Extract default values from a tool's JSON Schema inputSchema.
10+
* Returns a formatted JSON string with defaults, or "{}" if none found.
11+
*/
12+
function getToolDefaults(tool: Tool | undefined): string {
13+
if (!tool?.inputSchema?.properties) return "{}";
14+
15+
const defaults: Record<string, unknown> = {};
16+
for (const [key, prop] of Object.entries(tool.inputSchema.properties)) {
17+
if (prop && typeof prop === "object" && "default" in prop) {
18+
defaults[key] = prop.default;
19+
}
20+
}
21+
22+
return Object.keys(defaults).length > 0
23+
? JSON.stringify(defaults, null, 2)
24+
: "{}";
25+
}
26+
27+
728
// Host passes serversPromise to CallToolPanel
829
interface HostProps {
930
serversPromise: Promise<ServerInfo[]>;
@@ -74,6 +95,14 @@ function CallToolPanel({ serversPromise, addToolCall }: CallToolPanelProps) {
7495
setSelectedServer(server);
7596
const [firstTool] = server.tools.keys();
7697
setSelectedTool(firstTool ?? "");
98+
// Set input JSON to tool defaults (if any)
99+
setInputJson(getToolDefaults(server.tools.get(firstTool ?? "")));
100+
};
101+
102+
const handleToolSelect = (toolName: string) => {
103+
setSelectedTool(toolName);
104+
// Set input JSON to tool defaults (if any)
105+
setInputJson(getToolDefaults(selectedServer?.tools.get(toolName)));
77106
};
78107

79108
const handleSubmit = () => {
@@ -96,7 +125,7 @@ function CallToolPanel({ serversPromise, addToolCall }: CallToolPanelProps) {
96125
<select
97126
className={styles.toolSelect}
98127
value={selectedTool}
99-
onChange={(e) => setSelectedTool(e.target.value)}
128+
onChange={(e) => handleToolSelect(e.target.value)}
100129
>
101130
{selectedServer && toolNames.map((name) => (
102131
<option key={name} value={name}>{name}</option>

examples/basic-server-react/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"@modelcontextprotocol/sdk": "^1.22.0",
1616
"react": "^19.2.0",
1717
"react-dom": "^19.2.0",
18-
"zod": "^3.25.0"
18+
"zod": "^4.1.13"
1919
},
2020
"devDependencies": {
2121
"@types/cors": "^2.8.19",
Lines changed: 33 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,34 @@
11
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2-
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
2+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
33
import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
4-
import cors from "cors";
5-
import express, { type Request, type Response } from "express";
64
import fs from "node:fs/promises";
75
import path from "node:path";
86
import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app";
7+
import { startServer } from "../shared/server-utils.js";
98

10-
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3001;
119
const DIST_DIR = path.join(import.meta.dirname, "dist");
10+
const RESOURCE_URI = "ui://get-time/mcp-app.html";
11+
12+
/**
13+
* Creates a new MCP server instance with tools and resources registered.
14+
* Each HTTP session needs its own server instance because McpServer only supports one transport.
15+
*/
16+
function createServer(): McpServer {
17+
const server = new McpServer({
18+
name: "Basic MCP App Server (React-based)",
19+
version: "1.0.0",
20+
});
1221

13-
14-
const server = new McpServer({
15-
name: "Basic MCP App Server (React-based)",
16-
version: "1.0.0",
17-
});
18-
19-
20-
// MCP Apps require two-part registration: a tool (what the LLM calls) and a
21-
// resource (the UI it renders). The `_meta` field on the tool links to the
22-
// resource URI, telling hosts which UI to display when the tool executes.
23-
{
24-
const resourceUri = "ui://get-time/mcp-app.html";
25-
22+
// MCP Apps require two-part registration: a tool (what the LLM calls) and a
23+
// resource (the UI it renders). The `_meta` field on the tool links to the
24+
// resource URI, telling hosts which UI to display when the tool executes.
2625
server.registerTool(
2726
"get-time",
2827
{
2928
title: "Get Time",
3029
description: "Returns the current server time as an ISO 8601 string.",
3130
inputSchema: {},
32-
_meta: { [RESOURCE_URI_META_KEY]: resourceUri },
31+
_meta: { [RESOURCE_URI_META_KEY]: RESOURCE_URI },
3332
},
3433
async (): Promise<CallToolResult> => {
3534
const time = new Date().toISOString();
@@ -40,66 +39,35 @@ const server = new McpServer({
4039
);
4140

4241
server.registerResource(
43-
resourceUri,
44-
resourceUri,
45-
{},
42+
RESOURCE_URI,
43+
RESOURCE_URI,
44+
{ mimeType: RESOURCE_MIME_TYPE },
4645
async (): Promise<ReadResourceResult> => {
4746
const html = await fs.readFile(path.join(DIST_DIR, "mcp-app.html"), "utf-8");
4847

4948
return {
5049
contents: [
5150
// Per the MCP App specification, "text/html;profile=mcp-app" signals
5251
// to the Host that this resource is indeed for an MCP App UI.
53-
{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html },
52+
{ uri: RESOURCE_URI, mimeType: RESOURCE_MIME_TYPE, text: html },
5453
],
5554
};
5655
},
5756
);
58-
}
59-
60-
61-
const app = express();
62-
app.use(cors());
63-
app.use(express.json());
6457

65-
app.post("/mcp", async (req: Request, res: Response) => {
66-
try {
67-
const transport = new StreamableHTTPServerTransport({
68-
sessionIdGenerator: undefined,
69-
enableJsonResponse: true,
70-
});
71-
res.on("close", () => { transport.close(); });
72-
73-
await server.connect(transport);
74-
75-
await transport.handleRequest(req, res, req.body);
76-
} catch (error) {
77-
console.error("Error handling MCP request:", error);
78-
if (!res.headersSent) {
79-
res.status(500).json({
80-
jsonrpc: "2.0",
81-
error: { code: -32603, message: "Internal server error" },
82-
id: null,
83-
});
84-
}
85-
}
86-
});
58+
return server;
59+
}
8760

88-
const httpServer = app.listen(PORT, (err) => {
89-
if (err) {
90-
console.error("Error starting server:", err);
91-
process.exit(1);
61+
async function main() {
62+
if (process.argv.includes("--stdio")) {
63+
await createServer().connect(new StdioServerTransport());
64+
} else {
65+
const port = parseInt(process.env.PORT ?? "3101", 10);
66+
await startServer(createServer, { port, name: "Basic MCP App Server (React-based)" });
9267
}
93-
console.log(`Server listening on http://localhost:${PORT}/mcp`);
94-
});
95-
96-
function shutdown() {
97-
console.log("\nShutting down...");
98-
httpServer.close(() => {
99-
console.log("Server closed");
100-
process.exit(0);
101-
});
10268
}
10369

104-
process.on("SIGINT", shutdown);
105-
process.on("SIGTERM", shutdown);
70+
main().catch((e) => {
71+
console.error(e);
72+
process.exit(1);
73+
});

examples/basic-server-react/src/mcp-app.module.css

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
--color-primary-hover: #1d4ed8;
44
--color-notice-bg: #eff6ff;
55

6-
min-width: 425px;
6+
width: 100%;
7+
max-width: 425px;
8+
box-sizing: border-box;
79

810
> * {
911
margin-top: 0;

0 commit comments

Comments
 (0)