From 8486261555968b5101d7c1cc40ecc6926259db03 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Tue, 10 Mar 2026 13:32:16 +0100 Subject: [PATCH] fix(mcp): refactor all mcp servers to use multi-session sse transport --- .env | 4 +- docker-compose.mcps.yml | 14 -- packages/gitea-mcp/Dockerfile | 4 +- packages/gitea-mcp/src/index.ts | 29 +++- packages/glitchtip-mcp/src/index.ts | 30 +++- packages/kabelfachmann-mcp/Dockerfile | 11 -- packages/kabelfachmann-mcp/package.json | 31 ---- packages/kabelfachmann-mcp/src/index.ts | 142 ------------------ packages/kabelfachmann-mcp/src/ingest.ts | 123 --------------- packages/kabelfachmann-mcp/src/llm.ts | 90 ----------- packages/kabelfachmann-mcp/src/qdrant.ts | 104 ------------- packages/kabelfachmann-mcp/src/start.ts | 16 -- .../kabelfachmann-mcp/test-kabelfachmann.js | 38 ----- packages/kabelfachmann-mcp/tsconfig.json | 15 -- packages/klz-payload-mcp/src/index.ts | 28 +++- packages/memory-mcp/src/index.ts | 28 +++- packages/serpbear-mcp/src/index.ts | 28 +++- packages/umami-mcp/src/index.ts | 28 +++- 18 files changed, 144 insertions(+), 619 deletions(-) delete mode 100644 packages/kabelfachmann-mcp/Dockerfile delete mode 100644 packages/kabelfachmann-mcp/package.json delete mode 100644 packages/kabelfachmann-mcp/src/index.ts delete mode 100644 packages/kabelfachmann-mcp/src/ingest.ts delete mode 100644 packages/kabelfachmann-mcp/src/llm.ts delete mode 100644 packages/kabelfachmann-mcp/src/qdrant.ts delete mode 100644 packages/kabelfachmann-mcp/src/start.ts delete mode 100644 packages/kabelfachmann-mcp/test-kabelfachmann.js delete mode 100644 packages/kabelfachmann-mcp/tsconfig.json diff --git a/.env b/.env index 1e221df..4e85276 100644 --- a/.env +++ b/.env @@ -13,9 +13,9 @@ DATA_FOR_SEO_LOGIN=marc@mintel.me DATA_FOR_SEO_PASSWORD=244b0cfb38f7523d # Kabelfachmann LLM Configuration -KABELFACHMANN_LLM_PROVIDER=ollama +KABELFACHMANN_LLM_PROVIDER=openrouter KABELFACHMANN_OLLAMA_MODEL=qwen3.5 -KABELFACHMANN_OLLAMA_HOST=http://127.0.0.1:11434 +KABELFACHMANN_OLLAMA_HOST=http://host.docker.internal:11434 # Authentication GATEKEEPER_PASSWORD=mintel diff --git a/docker-compose.mcps.yml b/docker-compose.mcps.yml index c299acf..4976480 100644 --- a/docker-compose.mcps.yml +++ b/docker-compose.mcps.yml @@ -85,20 +85,6 @@ services: networks: - mcp-network - kabelfachmann-mcp: - build: - context: ./packages/kabelfachmann-mcp - container_name: kabelfachmann-mcp - env_file: - - .env - ports: - - "3007:3007" - depends_on: - - qdrant - restart: unless-stopped - networks: - - mcp-network - networks: mcp-network: driver: bridge diff --git a/packages/gitea-mcp/Dockerfile b/packages/gitea-mcp/Dockerfile index 2ef2160..70998c2 100644 --- a/packages/gitea-mcp/Dockerfile +++ b/packages/gitea-mcp/Dockerfile @@ -15,5 +15,5 @@ COPY --from=builder /app/package.json ./ COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/dist ./dist -# Use node to run the compiled index.js -ENTRYPOINT ["node", "dist/index.js"] +# Use node to run the compiled start.js +ENTRYPOINT ["node", "dist/start.js"] diff --git a/packages/gitea-mcp/src/index.ts b/packages/gitea-mcp/src/index.ts index aab79ab..abbe7af 100644 --- a/packages/gitea-mcp/src/index.ts +++ b/packages/gitea-mcp/src/index.ts @@ -1,6 +1,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import express from "express"; +import crypto from "crypto"; import { CallToolRequestSchema, ListToolsRequestSchema, @@ -1265,17 +1266,35 @@ async function run() { console.error("Gitea MCP server is running on stdio"); } else { const app = express(); - let transport: SSEServerTransport | null = null; + const transports = new Map(); + + // Middleware to log all requests for debugging + app.use((req, _res, next) => { + console.error(`${req.method} ${req.url}`); + next(); + }); app.get("/sse", async (req, res) => { - console.error("New SSE connection established"); - transport = new SSEServerTransport("/message", res); + const sessionId = crypto.randomUUID(); + console.error(`New SSE connection: ${sessionId}`); + const transport = new SSEServerTransport(`/message/${sessionId}`, res); + transports.set(sessionId, transport); + + req.on("close", () => { + console.error(`SSE connection closed: ${sessionId}`); + transports.delete(sessionId); + }); + await server.connect(transport); }); - app.post("/message", async (req, res) => { + app.post("/message/:sessionId", async (req, res) => { + const { sessionId } = req.params; + const transport = transports.get(sessionId); + if (!transport) { - res.status(400).send("No active SSE connection"); + console.error(`No transport found for session: ${sessionId}`); + res.status(400).send("No active SSE connection for this session"); return; } await transport.handlePostMessage(req, res); diff --git a/packages/glitchtip-mcp/src/index.ts b/packages/glitchtip-mcp/src/index.ts index a3a40bb..1f5b8f0 100644 --- a/packages/glitchtip-mcp/src/index.ts +++ b/packages/glitchtip-mcp/src/index.ts @@ -1,6 +1,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; -import express from 'express'; +import express, { Request, Response } from 'express'; +import crypto from 'crypto'; import { CallToolRequestSchema, ListToolsRequestSchema, @@ -141,17 +142,34 @@ async function run() { console.error('GlitchTip MCP server is running on stdio'); } else { const app = express(); - let transport: SSEServerTransport | null = null; + const transports = new Map(); + + app.use((req, _res, next) => { + console.error(`${req.method} ${req.url}`); + next(); + }); app.get('/sse', async (req, res) => { - console.error('New SSE connection established'); - transport = new SSEServerTransport('/message', res); + const sessionId = crypto.randomUUID(); + console.error(`New SSE connection: ${sessionId}`); + const transport = new SSEServerTransport(`/message/${sessionId}`, res); + transports.set(sessionId, transport); + + req.on('close', () => { + console.error(`SSE connection closed: ${sessionId}`); + transports.delete(sessionId); + }); + await server.connect(transport); }); - app.post('/message', async (req, res) => { + app.post('/message/:sessionId', async (req: Request, res: Response) => { + const sessionId = req.params.sessionId; + const transport = transports.get(sessionId as string); + if (!transport) { - res.status(400).send('No active SSE connection'); + console.error(`No transport found for session: ${sessionId}`); + res.status(400).send('No active SSE connection for this session'); return; } await transport.handlePostMessage(req, res); diff --git a/packages/kabelfachmann-mcp/Dockerfile b/packages/kabelfachmann-mcp/Dockerfile deleted file mode 100644 index da517e6..0000000 --- a/packages/kabelfachmann-mcp/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM node:20-slim -WORKDIR /app -COPY package.json ./ - -# Install prod dependencies -RUN npm install --omit=dev --legacy-peer-deps - -COPY ./dist ./dist -COPY ./data ./data - -ENTRYPOINT ["node", "dist/index.js"] diff --git a/packages/kabelfachmann-mcp/package.json b/packages/kabelfachmann-mcp/package.json deleted file mode 100644 index ad14883..0000000 --- a/packages/kabelfachmann-mcp/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "@mintel/kabelfachmann-mcp", - "version": "1.0.0", - "description": "Kabelfachmann MCP server", - "main": "dist/index.js", - "type": "module", - "scripts": { - "build": "tsc", - "start": "node dist/index.js", - "dev": "tsx watch src/index.ts", - "ingest": "tsx src/ingest.ts" - }, - "dependencies": { - "@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", - "node-fetch": "^3.3.2", - "onnxruntime-node": "^1.14.0", - "pdf-parse": "^1.1.1", - "zod": "^3.23.8" - }, - "devDependencies": { - "@types/express": "^5.0.6", - "@types/node": "^20.14.10", - "@types/pdf-parse": "^1.1.4", - "tsx": "^4.19.1", - "typescript": "^5.5.3" - } -} diff --git a/packages/kabelfachmann-mcp/src/index.ts b/packages/kabelfachmann-mcp/src/index.ts deleted file mode 100644 index d1de808..0000000 --- a/packages/kabelfachmann-mcp/src/index.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; -import express from "express"; -import { z } from "zod"; -import { QdrantMemoryService } from "./qdrant.js"; -import { askKabelfachmannLLM } from "./llm.js"; - -async function main() { - const server = new McpServer({ - name: "@mintel/kabelfachmann-mcp", - version: "1.0.0", - }); - - const qdrantService = new QdrantMemoryService(); - - server.tool( - "ask_kabelfachmann", - "Ask the KLZ Kabelfachmann a question about cables based on the KLZ technical handbook. This consultant knows everything about cable specifications, geometries, weights, ampacity (Strombelastbarkeit), and materials.", - { - query: z - .string() - .describe( - "The user's question regarding cables or a specific cable type.", - ), - }, - async (args) => { - console.error(`Kabelfachmann received query: ${args.query}`); - - // Retrieve relevant chunks from the handbook - const results = await qdrantService.retrieveMemory(args.query, 10); - - const contextText = results - .map( - (r) => - `--- Excerpt (Relevance: ${r.score.toFixed(2)}) ---\n${r.content}`, - ) - .join("\n\n"); - - if (!contextText) { - return { - content: [ - { - type: "text", - text: "Der Kabelfachmann konnte keine relevanten Informationen im Handbuch finden.", - }, - ], - }; - } - - const systemPrompt = `Du bist der "KLZ Kabelfachmann" (KLZ Cable Expert), ein professioneller beratender KI-Experte. -Du arbeitest für die Kabeltechnik-Firma "KLZ". -Beantworte die folgende Frage des Nutzers fachlich absolut korrekt und **nur** basierend auf den bereitgestellten Auszügen aus dem KLZ Kabelhandbuch. -Wenn die Information nicht im Kontext enthalten ist, sage höflich, dass dir dazu keine KLZ-Daten vorliegen. Erfinde niemals Spezifikationen oder Daten. -Halte dich relativ knapp und präzise, aber professionell (Siezen). -Hier ist der Kontext aus dem Handbuch: - -${contextText}`; - - try { - const answer = await askKabelfachmannLLM(systemPrompt, args.query); - return { - content: [{ type: "text", text: answer }], - }; - } catch (error: any) { - console.error("Error querying OpenRouter:", error); - return { - content: [ - { - type: "text", - text: `Ein Fehler ist bei der KI-Anfrage aufgetreten: ${error.message}`, - }, - ], - isError: true, - }; - } - }, - ); - - const isStdio = process.argv.includes("--stdio"); - - if (isStdio) { - const { StdioServerTransport } = - await import("@modelcontextprotocol/sdk/server/stdio.js"); - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error("Kabelfachmann MCP server is running on stdio"); - - try { - await qdrantService.initialize(); - } catch (e) { - console.error("Failed to initialize local dependencies:", e); - } - } else { - const app = express(); - let transport: SSEServerTransport | null = null; - - app.get("/sse", async (req, res) => { - console.error("New SSE connection established"); - - if (transport) { - try { - await transport.close(); - } catch (e) { - console.error("Error closing previous transport:", e); - } - } - - transport = new SSEServerTransport("/message", res); - try { - await server.connect(transport); - } catch (e) { - console.error("Failed to connect new transport:", e); - } - }); - - 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.KABELFACHMANN_MCP_PORT || 3007; - const HOST = process.env.HOST || "0.0.0.0"; - app.listen(PORT as number, HOST, async () => { - console.error( - `Kabelfachmann MCP server running on http://${HOST}:${PORT}/sse`, - ); - try { - await qdrantService.initialize(); - } catch (e) { - console.error("Failed to initialize local dependencies:", e); - } - }); - } -} - -main().catch((error) => { - console.error("Fatal error:", error); - process.exit(1); -}); diff --git a/packages/kabelfachmann-mcp/src/ingest.ts b/packages/kabelfachmann-mcp/src/ingest.ts deleted file mode 100644 index e8b6053..0000000 --- a/packages/kabelfachmann-mcp/src/ingest.ts +++ /dev/null @@ -1,123 +0,0 @@ -import fs from "fs"; -import fsPromises from "fs/promises"; -import path from "path"; -import pdf from "pdf-parse"; -import { QdrantMemoryService } from "./qdrant.js"; - -async function findPdfs(dir: string): Promise { - const entries = await fsPromises.readdir(dir, { withFileTypes: true }); - const files = await Promise.all( - entries.map((entry) => { - const res = path.resolve(dir, entry.name); - return entry.isDirectory() ? findPdfs(res) : res; - }), - ); - return Array.prototype - .concat(...files) - .filter((file: string) => file.toLowerCase().endsWith(".pdf")); -} - -async function start() { - const qdrantUrl = process.env.QDRANT_URL || "http://localhost:6333"; - console.error(`Initializing Qdrant at ${qdrantUrl}...`); - const qdrant = new QdrantMemoryService(qdrantUrl); - await qdrant.initialize(); - - const dataDir = - process.env.PDF_DATA_DIR || path.join(process.cwd(), "data", "pdf"); - console.error(`Scanning for PDFs in ${dataDir}...`); - - let pdfPaths: string[] = []; - try { - pdfPaths = await findPdfs(dataDir); - } catch (e) { - console.error(`Failed to read directory ${dataDir}. Error:`, e); - process.exit(1); - } - - if (pdfPaths.length === 0) { - console.error(`No PDFs found in ${dataDir}`); - process.exit(0); - } - - console.error(`Found ${pdfPaths.length} PDFs. Starting ingestion...`); - - let totalSuccess = 0; - let totalChunks = 0; - - for (const pdfPath of pdfPaths) { - console.error(`\nProcessing: ${pdfPath}`); - const filename = path.basename(pdfPath); - - let dataBuffer; - try { - dataBuffer = fs.readFileSync(pdfPath); - } catch (e) { - console.error(`Failed to read ${pdfPath}. Skipping...`); - continue; - } - - try { - const data = await pdf(dataBuffer); - const text = data.text; - - // chunk text - // A simple chunking strategy by paragraph or chunks of ~1000 characters - const paragraphs = text - .split(/\n\s*\n/) - .map((p) => p.trim()) - .filter((p) => p.length > 50); - - let currentChunk = ""; - const chunks: string[] = []; - const MAX_CHUNK_LENGTH = 1500; - - for (const p of paragraphs) { - if (currentChunk.length + p.length > MAX_CHUNK_LENGTH) { - chunks.push(currentChunk); - currentChunk = p; - } else { - currentChunk += (currentChunk.length ? "\n\n" : "") + p; - } - } - if (currentChunk.length > 0) { - chunks.push(currentChunk); - } - - console.error( - `Split ${filename} into ${chunks.length} chunks. Ingesting to Qdrant...`, - ); - - let fileSuccessCount = 0; - for (let i = 0; i < chunks.length; i++) { - const chunk = chunks[i]; - const success = await qdrant.storeMemory( - `${filename} - Teil ${i + 1}`, - chunk, - ); - if (success) { - fileSuccessCount++; - totalSuccess++; - } - if ((i + 1) % 10 === 0) { - console.error(`Ingested ${i + 1}/${chunks.length} chunks for ${filename}...`); - } - } - totalChunks += chunks.length; - - console.error(`Finished ${filename}: stored ${fileSuccessCount}/${chunks.length} chunks.`); - } catch (e) { - console.error(`Error processing ${pdfPath}:`, e); - } - } - - console.error( - `\nIngestion complete! Successfully stored ${totalSuccess}/${totalChunks} chunks across ${pdfPaths.length} files.`, - ); - process.exit(0); -} - -start().catch((e) => { - console.error(e); - process.exit(1); -}); diff --git a/packages/kabelfachmann-mcp/src/llm.ts b/packages/kabelfachmann-mcp/src/llm.ts deleted file mode 100644 index cd98dbd..0000000 --- a/packages/kabelfachmann-mcp/src/llm.ts +++ /dev/null @@ -1,90 +0,0 @@ -import fetch from "node-fetch"; - -export async function askKabelfachmannLLM( - systemPrompt: string, - userPrompt: string, -): Promise { - const provider = process.env.KABELFACHMANN_LLM_PROVIDER || "openrouter"; - - if (provider === "ollama") { - return askOllama(systemPrompt, userPrompt); - } else { - return askOpenRouter(systemPrompt, userPrompt); - } -} - -async function askOllama( - systemPrompt: string, - userPrompt: string, -): Promise { - const host = process.env.KABELFACHMANN_OLLAMA_HOST || "http://127.0.0.1:11434"; - const model = process.env.KABELFACHMANN_OLLAMA_MODEL || "qwen2.5:32b"; - - const response = await fetch(`${host}/api/chat`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - model: model, - messages: [ - { role: "system", content: systemPrompt }, - { role: "user", content: userPrompt }, - ], - stream: false, - }), - }); - - if (!response.ok) { - const text = await response.text(); - throw new Error( - `Ollama API error: ${response.status} ${response.statusText} - ${text}`, - ); - } - - const data = (await response.json()) as any; - if (!data.message || !data.message.content) { - throw new Error("Invalid response from Ollama API"); - } - return data.message.content; -} - -async function askOpenRouter( - systemPrompt: string, - userPrompt: string, -): Promise { - const apiKey = process.env.OPENROUTER_API_KEY; - if (!apiKey) { - throw new Error("OPENROUTER_API_KEY is not set"); - } - - const response = await fetch( - "https://openrouter.ai/api/v1/chat/completions", - { - method: "POST", - headers: { - Authorization: `Bearer ${apiKey}`, - "HTTP-Referer": "https://mintel.me", - "X-Title": "Mintel MCP", - "Content-Type": "application/json", - }, - body: JSON.stringify({ - model: "google/gemini-3-flash-preview", - messages: [ - { role: "system", content: systemPrompt }, - { role: "user", content: userPrompt }, - ], - }), - }, - ); - - if (!response.ok) { - const text = await response.text(); - throw new Error( - `OpenRouter API error: ${response.status} ${response.statusText} - ${text}`, - ); - } - - const data = (await response.json()) as any; - return data.choices[0].message.content; -} diff --git a/packages/kabelfachmann-mcp/src/qdrant.ts b/packages/kabelfachmann-mcp/src/qdrant.ts deleted file mode 100644 index 3622eaa..0000000 --- a/packages/kabelfachmann-mcp/src/qdrant.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { pipeline, env } from "@xenova/transformers"; -import { QdrantClient } from "@qdrant/js-client-rest"; -import crypto from "crypto"; - -env.allowRemoteModels = true; -env.localModelPath = "./models"; - -export class QdrantMemoryService { - private client: QdrantClient; - private collectionName = "kabelfachmann"; - private embedder: any = null; - - constructor( - url: string = process.env.QDRANT_URL || "http://qdrant-mcp:6333", - ) { - this.client = new QdrantClient({ url }); - } - - async initialize() { - console.error("Loading embedding model..."); - this.embedder = await pipeline( - "feature-extraction", - "Xenova/all-MiniLM-L6-v2", - ); - - console.error(`Checking for collection: ${this.collectionName}`); - try { - const collections = await this.client.getCollections(); - const exists = collections.collections.some( - (c) => c.name === this.collectionName, - ); - - if (!exists) { - console.error(`Creating collection: ${this.collectionName}`); - await this.client.createCollection(this.collectionName, { - vectors: { - size: 384, - distance: "Cosine", - }, - }); - console.error("Collection created successfully."); - } - } catch (e) { - console.error("Failed to initialize Qdrant collection:", e); - throw e; - } - } - - private async getEmbedding(text: string): Promise { - if (!this.embedder) { - throw new Error("Embedder not initialized. Call initialize() first."); - } - const output = await this.embedder(text, { - pooling: "mean", - normalize: true, - }); - return Array.from(output.data); - } - - async storeMemory(label: string, content: string): Promise { - try { - const vector = await this.getEmbedding(content); - const id = crypto.randomUUID(); - - await this.client.upsert(this.collectionName, { - wait: true, - points: [ - { - id, - vector, - payload: { label, content, timestamp: new Date().toISOString() }, - }, - ], - }); - return true; - } catch (e) { - console.error("Failed to store memory:", e); - return false; - } - } - - async retrieveMemory( - query: string, - limit: number = 5, - ): Promise> { - try { - const vector = await this.getEmbedding(query); - const searchResults = await this.client.search(this.collectionName, { - vector, - limit, - with_payload: true, - }); - - return searchResults.map((result) => ({ - label: String(result.payload?.label || ""), - content: String(result.payload?.content || ""), - score: result.score, - })); - } catch (e) { - console.error("Failed to retrieve memory:", e); - return []; - } - } -} diff --git a/packages/kabelfachmann-mcp/src/start.ts b/packages/kabelfachmann-mcp/src/start.ts deleted file mode 100644 index df03cef..0000000 --- a/packages/kabelfachmann-mcp/src/start.ts +++ /dev/null @@ -1,16 +0,0 @@ -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({ quiet: true, path: resolve(__dirname, "../../../.env.local") }); -// Fallback to .env (contains defaults) -config({ quiet: true, 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/kabelfachmann-mcp/test-kabelfachmann.js b/packages/kabelfachmann-mcp/test-kabelfachmann.js deleted file mode 100644 index 0dfc14d..0000000 --- a/packages/kabelfachmann-mcp/test-kabelfachmann.js +++ /dev/null @@ -1,38 +0,0 @@ -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; - -async function main() { - console.log("Connecting to Kabelfachmann MCP on localhost:3007/sse..."); - const transport = new SSEClientTransport( - new URL("http://localhost:3007/sse"), - ); - const client = new Client( - { name: "test-client", version: "1.0.0" }, - { capabilities: {} }, - ); - - await client.connect(transport); - console.log("Connected! Requesting tools..."); - - const tools = await client.listTools(); - console.log( - "Available tools:", - tools.tools.map((t) => t.name), - ); - - console.log("Calling ask_kabelfachmann..."); - const result = await client.callTool({ - name: "ask_kabelfachmann", - arguments: { - query: - "Was ist der Mindestbiegeradius von einem NYY-J 5x1,5 Kabel laut Handbuch?", - }, - }); - - console.log("\n--- RESULT ---"); - console.log(JSON.stringify(result, null, 2)); - - process.exit(0); -} - -main().catch(console.error); diff --git a/packages/kabelfachmann-mcp/tsconfig.json b/packages/kabelfachmann-mcp/tsconfig.json deleted file mode 100644 index 36cd6f4..0000000 --- a/packages/kabelfachmann-mcp/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true - }, - "include": ["src/**/*"] -} diff --git a/packages/klz-payload-mcp/src/index.ts b/packages/klz-payload-mcp/src/index.ts index 9e66dee..5ee1451 100644 --- a/packages/klz-payload-mcp/src/index.ts +++ b/packages/klz-payload-mcp/src/index.ts @@ -7,6 +7,7 @@ import { Tool, } from "@modelcontextprotocol/sdk/types.js"; import axios from "axios"; +import crypto from "crypto"; import https from "https"; const PAYLOAD_URL = process.env.PAYLOAD_URL || "https://klz-cables.com"; @@ -588,17 +589,34 @@ async function run() { console.error('KLZ Payload MCP server is running on stdio'); } else { const app = express(); - let transport: SSEServerTransport | null = null; + const transports = new Map(); + + app.use((req, _res, next) => { + console.error(`${req.method} ${req.url}`); + next(); + }); app.get('/sse', async (req: Request, res: Response) => { - console.error('New SSE connection established'); - transport = new SSEServerTransport('/message', res); + const sessionId = crypto.randomUUID(); + console.error(`New SSE connection: ${sessionId}`); + const transport = new SSEServerTransport(`/message/${sessionId}`, res); + transports.set(sessionId, transport); + + req.on('close', () => { + console.error(`SSE connection closed: ${sessionId}`); + transports.delete(sessionId); + }); + await server.connect(transport); }); - app.post('/message', async (req: Request, res: Response) => { + app.post('/message/:sessionId', async (req: Request, res: Response) => { + const sessionId = req.params.sessionId; + const transport = transports.get(sessionId as string); + if (!transport) { - res.status(400).send('No active SSE connection'); + console.error(`No transport found for session: ${sessionId}`); + res.status(400).send('No active SSE connection for this session'); return; } await transport.handlePostMessage(req, res); diff --git a/packages/memory-mcp/src/index.ts b/packages/memory-mcp/src/index.ts index da70118..e6be58f 100644 --- a/packages/memory-mcp/src/index.ts +++ b/packages/memory-mcp/src/index.ts @@ -1,6 +1,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; import express from 'express'; +import crypto from 'crypto'; import { z } from 'zod'; import { QdrantMemoryService } from './qdrant.js'; @@ -77,17 +78,34 @@ async function main() { } } else { const app = express(); - let transport: SSEServerTransport | null = null; + const transports = new Map(); + + app.use((req, _res, next) => { + console.error(`${req.method} ${req.url}`); + next(); + }); app.get('/sse', async (req, res) => { - console.error('New SSE connection established'); - transport = new SSEServerTransport('/message', res); + const sessionId = crypto.randomUUID(); + console.error(`New SSE connection: ${sessionId}`); + const transport = new SSEServerTransport(`/message/${sessionId}`, res); + transports.set(sessionId, transport); + + req.on('close', () => { + console.error(`SSE connection closed: ${sessionId}`); + transports.delete(sessionId); + }); + await server.connect(transport); }); - app.post('/message', async (req, res) => { + app.post('/message/:sessionId', async (req, res) => { + const { sessionId } = req.params; + const transport = transports.get(sessionId as string); + if (!transport) { - res.status(400).send('No active SSE connection'); + console.error(`No transport found for session: ${sessionId}`); + res.status(400).send('No active SSE connection for this session'); return; } await transport.handlePostMessage(req, res); diff --git a/packages/serpbear-mcp/src/index.ts b/packages/serpbear-mcp/src/index.ts index 050dcd7..ab838a8 100644 --- a/packages/serpbear-mcp/src/index.ts +++ b/packages/serpbear-mcp/src/index.ts @@ -1,6 +1,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import express from 'express'; +import crypto from 'crypto'; import { CallToolRequestSchema, ListToolsRequestSchema, @@ -213,17 +214,34 @@ async function run() { console.error('SerpBear MCP server is running on stdio'); } else { const app = express(); - let transport: SSEServerTransport | null = null; + const transports = new Map(); + + app.use((req, _res, next) => { + console.error(`${req.method} ${req.url}`); + next(); + }); app.get('/sse', async (req, res) => { - console.error('New SSE connection established'); - transport = new SSEServerTransport('/message', res); + const sessionId = crypto.randomUUID(); + console.error(`New SSE connection: ${sessionId}`); + const transport = new SSEServerTransport(`/message/${sessionId}`, res); + transports.set(sessionId, transport); + + req.on('close', () => { + console.error(`SSE connection closed: ${sessionId}`); + transports.delete(sessionId); + }); + await server.connect(transport); }); - app.post('/message', async (req, res) => { + app.post('/message/:sessionId', async (req, res) => { + const { sessionId } = req.params; + const transport = transports.get(sessionId as string); + if (!transport) { - res.status(400).send('No active SSE connection'); + console.error(`No transport found for session: ${sessionId}`); + res.status(400).send('No active SSE connection for this session'); return; } await transport.handlePostMessage(req, res); diff --git a/packages/umami-mcp/src/index.ts b/packages/umami-mcp/src/index.ts index ba159f7..f2c8a74 100644 --- a/packages/umami-mcp/src/index.ts +++ b/packages/umami-mcp/src/index.ts @@ -1,6 +1,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import express from 'express'; +import crypto from 'crypto'; import { CallToolRequestSchema, ListToolsRequestSchema, @@ -251,17 +252,34 @@ async function run() { console.error('Umami MCP server is running on stdio'); } else { const app = express(); - let transport: SSEServerTransport | null = null; + const transports = new Map(); + + app.use((req, _res, next) => { + console.error(`${req.method} ${req.url}`); + next(); + }); app.get('/sse', async (req, res) => { - console.error('New SSE connection established'); - transport = new SSEServerTransport('/message', res); + const sessionId = crypto.randomUUID(); + console.error(`New SSE connection: ${sessionId}`); + const transport = new SSEServerTransport(`/message/${sessionId}`, res); + transports.set(sessionId, transport); + + req.on('close', () => { + console.error(`SSE connection closed: ${sessionId}`); + transports.delete(sessionId); + }); + await server.connect(transport); }); - app.post('/message', async (req, res) => { + app.post('/message/:sessionId', async (req, res) => { + const { sessionId } = req.params; + const transport = transports.get(sessionId as string); + if (!transport) { - res.status(400).send('No active SSE connection'); + console.error(`No transport found for session: ${sessionId}`); + res.status(400).send('No active SSE connection for this session'); return; } await transport.handlePostMessage(req, res);