bel-crm-db
Uses the mcp postgresql to read and write crm relevant data to the crm database: Its about: sales_opportunity (Verkaufschancen) person (Kontaktperson im Unternehmen) company_site (Ein Standort eines Unternehmens) event (Aktivität, TODO, ... insgesamt bilden die Aktivitäten die Historie und die Zukunft von company_site, person und sales_opportunity ab) data_files (Dateien: PDFs, Bilder, E-Mail-Anhänge, Office-Dokumente - verknüpft mit CRM-Entitäten)
When & Why to Use This Skill
This Claude skill provides a comprehensive interface for managing a PostgreSQL-based CRM system. It enables seamless reading and writing of core business entities including sales opportunities, contact persons, company sites, and activity logs. By integrating advanced document handling and email attachment processing, it allows users to maintain a centralized, searchable database of all customer interactions and sales pipeline data.
Use Cases
- Sales Pipeline Management: Automate the creation and updating of sales opportunities, tracking deal values, win probabilities, and next steps to optimize the sales funnel.
- Contact and Account Organization: Manage detailed profiles for companies and individual contacts, including industry data, social media links, and hierarchical relationships between sites.
- Activity and History Tracking: Log meetings, calls, and tasks (events) to build a complete chronological history of interactions with any person or company.
- Document and Email Archiving: Store and retrieve business-critical files like PDFs and email attachments directly within the CRM, utilizing full-text search on extracted content for quick information retrieval.
- Data-Driven CRM Maintenance: Perform complex database operations while adhering to specific SQL constraints to ensure data integrity and prevent duplicate entries.
| name | bel-crm-db |
|---|---|
| description | " Uses the mcp postgresql to read and write crm relevant data to the crm database: |
| data_files (Dateien | PDFs, Bilder, E-Mail-Anhänge, Office-Dokumente - verknüpft mit CRM-Entitäten)" |
CRITICAL: SQL Limitations - READ FIRST!
Before writing ANY SQL, know these PostgreSQL MCP server limitations:
| FORBIDDEN | WHY | USE INSTEAD |
|---|---|---|
RETURNING |
Syntax error | Query ID separately with read_query |
ON CONFLICT |
Not supported | Check existence first, then INSERT or UPDATE |
| Multiple statements | Not allowed | One statement per tool call |
Example - CORRECT pattern for INSERT:
-- Step 1: write_query (NO RETURNING!)
INSERT INTO company_site (name, created_at, updated_at)
VALUES ('Acme GmbH', now(), now());
-- Step 2: read_query (get ID if needed)
SELECT id FROM company_site WHERE name = 'Acme GmbH' ORDER BY created_at DESC LIMIT 1;
For complete SQL rules, see the bel-crm-sql-rules skill.
You will probably only use those tools from postgresql mcp server:
- mcp__postgresql__read_query,
- mcp__postgresql__write_query
This is the SCHEMA you will work on:
Hier ist das vollständige Datenbankschema:
📊 Datenbank-Schema Übersicht
Die Datenbank enthält 6 Tabellen für ein CRM-System:
- adressen (Legacy-Adresstabelle)
Persönliche Kontaktdaten (deutschsprachig):
| Spalte | Typ | Beschreibung |
|---|---|---|
| id | INTEGER | 🔑 Primary Key |
| vorname | VARCHAR | Vorname |
| nachname | VARCHAR | Nachname |
| strasse_hausnummer | VARCHAR | Straße & Hausnummer |
| plz | VARCHAR | Postleitzahl |
| ort | VARCHAR | Ort |
| VARCHAR | ||
| mobil | VARCHAR | Mobilnummer |
| tel | VARCHAR | Telefon |
| erstellt_am | TIMESTAMP | Erstellungsdatum |
| aktualisiert_am | TIMESTAMP | Aktualisierungsdatum |
- company_site (Unternehmensstandorte)
Firmeninformationen und Standorte:
| Spalte | Typ | Beschreibung |
|---|---|---|
| id | INTEGER | 🔑 Primary Key |
| name | VARCHAR | ⚠️ NOT NULL - Firmenname |
| address_street | VARCHAR | Straße |
| address_city | VARCHAR | Stadt |
| address_state | VARCHAR | Bundesland |
| address_postal_code | VARCHAR | PLZ |
| address_country | VARCHAR | Land |
| industry | VARCHAR | Branche |
| website | VARCHAR | Website |
| linkedin_company_url | VARCHAR | LinkedIn-Profil |
| company_size | VARCHAR | Unternehmensgröße |
| annual_revenue | BIGINT | Jahresumsatz |
| notes | TEXT | Notizen |
| tags | JSONB | Tags (JSON) |
| created_at | TIMESTAMP | Erstellt am |
| updated_at | TIMESTAMP | Aktualisiert am |
- person (Kontaktpersonen)
Ansprechpartner in Unternehmen:
| Spalte | Typ | Beschreibung |
|---|---|---|
| id | INTEGER | 🔑 Primary Key |
| name | VARCHAR | ⚠️ NOT NULL - Name |
| VARCHAR | ||
| phone | VARCHAR | Telefon |
| linkedin_url | VARCHAR | LinkedIn-Profil |
| company_site_id | INTEGER | 🔗 FK → company_site |
| job_title | VARCHAR | Jobtitel |
| department | VARCHAR | Abteilung |
| notes | TEXT | Notizen |
| tags | JSONB | Tags (JSON) |
| created_at | TIMESTAMP | Erstellt am |
| updated_at | TIMESTAMP | Aktualisiert am |
- sales_opportunity (Verkaufschancen)
Sales-Pipeline und Opportunities:
| Spalte | Typ | Beschreibung |
|---|---|---|
| id | INTEGER | 🔑 Primary Key |
| title | VARCHAR | ⚠️ NOT NULL - Titel |
| value_eur | NUMERIC | Wert in EUR |
| probability | INTEGER | Wahrscheinlichkeit (%) |
| status | VARCHAR | Status (default: 'open') |
| description | TEXT | Beschreibung |
| expected_close_date | DATE | Erwarteter Abschluss |
| actual_close_date | DATE | Tatsächlicher Abschluss |
| person_id | INTEGER | 🔗 FK → person |
| company_site_id | INTEGER | 🔗 FK → company_site |
| source | VARCHAR | Quelle |
| competitors | TEXT | Wettbewerber |
| next_steps | TEXT | Nächste Schritte |
| notes | TEXT | Notizen |
| tags | JSONB | Tags (JSON) |
| created_at | TIMESTAMP | Erstellt am |
| updated_at | TIMESTAMP | Aktualisiert am |
- event (Aktivitäten/Events)
Aktivitätsverlauf (Meetings, Calls, etc.):
| Spalte | Typ | Beschreibung |
|---|---|---|
| id | INTEGER | 🔑 Primary Key |
| type | VARCHAR | ⚠️ NOT NULL - Event-Typ |
| description | TEXT | ⚠️ NOT NULL - Beschreibung |
| event_date | TIMESTAMP | ⚠️ NOT NULL - Event-Datum |
| person_id | INTEGER | 🔗 FK → person |
| company_site_id | INTEGER | 🔗 FK → company_site |
| opportunity_id | INTEGER | 🔗 FK → sales_opportunity |
| metadata | JSONB | Zusatzdaten (JSON) |
| created_at | TIMESTAMP | Erstellt am |
- data_files (Dateien & E-Mail-Anhänge)
Speichert Dateien (PDFs, Bilder, Office-Dokumente, E-Mail-Anhänge) mit Base64-Encoding:
| Spalte | Typ | Beschreibung |
|---|---|---|
| id | INTEGER | 🔑 Primary Key |
| person_id | INTEGER | 🔗 FK → person |
| company_site_id | INTEGER | 🔗 FK → company_site |
| event_id | INTEGER | 🔗 FK → event |
| sales_opportunity_id | INTEGER | 🔗 FK → sales_opportunity |
| filename | VARCHAR | ⚠️ NOT NULL - Dateiname (oder E-Mail-Subject) |
| file_type | VARCHAR | ⚠️ NOT NULL - pdf, image, docx, pptx, xlsx, email, other |
| mime_type | VARCHAR | MIME-Type (z.B. application/pdf, message/rfc822) |
| file_size_bytes | BIGINT | Dateigröße in Bytes (max 100MB) |
| file_hash | VARCHAR | SHA-256 Hash (Duplikat-Erkennung) |
| source | VARCHAR | ⚠️ NOT NULL - email_attachment, user_upload, agent_download, email_message |
| source_email_id | VARCHAR | MS365 Message ID |
| source_path | VARCHAR | Originaler Dateipfad |
| file_data | TEXT | Base64-kodierter Dateiinhalt (NULL bei E-Mails ohne Anhang) |
| email_metadata | JSONB | E-Mail-Metadaten (from, to, cc, subject, date, importance) |
| email_body_text | TEXT | E-Mail-Body als Plain Text |
| email_body_html | TEXT | E-Mail-Body als HTML |
| extracted_text | TEXT | Extrahierter Text (für Volltextsuche) |
| extraction_method | VARCHAR | pdf_text, ocr, docx_parse, email_body, etc. |
| extraction_status | VARCHAR | pending, completed, failed, skipped |
| extraction_error | TEXT | Fehlermeldung bei Extraktion |
| description | TEXT | Benutzer-Beschreibung |
| tags | JSONB | Tags (JSON) |
| created_at | TIMESTAMP | Erstellt am |
| updated_at | TIMESTAMP | Aktualisiert am |
Beispiel - Komplette E-Mail speichern (mit Body):
INSERT INTO data_files (
person_id, filename, file_type, mime_type,
source, source_email_id, email_metadata,
email_body_text, email_body_html,
extracted_text, extraction_method, extraction_status
) VALUES (
1,
'Re: Contract Draft', -- Subject als filename
'email',
'message/rfc822',
'email_message',
'AAMkAGI2TG93AAA=',
'{
"subject": "Re: Contract Draft",
"from": {"name": "John Doe", "email": "john@example.com"},
"to": [{"email": "you@company.com"}],
"cc": [{"email": "legal@company.com"}],
"received_at": "2025-01-15T10:30:00Z",
"importance": "high",
"hasAttachments": true,
"conversationId": "AAQkAGI2..."
}'::jsonb,
'Hallo, anbei der überarbeitete Vertragsentwurf...', -- Plain text
'<html><body><p>Hallo, anbei der überarbeitete Vertragsentwurf...</p></body></html>',
'Hallo, anbei der überarbeitete Vertragsentwurf...', -- Für Suche
'email_body',
'completed'
) RETURNING id, filename;
Beispiel - E-Mail-Anhang speichern (verknüpft mit E-Mail):
-- Erst E-Mail speichern, dann Anhänge mit gleicher source_email_id
INSERT INTO data_files (
person_id, filename, file_type, mime_type, file_size_bytes,
source, source_email_id, file_data, email_metadata
) VALUES (
1, 'contract.pdf', 'pdf', 'application/pdf', 245678,
'email_attachment', 'AAMkAGI2TG93AAA=', -- Gleiche ID wie E-Mail!
'<base64-encoded-content>',
'{"subject": "Re: Contract Draft", "from": {"email": "john@example.com"}}'::jsonb
) RETURNING id, filename;
Alle Dateien einer E-Mail finden (E-Mail + Anhänge):
SELECT id, filename, file_type, source, file_size_bytes
FROM data_files
WHERE source_email_id = 'AAMkAGI2TG93AAA='
ORDER BY file_type = 'email' DESC, filename;
Beispiel - Datei aus _DATA_FROM_USER hochladen:
INSERT INTO data_files (
company_site_id, filename, file_type, mime_type, file_size_bytes,
source, source_path, file_data
) VALUES (
5, 'proposal.pdf', 'pdf', 'application/pdf', 512000,
'user_upload', '_DATA_FROM_USER/proposal.pdf',
'<base64-encoded-content>'
) RETURNING id, filename;
Volltextsuche in extrahiertem Text:
SELECT id, filename, person_id, company_site_id
FROM data_files
WHERE to_tsvector('german', extracted_text) @@ to_tsquery('german', 'Vertrag & Angebot');
- schema_migrations (System-Tabelle)
Datenbank-Migrationen:
| Spalte | Typ | Beschreibung |
|---|---|---|
| id | BIGINT | Migrations-ID |
| applied | TIMESTAMP | Ausführungsdatum |
| description | VARCHAR | Beschreibung |
🔗 Beziehungen (Foreign Keys)
company_site (1) ──┬── (N) person ├── (N) sales_opportunity ├── (N) event └── (N) data_files
person (1) ────────┬── (N) sales_opportunity ├── (N) event └── (N) data_files
sales_opportunity (1) ─┬── (N) event └── (N) data_files
event (1) ──────────── (N) data_files
⚠️ WICHTIG: KEINE UNIQUE CONSTRAINTS AUF name!
Die Tabellen haben KEINE UNIQUE-Constraints auf name:
- company_site.name ist NICHT unique
- person.name ist NICHT unique
- sales_opportunity.title ist NICHT unique
NIEMALS ON CONFLICT (name) verwenden! Das führt zu Fehlern!
Stattdessen:
- ERST prüfen ob Eintrag existiert:
SELECT id FROM company_site WHERE name ILIKE '%..%' - DANN entweder UPDATE (wenn gefunden) oder INSERT (wenn nicht gefunden)
Beispiel - Firma erstellen oder finden:
-- SCHRITT 1: Prüfen ob existiert
SELECT id, name FROM company_site WHERE name ILIKE '%Kölner Stadt%' LIMIT 1;
-- SCHRITT 2a: Falls gefunden → UPDATE
UPDATE company_site SET updated_at = CURRENT_TIMESTAMP, notes = 'Updated...' WHERE id = <gefundene_id>;
-- SCHRITT 2b: Falls nicht gefunden → INSERT
INSERT INTO company_site (name, address_city, created_at, updated_at)
VALUES ('Neue Firma GmbH', 'Berlin', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
RETURNING id;
💡 Besonderheiten
- JSONB-Felder: tags, metadata, email_metadata für flexible Erweiterungen
- Timestamps: Auto-Update via CURRENT_TIMESTAMP
- Dual-System: Alte adressen-Tabelle + neues CRM-Schema
- Multi-Entity Events: Events können mit Person, Firma ODER Opportunity verknüpft sein
- Dateispeicherung: Base64-kodiert in TEXT-Feld (max 100MB)
- Volltextsuche: GIN-Index auf extracted_text für deutsche Suche
- Duplikat-Erkennung: SHA-256 Hash auf file_hash
📎 Datei-Workflows
E-Mail-Anhänge extrahieren (MS365)
E-Mails suchen:
mcp__ms365__list-mail-messages mit filter oder searchAnhänge auflisten:
mcp__ms365__list-mail-attachments(messageId)Anhang-Inhalt holen (bereits Base64!):
mcp__ms365__get-mail-attachment(messageId, attachmentId) → contentBytes ist bereits Base64-kodiertIn CRM speichern:
INSERT INTO data_files (person_id, filename, file_type, source, file_data, email_metadata) VALUES (...) RETURNING id;
User-Upload aus _DATA_FROM_USER
- Datei lesen und Base64-kodieren (in Python/Node)
- SHA-256 Hash berechnen für Duplikat-Check
- In CRM mit source = 'user_upload' speichern
Text-Extraktion
| Dateityp | Methode | extraction_method |
|---|---|---|
| Text-Extraktion (pypdf) | pdf_text | |
| PDF (gescannt) | OCR (pytesseract) | pdf_ocr |
| Bild | OCR (pytesseract) | ocr |
| DOCX | python-docx | docx_parse |
| PPTX | python-pptx | pptx_parse |
| XLSX | openpyxl | xlsx_parse |