InitalCommit

This commit is contained in:
2026-02-17 16:00:34 +01:00
commit ce89fccdb5
20 changed files with 6296 additions and 0 deletions

11
.env.example Normal file
View File

@@ -0,0 +1,11 @@
# MS SQL Server Configuration
DB_SERVER=localhost
DB_PORT=1433
DB_DATABASE=your_database_name
DB_USER=your_username
DB_PASSWORD=your_password
DB_ENCRYPT=true
DB_TRUST_SERVER_CERTIFICATE=true
# Server Configuration
PORT=3000

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
node_modules/
.env
*.log
.DS_Store

13
Dockerfile Normal file
View File

@@ -0,0 +1,13 @@
FROM node:20-alpine
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
ENV NODE_ENV=production
EXPOSE 3001
CMD ["npm", "start"]

164
MCP_SECURITY.md Normal file
View File

@@ -0,0 +1,164 @@
# MCP Read-Only Security Guide
## Current Security Measures
The `mssql-mcp-readonly.js` file implements multiple layers of read-only enforcement:
### 1. Query Validation (Application Layer)
- ✅ Only SELECT queries allowed
- ✅ Blocks INSERT, UPDATE, DELETE, DROP, CREATE, ALTER, TRUNCATE
- ✅ Blocks EXEC/EXECUTE (prevents stored procedure execution)
- ✅ Blocks SP_/XP_ (prevents system stored procedures)
- ✅ Blocks MERGE, GRANT, REVOKE, DENY
- ✅ Prevents query chaining with semicolons
### 2. Recommended: Database User Permissions (Database Layer)
For **maximum security**, create a read-only database user:
```sql
-- Create a read-only user
CREATE LOGIN mcp_readonly WITH PASSWORD = 'YourSecurePassword123!';
USE Infra;
CREATE USER mcp_readonly FOR LOGIN mcp_readonly;
-- Grant only SELECT permission
GRANT SELECT ON SCHEMA::dbo TO mcp_readonly;
-- Or grant SELECT on specific tables only:
GRANT SELECT ON dbo.SDS_Teile TO mcp_readonly;
GRANT SELECT ON dbo.SDS_Teile_CA TO mcp_readonly;
GRANT SELECT ON dbo.SDS_Teile_CS TO mcp_readonly;
GRANT SELECT ON dbo.SDS_Teile_LC TO mcp_readonly;
GRANT SELECT ON dbo.SDS_Teile_ME TO mcp_readonly;
GRANT SELECT ON dbo.SDS_Teile_MK TO mcp_readonly;
GRANT SELECT ON dbo.SDS_Teile_MS TO mcp_readonly;
GRANT SELECT ON dbo.SDS_Teile_WT TO mcp_readonly;
GRANT SELECT ON dbo.SDS_Teile_EL TO mcp_readonly;
-- Explicitly deny write permissions (extra safety)
DENY INSERT, UPDATE, DELETE, ALTER ON SCHEMA::dbo TO mcp_readonly;
```
### 3. Update MCP Configuration
After creating the read-only user, update `~/.cursor/mcp.json`:
```json
{
"mcpServers": {
"mssql-mcp-readonly": {
"command": "node ",
"args": ["/home/sdsadmin/infraviewer/mssql-mcp-readonly.js"],
"env": {
"MSSQL_SERVER": "192.168.120.88",
"MSSQL_PORT": "1433",
"MSSQL_USER": "mcp_readonly",
"MSSQL_PASSWORD": "YourSecurePassword123!",
"MSSQL_DATABASE": "Infra",
"MSSQL_ENCRYPT": "true",
"MSSQL_TRUST_SERVER_CERTIFICATE": "true"
}
}
}
}
```
## Security Layers
| Layer | Protection | Bypassed if... |
|-------|-----------|----------------|
| **Application Validation** | Keyword blocking | Code has bugs |
| **Database Permissions** | SQL Server enforced | Wrong credentials used |
| **Both Combined** | Double protection | Nearly impossible ✅ |
## Testing Read-Only Access
### Test 1: Valid SELECT (should work)
```sql
SELECT TOP 10 * FROM SDS_Teile
```
### Test 2: Invalid INSERT (should fail)
```sql
INSERT INTO SDS_Teile (Teil) VALUES ('TEST')
```
**Expected:** Error: "Forbidden keyword detected: INSERT"
### Test 3: Query Chaining (should fail)
```sql
SELECT 1; DROP TABLE SDS_Teile
```
**Expected:** Error: "Multiple statements not allowed"
### Test 4: Stored Procedure (should fail)
```sql
EXEC sp_helpdb
```
**Expected:** Error: "Forbidden keyword detected: EXEC"
## Current vs Recommended Setup
### Current Setup (Application Layer Only)
```
[AI] → [mssql-mcp-readonly.js] → [SQL Server]
↑ Validates queries ↑ Full permissions
```
**Risk:** If validation code has a bug, data could be modified
### Recommended Setup (Defense in Depth)
```
[AI] → [mssql-mcp-readonly.js] → [SQL Server with read-only user]
↑ Validates queries ↑ SELECT-only permissions
```
**Benefit:** Even if validation fails, SQL Server will block write operations
## Monitoring
Check MCP server logs for blocked attempts:
```bash
# If running as systemd service
journalctl -u mcp-readonly -f
# If running manually
tail -f /var/log/mcp-readonly.log
```
## Best Practices
1. ✅ Use dedicated read-only database user
2. ✅ Keep validation keywords up to date
3. ✅ Monitor for failed query attempts
4. ✅ Regularly audit database permissions
5. ✅ Use strong passwords for database users
6. ✅ Enable SQL Server audit logging
7. ✅ Limit network access to database server
8. ✅ Keep mssql package updated (`npm update mssql`)
## Additional Hardening
### 1. Add Query Size Limits
Prevent resource exhaustion:
```javascript
function validateQuery(query) {
if (query.length > 10000) {
throw new Error("Query too long!");
}
// ... rest of validation
}
```
### 2. Add Rate Limiting
Prevent abuse:
```javascript
const rateLimit = require('express-rate-limit');
// Limit to 100 queries per minute
```
### 3. Add Query Logging
Track all queries:
```javascript
console.log(`[${new Date().toISOString()}] Query: ${query.substring(0, 100)}...`);
```

311
MCP_TOOLS.md Normal file
View File

