Merge pull request #30 from modelcontextprotocol/feature/mcp-apps-example-servers
feat: add MCP Apps example servers module
This commit is contained in:
commit
58554ed503
7 changed files with 4644 additions and 61 deletions
1
.nvmrc
Normal file
1
.nvmrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
v22.20.0
|
||||||
4394
package-lock.json
generated
4394
package-lock.json
generated
File diff suppressed because it is too large
Load diff
15
package.json
15
package.json
|
|
@ -25,6 +25,19 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/ext-apps": "^0.4.0",
|
"@modelcontextprotocol/ext-apps": "^0.4.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.24.2",
|
"@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",
|
"@redis/client": "^1.6.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
|
|
@ -59,7 +72,7 @@
|
||||||
"@types/express-serve-static-core": "^5.0.2"
|
"@types/express-serve-static-core": "^5.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16"
|
"node": ">=20.16.0 || >=22.3.0"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
|
|
|
||||||
29
src/index.ts
29
src/index.ts
|
|
@ -19,6 +19,7 @@ import rateLimit from 'express-rate-limit';
|
||||||
import { config } from './config.js';
|
import { config } from './config.js';
|
||||||
import { AuthModule } from './modules/auth/index.js';
|
import { AuthModule } from './modules/auth/index.js';
|
||||||
import { MCPModule } from './modules/mcp/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 { ExternalTokenValidator, InternalTokenValidator, ITokenValidator } from './interfaces/auth-validator.js';
|
||||||
import { redisClient } from './modules/shared/redis.js';
|
import { redisClient } from './modules/shared/redis.js';
|
||||||
import { logger } from './modules/shared/logger.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 __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
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() {
|
async function main() {
|
||||||
// Determine server type based on auth mode
|
// Determine server type based on auth mode
|
||||||
const isAuthServerOnly = config.auth.mode === 'auth_server';
|
const isAuthServerOnly = config.auth.mode === 'auth_server';
|
||||||
|
|
@ -161,11 +167,23 @@ async function main() {
|
||||||
// Mount MCP routes
|
// Mount MCP routes
|
||||||
app.use('/', mcpModule.getRouter());
|
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('');
|
||||||
console.log('MCP Endpoints:');
|
console.log('MCP Endpoints:');
|
||||||
console.log(` Streamable HTTP: ${config.baseUri}/mcp`);
|
console.log(` Streamable HTTP: ${config.baseUri}/mcp`);
|
||||||
console.log(` SSE (legacy): ${config.baseUri}/sse`);
|
console.log(` SSE (legacy): ${config.baseUri}/sse`);
|
||||||
console.log(` OAuth Metadata: ${config.baseUri}/.well-known/oauth-authorization-server`);
|
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)
|
// Rate limiter for splash page (moderate limit)
|
||||||
|
|
@ -218,7 +236,16 @@ async function main() {
|
||||||
} else {
|
} else {
|
||||||
const srcStaticDir = path.join(__dirname, 'static');
|
const srcStaticDir = path.join(__dirname, 'static');
|
||||||
const splashPath = path.join(srcStaticDir, 'index.html');
|
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);
|
res.send(html);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
148
src/modules/example-apps/index.ts
Normal file
148
src/modules/example-apps/index.ts
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* 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 { 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 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 - 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();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bearer auth middleware
|
||||||
|
const bearerAuthOptions: BearerAuthMiddlewareOptions = {
|
||||||
|
verifier: this.tokenValidator,
|
||||||
|
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(new URL(this.config.baseUri))
|
||||||
|
};
|
||||||
|
const bearerAuth = requireBearerAuth(bearerAuthOptions);
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
// Create fresh server and transport for each request (stateless mode)
|
||||||
|
const server = createServer();
|
||||||
|
const transport = new StreamableHTTPServerTransport({
|
||||||
|
sessionIdGenerator: undefined, // Stateless: no session management
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
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);
|
||||||
|
|
@ -71,6 +71,14 @@
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="links">
|
||||||
<a href="https://github.com/modelcontextprotocol/example-remote-server" class="link-button">
|
<a href="https://github.com/modelcontextprotocol/example-remote-server" class="link-button">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
|
|
||||||
|
|
@ -91,10 +91,16 @@ h1 {
|
||||||
|
|
||||||
.endpoints h2 {
|
.endpoints h2 {
|
||||||
font-size: 1.75rem;
|
font-size: 1.75rem;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-description {
|
||||||
|
color: #666666;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
.endpoint-list {
|
.endpoint-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue