Przejdź do treści

Serwer MCP w TypeScript: bezpieczny przewodnik dla Next.js

Zbuduj serwer MCP w TypeScript SDK v2, dodaj zasoby i narzędzia, przetestuj go przez stdio oraz przygotuj bezpieczną integrację Streamable HTTP.

Maciej Sala

Founder StriveLab

8 min czytaniaOpublikowano 31 marca 2026 (Aktualizacja 1 sierpnia 2026)

Model Context Protocol jest otwartym protokołem komunikacji opartym na JSON-RPC 2.0. Oddziela sposób dostarczania kontekstu i akcji od wywołania konkretnego modelu. Host AI tworzy osobnego klienta dla każdego serwera MCP, negocjuje z nim wersję protokołu i obsługiwane możliwości, a następnie odkrywa dostępne funkcje.

MCP zmniejsza liczbę własnych adapterów, lecz nie usuwa logiki domenowej ani różnic między hostami. Serwer powinien pozostać cienką warstwą nad istniejącymi usługami aplikacji. Reguły biznesowe, transakcje, kontrola dostępu i audyt nie powinny zależeć od tego, czy żądanie przyszło przez MCP, REST czy kolejkę.

Nie myl jednak MCP z WebMCP, które pojawia się w audycie Przeglądanie Agentowe w Lighthouse. MCP opisuje połączenie modelu z zewnętrznymi narzędziami i danymi przez serwer. WebMCP dotyczy strony internetowej w przeglądarce: formularzy, akcji i narzędzi dostępnych dla agenta podczas korzystania z UI. Nazwy są podobne, ale to różne warstwy architektury.

Czym jest MCP i jak wygląda jego architektura?

MCP porządkuje komunikację między aplikacją AI a systemami zewnętrznymi. Host, czyli na przykład aplikacja desktopowa albo IDE, tworzy osobnego klienta MCP dla każdego serwera. Podczas inicjalizacji strony uzgadniają wersję protokołu i obsługiwane możliwości. Dopiero później host pobiera listę funkcji i pozwala z nich korzystać.

  • Host zarządza połączeniami i zgodą użytkownika na wykonanie operacji.
  • Tools wykonują opisane operacje, od wyszukania wpisu po utworzenie szkicu.
  • Resources dostarczają dane do odczytu, takie jak lista artykułów lub dokumentacja.
  • Prompts są szablonami wybieranymi przez użytkownika w obsługujących je hostach.

Serwer może również korzystać z funkcji udostępnianych przez klienta, między innymi logowania, elicitation lub sampling. Obsługa każdej funkcji zależy od negocjacji możliwości. Sam komunikat „wspieramy MCP” nie potwierdza więc obsługi zasobów, promptów, wszystkich transportów ani danego mechanizmu autoryzacji.

W projekcie lokalnym najczęściej użyjesz stdio. Host uruchamia wtedy proces serwera, wysyła komunikaty przez standardowe wejście i odbiera je przez standardowe wyjście. Stdout pozostaje kanałem protokołu MCP, dlatego logi trzeba kierować do stderr. Dla połączeń sieciowych służy Streamable HTTP.

Jak zaprojektować serwer MCP dla aplikacji Next.js?

MCP powinien być adapterem nad logiką aplikacji, a nie jej drugim backendem. Przykładowy serwer odczyta metadane plików MDX, udostępni wyszukiwanie i zapisze nowy artykuł jako szkic. Publikację pozostawimy istniejącemu procesowi redakcji. Tak samo można opakować funkcje z modułu services, repozytorium bazy danych lub wewnętrznego API używanego przez Next.js.

Krok 1: instalacja TypeScript SDK v2

Stabilna linia v2 wymaga Node.js 20 lub nowszego, ESM oraz Zod 4.2 lub nowszego. Pakiet gray-matter bezpieczniej obsłuży YAML niż parser oparty na dzieleniu wierszy po dwukropku.

Code
mkdir mcp-blog-server
cd mcp-blog-server
npm init -y
npm pkg set type=module
npm install @modelcontextprotocol/server zod@^4.2 gray-matter
npm install -D typescript @types/node tsx
mkdir src

