Budowanie niestandardowych serwerów MCP: rozszerzaj Cursor o swoje narzędzia
Twój zespół wciąż wkleja ten sam schemat Postgresa do czatu, a Cursor i tak w połowie przypadków źle zgaduje typy kolumn. Dane siedzą za wewnętrznym API, którego model nigdy nie widział, więc każde „napisz mi zapytanie” zamienia się w trzy rundy poprawek. Niestandardowy serwer MCP rozwiązuje to na stałe: udostępniasz swoją bazę danych (albo dowolne wewnętrzne narzędzie) jako pełnoprawne narzędzia, które agent może wywołać, i Cursor przestaje zgadywać.
Dobra wiadomość jest taka, że serwera nie musisz pisać ręcznie. Najszybsza droga to pozwolić agentowi Cursor zbudować go na podstawie krótkiej specyfikacji i oficjalnej dokumentacji SDK, a następnie zweryfikować całość w MCP Inspector, zanim w ogóle podłączysz go do edytora. To właśnie ten przepływ pracy omawia ten przewodnik.
Co z tego wyniesiesz
Dział zatytułowany „Co z tego wyniesiesz”- Powtarzalny przepływ pracy z agentem Cursor do generowania serwera MCP z pliku
spec.mdoraz dokumentacji SDK wskazanej przez@ - Działający, tylko-do-odczytu serwer MCP dla Postgresa zbudowany na aktualnym
@modelcontextprotocol/sdk(1.29.x) - Gotowe do wklejenia prompty, które wygenerują serwer, utwardzą narzędzie walidacją i przekonwertują stdio na zdalny Streamable HTTP
- Dokładny wpis w
~/.cursor/mcp.json(z wymaganym polemtype: "stdio"), aby go zarejestrować - Prawdziwą listę „kiedy to się psuje” dla awarii, które naprawdę bolą: zaśmiecanie stdout, uwierzytelnianie i odrzucenia schematu
Przepływ pracy: pozwól Cursorowi zbudować serwer
Dział zatytułowany „Przepływ pracy: pozwól Cursorowi zbudować serwer”Model Context Protocol to standard klient/serwer. Cursor jest klientem; twój serwer ogłasza narzędzia (wywoływalne funkcje jak query), zasoby (przeglądalne dane jak listy tabel) i komunikuje się przez stdio w przypadku serwerów lokalnych lub Streamable HTTP w przypadku zdalnych. Praktycznie nigdy nie piszesz instalacji protokołu ręcznie — robi to SDK, a agent Cursor podłącza SDK do twoich danych.
Krok 1: napisz specyfikację, którą agent może realizować
Dział zatytułowany „Krok 1: napisz specyfikację, którą agent może realizować”-
Utwórz projekt i zainstaluj dwie potrzebne zależności:
Okno terminala mkdir pg-mcp && cd pg-mcpnpm init -ynpm install @modelcontextprotocol/sdk zod postgres -
Napisz plik
spec.mdobok swojego kodu. Trzymaj go lekkim — kilka punktów w zupełności wystarczy agentowi do wygenerowania kodu:# Spec: Postgres MCP server- Read DATABASE_URL from the MCP env config- Expose a `query` tool that runs read-only SQL- Reject DROP / DELETE / TRUNCATE / UPDATE / INSERT unlessDANGEROUSLY_ALLOW_WRITE_OPS=1- Expose tables as resources (one per table) so the agent can browse the schema- Use Zod for all tool input schemas- Transport: stdio
Krok 2: generuj z agentem, a nie z pamięci
Dział zatytułowany „Krok 2: generuj z agentem, a nie z pamięci”Najważniejszy ruch: podaj agentowi aktualny plik README SDK jako kontekst, żeby nie wygenerował API sprzed wersji 1.x ze swoich danych treningowych. Własny cookbook MCP Cursora robi dokładnie to — wskazuje przez @ surowe pliki README SDK i bibliotek w promcie.
Po rundzie czy dwóch wymiany zdań powinieneś wylądować na czymś zbliżonym do tego. Zwróć uwagę na kształt: McpServer + registerTool, schemat Zod, pojedynczy zwracany CallToolResult oraz StdioServerTransport do uruchomienia serwera.
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';import { z } from 'zod';import postgres from 'postgres';
const sql = postgres(process.env.DATABASE_URL!);const allowWrites = process.env.DANGEROUSLY_ALLOW_WRITE_OPS === '1';
const server = new McpServer({ name: 'pg-mcp', version: '1.0.0' });
server.registerTool( 'query', { title: 'Run SQL query', description: 'Execute a read-only SQL query against the database', inputSchema: { statement: z .string() .min(1) .refine( (s) => allowWrites || !/\b(drop|delete|truncate|update|insert)\b/i.test(s), 'Write operations are disabled. Set DANGEROUSLY_ALLOW_WRITE_OPS=1 to enable.', ), }, }, async ({ statement }) => { const rows = await sql.unsafe(statement); return { content: [{ type: 'text', text: JSON.stringify(rows, null, 2) }] }; },);
const transport = new StdioServerTransport();await server.connect(transport);Kluczowy szczegół poprawności: callback narzędzia zwraca jeden CallToolResult (tablicę content), a nie asynchroniczny generator. Jeśli chcesz raportować postęp długiego zadania, SDK udostępnia powiadomienia przez parametr extra handlera — a nie przez yield.
Krok 3: udostępnij tabele jako zasoby
Dział zatytułowany „Krok 3: udostępnij tabele jako zasoby”Zasoby pozwalają agentowi przeglądać twój schemat bez zużywania wywołania narzędzia. Użyj registerResource z ResourceTemplate, aby URI był dynamiczny:
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
server.registerResource( 'table', new ResourceTemplate('pg://table/{name}', { list: async () => { const tables = await sql` select table_name from information_schema.tables where table_schema = 'public'`; return { resources: tables.map((t) => ({ uri: `pg://table/${t.table_name}`, name: t.table_name, })), }; }, }), { title: 'Database tables', description: 'Browse columns for each table' }, async (uri, { name }) => { const cols = await sql` select column_name, data_type from information_schema.columns where table_name = ${name}`; return { contents: [{ uri: uri.href, text: JSON.stringify(cols, null, 2) }] }; },);Krok 4: zweryfikuj w MCP Inspector, zanim ruszysz Cursor
Dział zatytułowany „Krok 4: zweryfikuj w MCP Inspector, zanim ruszysz Cursor”Nie rejestruj nieprzetestowanego serwera w edytorze — najpierw zdebuguj go w izolacji. Inspector to przeglądarkowe narzędzie, które uruchamiasz za pomocą npx; nie ma instalacji globalnej ani REPL-a.
npx @modelcontextprotocol/inspector npx tsx src/index.tsOtwiera to lokalny interfejs WWW, w którym możesz wylistować narzędzia i zasoby serwera, odpalić query z prawdziwym zapytaniem i obserwować surową wymianę JSON-RPC. Jeśli narzędzie rzuca wyjątek albo twój schemat odrzuca wejście, zobaczysz to tutaj — na długo przed agentem Cursora.
Krok 5: zarejestruj go w Cursor
Dział zatytułowany „Krok 5: zarejestruj go w Cursor”Dodaj serwer do ~/.cursor/mcp.json (globalnie) lub .cursor/mcp.json (w zakresie projektu). W przypadku serwerów stdio Cursor wymaga teraz pola type:
{ "mcpServers": { "pg-mcp": { "type": "stdio", "command": "npx", "args": ["tsx", "/abs/path/to/pg-mcp/src/index.ts"], "env": { "DATABASE_URL": "${env:DATABASE_URL}" } } }}Otwórz Cursor Settings -> MCP / Tools & Integrations, aby potwierdzić, że serwer się połączył i jego narzędzia są na liście (zielona kropka). Teraz zapytaj agenta „ile zamówień wysłano w zeszłym tygodniu?”, a wywoła on query na twoim prawdziwym schemacie zamiast halucynować kolumny.
Wyjście na zewnątrz: ze stdio na Streamable HTTP
Dział zatytułowany „Wyjście na zewnątrz: ze stdio na Streamable HTTP”Działanie przez stdio jest idealne dla jednego programisty. Gdy całemu zespołowi potrzebne jest to samo połączenie — wspólne poświadczenia, centralne ograniczanie liczby żądań, jedno miejsce do aktualizacji schematu — wdrażasz serwer jako usługę HTTP. Nowoczesny transport to Streamable HTTP na pojedynczym endpoincie /mcp. Stary transport HTTP+SSE został wycofany w specyfikacji MCP na rzecz Streamable HTTP, a poszczególni dostawcy wygaszają swoje endpointy /sse według własnych harmonogramów (serwer Rovo od Atlassiana na przykład wyłącza /v1/sse 30 czerwca 2026), więc nie buduj nowych serwerów na /sse.
Wynikowa konfiguracja Cursora porzuca command/args i używa url plus obiektu headers — dla serwerów zdalnych nie ma klucza transport:
{ "mcpServers": { "pg-mcp": { "url": "https://mcp.company.com/mcp", "headers": { "Authorization": "Bearer ${env:MCP_TOKEN}" } } }}W przypadku dystrybucji wersji stdio do zespołu opublikuj ją jako pakiet npm z wpisem bin i wskaż command na binarkę (npx @company/pg-mcp) zamiast na ścieżkę bezwzględną.
Testowanie serwera w CI
Dział zatytułowany „Testowanie serwera w CI”Nie potrzebujesz biblioteki z atrapą klienta — nie ma pakietu @mcp/testing. Testuj handlery narzędzi na dwa sposoby. Najtańszy to wywołać logikę handlera bezpośrednio w Vitest. Do sprawdzenia end-to-end SDK dostarcza parę transportów działających w pamięci, dzięki czemu możesz połączyć prawdziwego Client z twoim McpServer bez uruchamiania procesu:
import { describe, it, expect } from 'vitest';import { Client } from '@modelcontextprotocol/sdk/client/index.js';import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';import { server } from '../src/index.js'; // export your McpServer
describe('pg-mcp', () => { it('rejects write operations by default', async () => { const [clientT, serverT] = InMemoryTransport.createLinkedPair(); const client = new Client({ name: 'test', version: '1.0.0' }); await Promise.all([server.connect(serverT), client.connect(clientT)]);
const res = await client.callTool({ name: 'query', arguments: { statement: 'DROP TABLE users' }, });
expect(res.isError).toBe(true); });});Kiedy to się psuje
Dział zatytułowany „Kiedy to się psuje”Serwery MCP psują się na kilka przewidywalnych sposobów. Oto te, które kosztują najwięcej czasu:
- Zaśmiecanie stdout zabija połączenie. Serwer stdio mówi po JSON-RPC przez stdout. Każdy zabłąkany
console.loguszkadza strumień, a Cursor zgłasza serwer jako uszkodzony bez żadnego użytecznego błędu. Kieruj całe logowanie nastderr(console.error) albo do pliku, nigdy naconsole.log. - Serwer się łączy, ale nie pojawiają się żadne narzędzia. Niemal zawsze jest to awaria podczas startu — brakujący
DATABASE_URL, nieobsłużona obietnica albo rzucony wyjątek przedserver.connect(). Uruchom go samodzielnie w Inspectorze (Krok 4), żeby zobaczyć prawdziwy ślad stosu; Cursor go połyka. - Wywołania narzędzi przekraczają timeout przy dużych wynikach. Zwrócenie zapytania na 50 tys. wierszy jako jednego bloku tekstu zawiesza agenta. Dodaj
LIMITw narzędziu albo stronicuj. Długotrwała praca powinna raportować postęp przez parametrextrahandlera, a nie blokować. - Odrzucenia walidacji schematu wyglądają jak „narzędzie nic nie zrobiło”. Gdy Zod odrzuca wejście, agent dostaje błąd walidacji, a nie wynik. Spraw, by komunikaty
.refine()były jednoznaczne („Write operations are disabled…”), żeby agent sam się poprawił, zamiast ślepo ponawiać próbę. - Awarie uwierzytelniania na serwerach zdalnych objawiają się jako ciche rozłączenie. Błąd 401 z twojego endpointu
/mcppokazuje się w Cursor jako „server unavailable”. Sprawdź logi serwera i potwierdź, że nagłówekAuthorizationwmcp.jsonfaktycznie podstawił twoją zmienną środowiskową (${env:MCP_TOKEN}wymaga, by zmienna istniała w środowisku uruchomieniowym Cursora).
Co dalej
Dział zatytułowany „Co dalej”- Przepływy automatyzacji — połącz swoje nowe narzędzia MCP w powtarzalne potoki Cursora
- Prywatność i bezpieczeństwo w Cursor — ogranicz zakres poświadczeń i audytuj, do czego agent może dotrzeć przez MCP
- Niestandardowe reguły i szablony — naucz agenta, kiedy sięgać po twoje niestandardowe narzędzia
- Projektowanie baz danych z AI — przepływ projektowania schematu, który twoje narzędzie
queryteraz wzmacnia