Żywy schemat, nie migawka
Model widzi prawdziwe tabele, kolumny, typy i ograniczenia przez połączenie, więc przestaje wymyślać klucze obce, które nie istnieją.
Patrzysz na migrację, która dotyka milionów wierszy, musi zachować integralność referencyjną i nie może wyłączyć aplikacji. Schemat ma osiemnaście tabel, które tylko po części pamiętasz, oraz graf kluczy obcych, którego nikt nie narysował od 2022 roku. Wklejasz okrojony zrzut do okna czatu, a AI zgaduje relacje, których nie widzi; zwrócony SQL odwołuje się do kolumn, które nie istnieją.
Serwer MCP do baz danych likwiduje ten martwy punkt. Daje Cursorowi, Claude Code i Codeksowi żywe, strukturalne połączenie z bazą danych, dzięki czemu mogą zbadać prawdziwy schemat, odczytać rzeczywiste wyjście EXPLAIN i sprawdzić ograniczenia, zanim zaproponują choćby jeden wiersz DDL. Praca przesuwa się z opisywania bazy danych na wspólne rozumowanie o niej.
Żywy schemat, nie migawka
Model widzi prawdziwe tabele, kolumny, typy i ograniczenia przez połączenie, więc przestaje wymyślać klucze obce, które nie istnieją.
Prawdziwe plany wykonania
Utrzymywany serwer potrafi uruchomić EXPLAIN/EXPLAIN ANALYZE i odczytać pg_stat_statements, więc porady dotyczące indeksów opierają się na selektywności, a nie na przeczuciach.
Bezpieczniejsze migracje
Mając rzeczywisty schemat w kontekście, AI może zaplanować addytywne, odwracalne kroki i wygenerować rollback równolegle ze skryptem do przodu.
Ograniczony dostęp
Skieruj serwer na rolę tylko do odczytu na replice deweloperskiej lub staging. Model zyskuje pełną introspekcję bez dostępu zapisu do czegokolwiek, co ma znaczenie.
Konfiguracja ma ten sam kształt we wszystkich trzech narzędziach: serwer stdio uruchamiany przez uvx (serwery w Pythonie) lub npx (serwery w Node), z łańcuchem połączenia przekazywanym przez zmienną środowiskową, dzięki czemu nigdy nie ląduje w historii powłoki ani w konfiguracji wrzuconej do repozytorium.
Rekomendowany serwer to Postgres MCP Pro od Crystal DBA (postgres-mcp na PyPI). Oprócz zwykłego SQL dodaje analizę EXPLAIN, rekomendacje strojenia indeksów i kontrole kondycji bazy danych, a do tego dostarcza flagę --access-mode restricted, która wymusza wykonywanie tylko do odczytu, pojedynczych instrukcji.
Dodaj do .cursor/mcp.json (projekt) lub ~/.cursor/mcp.json (globalnie):
{ "mcpServers": { "postgres": { "command": "uvx", "args": ["postgres-mcp", "--access-mode", "restricted"], "env": { "DATABASE_URI": "postgresql://readonly:secret@localhost:5432/mydb" } } }}claude mcp add postgres \ -e DATABASE_URI=postgresql://readonly:secret@localhost:5432/mydb \ -- uvx postgres-mcp --access-mode restrictedDodaj do ~/.codex/config.toml:
[mcp_servers.postgres]command = "uvx"args = ["postgres-mcp", "--access-mode", "restricted"]env = { DATABASE_URI = "postgresql://readonly:secret@localhost:5432/mydb" }Albo z poziomu CLI:
codex mcp add postgres \ --env DATABASE_URI=postgresql://readonly:secret@localhost:5432/mydb \ -- uvx postgres-mcp --access-mode restrictedJeśli korzystasz z Supabase, Supabase MCP (@supabase/mcp-server-supabase) jest gotową do podmiany alternatywą, która uwierzytelnia się osobistym tokenem dostępu i obsługuje --read-only.
Pozostałe trzy działają dokładnie według tego samego wzorca, co Postgres powyżej: blok command/args/env w .cursor/mcp.json, wiersz claude mcp add ... -- ... lub tabela [mcp_servers.<name>] w ~/.codex/config.toml. Zmienia się tylko nazwa pakietu i zmienna środowiskowa połączenia. Poniżej pokazano formę dla Claude Code; odpowiedniki dla Cursora i Codeksa mapują się jeden do jednego.
Serwer: mysql-mcp-server (PyPI). Czyta osobne zmienne środowiskowe, a nie pojedynczy URL połączenia.
claude mcp add mysql \ -e MYSQL_HOST=localhost -e MYSQL_PORT=3306 \ -e MYSQL_USER=readonly -e MYSQL_PASSWORD=secret -e MYSQL_DATABASE=mydb \ -- uvx mysql-mcp-serverW przypadku Cursora umieść te same pięć kluczy MYSQL_* w obiekcie env obok "command": "uvx", "args": ["mysql-mcp-server"]. Samo MYSQL_URL nic nie da — serwer wystartuje bez poświadczeń i nie nawiąże połączenia.
Serwer: oficjalny mongodb-mcp-server (npm). Łańcuch połączenia przekazujesz przez MDB_MCP_CONNECTION_STRING (preferowane) lub flagę --connectionString — nigdy jako goły argument pozycyjny.
claude mcp add mongodb \ -e MDB_MCP_CONNECTION_STRING=mongodb://localhost:27017/mydb \ -- npx -y mongodb-mcp-serverDodaj --readOnly po nazwie pakietu, aby ograniczyć go do operacji find/aggregate.
Serwer: oficjalny redis-mcp-server (PyPI). Przekaż URI przez --url i przypnij pakiet przez --from.
claude mcp add redis \ -- uvx --from redis-mcp-server@latest redis-mcp-server --url redis://localhost:6379/0Mając schemat w kontekście, projektowanie przestaje być odizolowanym planowaniem i staje się dialogiem. Model bada twoje prawdziwe tabele users i orders, a następnie proponuje dodatki, które pasują do twoich konwencji nazewnictwa, typów i ograniczeń.
Typowy wynik, osadzony w twojej rzeczywistej strukturze:
-- Required for the EXCLUDE constraint below: GiST equality on uuid/text-- columns needs the btree_gist operator classes.CREATE EXTENSION IF NOT EXISTS btree_gist;
CREATE TABLE subscription_plans ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, billing_cycle INTERVAL NOT NULL, -- e.g. '1 month'::interval features JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT now());
CREATE TABLE user_subscriptions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, plan_id UUID NOT NULL REFERENCES subscription_plans(id), status VARCHAR(20) NOT NULL DEFAULT 'active', current_period_start TIMESTAMPTZ NOT NULL, current_period_end TIMESTAMPTZ NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-- One active subscription per user, enforced at the DB level CONSTRAINT one_active_subscription EXCLUDE (user_id WITH =) WHERE (status = 'active'));Jeśli wolisz pominąć rozszerzenie, tę samą regułę można wyrazić jako częściowy unikalny indeks: CREATE UNIQUE INDEX one_active_sub ON user_subscriptions(user_id) WHERE status = 'active';. Wartość pracy przez serwer MCP polega na tym, że model już wie, iż users(id) jest typu UUID, i dopasowuje się do niego, zamiast zgadywać BIGINT.
Schematy rzadko wyłaniają się od razu doskonałe. Ponieważ serwer utrzymuje kontekst między turami, możesz iterować: „Teraz dodaj dziennik audytu, który zapisuje każdą zmianę planu z planem poprzednim i nowym, powodem (upgrade, downgrade, renewal) oraz kwotą proporcjonalnego rozliczenia”, a kolejny DDL poprawnie odwoła się do user_subscriptions(id).
Najmocniejszą częścią integracji MCP z bazą danych jest przekładanie pytania biznesowego na efektywny SQL, a następnie walidowanie go względem prawdziwego planu wykonania.
SELECT c.id, c.email, COUNT(o.id) AS order_count, SUM(o.total_amount) AS total_revenue, AVG(o.total_amount) AS avg_order_value, MAX(o.created_at) AS last_order_atFROM customers cJOIN orders o ON c.id = o.customer_idWHERE o.created_at >= date_trunc('quarter', CURRENT_DATE) AND o.status <> 'cancelled' AND c.is_test_account = falseGROUP BY c.id, c.emailORDER BY total_revenue DESCLIMIT 10;Ponieważ serwer potrafi odczytać plan, porada dotycząca indeksu jest konkretna, a nie ogólnikowa:
-- Supports the join + WHERE + sort, scoped to live ordersCREATE INDEX CONCURRENTLY idx_orders_customer_created_activeON orders (customer_id, created_at)WHERE status <> 'cancelled';Gdy coś działa wolno na produkcji, przepływem pracy jest śledztwo, a nie zgadywanie.
Osadzona w danych odpowiedź przechodzi plan krok po kroku, a potem proponuje celowane poprawki:
price + created_at na tabeli products.EXPLAIN ANALYZE, aby potwierdzić, że skanowanie sekwencyjne zmieniło się w skanowanie indeksu.CREATE INDEX CONCURRENTLY idx_products_price_createdON products (price, created_at)INCLUDE (name, category_id)WHERE created_at > CURRENT_DATE - INTERVAL '2 years';
WITH product_stats AS ( SELECT product_id, AVG(rating) AS avg_rating, COUNT(*) AS review_count FROM reviews GROUP BY product_id)SELECT p.id, p.name, p.price, c.name AS category_name, COALESCE(ps.avg_rating, 0) AS avg_rating, COALESCE(ps.review_count, 0) AS review_countFROM products pLEFT JOIN categories c ON p.category_id = c.idLEFT JOIN product_stats ps ON p.id = ps.product_idWHERE p.price BETWEEN 100 AND 500 AND p.created_at > CURRENT_DATE - INTERVAL '2 years'ORDER BY ps.avg_rating DESC NULLS LAST, ps.review_count DESC NULLS LAST;Serwer MongoDB wnosi ten sam wzorzec do agregacji. Poproś o „użytkowników pogrupowanych według miesiąca rejestracji, ze średnią liczbą zamówień na użytkownika i liczbą użytkowników bez zamówienia w ostatnich 90 dniach”, a wygeneruje potok wraz z indeksami, które przyspieszają jego działanie (db.orders.createIndex({ user_id: 1, created_at: -1 })).
Migracje dużych tabel wymagają addytywnych, odwracalnych kroków. Mając rzeczywisty schemat w kontekście, model może zaplanować fazy i wygenerować rollback równolegle ze skryptem do przodu.
CHECK (bez przepisywania tabeli, bez blokady).CREATE INDEX CONCURRENTLY (bez blokady zapisu).-- Phase 1: additive, no rewriteALTER TABLE users ADD COLUMN email_new VARCHAR(320) CHECK (email_new ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$');
-- Phase 3: batched backfill, keeps locks shortDO $$DECLARE batch INTEGER := 10000; affected INTEGER;BEGIN LOOP UPDATE users SET email_new = email WHERE id IN ( SELECT id FROM users WHERE email_new IS NULL LIMIT batch ); GET DIAGNOSTICS affected = ROW_COUNT; EXIT WHEN affected = 0; PERFORM pg_sleep(0.1); -- breathe between batches END LOOP;END $$;
-- Phase 4: enforce without blocking writesCREATE UNIQUE INDEX CONCURRENTLY idx_users_email_new ON users (email_new);ALTER TABLE users ALTER COLUMN email_new SET NOT NULL;
-- Phase 5 (after app cutover)ALTER TABLE users DROP COLUMN email;ALTER TABLE users RENAME COLUMN email_new TO email;Przez cały czas proś model o generowanie zapytań walidacyjnych — wykrywanie duplikatów, liczenie naruszeń ograniczeń, sprawdzanie osieroconych kluczy obcych — abyś mógł udowodnić integralność przed każdą fazą.
Konwersja aplikacji jednodostępnej na bezpieczeństwo na poziomie wierszy to klasyczna zmiana wysokiego ryzyka. Mając schemat w kontekście, model może zaproponować kolumny dzierżawcy wraz z politykami RLS, które pasują do twoich ról.
ALTER TABLE orders ADD COLUMN tenant_id UUID REFERENCES tenants(id);ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_orders ON orders FOR ALL TO application_role USING (tenant_id = current_setting('app.tenant_id')::UUID);
CREATE INDEX idx_orders_tenant_customer ON orders (tenant_id, customer_id, created_at);W przypadku danych szeregów czasowych o dużej objętości poproś o tabelę PARTITION BY RANGE z partycjami miesięcznymi i funkcją automatycznego tworzenia. Trzymaj przykładowe partycje na bieżących, przyszłych miesiącach, aby granice pozostały poprawne:
CREATE TABLE sensor_readings ( id BIGSERIAL, device_id INTEGER NOT NULL, metric_type VARCHAR(50) NOT NULL, value DOUBLE PRECISION NOT NULL, recorded_at TIMESTAMPTZ NOT NULL DEFAULT now()) PARTITION BY RANGE (recorded_at);
CREATE TABLE sensor_readings_2026_06 PARTITION OF sensor_readings FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');CREATE TABLE sensor_readings_2026_07 PARTITION OF sensor_readings FOR VALUES FROM ('2026-07-01') TO ('2026-08-01');Stosuj zasadę najmniejszych uprawnień podczas tworzenia roli, jako która łączy się serwer MCP:
CREATE ROLE mcp_readonly WITH LOGIN PASSWORD 'use-a-secret-manager';GRANT CONNECT ON DATABASE myapp TO mcp_readonly;GRANT USAGE ON SCHEMA public TO mcp_readonly;GRANT SELECT ON ALL TABLES IN SCHEMA public TO mcp_readonly;-- Let the AI read plans and stats without touching dataGRANT pg_read_all_stats TO mcp_readonly;W połączeniu z własnym trybem tylko do odczytu serwera (--access-mode restricted dla postgres-mcp, --read-only dla Supabase, --readOnly dla MongoDB) daje to modelowi pełną introspekcję i zero dostępu do zapisu.