@@ -0,0 +1,311 @@
# MCP Tools Documentation
Your MCP server now has **4 specialized tools** for working with the MS SQL Server Infra database.
## 🔧 Available Tools
### 1. `execute_sql`
**Description:** Execute custom read-only SQL queries
**Parameters:**
- `query` (string, required): SQL SELECT query to execute
**Example:**
```javascript
{
"query": "SELECT TOP 10 Teil, Bez, Hersteller FROM TEILE WHERE Hersteller LIKE '%Bosch%'"
}
```
**Use Cases:**
- Complex queries with JOINs
- Aggregations (COUNT, SUM, AVG, etc.)
- Custom filtering and sorting
- Any read-only data analysis
---
### 2. `list_tables`
**Description:** List all available tables in the Infra database
**Parameters:** None
**Example:**
```javascript
{}
```
**Returns:**
```json
[
{ "TABLE_NAME": "TEILE" },
...
]
```
**Use Cases:**
- Discover what tables exist
- Explore database structure
- Find table names for queries
---
### 3. `describe_table`
**Description:** Get detailed schema information for a specific table
**Parameters:**
- `table_name` (string, required): Name of the table to describe
**Example:**
```javascript
{
"table_name": "TEILE"
}
```
**Returns:**
```json
[
{
"COLUMN_NAME": "Teil",
"DATA_TYPE": "nchar",
"CHARACTER_MAXIMUM_LENGTH": 15,
"IS_NULLABLE": "YES",
"COLUMN_DEFAULT": null
},
{
"COLUMN_NAME": "Bez",
"DATA_TYPE": "nchar",
"CHARACTER_MAXIMUM_LENGTH": 160,
"IS_NULLABLE": "YES",
"COLUMN_DEFAULT": null
},
...
]
```
**Use Cases:**
- Understand table structure before querying
- Check column data types
- Verify column names
- See which columns allow NULL values
---
### 4. `search_parts`
**Description:** Quick search across the TEILE table
**Parameters:**
- `search_term` (string, required): Term to search for
- `limit` (number, optional): Max results per table (default: 50)
**Example:**
```javascript
{
"search_term": "Bosch",
"limit": 20
}
```
**Returns:**
```json
[
{
"TableName": "TEILE",
"Teil": "10.1.000040",
"Art": "2",
"Gruppe": "CA",
"Bez": "ITT1 Bosch LST-Motorkabel 15m",
"Bez2": "ITT1 Bosch LST motor cable 15m",
"Hersteller": "SDS",
"WarenNr": null
},
...
]
```
**Searches in:**
- Teil (Part number)
- Bez (Description)
- Bez2 (Description 2)
- Hersteller (Manufacturer)
**Use Cases:**
- Quick part lookup
- Find parts by manufacturer
- Search by description
- Find part numbers
---
## 🔒 Security Features
All tools enforce read-only access:
- ✅ Only SELECT queries allowed
- ✅ Blocks INSERT, UPDATE, DELETE, DROP, etc.
- ✅ Prevents query chaining with semicolons
- ✅ Blocks stored procedure execution
- ✅ SQL injection protection with parameter escaping
---
## 📝 Usage in Cursor/AI
The AI can now use these tools automatically. Examples:
**Question:** "Show me all Bosch parts"
**AI will use:** `search_parts` tool with `{ "search_term": "Bosch" }`
**Question:** "What columns does TEILE have?"
**AI will use:** `describe_table` tool with `{ "table_name": "TEILE" }`
**Question:** "List all tables in the database"
**AI will use:** `list_tables` tool
**Question:** "Find all parts with weight over 1kg"
**AI will use:** `execute_sql` tool with a custom query
---
## 🧪 Testing the MCP Server
### Test Tool Listing
```bash
# Use Cursor's MCP inspector or call the server directly
# The server should list all 4 tools
```
### Test Execute SQL
Query:
```json
{
"name": "execute_sql",
"arguments": {
"query": "SELECT TOP 5 Teil, Bez FROM TEILE"
}
}
```
### Test Search Parts
Query:
```json
{
"name": "search_parts",
"arguments": {
"search_term": "Kabel",
"limit": 10
}
}
```
### Test List Tables
Query:
```json
{
"name": "list_tables",
"arguments": {}
}
```
### Test Describe Table
Query:
```json
{
"name": "describe_table",
"arguments": {
"table_name": "TEILE"
}
}
```
---
> Note: The former import tables `SDS_Teile` and `SDS_Teile_**` are no longer used.
> All queries should now use the productive `TEILE` table instead.
---
## 🔄 Restart MCP Server
After making changes, restart Cursor to reload the MCP server:
1. Close Cursor completely
2. Reopen Cursor
3. The MCP server will start automatically
Or check Cursor's MCP server status in Settings → MCP Servers
---
## 📊 Tool Selection Guide
| Task | Best Tool | Why |
|------|----------|-----|
| Quick part search | `search_parts` | Optimized for common searches |
| Complex filtering | `execute_sql` | Full SQL flexibility |
| Explore database | `list_tables` | See what's available |
| Check columns | `describe_table` | Understand table structure |
| Joins/aggregations | `execute_sql` | Custom query needed |
| Multi-table search | `search_parts` | Searches across tables |
---
## 🎯 Example Workflows
### Workflow 1: Find a Part
1. Use `search_parts` to find parts by keyword
2. Use `describe_table` if you need more column details
3. Use `execute_sql` for precise filtering
### Workflow 2: Explore New Table
1. Use `list_tables` to see all tables
2. Use `describe_table` to understand structure
3. Use `execute_sql` to query data
### Workflow 3: Complex Analysis
1. Use `describe_table` to check available columns
2. Use `execute_sql` with JOINs/aggregations
3. Use `search_parts` for quick validation
---
## 🚨 Error Handling
All tools return helpful error messages:
**Error Example:**
```json
{
"error": "Forbidden keyword detected: DELETE. Only SELECT queries are allowed!"
}
```
**Common Errors:**
- "Only SELECT queries are allowed" → Tried to modify data
- "Multiple statements not allowed" → Query contains semicolon
- "Table not found" → Table name doesn't exist
- "Invalid column name" → Column doesn't exist in table
---
## 📈 Performance Tips
1. **Use LIMIT:** Add `TOP N` to queries for faster results
2. **Specific columns:** Select only needed columns, not `*`
3. **Index-aware:** Filter on Teil (likely indexed) when possible
4. **Use search_parts:** For simple searches, it's optimized
5. **Avoid LIKE %term%:** If possible, use `LIKE 'term%'` (faster)
---
## 🔐 Best Practices
1.**Use describe_table** before complex queries
2.**Start with small limits** (TOP 10) when testing
3.**Validate table names** with list_tables first
4.**Use search_parts** for simple part lookups
5.**Keep queries focused** - one question per query
6.**Don't** try to modify data (will be blocked)
7.**Don't** chain multiple queries with semicolons
8.**Don't** use stored procedures or functions

203
README.md Normal file
View File

