Jun 21, 2025
9 min

Jak stworzyć serwer MCP? STDIO + Streamable HTTP z Hono

Jak stworzyć serwer MCP? STDIO + Streamable HTTP z Hono

Możliwości serwera MCP są ogromne. MCP umożliwia modelom AI uzyskać dostęp do zasobów na serwerze, zarządzać danymi i integrować się z zewnętrznymi danymi. Deweloper, który potrafi coś takiego stworzyć, może znacząco rozszerzyć możliwości klientów MCP (jak Claude Desktop) dla siebie i swojej firmy.

Ten post powstał dla wersji 2025-03-26 standardu MCP. W najnowszych wersji wprowadzono parę zmian ale poniższe przykłady będą ciągle działać. Pełna aktualizacja wkrótce.

Czym jest protokół MCP (Model Context Protocol)?

Model Context Protocol to otwarty standard stworzony przez firmę Anthropic, która stoi za rodziną modeli Claude. Jest to protokół, który definiuje wspólny interfejs standaryzujący sposób dostarczania kontekstu dla dużych modeli językowych. Dzięki temu możemy podłączać różne narzędzia do modeli LLM. To trochę jak wspólny standard dla wtyczek do Chrome, które działają na każdej przeglądarce Chromium, albo port USB-C.

Klienci MCP

By korzystać z MCP, potrzebny jest klient, który implementuje ten protokół. Najpopularniejszym jest aktualnie Claude Desktop. Oprócz tego mamy klientów w AI IDE, np.: Cursor, Windsurf, GitHub Copilot i inne. Warto również zwrócić uwagę na Alice stworzoną przez Adama Gospodarczyka, znanego jako overment.

A listę wszystkich ciekawych klientów MCP możesz znaleźć na Awesome List na GitHub.

Dlaczego warto stworzyć serwer MCP?

Jeśli korzystasz z Claude Desktop (albo innego klienta), to stworzenie własnego serwera może znacząco podnieść twoją produktywność i wykorzystanie narzędzi. Jest to ustandaryzowany sposób, by dać dostęp klientowi do konkretnych danych. To w połączeniu z innymi ogólnodostępnymi serwerami może przyspieszyć twoją pracę. Jest też kwestia bezpieczeństwa. Własny serwer MCP może być proxy między klientem a poufnymi danymi. Zamiast dawać dostęp do wszystkiego, możesz selekcjonować konkretne dane.

Przykładem zastosowania będzie połączenie danych z Google Drive, Confluence, Slack i własnej hurtowni danych. Do pierwszych trzech są dostępne gotowe serwery, a do danych z hurtowni możesz postawić własny serwer. Szczególnie, że nie jest to trudne. Dzięki temu zyskujesz możliwość analizowania danych ze wszystkich źródeł w jednym miejscu.

