Mounts @modelcontextprotocol/server-lazy-auth as an additional example, served at /lazy-auth/mcp alongside the other example MCP App servers. Unlike the other examples (plain createServer() factories behind the stateless handler), lazy-auth's value is its HTTP layer: public tools work unauthenticated, protected tools answer 401 + WWW-Authenticate, and a built-in mock OAuth authorization server completes the flow. So it mounts its full Express app (createApp()) under /lazy-auth. - New src/modules/example-apps/lazy-auth.ts mounts the app before the host middleware and adds the RFC 8414/9728 path-insertion well-known rewrite (/.well-known/oauth-authorization-server/lazy-auth -> into the mount) that SDK clients require for discovery under a base path. - PUBLIC_URL for the example is derived from this server's own BASE_URI, so no extra deploy-time configuration is needed; an explicit PUBLIC_URL still wins (e.g. a tunnel). - Integration test covers the full flow: public initialize/tools/list, 401 on the protected tool, mount-prefixed discovery metadata, and the PKCE -> token -> authed get_secret round trip.
170 lines
6.8 KiB
TypeScript
170 lines
6.8 KiB
TypeScript
/**
|
|
* Example Apps Module - Mounts ext-apps example servers at /:slug/mcp
|
|
*
|
|
* Each example MCP App server is mounted at its own path without authentication.
|
|
* The root /mcp endpoint requires OAuth bearer token authentication, but these
|
|
* additional example servers are publicly accessible.
|
|
*
|
|
* These servers run in STATELESS mode - each request creates a fresh server
|
|
* instance without maintaining session state across requests.
|
|
*/
|
|
|
|
import { Router, Request, Response, NextFunction } from 'express';
|
|
import cors from 'cors';
|
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
import { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
|
|
import { isInitializeRequest } from '@modelcontextprotocol/sdk/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 createDebugServer } from '@modelcontextprotocol/server-debug';
|
|
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';
|
|
|
|
import { LAZY_AUTH_SLUG } from './lazy-auth.js';
|
|
|
|
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,
|
|
'debug': createDebugServer,
|
|
'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;
|
|
}
|
|
|
|
export class ExampleAppsModule {
|
|
private router: Router;
|
|
|
|
constructor(
|
|
private config: ExampleAppsConfig,
|
|
) {
|
|
this.router = this.setupRouter();
|
|
}
|
|
|
|
getRouter(): Router {
|
|
return this.router;
|
|
}
|
|
|
|
private setupRouter(): Router {
|
|
const router = Router();
|
|
|
|
// CORS configuration - intentionally permissive for public MCP reference server
|
|
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();
|
|
};
|
|
|
|
// Handler for /:slug/mcp - stateless: each request creates a fresh server
|
|
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" },
|
|
id: null,
|
|
});
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Log initialization requests to inspect client capabilities/extensions
|
|
if (isInitializeRequest(req.body)) {
|
|
const initParams = req.body?.params;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const capabilities = initParams?.capabilities as Record<string, any> | undefined;
|
|
console.log(JSON.stringify({
|
|
severity: 'INFO',
|
|
message: `=== MCP INITIALIZE REQUEST (${slug}) ===`,
|
|
timestamp: new Date().toISOString(),
|
|
slug,
|
|
clientInfo: initParams?.clientInfo,
|
|
protocolVersion: initParams?.protocolVersion,
|
|
capabilities,
|
|
hasExtensions: !!capabilities?.extensions,
|
|
extensions: capabilities?.extensions,
|
|
rawMessage: req.body
|
|
}));
|
|
}
|
|
|
|
// Create fresh server and transport for each request (stateless mode)
|
|
const server = createServer();
|
|
const transport = new StreamableHTTPServerTransport({
|
|
sessionIdGenerator: undefined, // Stateless: no session management
|
|
});
|
|
|
|
res.on('close', () => {
|
|
transport.close();
|
|
server.close();
|
|
});
|
|
|
|
await server.connect(transport);
|
|
await transport.handleRequest(req, res, req.body);
|
|
} catch (error) {
|
|
console.error('Error handling 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 (unauthenticated)
|
|
router.get('/:slug/mcp', cors(corsOptions), securityHeaders, handleExampleMcp);
|
|
router.post('/:slug/mcp', cors(corsOptions), securityHeaders, handleExampleMcp);
|
|
router.delete('/:slug/mcp', cors(corsOptions), securityHeaders, handleExampleMcp);
|
|
|
|
return router;
|
|
}
|
|
}
|
|
|
|
// Export list of available examples for documentation. The lazy-auth example
|
|
// is served at the same /:slug/mcp path shape, but as a full Express app with
|
|
// its own OAuth endpoints rather than through the stateless handler above
|
|
// (see ./lazy-auth.ts).
|
|
export const AVAILABLE_EXAMPLES = [...Object.keys(EXAMPLE_SERVERS), LAZY_AUTH_SLUG];
|