Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ jobs:
- budget-allocator-server
- cohort-heatmap-server
- customer-segmentation-server
- conformance-server
- debug-server
- lazy-auth-server
- map-server
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ jobs:
./examples/budget-allocator-server \
./examples/cohort-heatmap-server \
./examples/customer-segmentation-server \
./examples/conformance-server \
./examples/debug-server \
./examples/lazy-auth-server \
./examples/map-server \
Expand Down
2 changes: 2 additions & 0 deletions examples/conformance-server/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
dist/
134 changes: 134 additions & 0 deletions examples/conformance-server/README.md

Large diffs are not rendered by default.

Binary file added examples/conformance-server/grid-cell.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
93 changes: 93 additions & 0 deletions examples/conformance-server/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* Entry point for running the MCP server.
* Run with: npx @modelcontextprotocol/server-basic-react
* Or: node dist/index.js [--stdio]
*/

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";
import { createServer } from "./server.js";

/**
* Starts an MCP server with Streamable HTTP transport in stateless mode.
*
* @param createServer - Factory function that creates a new McpServer instance per request.
*/
export async function startStreamableHTTPServer(
createServer: () => McpServer,
): Promise<void> {
const port = parseInt(process.env.PORT ?? "3001", 10);

const app = createMcpExpressApp({ host: "0.0.0.0" });
app.use(cors());

app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
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, (err) => {
if (err) {
console.error("Failed to start server:", err);
process.exit(1);
}
console.log(`MCP server listening on http://localhost:${port}/mcp`);
});

const shutdown = () => {
console.log("\nShutting down...");
httpServer.close(() => process.exit(0));
};

process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
}

/**
* Starts an MCP server with stdio transport.
*
* @param createServer - Factory function that creates a new McpServer instance.
*/
export async function startStdioServer(
createServer: () => McpServer,
): Promise<void> {
await createServer().connect(new StdioServerTransport());
}

async function main() {
if (process.argv.includes("--stdio")) {
await startStdioServer(createServer);
} else {
await startStreamableHTTPServer(createServer);
}
}

main().catch((e) => {
console.error(e);
process.exit(1);
});
13 changes: 13 additions & 0 deletions examples/conformance-server/mcp-app.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark">
<title>MCP Apps Conformance Runner</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/mcp-app.tsx"></script>
</body>
</html>
56 changes: 56 additions & 0 deletions examples/conformance-server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"name": "@modelcontextprotocol/server-conformance",
"version": "1.0.0",
"type": "module",
"description": "Host-conformance test server for the MCP Apps spec: ships a ui:// runner that asserts host behaviour and reports PASS/FAIL inside the iframe",
"repository": {
"type": "git",
"url": "https://github.com/modelcontextprotocol/ext-apps",
"directory": "examples/conformance-server"
},
"license": "MIT",
"main": "dist/server.js",
"types": "dist/server.d.ts",
"bin": {
"mcp-server-conformance": "dist/index.js"
},
"files": [
"dist"
],
"exports": {
".": {
"types": "./dist/server.d.ts",
"default": "./dist/server.js"
}
},
"scripts": {
"build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --banner \"#!/usr/bin/env node\"",
"watch": "cross-env INPUT=mcp-app.html vite build --watch",
"serve": "bun --watch main.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\"",
"prepublishOnly": "npm run build"
},
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.7.4",
"@modelcontextprotocol/sdk": "^1.24.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"zod": "^4.1.13"
},
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.0",
"@types/node": "22.10.0",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^4.3.4",
"concurrently": "^9.2.1",
"cross-env": "^10.1.0",
"typescript": "^5.9.3",
"vite": "^6.0.0",
"vite-plugin-singlefile": "^2.3.0"
}
}
Binary file added examples/conformance-server/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
112 changes: 112 additions & 0 deletions examples/conformance-server/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/**
* The reference conformance test server.
*
* Exposes one ui:// test page (the conformance runner) plus the fixture tools
* the in-iframe harness needs: a model-visible launcher, an app-only echo probe
* (for the tool-proxying test), and a model-only tool (for the visibility test).
* Point any MCP Apps host at this server's /mcp endpoint and run the suite.
*
* POC scope: the runner only, results are shown in the iframe, not persisted.
*/

import { existsSync, readFileSync } from "node:fs";
import path from "node:path";
import {
RESOURCE_MIME_TYPE,
registerAppResource,
registerAppTool,
} 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";

const RUNNER_URI = "ui://conformance/runner";
// Works both from source (server.ts) and compiled (dist/server.js)
const DIST_DIR = import.meta.filename.endsWith(".ts")
? path.join(import.meta.dirname, "dist")
: import.meta.dirname;
const VIEW_HTML = path.join(DIST_DIR, "mcp-app.html");

// The runner declares a CSP so the suite can test both directions: this origin
// is ALLOWED (connectDomains), and any other origin must stay blocked.
const CSP_ALLOWED_ORIGIN = "https://modelcontextprotocol.io";

function loadRunnerHtml(): string {
if (existsSync(VIEW_HTML)) return readFileSync(VIEW_HTML, "utf-8");
return `<!DOCTYPE html><html><body style="font-family:sans-serif;padding:24px">
<h2>Runner not built</h2><p>Run <code>npm run build</code> first.</p></body></html>`;
}

export function createServer(): McpServer {
const server = new McpServer({
name: "MCP Apps Conformance Server",
version: "0.1.0",
});

const cspMeta = { ui: { csp: { connectDomains: [CSP_ALLOWED_ORIGIN] } } };
registerAppResource(
server,
"Conformance Runner",
RUNNER_URI,
{
description: "Runs the MCP Apps conformance suite inside the host.",
_meta: cspMeta,
},
() => ({
contents: [
{
uri: RUNNER_URI,
mimeType: RESOURCE_MIME_TYPE,
text: loadRunnerHtml(),
_meta: cspMeta,
},
],
}),
);

registerAppTool(
server,
"run_conformance",
{
description: "Run the MCP Apps conformance test suite against this host.",
_meta: { ui: { resourceUri: RUNNER_URI, visibility: ["model", "app"] } },
},
(): CallToolResult => ({
content: [
{ type: "text", text: "Launching the MCP Apps conformance runner…" },
],
}),
);

registerAppTool(
server,
"conformance_probe",
{
description:
"Echo probe used by the conformance harness to verify tool proxying.",
inputSchema: { ping: z.string() },
_meta: { ui: { visibility: ["app"] } },
},
({ ping }): CallToolResult => ({
content: [{ type: "text", text: `echo:${ping}` }],
}),
);

// Model-only fixture tool (NOT app-visible). The visibility test calls this
// from the view; a conformant host MUST reject that call.
registerAppTool(
server,
"model_only_probe",
{
description:
"Model-only fixture; an app calling this MUST be rejected by the host.",
inputSchema: { ping: z.string() },
_meta: { ui: { visibility: ["model"] } },
},
({ ping }): CallToolResult => ({
content: [{ type: "text", text: `model-only:${ping}` }],
}),
);

return server;
}
Loading