fix(mcp): refactor all mcp servers to use multi-session sse transport
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m1s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m44s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m55s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m1s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m44s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m55s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
This commit is contained in:
4
.env
4
.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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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<string, SSEServerTransport>();
|
||||
|
||||
// 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);
|
||||
|
||||
@@ -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<string, SSEServerTransport>();
|
||||
|
||||
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);
|
||||
|
||||
@@ -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"]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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<string[]> {
|
||||
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);
|
||||
});
|
||||
@@ -1,90 +0,0 @@
|
||||
import fetch from "node-fetch";
|
||||
|
||||
export async function askKabelfachmannLLM(
|
||||
systemPrompt: string,
|
||||
userPrompt: string,
|
||||
): Promise<string> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
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;
|
||||
}
|
||||
@@ -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<number[]> {
|
||||
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<boolean> {
|
||||
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<Array<{ label: string; content: string; score: number }>> {
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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/**/*"]
|
||||
}
|
||||
@@ -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<string, SSEServerTransport>();
|
||||
|
||||
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);
|
||||
|
||||
@@ -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<string, SSEServerTransport>();
|
||||
|
||||
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);
|
||||
|
||||
@@ -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<string, SSEServerTransport>();
|
||||
|
||||
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);
|
||||
|
||||
@@ -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<string, SSEServerTransport>();
|
||||
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user