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