privateGPT MCP Server

  • ver
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import axios from 'axios'; import https from 'https'; import dotenv from 'dotenv'; import net from 'net'; import { fileURLToPath } from 'url'; import path from 'path'; import fs from 'fs'; import crypto from 'crypto'; dotenv.config({ path: './pgpt.env' }); // Geben Sie explizit den Pfad zur Datei an const templates = { success: '✔️ ${action}: ${details}', error: '❌ ${action} Fehler: ${details}', warning: '⚠️ ${action}: ${details}', configStart: '🚀 ${action}: ${details}', info: '📤 ${action}...', }; const messages = { de: { apiErrorDetails: templates.error.replace('${action}', 'API-Fehler').replace('${details}', 'Status: ${status}, Daten: ${data}'), apiUrlInvalid: templates.error.replace('${action}', 'Ungültige API_URL').replace('${details}', ''), apiUrlValidated: templates.success.replace('${action}', 'API_URL erfolgreich validiert').replace('${details}', '${url}'), apiUrlWarning: templates.warning.replace('${action}', 'Warnung').replace('${details}', 'API_URL beginnt nicht mit "https://". Die URL wird angepasst.'), apiUrlWarningV1: templates.warning.replace('${action}', 'Warnung').replace('${details}', 'API_URL endet nicht mit "/api/v1". Die URL wird angepasst.'), availableGroups: templates.success.replace('${action}', 'Verfügbare Gruppen').replace('${details}', '${data}'), errorHandlingRequest: templates.error.replace('${action}', 'Fehler bei der Verarbeitung der Anfrage').replace('${details}', '${error}'), fetchingSources: templates.info.replace('${action}', 'Abrufen von Quellen für Gruppe: ${groupName}'), groupFetchError: templates.error.replace('${action}', 'Fehler beim Abrufen der Gruppen').replace('${details}', 'Bitte versuchen Sie es später erneut.'), groupNameRequired: 'Gruppenname erforderlich für diese Anfrage.', gotGetSourceResponse: templates.success.replace('${action}', 'Antwort auf get_source erhalten').replace('${details}', '${data}'), gotListGroupsResponse: templates.success.replace('${action}', 'Antwort auf list_groups erhalten').replace('${details}', '${response}'), gotListSourcesResponse: templates.success.replace('${action}', 'Antwort auf list_sources erhalten').replace('${details}', '${data}'), handlingToolRequest: templates.info.replace('${action}', 'Verarbeite Tool-Anfrage: ${name}'), headers: 'Header: ${headers}', incomingMessage: '📥 Eingehende Nachricht:', internalServerError: 'Interner Serverfehler', invalidGroups: templates.error.replace('${action}', 'Ungültige Gruppen').replace('${details}', '${groups}'), loginResponse: templates.success.replace('${action}', 'Login-Antwort').replace('${details}', ''), makingGetSourceRequest: templates.info.replace('${action}', 'Erstellen einer get_source-Anfrage').replace('${details}', '${args}'), makingListGroupsRequest: templates.info.replace('${action}', 'Erstellen einer list_groups-Anfrage'), makingListSourcesRequest: templates.info.replace('${action}', 'Erstellen einer list_sources-Anfrage').replace('${details}', '${args}'), mcpError: templates.error.replace('${action}', '[MCP Fehler]').replace('${details}', '${error}'), method: 'Methode: ${method}', payload: 'Daten: ${payload}', portInUse: templates.error.replace('${action}', 'Port Fehler').replace('${details}', 'Port ${PORT} ist bereits in Verwendung.'), portInvalid: templates.error.replace('${action}', 'Port Fehler').replace('${details}', 'PORT muss eine Zahl zwischen 1 und 65535 sein.'), portValidated: templates.success.replace('${action}', 'PORT erfolgreich validiert').replace('${details}', '${port}'), requestError: templates.error.replace('${action}', 'Anfragefehler').replace('${details}', '${error}'), requestSent: templates.info.replace('${action}', 'Anfrage gesendet'), responseError: templates.error.replace('${action}', 'Fehler bei der Antwort').replace('${details}', '${error}'), responseReceived: templates.success.replace('${action}', 'Antwort erhalten').replace('${details}', '${response}'), serverError: templates.error.replace('${action}', 'Server-Fehler').replace('${details}', '${error}'), sendingChatRequest: templates.info.replace('${action}', 'Senden einer Chat-Anfrage an die API'), sourcesRetrieved: templates.success.replace('${action}', 'Quellen erhalten').replace('${details}', '${data}'), tcpServerError: templates.error.replace('${action}', 'TCP Server Fehler').replace('${details}', '${error}'), restrictedGroupsError: templates.error.replace('${action}', 'Ungültige RESTRICTED_GROUPS-Konfiguration').replace('${details}', "muss 'true' oder 'false' sein. Aktueller Wert: ${value}"), restrictedGroupsSuccess: templates.success.replace('${action}', 'RESTRICTED_GROUPS').replace('${details}', 'ist aktiviert: ${status}'), toolDisabledError: templates.error.replace('${action}', 'Tool deaktiviert').replace('${details}', 'Die Funktion "${toolName}" ist auf dem Server deaktiviert.'), sslValidationSet: '${symbol} SSL-Validierung ist eingestellt auf: ${value}', startingServerWithConfig: templates.configStart.replace('${action}', 'Starten des Servers').replace('${details}', 'mit folgender Konfiguration:\n${config}'), serverRunning: templates.info.replace('${action}', 'Server läuft').replace('${details}', 'auf Port ${port}'), connection: { new: '📥 Neue Verbindung akzeptiert von ${ip}:${port}', established: '🚀 Verbindung hergestellt', closed: '🔌 Verbindung geschlossen: ${ip}:${port}', dataReceived: '📥 Empfangene Daten von ${ip}:${port}: ${data}', }, errors: { processMessage: '❌ Fehler bei der Verarbeitung der Nachricht: ${error}', invalidMessage: 'Ungültiges Nachrichtenformat', socketError: '❌ Socket-Fehler bei ${ip}:${port}: ${error}', serverError: '❌ Server-Fehler: ${error}', }, server: { running: '📡 Server läuft auf Port ${port}', stopped: '🛑 Server wurde gestopped', }, loginSuccess: '✔️ Login erfolgreich: ${data}', invalidArgumentsError: '❌ Fehler: Keine gültigen Argumente in der Eingabe gefunden: ${input}', missingArgumentsError: '❌ Fehlende Argumente: ${args}', noTokenError: '❌ Kein Token bereitgestellt.', publicGroupsConflictWarning: '⚠️ Konflikt: usePublic wurde auf false gesetzt, da Gruppen angegeben sind.', sendingChatRequest: '📤 Sende Chat-Anfrage: Frage: "${question}", Öffentlich: ${usePublic}, Gruppen: ${groups}, Sprache: ${language}', checkingGroups: '📤 Überprüfen der Gruppen: ${groups}', invalidGroupsError: '❌ Ungültige Gruppen gefunden: ${invalidGroups}', availableGroups: '✔️ Verfügbare Gruppen: ${availableGroups}', groupValidationError: '❌ Fehler bei der Gruppenvalidierung: ${error}', loginError: '❌ Fehler beim Login: ${error}', logoutSuccess: '✔️ Logout erfolgreich: ${data}', logoutError: '❌ Fehler beim Logout: ${error}', chatResponseSuccess: '✔️ Chat-Antwort erfolgreich erhalten: ${data}', chatApiError: '❌ Fehler bei der Chat-API-Anfrage: ${error}', conversationContinuation: '✔️ Fortsetzung der Konversation mit ID: ${chatId}', conversationSuccess: '✔️ Konversation erfolgreich fortgesetzt: ${data}', apiRequestError: '❌ Fehler bei der API-Anfrage: ${error}', noChatData: 'Keine Daten für den angegebenen Chat gefunden.', fetchChatInfoError: '❌ Fehler beim Abrufen der Chat-Informationen: ${error}', missingTokenError: 'Token fehlt. Bitte einloggen und erneut versuchen.', missingParametersError: 'Fehlende erforderliche Parameter: ${parameters}.', invalidGroupsError: 'Ungültige Gruppen: ${invalidGroups}', createSourceSuccess: '✔️ Quelle erfolgreich erstellt: ${data}', createSourceError: '❌ Fehler beim Erstellen der Quelle: ${error}', noServerResponse: 'Keine Antwort vom Server erhalten.', missingParameterError: 'Fehlender erforderlicher Parameter: ${parameter}.', editSourceSuccess: '✔️ Quelle erfolgreich bearbeitet: ${data}', editSourceError: '❌ Fehler beim Bearbeiten der Quelle: ${error}', deleteSourceError: '❌ Fehler beim Löschen der Quelle: ${error}', storeGroupLog: 'Speichere neue Gruppe mit Name: ${groupName} und Beschreibung: ${description}', storeGroupSuccess: 'Gruppe erfolgreich gespeichert: ${data}', storeGroupText: 'Gruppe "${groupName}" erfolgreich gespeichert mit ID: ${id}', deleteGroupLog: 'Lösche Gruppe mit Name: ${groupName}', deleteGroupSuccessLog: 'Gruppe erfolgreich gelöscht: ${data}', deleteGroupText: 'Gruppe "${groupName}" wurde erfolgreich gelöscht.', apiRequestError: 'Fehler bei der API-Anfrage: ${error}', createUserError: '❌ Fehler beim Erstellen des Benutzers: ${error}', editUserError: '❌ Fehler beim Bearbeiten des Benutzers: ${error}', deleteUserError: '❌ Fehler beim Löschen des Benutzers: ${error}', deleteUserSuccess: '✔️ Benutzer erfolgreich gelöscht: ${data}', editUserSuccess: '✔️ Benutzer erfolgreich bearbeitet: ${data}', createUserSuccess: '✔️ Benutzer erfolgreich erstellt: ${data}', createUserLog: '📤 Erstellen eines neuen Benutzers: ${payload}', createGroupSuccess: '✔️ Gruppe erfolgreich erstellt: ${data}', fetchGroupsError: '❌ Fehler beim Abrufen der Gruppen: ${error}', apiRequestError: '❌ Fehler bei der API-Anfrage: ${error}', restrictedGroupsWarning: '⚠️ RESTRICTED_GROUPS aktiviert. Verfügbare Gruppen werden eingeschränkt.', editSourceLog: 'Bearbeite Quelle mit ID: ${sourceId}, Titel: ${title}', }, en: { editSourceLog: 'Editing source with ID: ${sourceId}, Title: ${title}', fetchGroupsError: '❌ Error fetching groups: ${error}', apiRequestError: '❌ Error during API request: ${error}', restrictedGroupsWarning: '⚠️ RESTRICTED_GROUPS enabled. Available groups are restricted.', deleteUserSuccess: '✔️ User successfully deleted: ${data}', editUserSuccess: '✔️ User successfully edited: ${data}', createUserSuccess: '✔️ User successfully created: ${data}', createUserLog: '📤 Creating a new user: ${payload}', createGroupSuccess: '✔️ Group successfully created: ${data}', deleteSourceError: '❌ Error deleting source: ${error}', storeGroupLog: 'Storing new group with name: ${groupName} and description: ${description}', storeGroupSuccess: 'Group stored successfully: ${data}', storeGroupText: 'Group "${groupName}" successfully stored with ID: ${id}', deleteGroupLog: 'Deleting group with name: ${groupName}', deleteGroupSuccessLog: 'Group successfully deleted: ${data}', deleteGroupText: 'Group "${groupName}" was successfully deleted.', apiRequestError: 'Error during API request: ${error}', createUserError: '❌ Error creating user: ${error}', editUserError: '❌ Error editing user: ${error}', deleteUserError: '❌ Error deleting user: ${error}', missingTokenError: 'Token is missing. Please log in and try again.', missingParametersError: 'Missing required parameters: ${parameters}.', invalidGroupsError: 'Invalid groups: ${invalidGroups}', createSourceSuccess: '✔️ Source successfully created: ${data}', createSourceError: '❌ Error creating the source: ${error}', noServerResponse: 'No response received from the server.', missingParameterError: 'Missing required parameter: ${parameter}.', editSourceSuccess: '✔️ Source successfully edited: ${data}', editSourceError: '❌ Error editing the source: ${error}', loginError: '❌ Error during login: ${error}', logoutSuccess: '✔️ Logout successful: ${data}', logoutError: '❌ Error during logout: ${error}', chatResponseSuccess: '✔️ Chat response successfully received: ${data}', chatApiError: '❌ Error during chat API request: ${error}', conversationContinuation: '✔️ Continuing conversation with ID: ${chatId}', conversationSuccess: '✔️ Conversation successfully continued: ${data}', apiRequestError: '❌ Error during API request: ${error}', noChatData: 'No data found for the specified chat.', fetchChatInfoError: '❌ Error fetching chat information: ${error}', checkingGroups: '📤 Checking groups: ${groups}', invalidGroupsError: '❌ Invalid groups found: ${invalidGroups}', availableGroups: '✔️ Available groups: ${availableGroups}', groupValidationError: '❌ Error during group validation: ${error}', missingArgumentsError: '❌ Missing arguments: ${args}', invalidArgumentsError: '❌ Error: No valid arguments found in the input: ${input}', loginSuccess: '✔️ Login successful: ${data}', sslValidationSet: '${symbol} SSL validation is set to: ${value}', serverRunning: templates.info.replace('${action}', 'Server is running').replace('${details}', 'on port ${port}'), toolDisabledError: templates.error.replace('${action}', 'Tool disabled').replace('${details}', 'The feature "${toolName}" is disabled on the server.'), restrictedGroupsError: templates.error.replace('${action}', 'Invalid RESTRICTED_GROUPS configuration').replace('${details}', "must be 'true' or 'false'. Current value: ${value}"), restrictedGroupsSuccess: templates.success.replace('${action}', 'RESTRICTED_GROUPS').replace('${details}', 'is enabled: ${status}'), apiErrorDetails: templates.error.replace('${action}', 'API Error').replace('${details}', 'Status: ${status}, Data: ${data}'), apiUrlInvalid: templates.error.replace('${action}', 'Invalid API_URL').replace('${details}', ''), apiUrlValidated: templates.success.replace('${action}', 'API_URL successfully validated').replace('${details}', '${url}'), apiUrlWarning: templates.warning.replace('${action}', 'Warning').replace('${details}', 'API_URL does not start with "https://". The URL will be adjusted.'), apiUrlWarningV1: templates.warning.replace('${action}', 'Warning').replace('${details}', 'API_URL does not end with "/api/v1". The URL will be adjusted.'), availableGroups: templates.success.replace('${action}', 'Available groups').replace('${details}', '${data}'), errorHandlingRequest: templates.error.replace('${action}', 'Error handling request').replace('${details}', '${error}'), fetchingSources: templates.info.replace('${action}', 'Fetching sources for group: ${groupName}'), groupFetchError: templates.error.replace('${action}', 'Error fetching groups').replace('${details}', 'Please try again later.'), groupNameRequired: 'Group name is required for this request.', gotGetSourceResponse: templates.success.replace('${action}', 'Got get_source response').replace('${details}', '${data}'), gotListGroupsResponse: templates.success.replace('${action}', 'Got list_groups response').replace('${details}', '${response}'), gotListSourcesResponse: templates.success.replace('${action}', 'Got list_sources response').replace('${details}', '${data}'), handlingToolRequest: templates.info.replace('${action}', 'Handling tool request: ${name}'), headers: 'Headers: ${headers}', incomingMessage: '📥 Incoming message:', internalServerError: 'Internal server error', invalidGroups: templates.error.replace('${action}', 'Invalid groups').replace('${details}', '${groups}'), loginResponse: templates.success.replace('${action}', 'Login response').replace('${details}', ''), makingGetSourceRequest: templates.info.replace('${action}', 'Making get_source request').replace('${details}', '${args}'), makingListGroupsRequest: templates.info.replace('${action}', 'Making list_groups request'), makingListSourcesRequest: templates.info.replace('${action}', 'Making list_sources request').replace('${details}', '${args}'), mcpError: templates.error.replace('${action}', '[MCP Error]').replace('${details}', '${error}'), method: 'Method: ${method}', payload: 'Payload: ${payload}', portInUse: templates.error.replace('${action}', 'Port Error').replace('${details}', 'Port ${PORT} is already in use.'), portInvalid: templates.error.replace('${action}', 'Port Error').replace('${details}', 'PORT must be a number between 1 and 65535.'), portValidated: templates.success.replace('${action}', 'PORT successfully validated').replace('${details}', '${port}'), requestError: templates.error.replace('${action}', 'Request Error').replace('${details}', '${error}'), requestSent: templates.info.replace('${action}', 'Request sent'), responseError: templates.error.replace('${action}', 'Response error').replace('${details}', '${error}'), responseReceived: templates.success.replace('${action}', 'Response received').replace('${details}', '${response}'), serverError: templates.error.replace('${action}', 'Server Error').replace('${details}', '${error}'), sendingChatRequest: templates.info.replace('${action}', 'Sending a chat request to the API'), sourcesRetrieved: templates.success.replace('${action}', 'Sources retrieved').replace('${details}', '${data}'), startingServerWithConfig: templates.warning.replace('${action}', 'Starting the server').replace('${details}', 'with the following configuration:'), tcpServerError: templates.error.replace('${action}', 'TCP Server Error').replace('${details}', '${error}'), connection: { new: '📥 New connection accepted from ${ip}:${port}', established: '🚀 Connection established', closed: '🔌 Connection closed: ${ip}:${port}', dataReceived: '📥 Received data from ${ip}:${port}: ${data}', }, errors: { processMessage: '❌ Error while processing the message: ${error}', invalidMessage: 'Invalid message format', socketError: '❌ Socket error for ${ip}:${port}: ${error}', serverError: '❌ Server error: ${error}', }, server: { running: '📡 Server runs on port ${port}', stopped: '🛑 Server stopped', }, } }; // JSON-Datei laden // `__dirname`-Ersatz für ES-Module const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // JSON-Dateipfad relativ zum Skript const envFilePath = path.resolve(__dirname, '../pgpt.env.json'); let envConfig; try { envConfig = JSON.parse(fs.readFileSync(envFilePath, 'utf-8')); } catch (error) { console.error(`❌ Error loading pgpt.env.json: ${error.message}`); process.exit(1); } // Helper-Funktionen function getEnvVar(key, nestedPath = null, fallback = null) { // Prüfen, ob ein verschachtelter Pfad angegeben ist if (nestedPath) { const value = nestedPath.reduce((acc, part) => acc && acc[part], envConfig); if (!value) { if (fallback !== null) return fallback; console.error(`❌ Missing .json configuration variable: ${key}`); process.exit(1); } return value; } // Direkter Zugriff if (!envConfig[key]) { if (fallback !== null) return fallback; console.error(`❌ Missing .json configuration variable: ${key}`); process.exit(1); } return envConfig[key]; } const privateApiUrl = getEnvVar('PRIVATE_GPT_API_URL', ['PGPT_Url', 'PRIVATE_GPT_API_URL']); const requestedLang = getEnvVar('LANGUAGE', ['Server_Config', 'LANGUAGE'], 'en').toLowerCase(); const apiUrl = getEnvVar('API_URL', ['PGPT_Url', 'API_URL']); const Port = getEnvVar('PORT', ['Server_Config', 'PORT'], '5000'); const restrictedGroups = getEnvVar('RESTRICTED_GROUPS', ['Restrictions', 'RESTRICTED_GROUPS'], 'false').toString(); const sslValidate = getEnvVar('SSL_VALIDATE', ['Server_Config', 'SSL_VALIDATE'], 'false').toString(); const PwEncryption = getEnvVar('PW_ENCRYPTION', ['Server_Config', 'PW_ENCRYPTION'], 'false') === 'true'; const AllowKeygen = getEnvVar('ALLOW_KEYGEN', ['Server_Config', 'ALLOW_KEYGEN'], 'false') === 'true'; const publicKeyPath = getEnvVar('PUBLIC_KEY', ['Server_Config', 'PUBLIC_KEY']); const privateKeyPath = getEnvVar('PRIVATE_KEY', ['Server_Config', 'PRIVATE_KEY']); // Load the public key const publicKey = fs.readFileSync(getEnvVar('PUBLIC_KEY', ['Server_Config', 'PUBLIC_KEY']), 'utf8'); // Load the private key const privateKey = fs.readFileSync(getEnvVar('PRIVATE_KEY', ['Server_Config', 'PRIVATE_KEY']), 'utf8'); if (PwEncryption) { console.log('Password encryption is enabled.'); } else { console.log('Password encryption is disabled.'); } function validateUrl(url, t) { if (!url.startsWith('https://')) { console.warn(t.apiUrlWarning); url = url.replace(/^http:\/\//, 'https://'); } url = url.replace(/([^:]\/)\/+/g, '$1'); // Doppelte Schrägstriche nach "://" entfernen if (!url.endsWith('/api/v1')) { console.warn(t.apiUrlWarningV1); url = `${url.replace(/\/$/, '')}/api/v1`; } try { new URL(url); } catch { console.error(t.apiUrlInvalid, url); process.exit(1); } return url; } function validatePort(port, t) { const portNumber = parseInt(port, 10); if (isNaN(portNumber) || portNumber < 1 || portNumber > 65535) { console.error(t.portInvalid); process.exit(1); } return portNumber; } function validateBoolean(varName, value, t) { if (value !== 'true' && value !== 'false') { console.error( t.restrictedGroupsError.replace('${value}', value) ); process.exit(1); } return value === 'true'; } /** * Decrypt a cryptographic string using the private key. * @param {string} encryptedData - The encrypted string in Base64 format. * @returns {string} - The decrypted password. */ function decryptPassword(encryptedData) { try { const decryptedPassword = crypto.privateDecrypt( { key: privateKey, padding: crypto.constants.RSA_PKCS1_PADDING, // Ensure consistent padding }, Buffer.from(encryptedData, 'base64') ).toString('utf8'); return decryptedPassword; } catch (error) { console.error('Decryption error:', error.message); throw new Error('Failed to decrypt the password.'); } } // Function for encryption function encryptWithPublicKey(data) { return crypto.publicEncrypt( { key: publicKey, padding: crypto.constants.RSA_PKCS1_PADDING, // Explicitly set padding }, Buffer.from(data) ).toString('base64'); } /** * Encrypt a given password and return the encrypted key. * @param {string} password - The password to be encrypted. * @returns {string} - The encrypted password as a Base64 string. */ function getEncryptedKey(password) { try { return encryptWithPublicKey(password); } catch (err) { console.error('Encryption error:', err.message); throw new Error('Failed to encrypt the password.'); } } const enableLogin = getEnvVar('ENABLE_LOGIN', ['Functions', 'ENABLE_LOGIN'], false); const enableLogout = getEnvVar('ENABLE_LOGOUT', ['Functions', 'ENABLE_LOGOUT'], false); const enableChat = getEnvVar('ENABLE_CHAT', ['Functions', 'ENABLE_CHAT'], false); const enableContinueChat = getEnvVar('ENABLE_CONTINUE_CHAT', ['Functions', 'ENABLE_CONTINUE_CHAT'], false); const enableGetChatInfo = getEnvVar('ENABLE_GET_CHAT_INFO', ['Functions', 'ENABLE_GET_CHAT_INFO'], false); const enableListGroups = getEnvVar('ENABLE_LIST_GROUPS', ['Functions', 'ENABLE_LIST_GROUPS'], false); const enableStoreGroup = getEnvVar('ENABLE_STORE_GROUP', ['Functions', 'ENABLE_STORE_GROUP'], false); const enableDeleteGroup = getEnvVar('ENABLE_DELETE_GROUP', ['Functions', 'ENABLE_DELETE_GROUP'], false); const enableCreateSource = getEnvVar('ENABLE_CREATE_SOURCE', ['Functions', 'ENABLE_CREATE_SOURCE'], false); const enableEditSource = getEnvVar('ENABLE_EDIT_SOURCE', ['Functions', 'ENABLE_EDIT_SOURCE'], false); const enableDeleteSource = getEnvVar('ENABLE_DELETE_SOURCE', ['Functions', 'ENABLE_DELETE_SOURCE'], false); const enableGetSource = getEnvVar('ENABLE_GET_SOURCE', ['Functions', 'ENABLE_GET_SOURCE'], false); const enableListSources = getEnvVar('ENABLE_LIST_SOURCES', ['Functions', 'ENABLE_LIST_SOURCES'], false); const enableStoreUser = getEnvVar('ENABLE_STORE_USER', ['Functions', 'ENABLE_STORE_USER'], false); const enableEditUser = getEnvVar('ENABLE_EDIT_USER', ['Functions', 'ENABLE_EDIT_USER'], false); const enableDeleteUser = getEnvVar('ENABLE_DELETE_USER', ['Functions', 'ENABLE_DELETE_USER'], false); console.log('🌐 Server Config:', JSON.stringify(envConfig, null, 2)); console.log('✅ Private API URL:', privateApiUrl); console.log('✅ Public API URL:', apiUrl); console.log('✅ Port:', Port); console.log('✅ Language:', requestedLang); console.log('✅ SSL-validation:', sslValidate); console.log('✅ PW_Encryption:', PwEncryption); console.log('✅ Allow_Keygen:', AllowKeygen); console.log('✅ Private_Key:', privateKeyPath); console.log('✅ Public_Key:', publicKeyPath); console.log('✅ Restricted Groups:', restrictedGroups); // Alle Funktionen mit ihren Enable-Flags in einem Array organisieren const allFunctions = [ { name: 'Login', enabled: enableLogin }, { name: 'Logout', enabled: enableLogout }, { name: 'Chat', enabled: enableChat }, { name: 'Continue Chat', enabled: enableContinueChat }, { name: 'Get Chat Info', enabled: enableGetChatInfo }, { name: 'List Groups', enabled: enableListGroups }, { name: 'Store Group', enabled: enableStoreGroup }, { name: 'Delete Group', enabled: enableDeleteGroup }, { name: 'Create Source', enabled: enableCreateSource }, { name: 'Edit Source', enabled: enableEditSource }, { name: 'Delete Source', enabled: enableDeleteSource }, { name: 'Get Source', enabled: enableGetSource }, { name: 'List Sources', enabled: enableListSources }, { name: 'Store User', enabled: enableStoreUser }, { name: 'Edit User', enabled: enableEditUser }, { name: 'Delete User', enabled: enableDeleteUser } ]; // Filtern, um nur deaktivierte (false) zu bekommen const disabledFunctions = allFunctions.filter(f => !f.enabled); // Wenn gar keine Funktionen deaktiviert sind, kurze Info ausgeben if (disabledFunctions.length === 0) { console.log('✅ All functions are enabled.'); } else { console.log('⚠️ Deactivated functions:'); disabledFunctions.forEach(func => { console.log(` ➡️ ${func.name}: false`); }); } // Nachrichten basierend auf Sprache let lang = getEnvVar('LANGUAGE', ['Server_Config', 'LANGUAGE'], 'en').toLowerCase(); if (!(lang in messages)) { console.warn(`⚠️ Language "${lang}" is not supported. Fallback in English.`); lang = 'en'; } const t = messages[lang]; console.log(t.apiUrlValidated.replace('${url}', apiUrl)); // Port validieren console.log(t.portValidated.replace('${port}', Port)); // Debugging für RESTRICTED_GROUPS console.log('🛠️ Access to RESTRICTED_GROUPS:', envConfig.Restrictions?.RESTRICTED_GROUPS); // Zugriff und Validierung von RESTRICTED_GROUPS const isRestrictedGroupsEnabled = validateBoolean( 'RESTRICTED_GROUPS', restrictedGroups, t ); console.log( t.restrictedGroupsSuccess.replace('${status}', isRestrictedGroupsEnabled) ); // SSL-Validierung const isSSLValidationEnabled = validateBoolean( 'SSL_VALIDATE', getEnvVar('SSL_VALIDATE', ['Server_Config', 'SSL_VALIDATE'], 'false').toString(), t ); const sslSymbol = isSSLValidationEnabled ? '✔️' : '⚠️'; console.log( t.sslValidationSet .replace('${symbol}', sslSymbol) .replace('${value}', String(isSSLValidationEnabled)) ); // console.log('sslValidationSet String ist:', t.sslValidationSet); const validatedPort = validatePort(Port, t); console.log(t.portValidated.replace('${port}', validatedPort)); // Beispiel: Tool-Überprüfung function isToolEnabled(toolName) { const envKey = `ENABLE_${toolName.toUpperCase()}`; if (!(envKey in envConfig.Functions)) { console.warn(`⚠️ Tool "${toolName}" is not defined in the configuration. Default: deactivated.`); return false; } return envConfig.Functions[envKey] === true; } /* ################ Helper Functions ############################*/ // Helper-Funktion, um zu prüfen, ob ein Tool aktiviert ist, und eine Fehlermeldung zu generieren function checkToolEnabled(toolName) { if (toolName === "keygen" && AllowKeygen) { return null; } if (!isToolEnabled(toolName)) { return { status: 'error', message: messages[lang].toolDisabledError.replace('${toolName}', toolName), }; } return null; // Tool ist aktiviert } function validateToken(token) { if (!token) { return { status: 'error', message: 'Token fehlt. Bitte einloggen und erneut versuchen.', statusCode: 401 // Optional für konsistenten HTTP-Status }; } return null; } function getArguments(input) { if (input.arguments) { return input.arguments; } else if (input.params?.arguments) { return input.params.arguments; } else { console.error(t.invalidArgumentsError.replace('${input}', JSON.stringify(input))); return {}; // Leeres Objekt als Fallback } } // Parameter zuordnen const API_URL = apiUrl; const PORT = Port; // Server-Startkonfiguration ausgeben const serverConfig = JSON.stringify({ API_URL, PORT }, null, 2); console.log( messages[lang].startingServerWithConfig .replace('${config}', serverConfig) ); //const net = require('net'); class TcpServerTransport { constructor(port) { this.port = port; this.server = null; this.clients = new Map(); // Map zur Speicherung der Clients } async start(onMessage) { return new Promise((resolve, reject) => { // Server erstellen this.server = net.createServer((socket) => { const clientIP = socket.remoteAddress || 'unbekannt'; const clientPort = socket.remotePort || 'unbekannt'; // Client-Informationen in der Map speichern this.clients.set(socket, { ip: clientIP, port: clientPort }); console.log(t.connection.new.replace('${ip}', clientIP).replace('${port}', clientPort)); // Ereignis: Daten empfangen socket.on('data', async (data) => { const client = this.clients.get(socket); console.log(t.connection.dataReceived .replace('${ip}', client.ip) .replace('${port}', client.port) .replace('${data}', data.toString())); try { const message = JSON.parse(data.toString()); const response = await onMessage(message); socket.write(JSON.stringify(response)); } catch (err) { console.error(t.errors.processMessage.replace('${error}', err.message || err)); socket.write(JSON.stringify({ error: t.errors.invalidMessage })); } }); // Ereignis: Verbindung geschlossen socket.on('close', () => { const client = this.clients.get(socket); console.log(t.connection.closed.replace('${ip}', client.ip).replace('${port}', client.port)); this.clients.delete(socket); // Client aus der Map entfernen }); // Fehlerbehandlung für einzelne Sockets socket.on('error', (err) => { const client = this.clients.get(socket); console.error(t.errors.socketError .replace('${ip}', client?.ip || 'unknown') .replace('${port}', client?.port || 'unknown') .replace('${error}', err.message || err)); }); // Server-Ereignis: Verbindung hergestellt this.server.on('connection', (socket) => { console.log(t.connection.established); socket.setKeepAlive(true, 30000); // Keep-Alive für jede Verbindung setzen }); }); // Server starten this.server.listen(this.port, () => { console.log(t.server.running.replace('${port}', this.port)); resolve(); }); // Server-Ereignis: Fehler this.server.on('error', (err) => { console.error(t.errors.serverError.replace('${error}', err.message || err)); reject(err); }); }); } async stop() { if (this.server) { this.server.close(() => { console.error(t.server.stopped); this.clients.clear(); // Alle Clients aus der Map entfernen }); } } } class PrivateGPTServer { server; axiosInstance; authToken = null; // Initialisierung des Authentifizierungs-Tokens authTokenTimestamp = 0; // Initialisierung des Zeitstempels für Token-Gültigkeit constructor() { this.server = new Server({ name: 'pgpt-mcp-server', version: '2.1.0', }, { capabilities: { resources: {}, tools: {}, }, }); // Create axios instance with SSL disabled for development this.axiosInstance = axios.create({ baseURL: API_URL, headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, httpsAgent: new https.Agent({ rejectUnauthorized: isSSLValidationEnabled // Dynamisch durch SSL_VALIDATE gesteuert }) }); // console.log(t.sslValidationSet.replace('${value}', sslValidate)); // Interceptors for logging requests and responses this.axiosInstance.interceptors.request.use((config) => { console.log(t.requestSent); console.log('URL:', config.baseURL + config.url); console.log('Method:', config.method.toUpperCase()); console.log('Headers:', config.headers); if (config.data) { console.log('Payload:', config.data); } return config; }, (error) => { console.error(t.requestError.replace('${error}', error.message || error)); return Promise.reject(error); }); this.axiosInstance.interceptors.response.use((response) => { console.log(t.responseReceived); console.log('Status:', response.status); console.log('Data:', response.data); return response; }, (error) => { console.error(t.responseError, error.response ? error.response.data : error.message); return Promise.reject(error); }); this.setupResourceHandlers(); this.setupToolHandlers(); // Error handling this.server.onerror = (error) => { console.error(t.mcpError.replace('${error}', error.message || JSON.stringify(error, null, 2))); }; process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } // Neue Login-Funktion async login(email, password) { console.error(`Authenticating user: ${email}`); try { const loginResponse = await this.axiosInstance.post('/login', { email, password }); console.error('Login response:', loginResponse.data); // Rückgabe des Tokens return loginResponse.data.data.token; } catch (error) { console.error('Login error:', error.message || error); throw new Error('Authentication failed'); } } // ensureAuthenticated nutzt die neue login-Funktion async ensureAuthenticated(token) { if (!token) { console.error('Fehlendes Token. Bitte zuerst einloggen.'); throw new Error('Fehlendes Token.'); } console.log('Setting token for authentication...'); // Setze das Token als Authorization-Header this.axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${token}`; console.log('Token erfolgreich gesetzt.'); } isTokenExpired() { // Beispielprüfung: Token sollte innerhalb einer Stunde erneuert werden const EXPIRATION_THRESHOLD = 3600 * 1000; // 1 Stunde const now = Date.now(); const tokenAge = now - this.authTokenTimestamp; return tokenAge >= EXPIRATION_THRESHOLD; } async validateGroups(groups, token) { try { console.log(t.checkingGroups.replace('${groups}', JSON.stringify(groups))); // Sicherstellen, dass der Token gesetzt ist if (!token) { throw new Error('Token fehlt. Kann Gruppen nicht validieren.'); } // Temporär den Header setzen, falls global nicht vorhanden const response = await this.axiosInstance.get('/groups', { headers: { Authorization: `Bearer ${token}` } }); const availableGroups = response.data?.data?.assignableGroups || []; console.log(t.availableGroups.replace('${availableGroups}', JSON.stringify(availableGroups))); // Überprüfen, ob die übergebenen Gruppen gültig sind const invalidGroups = groups.filter(group => !availableGroups.includes(group)); if (invalidGroups.length > 0) { console.error(t.invalidGroupsError.replace('${invalidGroups}', JSON.stringify(invalidGroups))); return { isValid: false, invalidGroups }; } return { isValid: true }; } catch (error) { const errorMessage = error.response?.data || error.message; console.error(t.groupValidationError.replace('${error}', errorMessage)); throw new Error(error.response?.data?.message || 'Fehler beim Abrufen der Gruppen.'); } } setupResourceHandlers() { // List available resources this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [] })); // List resource templates this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({ resourceTemplates: [] })); // Read resource this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { if (!request.params?.uri) { throw new McpError(ErrorCode.InvalidRequest, 'Missing URI parameter'); } throw new McpError(ErrorCode.InvalidRequest, `Invalid URI: ${request.params.uri}`); }); } setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { /* 1.0 Login #####################################################################################*/ name: 'login', description: 'User login to retrieve an API token', inputSchema: { type: 'object', properties: { email: { type: 'string', description: 'User email address for login' }, password: { type: 'string', description: 'User password for login' } }, required: ['email', 'password'] } }, { /* 1.1 Logout ####################################################################################*/ name: 'logout', description: 'Invalidate the API token', inputSchema: { type: 'object', properties: {} }, }, { /* 2.0 Chat ######################################################################################*/ name: 'chat', description: 'Start or continue a chat with PrivateGPT with optional RAG capabilities', inputSchema: { type: 'object', properties: { question: { type: 'string', description: 'The question or prompt to send' }, usePublic: { type: 'boolean', description: 'Use public knowledge base', default: false }, groups: { type: 'array', items: { type: 'string' }, description: 'Group names for RAG (exclusive with usePublic)', }, language: { type: 'string', description: 'Language code (e.g., "en")', default: 'en' }, }, required: ['question'], }, }, { /* 2.1 Continue Chat #############################################################################*/ name: 'continue_chat', description: 'Continue an existing chat', inputSchema: { type: 'object', properties: { chatId: { type: 'string', description: 'ID of the existing chat to continue' }, question: { type: 'string', description: 'The next question or message in the chat' }, }, required: ['chatId', 'question'], }, }, { /* 2.2 Get Chat Info #############################################################################*/ name: 'get_chat_info', description: 'Retrieve details about an existing chat using its ID', inputSchema: { type: 'object', properties: { chatId: { type: 'string', description: 'ID of the chat to retrieve details for' }, token: { type: 'string', description: 'Authorization token for API access' }, }, required: ['chatId', 'token'], }, }, { /* 3.0 Create Source #############################################################################*/ name: 'create_source', description: 'Create a new source with automatic markdown formatting', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Name of the source' }, content: { type: 'string', description: 'Markdown-formatted content' }, groups: { type: 'array', items: { type: 'string' }, description: 'Optional groups to assign the source to', }, }, required: ['name', 'content'], }, }, { /* 3.1 Get Source ################################################################################*/ name: 'get_source', description: 'Retrieve information about a specific source', inputSchema: { type: 'object', properties: { sourceId: { type: 'string', description: 'ID of the source to retrieve' }, }, required: ['sourceId'], }, }, { /* 3.2 List Sources ##############################################################################*/ name: 'list_sources', description: 'List all sources in a specific group', inputSchema: { type: 'object', properties: { groupName: { type: 'string', description: 'Group name to list sources from' }, }, required: ['groupName'], }, }, { /* 3.3 Edit Source ###############################################################################*/ name: 'edit_source', description: 'Edit an existing source', inputSchema: { type: 'object', properties: { sourceId: { type: 'string', description: 'ID of the source to edit' }, token: { type: 'string', description: 'Authorization token for API access' }, title: { type: 'string', description: 'New title for the source (optional)' }, content: { type: 'string', description: 'New markdown-formatted content for the source (optional)' }, groups: { type: 'array', items: { type: 'string' }, description: 'Updated group(s) to assign to the source (optional)' } }, required: ['sourceId', 'token'] } }, { /* 3.4 Delete Source #############################################################################*/ name: 'delete_source', description: 'Delete a specific source', inputSchema: { type: 'object', properties: { sourceId: { type: 'string', description: 'ID of the source to delete' }, }, required: ['sourceId'], }, }, { /* 4.0 List Groups ###############################################################################*/ name: 'list_groups', description: 'Retrieve personal and assignable groups', inputSchema: { type: 'object', properties: {} }, }, { /* 4.1 Store Group ###############################################################################*/ name: 'store_group', description: 'Create a new group', inputSchema: { type: 'object', properties: { groupName: { type: 'string', description: 'Name of the new group' }, description: { type: 'string', description: 'Description of the new group' }, }, required: ['groupName'], }, }, { /* 4.2 Delete Group ##############################################################################*/ name: 'delete_group', description: 'Delete an existing group', inputSchema: { type: 'object', properties: { groupName: { type: 'string', description: 'Name of the group to delete' }, }, required: ['groupName'], }, }, { /* 5.0 Store User ################################################################################*/ name: 'store_user', description: 'Create a new user', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Name of the user' }, email: { type: 'string', description: 'Email of the user' }, password: { type: 'string', description: 'Password for the user' }, language: { type: 'string', description: 'Preferred language (optional)', default: 'en' }, timezone: { type: 'string', description: 'Timezone (optional)', default: 'Europe/Berlin' }, roles: { type: 'array', items: { type: 'string' }, description: 'Roles to assign (optional)' }, groups: { type: 'array', items: { type: 'string' }, description: 'Groups to assign (optional)' }, usePublic: { type: 'boolean', description: 'Enable public knowledge (optional)', default: false } }, required: ['name', 'email', 'password'] }, }, { /* 5.1 Edit User #################################################################################*/ name: 'edit_user', description: 'Edit an existing user', inputSchema: { type: 'object', properties: { email: { type: 'string', description: 'Email of the user to edit' }, name: { type: 'string', description: 'New name for the user (optional)' }, password: { type: 'string', description: 'New password for the user (optional)' }, language: { type: 'string', description: 'Preferred language (optional)' }, timezone: { type: 'string', description: 'Timezone (optional)' }, roles: { type: 'array', items: { type: 'string' }, description: 'Updated roles (optional)' }, groups: { type: 'array', items: { type: 'string' }, description: 'Updated groups (optional)' }, usePublic: { type: 'boolean', description: 'Enable public knowledge (optional)' } }, required: ['email'] } }, { /* 5.2 Delete User ###############################################################################*/ name: 'delete_user', description: 'Delete an existing user', inputSchema: { type: 'object', properties: { email: { type: 'string', description: 'Email of the user to delete' } }, required: ['email'] } } ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { if (!request.params?.name) { throw new McpError(ErrorCode.InvalidRequest, 'Missing tool name'); } try { //await this.ensureAuthenticated(); console.error(t.handlingToolRequest.replace('${name}', request.params.name), request.params); switch (request.params.name) { /* 1.0 Login ######################################################################################*/ case 'login': { const disabledResponse = checkToolEnabled('login'); if (disabledResponse) return disabledResponse; const { email, password } = request.params.arguments; // Extrahiere email und password aus der Nachricht if (!email || !password) { return { status: 'E10-R-1000', message: 'Login fehlgeschlagen: E-Mail und Passwort sind erforderlich.', }; } try { // Aufruf des Login-Endpunkts der API const loginResponse = await this.axiosInstance.post('/login', { email, password }); console.log(t.loginSuccess.replace('${data}', JSON.stringify(loginResponse.data))); ; // Token zurückgeben return { status: loginResponse.data?.status || 'ok', // Dynamisch, falls der API-Status einheitlich ist message: loginResponse.data?.message || 'Login erfolgreich.', // API-Nachricht verwenden oder Standardnachricht token: loginResponse.data?.data?.token, // Token aus API-Antwort }; } catch (error) { const errorMessage = error.response?.data || error.message; console.error(t.loginError.replace('${error}', errorMessage)); return { status: error.response?.status || 'E10-R-1001', // API-Fehlerstatus oder Standardfehlerstatus, message: error.response?.data?.message || 'Login fehlgeschlagen.', }; } } /* 1.1 Logout #####################################################################################*/ case 'logout': { const disabledResponse = checkToolEnabled('logout'); if (disabledResponse) return disabledResponse; const { token } = message; //const { token, arguments: args } = request.params; const tokenValidation = validateToken(token); if (tokenValidation) return tokenValidation; try { const logoutResponse = await this.axiosInstance.delete('/logout', { headers: { Authorization: `Bearer ${token}` } }); console.log(t.logoutSuccess.replace('${data}', JSON.stringify(logoutResponse.data))); return { data: {}, // Optional: Zusätzliche Daten könnten hier eingefügt werden message: 'success', status: 200, // OK }; } catch (error) { const logoutErrorMessage = error.response?.data || error.message; console.error(t.logoutError.replace('${error}', logoutErrorMessage)); return { data: {}, message: error.response?.data?.message || 'Logout fehlgeschlagen. Bitte versuchen Sie es später erneut.', status: error.response?.status || 'E11-R-1100', // Internal Server Error oder spezifischer Statuscode }; } } /* 2.0 Chat #######################################################################################*/ case 'chat': { const disabledResponse = checkToolEnabled('chat'); if (disabledResponse) return disabledResponse; const { token, arguments: args } = request.params; console.log('Extrahierter Token:', token); // Token prüfen und validieren if (!token) { console.error(t.noTokenError); return { status: 'E20-R-2000', message: t.missingTokenError }; } /*const tokenValidation = validateToken(token); if (tokenValidation) { console.error('❌ Token-Validierung fehlgeschlagen. Token:', token, 'Fehler:', tokenValidation); return { status: 'error', message: 'Token ist ungültig oder abgelaufen. Bitte erneut einloggen.', }; }*/ // Argument-Validierung if (!args || !args.question) { console.error(t.missingArgumentsError.replace('${args}', JSON.stringify(args))); return { status: 'error', message: t.missingArgumentsError.replace('${args}', JSON.stringify(args)), }; } const { question, usePublic, groups, language } = args; // Konflikt zwischen `usePublic` und `groups` lösen if (usePublic && groups && groups.length > 0) { console.warn(t.publicGroupsConflictWarning); args.usePublic = false; } try { console.log(t.sendingChatRequest .replace('${question}', question) .replace('${usePublic}', usePublic) .replace('${groups}', JSON.stringify(groups)) .replace('${language}', language) ); const response = await this.axiosInstance.post( '/chats', { question, usePublic: usePublic || false, groups: Array.isArray(groups) ? groups : [groups], language: language || 'de', }, { headers: { Authorization: `Bearer ${token}` } } ); const data = response.data?.data || {}; console.log(t.chatResponseSuccess.replace('${data}', JSON.stringify(data))); // Erfolgsantwort mit Status und Daten return { status: response.data?.status || 'ok', message: response.data?.message || 'Chat erfolgreich.', content: { chatId: data.chatId, answer: data.answer, sources: data.sources || [], }, }; } catch (error) { const chatApiErrorMessage = error.message || error.response?.data; console.error(t.chatApiError.replace('${error}', chatApiErrorMessage)); // Fehlerantwort mit Status und Nachricht return { status: error.response?.status || 'E20-R-2002', message: error.response?.data?.message || 'Fehler bei der Chat-Anfrage.', }; } } /* 2.1 Continue Chat ##############################################################################*/ case 'continue_chat': { const disabledResponse = checkToolEnabled('continue_chat'); if (disabledResponse) return disabledResponse; const args = request.params.arguments; if (!args || !args.chatId || !args.question) { return { status: 'E21-R-2100', message: 'Fehlende erforderliche Parameter: chatId und/oder question.' }; } const { chatId, question } = args; console.log(t.conversationContinuation.replace('${chatId}', chatId)); try { const continueChatResponse = await this.axiosInstance.patch(`/chats/${chatId}`, { question: question, }); console.log(t.conversationSuccess.replace('${data}', JSON.stringify(continueChatResponse.data, null, 2))); return { content: { chatId: continueChatResponse.data.data.chatId, answer: continueChatResponse.data.data.answer, sources: continueChatResponse.data.sources || [], message: continueChatResponse.data.message, status: continueChatResponse.data.status, }, }; } catch (error) { console.error(t.apiRequestError.replace('${error}', error.message)); return { status: error.response?.status || 'E21-R-2101', message: error.response?.data?.message || 'Fehler beim Fortsetzen der Konversation.', }; } } /* 2.2 Get Chat Info ##############################################################################*/ case 'get_chat_info': { const disabledResponse = checkToolEnabled('get_chat_info'); if (disabledResponse) return disabledResponse; const { token, arguments: args } = request.params; const tokenValidation = validateToken(token); if (tokenValidation) return tokenValidation; const { chatId } = args; if (!chatId) { return { status: 'E22-R-2200', message: 'chatId ist erforderlich, um Chat-Informationen abzurufen.' }; } try { const response = await this.axiosInstance.get(`/chats/${chatId}`, { headers: { Authorization: `Bearer ${token}` } }); const chatData = response.data?.data; if (!chatData) { return { status: 'E22-R-2201', message: t.noChatData, }; } return { data: { chatId: chatData.chatId, title: chatData.title || 'Unbenannter Chat', language: chatData.language || 'Unbekannt', groups: chatData.groups || [], messages: chatData.messages || [] }, message: response.data?.message || 'Erfolgreich abgerufen.' }; } catch (error) { const fetchChatErrorMessage = error.message || error.response?.data; console.error(t.fetchChatInfoError.replace('${error}', fetchChatErrorMessage)); return { status: 'E22-R-2202', message: error.response?.data?.message || 'Fehler beim Abrufen der Chat-Informationen.' }; } } /* 3.0 Create Source ##############################################################################*/ case 'create_source': { const disabledResponse = checkToolEnabled('create_source'); if (disabledResponse) return disabledResponse; const args = request.params.arguments; const token = request.params.token; // Validierung: Erforderliche Parameter prüfen if (!token) { return { status: 'E30-R-3000', message: t.missingTokenError }; } if (!args || !args.name || !args.content) { return { status: 'E30-R-3001', message: t.missingParametersError.replace('${parameters}', 'name und content') }; } const { name, content, groups } = args; try { // Token im Header setzen this.axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${token}`; // Gruppenvalidierung vorab durchführen if (groups && groups.length > 0) { console.log('📤 Überprüfen der Gruppen:', groups); const response = await this.axiosInstance.get('/groups'); const availableGroups = response.data?.data?.assignableGroups || []; // Ungültige Gruppen ermitteln const invalidGroups = groups.filter(group => !availableGroups.includes(group)); if (invalidGroups.length > 0) { console.error('❌ Ungültige Gruppen gefunden:', invalidGroups); return { status: 'E30-R-3002', message: `Ungültige Gruppen: ${invalidGroups.join(', ')}`, }; } } // API-Aufruf zur Erstellung der Quelle const createSourceResponse = await this.axiosInstance.post( '/sources', { name, content, groups }, { headers: { Authorization: `Bearer ${token}` } } ); console.log('✔️ Quelle erfolgreich erstellt:', createSourceResponse.data); // Erfolgsantwort return { status: createSourceResponse.data?.status || 'ok', message: createSourceResponse.data?.message || 'Quelle erfolgreich erstellt.', data: createSourceResponse.data?.data, }; } catch (error) { const createSourceError = error.response?.data || error.message; console.error(t.createSourceError.replace('${error}', createSourceError)); // console.error('❌ Fehler beim Erstellen der Quelle:', error.response?.data || error.message); // Fehlerhafte Antwort if (error.response) { return { status: 'E30-R-3003', message: error.response.data?.message || 'Ein Fehler ist aufgetreten.', details: { status: error.response.status, headers: error.response.headers, data: error.response.data, }, }; } else if (error.request) { return { status: 'E30-R-3004', message: t.noServerResponse, details: { request: error.request }, }; } else { return { status: 'E30-R-3005', message: error.message || 'Ein unbekannter Fehler ist aufgetreten.', }; } } } /* 3.1 Get Source #################################################################################*/ case 'get_source': { const disabledResponse = checkToolEnabled('get_source'); if (disabledResponse) return disabledResponse; const args = request.params.arguments; console.log(t.makingGetSourceRequest.replace('${args}', JSON.stringify(args, null, 2))); const getSourceResponse = await this.axiosInstance.get(`/sources/${args.sourceId}`); console.log(t.gotGetSourceResponse.replace('${data}', JSON.stringify(getSourceResponse.data, null, 2))); return { content: [ { type: 'text', text: JSON.stringify(getSourceResponse.data, null, 2) } ] }; } /* 3.2 List Sources ###############################################################################*/ case 'list_sources': { const disabledResponse = checkToolEnabled('list_sources'); if (disabledResponse) return disabledResponse; const args = request.params.arguments; console.log(t.makingListSourcesRequest.replace('${args}', JSON.stringify(args, null, 2))); const listSourcesResponse = await this.axiosInstance.post('/sources/groups', { groupName: args.groupName }); console.log(t.gotListSourcesResponse.replace('${data}', JSON.stringify(listSourcesResponse.data, null, 2))); return { content: [ { type: 'text', text: JSON.stringify(listSourcesResponse.data, null, 2) } ] }; } /* 3.3 Edit Source ################################################################################*/ case 'edit_source': { const disabledResponse = checkToolEnabled('edit_source'); if (disabledResponse) return disabledResponse; const { token, arguments: args } = request.params; // Validierung: Token erforderlich const tokenValidation = validateToken(token); if (tokenValidation) return tokenValidation; const { sourceId, title, content, groups } = args; // Validierung: Pflichtfeld `sourceId` if (!sourceId) { return { data: {}, message: t.missingParameterError.replace('${parameter}', 'sourceId'), status: 'E33-R-3300', // Bad Request }; } console.log(`Bearbeite Quelle mit ID: ${sourceId}, Titel: ${title || 'unverändert'}`); try { // Payload dynamisch erstellen const payload = {}; if (title) payload.title = title; if (content) payload.content = content; if (groups) payload.groups = groups; // API-Aufruf: Quelle bearbeiten const editSourceResponse = await this.axiosInstance.patch( `/sources/${sourceId}`, payload, { headers: { Authorization: `Bearer ${token}`, // Nutze den bereitgestellten Token }, } ); console.log(t.editSourceSuccess.replace('${data}', JSON.stringify(editSourceResponse.data, null, 2))); // Erfolgreiche Antwort return { data: editSourceResponse.data?.data || {}, // Optionale Daten aus der API message: editSourceResponse.data?.message || 'Quelle erfolgreich bearbeitet.', status: editSourceResponse.status || 200, // OK }; } catch (error) { const editSourceError = error.message || JSON.stringify(error.response?.data); console.error(t.editSourceError.replace('${error}', editSourceError)); // console.error(`❌ Fehler beim Bearbeiten der Quelle: ${error.message || JSON.stringify(error.response?.data)}`); // Fehlerhafte Antwort return { data: {}, message: error.response?.data?.message || 'Bearbeiten der Quelle fehlgeschlagen. Bitte versuchen Sie es später erneut.', status: error.response?.status || 'E33-R-3301', // Internal Server Error }; } } /* 3.4 Delete Source ##############################################################################*/ case 'delete_source': { const disabledResponse = checkToolEnabled('delete_source'); if (disabledResponse) return disabledResponse; const { token, arguments: args } = request.params.arguments; // Validierung: Token erforderlich const tokenValidation = validateToken(token); if (tokenValidation) return tokenValidation; const { sourceId } = args; // Validierung: sourceId erforderlich if (!sourceId) { return { data: {}, message: t.missingParameterError.replace('${parameter}', 'sourceId'), status: 'E34-R-3400', // Bad Request }; } try { // API-Aufruf: Quelle löschen const deleteResponse = await this.axiosInstance.delete(`/sources/${sourceId}`, { headers: { Authorization: `Bearer ${token}` }, }); console.log(`Quelle erfolgreich gelöscht: ${JSON.stringify(deleteResponse.data, null, 2)}`); // Erfolgreiche Antwort return { data: deleteResponse.data?.data || {}, // Optionale Daten aus der API message: deleteResponse.data?.message || 'Quelle erfolgreich gelöscht.', status: deleteResponse.status || 200, // OK }; } catch (error) { // console.error(`❌ Fehler beim Löschen der Quelle: ${error.message || JSON.stringify(error.response?.data)}`); const deleteSourceError = error.message || JSON.stringify(error.response?.data); console.error(t.deleteSourceError.replace('${error}', deleteSourceError)); // Fehlerhafte Antwort return { data: {}, message: error.response?.data?.message || 'Löschen der Quelle fehlgeschlagen. Bitte versuchen Sie es später erneut.', status: error.response?.status || 'E34-R-3401', // Internal Server Error }; } } /* 4.0 List Groups ################################################################################*/ case 'list_groups': { const disabledResponse = checkToolEnabled('list_groups'); if (disabledResponse) return disabledResponse; console.log(t.makingListGroupsRequest); const listGroupsResponse = await this.axiosInstance.get('/groups'); console.log(t.gotListGroupsResponse, listGroupsResponse.data); return { content: [ { type: 'text', text: JSON.stringify(listGroupsResponse.data, null, 2) } ] }; } /* 4.1 Store Group ################################################################################*/ case 'store_group': { const disabledResponse = checkToolEnabled('store_group'); if (disabledResponse) return disabledResponse; const args = request.params.arguments; if (!args || !args.groupName) { throw new McpError(ErrorCode.InvalidRequest, 'Store: Fehlender erforderlicher Parameter: groupName.'); } console.log( t.storeGroupLog .replace('${groupName}', args.groupName) .replace('${description}', args.description || 'Keine Beschreibung angegeben') ); // console.log(`Storing new group with name: ${args.groupName} and description: ${args.description || 'No description provided'}`); try { const storeGroupResponse = await this.axiosInstance.post('/groups', { groupName: args.groupName, description: args.description || '' }); console.log( t.storeGroupSuccess.replace('${data}', JSON.stringify(storeGroupResponse.data, null, 2)) ); // console.log(`Group stored successfully: ${JSON.stringify(storeGroupResponse.data, null, 2)}`); return { content: [ { type: 'text', text: `Group "${args.groupName}" successfully stored with ID: ${storeGroupResponse.data.id}` } ] }; } catch (error) { console.error(t.errorHandlingRequest.replace('${error}', error.message || JSON.stringify(error, null, 2))); if (axios.isAxiosError(error)) { const message = error.response?.data?.message ?? error.message; console.error( t.apiErrorDetails.replace('${status}', error.response?.status || 'E41-R-4100').replace('${data}', JSON.stringify(error.response?.data || {}, null, 2)) ); return { content: [ { type: 'text', text: `API error: ${message}` } ], isError: true }; } throw error; } }; /* 4.2 Delete Group ###############################################################################*/ case 'delete_group': { const disabledResponse = checkToolEnabled('delete_group'); if (disabledResponse) return disabledResponse; const { groupName } = request.params.arguments; // Extrahiere die Gruppe if (!groupName) { throw new McpError(ErrorCode.InvalidRequest, 'Delete Fehlender erforderlicher Parameter: groupName.'); } // console.log(`Lösche Gruppe mit Name: ${groupName}`); console.log(t.deleteGroupLog.replace('${groupName}', groupName)); try { // API-Aufruf mit dem notwendigen JSON-Body const deleteGroupResponse = await this.axiosInstance.delete('/groups', { data: { groupName }, // JSON-Body für den DELETE-Request }); // console.log(t.deleteGroupSuccessLog.replace('${data}', JSON.stringify(deleteGroupResponse.data))); console.log(t.deleteGroupSuccessLog.replace('${data}', JSON.stringify(deleteGroupResponse.data))); return { content: [ { type: 'text', text: `Gruppe "${groupName}" wurde erfolgreich gelöscht.`, }, ], }; } catch (error) { // console.error(`Fehler bei der API-Anfrage: ${error.message || JSON.stringify(error.response?.data)}`); const apiError = error.message || JSON.stringify(error.response?.data); console.error(t.apiRequestError.replace('${error}', apiError)); if (axios.isAxiosError(error)) { const message = error.response?.data?.message || 'Fehler beim Löschen der Gruppe.'; return { content: [ { type: 'text', text: `API-Fehler: ${message}`, }, ], isError: true, }; } throw new McpError(ErrorCode.InternalError, 'Interner Fehler beim Löschen der Gruppe.'); } } /* 5.0 Store User #################################################################################*/ case 'store_user': { const disabledResponse = checkToolEnabled('store_user'); if (disabledResponse) return disabledResponse; // Token und Argumente aus request.params entnehmen const { token, arguments: args } = request.params; // Token validieren const tokenValidation = validateToken(token); if (tokenValidation) return tokenValidation; // Erforderliche Felder prüfen if (!args || !args.name || !args.email || !args.password) { return { status: 'E50-R-5000', message: 'Fehlende erforderliche Felder: name, email oder password.' }; } try { // Benutzer anlegen const response = await this.axiosInstance.post( '/users', { name: args.name, email: args.email, password: args.password, language: args.language || 'en', timezone: args.timezone || 'Europe/Berlin', roles: args.roles || [], groups: args.groups || [], usePublic: args.usePublic || false }, { headers: { Authorization: `Bearer ${token}` } } ); // Erfolgreiche Antwort return { status: response.data?.status || 'ok', message: response.data?.message || 'Benutzer erfolgreich erstellt.', data: response.data?.data }; } catch (error) { // console.error('❌ Fehler beim Erstellen des Benutzers:', error.response?.data || error.message); const createUserError = error.response?.data || error.message; console.error(t.createUserError.replace('${error}', createUserError)); return { status: error.response?.status || 'E50-R-5001', message: error.response?.data?.message || 'Fehler beim Erstellen des Benutzers.' }; } } /* 5.1 Edit User ##################################################################################*/ case 'edit_user': { const disabledResponse = checkToolEnabled('edit_user'); if (disabledResponse) return disabledResponse; const { token, arguments: args } = request.params; const tokenValidation = validateToken(token); if (tokenValidation) return tokenValidation; // Mindestens die E-Mail muss angegeben sein, um den User zu identifizieren if (!args || !args.email) { return { status: 'E51-R-5100', message: 'Die E-Mail des Benutzers ist erforderlich, um den Datensatz zu bearbeiten.' }; } try { // Nur Felder senden, die tatsächlich aktualisiert werden sollen const payload = {}; if (args.name) payload.name = args.name; if (args.password) payload.password = args.password; if (args.language) payload.language = args.language; if (args.timezone) payload.timezone = args.timezone; if (Array.isArray(args.roles)) payload.roles = args.roles; if (Array.isArray(args.groups)) payload.groups = args.groups; if (typeof args.usePublic === 'boolean') payload.usePublic = args.usePublic; // E-Mail ist Pflicht, um den Benutzer auf dem Server zu finden payload.email = args.email; const response = await this.axiosInstance.patch( '/users', payload, { headers: { Authorization: `Bearer ${token}` } } ); return { status: response.data?.status || 'ok', message: response.data?.message || 'Benutzer erfolgreich bearbeitet.', data: response.data?.data }; } catch (error) { const editUserError = error.response?.data || error.message; console.error(t.editUserError.replace('${error}', editUserError)); // console.error('❌ Fehler beim Bearbeiten des Benutzers:', error.response?.data || error.message); return { status: error.response?.status || 'E51-R-5101', message: error.response?.data?.message || 'Fehler beim Bearbeiten des Benutzers.' }; } } /* 5.2 Delete User ################################################################################*/ case 'delete_user': { const disabledResponse = checkToolEnabled('delete_user'); if (disabledResponse) return disabledResponse; const { token, arguments: args } = request.params; const tokenValidation = validateToken(token); if (tokenValidation) return tokenValidation; // E-Mail ist nötig, um den Benutzer zu löschen if (!args || !args.email) { return { status: 'E52-R-5200', message: 'Die E-Mail ist erforderlich, um einen Benutzer zu löschen.' }; } try { // DELETE-Anfrage mit JSON-Body const response = await this.axiosInstance.delete( '/users', { data: { email: args.email }, headers: { Authorization: `Bearer ${token}` } } ); return { status: response.data?.status || 'ok', message: response.data?.message || 'Benutzer erfolgreich gelöscht.', data: response.data?.data }; } catch (error) { // console.error('❌ Fehler beim Löschen des Benutzers:', error.response?.data || error.message); const deleteUserError = error.response?.data || error.message; console.error(t.deleteUserError.replace('${error}', deleteUserError)); return { status: error.response?.status || 'E52-R-5201', message: error.response?.data?.message || 'Fehler beim Löschen des Benutzers.' }; } } default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`); } } catch (error) { console.error(t.errorHandlingRequest.replace('${error}', error.message || JSON.stringify(error, null, 2))); if (axios.isAxiosError(error)) { const message = error.response?.data?.message ?? error.message; console.error(t.apiErrorDetails.replace('${status}', error.response?.status || 'Unknown').replace('${data}', JSON.stringify(error.response?.data || {}, null, 2))); return { content: [ { type: 'text', text: `API error: ${message}` } ], isError: true }; } throw error; } }); } /* ################################################################################################## # MESSAGE HANDLER ##################################################################################################*/ async run() { const isPortInUse = (port) => new Promise((resolve, reject) => { const tester = net.createServer() .once('error', (err) => (err.code === 'EADDRINUSE' ? resolve(true) : reject(err))) .once('listening', () => tester.once('close', () => resolve(false)).close()) .listen(port); }); const PORT = Port; if (await isPortInUse(PORT)) { throw new Error(t.portInUse.replace('${PORT}', PORT)); } const transport = new TcpServerTransport(PORT); await transport.start(async (message) => { try { console.log(t.incomingMessage, message); // Token-Validierung nur durchführen, wenn es nicht der "login"-Befehl ist //if (message.command !== 'login') { // const tokenValidation = validateToken(message.token); // if (tokenValidation) return tokenValidation; // } if (!message || typeof message !== 'object') { throw new McpError(ErrorCode.InvalidRequest, 'Ungültige Nachricht oder leere Anfrage.'); } // Verarbeite verschiedene Anfragen dynamisch if (!message.command) { throw new McpError(ErrorCode.InvalidRequest, 'Fehlender Befehlsparameter in der Nachricht.'); } switch (message.command) { /* 1.0 Login ######################################################################################*/ case 'login': { const disabledResponse = checkToolEnabled('login'); if (disabledResponse) return disabledResponse; // Extrahiere die Argumente aus der Nachricht const args = getArguments(message); const { email, password: Pwd } = args; // Überprüfe, ob die E-Mail und das Passwort vorhanden sind if (!email || !Pwd) { return { status: 'E10-M-1050', message: 'E-Mail und Passwort sind erforderlich.', }; } let password; // Passwort entschlüsseln, falls erforderlich if (typeof PwEncryption !== 'undefined' && PwEncryption) { password = decryptPassword(Pwd); } else { password = Pwd; } try { // Login-API aufrufen const loginResponse = await this.axiosInstance.post('/login', { email, password }); console.log(t.loginSuccess.replace('${data}', JSON.stringify(loginResponse.data))); // Token zurückgeben return { status: loginResponse.data?.status || 'ok', // Dynamisch, falls der API-Status einheitlich ist message: loginResponse.data?.message || 'Login erfolgreich.', // API-Nachricht verwenden oder Standardnachricht token: loginResponse.data?.data?.token, // Token aus API-Antwort }; } catch (error) { const errorMessage = error.response?.data || error.message; console.error(t.loginError.replace('${error}', errorMessage)); // console.error('❌ Fehler beim Login:', error.response?.data || error.message); return { status: error.response?.status || 'E10-M-1051', // API-Fehlerstatus oder Standardfehlerstatus, message: error.response?.data || error.message || 'no error message' }; } } /* 1.1 Logout #####################################################################################*/ case 'logout': { const disabledResponse = checkToolEnabled('logout'); if (disabledResponse) return disabledResponse; const { token } = message; try { const logoutResponse = await this.axiosInstance.delete('/logout', { headers: { Authorization: `Bearer ${token}` } }); console.log(t.logoutSuccess.replace('${data}', JSON.stringify(logoutResponse.data))); // console.log('✔️ Logout erfolgreich:', logoutResponse.data); return { data: {}, // Optional: Zusätzliche Daten könnten hier eingefügt werden status: logoutResponse.data?.status || 'no status', // Dynamisch, falls der API-Status einheitlich ist message: logoutResponse.data?.message || 'no message', // API-Nachricht verwenden oder Standardnachricht }; } catch (error) { const logoutErrorMessage = error.response?.data || error.message; console.error(t.logoutError.replace('${error}', logoutErrorMessage)); // console.error('❌ Fehler beim Logout:', error.response?.data || error.message); return { data: {}, message: error.response?.data || error.message || 'no error message', status: error.response?.status || 'E11-M-1150', // Internal Server Error oder spezifischer Statuscode }; } } /* 2.0 Chat #######################################################################################*/ case 'chat': { const disabledResponse = checkToolEnabled('chat'); if (disabledResponse) return disabledResponse; // Extrahiere den Token und die Argumente const token = message.token; // Token direkt extrahieren const args = message.arguments || {}; // Sichere Extraktion der Argumente const { question, usePublic, groups, language } = args; //const { token, arguments: args } = message; // const { token, question, usePublic, groups, language } = args; // Argument-Validierung if (!args || !args.question) { return { status: 'E20-M-2050', message: 'Fehlende Frage in den Argumenten.', }; } console.log('message Extrahierter Token:', token); //const { question, usePublic, groups, language } = args; // Konflikt zwischen `usePublic` und `groups` lösen if (usePublic && groups && groups.length > 0) { console.warn("⚠️ Konflikt: usePublic wurde auf false gesetzt, da Gruppen angegeben sind."); args.usePublic = false; } try { // API-Aufruf zur Verarbeitung der Chat-Anfrage const response = await this.axiosInstance.post( '/chats', { question, usePublic: usePublic || false, // groups: Array.isArray(groups) ? groups : [groups], groups: Array.isArray(groups) ? groups : groups ? [groups] : [], language: language || 'de', }, { headers: { Authorization: `Bearer ${token}` }, } ); const data = response.data?.data || {}; console.log(t.chatResponseSuccess.replace('${data}', JSON.stringify(data))); // Erfolgsantwort mit Status und Daten return { status: response.data?.status || 'ok', message: response.data?.message || 'Chat erfolgreich.', content: { chatId: data.chatId, answer: data.answer, sources: data.sources || [], }, }; } catch (error) { const chatApiErrorMessage = error.message || error.response?.data; console.error(t.chatApiError.replace('${error}', chatApiErrorMessage)); // Fehlerantwort mit Status und Nachricht return { status: error.response?.data?.status || error.message.status || 'E20-M-2051', message: error.response?.data?.message || error.message || 'no error message', }; } } /* 2.1 Continue Chat ##############################################################################*/ case 'continue_chat': { const disabledResponse = checkToolEnabled('continue_chat'); if (disabledResponse) return disabledResponse; //const { token, arguments: args } = message; //const { token, chatId, question } = args; const token = message.token; // Token direkt extrahieren const args = message.arguments || {}; // Sichere Extraktion der Argumente //const { token } = message; //const args = message.arguments || {}; const { chatId, question } = args; if (!args || !args.chatId || !args.question) { return { status: 'E21-M-2150', message: 'Fehlende erforderliche Parameter: chatId und/oder question.' }; } //const { chatId, question } = args; try { const continueChatResponse = await this.axiosInstance.patch( `/chats/${chatId}`, { question }, { headers: { Authorization: `Bearer ${token}` } } ); console.log(t.conversationSuccess.replace('${data}', JSON.stringify(continueChatResponse.data, null, 2))); return { content: { chatId: continueChatResponse.data.data.chatId, answer: continueChatResponse.data.data.answer, sources: continueChatResponse.data.sources || [], message: continueChatResponse.data.message, status: continueChatResponse.data.status, }, }; } catch (error) { //console.error(`Fehler bei der API-Anfrage: ${error.message}`); console.error(t.apiRequestError.replace('${error}', error.message)); return { status: 'E21-M-2151', message: error.response?.data?.message || error.message || 'no error message', }; } } /* 2.2 Get Chat Info ##############################################################################*/ case 'get_chat_info': { const disabledResponse = checkToolEnabled('get_chat_info'); if (disabledResponse) return disabledResponse; //const { token, chatId } = args; //const { token, arguments: args } = message; //const { chatId } = args; const { token } = message; // Token direkt aus `message` extrahieren const args = message.arguments || {}; // Argumente aus `message` extrahieren const { chatId } = args; // chatId aus den Argumenten extrahieren if (!chatId) { return { status: 'E22-M-2250', message: 'chatId ist erforderlich, um Chat-Informationen abzurufen.' }; } try { const response = await this.axiosInstance.get(`/chats/${chatId}`, { headers: { Authorization: `Bearer ${token}` } }); const chatData = response.data?.data; if (!chatData) { return { status: 'E22-M-2251', message: t.noChatData, }; } // Formatiertes Ergebnis zurückgeben return { data: { chatId: chatData.chatId, title: chatData.title || 'Unbenannter Chat', language: chatData.language || 'Unbekannt', groups: chatData.groups || [], messages: chatData.messages || [] }, message: response.data?.message || 'Erfolgreich abgerufen.' }; } catch (error) { const fetchChatErrorMessage = error.message || error.response?.data; console.error(t.fetchChatInfoError.replace('${error}', fetchChatErrorMessage)); return { status: 'E22-M-2252', message: error.response?.data?.message || error.message || 'no error message' }; } } /* 3.0 Create Source ##############################################################################*/ case 'create_source': { const disabledResponse = checkToolEnabled('create_source'); if (disabledResponse) return disabledResponse; const args = getArguments(message); // const args = request.params.arguments; const token = message.token; // Validierung: Erforderliche Parameter prüfen if (!token) { return { status: 'E30-M-3050', message: t.missingTokenError }; } if (!args || !args.name || !args.content) { return { status: 'E30-M-3051', message: t.missingParametersError.replace('${parameters}', 'name und content') }; } const { name, content, groups } = args; try { // Gruppenvalidierung vorab durchführen if (groups && groups.length > 0) { const groupValidation = await this.validateGroups(groups, token); if (!groupValidation.isValid) { return { status: 'E30-M-3052', message: t.invalidGroupsError.replace('${invalidGroups}', groupValidation.invalidGroups.join(', ')) }; } } // API-Aufruf zur Erstellung der Quelle const createSourceResponse = await this.axiosInstance.post( '/sources', { name, content, groups }, { headers: { Authorization: `Bearer ${token}` } } ); console.log('✔️ Quelle erfolgreich erstellt:', createSourceResponse.data); // Erfolgsantwort return { status: createSourceResponse.data?.status || 'ok', message: createSourceResponse.data?.message || 'Quelle erfolgreich erstellt.', data: createSourceResponse.data?.data, }; } catch (error) { // console.error('❌ Fehler beim Erstellen der Quelle:', error.response?.data || error.message); const createSourceError = error.response?.data || error.message; console.error(t.createSourceError.replace('${error}', createSourceError)); // Fehlerhafte Antwort if (error.response) { return { status: 'E30-M-3053', message: error.response?.data?.message || error.message || 'no error message', details: { status: error.response.status, headers: error.response.headers, data: error.response.data, }, }; } else if (error.request) { return { status: 'E30-M-3054', message: t.noServerResponse, details: { request: error.request }, }; } else { return { status: 'E30-M-3055', message: error.message || 'Ein unbekannter Fehler ist aufgetreten.', }; } } } /* 3.1 Get Source #################################################################################*/ case 'get_source': { const disabledResponse = checkToolEnabled('get_source'); if (disabledResponse) return disabledResponse; const { token, arguments: args } = message; // Extrahiere den Token und die Argumente //const { token, sourceId } = args; const { sourceId } = args; // Extrahiere sourceId aus den Argumenten if (!sourceId) { return { status: 'E31-M-3150', message: t.groupNameRequired.replace('${param}', 'sourceId') }; } console.log(t.makingGetSourceRequest.replace('${sourceId}', sourceId)); try { const sourceResponse = await this.axiosInstance.get(`/sources/${sourceId}`, { headers: { Authorization: `Bearer ${token}`, // Nutze den vom Client bereitgestellten Token }, }); console.log(t.gotGetSourceResponse.replace('${data}', JSON.stringify(sourceResponse.data, null, 2))); return { content: sourceResponse.data, }; } catch (error) { console.error(t.errorHandlingRequest.replace('${error}', error.message || JSON.stringify(error, null, 2))); return { status: 'E31-M-3151', message: t.apiErrorDetails .replace('${status}', error.response?.status || 'E31-M-3151') .replace('${data}', JSON.stringify(error.response?.data || {}, null, 2)), }; } } /* 3.2 List Sources ###############################################################################*/ case 'list_sources': { const disabledResponse = checkToolEnabled('list_sources'); if (disabledResponse) return disabledResponse; const { token, attributes } = message; // Extrahiere den Token und die Attribute //const { token, groupName } = args; if (!attributes || !attributes.groupName) { return { status: 'E32-M-3250', message: 'Gruppenname erforderlich für diese Anfrage.' }; } const groupName = attributes.groupName; // Extrahiere den groupName aus attributes console.log(t.fetchingSources.replace('${groupName}', groupName)); try { // Führe die API-Anfrage aus, um die Quellen für die Gruppe zu erhalten const sourceResponse = await this.axiosInstance.post( '/sources/groups', { groupName }, { headers: { Authorization: `Bearer ${token}`, // Nutze den vom Client bereitgestellten Token }, } ); console.log(t.sourcesRetrieved.replace('${data}', JSON.stringify(sourceResponse.data, null, 2))); return { content: sourceResponse.data, // Sende die Antwort zurück }; } catch (error) { console.error(t.groupValidationError.replace('${error}', error.message || JSON.stringify(error))); return { status: 'E32-M-3251', message: error.response?.data?.message || error.message || 'no error message', }; // status: 'error', //message: t.groupFetchError, } } /* 3.3 Edit Source ################################################################################*/ case 'edit_source': { const disabledResponse = checkToolEnabled('edit_source'); if (disabledResponse) return disabledResponse; const { token, arguments: args } = message; const { sourceId, title, content, groups } = args; // Validierung: Erforderliche Parameter if (!sourceId) { return { data: {}, message: t.missingParameterError.replace('${parameter}', 'sourceId'), status: 'E33-M-3350', // Bad Request }; } // console.log(`Bearbeite Quelle mit ID: ${sourceId}, Titel: ${title || 'unverändert'}`); console.log( t.editSourceLog .replace('${sourceId}', sourceId) .replace('${title}', title || 'unverändert') ); try { // API-Aufruf: Quelle bearbeiten const payload = {}; if (title) payload.title = title; if (content) payload.content = content; if (groups) payload.groups = groups; const editSourceResponse = await this.axiosInstance.patch( `/sources/${sourceId}`, payload, { headers: { Authorization: `Bearer ${token}`, // Nutze den bereitgestellten Token }, } ); console.log(t.editSourceSuccess.replace('${data}', JSON.stringify(editSourceResponse.data, null, 2))); // Erfolgreiche Antwort return { data: editSourceResponse.data?.data || {}, // Optionale Daten aus der API message: editSourceResponse.data?.message || 'Quelle erfolgreich bearbeitet.', status: editSourceResponse.status || 200, // OK }; } catch (error) { const editSourceError = error.message || JSON.stringify(error.response?.data); console.error(t.editSourceError.replace('${error}', editSourceError)); // console.error(`❌ Fehler beim Bearbeiten der Quelle: ${error.message || JSON.stringify(error.response?.data)}`); // Fehlerhafte Antwort return { data: {}, message: error.response?.data?.message || 'Bearbeiten der Quelle fehlgeschlagen. Bitte versuchen Sie es später erneut.', status: error.response?.status || 'E33-M-3351', // Internal Server Error }; } } /* 3.4 Delete Source ##############################################################################*/ case 'delete_source': { const disabledResponse = checkToolEnabled('delete_source'); if (disabledResponse) return disabledResponse; const { token, arguments: args } = message; const { sourceId } = args; if (!sourceId) { return { status: 'E34-M-3450', message: t.groupNameRequired.replace('${param}', 'sourceId') }; } try { const deleteResponse = await this.axiosInstance.delete(`/sources/${sourceId}`, { headers: { Authorization: `Bearer ${token}` }, }); console.log(`Quelle erfolgreich gelöscht: ${JSON.stringify(deleteResponse.data, null, 2)}`); return { content: deleteResponse.data, }; } catch (error) { console.error(t.errorHandlingRequest.replace('${error}', error.message || JSON.stringify(error, null, 2))); return { status: 'E34-M-3451', message: t.apiErrorDetails .replace('${status}', error.response?.status || 'E34-M-3451') .replace('${data}', JSON.stringify(error.response?.data || {}, null, 2)), }; } } /* 4.0 List Groups ################################################################################*/ case 'list_groups': { const disabledResponse = checkToolEnabled('list_groups'); if (disabledResponse) return disabledResponse; //const token = message.arguments?.token || message.token; const token = message.token; // Token direkt aus `message` extrahieren try { await this.ensureAuthenticated(token); const response = await this.axiosInstance.get('/groups'); let assignableGroups = response.data?.data?.assignableGroups || []; const personalGroups = response.data?.data?.personalGroups || []; const message = response.data?.message || 'no_message'; // Fallback für Nachricht const status = response.data?.status || 'E40-M-4050'; // Fallback für Status if (isRestrictedGroupsEnabled) { // console.log('⚠️ RESTRICTED_GROUPS aktiviert. Verfügbare Gruppen werden eingeschränkt.'); console.log(t.restrictedGroupsWarning); assignableGroups = ["NO ACCESS ALLOWED BY THE MCP-SERVER CONFIG"]; // Alle assignableGroups entfernen } return { data: { personalGroups, assignableGroups, message, status, }, }; } catch (error) { const fetchGroupsError = error.message || JSON.stringify(error.response?.data); console.error(t.fetchGroupsError.replace('${error}', fetchGroupsError)); // Detaillierte Fehlerbehandlung if (axios.isAxiosError(error)) { const status = error.response?.status; const serverMessage = error.response?.data?.message || error.message || 'no error message'; return { status: 'E40-M-4051', message: `Fehler beim Abrufen der Gruppen: ${serverMessage} (Status: ${status})`, }; } return { status: 'E40-M-4052', message: 'Ein unbekannter Fehler ist aufgetreten.', }; } } /* 4.1 Store Group ################################################################################*/ case 'store_group': { const disabledResponse = checkToolEnabled('store_group'); if (disabledResponse) return disabledResponse; //const { groupName, description } = args; // Einheitliche Extraktion über getArguments const { groupName, description } = message.arguments; // Extrahiere die Argumente const clientToken = message.token; // Token aus der Anfrage if (!groupName || !description) { return { status: 'E41-M-4150', message: 'Fehlende erforderliche Parameter: groupName und description.' }; } if (!clientToken) { return { status: 'E41-M-4151', message: t.missingTokenError }; } try { // API-Aufruf mit dem vom Client bereitgestellten Token const createGroupResponse = await this.axiosInstance.post( '/groups', { groupName, description }, { headers: { Authorization: `Bearer ${clientToken}` // Nutze nur den Client-Token } } ); console.log(t.createGroupSuccess.replace('${data}', JSON.stringify(createGroupResponse.data))); return { content: createGroupResponse.data, }; } catch (error) { // console.error('❌ Fehler bei der API-Anfrage:', error.response?.data || error.message); const apiError = error.response?.data || error.message; console.error(t.apiRequestError.replace('${error}', apiError)); return { status: 'E41-M-4152', message: error.response?.data?.message || error.message || 'no error message', }; } } /* 4.2 Delete Group ###############################################################################*/ case 'delete_group': { const disabledResponse = checkToolEnabled('delete_group'); if (disabledResponse) return disabledResponse; //const { token, groupName } = args; const { token, arguments: args } = message; // Extrahiere Token und Argumente const { groupName } = args; if (!groupName) { return { status: 'E42-M-4250', message: 'Fehlender erforderlicher Parameter: groupName.' }; } // console.log(`Lösche Gruppe mit Name: ${groupName}`); console.log(t.deleteGroupLog.replace('${groupName}', groupName)); try { // API-Aufruf mit dem Token des Clients const deleteGroupResponse = await this.axiosInstance.delete('/groups', { data: { groupName }, // JSON-Body für DELETE-Request headers: { Authorization: `Bearer ${token}`, // Nutze den vom Client bereitgestellten Token }, }); console.log(t.deleteGroupSuccessLog.replace('${data}', JSON.stringify(deleteGroupResponse.data))); return { data: deleteGroupResponse.data?.data || {}, message: deleteGroupResponse.data?.message || 'success', status: deleteGroupResponse.status || 200, }; } catch (error) { // console.error(`❌ Fehler beim Löschen der Quelle: ${error.message || JSON.stringify(error.response?.data)}`); const deleteSourceError = error.message || JSON.stringify(error.response?.data); console.error(t.deleteSourceError.replace('${error}', deleteSourceError)); // Fehlerhafte Antwort return { data: {}, message: error.response?.data?.message || error.message || 'no error message', status: error.response?.status || 'E42-M-4251', // Internal Server Error oder spezifischer Statuscode }; } } /* 5.0 Store User #################################################################################*/ case 'store_user': { const disabledResponse = checkToolEnabled('store_user'); if (disabledResponse) return disabledResponse; const { token, arguments: args } = message; //const { token, name, email, password } = args; if (!args || !args.name || !args.email || !args.password) { return { status: 'E50-M-5050', message: 'Fehlende erforderliche Parameter: name, email oder password.' }; } // Extrahiere die Argumente aus der Nachricht //const args = getArguments(message); const Pwd = args.password; // Überprüfe, ob die E-Mail und das Passwort vorhanden sind if (!Pwd) { return { status: 'E10-M-1050', message: 'Passwort ist erforderlich.', }; } let password; // Passwort entschlüsseln, falls erforderlich if (typeof PwEncryption !== 'undefined' && PwEncryption) { password = decryptPassword(Pwd); } else { password = Pwd; } try { // Payload für die API-Anfrage const payload = { name: args.name, email: args.email, language: args.language || 'en', timezone: args.timezone || 'Europe/Berlin', password: password, usePublic: args.usePublic || false, groups: args.groups || [], roles: args.roles || [], activateFtp: args.activateFtp || false, ftpPassword: args.ftpPassword || '', }; console.log(t.createUserLog.replace('${payload}', JSON.stringify(payload))); // API-Aufruf const createUserResponse = await this.axiosInstance.post('/users', payload, { headers: { Authorization: `Bearer ${token}` } }); console.log(t.createUserSuccess.replace('${data}', JSON.stringify(createUserResponse.data))); return { content: createUserResponse.data, }; } catch (error) { const createUserError = error.response?.data || error.message; console.error(t.createUserError.replace('${error}', createUserError)); // console.error('❌ Fehler beim Erstellen des Benutzers:', error.response?.data || error.message); const errors = error.response?.data?.errors; if (errors) { const errorMessages = []; for (const [key, messages] of Object.entries(errors)) { messages.forEach(message => errorMessages.push(`${key}: ${message}`)); } return { status: 'E50-M-5051', message: 'Fehler beim Erstellen des Benutzers:', errors: errorMessages }; } return { status: 'E50-M-5052', message: error.response?.data?.message || error.message || 'no error message', }; } } /* 5.1 Edit User ##################################################################################*/ case 'edit_user': { const disabledResponse = checkToolEnabled('edit_user'); if (disabledResponse) return disabledResponse; const { token, arguments: args } = message; const tokenValidation = validateToken(token); if (tokenValidation) return tokenValidation; // Mindestens die E-Mail muss angegeben sein, um den User zu identifizieren if (!args || !args.email) { return { status: 'E51-R-5100', message: 'Die E-Mail des Benutzers ist erforderlich, um den Datensatz zu bearbeiten.' }; } let password = null; if (args.password) { const Pwd = args.password; if (!Pwd) { return { status: 'E51-M-1050', message: 'Passwort ist erforderlich.', }; } if (typeof PwEncryption !== 'undefined' && PwEncryption) { password = decryptPassword(Pwd); } else { password = Pwd; } } try { // Nur Felder senden, die tatsächlich aktualisiert werden sollen const payload = {}; if (args.name) payload.name = args.name; if (args.password) payload.password = password; if (args.language) payload.language = args.language; if (args.timezone) payload.timezone = args.timezone; if (Array.isArray(args.roles)) payload.roles = args.roles; if (Array.isArray(args.groups)) payload.groups = args.groups; if (typeof args.usePublic === 'boolean') payload.usePublic = args.usePublic; // E-Mail ist Pflicht, um den Benutzer auf dem Server zu finden payload.email = args.email; const response = await this.axiosInstance.patch( '/users', payload, { headers: { Authorization: `Bearer ${token}` } } ); console.log(t.editUserSuccess.replace('${data}', JSON.stringify(response.data))); return { status: response.data?.status || 'ok', message: response.data?.message || 'Benutzer erfolgreich bearbeitet.', data: response.data?.data }; } catch (error) { const editUserError = error.response?.data || error.message; console.error(t.editUserError.replace('${error}', editUserError)); // console.error('❌ Fehler beim Bearbeiten des Benutzers:', error.response?.data || error.message); return { status: error.response?.status || 'E51-M-5151', message: error.response?.data?.message || 'Fehler beim Bearbeiten des Benutzers.' }; } /* const { token, arguments: args } = message; //const { token, email, ...updates } = args; const { email, ...updates } = args; if (!email) { return { status: 'E51-M-5150', message: 'Email ist erforderlich, um einen Benutzer zu bearbeiten.' }; } try { const response = await this.axiosInstance.patch( '/users', { email, ...updates }, { headers: { Authorization: `Bearer ${token}` } } ); console.log(t.editUserSuccess.replace('${data}', JSON.stringify(response.data))); return { content: response.data, }; } catch (error) { const editUserError = error.response?.data || error.message; console.error(t.editUserError.replace('${error}', editUserError)); // console.error('❌ Fehler beim Bearbeiten des Benutzers:', error.response?.data || error.message); return { status: 'E51-M-5151', message: error.response?.data?.message || error.message || 'no error message', }; }*/ } /* 5.2 Delete User ################################################################################*/ case 'delete_user': { const disabledResponse = checkToolEnabled('delete_user'); if (disabledResponse) return disabledResponse; const { token, arguments: args } = message; const { email } = args; //const { token, email } = args; if (!email) { return { status: 'E52-M-5250', message: 'Email ist erforderlich, um einen Benutzer zu löschen.' }; } try { const response = await this.axiosInstance.delete( '/users', { data: { email }, headers: { Authorization: `Bearer ${token}` } } ); console.log(t.deleteUserSuccess.replace('${data}', JSON.stringify(response.data))); return { content: response.data, }; } catch (error) { const deleteUserError = error.response?.data || error.message; console.error(t.deleteUserError.replace('${error}', deleteUserError)); // console.error('❌ Fehler beim Löschen des Benutzers:', error.response?.data || error.message); return { status: 'E52-M-5251', message: error.response?.data?.message || error.message || 'no error message', }; } } /* 9.0 Generate Key ###############################################################################*/ case 'keygen': { const disabledResponse = checkToolEnabled('keygen'); if (disabledResponse) return disabledResponse; const { password } = message.arguments; try { // Passwort verschlüsseln const encryptedPassword = getEncryptedKey(password); // Schlüssel zurückgeben return { data: { key: encryptedPassword }, status: 'ok', message: 'Keygen erfolgreich.', }; } catch (error) { console.error('❌ Fehler bei der Keygen-Anfrage:', error.message); return { data: {}, message: error.message || 'Keygen fehlgeschlagen.', status: 'E90-M-1150', }; } } } } catch (err) { console.error(t.tcpServerError, err.message || err); return { status: 'E52-M-5252', message: err.message || t.internalServerError }; } }); } } const server = new PrivateGPTServer(); // Server läuft console.log( messages[lang].serverRunning .replace('${port}', PORT) ); server.run().catch(console.error);