Ustaw module i moduleResolution na NodeNext, włącz strict, a jako target wybierz co najmniej ES2022. Katalog artykułów przekaż w BLOG_DIR, zamiast wiązać serwer z układem jednego repozytorium.

Krok 2: utworzenie serwera i zasobu

Poniższy fragment pokazuje rdzeń pliku src/index.ts. Funkcja tworząca serwer ułatwia później testy i uruchamianie wielu niezależnych połączeń.

Code
import { McpServer } from '@modelcontextprotocol/server'
import { serveStdio } from '@modelcontextprotocol/server/stdio'
import matter from 'gray-matter'
import { readdir, readFile, writeFile } from 'node:fs/promises'
import { isAbsolute, relative, resolve } from 'node:path'
import * as z from 'zod/v4'
 
const BLOG_DIR = resolve(process.env.BLOG_DIR ?? './content/blog')
 
function createServer(): McpServer {
  const server = new McpServer({
    name: 'blog-content-server',
    version: '2.0.0',
  })
 
  server.registerResource(
    'blog-posts',
    'blog://posts',
    {
      title: 'Lista wpisów blogowych',
      description: 'Metadane artykułów MDX dostępnych w katalogu bloga',
      mimeType: 'application/json',
    },
    async (uri) => {
      const files = (await readdir(BLOG_DIR)).filter((file) =>
        file.endsWith('.mdx'),
      )
      const posts = await Promise.all(
        files.map(async (file) => {
          const source = await readFile(resolve(BLOG_DIR, file), 'utf8')
          const { data } = matter(source)
 
          return {
            slug: file.slice(0, -4),
            title: String(data.title ?? ''),
            description: String(data.description ?? ''),
            tags: Array.isArray(data.tags) ? data.tags.map(String) : [],
          }
        }),
      )
 
      return {
        contents: [
          {
            uri: uri.href,
            mimeType: 'application/json',
            text: JSON.stringify(posts),
          },
        ],
      }
    },
  )

Zasób przekazuje metadane, nie pełną treść wszystkich artykułów. Ogranicza to rozmiar odpowiedzi i ryzyko wysłania zbędnych danych. Jeśli host ma czytać pojedynczy tekst, dodaj osobny szablon URI, na przykład blog://posts/{slug}, oraz ponownie sprawdź dostęp użytkownika do wskazanego wpisu.

Krok 3: narzędzie do wyszukiwania wpisów

Narzędzie przyjmuje ograniczoną długość frazy i zamknięty zestaw pól. Adnotacje informują host, że operacja jest tylko do odczytu, ale pozostają wskazówkami.

Code
server.registerTool(
  'search-posts',
  {
    title: 'Wyszukiwanie wpisów',
    description: 'Wyszukuje frazę w metadanych i treści lokalnych plików MDX',
    inputSchema: z.object({
      query: z.string().trim().min(2).max(100),
      searchIn: z.enum(['title', 'tags', 'content', 'all']).default('all'),
    }),
    annotations: {
      readOnlyHint: true,
      destructiveHint: false,
      idempotentHint: true,
      openWorldHint: false,
    },
  },
  async ({ query, searchIn }) => {
    const phrase = query.toLocaleLowerCase('pl')
    const files = (await readdir(BLOG_DIR)).filter((file) =>
      file.endsWith('.mdx'),
    )
    const posts: Array<{ slug: string; title: string }> = []
 
    for (const file of files) {
      const source = await readFile(resolve(BLOG_DIR, file), 'utf8')
      const { data, content } = matter(source)
      const title = String(data.title ?? '')
      const tags = Array.isArray(data.tags) ? data.tags.map(String) : []
      const fields = {
        title: title.toLocaleLowerCase('pl'),
        tags: tags.join(' ').toLocaleLowerCase('pl'),
        content: content.toLocaleLowerCase('pl'),
      }
      const matched =
        searchIn === 'all'
          ? Object.values(fields).some((value) => value.includes(phrase))
          : fields[searchIn].includes(phrase)
 
      if (matched) posts.push({ slug: file.slice(0, -4), title })
    }
 
    return {
      content: [{ type: 'text', text: JSON.stringify({ posts }) }],
      structuredContent: { posts },
    }
  },
)

W dużym repozytorium nie skanuj całego katalogu przy każdym wywołaniu. Zbuduj indeks, odświeżaj go po zmianie treści i ogranicz liczbę wyników. Zwracaj także stabilny identyfikator, aby kolejne narzędzie nie musiało polegać na tytule.

Krok 4: bezpieczne tworzenie szkicu MDX

Największa luka w naiwnym przykładzie zapisu to możliwość wyjścia poza katalog przez spreparowany slug oraz nadpisania istniejącego pliku. Rozwiązują ją zamknięty format identyfikatora, kontrola ścieżki i flaga wx.

Code
  server.registerTool(
    'create-post-draft',
    {
      title: 'Utworzenie szkicu wpisu',
      description: 'Tworzy nowy szkic MDX bez publikowania i nadpisywania pliku',
      inputSchema: z.object({
        slug: z
          .string()
          .max(80)
          .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
        title: z.string().trim().min(5).max(120),
        description: z.string().trim().min(20).max(180),
        tags: z.array(z.string().trim().min(2).max(40)).max(8),
        content: z.string().trim().min(100).max(100_000),
      }),
      annotations: {
        readOnlyHint: false,
        destructiveHint: false,
        idempotentHint: false,
        openWorldHint: false,
      },
    },
    async ({ slug, title, description, tags, content }) => {
      const filePath = resolve(BLOG_DIR, `${slug}.mdx`)
      const localPath = relative(BLOG_DIR, filePath)
 
      if (localPath.startsWith('..') || isAbsolute(localPath)) {
        throw new Error('Nieprawidłowa ścieżka szkicu')
      }
 
      const document = matter.stringify(content, {
        title,
        description,
        datePublished: new Date().toISOString().slice(0, 10),
        author: 'Redakcja',
        tags,
        checked: false,
      })
 
      await writeFile(filePath, document, {
        encoding: 'utf8',
        flag: 'wx',
      })
 
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify({ status: 'draft_created', slug }),
          },
        ],
      }
    },
  )
 
  return server
}
 