@@ -0,0 +1,203 @@
# Infraviewer - Volltextsuche für MS SQL Server Express
Eine leistungsstarke Webanwendung zur Durchführung von Volltextsuchen über mehrere Tabellen in einer MS SQL Server Express Datenbank.
## Features
- 🔍 **Volltextsuche** über mehrere Tabellen gleichzeitig
- 📊 **Übersichtliche Darstellung** der Suchergebnisse
-**Schnelle Performance** mit Connection Pooling
- 🎨 **Moderne Benutzeroberfläche** mit responsivem Design
- 📝 **Einfache Konfiguration** der zu durchsuchenden Tabellen und Spalten
- 🔧 **REST API** für Integrationen
## Installation
### Voraussetzungen
- Node.js (Version 14 oder höher)
- MS SQL Server Express
- npm oder yarn
### Schritte
1. **Abhängigkeiten installieren:**
```bash
npm install
```
2. **Umgebungsvariablen konfigurieren:**
Kopieren Sie `.env.example` zu `.env` und passen Sie die Datenbankverbindung an:
```bash
cp .env.example .env
```
Bearbeiten Sie `.env` mit Ihren Datenbankzugangsdaten:
```env
DB_SERVER=localhost
DB_PORT=1433
DB_DATABASE=IhreDatenbankName
DB_USER=IhrBenutzername
DB_PASSWORD=IhrPasswort
DB_ENCRYPT=true
DB_TRUST_SERVER_CERTIFICATE=true
PORT=3000
```
3. **Suchkonfiguration anpassen:**
Öffnen Sie `services/searchService.js` und passen Sie das `SEARCH_CONFIG` Array an Ihre Tabellen an:
```javascript
const SEARCH_CONFIG = [
{
tableName: 'IhreTabellenName',
columns: ['Spalte1', 'Spalte2', 'Spalte3'],
primaryKey: 'ID'
},
// Weitere Tabellen hinzufügen...
];
```
4. **Server starten:**
```bash
npm start
```
Für Entwicklung mit automatischem Neustart:
```bash
npm run dev
```
5. **Anwendung öffnen:**
Öffnen Sie Ihren Browser und navigieren Sie zu: `http://localhost:3000`
## Verwendung
### Web-Oberfläche
1. Öffnen Sie die Anwendung in Ihrem Browser
2. Geben Sie Ihren Suchbegriff in das Suchfeld ein
3. Klicken Sie auf "Suchen" oder drücken Sie Enter
4. Die Ergebnisse werden übersichtlich nach Tabellen gruppiert angezeigt
### API-Endpunkte
#### Suche durchführen
```
GET /api/search?q=suchbegriff
```
**Antwort:**
```json
{
"success": true,
"searchTerm": "suchbegriff",
"totalMatches": 15,
"tablesSearched": 10,
"results": [
{
"tableName": "Kunden",
"matchCount": 5,
"records": [...]
}
]
}
```
#### Verfügbare Tabellen abrufen
```
GET /api/search/tables
```
#### Spalten einer Tabelle abrufen
```
GET /api/search/tables/:tableName
```
#### Suchkonfiguration anzeigen
```
GET /api/search/config
```
#### Health Check
```
GET /api/health
```
## Konfiguration
### Tabellen hinzufügen/ändern
Bearbeiten Sie `services/searchService.js` und fügen Sie neue Einträge zum `SEARCH_CONFIG` Array hinzu:
```javascript
{
tableName: 'NeueTabelle', // Name der Tabelle in der Datenbank
columns: ['Spalte1', 'Spalte2'], // Spalten, die durchsucht werden sollen
primaryKey: 'ID' // Primärschlüssel der Tabelle
}
```
### Suchverhalten anpassen
Die Suche verwendet standardmäßig das SQL `LIKE` Pattern mit Wildcards (`%suchbegriff%`).
Dies findet den Suchbegriff überall in den konfigurierten Spalten.
Für weitere Anpassungen können Sie die Funktion `buildSearchCondition` in `services/searchService.js` modifizieren.
## Projektstruktur
```
infraviewer/
├── config/
│ └── database.js # Datenbankkonfiguration
├── services/
│ └── searchService.js # Such-Logik
├── routes/
│ └── search.js # API-Routen
├── public/
│ └── index.html # Web-Oberfläche
├── server.js # Express-Server
├── package.json # Abhängigkeiten
├── .env.example # Beispiel-Umgebungsvariablen
└── README.md # Diese Datei
```
## Sicherheitshinweise
- ✅ Verwendet parametrisierte Queries zum Schutz vor SQL-Injection
- ✅ `.env` Datei wird nicht ins Repository commited (.gitignore)
- ✅ Passwörter werden niemals im Code gespeichert
- ⚠️ Stellen Sie sicher, dass der Datenbankbenutzer nur die benötigten Berechtigungen hat (READ-only für Suche)
## Fehlerbehebung
### Verbindungsfehler
- Überprüfen Sie die Datenbankverbindungsparameter in `.env`
- Stellen Sie sicher, dass SQL Server läuft und TCP/IP aktiviert ist
- Prüfen Sie die Firewall-Einstellungen
### Tabellen nicht gefunden
- Überprüfen Sie die Schreibweise der Tabellennamen in `SEARCH_CONFIG`
- Stellen Sie sicher, dass der Datenbankbenutzer Leserechte auf die Tabellen hat
- Verwenden Sie `/api/search/tables` um alle verfügbaren Tabellen anzuzeigen
### Performance-Probleme
- Erstellen Sie Indizes auf häufig durchsuchten Spalten
- Reduzieren Sie die Anzahl der durchsuchten Spalten
- Erwägen Sie die Verwendung von SQL Server Full-Text Search für große Datenmengen
## Lizenz
ISC
## Support
Bei Fragen oder Problemen öffnen Sie bitte ein Issue im Repository.

156
SERVICE_ANLEITUNG.md Normal file
View File

