automagik-genie
Version:
Self-evolving AI agent orchestration framework with Model Context Protocol support
321 lines (320 loc) ⢠15.2 kB
JavaScript
;
/**
* HTTP Server Setup for Genie MCP with Multi-Authentication Support
*
* Uses official MCP SDK with Express for HTTP transport.
* - OAuth2 Client Credentials + Authorization Code flows for ChatGPT
* - Secret Token authentication for ElevenLabs and similar services
* - Bearer token middleware from SDK
* - Streamable HTTP transport from SDK
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.startHttpServer = startHttpServer;
const express_1 = __importDefault(require("express"));
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
const bearerAuth_js_1 = require("@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js");
const oauth_provider_js_1 = require("./oauth-provider.js");
const oauth2_endpoints_js_1 = require("./oauth2-endpoints.js");
const oidc_endpoints_js_1 = require("./oidc-endpoints.js");
const secret_token_auth_js_1 = require("./secret-token-auth.js");
const crypto_1 = require("crypto");
/**
* Start HTTP server with OAuth2 authentication using official MCP SDK
*/
async function startHttpServer(options) {
const { server, oauth2Config, port, onReady } = options;
const app = (0, express_1.default)();
// Use public URL if behind tunnel (e.g., ngrok), otherwise localhost
const serverUrl = process.env.MCP_PUBLIC_URL || `http://localhost:${port}`;
// Use OAuth client secret as the secret token (dual authentication)
// This allows services like ElevenLabs to authenticate with the same secret
const secretToken = oauth2Config.clientSecret;
// Debug mode (enabled via MCP_DEBUG=1 environment variable)
const debugMode = process.env.MCP_DEBUG === '1' || process.env.DEBUG === '1';
// ========================================
// RAW REQUEST/RESPONSE LOGGER (FIRST - catches everything)
// ========================================
if (debugMode) {
app.use((req, res, next) => {
const timestamp = new Date().toISOString();
const requestId = (0, crypto_1.randomUUID)().slice(0, 8);
console.error(`\n${'='.repeat(80)}`);
console.error(`š [${timestamp}] [${requestId}] ${req.method} ${req.path}`);
console.error(`${'='.repeat(80)}`);
// Capture response status
const originalSend = res.send;
const originalJson = res.json;
const originalStatus = res.status;
let statusCode = 200;
res.status = function (code) {
statusCode = code;
return originalStatus.call(this, code);
};
res.send = function (body) {
console.error(`š¤ [${requestId}] Response: ${statusCode} ${req.method} ${req.path}`);
if (statusCode >= 400) {
console.error(` ā ļø Error response body:`, typeof body === 'string' ? body : JSON.stringify(body));
}
return originalSend.call(this, body);
};
res.json = function (body) {
console.error(`š¤ [${requestId}] Response: ${statusCode || res.statusCode} ${req.method} ${req.path}`);
if ((statusCode || res.statusCode) >= 400) {
console.error(` ā ļø Error response body:`, JSON.stringify(body));
}
return originalJson.call(this, body);
};
next();
});
}
// CORS middleware (allow ChatGPT to access OAuth endpoints)
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.header('Access-Control-Allow-Credentials', 'true');
// Handle preflight requests
if (req.method === 'OPTIONS') {
return res.sendStatus(200);
}
next();
});
// Body parser middleware with error handling
app.use(express_1.default.json());
app.use(express_1.default.urlencoded({ extended: false }));
// Catch body-parser errors (invalid JSON, etc.)
app.use((err, req, res, next) => {
if (err instanceof SyntaxError && 'body' in err) {
console.error(`ā Body parser error on ${req.method} ${req.path}:`, err.message);
console.error(` Request body: ${req.body || 'Unable to parse'}`);
return res.status(400).json({
error: 'Bad Request',
message: 'Invalid JSON in request body'
});
}
next(err);
});
// Debug logging middleware (logs all incoming requests)
if (debugMode) {
app.use((req, res, next) => {
const timestamp = new Date().toISOString();
console.error(`\n${'='.repeat(80)}`);
console.error(`š [${timestamp}] ${req.method} ${req.path}`);
console.error(`${'='.repeat(80)}`);
// Log query parameters
if (Object.keys(req.query).length > 0) {
console.error('š Query Parameters:');
Object.entries(req.query).forEach(([key, value]) => {
console.error(` ${key}: ${value}`);
});
}
// Log headers (filter sensitive ones)
console.error('šØ Headers:');
Object.entries(req.headers).forEach(([key, value]) => {
if (key.toLowerCase() === 'authorization') {
console.error(` ${key}: ${String(value).substring(0, 20)}...`);
}
else {
console.error(` ${key}: ${value}`);
}
});
// Log body (for POST/PUT requests)
if (req.body && Object.keys(req.body).length > 0) {
console.error('š¦ Body:');
console.error(JSON.stringify(req.body, null, 2));
}
console.error(`${'='.repeat(80)}\n`);
next();
});
}
// Create OAuth provider for token verification
const oauthProvider = new oauth_provider_js_1.GenieOAuthProvider(oauth2Config, serverUrl);
// Ensure default ChatGPT client is registered
(0, oidc_endpoints_js_1.ensureDefaultChatGPTClient)(oauth2Config.clientId);
// Create dual authentication middleware (OAuth2 + Secret Token)
const oauthMiddleware = (0, bearerAuth_js_1.requireBearerAuth)({
verifier: oauthProvider,
requiredScopes: ['mcp:read', 'mcp:write'],
resourceMetadataUrl: `${serverUrl}/.well-known/oauth-protected-resource`
});
const dualAuthMiddleware = (0, secret_token_auth_js_1.requireOAuth2OrSecretToken)(oauthMiddleware, {
secretToken: secretToken
});
// ========================================
// Public Endpoints (no authentication)
// ========================================
// Health check endpoint
app.get('/health', (req, res) => {
res.json({
status: 'ok',
service: 'genie-mcp-server',
transport: 'httpStream',
auth: 'oauth2'
});
});
// OpenID Connect Discovery (for ChatGPT)
app.get('/.well-known/openid-configuration', (req, res) => {
(0, oidc_endpoints_js_1.handleOpenIDConfiguration)(req, res, serverUrl);
});
// OAuth 2.0 Authorization Server Metadata (for MCP spec)
app.get('/.well-known/oauth-authorization-server', (req, res) => {
(0, oidc_endpoints_js_1.handleAuthorizationServerMetadata)(req, res, serverUrl);
});
// OAuth2 protected resource metadata endpoint (RFC 9728) - kept for backward compatibility
app.get('/.well-known/oauth-protected-resource', (req, res) => {
(0, oauth2_endpoints_js_1.handleProtectedResourceMetadata)(req, res, serverUrl);
});
// OAuth2 token endpoint (client_credentials + authorization_code flows)
app.post('/oauth/token', (req, res) => {
(0, oauth2_endpoints_js_1.handleTokenEndpoint)(req, res, oauth2Config, serverUrl);
});
// OAuth2 dynamic client registration (RFC 7591)
app.post('/oauth2/register', (req, res) => {
(0, oidc_endpoints_js_1.handleClientRegistration)(req, res);
});
// OAuth2 authorization endpoint (GET - shows consent page)
app.get('/oauth2/authorize', (req, res) => {
(0, oidc_endpoints_js_1.handleAuthorizationRequest)(req, res);
});
// OAuth2 authorization consent endpoint (POST - processes user consent)
app.post('/oauth2/authorize/consent', (req, res) => {
(0, oidc_endpoints_js_1.handleAuthorizationConsent)(req, res);
});
// ========================================
// Protected Endpoints (OAuth2 required)
// ========================================
// MCP endpoint - protected by dual authentication (OAuth2 or Secret Token)
// IMPORTANT: Create a NEW transport for EACH request to prevent race conditions
// (per MCP SDK docs - stateless mode prevents request ID collisions)
app.post('/mcp', dualAuthMiddleware, async (req, res) => {
try {
// Create new transport for this request (stateless mode)
const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // Stateless - no session management
enableJsonResponse: true // Required for initialize handshake
});
// Close transport when response completes
res.on('close', () => {
transport.close();
});
// Connect server to this transport
await server.server.connect(transport);
// Handle the request
await transport.handleRequest(req, res, req.body);
if (debugMode) {
console.error('ā
POST /mcp handled successfully');
}
}
catch (error) {
console.error('ā Error handling POST /mcp:', error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: '2.0',
error: {
code: -32603,
message: 'Internal server error'
},
id: null
});
}
}
});
// SSE endpoint for streaming responses (also protected by dual authentication)
// IMPORTANT: Create a NEW transport for EACH request to prevent race conditions
app.get('/mcp', dualAuthMiddleware, async (req, res) => {
try {
// Create new transport for this request (stateless mode)
const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // Stateless - no session management
enableJsonResponse: true
});
// Close transport when response completes
res.on('close', () => {
transport.close();
});
// Connect server to this transport
await server.server.connect(transport);
// Handle the request
await transport.handleRequest(req, res);
if (debugMode) {
console.error('ā
GET /mcp (SSE) connection established');
}
}
catch (error) {
console.error('ā Error handling GET /mcp:', error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: '2.0',
error: {
code: -32603,
message: 'Internal server error'
},
id: null
});
}
}
});
// ========================================
// Error handling
// ========================================
// 404 handler
app.use((req, res) => {
res.status(404).json({
error: 'Not Found',
message: `Endpoint ${req.method} ${req.path} not found`
});
});
// Global error handler
app.use((err, req, res, next) => {
console.error('Server error:', err);
res.status(err.status || 500).json({
error: err.name || 'Internal Server Error',
message: err.message || 'An unexpected error occurred'
});
});
// ========================================
// Start server
// ========================================
return new Promise((resolve) => {
app.listen(port, () => {
if (debugMode) {
// Verbose mode - show everything
console.error(`\nā
Genie MCP Server started successfully`);
console.error(` HTTP Stream: ${serverUrl}/mcp`);
console.error(` SSE Stream: ${serverUrl}/mcp (GET)`);
console.error(` Health: ${serverUrl}/health`);
console.error(`\nš OAuth 2.0 Endpoints:`);
console.error(` āā OIDC Discovery: ${serverUrl}/.well-known/openid-configuration`);
console.error(` āā Authorization: ${serverUrl}/oauth2/authorize`);
console.error(` āā Token Exchange: ${serverUrl}/oauth/token`);
console.error(` āā Client Registration: ${serverUrl}/oauth2/register`);
console.error(` āā Resource Metadata: ${serverUrl}/.well-known/oauth-protected-resource`);
console.error(`\nš Authentication Methods:`);
console.error(` āā OAuth2: Authorization Code + PKCE (ChatGPT)`);
console.error(` āā OAuth2: Client Credentials (machine-to-machine)`);
console.error(` āā Secret Token: Bearer/Header/Query (ElevenLabs, etc.)`);
console.error(`\nāļø OAuth Config:`);
console.error(` āā Issuer: ${oauth2Config.issuer}`);
console.error(` āā Client ID: ${oauth2Config.clientId}`);
console.error(` āā Client Secret: ${oauth2Config.clientSecret}`);
console.error(` ā āā (Also used as secret token for services like ElevenLabs)`);
console.error(` āā Authorization PIN: ${oauth2Config.pin || 'NOT SET'}`);
console.error(` āā Token Expiry: ${oauth2Config.tokenExpiry}s`);
console.error(`\nš” Transport: Streamable HTTP (MCP SDK official)`);
console.error(`\nš DEBUG MODE ENABLED`);
console.error(` All incoming requests will be logged`);
}
else {
// Concise mode - minimal output (PIN already shown by genie-cli)
// No additional logging - keeps terminal clean
}
if (onReady) {
onReady(serverUrl);
}
resolve();
});
});
}