void serveStdio(createServer)
console.error('Serwer MCP działa przez stdio')

Szkic nadal wymaga kontroli redakcyjnej. Host powinien pokazać argumenty narzędzia i uzyskać zgodę przed zapisem. Pole destructiveHint: false mówi, że operacja nie usuwa danych, lecz nie czyni jej automatycznie bezpieczną i nie zastępuje autoryzacji.

Krok 5: testowanie przez MCP Inspector

Uruchom Inspector z serwerem przekazanym jako proces potomny:

Code
npx @modelcontextprotocol/inspector npx tsx src/index.ts

Sprawdź inicjalizację, listę zasobów, schemat każdego narzędzia, poprawne dane, wartości graniczne i błędy. Osobno przetestuj próbę użycia ../, drugi zapis pod tym samym identyfikatorem oraz bardzo długą treść. Do testów automatycznych wyodrębnij logikę odczytu i zapisu z handlerów MCP.

Krok 6: podłączenie lokalnego hosta MCP

Host obsługujący stdio potrzebuje polecenia startowego i bezwzględnych ścieżek. Nazwa pliku konfiguracyjnego oraz format mogą się różnić, dlatego potwierdź je w dokumentacji używanej aplikacji. Typowy wpis wygląda następująco:

Code
{
  "mcpServers": {
    "blog-content": {
      "command": "npx",
      "args": ["tsx", "/sciezka/mcp-blog-server/src/index.ts"],
      "env": {
        "BLOG_DIR": "/sciezka/projektu/content/blog"
      }
    }
  }
}

Nie przekazuj sekretów w pliku, który trafia do repozytorium. Proces lokalny ma uprawnienia konta systemowego uruchamiającego host, więc ogranicz mu dostęp do konkretnego katalogu i usług.

