205 lines
7.7 KiB
TypeScript
205 lines
7.7 KiB
TypeScript
/**
|
|
* MCPEngine Studio — Cloudflare Worker Template
|
|
*
|
|
* Wraps a compiled MCP server in an HTTP handler:
|
|
* POST /mcp → MCP JSON-RPC handler
|
|
* GET /health → 200 OK
|
|
* GET / → Info page with server name and tool list
|
|
*/
|
|
|
|
export interface WorkerTemplateParams {
|
|
serverName: string;
|
|
serverSlug: string;
|
|
compiledCode: string;
|
|
toolNames: string[];
|
|
toolCount: number;
|
|
version?: string;
|
|
}
|
|
|
|
export function generateWorkerScript(params: WorkerTemplateParams): string {
|
|
const {
|
|
serverName,
|
|
serverSlug,
|
|
compiledCode,
|
|
toolNames,
|
|
toolCount,
|
|
version = '1.0.0',
|
|
} = params;
|
|
|
|
const toolListJSON = JSON.stringify(toolNames, null, 2);
|
|
const escapedName = serverName.replace(/'/g, "\\'");
|
|
const builtAt = new Date().toISOString();
|
|
|
|
return `
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// ${serverName} — MCPEngine Worker
|
|
// Deployed via MCPEngine Studio
|
|
// Built: ${builtAt} | Version: ${version} | Tools: ${toolCount}
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
// ── Compiled server code ─────────────────────────────────────────────────
|
|
${compiledCode}
|
|
|
|
// ── Tool registry ────────────────────────────────────────────────────────
|
|
const SERVER_NAME = '${escapedName}';
|
|
const SERVER_SLUG = '${serverSlug}';
|
|
const SERVER_VERSION = '${version}';
|
|
const TOOL_NAMES = ${toolListJSON};
|
|
|
|
// ── MCP JSON-RPC handler ─────────────────────────────────────────────────
|
|
async function handleMCPRequest(request) {
|
|
try {
|
|
const body = await request.json();
|
|
const { method, params, id } = body;
|
|
|
|
// Standard MCP methods
|
|
if (method === 'initialize') {
|
|
return jsonResponse({
|
|
jsonrpc: '2.0',
|
|
id,
|
|
result: {
|
|
protocolVersion: '2024-11-05',
|
|
capabilities: { tools: { listChanged: false } },
|
|
serverInfo: {
|
|
name: SERVER_NAME,
|
|
version: SERVER_VERSION,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
if (method === 'tools/list') {
|
|
// Delegate to server module if available
|
|
if (typeof serverModule?.listTools === 'function') {
|
|
const tools = await serverModule.listTools();
|
|
return jsonResponse({ jsonrpc: '2.0', id, result: { tools } });
|
|
}
|
|
return jsonResponse({
|
|
jsonrpc: '2.0',
|
|
id,
|
|
result: {
|
|
tools: TOOL_NAMES.map((name) => ({
|
|
name,
|
|
description: name + ' tool',
|
|
inputSchema: { type: 'object', properties: {} },
|
|
})),
|
|
},
|
|
});
|
|
}
|
|
|
|
if (method === 'tools/call') {
|
|
if (typeof serverModule?.callTool === 'function') {
|
|
const result = await serverModule.callTool(params?.name, params?.arguments ?? {});
|
|
return jsonResponse({ jsonrpc: '2.0', id, result });
|
|
}
|
|
return jsonResponse({
|
|
jsonrpc: '2.0',
|
|
id,
|
|
error: { code: -32601, message: 'Tool not found: ' + (params?.name ?? 'unknown') },
|
|
});
|
|
}
|
|
|
|
// Notifications (no response needed per spec)
|
|
if (method === 'notifications/initialized') {
|
|
return new Response(null, { status: 204 });
|
|
}
|
|
|
|
return jsonResponse({
|
|
jsonrpc: '2.0',
|
|
id,
|
|
error: { code: -32601, message: 'Method not found: ' + method },
|
|
});
|
|
} catch (err) {
|
|
return jsonResponse(
|
|
{ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error: ' + err.message } },
|
|
400,
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── Info page ────────────────────────────────────────────────────────────
|
|
function infoPage() {
|
|
const toolList = TOOL_NAMES.map((t) => '<li><code>' + t + '</code></li>').join('\\n');
|
|
const html = \`<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>\${SERVER_NAME} — MCP Server</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: system-ui, -apple-system, sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
|
|
.card { max-width: 640px; width: 90%; background: #1e293b; border-radius: 16px; padding: 2.5rem; border: 1px solid #334155; }
|
|
h1 { font-size: 1.75rem; margin-bottom: .5rem; background: linear-gradient(135deg, #818cf8, #34d399); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
|
.badge { display: inline-block; background: #10b981; color: #fff; padding: 2px 10px; border-radius: 99px; font-size: .75rem; margin-bottom: 1rem; }
|
|
.meta { color: #94a3b8; font-size: .875rem; margin-bottom: 1.5rem; }
|
|
h2 { font-size: 1rem; color: #94a3b8; margin-bottom: .75rem; }
|
|
ul { list-style: none; display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: .5rem; }
|
|
li { background: #0f172a; padding: .5rem .75rem; border-radius: 8px; font-size: .875rem; }
|
|
code { color: #818cf8; }
|
|
.endpoint { margin-top: 1.5rem; background: #0f172a; padding: 1rem; border-radius: 8px; font-family: monospace; font-size: .8rem; color: #34d399; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="card">
|
|
<h1>\${SERVER_NAME}</h1>
|
|
<span class="badge">● Live</span>
|
|
<div class="meta">Version \${SERVER_VERSION} · \${TOOL_NAMES.length} tools · Powered by MCPEngine</div>
|
|
<h2>Available Tools</h2>
|
|
<ul>\${toolList}</ul>
|
|
<div class="endpoint">POST /mcp — JSON-RPC 2.0 endpoint</div>
|
|
</div>
|
|
</body>
|
|
</html>\`;
|
|
return new Response(html, { headers: { 'Content-Type': 'text/html; charset=utf-8' } });
|
|
}
|
|
|
|
// ── Utilities ────────────────────────────────────────────────────────────
|
|
function jsonResponse(data, status = 200) {
|
|
return new Response(JSON.stringify(data), {
|
|
status,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
'Access-Control-Allow-Headers': 'Content-Type',
|
|
},
|
|
});
|
|
}
|
|
|
|
// ── Worker entry ─────────────────────────────────────────────────────────
|
|
export default {
|
|
async fetch(request, env, ctx) {
|
|
const url = new URL(request.url);
|
|
|
|
// CORS preflight
|
|
if (request.method === 'OPTIONS') {
|
|
return new Response(null, {
|
|
status: 204,
|
|
headers: {
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
'Access-Control-Allow-Headers': 'Content-Type',
|
|
},
|
|
});
|
|
}
|
|
|
|
// Routes
|
|
if (url.pathname === '/mcp' && request.method === 'POST') {
|
|
return handleMCPRequest(request);
|
|
}
|
|
|
|
if (url.pathname === '/health') {
|
|
return jsonResponse({ status: 'ok', server: SERVER_NAME, tools: TOOL_NAMES.length });
|
|
}
|
|
|
|
if (url.pathname === '/') {
|
|
return infoPage();
|
|
}
|
|
|
|
return jsonResponse({ error: 'Not found' }, 404);
|
|
},
|
|
};
|
|
`.trimStart();
|
|
}
|