@@ -0,0 +1,156 @@
# Infraviewer als systemd Service installieren
## Automatische Installation (empfohlen)
```bash
cd /home/sdsadmin/infraviewer
./install-service.sh
```
## Manuelle Installation
Falls Sie die Installation Schritt für Schritt durchführen möchten:
### 1. Service-Datei kopieren
```bash
sudo cp /tmp/infraviewer.service /etc/systemd/system/infraviewer.service
```
### 2. Alte Server-Prozesse beenden
```bash
pkill -f "node server.js"
```
### 3. systemd neu laden
```bash
sudo systemctl daemon-reload
```
### 4. Service aktivieren (Autostart beim Booten)
```bash
sudo systemctl enable infraviewer.service
```
### 5. Service starten
```bash
sudo systemctl start infraviewer.service
```
### 6. Status prüfen
```bash
sudo systemctl status infraviewer.service
```
---
## Service-Verwaltung
### Status anzeigen
```bash
sudo systemctl status infraviewer
```
### Service starten
```bash
sudo systemctl start infraviewer
```
### Service stoppen
```bash
sudo systemctl stop infraviewer
```
### Service neu starten
```bash
sudo systemctl restart infraviewer
```
### Autostart aktivieren
```bash
sudo systemctl enable infraviewer
```
### Autostart deaktivieren
```bash
sudo systemctl disable infraviewer
```
### Logs anzeigen
```bash
# Letzte Logs anzeigen
sudo journalctl -u infraviewer -n 50
# Live-Logs verfolgen
sudo journalctl -u infraviewer -f
# Logs seit heute
sudo journalctl -u infraviewer --since today
```
---
## Service-Konfiguration
Die Service-Datei befindet sich unter:
```
/etc/systemd/system/infraviewer.service
```
### Eigenschaften des Service:
-**Automatischer Start** beim Systemstart
-**Automatischer Neustart** bei Absturz (nach 10 Sekunden)
-**Logs** werden ins Systemlog geschrieben
-**Läuft als Benutzer** `sdsadmin`
-**Arbeitsverzeichnis** `/home/sdsadmin/infraviewer`
### Service bearbeiten:
```bash
sudo nano /etc/systemd/system/infraviewer.service
# Nach Änderungen:
sudo systemctl daemon-reload
sudo systemctl restart infraviewer
```
---
## Fehlerbehebung
### Service startet nicht
```bash
# Detaillierte Fehlermeldungen anzeigen
sudo systemctl status infraviewer -l
# Logs prüfen
sudo journalctl -u infraviewer -n 100
```
### Port bereits belegt
```bash
# Prüfen, welcher Prozess Port 3001 verwendet
sudo lsof -i :3001
# Prozess beenden
sudo kill -9 <PID>
```
### Service deinstallieren
```bash
sudo systemctl stop infraviewer
sudo systemctl disable infraviewer
sudo rm /etc/systemd/system/infraviewer.service
sudo systemctl daemon-reload
```
---
## Zugriff auf die Anwendung
Nach erfolgreicher Installation ist die Anwendung erreichbar unter:
**http://localhost:3001**
oder von anderen Rechnern im Netzwerk:
**http://192.168.120.88:3001**

9
Spalten.txt Normal file
View File

@@ -0,0 +1,9 @@
Teil
Bez
Bez2
Ben7 (Englischer Text)
Ben8 (Deutscher Text)
Hersteller
Text -> Text aus der tabelle TEXT durch TextId verknüpft
PrsVK -> Aus Tabelle TSSAEF verknüpft durch Teil

46
config/database.js Normal file
View File

@@ -0,0 +1,46 @@
require('dotenv').config();
const sql = require('mssql');
const config = {
server: process.env.DB_SERVER || 'localhost',
port: parseInt(process.env.DB_PORT) || 1433,
database: process.env.DB_DATABASE,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
options: {
encrypt: process.env.DB_ENCRYPT === 'true',
trustServerCertificate: process.env.DB_TRUST_SERVER_CERTIFICATE === 'true',
enableArithAbort: true,
connectionTimeout: 30000,
requestTimeout: 30000
},
pool: {
max: 10,
min: 0,
idleTimeoutMillis: 30000
}
};
let pool = null;
const getConnection = async () => {
if (!pool) {
pool = await sql.connect(config);
}
return pool;
};
const closeConnection = async () => {
if (pool) {
await pool.close();
pool = null;
}
};
module.exports = {
sql,
getConnection,
closeConnection,
config
};

15
docker-compose.yml Normal file
View File

@@ -0,0 +1,15 @@
services:
infraviewer:
build:
context: .
dockerfile: Dockerfile
container_name: infraviewer-app
restart: unless-stopped
env_file:
- .env
environment:
# Fallback, falls PORT in .env nicht gesetzt ist
- PORT=3001
ports:
- "3001:3001"

66
install-service.sh Executable file
View File

@@ -0,0 +1,66 @@
#!/bin/bash
# Installation des Infraviewer systemd Service
echo "🔧 Installiere Infraviewer als systemd Service..."
# Service-Datei erstellen
cat > /tmp/infraviewer.service << 'EOF'
[Unit]
Description=Infraviewer - Volltextsuche für MS SQL Server
After=network.target
[Service]
Type=simple
User=sdsadmin
WorkingDirectory=/home/sdsadmin/infraviewer
ExecStart=/usr/bin/node /home/sdsadmin/infraviewer/server.js
Restart=always
RestartSec=10
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=infraviewer
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.target
EOF
# Service-Datei kopieren
echo "📋 Kopiere Service-Datei..."
sudo cp /tmp/infraviewer.service /etc/systemd/system/infraviewer.service
# Alte node server.js Prozesse beenden
echo "🛑 Stoppe alte Server-Prozesse..."
pkill -f "node server.js" 2>/dev/null || true
# systemd neu laden
echo "🔄 Lade systemd neu..."
sudo systemctl daemon-reload
# Service aktivieren (Autostart)
echo "✅ Aktiviere Autostart..."
sudo systemctl enable infraviewer.service
# Service starten
echo "🚀 Starte Service..."
sudo systemctl start infraviewer.service
# Status anzeigen
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "✅ Installation abgeschlossen!"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
sudo systemctl status infraviewer.service --no-pager
echo ""
echo "📝 Wichtige Befehle:"
echo " sudo systemctl status infraviewer # Status anzeigen"
echo " sudo systemctl stop infraviewer # Service stoppen"
echo " sudo systemctl start infraviewer # Service starten"
echo " sudo systemctl restart infraviewer # Service neu starten"
echo " sudo systemctl disable infraviewer # Autostart deaktivieren"
echo " sudo journalctl -u infraviewer -f # Live-Logs anzeigen"
echo ""
echo "🌐 Anwendung erreichbar unter: http://localhost:3001"
echo ""

247
mssql-mcp-readonly.js Normal file
View File

