feat: add MCP Apps example servers module

Wire up 13 MCP Apps servers from the @modelcontextprotocol/server-* packages:
- budget-allocator: Interactive budget allocation UI
- cohort-heatmap: Customer retention heatmap
- customer-segmentation: Scatter chart with clustering
- map: Interactive 3D globe viewer (CesiumJS)
- pdf: Interactive PDF viewer
- scenario-modeler: SaaS business projections dashboard
- shadertoy: Real-time GLSL shader renderer
- sheet-music: ABC notation to sheet music visualizer
- system-monitor: Real-time OS metrics display
- threejs: 3D scene renderer
- transcript: Live speech transcription
- video-resource: Binary video streaming via MCP resources
- wiki-explorer: Wikipedia link graph visualization

Each server is mounted at /{slug}/mcp and shares the same OAuth
authentication as the main MCP server.

Also updates the splash page to dynamically list all available
example servers.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jerome Swannack 2026-01-16 15:05:59 +00:00
commit 56a0835fd4
6 changed files with 4690 additions and 61 deletions

4394
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -25,6 +25,19 @@
"dependencies": {
"@modelcontextprotocol/ext-apps": "^0.4.0",
"@modelcontextprotocol/sdk": "^1.24.2",
"@modelcontextprotocol/server-budget-allocator": "^0.4.1",
"@modelcontextprotocol/server-cohort-heatmap": "^0.4.1",
"@modelcontextprotocol/server-customer-segmentation": "^0.4.1",
"@modelcontextprotocol/server-map": "^0.4.1",
"@modelcontextprotocol/server-pdf": "^0.4.1",
"@modelcontextprotocol/server-scenario-modeler": "^0.4.1",
"@modelcontextprotocol/server-shadertoy": "^0.4.1",
"@modelcontextprotocol/server-sheet-music": "^0.4.1",
"@modelcontextprotocol/server-system-monitor": "^0.4.1",
"@modelcontextprotocol/server-threejs": "^0.4.1",
"@modelcontextprotocol/server-transcript": "^0.4.1",
"@modelcontextprotocol/server-video-resource": "^0.4.1",
"@modelcontextprotocol/server-wiki-explorer": "^0.4.1",
"@redis/client": "^1.6.0",
"cors": "^2.8.5",
"dotenv": "^16.4.7",
@ -59,7 +72,7 @@
"@types/express-serve-static-core": "^5.0.2"
},
"engines": {
"node": ">=16"
"node": ">=20.16.0 || >=22.3.0"
},
"author": "",
"license": "MIT"

View file

@ -19,6 +19,7 @@ import rateLimit from 'express-rate-limit';
import { config } from './config.js';
import { AuthModule } from './modules/auth/index.js';
import { MCPModule } from './modules/mcp/index.js';
import { ExampleAppsModule, AVAILABLE_EXAMPLES } from './modules/example-apps/index.js';
import { ExternalTokenValidator, InternalTokenValidator, ITokenValidator } from './interfaces/auth-validator.js';
import { redisClient } from './modules/shared/redis.js';
import { logger } from './modules/shared/logger.js';
@ -26,6 +27,11 @@ import { logger } from './modules/shared/logger.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Format slug to human-readable name (e.g., 'budget-allocator' -> 'Budget Allocator')
function formatServerName(slug: string): string {
return slug.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
}
async function main() {
// Determine server type based on auth mode
const isAuthServerOnly = config.auth.mode === 'auth_server';
@ -161,11 +167,23 @@ async function main() {
// Mount MCP routes
app.use('/', mcpModule.getRouter());
// Mount Example Apps module (MCP Apps servers at /:slug/mcp)
const exampleAppsModule = new ExampleAppsModule(
{ baseUri: config.baseUri },
tokenValidator
);
app.use('/', exampleAppsModule.getRouter());
console.log('');
console.log('MCP Endpoints:');
console.log(` Streamable HTTP: ${config.baseUri}/mcp`);
console.log(` SSE (legacy): ${config.baseUri}/sse`);
console.log(` OAuth Metadata: ${config.baseUri}/.well-known/oauth-authorization-server`);
console.log('');
console.log('MCP App Example Servers:');
for (const slug of AVAILABLE_EXAMPLES) {
console.log(` ${config.baseUri}/${slug}/mcp`);
}
}
// Rate limiter for splash page (moderate limit)
@ -218,7 +236,16 @@ async function main() {
} else {
const srcStaticDir = path.join(__dirname, 'static');
const splashPath = path.join(srcStaticDir, 'index.html');
const html = fs.readFileSync(splashPath, 'utf8');
let html = fs.readFileSync(splashPath, 'utf8');
// Inject example server endpoints
const exampleServersHtml = AVAILABLE_EXAMPLES.map(slug => `
<div class="endpoint">
<span class="method post">POST</span>
<span>/${slug}/mcp - ${formatServerName(slug)} MCP App Server</span>
</div>`).join('');
html = html.replace('<!-- EXAMPLE_SERVERS_PLACEHOLDER -->', exampleServersHtml);
res.send(html);
}
});