Jak udostępnić serwer przez Streamable HTTP?

Zdalny serwer wymaga adaptera HTTP zgodnego z SDK v2. Nie wystarczy opakować handlera stdio dowolnym endpointem Next.js. Trzeba zachować semantykę transportu, wymagane nagłówki i negocjację wersji protokołu. Aktualna specyfikacja upraszcza warstwę protokołu do modelu bezstanowego, ale aplikacja nadal może przechowywać stan domenowy, zadania lub sesję autoryzacji.

W środowisku produkcyjnym:

  • Wymuszaj HTTPS, weryfikuj nagłówek Origin i podczas pracy lokalnej nasłuchuj wyłącznie na interfejsie loopback.

  • Uwierzytelniaj użytkownika i sprawdzaj jego uprawnienia osobno dla każdego narzędzia, zasobu oraz rekordu domenowego.

  • Waliduj odbiorcę tokenu i zakresy. Token passthrough, czyli przekazanie cudzego tokenu bez weryfikacji, jest zabronionym wzorcem.

  • Dodaj limity czasu, rozmiaru, częstotliwości i współbieżności, a dla operacji zapisu także ochronę przed powtórzeniem żądania.

  • Rejestruj nazwę narzędzia, wynik autoryzacji, czas i identyfikator żądania. Usuwaj z logów tokeny, pełne treści dokumentów i dane osobowe.

Adnotacje nie egzekwują żadnych uprawnień. readOnlyHint i destructiveHint pomagają hostowi przedstawić ryzyko, lecz serwer musi sam podjąć decyzję na podstawie zweryfikowanej tożsamości i polityki dostępu.

Odporność na prompt injection

Opis narzędzia, zawartość zasobu i wynik zewnętrznego API traktuj jako dane, a nie polecenia o wyższym priorytecie. Ogranicz listę dostępnych narzędzi do potrzebnego zadania, pokazuj użytkownikowi argumenty działań modyfikujących dane i nie pozwalaj, aby tekst pobrany ze strony sam rozszerzył uprawnienia agenta.

Przy połączeniu zdalnego serwera z Responses API OpenAI używa się narzędzia typu mcp i pola server_url. allowed_tools ogranicza importowane narzędzia, co zmniejsza koszt oraz powierzchnię ataku. Wywołania domyślnie korzystają z procesu zatwierdzania; dla operacji wrażliwych nie wyłączaj require_approval. Serwer prywatny wymaga również odpowiedniej warstwy dostępu lub bezpiecznego tunelu.

Stabilna obsługa błędów

Klient powinien otrzymać kod, bezpieczny komunikat i informację, czy ponowienie ma sens. Szczegóły wyjątku zachowaj w logach serwera pod identyfikatorem żądania.

Code
try {
  const result = await fetchExternalData(query)
  return {
    content: [{ type: 'text', text: JSON.stringify(result) }],
  }
} catch (error: unknown) {
  const message = error instanceof Error ? error.message : 'Unknown error'
  console.error({ requestId, message })
 
  return {
    content: [
      {
        type: 'text',
        text: JSON.stringify({
          code: 'EXTERNAL_API_ERROR',
          requestId,
          retryable: true,
        }),
      },
    ],
    isError: true,
  }
}

Nie oznaczaj każdego błędu jako ponawialnego. Błąd walidacji lub brak uprawnień wymaga zmiany danych albo decyzji użytkownika, a nie automatycznej kolejnej próby.

Przykłady MCP w aplikacjach Next.js

Headless CMS i proces redakcyjny

Narzędzia mogą pobrać wpis, zaproponować metadane i utworzyć szkic. Publikacja, masowa zmiana linków lub usunięcie strony powinny przejść walidację domenową, podgląd różnic oraz zatwierdzenie redaktora. Dzięki temu MCP korzysta z tego samego procesu publikacji co panel CMS.

Google Search Console i GA4

Serwer może udostępnić zagregowane raporty dla wybranej usługi i zakresu dat. Agent porówna okresy i wskaże hipotezy, ale nie powinien przedstawiać korelacji jako przyczyny. Uprawnienia muszą uwzględniać konkretną usługę, konto i zakres danych, a wyniki powinny zawierać strefę czasową oraz informację o kompletności.