@@ -0,0 +1,247 @@
#!/usr/bin/env node
const { Server } = require("@modelcontextprotocol/sdk/server/index.js");
const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
const {
CallToolRequestSchema,
ListToolsRequestSchema,
} = require("@modelcontextprotocol/sdk/types.js");
const sql = require("mssql");
// Read DB config from environment variables
const config = {
server: process.env.MSSQL_SERVER,
port: parseInt(process.env.MSSQL_PORT || "1433"),
user: process.env.MSSQL_USER,
password: process.env.MSSQL_PASSWORD,
database: process.env.MSSQL_DATABASE,
options: {
encrypt: process.env.MSSQL_ENCRYPT === "true",
trustServerCertificate: process.env.MSSQL_TRUST_SERVER_CERTIFICATE === "true"
}
};
// Helper to allow only SELECT queries (strict read-only enforcement)
function validateQuery(query) {
const trimmed = query.trim().toUpperCase();
// Must start with SELECT
if (!trimmed.startsWith("SELECT")) {
throw new Error("Only SELECT queries are allowed!");
}
// Block dangerous keywords that could modify data
const dangerousKeywords = [
'INSERT', 'UPDATE', 'DELETE', 'DROP', 'CREATE', 'ALTER',
'TRUNCATE', 'EXEC', 'EXECUTE', 'SP_', 'XP_', 'MERGE',
'GRANT', 'REVOKE', 'DENY'
];
for (const keyword of dangerousKeywords) {
if (trimmed.includes(keyword)) {
throw new Error(`Forbidden keyword detected: ${keyword}. Only SELECT queries are allowed!`);
}
}
// Block semicolons (prevents query chaining like "SELECT 1; DROP TABLE")
if (query.includes(';')) {
throw new Error("Multiple statements not allowed. Only single SELECT queries permitted!");
}
return query;
}
async function runQuery(query) {
validateQuery(query);
const pool = await sql.connect(config);
const result = await pool.request().query(query);
await pool.close();
return result.recordset;
}
// Create MCP server instance
const server = new Server(
{
name: "mssql-readonly-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "execute_sql",
description: "Execute read-only SQL queries on MS SQL Server. Only SELECT queries are allowed. Use this to fetch data from the Infra database tables such as TEILE.",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "SQL SELECT query to execute. Must start with SELECT. No INSERT, UPDATE, DELETE, or other write operations allowed.",
},
},
required: ["query"],
},
},
{
name: "list_tables",
description: "List all available tables in the Infra database",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "describe_table",
description: "Get the schema/structure of a specific table including column names, data types, and constraints",
inputSchema: {
type: "object",
properties: {
table_name: {
type: "string",
description: "Name of the table to describe (e.g., 'TEILE')",
},
},
required: ["table_name"],
},
},
{
name: "search_parts",
description: "Search for parts in the TEILE table by term. Searches in Teil, Bez, Bez2, Hersteller, and other fields.",
inputSchema: {
type: "object",
properties: {
search_term: {
type: "string",
description: "Term to search for (e.g., manufacturer name, part number, description)",
},
limit: {
type: "number",
description: "Maximum number of results to return (default: 50)",
default: 50,
},
},
required: ["search_term"],
},
},
],
};
});
// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { name, arguments: args } = request.params;
switch (name) {
case "execute_sql": {
const { query } = args;
const rows = await runQuery(query);
return {
content: [
{
type: "text",
text: JSON.stringify(rows, null, 2),
},
],
};
}
case "list_tables": {
const listQuery = `
SELECT TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_TYPE = 'BASE TABLE'
ORDER BY TABLE_NAME
`;
const rows = await runQuery(listQuery);
return {
content: [
{
type: "text",
text: JSON.stringify(rows, null, 2),
},
],
};
}
case "describe_table": {
const { table_name } = args;
const describeQuery = `
SELECT
COLUMN_NAME,
DATA_TYPE,
CHARACTER_MAXIMUM_LENGTH,
IS_NULLABLE,
COLUMN_DEFAULT
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = '${table_name.replace(/'/g, "''")}'
ORDER BY ORDINAL_POSITION
`;
const rows = await runQuery(describeQuery);
return {
content: [
{
type: "text",
text: JSON.stringify(rows, null, 2),
},
],
};
}
case "search_parts": {
const { search_term, limit = 50 } = args;
const searchQuery = `
SELECT TOP ${parseInt(limit)}
'TEILE' AS TableName, Teil, Art, Gruppe, Bez, Bez2, Hersteller, WarenNr
FROM TEILE
WHERE
LOWER(CAST(Teil AS NVARCHAR(MAX))) LIKE LOWER('%${search_term.replace(/'/g, "''")}%')
OR LOWER(CAST(Bez AS NVARCHAR(MAX))) LIKE LOWER('%${search_term.replace(/'/g, "''")}%')
OR LOWER(CAST(Bez2 AS NVARCHAR(MAX))) LIKE LOWER('%${search_term.replace(/'/g, "''")}%')
OR LOWER(CAST(Hersteller AS NVARCHAR(MAX))) LIKE LOWER('%${search_term.replace(/'/g, "''")}%')
`;
const rows = await runQuery(searchQuery);
return {
content: [
{
type: "text",
text: JSON.stringify(rows, null, 2),
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${error.message}`,
},
],
isError: true,
};
}
});
// Start the server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MS SQL Read-only MCP Server running on stdio");
}
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});

4009
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

28
package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "infraviewer",
"version": "1.0.0",
"description": "Full-text search application for MS SQL Server Express database",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"keywords": [
"fulltext",
"search",
"mssql",
"express"
],
"author": "",
"license": "ISC",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.18.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"mssql": "^10.0.1"
},
"devDependencies": {
"nodemon": "^3.0.1"
}
}

446
public/dev.html Normal file
View File

@@ -0,0 +1,446 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Infraviewer - Volltextsuche</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
text-align: center;
color: white;
margin-bottom: 40px;
}
.header h1 {
font-size: 2.5rem;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
}
.header p {
font-size: 1.1rem;
opacity: 0.9;
}
.search-card {
background: white;
border-radius: 12px;
padding: 30px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
margin-bottom: 30px;
}
.search-box {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.search-input {
flex: 1;
padding: 15px 20px;
font-size: 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
outline: none;
transition: border-color 0.3s;
}
.search-input:focus {
border-color: #667eea;
}
.search-button {
padding: 15px 40px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.search-button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
.search-button:active {
transform: translateY(0);
}
.search-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.stats {
display: flex;
gap: 20px;
padding: 15px;
background: #f5f5f5;
border-radius: 8px;
margin-bottom: 20px;
}
.stat-item {
flex: 1;
text-align: center;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: #667eea;
}
.stat-label {
font-size: 0.9rem;
color: #666;
margin-top: 5px;
}
.results {
margin-top: 30px;
}
.table-result {
background: white;
border-radius: 8px;
margin-bottom: 20px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.table-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.table-name {
font-size: 1.2rem;
font-weight: 600;
}
.table-count {
background: rgba(255,255,255,0.2);
padding: 5px 15px;
border-radius: 20px;
font-size: 0.9rem;
}
.records {
padding: 20px;
}
.record {
background: #f9f9f9;
padding: 15px;
margin-bottom: 10px;
border-radius: 6px;
border-left: 4px solid #667eea;
}
.record-field {
display: flex;
padding: 5px 0;
}
.field-name {
font-weight: 600;
color: #666;
min-width: 150px;
}
.field-value {
color: #333;
word-break: break-word;
}
.highlight {
background-color: #ffeb3b;
padding: 2px 4px;
border-radius: 3px;
}
.loading {
text-align: center;
padding: 40px;
color: #666;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error {
background: #ffebee;
color: #c62828;
padding: 15px;
border-radius: 8px;
margin-top: 20px;
}
.no-results {
text-align: center;
padding: 40px;
color: #666;
}
.config-button {
padding: 10px 20px;
background: white;
color: #667eea;
border: 2px solid white;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
margin-left: 10px;
}
.config-button:hover {
background: transparent;
color: white;
}
@media (max-width: 768px) {
.search-box {
flex-direction: column;
}
.stats {
flex-direction: column;
}
.header h1 {
font-size: 1.8rem;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔍 Infraviewer</h1>
<p>Volltextsuche über MS SQL Server Express Datenbank</p>
</div>
<div class="search-card">
<div class="search-box">
<input
type="text"
id="searchInput"
class="search-input"
placeholder="Suchbegriff eingeben..."
autofocus
>
<button id="searchButton" class="search-button">Suchen</button>
<button id="configButton" class="config-button" style="display: none;">Konfiguration</button>
</div>
<div id="stats" class="stats" style="display: none;">
<div class="stat-item">
<div class="stat-value" id="totalMatches">0</div>
<div class="stat-label">Treffer</div>
</div>
<div class="stat-item">
<div class="stat-value" id="tablesSearched">0</div>
<div class="stat-label">Tabellen durchsucht</div>
</div>
<div class="stat-item">
<div class="stat-value" id="searchTime">0ms</div>
<div class="stat-label">Suchzeit</div>
</div>
</div>
</div>
<div id="results" class="results"></div>
</div>
<script>
const searchInput = document.getElementById('searchInput');
const searchButton = document.getElementById('searchButton');
const resultsDiv = document.getElementById('results');
const statsDiv = document.getElementById('stats');
let currentSearchTerm = '';
// Enter-Taste zum Suchen
searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
performSearch();
}
});
searchButton.addEventListener('click', performSearch);
async function performSearch() {
const searchTerm = searchInput.value.trim();
if (!searchTerm) {
alert('Bitte geben Sie einen Suchbegriff ein');
return;
}
currentSearchTerm = searchTerm;
searchButton.disabled = true;
searchButton.textContent = 'Suche läuft...';
resultsDiv.innerHTML = '<div class="loading"><div class="spinner"></div><p>Datenbank wird durchsucht...</p></div>';
const startTime = performance.now();
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(searchTerm)}`);
const data = await response.json();
const endTime = performance.now();
const searchTime = Math.round(endTime - startTime);
if (data.success) {
displayResults(data, searchTime);
} else {
displayError(data.error);
}
} catch (error) {
displayError('Verbindungsfehler: ' + error.message);
} finally {
searchButton.disabled = false;
searchButton.textContent = 'Suchen';
}
}
function displayResults(data, searchTime) {
// Statistiken anzeigen
statsDiv.style.display = 'flex';
document.getElementById('totalMatches').textContent = data.totalMatches;
document.getElementById('tablesSearched').textContent = data.tablesSearched;
document.getElementById('searchTime').textContent = searchTime + 'ms';
// Ergebnisse anzeigen
if (data.totalMatches === 0) {
resultsDiv.innerHTML = `
<div class="no-results">
<h2>Keine Ergebnisse gefunden</h2>
<p>Der Suchbegriff "${escapeHtml(data.searchTerm)}" wurde in keiner Tabelle gefunden.</p>
</div>
`;
return;
}
let html = '';
data.results.forEach(tableResult => {
if (tableResult.matchCount > 0) {
html += `
<div class="table-result">
<div class="table-header">
<span class="table-name">${escapeHtml(tableResult.tableName)}</span>
<span class="table-count">${tableResult.matchCount} Treffer</span>
</div>
<div class="records">
${tableResult.records.map(record => renderRecord(record)).join('')}
</div>
</div>
`;
}
});
resultsDiv.innerHTML = html;
}
function renderRecord(record) {
let html = '<div class="record">';
for (const [key, value] of Object.entries(record)) {
const displayValue = value === null ? '<em>NULL</em>' : highlightSearchTerm(String(value));
html += `
<div class="record-field">
<span class="field-name">${escapeHtml(key)}:</span>
<span class="field-value">${displayValue}</span>
</div>
`;
}
html += '</div>';
return html;
}
function highlightSearchTerm(text) {
if (!currentSearchTerm) return escapeHtml(text);
const escapedText = escapeHtml(text);
const searchRegex = new RegExp(`(${escapeRegex(currentSearchTerm)})`, 'gi');
return escapedText.replace(searchRegex, '<span class="highlight">$1</span>');
}
function displayError(errorMessage) {
statsDiv.style.display = 'none';
resultsDiv.innerHTML = `
<div class="error">
<strong>Fehler:</strong> ${escapeHtml(errorMessage)}
</div>
`;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function escapeRegex(text) {
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// Health Check beim Laden
window.addEventListener('load', async () => {
try {
const response = await fetch('/api/health');
const data = await response.json();
if (data.status !== 'ok') {
displayError('Datenbankverbindung fehlgeschlagen. Bitte überprüfen Sie die Konfiguration.');
}
} catch (error) {
console.error('Health check failed:', error);
}
});
</script>
</body>
</html>

136
public/index.html Normal file
View File

@@ -0,0 +1,136 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Infraviewer - Wartung</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.maintenance-container {
background: white;
border-radius: 16px;
padding: 60px 40px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
text-align: center;
max-width: 600px;
width: 100%;
}
.maintenance-icon {
font-size: 5rem;
margin-bottom: 30px;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.1);
opacity: 0.8;
}
}
h1 {
color: #333;
font-size: 2.5rem;
margin-bottom: 20px;
font-weight: 700;
}
.subtitle {
color: #666;
font-size: 1.2rem;
margin-bottom: 30px;
line-height: 1.6;
}
.info-box {
background: #f5f5f5;
border-left: 4px solid #667eea;
padding: 20px;
border-radius: 8px;
margin: 30px 0;
text-align: left;
}
.info-box p {
color: #555;
margin-bottom: 10px;
}
.info-box p:last-child {
margin-bottom: 0;
}
.dev-link {
display: inline-block;
margin-top: 30px;
padding: 15px 30px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
transition: transform 0.2s, box-shadow 0.2s;
}
.dev-link:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
@media (max-width: 768px) {
.maintenance-container {
padding: 40px 30px;
}
h1 {
font-size: 2rem;
}
.maintenance-icon {
font-size: 4rem;
}
}
</style>
</head>
<body>
<div class="maintenance-container">
<div class="maintenance-icon">🔧</div>
<h1>Grade unter Wartung</h1>
<p class="subtitle">
Die Anwendung wird derzeit gewartet und ist vorübergehend nicht verfügbar.
</p>
<div class="info-box">
<p><strong>Was passiert gerade?</strong></p>
<p>Wir arbeiten an Verbesserungen und Updates für die Infraviewer-Anwendung.</p>
</div>
<div class="info-box">
<p><strong>Wann ist die Anwendung wieder verfügbar?</strong></p>
<p>Die Wartungsarbeiten werden voraussichtlich in Kürze abgeschlossen sein.</p>
</div>
<a href="/dev.html" class="dev-link">Entwicklungsversion öffnen</a>
</div>
</body>
</html>

116
routes/search.js Normal file
View File

@@ -0,0 +1,116 @@
const express = require('express');
const router = express.Router();
const { fullTextSearch, getAvailableTables, getTableColumns, SEARCH_CONFIG } = require('../services/searchService');
/**
* GET /api/search?q=suchbegriff
* Führt eine Volltextsuche durch
*/
router.get('/', async (req, res) => {
try {
const searchTerm = req.query.q;
if (!searchTerm) {
return res.status(400).json({
success: false,
error: 'Suchbegriff fehlt. Verwenden Sie ?q=IhrSuchbegriff'
});
}
const results = await fullTextSearch(searchTerm);
const totalMatches = results.reduce((sum, table) => sum + table.matchCount, 0);
const tableErrors = results.filter(table => table && table.error);
// Wenn es Konfigurations-/SQL-Fehler gibt und gleichzeitig keine Treffer gefunden wurden,
// deutet das sehr wahrscheinlich auf ein Problem mit der Suchkonfiguration hin
if (totalMatches === 0 && tableErrors.length > 0) {
return res.status(500).json({
success: false,
searchTerm: searchTerm,
totalMatches: totalMatches,
tablesSearched: results.length,
results: results,
error: 'Fehler bei der Suche in einer oder mehreren Tabellen. Bitte prüfen Sie die Suchkonfiguration (Spaltennamen, Tabellen).',
tableErrors: tableErrors
});
}
res.json({
success: true,
searchTerm: searchTerm,
totalMatches: totalMatches,
tablesSearched: results.length,
results: results,
tableErrors: tableErrors
});
} catch (error) {
console.error('Suchfehler:', error);
res.status(500).json({
success: false,
error: error.message
});
}
});
/**
* GET /api/search/tables
* Gibt alle verfügbaren Tabellen zurück
*/
router.get('/tables', async (req, res) => {
try {
const tables = await getAvailableTables();
res.json({
success: true,
tables: tables,
configured: SEARCH_CONFIG.map(t => t.tableName)
});
} catch (error) {
console.error('Fehler beim Abrufen der Tabellen:', error);
res.status(500).json({
success: false,
error: error.message
});
}
});
/**
* GET /api/search/tables/:tableName
* Gibt die Spalten einer Tabelle zurück
*/
router.get('/tables/:tableName', async (req, res) => {
try {
const tableName = req.params.tableName;
const columns = await getTableColumns(tableName);
res.json({
success: true,
tableName: tableName,
columns: columns
});
} catch (error) {
console.error('Fehler beim Abrufen der Spalten:', error);
res.status(500).json({
success: false,
error: error.message
});
}
});
/**
* GET /api/search/config
* Gibt die aktuelle Suchkonfiguration zurück
*/
router.get('/config', (req, res) => {
res.json({
success: true,
config: SEARCH_CONFIG
});
});
module.exports = router;

84
server.js Normal file
View File

@@ -0,0 +1,84 @@
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const path = require('path');
const { getConnection, closeConnection } = require('./config/database');
const searchRoutes = require('./routes/search');
const app = express();
// Standardmäßig läuft der Entwicklungsserver lokal auf 3002.
// Im Docker-Container wird der Port über die Umgebungsvariable PORT (z.B. 3001) gesetzt.
const PORT = process.env.PORT || 3002;
// Middleware
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Static files
app.use(express.static(path.join(__dirname, 'public')));
// API Routes
app.use('/api/search', searchRoutes);
// Health check endpoint
app.get('/api/health', async (req, res) => {
try {
await getConnection();
res.json({
status: 'ok',
message: 'Server und Datenbankverbindung funktionieren',
timestamp: new Date().toISOString()
});
} catch (error) {
res.status(500).json({
status: 'error',
message: 'Datenbankverbindung fehlgeschlagen',
error: error.message
});
}
});
// Root route
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
// Error handling middleware
app.use((err, req, res, next) => {
console.error('Server Error:', err);
res.status(500).json({
success: false,
error: 'Interner Serverfehler',
message: err.message
});
});
// Graceful shutdown
process.on('SIGINT', async () => {
console.log('\nServer wird heruntergefahren...');
await closeConnection();
process.exit(0);
});
process.on('SIGTERM', async () => {
console.log('\nServer wird heruntergefahren...');
await closeConnection();
process.exit(0);
});
// Start server
app.listen(PORT, async () => {
console.log(`\n===========================================`);
console.log(`🚀 Server läuft auf http://localhost:${PORT}`);
console.log(`===========================================\n`);
try {
await getConnection();
console.log('✅ Datenbankverbindung erfolgreich hergestellt\n');
} catch (error) {
console.error('❌ Datenbankverbindung fehlgeschlagen:', error.message);
console.error(' Bitte überprüfen Sie Ihre .env Konfiguration\n');
}
});

182
services/searchService.js Normal file
View File

@@ -0,0 +1,182 @@
const { sql, getConnection } = require('../config/database');
/**
* Definieren Sie hier die Tabellen und Spalten, die durchsucht werden sollen
*
* Format:
* {
* tableName: 'TabellenName',
* columns: ['Spalte1', 'Spalte2', 'Spalte3'],
* primaryKey: 'ID' // Primärschlüssel der Tabelle
* }
*/
// Gemeinsame Spalten für die TEILE Tabelle
// WICHTIG:
// - Die Teilenummer steht in der Spalte `Teil`
// - Es gibt KEINE Spalte `WarenNr` in TEILE, daher darf diese hier NICHT verwendet werden
// - Spaltennamen müssen exakt den vorhandenen Spalten in TEILE entsprechen,
// sonst schlägt die Suche mit "invalid column name" fehl.
const COMMON_COLUMNS = [
'Teil', // Teilenummer
'Art', // Art
'Gruppe', // Gruppe
'Bez', // Bezeichnung
'Bez2', // Bezeichnung 2
'Hersteller' // Hersteller
// Weitere Felder (z.B. SuchL, SuchR, EUWarenNr, Ben-Felder) können bei Bedarf ergänzt werden
];
// Erweiterte Spalten (falls benötigt) für spezielle Varianten mit zusätzlichen Feldern
// Basieren auf den gemeinsamen Spalten der TEILE Tabelle
const COLUMNS_EL = [
...COMMON_COLUMNS,
'AUF', // Auftragsfeld
'AUF1', // Auftragsfeld 1
'AUF2', // Auftragsfeld 2
'AUF3' // Auftragsfeld 3
];
const SEARCH_CONFIG = [
{
tableName: 'TEILE',
columns: COMMON_COLUMNS,
primaryKey: 'ISN' // technisch primärer Schlüssel der Tabelle
}
];
/**
* Erstellt eine SQL WHERE-Klausel für die Volltextsuche
* Findet Teilübereinstimmungen in allen Spalten (case-insensitive)
* Optimiert durch:
* - Verwendung von COLLATE für case-insensitive Vergleich (schneller als LOWER)
* - Effiziente CAST-Größen statt NVARCHAR(MAX)
* - Vermeidung von redundanten Funktionsaufrufen
* @param {Array} columns - Array von Spaltennamen
* @param {string} searchTerm - Suchbegriff
* @returns {string} WHERE-Klausel
*/
const buildSearchCondition = (columns, searchTerm) => {
return columns
.map(col => `[${col}] LIKE @searchPattern COLLATE SQL_Latin1_General_CP1_CI_AS`)
.join(' OR ');
};
/**
* Führt eine Volltextsuche über alle konfigurierten Tabellen durch
* Findet alle Datensätze, die den Suchbegriff als Teilstring enthalten (case-insensitive)
*
* Optimierungen:
* - Parallele Suche über alle Tabellen (Promise.all statt sequentiell)
* - Effiziente WHERE-Klauseln ohne CAST zu NVARCHAR(MAX)
* - Case-insensitive Suche via COLLATE statt LOWER()
* - Optimierte Query-Struktur mit TOP für bessere Performance
*
* @param {string} searchTerm - Der zu suchende Begriff
* @returns {Promise<Array>} Array mit Suchergebnissen
*/
const fullTextSearch = async (searchTerm) => {
if (!searchTerm || searchTerm.trim() === '') {
throw new Error('Suchbegriff darf nicht leer sein');
}
const pool = await getConnection();
// Pattern für Teilübereinstimmungen: findet den Begriff überall im Text
// % = beliebige Zeichen davor/dahinter (SQL Wildcard)
const searchPattern = `%${searchTerm.trim()}%`;
// OPTIMIERUNG: Parallele Suche über alle Tabellen statt sequentiell
const searchPromises = SEARCH_CONFIG.map(async (tableConfig) => {
try {
const whereClause = buildSearchCondition(tableConfig.columns, searchTerm);
// Optimierte Query:
// - Verwendet COLLATE für case-insensitive Vergleich (schneller als LOWER)
// - TOP 100 limitiert Ergebnisse früh im Execution Plan
// - WITH (NOLOCK) für Read-Uncommitted (schneller, da keine Locks)
const query = `
SELECT TOP 100 *
FROM [${tableConfig.tableName}] WITH (NOLOCK)
WHERE ${whereClause}
`;
const request = pool.request();
request.input('searchPattern', sql.NVarChar, searchPattern);
const result = await request.query(query);
if (result.recordset && result.recordset.length > 0) {
return {
tableName: tableConfig.tableName,
matchCount: result.recordset.length,
records: result.recordset
};
}
return null; // Keine Ergebnisse gefunden
} catch (error) {
console.error(`Fehler beim Durchsuchen der Tabelle ${tableConfig.tableName}:`, error.message);
// Tabelle existiert möglicherweise nicht - Fehler zurückgeben
return {
tableName: tableConfig.tableName,
matchCount: 0,
records: [],
error: error.message
};
}
});
// Warte auf alle Suchanfragen parallel
const allResults = await Promise.all(searchPromises);
// Filtere null-Werte (Tabellen ohne Ergebnisse) heraus
const results = allResults.filter(result => result !== null);
return results;
};
/**
* Holt alle verfügbaren Tabellen aus der Datenbank
* @returns {Promise<Array>} Array mit Tabellennamen
*/
const getAvailableTables = async () => {
const pool = await getConnection();
const query = `
SELECT TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_TYPE = 'BASE TABLE'
ORDER BY TABLE_NAME
`;
const result = await pool.request().query(query);
return result.recordset.map(row => row.TABLE_NAME);
};
/**
* Holt die Spalten einer spezifischen Tabelle
* @param {string} tableName - Name der Tabelle
* @returns {Promise<Array>} Array mit Spalteninformationen
*/
const getTableColumns = async (tableName) => {
const pool = await getConnection();
const query = `
SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = @tableName
ORDER BY ORDINAL_POSITION
`;
const request = pool.request();
request.input('tableName', sql.NVarChar, tableName);
const result = await request.query(query);
return result.recordset;
};
module.exports = {
fullTextSearch,
getAvailableTables,
getTableColumns,
SEARCH_CONFIG
};

49
test-mcp.sh Executable file
View File

@@ -0,0 +1,49 @@
#!/bin/bash
# Test MCP Server
echo "🧪 Testing MCP Server Configuration..."
echo ""
# Set environment variables
export MSSQL_SERVER="192.168.120.88"
export MSSQL_PORT="1433"
export MSSQL_USER="reptil1990"
export MSSQL_PASSWORD="!Delfine1!!!"
export MSSQL_DATABASE="Infra"
export MSSQL_ENCRYPT="true"
export MSSQL_TRUST_SERVER_CERTIFICATE="true"
echo "✅ Environment variables set"
echo ""
# Check syntax
echo "📝 Checking JavaScript syntax..."
node -c mssql-mcp-readonly.js
if [ $? -eq 0 ]; then
echo "✅ Syntax check passed"
else
echo "❌ Syntax errors found"
exit 1
fi
echo ""
echo "📦 Checking dependencies..."
npm list @modelcontextprotocol/sdk mssql 2>/dev/null | grep -E "@modelcontextprotocol|mssql"
echo ""
echo "✅ MCP Server is ready!"
echo ""
echo "📋 Available tools:"
echo " 1. execute_sql - Execute custom SELECT queries"
echo " 2. list_tables - List all database tables"
echo " 3. describe_table - Get table schema"
echo " 4. search_parts - Quick part search"
echo ""
echo "🔄 To use in Cursor:"
echo " 1. Restart Cursor completely"
echo " 2. MCP server will start automatically"
echo " 3. Ask AI to use the tools"
echo ""
echo "📖 Full documentation: MCP_TOOLS.md"