examples: remove arcade (#414)

This commit is contained in:
Ido Salomon 2026-01-30 22:06:50 +02:00 • committed by GitHub
commit 0278af6a2a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 0 additions and 921 deletions

View file

@ -98,7 +98,6 @@ jobs:
fail-fast: false
matrix:
example:
- arcade-server
- basic-server-preact
- basic-server-react
- basic-server-solid

View file

@ -26,7 +26,6 @@ jobs:
. \
./examples/basic-server-react \
./examples/basic-server-vanillajs \
./examples/arcade-server \
./examples/budget-allocator-server \
./examples/cohort-heatmap-server \
./examples/customer-segmentation-server \

View file

@ -176,16 +176,6 @@ To use these examples with MCP clients that support the stdio transport (such as
"--stdio"
]
},
"arcade": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-arcade",
"--stdio"
]
},
"budget-allocator": {
"command": "npx",
"args": [
@ -403,13 +393,6 @@ Then configure your MCP client to build and run the local server. Replace `~/cod
"cd ~/code/ext-apps/examples/basic-server-solid && npm run build >&2 && node dist/index.js --stdio"
]
},
"arcade": {
"command": "bash",
"args": [
"-c",
"cd ~/code/ext-apps/examples/arcade-server && npm run build >&2 && node dist/index.js --stdio"
]
},
"budget-allocator": {
"command": "bash",
"args": [

View file

@ -1,2 +0,0 @@
node_modules/
dist/

View file

@ -1,83 +0,0 @@
# Example: Arcade Server
An MCP Apps server that lets you browse and play classic arcade games from [archive.org](https://archive.org) directly in an MCP-enabled host.
## Overview
This example demonstrates serving **external HTML content** as an MCP App resource. The resource is a static loader that uses the MCP Apps protocol to receive tool arguments, then fetches the processed game HTML from a server endpoint. This pattern allows the same resource to display different games based on tool input.
Key techniques:
- MCP Apps protocol handshake (`ui/initialize` → `ui/notifications/tool-input`) to receive game ID dynamically
- Server-side HTML fetching and processing per game ID
- `<base href>` tag for resolving relative URLs against archive.org
- `baseUriDomains` CSP metadata to allow the base tag
- Rewriting ES module `import()` to classic `<script src>` loading (for srcdoc iframe compatibility)
- Local script endpoint to bypass CORS restrictions in sandboxed iframes
## Key Files
- [`server.ts`](server.ts) - MCP server with tool and resource registration
- [`index.ts`](index.ts) - HTTP transport and Express setup
- [`game-processor.ts`](game-processor.ts) - Fetches and processes archive.org HTML
- [`search.ts`](search.ts) - Archive.org search with smart fallbacks
## Getting Started
```bash
npm install
npm run dev
```
The server starts on `http://localhost:3002/mcp` by default. Set the `PORT` environment variable to change it.
### MCP Client Configuration
```json
{
"mcpServers": {
"arcade": {
"url": "http://localhost:3002/mcp"
}
}
}
```
## Tools
| Tool | Description | UI |
| ---------------- | ------------------------------------------- | --- |
| `search_games` | Search archive.org for arcade games by name | No |
| `get_game_by_id` | Load and play a specific game | Yes |
## How It Works
```
1. Host calls search_games → Server queries archive.org API → Returns game list
2. Host calls get_game_by_id → Tool validates gameId and returns success
3. Host reads resource → Gets static loader with MCP Apps protocol handler
4. View performs ui/initialize handshake with host
5. Host sends tool-input with gameId → View fetches /game-html/:gameId
6. Server fetches embed HTML from archive.org and processes it:
- Removes archive.org's <base> tag
- Injects <base href="https://archive.org/"> for URL resolution
- Rewrites ES module import() to <script src> loading
- Fetches emulation.min.js, patches it, serves from local endpoint
- Injects layout CSS for full-viewport display
7. Game runs: emulator loads ROM, initializes MAME, game is playable
```
### Why the Processing?
Archive.org's game embed pages use ES module `import()` for loading the emulation engine. In `srcdoc` iframes (used by MCP hosts), `import()` fails because the iframe has a `null` origin. The server works around this by:
1. **Fetching `emulation.min.js` server-side** and replacing `import()` with `window.loadScript()`
2. **Serving the patched script** from a local Express endpoint (`/scripts/emulation.js`)
3. **Using `<script src>`** which is not subject to CORS restrictions, unlike `fetch()` or `import()`
## Example Game IDs
- `arcade_20pacgal` - Ms. Pac-Man / Galaga
- `arcade_galaga` - Galaga
- `arcade_sf2` - Street Fighter II
- `doom-play` - The Ultimate DOOM

View file

@ -1,187 +0,0 @@
/**
* Game Processor
*
* Fetches and processes archive.org game HTML for embedding in MCP Apps.
* Uses <base href="https://archive.org/"> for relative URL resolution,
* and rewrites ES module import() calls to classic <script src> loading
* (dynamic import() doesn't work in srcdoc iframes due to null origin).
*/
// Cache for the modified emulation script content
let cachedEmulationScript: string | null = null;
/**
* Returns the cached modified emulation script.
* Called by the server's /scripts/emulation.js endpoint.
*/
export function getCachedEmulationScript(): string | null {
return cachedEmulationScript;
}
/**
* Fetches and processes archive.org game HTML for inline embedding.
*/
export async function processGameEmbed(
gameId: string,
serverPort: number,
): Promise<string> {
const encodedGameId = encodeURIComponent(gameId);
const embedUrl = `https://archive.org/embed/${encodedGameId}`;
const response = await fetch(embedUrl, {
headers: {
"User-Agent": "MCP-Arcade-Server/1.0",
Accept: "text/html",
},
});
if (!response.ok) {
throw new Error(
`Failed to fetch game: ${response.status} ${response.statusText}`,
);
}
let html = await response.text();
// Remove archive.org's <base> tag (would violate CSP base-uri)
html = html.replace(/<base\s+[^>]*>/gi, "");
// Inject our <base> tag, hash-link interceptor, script loader, and layout CSS
const headMatch = html.match(/<head[^>]*>/i);
if (headMatch) {
html = html.replace(
headMatch[0],
`${headMatch[0]}
<base href="https://archive.org/">
<script>
// Intercept hash-link clicks that would navigate away due to <base>
document.addEventListener("click", function(e) {
var el = e.target;
while (el && el.tagName !== "A") el = el.parentElement;
if (el && el.getAttribute("href") && el.getAttribute("href").charAt(0) === "#") {
e.preventDefault();
}
}, true);
// Script loader: replaces dynamic import() which fails in srcdoc iframes.
// Uses <script src> which respects <base> and bypasses CORS.
if (!window.loadScript) {
window.loadScript = function(url) {
return new Promise(function(resolve) {
var s = document.createElement("script");
s.src = url;
s.onload = function() {
resolve({
default: window.Emulator || window.IALoader || window.Loader || {},
__esModule: true
});
};
s.onerror = function() {
console.error("Failed to load script:", url);
resolve({ default: {}, __esModule: true });
};
document.head.appendChild(s);
});
};
}
</script>
<style>
html, body { width: 100% !important; height: 100% !important; margin: 0 !important; padding: 0 !important; overflow: hidden !important; }
#wrap, #emulate { width: 100% !important; height: 100% !important; margin: 0 !important; padding: 0 !important; max-width: none !important; }
#canvasholder { width: 100% !important; height: 100% !important; }
#canvas { width: 100% !important; height: 100% !important; max-width: 100% !important; max-height: 100% !important; object-fit: contain; }
</style>`,
);
}
// Convert inline ES module scripts to classic scripts
html = convertModuleScripts(html);
// Fetch the emulation script server-side and serve from local endpoint
html = await rewriteEmulationScript(html, serverPort);
return html;
}
/**
* Fetches emulation.min.js server-side, rewrites import() → loadScript(),
* caches it, and points the HTML <script src> to our local endpoint.
* This avoids: 1) import() failing in srcdoc, 2) CORS blocking fetch from srcdoc.
*/
async function rewriteEmulationScript(
html: string,
serverPort: number,
): Promise<string> {
// NOTE: We intentionally match only the first <script> tag whose src contains
// "emulation.min.js". Archive.org embeds are expected to include a single
// relevant emulation script, so rewriting the first match is sufficient.
// If Archive.org's HTML structure changes to include multiple such scripts,
// this logic may need to be revisited.
const pattern =
/<script\s+[^>]*src=["']([^"']*emulation\.min\.js[^"']*)["'][^>]*><\/script>/i;
const match = html.match(pattern);
if (!match) return html;
const scriptTag = match[0];
let scriptUrl = match[1];
if (scriptUrl.startsWith("//")) {
scriptUrl = "https:" + scriptUrl;
}
try {
const response = await fetch(scriptUrl, {
headers: { "User-Agent": "MCP-Arcade-Server/1.0" },
});
if (!response.ok) return html;
let content = await response.text();
content = content.replace(/\bimport\s*\(/g, "window.loadScript(");
cachedEmulationScript = content;
const localUrl = `http://localhost:${serverPort}/scripts/emulation.js`;
html = html.replace(scriptTag, `<script src="${localUrl}"></script>`);
} catch {
// If fetch fails, leave the original script tag
}
return html;
}
/**
* Converts ES module scripts to classic scripts and rewrites inline
* import() calls to use window.loadScript().
*/
function convertModuleScripts(html: string): string {
return html.replace(
/(<script[^>]*>)([\s\S]*?)(<\/script[^>]*>)/gi,
(match, openTag: string, content: string, closeTag: string) => {
// Skip our injected scripts
if (content.includes("window.loadScript")) return match;
// Remove type="module"
const newOpenTag = openTag.replace(/\s*type\s*=\s*["']module["']/gi, "");
// Rewrite dynamic import() to loadScript()
let newContent = content.replace(
/import\s*\(\s*(["'`])([^"'`]+)\1\s*\)/g,
(_m: string, quote: string, path: string) => {
if (path.startsWith("http://") || path.startsWith("https://"))
return _m;
return `window.loadScript(${quote}${path}${quote})`;
},
);
// Convert static import statements
newContent = newContent.replace(
/import\s+(\{[^}]*\}|[^"']+)\s+from\s+(["'])([^"']+)\2/g,
(_m: string, _imports: string, quote: string, path: string) => {
if (path.startsWith("http://") || path.startsWith("https://"))
return _m;
return `window.loadScript(${quote}${path}${quote})`;
},
);
return newOpenTag + newContent + closeTag;
},
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 446 KiB

View file

@ -1,105 +0,0 @@
#!/usr/bin/env node
/**
* Arcade MCP Server - Entry Point
*
* Sets up HTTP transport with Express and serves the modified emulation script.
*/
import cors from "cors";
import express from "express";
import type { Request, Response } from "express";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { createServer, validateGameId } from "./server.js";
import {
getCachedEmulationScript,
processGameEmbed,
} from "./game-processor.js";
const DEFAULT_PORT = 3001;
async function main() {
const port = parseInt(process.env.PORT ?? String(DEFAULT_PORT), 10);
const app = express();
app.use(cors());
app.use(express.json());
// Serve the modified emulation script (import() rewritten to loadScript()).
// <script src> is not subject to CORS, so this works from srcdoc iframes.
app.get("/scripts/emulation.js", (_req: Request, res: Response) => {
const script = getCachedEmulationScript();
if (!script) {
res.status(404).send("// No script cached. Load a game first.");
return;
}
res.setHeader("Content-Type", "application/javascript");
res.setHeader("Cache-Control", "no-cache");
res.send(script);
});
// Serve game HTML by ID. Fetches and processes the game from archive.org.
app.get("/game-html/:gameId", async (req: Request, res: Response) => {
const gameId = req.params.gameId as string;
if (!validateGameId(gameId)) {
res.status(400).send("Invalid game ID.");
return;
}
try {
const html = await processGameEmbed(gameId, port);
res.setHeader("Content-Type", "text/html");
res.setHeader("Cache-Control", "no-cache");
res.send(html);
} catch (error) {
console.error("Failed to load game:", gameId, error);
res.status(500).send("Failed to load game.");
}
});
// MCP endpoint - stateless transport (new server per request)
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer(port);
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
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 httpServer = app.listen(port, () => {
console.log(`Arcade MCP Server listening on http://localhost:${port}/mcp`);
});
httpServer.setMaxListeners(20);
const shutdown = () => {
console.log("\nShutting down...");
httpServer.close(() => process.exit(0));
// Force exit after 2 seconds if connections don't close gracefully
setTimeout(() => process.exit(0), 2000).unref();
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
}
main().catch((e) => {
console.error(e);
process.exit(1);
});

View file

@ -1,46 +0,0 @@
{
"name": "@modelcontextprotocol/server-arcade",
"version": "0.4.1",
"type": "module",
"description": "MCP Apps server for playing arcade games (archive.org)",
"repository": {
"type": "git",
"url": "https://github.com/modelcontextprotocol/ext-apps",
"directory": "examples/arcade-server"
},
"license": "MIT",
"main": "dist/server.js",
"types": "dist/server.d.ts",
"bin": {
"mcp-server-arcade": "dist/index.js"
},
"files": [
"dist"
],
"exports": {
".": {
"types": "./dist/server.d.ts",
"default": "./dist/server.js"
}
},
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx index.ts",
"prepublishOnly": "npm run build"
},
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.1",
"@modelcontextprotocol/sdk": "^1.24.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"zod": "^4.1.13"
},
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.0",
"@types/node": "^22.0.0",
"tsx": "^4.7.0",
"typescript": "^5.9.3"
}
}

View file

@ -1,112 +0,0 @@
/**
* Archive.org Search
*
* Searches archive.org for arcade/emulation games with smart fallbacks
* for underscores, spaces, and partial matches.
*/
export interface ArchiveOrgSearchResult {
identifier: string;
title: string;
mediatype: string;
description?: string;
year?: string;
creator?: string;
}
interface ArchiveOrgSearchResponse {
response: {
docs: ArchiveOrgSearchResult[];
numFound: number;
};
}
const GAME_COLLECTIONS = [
"collection:internetarcade",
"collection:consolelivingroom",
"collection:softwarelibrary_msdos_games",
"collection:atari_2600_library",
"collection:tosec",
"collection:emularity_engine_jsmess",
].join(" OR ");
/**
* Searches archive.org for arcade games matching the given term.
*/
export async function searchArchiveOrgGames(
searchTerm: string,
maxResults: number = 10,
): Promise<ArchiveOrgSearchResult[]> {
const normalizedTerm = searchTerm.trim();
const termWithSpaces = normalizedTerm.replace(/_/g, " ");
const termWithUnderscores = normalizedTerm.replace(/\s+/g, "_");
const words = normalizedTerm.split(/[_\s]+/).filter((w) => w.length > 0);
const exactVariations = [
normalizedTerm,
termWithSpaces,
termWithUnderscores,
].filter((term, index, self) => term && self.indexOf(term) === index);
const exactQuery = `title:(${exactVariations.join(" OR ")})`;
// Step 1: Search with exact terms in game collections
let games = await performSearch(exactQuery, true, maxResults);
// Step 2: Broader search with individual words in title
if (games.length === 0 && words.length > 1) {
const broaderQuery = `title:(${exactVariations.join(" OR ")} OR ${words.join(" OR ")})`;
games = await performSearch(broaderQuery, true, maxResults);
}
// Step 3: Search without collection filter as final fallback
if (games.length === 0) {
const fallbackQuery =
words.length > 1
? `title:(${exactVariations.join(" OR ")} OR ${words.join(" OR ")})`
: exactQuery;
try {
games = await performSearch(fallbackQuery, false, maxResults);
games = games.filter(
(doc) =>
doc.identifier.startsWith("arcade_") ||
doc.identifier.startsWith("msdos_") ||
doc.identifier.includes("game") ||
doc.mediatype === "software",
);
} catch {
// Continue with empty results
}
}
return games;
}
async function performSearch(
query: string,
withCollectionFilter: boolean,
maxResults: number,
): Promise<ArchiveOrgSearchResult[]> {
const fullQuery = withCollectionFilter
? `${query} AND (${GAME_COLLECTIONS})`
: query;
const searchUrl = `https://archive.org/advancedsearch.php?q=${encodeURIComponent(fullQuery)}&fl[]=identifier&fl[]=title&fl[]=mediatype&fl[]=description&fl[]=year&fl[]=creator&output=json&rows=${encodeURIComponent(String(maxResults))}`;
const response = await fetch(searchUrl, {
headers: {
"User-Agent": "MCP-Arcade-Server/1.0",
Accept: "application/json",
},
});
if (!response.ok) {
throw new Error(`Archive.org API returned status ${response.status}`);
}
const data = (await response.json()) as ArchiveOrgSearchResponse;
if (!data?.response?.docs) return [];
return data.response.docs.filter((doc) => doc.identifier);
}

View file

@ -1,266 +0,0 @@
/**
* Arcade MCP Server
*
* MCP server for browsing and playing arcade games from archive.org.
* Fetches game HTML server-side and serves it as an inline MCP App resource.
*/
import {
RESOURCE_MIME_TYPE,
registerAppResource,
} from "@modelcontextprotocol/ext-apps/server";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import { searchArchiveOrgGames } from "./search.js";
const GAME_VIEWER_RESOURCE_URI = "ui://arcade/game-viewer";
/**
* Validates an archive.org game identifier.
*/
export function validateGameId(gameId: string): boolean {
return (
gameId.length > 0 &&
!gameId.includes("/") &&
!gameId.includes("?") &&
!gameId.includes("#") &&
!gameId.includes("..") &&
!/\s/.test(gameId)
);
}
/**
* Creates a new MCP server instance with tools and resources registered.
*/
export function createServer(port: number): McpServer {
const server = new McpServer({
name: "Arcade Server",
version: "1.0.0",
});
// Tool: Search for arcade games (no UI)
server.registerTool(
"search_games",
{
description:
"Searches archive.org for arcade games matching the search term. Returns a list of game identifiers and titles sorted by relevance.",
inputSchema: z.object({
searchTerm: z
.string()
.describe(
'The game name or search term (e.g., "doom", "pacman", "mario").',
),
maxResults: z
.number()
.optional()
.default(10)
.describe(
"Maximum number of results to return (default: 10, max: 50)",
),
}) as any,
},
async (args: any): Promise<CallToolResult> => {
const { searchTerm, maxResults } = args;
if (!searchTerm || searchTerm.trim().length === 0) {
return {
content: [
{ type: "text", text: "Error: Search term cannot be empty." },
],
isError: true,
};
}
const limit = Math.min(Math.max(maxResults || 10, 1), 50);
try {
const games = await searchArchiveOrgGames(searchTerm.trim(), limit);
if (games.length === 0) {
return {
content: [
{
type: "text",
text: `No arcade games found for "${searchTerm}". Try a different search term.`,
},
],
};
}
const gameData = games.map((game) => ({
identifier: game.identifier,
title: game.title,
description: game.description || null,
year: game.year || null,
creator: game.creator || null,
}));
return {
content: [
{
type: "text",
text: JSON.stringify(
{ total: games.length, searchTerm, games: gameData },
null,
2,
),
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error searching for games: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
},
);
// Tool: Load and play a specific game (with UI)
server.registerTool(
"get_game_by_id",
{
title: "Play Arcade Game",
description:
"Loads and displays a playable arcade game from archive.org by its identifier.",
inputSchema: z.object({
gameId: z
.string()
.describe(
'The archive.org identifier (e.g., "doom-play", "arcade_20pacgal").',
),
}) as any,
_meta: {
ui: { resourceUri: GAME_VIEWER_RESOURCE_URI },
},
},
async (args: any): Promise<CallToolResult> => {
const { gameId } = args;
if (!gameId || !validateGameId(gameId)) {
return {
content: [
{
type: "text",
text: `Error: Invalid game ID "${gameId}". Must be a non-empty identifier without path separators.`,
},
],
isError: true,
};
}
return {
content: [{ type: "text", text: `Loading arcade game: ${gameId}` }],
};
},
);
// Resource: Serves the processed game HTML with CSP configuration
registerAppResource(
server,
"Game Viewer",
GAME_VIEWER_RESOURCE_URI,
{},
async () => {
// Static view shell with inline MCP Apps protocol handling.
// Performs the initialization handshake, then waits for tool input
// to know which game to load. Fetches game HTML from the server's
// HTTP endpoint keyed by game ID.
const html = `<!DOCTYPE html>
<html><head><meta charset="UTF-8"></head>
<body style="margin:0;background:#111;color:#0f0;font-family:monospace;display:flex;align-items:center;justify-content:center;height:100vh">
<div id="status">Loading game\u2026</div>
<script>
(function() {
var SERVER = "http://localhost:${port}";
var initialized = false;
function send(msg) { window.parent.postMessage(msg, "*"); }
window.addEventListener("message", function(event) {
var msg = event.data;
if (!msg || msg.jsonrpc !== "2.0") return;
// Handle initialize response
if (msg.id && msg.result && !initialized) {
initialized = true;
send({jsonrpc: "2.0", method: "ui/notifications/initialized"});
return;
}
// Handle ping
if (msg.method === "ping" && msg.id) {
send({jsonrpc: "2.0", id: msg.id, result: {}});
return;
}
// Handle tool input - load the game
if (msg.method === "ui/notifications/tool-input" && msg.params) {
var gameId = msg.params.arguments && msg.params.arguments.gameId;
if (gameId) loadGame(gameId);
return;
}
});
// Send initialize request
send({jsonrpc: "2.0", id: 1, method: "ui/initialize", params: {
appInfo: {name: "Arcade Game Viewer", version: "1.0.0"},
appCapabilities: {},
protocolVersion: "2025-11-21"
}});
function loadGame(gameId) {
document.getElementById("status").textContent = "Loading " + gameId + "\\u2026";
fetch(SERVER + "/game-html/" + encodeURIComponent(gameId))
.then(function(r) {
if (!r.ok) throw new Error(r.statusText);
return r.text();
})
.then(function(h) {
document.open();
document.write(h);
document.close();
})
.catch(function(e) {
document.getElementById("status").textContent = "Failed to load game: " + e.message;
});
}
})();
</script>
</body></html>`;
return {
contents: [
{
uri: GAME_VIEWER_RESOURCE_URI,
mimeType: RESOURCE_MIME_TYPE,
text: html,
_meta: {
ui: {
csp: {
resourceDomains: [
"https://archive.org",
"https://*.archive.org",
`http://localhost:${port}`,
],
connectDomains: [
"https://archive.org",
"https://*.archive.org",
`http://localhost:${port}`,
],
baseUriDomains: ["https://archive.org"],
},
},
},
},
],
};
},
);
return server;
}

View file

@ -1,17 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"rootDir": ".",
"declaration": true,
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"resolveJsonModule": true
},
"include": ["*.ts"],
"exclude": ["node_modules", "dist"]
}

79
package-lock.json generated
View file

@ -77,81 +77,6 @@
}
}
},
"examples/arcade-server": {
"name": "@modelcontextprotocol/server-arcade",
"version": "0.4.1",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.1",
"@modelcontextprotocol/sdk": "^1.24.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"zod": "^4.1.13"
},
"bin": {
"mcp-server-arcade": "dist/index.js"
},
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.0",
"@types/node": "^22.0.0",
"tsx": "^4.7.0",
"typescript": "^5.9.3"
}
},
"examples/arcade-server/node_modules/@modelcontextprotocol/ext-apps": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-1.0.1.tgz",
"integrity": "sha512-rAPzBbB5GNgYk216paQjGKUgbNXSy/yeR95c0ni6Y4uvhWI2AeF+ztEOqQFLBMQy/MPM+02pbVK1HaQmQjMwYQ==",
"hasInstallScript": true,
"license": "MIT",
"workspaces": [
"examples/*"
],
"optionalDependencies": {
"@oven/bun-darwin-aarch64": "^1.2.21",
"@oven/bun-darwin-x64": "^1.2.21",
"@oven/bun-darwin-x64-baseline": "^1.2.21",
"@oven/bun-linux-aarch64": "^1.2.21",
"@oven/bun-linux-aarch64-musl": "^1.2.21",
"@oven/bun-linux-x64": "^1.2.21",
"@oven/bun-linux-x64-baseline": "^1.2.21",
"@oven/bun-linux-x64-musl": "^1.2.21",
"@oven/bun-linux-x64-musl-baseline": "^1.2.21",
"@oven/bun-windows-x64": "^1.2.21",
"@oven/bun-windows-x64-baseline": "^1.2.21",
"@rollup/rollup-darwin-arm64": "^4.53.3",
"@rollup/rollup-darwin-x64": "^4.53.3",
"@rollup/rollup-linux-arm64-gnu": "^4.53.3",
"@rollup/rollup-linux-x64-gnu": "^4.53.3",
"@rollup/rollup-win32-arm64-msvc": "^4.53.3",
"@rollup/rollup-win32-x64-msvc": "^4.53.3"
},
"peerDependencies": {
"@modelcontextprotocol/sdk": "^1.24.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
"zod": "^3.25.0 || ^4.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"examples/arcade-server/node_modules/@types/node": {
"version": "22.19.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz",
"integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"examples/basic-host": {
"name": "@modelcontextprotocol/ext-apps-basic-host",
"version": "1.0.1",
@ -2692,10 +2617,6 @@
}
}
},
"node_modules/@modelcontextprotocol/server-arcade": {
"resolved": "examples/arcade-server",
"link": true
},
"node_modules/@modelcontextprotocol/server-basic-preact": {
"resolved": "examples/basic-server-preact",
"link": true

View file

@ -27,7 +27,6 @@ const EXTRA_WAIT_MS: Record<string, number> = {
// Servers to skip (screenshots maintained manually)
const SKIP_SERVERS = new Set([
"arcade-server", // Loads games from archive.org - screenshots maintained manually
"video-resource", // Uses custom screenshot from PR comment
"qr-server", // Uses custom screenshot from PR comment
"say-server", // TTS model download from HuggingFace can be slow
@ -38,7 +37,6 @@ const EXAMPLE_FILTER = process.env.EXAMPLE;
// Server configurations (excludes integration-server which is for E2E testing)
const ALL_SERVERS = [
{ key: "arcade-server", name: "Arcade Server", dir: "arcade-server" },
{
key: "basic-react",
name: "Basic MCP App Server (React)",

View file

@ -35,7 +35,6 @@ const DYNAMIC_MASKS: Record<string, string[]> = {
// Servers that need extra stabilization time (e.g., for tile loading, WebGL init)
const SLOW_SERVERS: Record<string, number> = {
"map-server": 15000, // CesiumJS needs time for tiles to load
"arcade-server": 15000, // Game loading from archive.org can be slow
threejs: 2000, // Three.js WebGL initialization
"say-server": 10000, // TTS model download from HuggingFace can be slow
};
@ -58,7 +57,6 @@ const HOST_MASKS: Record<string, string[]> = {
// Servers to skip in CI (require special resources like GPU, large ML models)
const SKIP_SERVERS = new Set<string>([
"arcade-server", // Loads games from archive.org which may be slow/unreliable in CI
"qr-server", // TODO
"say-server", // TTS model download from HuggingFace can be slow
]);
@ -68,7 +66,6 @@ const EXAMPLE_FILTER = process.env.EXAMPLE;
// Server configurations (key is used for screenshot filenames, name is the MCP server name, dir is the folder name)
const ALL_SERVERS = [
{ key: "arcade-server", name: "Arcade Server", dir: "arcade-server" },
{
key: "integration",
name: "Integration Test Server",