Obsługa katalogu e-commerce

Odczyt oferty, wykrywanie braków i przygotowanie propozycji opisów są dobrymi operacjami startowymi. Zmiana ceny, stanu magazynowego lub publikacja treści wymaga osobnego narzędzia, ograniczeń biznesowych i zgody człowieka. Agent nie powinien otrzymywać szerszych praw niż pracownik wykonujący to samo zadanie.

MCP czy klasyczne API?

MCP nie zastępuje , GraphQL ani kolejki zdarzeń. Udostępnia modelom ustandaryzowany katalog możliwości, podczas gdy API pozostaje interfejsem domenowym dla aplikacji. Często najlepsza architektura to serwer MCP wywołujący istniejący serwis, który egzekwuje reguły biznesowe.

Wybierz MCP jako warstwę agentową, gdy różne hosty mają odkrywać narzędzia, zasoby lub prompty, a schematy i zgody mają wspólny format. Zostań przy bezpośrednim API, gdy integracja jest deterministyczna, ma jednego znanego konsumenta albo przetwarza duże strumienie danych bez udziału modelu. Dodanie MCP nie gwarantuje zgodności z każdym hostem, dlatego kontrakt trzeba testować w każdym docelowym środowisku.

Lista kontrolna przed wdrożeniem serwera MCP

  • Przypnij wersje pakietów, zapisz obsługiwaną wersję specyfikacji i przeczytaj przewodnik migracji przed aktualizacją SDK.

  • Przetestuj negocjację możliwości oraz zachowanie hosta, gdy nie obsługuje zasobów, promptów, zatwierdzeń albo wybranego transportu.

  • Dodaj testy kontraktowe schematów wejścia i wyjścia, błędów, limitów, autoryzacji oraz powtórzonych operacji zapisu.

  • Mierz czas, odsetek błędów i liczbę wywołań per narzędzie. Ustaw alerty bez zapisywania poufnej treści w telemetrii.

  • Rozdziel narzędzia odczytujące, tworzące szkice, publikujące i usuwające. Każdemu nadaj osobną politykę dostępu oraz zatwierdzania.

Bezpieczne automatyzacje procesów i agenci AI w n8n, Make i Claude.
Automatyzacja AI

Często zadawane pytania

Czym jest MCP (Model Context Protocol)?

Model Context Protocol to otwarty protokół oparty na JSON-RPC 2.0. Host AI tworzy klienta MCP, który łączy się z serwerem udostępniającym narzędzia, zasoby i prompty. Protokół standaryzuje komunikację, ale zakres obsługiwanych funkcji nadal zależy od konkretnego hosta.

W jakim języku programowania mogę napisać serwer MCP?

Dostępne są oficjalne i społecznościowe SDK o różnych poziomach wsparcia. Ich klasyfikacja może się zmieniać, dlatego sprawdź aktualną tabelę tierów i zgodność z używaną wersją specyfikacji. Dla projektu Next.js wygodny jest TypeScript SDK v2 z pakietem @modelcontextprotocol/server, ale logikę domenową nadal utrzymuj poza warstwą MCP.

Czy MCP jest w pełni bezpieczny do wdrożeń na produkcji?

Nie z samego faktu użycia protokołu. Specyfikacja opisuje mechanizmy autoryzacji dla transportu HTTP, ale serwer nadal musi poprawnie sprawdzać tokeny, odbiorcę tokenu, zakresy i uprawnienia do każdego narzędzia. Potrzebne są też walidacja danych, limity, audyt, ochrona przed prompt injection oraz zatwierdzanie działań modyfikujących dane.

Jakie klienty AI wspierają protokół MCP w 2026 roku?

Wsparcie oferują wybrane aplikacje desktopowe, edytory, narzędzia programistyczne oraz platformy API. Nie każda implementacja obsługuje te same funkcje i transporty. Przed integracją sprawdź dokumentację hosta pod kątem tools, resources, prompts, zatwierdzeń, autoryzacji oraz stdio lub Streamable HTTP.