Dla deweloperów jest oficjalny zestaw SDK w kilku różnych językach (na dzień 8.06 są dostępne repozytoria dla TypeScript, Python, Java, Kotlin, C#).

Anatomia serwera MCP

Budując serwer MCP, trzeba wiedzieć, z czego można skorzystać do budowania swojego rozwiązania. Aktualnie są wspierane następujące możliwości:

  • Resources - pobieranie danych
  • Tools - dynamiczne operacje
  • Prompts - pobieranie promptów
  • Sampling - do odpytywania LLM przez klienta, może być użyte do stworzenia architektury human-in-the-loop

Trzy pierwsze są aktualnie najczęściej wspierane i poświęcę im więcej uwagi. Do tematu Sampling wrócę w innym wpisie.

Resources

Zachowanie resources w pełni zależy od wykorzystywanego klienta. Claude Desktop wymaga dodawania tych danych bezpośrednio przy zapytaniu. Czyni to Resources mniej użytecznymi w Claude Desktop.

Najbardziej podstawową możliwością dla dowolnego serwera MCP jest dostęp do zasobów. Resources jest podobne do zapytania GET w REST - zwraca dane bez ich modyfikacji z różnych źródeł danych. Mogą to być dane statyczne, rezultat zapytania API, dane z bazy danych, zawartość plików, obrazki i wiele więcej.

By zaimplementować najprostsze pobieranie danych, potrzebujemy podać nazwę oraz URI, pod którym zasób będzie dostępny. Najprościej pokazać to na przykładzie.

TypeScript
1server.resource(
2    'currentDate',
3    "date://currentDate",
4    async (uri) => ({
5        contents: [{
6            uri: uri.href,
7            text: new Date().toDateString()
8        }]
9    })
10)
11

Najważniejsza jest funkcja, która będzie zwracać dane. Możemy zwrócić dwa rodzaje danych - tekst i dane binarne. Tekst nadaje się dla standardowych danych (np. z bazy danych) i JSON-ów, tak jak w przykładzie. Jeśli zwracamy obrazki, pliki PDF, audio lub inne, to musimy wykorzystać dane binarne zakodowane w base64. Bardzo istotne jest, by podać odpowiedni mimeType, który pozwoli klientowi odpowiednio obsłużyć plik.

TypeScript
1server.resource(
2    'catImage', 
3    "image://cat",
4    async (uri) => ({
5        contents: [{
6            uri: uri.href,
7            blob: fs.readFileSync(path.resolve(__dirname, "./cat.jpg")).toString('base64'),
8            mimeType: 'image/jpeg'
9        }]
10    })
11)

Przykłady powyżej są statycznymi danymi. Oprócz tego mamy możliwość stworzenia bardziej dynamicznych zasobów, np.: pobieranie zasobu na bazie konkretnej danej.

TypeScript
1server.resource(
2    'users',
3    new ResourceTemplate("users://{userId}", {
4        list: () => {
5            return {
6                resources: users.map(user => ({
7                    name: `User #${user.id}`,
8                    uri: `users://${user.id}`,
9                    description: `The user with id#${user.id}`,
10                })),
11            }
12        },
13        complete: {
14            userId: () => {
15                return users.map(user => user.id);
16            }
17        }
18    }),
19    async (uri, { userId }) => ({
20        contents: [{
21            uri: uri.href,
22            text: JSON.stringify(users.find(user => user.id === userId))
23        }]
24    })
25)
26

Powyżej jest przykład dynamicznego zasobu, który na bazie id użytkownika zwraca jego dane. Wykorzystujemy tutaj ResourceTemplate, by odpowiednio skonfigurować zasób, ponieważ mamy tutaj kilka rzeczy:

  • "users://{userId}" - ten string będzie się wydawał znajomy wszystkim, co piszą backend. {{userId}} jest unikalnym identyfikatorem, który zostanie wykorzystany do wyboru konkretnego zasobu.
  • list: () => {} - zwraca listę zasobów, która może być wybrana przez użytkownika.
  • complete: {} - obiekt, w którym definiujemy, jakie wartości może przyjąć unikalny identyfikator.

W przypadku funkcji, która obsługuje ten zasób, jako jeden z parametrów dostajemy wartość unikalnego identyfikatora, który zdefiniowaliśmy.

Tools

Narzędzia są najbardziej podstawowym zastosowaniem dla MCP. Z założenia są to akcje, które będą modyfikować stan po stronie serwera. Definiujemy je bardzo podobnie do zasobów. Mamy tutaj 3 elementy:

  • nazwa
  • wymagane dane do walidacji
  • funkcja do obsługi
TypeScript
1server.tool(
2    'addUser',
3    {
4        name: z.string(),
5        email: z.string().email()
6    },
7    async ({ name, email }) => {
8        const newUser = {
9            id: crypto.randomUUID(),
10            name,
11            email
12        }
13        users.push(newUser)
14        return {
15            content: [{
16                type: "resource", resource: {
17                    text: JSON.stringify(newUser),
18                    uri: `users://${newUser.id}`
19                }
20            }]
21        }
22    }
23)
24

W funkcji obsługującej akcje musimy zwrócić dane albo w postaci tekstu, albo informacji o zasobie, gdzie można te informacje znaleźć.

Prompts

Oprócz zasobów i akcji możemy jeszcze zdefiniować prompty, które serwer zwróci użytkownikowi. Możemy definiować od prostych promptów aż do bardziej zaawansowanych przypadków z przekazywaniem danych (podobnie jak w akcjach).

TypeScript
1server.prompt(
2    'tell-joke',
3    "use to generate joke",
4    () => {
5        return {
6            description: "use to generate jokes",
7            messages: [
8                {
9                    role: "user",
10                    content: {
11                        type: "text",
12                        text: "Generate funny joke"
13                    }
14                }
15            ]
16        }
17    }
18)
19

Jak stworzyć własny serwer? Przykłady w kodzie

Aktualnie mamy dwa główne sposoby, by uruchomić serwer: jako STDIO oraz Streamable HTTP. Do niedawna było jeszcze SSE, ale aktualnie jest już niepolecane, więc nie będę omawiać.

Kod serwera

Tutaj nie ma wiele do opisywania, bo tworząc instancję serwera, podajemy tylko nazwę oraz wersję.

TypeScript
1import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
2import { z } from "zod";
3
4const server = new McpServer({
5    name: 'Utils',
6    version: '0.0.1'
7})
8
9// przykłady powyżej
10
11export const getServer = () => server;
12

Taki serwer można wykorzystać zarówno do uruchomienia jako STDIO, jak i jako Streamable HTTP.

Konfiguracja STDIO

Dla STDIO nie musimy za wiele robić poza stworzeniem instancji StdioServerTransport i podpięcia pod serwer. Taki serwer można podpiąć do dowolnego klienta, np.: Claude Desktop.

TypeScript
1const server  = getServer();
2const transport = new StdioServerTransport();
3await server.connect(transport);
4

Konfiguracja Streamable HTTP z Hono

Tu jest trochę więcej zabawy, ponieważ Streamable HTTP zostało zaprojektowane pod Express i wszystkie przykłady pokazują integrację z tą biblioteką. Ja jestem fanem Hono, które daje większe możliwości w kontekście wdrożenia na różnych platformach. By zintegrować z MCP, musimy skorzystać z biblioteki fetch-to-node, która przekształca request z Hono na taki, który rozumie StreamableHTTPServerTransport. Poniższy kod można traktować jako szablon i wykorzystać do dowolnego serwera.

TypeScript
1import { Hono } from 'hono'
2import { getServer } from './server';
3import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4import { toFetchResponse, toReqRes } from "fetch-to-node";
5
6const app = new Hono()
7
8app.post('/mcp', async (c) => {
9  const { req, res } = toReqRes(c.req.raw);
10  const body = await c.req.json();
11  const server = getServer(); 
12  try {
13    const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
14      sessionIdGenerator: undefined,
15    });
16    
17    await server.connect(transport);
18    await transport.handleRequest(req, res, body);
19
20    res.on('close', () => {
21      console.log('Request closed');
22      transport.close();
23      server.close();
24    });
25
26    return toFetchResponse(res);
27  } catch (error) {
28    console.error('Error handling MCP request:', error);
29    if (!res.headersSent) {
30      return c.json({
31        jsonrpc: '2.0',
32        error: {
33          code: -32603,
34          message: 'Internal server error',
35        },
36        id: null,
37      });
38    }
39  }
40})
41
42export default app
43

Po uruchomieniu można testować serwer MCP.

Testowanie serwera MCP

Gotowy serwer warto przetestować. Twórcy protokołu stworzyli narzędzie do testów serwerów MCP (modelcontextprotocol/inspector). Jest to najprostszy sposób na przetestowanie tego protokołu.

Narzędzie można uruchomić z poziomu narzędzia CLI. STDIO:

1bunx @modelcontextprotocol/inspector bun run 12_mcp_server/index.ts   
2
Wygląd interfejsu MCP Inspector dla STDIO
Wygląd interfejsu MCP Inspector dla STDIO

Wystarczy kliknąć Connect i możemy testować serwer.

Streamable HTTP:

1bunx @modelcontextprotocol/inspector
Wygląd interfejsu MCP Inspector dla Streamable HTTP
Wygląd interfejsu MCP Inspector dla Streamable HTTP

Musimy wybrać jako typ Streamable HTTP i wpisać adres serwera.

Resources

MCP Inspector - zakładka Resources
MCP Inspector - zakładka Resources

W przypadku Resources mamy możliwość albo wybrania konkretnego zasobu z listy, albo mamy sekcję Resource Templates, która daje wygodniejszy sposób pobrania danych (tutaj możemy wykorzystać sekcję list i complete z konfiguracji).

Tools

MCP Inspector - zakładka Tools
MCP Inspector - zakładka Tools

Dla narzędzi mamy osobną zakładkę i warto zwrócić uwagę, że automatycznie jest tworzony formularz na bazie wymaganych danych.

Prompts

MCP Inspector - zakładka Prompts
MCP Inspector - zakładka Prompts

Sekcja Prompts jest bardzo podobna do sekcji Resources i mamy dostęp do skonfigurowanych promptów.

Konfiguracja Claude Desktop pod MCP

Warto również przetestować serwer STDIO w Claude Desktop, by mieć pewność, że wszystko działa. Możesz do tego wykorzystać darmowe konto Claude. Aby dodać nowy serwer MCP, musisz wejść w Settings > Developer > Edit Config. Otworzy się wtedy lokalizacja pliku konfiguracyjnego claude_desktop_config.json, do którego musisz dodać poniższy wpis:

JSON
1"mcpServers": {
2    "local": {
3      "command": "/Users/olek/.bun/bin/bun",
4      "args": ["run", "/Users/olek/Documents/prywatne/ai-engineer-101/12_mcp_server/index.ts"]
5    }
6  }
7

Teraz musisz zrestartować aplikację i możesz testować swój serwer.

Claude Desktop - Resources
Claude Desktop - Resources

Resources są dostępne, jeśli bezpośrednio dodasz do zapytania (osobiście uważam to za wadę)

Claude Desktop - Tools
Claude Desktop - Tools

Narzędzia są dostępne w tle i jeśli chat zauważy, że może je wykorzystać, to zrobi to, o ile nie wyłączymy danego narzędzia.

Co dalej?

MCP daje jeszcze większe możliwości dla konfiguracji serwera. Istotnym tematem jest autoryzacja, którą omówię w osobnym wpisie, oraz sampling, który umożliwia implementację human-in-the-loop. Ten standard też się cały czas rozwija, więc pewnie w przyszłości dostaniemy nowe możliwości. A na razie warto eksperymentować z aktualnymi.

Czytaj więcej

MCP + AI SDK - jak to zintegrować i czy ma to sens?
Jun 21, 2025
Co to jest MCP? I jak zintegrować serwer MCP z AI SDK?
MCP jest równie gorącym tematem w bańce AI jak architektura agentowa. Ale czy warto się tym interesować? I jak można to wdrożyć z pomocą AI SDK od Vercel'a?
jak budować PoC dla aplikacji AI
Jun 21, 2025
Jak budować PoC aplikacji AI?
Pomyłki przy budowaniu aplikacji AI są KOSZTOWNE. Można temu zapobiec dodając etap prototypowania przed rozpoczęciem dużych prac.
Halucynacje AI  - skąd się biorą i jak je ograniczać?
Jun 21, 2025
Halucynacje AI, czyli dlaczego sztuczna inteligencja kłamie?
Modele LLM potrafią tworzyć wiarygodne informacje, które są kłamstwem. Zjawisko halucynacji AI jest poważnym problemem i wyzwaniem dla programistów aplikacji AI