From ee3d7714c252d18cc14d84cbbeadcbf5b174a9a6 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Wed, 4 Mar 2026 10:05:08 +0100 Subject: [PATCH] feat(mcps): migrate gitea and memory MCPs to SSE transport on pm2 --- ecosystem.mcps.config.cjs | 24 ++++++++++++++++++++++++ package.json | 9 ++++++--- packages/gitea-mcp/package.json | 15 +++++++++------ packages/gitea-mcp/src/index.ts | 27 +++++++++++++++++++++++---- packages/gitea-mcp/src/start.ts | 16 ++++++++++++++++ packages/memory-mcp/package.json | 9 ++++++--- packages/memory-mcp/src/index.ts | 27 +++++++++++++++++++++++---- packages/memory-mcp/src/start.ts | 16 ++++++++++++++++ 8 files changed, 123 insertions(+), 20 deletions(-) create mode 100644 ecosystem.mcps.config.cjs create mode 100644 packages/gitea-mcp/src/start.ts create mode 100644 packages/memory-mcp/src/start.ts diff --git a/ecosystem.mcps.config.cjs b/ecosystem.mcps.config.cjs new file mode 100644 index 0000000..6c03bc7 --- /dev/null +++ b/ecosystem.mcps.config.cjs @@ -0,0 +1,24 @@ +module.exports = { + apps: [ + { + name: 'gitea-mcp', + script: 'node', + args: 'dist/start.js', + cwd: './packages/gitea-mcp', + watch: false, + env: { + NODE_ENV: 'production' + } + }, + { + name: 'memory-mcp', + script: 'node', + args: 'dist/start.js', + cwd: './packages/memory-mcp', + watch: false, + env: { + NODE_ENV: 'production' + } + } + ] +}; diff --git a/package.json b/package.json index a07b75c..7ce5433 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,11 @@ "dev": "pnpm -r dev", "dev:gatekeeper": "bash -c 'trap \"COMPOSE_PROJECT_NAME=gatekeeper docker-compose -f docker-compose.gatekeeper.yml down\" EXIT INT TERM; docker network create infra 2>/dev/null || true && COMPOSE_PROJECT_NAME=gatekeeper docker-compose -f docker-compose.gatekeeper.yml down && COMPOSE_PROJECT_NAME=gatekeeper docker-compose -f docker-compose.gatekeeper.yml up --build --remove-orphans'", "dev:mcps:up": "docker-compose -f docker-compose.mcps.yml up -d", - "dev:mcps:down": "docker-compose -f docker-compose.mcps.yml down", + "dev:mcps:down": "docker-compose -f docker-compose.mcps.yml down && pm2 delete ecosystem.mcps.config.cjs || true", "dev:mcps:watch": "pnpm -r --filter=\"./packages/*-mcp\" exec tsc -w", - "dev:mcps": "npm run dev:mcps:up && npm run dev:mcps:watch", + "dev:mcps": "npm run dev:mcps:up && pm2 start ecosystem.mcps.config.cjs --watch && npm run dev:mcps:watch", + "start:mcps:run": "pm2 start ecosystem.mcps.config.cjs", + "start:mcps": "npm run dev:mcps:up && npm run start:mcps:run", "lint": "pnpm -r --filter='./packages/**' --filter='./apps/**' lint", "test": "pnpm -r test", "changeset": "changeset", @@ -40,6 +42,7 @@ "husky": "^9.1.7", "jsdom": "^27.4.0", "lint-staged": "^16.2.7", + "pm2": "^6.0.14", "prettier": "^3.8.1", "tsx": "^4.21.0", "typescript": "^5.0.0", @@ -72,4 +75,4 @@ "@sentry/nextjs": "10.38.0" } } -} +} \ No newline at end of file diff --git a/packages/gitea-mcp/package.json b/packages/gitea-mcp/package.json index d159631..a157f01 100644 --- a/packages/gitea-mcp/package.json +++ b/packages/gitea-mcp/package.json @@ -6,15 +6,18 @@ "type": "module", "scripts": { "build": "tsc", - "start": "node --env-file-if-exists=../../.env dist/index.js" + "start": "node dist/start.js" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.5.0", - "zod": "^3.23.8", - "axios": "^1.7.2" + "axios": "^1.7.2", + "dotenv": "^17.3.1", + "express": "^5.2.1", + "zod": "^3.23.8" }, "devDependencies": { - "typescript": "^5.5.3", - "@types/node": "^20.14.10" + "@types/express": "^5.0.6", + "@types/node": "^20.14.10", + "typescript": "^5.5.3" } -} +} \ No newline at end of file diff --git a/packages/gitea-mcp/src/index.ts b/packages/gitea-mcp/src/index.ts index 92b04b1..a3eaf3a 100644 --- a/packages/gitea-mcp/src/index.ts +++ b/packages/gitea-mcp/src/index.ts @@ -1,5 +1,6 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; +import express from 'express'; import { CallToolRequestSchema, ListToolsRequestSchema, @@ -251,9 +252,27 @@ async function pollSubscriptions() { async function run() { - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error("Gitea MCP Native Server running on stdio"); + const app = express(); + let transport: SSEServerTransport | null = null; + + app.get('/sse', async (req, res) => { + console.error('New SSE connection established'); + transport = new SSEServerTransport('/message', res); + await server.connect(transport); + }); + + app.post('/message', async (req, res) => { + if (!transport) { + res.status(400).send('No active SSE connection'); + return; + } + await transport.handlePostMessage(req, res); + }); + + const PORT = process.env.GITEA_MCP_PORT || 3001; + app.listen(PORT, () => { + console.error(`Gitea MCP Native Server running on http://localhost:${PORT}/sse`); + }); // Start the background poller pollSubscriptions(); diff --git a/packages/gitea-mcp/src/start.ts b/packages/gitea-mcp/src/start.ts new file mode 100644 index 0000000..be9ab8a --- /dev/null +++ b/packages/gitea-mcp/src/start.ts @@ -0,0 +1,16 @@ +import { config } from 'dotenv'; +import { resolve } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = fileURLToPath(new URL('.', import.meta.url)); + +// Try to load .env.local first (contains credentials usually) +config({ path: resolve(__dirname, '../../../.env.local') }); +// Fallback to .env (contains defaults) +config({ path: resolve(__dirname, '../../../.env') }); + +// Now boot the compiled MCP index +import('./index.js').catch(err => { + console.error('Failed to start MCP Server:', err); + process.exit(1); +}); diff --git a/packages/memory-mcp/package.json b/packages/memory-mcp/package.json index 2dddec2..08ff7c7 100644 --- a/packages/memory-mcp/package.json +++ b/packages/memory-mcp/package.json @@ -6,7 +6,7 @@ "type": "module", "scripts": { "build": "tsc", - "start": "node --env-file-if-exists=../../.env dist/index.js", + "start": "node dist/start.js", "dev": "tsx watch src/index.ts", "test:unit": "vitest run" }, @@ -14,12 +14,15 @@ "@modelcontextprotocol/sdk": "^1.5.0", "@qdrant/js-client-rest": "^1.12.0", "@xenova/transformers": "^2.17.2", + "dotenv": "^17.3.1", + "express": "^5.2.1", "zod": "^3.23.8" }, "devDependencies": { - "typescript": "^5.5.3", + "@types/express": "^5.0.6", "@types/node": "^20.14.10", "tsx": "^4.19.1", + "typescript": "^5.5.3", "vitest": "^2.1.3" } -} +} \ No newline at end of file diff --git a/packages/memory-mcp/src/index.ts b/packages/memory-mcp/src/index.ts index e2e937f..1251358 100644 --- a/packages/memory-mcp/src/index.ts +++ b/packages/memory-mcp/src/index.ts @@ -1,5 +1,6 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; +import express from 'express'; import { z } from 'zod'; import { QdrantMemoryService } from './qdrant.js'; @@ -67,9 +68,27 @@ async function main() { } ); - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error('Memory MCP server is running and ready to accept connections over stdio.'); + const app = express(); + let transport: SSEServerTransport | null = null; + + app.get('/sse', async (req, res) => { + console.error('New SSE connection established'); + transport = new SSEServerTransport('/message', res); + await server.connect(transport); + }); + + app.post('/message', async (req, res) => { + if (!transport) { + res.status(400).send('No active SSE connection'); + return; + } + await transport.handlePostMessage(req, res); + }); + + const PORT = process.env.MEMORY_MCP_PORT || 3002; + app.listen(PORT, () => { + console.error(`Memory MCP server is running on http://localhost:${PORT}/sse`); + }); } main().catch((error) => { diff --git a/packages/memory-mcp/src/start.ts b/packages/memory-mcp/src/start.ts new file mode 100644 index 0000000..be9ab8a --- /dev/null +++ b/packages/memory-mcp/src/start.ts @@ -0,0 +1,16 @@ +import { config } from 'dotenv'; +import { resolve } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = fileURLToPath(new URL('.', import.meta.url)); + +// Try to load .env.local first (contains credentials usually) +config({ path: resolve(__dirname, '../../../.env.local') }); +// Fallback to .env (contains defaults) +config({ path: resolve(__dirname, '../../../.env') }); + +// Now boot the compiled MCP index +import('./index.js').catch(err => { + console.error('Failed to start MCP Server:', err); + process.exit(1); +});