From fe5bb5f50794bdcf4f92845b596098d8325db33a Mon Sep 17 00:00:00 2001 From: Priyanshu Thapliyal <114170980+Priyanshuthapliyal2005@users.noreply.github.com> Date: Fri, 3 Oct 2025 01:09:09 +0530 Subject: [PATCH 1/2] feat: add HTTP and index transports to the MCP server --- packages/mcp-server-supabase/package.json | 4 +- .../src/transports/http.ts | 423 +++++++++++++ .../src/transports/index.ts | 576 ++++++++++++++++++ packages/mcp-server-supabase/tsup.config.ts | 2 + 4 files changed, 1004 insertions(+), 1 deletion(-) create mode 100644 packages/mcp-server-supabase/src/transports/http.ts create mode 100644 packages/mcp-server-supabase/src/transports/index.ts diff --git a/packages/mcp-server-supabase/package.json b/packages/mcp-server-supabase/package.json index ec06c13..a8b6ef7 100644 --- a/packages/mcp-server-supabase/package.json +++ b/packages/mcp-server-supabase/package.json @@ -26,7 +26,9 @@ }, "files": ["dist/**/*"], "bin": { - "mcp-server-supabase": "./dist/transports/stdio.js" + "mcp-server-supabase": "./dist/transports/index.js", + "mcp-server-supabase-http": "./dist/transports/http.js", + "mcp-server-supabase-stdio": "./dist/transports/stdio.js" }, "exports": { ".": { diff --git a/packages/mcp-server-supabase/src/transports/http.ts b/packages/mcp-server-supabase/src/transports/http.ts new file mode 100644 index 0000000..a5f5125 --- /dev/null +++ b/packages/mcp-server-supabase/src/transports/http.ts @@ -0,0 +1,423 @@ +#!/usr/bin/env node + +import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { AsyncLocalStorage } from 'node:async_hooks'; +import { createServer, type IncomingMessage, type Server } from 'node:http'; +import { parseArgs } from 'node:util'; +import packageJson from '../../package.json' with { type: 'json' }; +import { createSupabaseApiPlatform } from '../platform/api-platform.js'; +import { createSupabaseMcpServer } from '../server.js'; +import { parseList } from './util.js'; + +const { version } = packageJson; + +// AsyncLocalStorage for managing per-request state +export const asyncLocalStorage = new AsyncLocalStorage<{ + accessToken: string; +}>(); + +// Store SSE transports by session ID for the deprecated SSE protocol +const sseTransports: Record = {}; + +const DEFAULT_PORT = 3000; + +/** + * Extract client IP from request headers with fallback chain + */ +function getClientIp(req: IncomingMessage): string | undefined { + const forwarded = req.headers['x-forwarded-for']; + if (forwarded) { + return typeof forwarded === 'string' + ? forwarded.split(',')[0]?.trim() + : forwarded[0]; + } + + const realIp = req.headers['x-real-ip']; + if (realIp) { + return typeof realIp === 'string' ? realIp : realIp[0]; + } + + return req.socket.remoteAddress; +} + +/** + * Extract authentication token from request headers + */ +function extractAuthToken(req: IncomingMessage): string | undefined { + const authHeader = req.headers.authorization; + if (authHeader) { + const auth = typeof authHeader === 'string' ? authHeader : authHeader[0]; + // Remove 'Bearer ' prefix if present + if (auth.startsWith('Bearer ')) { + return auth.substring(7).trim(); + } + return auth; + } + + // Try alternative header names + const xAuthToken = req.headers['x-auth-token'] || req.headers['x-access-token']; + if (xAuthToken) { + return typeof xAuthToken === 'string' ? xAuthToken : xAuthToken[0]; + } + + return undefined; +} + +/** + * Create a server instance for a request + */ +function createServerInstance( + projectId?: string, + readOnly?: boolean, + features?: string[], + apiUrl?: string +) { + const store = asyncLocalStorage.getStore(); + if (!store) { + throw new Error('Access token not found in AsyncLocalStorage'); + } + + const platform = createSupabaseApiPlatform({ + accessToken: store.accessToken, + apiUrl, + }); + + return createSupabaseMcpServer({ + platform, + projectId, + readOnly, + features, + }); +} + +async function main() { + const { + values: { + ['project-ref']: projectId, + ['read-only']: readOnly, + ['api-url']: apiUrl, + ['port']: portArg, + ['version']: showVersion, + ['features']: cliFeatures, + }, + } = parseArgs({ + options: { + ['project-ref']: { + type: 'string', + }, + ['read-only']: { + type: 'boolean', + default: false, + }, + ['api-url']: { + type: 'string', + }, + ['port']: { + type: 'string', + }, + ['version']: { + type: 'boolean', + }, + ['features']: { + type: 'string', + }, + }, + }); + + if (showVersion) { + console.log(version); + process.exit(0); + } + + const features = cliFeatures ? parseList(cliFeatures) : undefined; + const port = portArg ? parseInt(portArg, 10) : DEFAULT_PORT; + + if (isNaN(port)) { + console.error(`Invalid port: ${portArg}`); + process.exit(1); + } + + const httpServer: Server = createServer(async (req, res) => { + try { + const url = new URL(req.url || '/', `http://${req.headers.host}`); + const pathname = url.pathname; + + // Set CORS headers for all responses + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS,DELETE'); + res.setHeader( + 'Access-Control-Allow-Headers', + 'Content-Type, Authorization, X-Auth-Token, X-Access-Token, MCP-Session-Id, MCP-Protocol-Version' + ); + res.setHeader('Access-Control-Expose-Headers', 'MCP-Session-Id'); + + // Handle preflight OPTIONS requests + if (req.method === 'OPTIONS') { + res.writeHead(200); + res.end(); + return; + } + + // Extract authentication token + const accessToken = extractAuthToken(req); + + if (!accessToken) { + res.writeHead(401, { 'Content-Type': 'application/json' }); + res.end( + JSON.stringify({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Missing authentication token. Provide via Authorization header or X-Auth-Token header.', + }, + id: null, + }) + ); + return; + } + + // Get client IP for logging/analytics + const clientIp = getClientIp(req); + if (clientIp) { + console.error(`Request from ${clientIp} to ${pathname}`); + } + + //============================================================================= + // STREAMABLE HTTP TRANSPORT (PROTOCOL VERSION 2025-03-26) + //============================================================================= + + if (pathname === '/mcp' && req.method === 'POST') { + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + }); + + // Create server inside AsyncLocalStorage context + await asyncLocalStorage.run({ accessToken }, async () => { + const server = createServerInstance( + projectId, + readOnly, + features, + apiUrl + ); + + try { + await server.connect(transport); + + // Parse request body for streamable HTTP + let body = ''; + req.on('data', (chunk) => { + body += chunk.toString(); + }); + + await new Promise((resolve, reject) => { + req.on('end', async () => { + try { + const requestBody = body ? JSON.parse(body) : undefined; + await transport.handleRequest(req, res, requestBody); + resolve(); + } catch (error) { + reject(error); + } + }); + req.on('error', reject); + }); + } catch (error) { + console.error('Error handling MCP request:', error); + if (!res.headersSent) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end( + JSON.stringify({ + jsonrpc: '2.0', + error: { + code: -32603, + message: 'Internal server error', + }, + id: null, + }) + ); + } + } finally { + res.on('close', () => { + transport.close(); + server.close(); + }); + } + }); + + return; + } + + if (pathname === '/mcp' && req.method === 'GET') { + res.writeHead(405, { 'Content-Type': 'application/json' }); + res.end( + JSON.stringify({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Method not allowed. Use POST for MCP requests.', + }, + id: null, + }) + ); + return; + } + + //============================================================================= + // DEPRECATED HTTP+SSE TRANSPORT (PROTOCOL VERSION 2024-11-05) + //============================================================================= + + if (pathname === '/sse' && req.method === 'GET') { + console.warn( + 'Warning: SSE transport is deprecated. Please use the /mcp endpoint with StreamableHTTP transport.' + ); + + const transport = new SSEServerTransport('/messages', res); + sseTransports[transport.sessionId] = transport; + + res.on('close', () => { + delete sseTransports[transport.sessionId]; + transport.close(); + }); + + await asyncLocalStorage.run({ accessToken }, async () => { + const server = createServerInstance( + projectId, + readOnly, + features, + apiUrl + ); + + await server.connect(transport); + }); + + return; + } + + if (pathname === '/messages' && req.method === 'POST') { + const sessionId = url.searchParams.get('sessionId'); + + if (!sessionId) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end( + JSON.stringify({ + error: 'Missing sessionId parameter', + }) + ); + return; + } + + const transport = sseTransports[sessionId]; + + if (!transport) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end( + JSON.stringify({ + error: `No transport found for sessionId: ${sessionId}`, + }) + ); + return; + } + + await asyncLocalStorage.run({ accessToken }, async () => { + await transport.handlePostMessage(req, res); + }); + + return; + } + + //============================================================================= + // HEALTH CHECK & INFO ENDPOINTS + //============================================================================= + + if (pathname === '/health' || pathname === '/ping') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end( + JSON.stringify({ + status: 'ok', + version, + message: 'Supabase MCP Server', + }) + ); + return; + } + + // 404 for unknown routes + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end( + JSON.stringify({ + error: 'Not found', + availableEndpoints: [ + 'POST /mcp - StreamableHTTP transport (recommended)', + 'GET /sse - SSE transport (deprecated)', + 'POST /messages?sessionId=... - SSE message handler', + 'GET /health - Health check', + ], + }) + ); + } catch (error) { + console.error('Unhandled error in request handler:', error); + if (!res.headersSent) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end( + JSON.stringify({ + jsonrpc: '2.0', + error: { + code: -32603, + message: 'Internal server error', + }, + id: null, + }) + ); + } + } + }); + + // Start server with port fallback + function startServer(attemptPort: number, maxAttempts = 10): void { + httpServer.once('error', (err: NodeJS.ErrnoException) => { + if (err.code === 'EADDRINUSE' && attemptPort < port + maxAttempts) { + console.warn(`Port ${attemptPort} is in use, trying port ${attemptPort + 1}...`); + startServer(attemptPort + 1, maxAttempts); + } else { + console.error(`Failed to start server: ${err.message}`); + process.exit(1); + } + }); + + httpServer.listen(attemptPort, () => { + console.error(`Supabase MCP Server v${version} running on HTTP`); + console.error(` - StreamableHTTP: http://localhost:${attemptPort}/mcp`); + console.error(` - SSE (deprecated): http://localhost:${attemptPort}/sse`); + console.error(` - Health check: http://localhost:${attemptPort}/health`); + console.error(''); + console.error('Authentication: Provide Supabase access token via:'); + console.error(' - Authorization: Bearer '); + console.error(' - X-Auth-Token: '); + }); + } + + startServer(port); + + // Graceful shutdown + process.on('SIGINT', () => { + console.error('\nShutting down server...'); + httpServer.close(() => { + console.error('Server closed'); + process.exit(0); + }); + }); + + process.on('SIGTERM', () => { + console.error('\nShutting down server...'); + httpServer.close(() => { + console.error('Server closed'); + process.exit(0); + }); + }); +} + +main().catch((error) => { + console.error('Fatal error:', error); + process.exit(1); +}); diff --git a/packages/mcp-server-supabase/src/transports/index.ts b/packages/mcp-server-supabase/src/transports/index.ts new file mode 100644 index 0000000..10199ad --- /dev/null +++ b/packages/mcp-server-supabase/src/transports/index.ts @@ -0,0 +1,576 @@ +#!/usr/bin/env node + +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { AsyncLocalStorage } from 'node:async_hooks'; +import { createServer, type IncomingMessage, type Server } from 'node:http'; +import { parseArgs } from 'node:util'; +import packageJson from '../../package.json' with { type: 'json' }; +import { createSupabaseApiPlatform } from '../platform/api-platform.js'; +import { createSupabaseMcpServer } from '../server.js'; +import { parseList } from './util.js'; + +const { version } = packageJson; + +// AsyncLocalStorage for managing per-request state in HTTP mode +const asyncLocalStorage = new AsyncLocalStorage<{ + accessToken: string; +}>(); + +// Store SSE transports by session ID for the deprecated SSE protocol +const sseTransports: Record = {}; + +const DEFAULT_PORT = 3000; +const ALLOWED_TRANSPORTS = ['stdio', 'http'] as const; +type TransportType = (typeof ALLOWED_TRANSPORTS)[number]; + +/** + * Extract client IP from request headers with fallback chain + */ +function getClientIp(req: IncomingMessage): string | undefined { + const forwarded = req.headers['x-forwarded-for']; + if (forwarded) { + return typeof forwarded === 'string' + ? forwarded.split(',')[0]?.trim() + : forwarded[0]; + } + + const realIp = req.headers['x-real-ip']; + if (realIp) { + return typeof realIp === 'string' ? realIp : realIp[0]; + } + + return req.socket.remoteAddress; +} + +/** + * Extract authentication token from request headers + */ +function extractAuthToken(req: IncomingMessage): string | undefined { + const authHeader = req.headers.authorization; + if (authHeader) { + const auth = typeof authHeader === 'string' ? authHeader : authHeader[0]; + // Remove 'Bearer ' prefix if present + if (auth.startsWith('Bearer ')) { + return auth.substring(7).trim(); + } + return auth; + } + + // Try alternative header names + const xAuthToken = + req.headers['x-auth-token'] || req.headers['x-access-token']; + if (xAuthToken) { + return typeof xAuthToken === 'string' ? xAuthToken : xAuthToken[0]; + } + + return undefined; +} + +/** + * Create a server instance for HTTP mode (stateless, per-request) + */ +function createHttpServerInstance( + projectId?: string, + readOnly?: boolean, + features?: string[], + apiUrl?: string +) { + const store = asyncLocalStorage.getStore(); + if (!store) { + throw new Error('Access token not found in AsyncLocalStorage'); + } + + const platform = createSupabaseApiPlatform({ + accessToken: store.accessToken, + apiUrl, + }); + + return createSupabaseMcpServer({ + platform, + projectId, + readOnly, + features, + }); +} + +/** + * Start HTTP server with both StreamableHTTP and SSE support + */ +async function startHttpServer( + port: number, + projectId?: string, + readOnly?: boolean, + features?: string[], + apiUrl?: string +) { + const httpServer: Server = createServer(async (req, res) => { + try { + const url = new URL(req.url || '/', `http://${req.headers.host}`); + const pathname = url.pathname; + + // Set CORS headers for all responses + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS,DELETE'); + res.setHeader( + 'Access-Control-Allow-Headers', + 'Content-Type, Authorization, X-Auth-Token, X-Access-Token, MCP-Session-Id, MCP-Protocol-Version' + ); + res.setHeader('Access-Control-Expose-Headers', 'MCP-Session-Id'); + + // Handle preflight OPTIONS requests + if (req.method === 'OPTIONS') { + res.writeHead(200); + res.end(); + return; + } + + // Extract authentication token + const accessToken = extractAuthToken(req); + + if (!accessToken) { + res.writeHead(401, { 'Content-Type': 'application/json' }); + res.end( + JSON.stringify({ + jsonrpc: '2.0', + error: { + code: -32000, + message: + 'Missing authentication token. Provide via Authorization header or X-Auth-Token header.', + }, + id: null, + }) + ); + return; + } + + // Get client IP for logging/analytics + const clientIp = getClientIp(req); + if (clientIp) { + console.error(`Request from ${clientIp} to ${pathname}`); + } + + //============================================================================= + // STREAMABLE HTTP TRANSPORT (PROTOCOL VERSION 2025-03-26) + //============================================================================= + + if (pathname === '/mcp' && req.method === 'POST') { + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + }); + + // Create server inside AsyncLocalStorage context + await asyncLocalStorage.run({ accessToken }, async () => { + const server = createHttpServerInstance( + projectId, + readOnly, + features, + apiUrl + ); + + try { + await server.connect(transport); + + // Parse request body for streamable HTTP + let body = ''; + req.on('data', (chunk) => { + body += chunk.toString(); + }); + + await new Promise((resolve, reject) => { + req.on('end', async () => { + try { + const requestBody = body ? JSON.parse(body) : undefined; + await transport.handleRequest(req, res, requestBody); + resolve(); + } catch (error) { + reject(error); + } + }); + req.on('error', reject); + }); + } catch (error) { + console.error('Error handling MCP request:', error); + if (!res.headersSent) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end( + JSON.stringify({ + jsonrpc: '2.0', + error: { + code: -32603, + message: 'Internal server error', + }, + id: null, + }) + ); + } + } finally { + res.on('close', () => { + transport.close(); + server.close(); + }); + } + }); + + return; + } + + if (pathname === '/mcp' && req.method === 'GET') { + res.writeHead(405, { 'Content-Type': 'application/json' }); + res.end( + JSON.stringify({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Method not allowed. Use POST for MCP requests.', + }, + id: null, + }) + ); + return; + } + + //============================================================================= + // DEPRECATED HTTP+SSE TRANSPORT (PROTOCOL VERSION 2024-11-05) + //============================================================================= + + if (pathname === '/sse' && req.method === 'GET') { + console.warn( + 'Warning: SSE transport is deprecated. Please use the /mcp endpoint with StreamableHTTP transport.' + ); + + const transport = new SSEServerTransport('/messages', res); + sseTransports[transport.sessionId] = transport; + + res.on('close', () => { + delete sseTransports[transport.sessionId]; + transport.close(); + }); + + await asyncLocalStorage.run({ accessToken }, async () => { + const server = createHttpServerInstance( + projectId, + readOnly, + features, + apiUrl + ); + + await server.connect(transport); + }); + + return; + } + + if (pathname === '/messages' && req.method === 'POST') { + const sessionId = url.searchParams.get('sessionId'); + + if (!sessionId) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end( + JSON.stringify({ + error: 'Missing sessionId parameter', + }) + ); + return; + } + + const transport = sseTransports[sessionId]; + + if (!transport) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end( + JSON.stringify({ + error: `No transport found for sessionId: ${sessionId}`, + }) + ); + return; + } + + await asyncLocalStorage.run({ accessToken }, async () => { + await transport.handlePostMessage(req, res); + }); + + return; + } + + //============================================================================= + // HEALTH CHECK & INFO ENDPOINTS + //============================================================================= + + if (pathname === '/health' || pathname === '/ping') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end( + JSON.stringify({ + status: 'ok', + version, + message: 'Supabase MCP Server', + }) + ); + return; + } + + // 404 for unknown routes + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end( + JSON.stringify({ + error: 'Not found', + availableEndpoints: [ + 'POST /mcp - StreamableHTTP transport (recommended)', + 'GET /sse - SSE transport (deprecated)', + 'POST /messages?sessionId=... - SSE message handler', + 'GET /health - Health check', + ], + }) + ); + } catch (error) { + console.error('Unhandled error in request handler:', error); + if (!res.headersSent) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end( + JSON.stringify({ + jsonrpc: '2.0', + error: { + code: -32603, + message: 'Internal server error', + }, + id: null, + }) + ); + } + } + }); + + // Start server with port fallback + function startServer(attemptPort: number, maxAttempts = 10): void { + httpServer.once('error', (err: NodeJS.ErrnoException) => { + if (err.code === 'EADDRINUSE' && attemptPort < port + maxAttempts) { + console.warn( + `Port ${attemptPort} is in use, trying port ${attemptPort + 1}...` + ); + startServer(attemptPort + 1, maxAttempts); + } else { + console.error(`Failed to start server: ${err.message}`); + process.exit(1); + } + }); + + httpServer.listen(attemptPort, () => { + console.error(`Supabase MCP Server v${version} running on HTTP`); + console.error(` - StreamableHTTP: http://localhost:${attemptPort}/mcp`); + console.error( + ` - SSE (deprecated): http://localhost:${attemptPort}/sse` + ); + console.error( + ` - Health check: http://localhost:${attemptPort}/health` + ); + console.error(''); + console.error('Authentication: Provide Supabase access token via:'); + console.error(' - Authorization: Bearer '); + console.error(' - X-Auth-Token: '); + }); + } + + startServer(port); + + // Graceful shutdown + process.on('SIGINT', () => { + console.error('\nShutting down server...'); + httpServer.close(() => { + console.error('Server closed'); + process.exit(0); + }); + }); + + process.on('SIGTERM', () => { + console.error('\nShutting down server...'); + httpServer.close(() => { + console.error('Server closed'); + process.exit(0); + }); + }); +} + +/** + * Start STDIO server (stateful, single connection) + */ +async function startStdioServer( + accessToken: string, + projectId?: string, + readOnly?: boolean, + features?: string[], + apiUrl?: string +) { + const platform = createSupabaseApiPlatform({ + accessToken, + apiUrl, + }); + + const server = createSupabaseMcpServer({ + platform, + projectId, + readOnly, + features, + }); + + const transport = new StdioServerTransport(); + await server.connect(transport); + + console.error('Supabase MCP Server running on STDIO'); +} + +async function main() { + const { + values: { + ['access-token']: cliAccessToken, + ['project-ref']: projectId, + ['read-only']: readOnly, + ['api-url']: apiUrl, + ['transport']: cliTransport, + ['port']: portArg, + ['version']: showVersion, + ['features']: cliFeatures, + ['help']: showHelp, + }, + } = parseArgs({ + options: { + ['access-token']: { + type: 'string', + }, + ['project-ref']: { + type: 'string', + }, + ['read-only']: { + type: 'boolean', + default: false, + }, + ['api-url']: { + type: 'string', + }, + ['transport']: { + type: 'string', + }, + ['port']: { + type: 'string', + }, + ['version']: { + type: 'boolean', + }, + ['features']: { + type: 'string', + }, + ['help']: { + type: 'boolean', + }, + }, + }); + + if (showHelp) { + console.log(` +Supabase MCP Server v${version} + +Usage: + mcp-server-supabase [options] + +Options: + --transport Transport type (default: stdio) + --port Port for HTTP transport (default: 3000) + --access-token Supabase access token (required for stdio, env: SUPABASE_ACCESS_TOKEN) + --project-ref Project reference ID to scope to specific project + --read-only Run in read-only mode (default: false) + --api-url Custom Supabase API URL + --features Comma-separated list of features to enable + --version Show version + --help Show this help + +Transport Modes: + stdio - Standard I/O transport (default, stateful, single connection) + Requires --access-token or SUPABASE_ACCESS_TOKEN env var + + http - HTTP server transport (stateless, multiple concurrent connections) + Authentication via headers: Authorization: Bearer or X-Auth-Token: + Endpoints: + - POST /mcp - StreamableHTTP transport (recommended) + - GET /sse - SSE transport (deprecated) + - GET /health - Health check + +Examples: + # STDIO mode (for MCP clients like Claude Desktop, Cursor) + mcp-server-supabase --access-token + + # HTTP mode (for hosting/scaling, web clients) + mcp-server-supabase --transport http --port 3000 + + # HTTP mode with specific project + mcp-server-supabase --transport http --port 3000 --project-ref + + # HTTP mode with specific features + mcp-server-supabase --transport http --features "database,functions,debugging" + +Environment Variables: + SUPABASE_ACCESS_TOKEN Access token for STDIO mode +`); + process.exit(0); + } + + if (showVersion) { + console.log(version); + process.exit(0); + } + + // Parse transport type + const transport: TransportType = + (cliTransport as TransportType) || 'stdio'; + + if (!ALLOWED_TRANSPORTS.includes(transport)) { + console.error( + `Invalid transport: ${cliTransport}. Must be one of: ${ALLOWED_TRANSPORTS.join(', ')}` + ); + process.exit(1); + } + + // Parse features + const features = cliFeatures ? parseList(cliFeatures) : undefined; + + // Parse port + const port = portArg ? parseInt(portArg, 10) : DEFAULT_PORT; + if (isNaN(port)) { + console.error(`Invalid port: ${portArg}`); + process.exit(1); + } + + // Validate flags based on transport + if (transport === 'stdio' && portArg) { + console.error('--port flag is not allowed with --transport stdio'); + process.exit(1); + } + + if (transport === 'http' && cliAccessToken) { + console.error( + '--access-token flag is not allowed with --transport http. Use header-based authentication instead.' + ); + process.exit(1); + } + + // Start server based on transport type + if (transport === 'http') { + await startHttpServer(port, projectId, readOnly, features, apiUrl); + } else { + // STDIO mode + const accessToken = cliAccessToken ?? process.env.SUPABASE_ACCESS_TOKEN; + + if (!accessToken) { + console.error( + 'Please provide a personal access token (PAT) with the --access-token flag or set the SUPABASE_ACCESS_TOKEN environment variable' + ); + process.exit(1); + } + + await startStdioServer(accessToken, projectId, readOnly, features, apiUrl); + } +} + +main().catch((error) => { + console.error('Fatal error:', error); + process.exit(1); +}); diff --git a/packages/mcp-server-supabase/tsup.config.ts b/packages/mcp-server-supabase/tsup.config.ts index b647276..21dc47a 100644 --- a/packages/mcp-server-supabase/tsup.config.ts +++ b/packages/mcp-server-supabase/tsup.config.ts @@ -5,6 +5,8 @@ export default defineConfig([ entry: [ 'src/index.ts', 'src/transports/stdio.ts', + 'src/transports/http.ts', + 'src/transports/index.ts', 'src/platform/index.ts', 'src/platform/api-platform.ts', ], From d31f0d42e586a2cbca42e1f845996ea8c99703e0 Mon Sep 17 00:00:00 2001 From: Priyanshu Thapliyal <114170980+Priyanshuthapliyal2005@users.noreply.github.com> Date: Fri, 3 Oct 2025 01:12:54 +0530 Subject: [PATCH 2/2] refactor: remove deprecated SSE transport support from HTTP server --- .../src/transports/http.ts | 73 +---------------- .../src/transports/index.ts | 79 +------------------ 2 files changed, 4 insertions(+), 148 deletions(-) diff --git a/packages/mcp-server-supabase/src/transports/http.ts b/packages/mcp-server-supabase/src/transports/http.ts index a5f5125..3ecbf80f 100644 --- a/packages/mcp-server-supabase/src/transports/http.ts +++ b/packages/mcp-server-supabase/src/transports/http.ts @@ -1,6 +1,5 @@ #!/usr/bin/env node -import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { AsyncLocalStorage } from 'node:async_hooks'; import { createServer, type IncomingMessage, type Server } from 'node:http'; @@ -17,9 +16,6 @@ export const asyncLocalStorage = new AsyncLocalStorage<{ accessToken: string; }>(); -// Store SSE transports by session ID for the deprecated SSE protocol -const sseTransports: Record = {}; - const DEFAULT_PORT = 3000; /** @@ -184,7 +180,7 @@ async function main() { } //============================================================================= - // STREAMABLE HTTP TRANSPORT (PROTOCOL VERSION 2025-03-26) + // STREAMABLE HTTP TRANSPORT //============================================================================= if (pathname === '/mcp' && req.method === 'POST') { @@ -263,69 +259,6 @@ async function main() { return; } - //============================================================================= - // DEPRECATED HTTP+SSE TRANSPORT (PROTOCOL VERSION 2024-11-05) - //============================================================================= - - if (pathname === '/sse' && req.method === 'GET') { - console.warn( - 'Warning: SSE transport is deprecated. Please use the /mcp endpoint with StreamableHTTP transport.' - ); - - const transport = new SSEServerTransport('/messages', res); - sseTransports[transport.sessionId] = transport; - - res.on('close', () => { - delete sseTransports[transport.sessionId]; - transport.close(); - }); - - await asyncLocalStorage.run({ accessToken }, async () => { - const server = createServerInstance( - projectId, - readOnly, - features, - apiUrl - ); - - await server.connect(transport); - }); - - return; - } - - if (pathname === '/messages' && req.method === 'POST') { - const sessionId = url.searchParams.get('sessionId'); - - if (!sessionId) { - res.writeHead(400, { 'Content-Type': 'application/json' }); - res.end( - JSON.stringify({ - error: 'Missing sessionId parameter', - }) - ); - return; - } - - const transport = sseTransports[sessionId]; - - if (!transport) { - res.writeHead(400, { 'Content-Type': 'application/json' }); - res.end( - JSON.stringify({ - error: `No transport found for sessionId: ${sessionId}`, - }) - ); - return; - } - - await asyncLocalStorage.run({ accessToken }, async () => { - await transport.handlePostMessage(req, res); - }); - - return; - } - //============================================================================= // HEALTH CHECK & INFO ENDPOINTS //============================================================================= @@ -349,8 +282,6 @@ async function main() { error: 'Not found', availableEndpoints: [ 'POST /mcp - StreamableHTTP transport (recommended)', - 'GET /sse - SSE transport (deprecated)', - 'POST /messages?sessionId=... - SSE message handler', 'GET /health - Health check', ], }) @@ -388,7 +319,6 @@ async function main() { httpServer.listen(attemptPort, () => { console.error(`Supabase MCP Server v${version} running on HTTP`); console.error(` - StreamableHTTP: http://localhost:${attemptPort}/mcp`); - console.error(` - SSE (deprecated): http://localhost:${attemptPort}/sse`); console.error(` - Health check: http://localhost:${attemptPort}/health`); console.error(''); console.error('Authentication: Provide Supabase access token via:'); @@ -399,7 +329,6 @@ async function main() { startServer(port); - // Graceful shutdown process.on('SIGINT', () => { console.error('\nShutting down server...'); httpServer.close(() => { diff --git a/packages/mcp-server-supabase/src/transports/index.ts b/packages/mcp-server-supabase/src/transports/index.ts index 10199ad..c0959f4 100644 --- a/packages/mcp-server-supabase/src/transports/index.ts +++ b/packages/mcp-server-supabase/src/transports/index.ts @@ -1,7 +1,6 @@ #!/usr/bin/env node import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { AsyncLocalStorage } from 'node:async_hooks'; import { createServer, type IncomingMessage, type Server } from 'node:http'; @@ -18,9 +17,6 @@ const asyncLocalStorage = new AsyncLocalStorage<{ accessToken: string; }>(); -// Store SSE transports by session ID for the deprecated SSE protocol -const sseTransports: Record = {}; - const DEFAULT_PORT = 3000; const ALLOWED_TRANSPORTS = ['stdio', 'http'] as const; type TransportType = (typeof ALLOWED_TRANSPORTS)[number]; @@ -96,7 +92,7 @@ function createHttpServerInstance( } /** - * Start HTTP server with both StreamableHTTP and SSE support + * Start HTTP server with StreamableHTTP support */ async function startHttpServer( port: number, @@ -152,7 +148,7 @@ async function startHttpServer( } //============================================================================= - // STREAMABLE HTTP TRANSPORT (PROTOCOL VERSION 2025-03-26) + // STREAMABLE HTTP TRANSPORT //============================================================================= if (pathname === '/mcp' && req.method === 'POST') { @@ -231,69 +227,6 @@ async function startHttpServer( return; } - //============================================================================= - // DEPRECATED HTTP+SSE TRANSPORT (PROTOCOL VERSION 2024-11-05) - //============================================================================= - - if (pathname === '/sse' && req.method === 'GET') { - console.warn( - 'Warning: SSE transport is deprecated. Please use the /mcp endpoint with StreamableHTTP transport.' - ); - - const transport = new SSEServerTransport('/messages', res); - sseTransports[transport.sessionId] = transport; - - res.on('close', () => { - delete sseTransports[transport.sessionId]; - transport.close(); - }); - - await asyncLocalStorage.run({ accessToken }, async () => { - const server = createHttpServerInstance( - projectId, - readOnly, - features, - apiUrl - ); - - await server.connect(transport); - }); - - return; - } - - if (pathname === '/messages' && req.method === 'POST') { - const sessionId = url.searchParams.get('sessionId'); - - if (!sessionId) { - res.writeHead(400, { 'Content-Type': 'application/json' }); - res.end( - JSON.stringify({ - error: 'Missing sessionId parameter', - }) - ); - return; - } - - const transport = sseTransports[sessionId]; - - if (!transport) { - res.writeHead(400, { 'Content-Type': 'application/json' }); - res.end( - JSON.stringify({ - error: `No transport found for sessionId: ${sessionId}`, - }) - ); - return; - } - - await asyncLocalStorage.run({ accessToken }, async () => { - await transport.handlePostMessage(req, res); - }); - - return; - } - //============================================================================= // HEALTH CHECK & INFO ENDPOINTS //============================================================================= @@ -317,8 +250,6 @@ async function startHttpServer( error: 'Not found', availableEndpoints: [ 'POST /mcp - StreamableHTTP transport (recommended)', - 'GET /sse - SSE transport (deprecated)', - 'POST /messages?sessionId=... - SSE message handler', 'GET /health - Health check', ], }) @@ -358,9 +289,6 @@ async function startHttpServer( httpServer.listen(attemptPort, () => { console.error(`Supabase MCP Server v${version} running on HTTP`); console.error(` - StreamableHTTP: http://localhost:${attemptPort}/mcp`); - console.error( - ` - SSE (deprecated): http://localhost:${attemptPort}/sse` - ); console.error( ` - Health check: http://localhost:${attemptPort}/health` ); @@ -490,8 +418,7 @@ Transport Modes: http - HTTP server transport (stateless, multiple concurrent connections) Authentication via headers: Authorization: Bearer or X-Auth-Token: Endpoints: - - POST /mcp - StreamableHTTP transport (recommended) - - GET /sse - SSE transport (deprecated) + - POST /mcp - StreamableHTTP transport - GET /health - Health check Examples: