InitalCommit
This commit is contained in:
11
.env.example
Normal file
11
.env.example
Normal 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
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
.env
|
||||
*.log
|
||||
.DS_Store
|
||||
|
||||
13
Dockerfile
Normal file
13
Dockerfile
Normal 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
164
MCP_SECURITY.md
Normal 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
311
MCP_TOOLS.md
Normal 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
203
README.md
Normal 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
156
SERVICE_ANLEITUNG.md
Normal 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
9
Spalten.txt
Normal 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
46
config/database.js
Normal 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
15
docker-compose.yml
Normal 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
66
install-service.sh
Executable 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
247
mssql-mcp-readonly.js
Normal 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
4009
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
package.json
Normal file
28
package.json
Normal 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
446
public/dev.html
Normal 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
136
public/index.html
Normal 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
116
routes/search.js
Normal 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
84
server.js
Normal 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
182
services/searchService.js
Normal 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
49
test-mcp.sh
Executable 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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user