View file

@ -0,0 +1,195 @@
/**
* Example Apps Module - Mounts ext-apps example servers at /:slug/mcp
*
* Each example MCP App server is mounted at its own path, sharing the same
* OAuth authentication as the main MCP server.
*/
import { Router, Request, Response, NextFunction } from 'express';
import cors from 'cors';
import { BearerAuthMiddlewareOptions, requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js';
import { getOAuthProtectedResourceMetadataUrl } from '@modelcontextprotocol/sdk/server/auth/router.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
import { randomUUID } from 'crypto';
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { ITokenValidator } from '../../interfaces/auth-validator.js';
import { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
// Import createServer from each example package (compiled JS)
// All packages are published on the public npm registry
import { createServer as createBudgetAllocatorServer } from '@modelcontextprotocol/server-budget-allocator';
import { createServer as createCohortHeatmapServer } from '@modelcontextprotocol/server-cohort-heatmap';
import { createServer as createCustomerSegmentationServer } from '@modelcontextprotocol/server-customer-segmentation';
import { createServer as createMapServer } from '@modelcontextprotocol/server-map';
import { createServer as createPdfServer } from '@modelcontextprotocol/server-pdf';
import { createServer as createScenarioModelerServer } from '@modelcontextprotocol/server-scenario-modeler';
import { createServer as createShadertoyServer } from '@modelcontextprotocol/server-shadertoy';
import { createServer as createSheetMusicServer } from '@modelcontextprotocol/server-sheet-music';
import { createServer as createSystemMonitorServer } from '@modelcontextprotocol/server-system-monitor';
import { createServer as createThreejsServer } from '@modelcontextprotocol/server-threejs';
import { createServer as createTranscriptServer } from '@modelcontextprotocol/server-transcript';
import { createServer as createVideoResourceServer } from '@modelcontextprotocol/server-video-resource';
import { createServer as createWikiExplorerServer } from '@modelcontextprotocol/server-wiki-explorer';
declare module "express-serve-static-core" {
interface Request {
auth?: AuthInfo;
}
}
// Map of slug to createServer function
const EXAMPLE_SERVERS: Record<string, () => McpServer> = {
'budget-allocator': createBudgetAllocatorServer,
'cohort-heatmap': createCohortHeatmapServer,
'customer-segmentation': createCustomerSegmentationServer,
'map': createMapServer,
'pdf': createPdfServer,
'scenario-modeler': createScenarioModelerServer,
'shadertoy': createShadertoyServer,
'sheet-music': createSheetMusicServer,
'system-monitor': createSystemMonitorServer,
'threejs': createThreejsServer,
'transcript': createTranscriptServer,
'video-resource': createVideoResourceServer,
'wiki-explorer': createWikiExplorerServer,
};
export interface ExampleAppsConfig {
baseUri: string;
}
// Session store: maps sessionId to { transport, server } per slug
const sessions = new Map<string, {
transport: StreamableHTTPServerTransport;
server: McpServer;
slug: string;
}>();
export class ExampleAppsModule {
private router: Router;
constructor(
private config: ExampleAppsConfig,
private tokenValidator: ITokenValidator
) {
this.router = this.setupRouter();
}
getRouter(): Router {
return this.router;
}
private setupRouter(): Router {
const router = Router();
// CORS configuration
const corsOptions = {
origin: true,
methods: ['GET', 'POST', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization', 'Mcp-Protocol-Version', 'Mcp-Protocol-Id', 'Mcp-Session-Id'],
exposedHeaders: ['Mcp-Protocol-Version', 'Mcp-Protocol-Id', 'Mcp-Session-Id'],
credentials: true
};
// Security headers
const securityHeaders = (req: Request, res: Response, next: NextFunction) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
next();
};
// Bearer auth middleware
const bearerAuthOptions: BearerAuthMiddlewareOptions = {
verifier: this.tokenValidator,
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(new URL(this.config.baseUri))
};
const bearerAuth = requireBearerAuth(bearerAuthOptions);
// Handler for /:slug/mcp
const handleExampleMcp = async (req: Request, res: Response) => {
const { slug } = req.params;
const createServer = EXAMPLE_SERVERS[slug];
if (!createServer) {
res.status(404).json({
jsonrpc: "2.0",
error: { code: -32001, message: `Unknown example server: ${slug}` },
id: null,
});
return;
}
let transport: StreamableHTTPServerTransport | undefined;
try {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
// Check for existing session
if (sessionId) {
const session = sessions.get(sessionId);
if (session && session.slug === slug) {
await session.transport.handleRequest(req, res, req.body);
return;
} else if (session) {
res.status(400).json({
jsonrpc: "2.0",
error: { code: -32000, message: "Session belongs to different server" },
id: null,
});
return;
}
}
// New session - must be initialize request
if (!isInitializeRequest(req.body)) {
res.status(400).json({
jsonrpc: "2.0",
error: { code: -32000, message: "Bad request: not initialized" },
id: null,
});
return;
}
// Create new server instance
const server = createServer();
const newSessionId = randomUUID();
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => newSessionId,
onsessioninitialized: (id) => {
sessions.set(id, { transport: transport!, server, slug });
},
});
transport.onclose = () => {
if (transport?.sessionId) {
sessions.delete(transport.sessionId);
}
};
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
} catch (error) {
console.error(`Error handling ${slug} MCP request:`, error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: "2.0",
error: { code: -32603, message: "Internal error" },
id: null,
});
}
}
};
// Mount routes for each example server
router.get('/:slug/mcp', cors(corsOptions), bearerAuth, securityHeaders, handleExampleMcp);
router.post('/:slug/mcp', cors(corsOptions), bearerAuth, securityHeaders, handleExampleMcp);
router.delete('/:slug/mcp', cors(corsOptions), bearerAuth, securityHeaders, handleExampleMcp);
return router;
}
}
// Export list of available examples for documentation
export const AVAILABLE_EXAMPLES = Object.keys(EXAMPLE_SERVERS);

View file

@ -71,6 +71,14 @@
</div>
</div>
<div class="endpoints">
<h2>MCP App Example Servers</h2>
<p class="section-description">Interactive MCP servers with rich UI capabilities. Each server demonstrates different MCP Apps features.</p>
<div class="endpoint-list" id="example-servers">
<!-- EXAMPLE_SERVERS_PLACEHOLDER -->
</div>
</div>
<div class="links">
<a href="https://github.com/modelcontextprotocol/example-remote-server" class="link-button">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">

View file

@ -91,10 +91,16 @@ h1 {
.endpoints h2 {
font-size: 1.75rem;
margin-bottom: 1.5rem;
margin-bottom: 1rem;
font-weight: 600;
}
.section-description {
color: #666666;
margin-bottom: 1.5rem;
line-height: 1.5;
}
.endpoint-list {
display: flex;
flex-direction: column;