diff --git a/Dockerfile.sse b/Dockerfile.sse new file mode 100644 index 0000000..8e04d22 --- /dev/null +++ b/Dockerfile.sse @@ -0,0 +1,41 @@ +# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile +## Stage 1: Builder +FROM node:lts-alpine AS builder + +# Set working directory +WORKDIR /app + +# Copy all files into the container +COPY . . + +# Install dependencies without running scripts +RUN npm install --ignore-scripts + +# Build the TypeScript source code +RUN npm run build + +## Stage 2: Runtime +FROM node:lts-alpine + +WORKDIR /app + +# Install Python and other programming languages +RUN apk add --no-cache \ + python3 \ + go \ + php \ + ruby + +# Copy only the necessary files from the builder stage +COPY --from=builder /app/dist ./dist +COPY package*.json ./ + +# Install only production dependencies +RUN npm install --production --ignore-scripts + +# Use a non-root user for security (optional) +RUN adduser -D mcpuser +USER mcpuser + +# Set the entrypoint command +CMD ["node", "./dist/cli.js", "--transport", "sse"] diff --git a/package.json b/package.json index 3a0041a..b607544 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,9 @@ "scripts": { "build": "tsc && shx chmod +x dist/index.js dist/cli.js", "watch": "tsc --watch", - "start": "node ./dist/cli.js" + "start": "node ./dist/cli.js", + "start-http": "node ./dist/cli.js --transport=http", + "start-sse": "node ./dist/cli.js --transport=sse" }, "files": [ "dist" diff --git a/src/cli.ts b/src/cli.ts index 7b1b43f..b49f8cd 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -9,8 +9,8 @@ program .name('mcp-server-code-runner') .description(pkg.description) .version(pkg.version) - .option('-t, --transport ', 'Transport (stdio or http)', 'stdio') - .option('-p, --port ', 'Port number for HTTP server') + .option('-t, --transport ', 'Transport (stdio, http, or sse)', 'stdio') + .option('-p, --port ', 'Port number for HTTP/SSE server') .action(async (options) => { try { await startMcpServer(options.transport, { port: parseInt(options.port) }); diff --git a/src/index.ts b/src/index.ts index 176aa28..d6af307 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,8 @@ import { startStdioMcpServer } from "./stdio.js"; import { startStreamableHttpMcpServer, McpServerEndpoint } from "./streamableHttp.js"; +import { startSSEMcpServer } from "./sseServer.js"; -export type Transport = 'stdio' | 'http'; +export type Transport = 'stdio' | 'http' | 'sse'; export interface HttpServerOptions { port?: number; @@ -12,7 +13,9 @@ export async function startMcpServer(transport: Transport, options?: HttpServerO return startStdioMcpServer(); } else if (transport === 'http') { return startStreamableHttpMcpServer(options?.port); + } else if (transport === 'sse') { + return startSSEMcpServer(options?.port); } else { - throw new Error('Invalid transport. Must be either "stdio" or "http"'); + throw new Error('Invalid transport. Must be either "stdio", "http", or "sse"'); } } diff --git a/src/sseServer.ts b/src/sseServer.ts new file mode 100644 index 0000000..6a4e827 --- /dev/null +++ b/src/sseServer.ts @@ -0,0 +1,125 @@ +import express, { Request, Response } from "express"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; +import { createServer } from "./server.js"; + +export interface McpServerEndpoint { + url: string; + port: number; +} + +export async function startSSEMcpServer(port?: number): Promise { + const app = express(); + app.use(express.json()); + + // Store transports by session ID + const transports: { [sessionId: string]: SSEServerTransport } = {}; + + // SSE endpoint for establishing connection + app.get('/sse', async (req: Request, res: Response) => { + console.log('Received SSE connection request'); + try { + const server: McpServer = createServer(); + const transport: SSEServerTransport = new SSEServerTransport('/messages', res); + + // Store the transport by session ID + transports[transport.sessionId] = transport; + + // Clean up transport when connection closes + transport.onclose = () => { + delete transports[transport.sessionId]; + console.log(`SSE session ${transport.sessionId} closed`); + }; + + // Connect the server to the transport + await server.connect(transport); + + console.log(`SSE session ${transport.sessionId} established`); + } catch (error) { + console.error('Error establishing SSE connection:', error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: '2.0', + error: { + code: -32603, + message: 'Internal server error', + }, + id: null, + }); + } + } + }); + + // Messages endpoint for receiving client messages + app.post('/messages', async (req: Request, res: Response) => { + const sessionId = req.query.sessionId as string; + console.log(`Received message for session: ${sessionId}`); + + if (!sessionId || !transports[sessionId]) { + console.error(`No transport found for sessionId: ${sessionId}`); + res.status(400).json({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Invalid or missing session ID', + }, + id: null, + }); + return; + } + + try { + const transport = transports[sessionId]; + await transport.handlePostMessage(req, res, req.body); + } catch (error) { + console.error('Error handling SSE message:', error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: '2.0', + error: { + code: -32603, + message: 'Internal server error', + }, + id: null, + }); + } + } + }); + + // Health check endpoint + app.get('/health', (req: Request, res: Response) => { + res.json({ + status: 'ok', + transport: 'sse', + activeSessions: Object.keys(transports).length + }); + }); + + // Start the server + const PORT = Number(port || process.env.PORT || 3088); + + return new Promise((resolve, reject) => { + const appServer = app.listen(PORT, (error) => { + if (error) { + console.error('Failed to start server:', error); + reject(error); + return; + } + const endpoint: McpServerEndpoint = { + url: `http://localhost:${PORT}/sse`, + port: PORT + }; + console.log(`Code Runner SSE MCP Server listening at ${endpoint.url}`); + console.log(`Messages endpoint: http://localhost:${PORT}/messages`); + console.log(`Health check: http://localhost:${PORT}/health`); + resolve(endpoint); + }); + + // Handle server errors + appServer.on('error', (error) => { + console.error('Server error:', error); + reject(error); + }); + }); +} +