Czym to się różni od klasycznego function calling w API OpenAI albo Anthropic?

Function calling opisuje narzędzia przekazane bezpośrednio do wywołania modelu lub API. MCP dodaje protokół odkrywania i wywoływania narzędzi oraz udostępniania zasobów i promptów przez osobny serwer. Zwiększa przenośność, lecz nie gwarantuje identycznego zachowania każdego hosta ani modelu.

Czy to przydaje się w działaniach marketingowych lub SEO?

Tak, jeśli potrzebujesz kontrolowanego interfejsu do raportów, CMS-a lub firmowej bazy wiedzy. Narzędzia zapisujące treści powinny tworzyć szkic i wymagać zatwierdzenia, a dostęp do danych analitycznych musi uwzględniać uprawnienia użytkownika, minimalizację zakresu i rejestrowanie operacji.

Co wybrać, transport stdio czy Streamable HTTP?

Użyj stdio, gdy host uruchamia lokalny proces i zarządza jego cyklem życia. Dla serwera dostępnego przez sieć wybierz Streamable HTTP. Wtedy potrzebujesz HTTPS, sprawdzania nagłówka Origin, uwierzytelniania, autoryzacji oraz limitów. Zgodność z konkretnym hostingiem potwierdź na podstawie obsługi żądań, streamingu i czasu wykonania.

Którą wersję TypeScript SDK pokazuje artykuł?

Przykłady używają stabilnej linii v2 wydanej razem ze specyfikacją z 28 lipca 2026 roku. Serwer instaluje pakiet @modelcontextprotocol/server i Zod 4.2 lub nowszy. Starszy pakiet zbiorczy @modelcontextprotocol/sdk należy do linii v1 i ma osobny przewodnik migracji.

O autorze

Maciej Sala

Maciej Sala — konsultant technologiczny produktów cyfrowych i web developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwija własne projekty.

Pomagam przekładać takie tematy na konkretne wdrożenia w frontendzie, SEO, analityce i procesie produktowym.

Skontaktuj się ze mną

Biblioteka wiedzy na temat Next.js

Czytaj dalej

Zobacz więcej wpisów
Vercel AI SDK i budowa strumieniującego chatbota w Next.js

Vercel AI SDK zdejmuje z Twoich barków większość żmudnej roboty przy dodawaniu AI do aplikacji w Next.js. Zapomnij o ręcznym czytaniu i zarządzaniu strumieniem bajtów z API, parsowaniu chunków czy doklejaniu poprzednich wypowiedzi do historii rozmowy. W tym tutorialu zbudujesz działającego, streamującego chatbota w mniej więcej 30 minut. Potrzebujesz dwóch plików aplikacji, konfiguracji dostawcy i klucza API.

Maciej Sala

Maciej Sala

Founder StriveLab

Backend dla frontendowca: serwer, bazy danych i API

Frontend rzadko kończy się na komponencie i jednym fetch , a im bliżej realnego produktu, tym częściej jakość UI zależy od zachowania backendu. Jak API paginuje dane, jak zwraca błędy, jak kontroluje dostęp, co robi po przekroczeniu limitu czasu i czy potrafi bezpiecznie przyjąć ponowione żądanie.

Maciej Sala

Maciej Sala

Founder StriveLab

Przeglądanie Agentowe w PageSpeed Insights: jak przygotować stronę pod agentów AI

Przez lata projektowaliśmy strony internetowe dla dwóch odbiorców: użytkownika i Googlebota. Z jednej strony staraliśmy się o czytelny interfejs dla użytkownika, a z drugiej łatwo indeksowalny HTML dla Googlebota. Pojawienie się kategorii Przeglądanie Agentowe w Lighthouse dodaje trzecią perspektywę: agenta AI, który ma nie tylko przeczytać stronę, ale też zrozumieć strukturę, znaleźć właściwy element i czasem wykonać akcję.

Maciej Sala

Maciej Sala

Founder StriveLab