Stwórz serwer MCP do dziennika pracy z możliwością wyszukiwania w 190 liniach kodu TypeScript
Mały serwer MCP do plików Markdown: cztery narzędzia oparte na intencjach, opisy w stylu promptów, klient demonstracyjny stdio służący jednocześnie jako test, oraz prawdziwe pułapki.
Źródła: I Built an MCP Server That Keeps My Work Journal — Here’s Everything I Learned
Server protokołu Model Context Protocol składający się z około 190 linijek kodu napisanego w TypeScript może przekształcić folder plików Markdown w dziennik pracy nadający się do wyszukiwania, który może czytać i edytować każdy asystent AI. Jest to również dobry pierwszy projekt typu MCP i pokazuje, jak zaprezentować server bez edytora.
Dlaczego dziennik?
Nawyki codziennego zapisywania informacji rzadko przetrwają więcej niż tydzień. Tymczasem asystent AI, który naprawił problem warunków konkurencyjnych, współstworzył połowę testów i obserwował każdą modyfikację, już wie, co wydarzyło się w ciągu dnia. Dyscyplina nigdy nie była prawdziwą przeszkodą. Zapisy znajdują się gdzie indziej niż w miejscu pracy.
Odwróćmy kolejność: zamiast kazać osobie prowadzić dziennik, dajmy asystentowi narzędzia do jego utrzymywania. MCP, standard umożliwiający klientom LLM odkrywanie i wywoływanie zewnętrznych narzędzi, pasuje tu idealnie. Serwer dev-diary, który obejmuje zwykłą strukturę katalogów z plikami Markdown, realizuje wszystkie podstawowe koncepcje (narzędzia, schematy, transport stdio, przechowywanie bez stanu) w mniej niż 200 liniach kodu, co czyni go odpowiednio dobrą platformą do nauki.
Sam dziennik nie jest celem. Aplikacje do prowadzenia dzienników już istnieją. Temat jest prosty, nudny i powszechnie zrozumiały, co nadaje się do projektu edukacyjnego. Kluczowe pytanie brzmi, czy ktokolwiek może stworzyć serwer MCP do dowolnego celu. Tak, przy czym protokół okazuje się być łatwą częścią. Trudniej jest zdecydować, co obejmie ten serwer. Rejestr treningów, lista lektur, wewnętrzna wiki czy inteligentny dom mogą być obsługiwane przez serwer MCP, o ile kod może je czytać lub zapisywać.
Cztery narzędzia
| Narzędzie | Cel |
|---|---|
log_work | Dodaje zaznaczoną datą i tagiem wpis do dzisiejszego pliku |
search_diary | Wyszukiwanie pełnego tekstu we wszystkich wpisach |
daily_summary | Wszystko, co zostało zapisane w wybranej dacie |
stats | Sumy, serie i najpopularniejsze tagi |
Przechowywanie jest celowo proste: jeden plik Markdown na dzień, zawierający takie wpisy.
## 14:32 #bugfix #websocket
Fixed the race condition in the WebSocket broadcast queue
## 16:10 #testing
Wrote E2E test covering two-client sync
Nie ma bazy danych. Pliki można przeszukać za pomocą grepa, zapisać oraz edytować ręcznie, a taka decyzja przynosi korzyści później.
Budowa serwera
Bazą jest oficjalne SDK TypeScript (wersja 2, które implementuje specyfikację z 2026-07-28). Do instalacji potrzebne są pakiet serwera oraz Zod:
npm install @modelcontextprotocol/server zod
Importy wprowadzają klasę serwera, transport stdio oraz punkt wejścia v4 biblioteki Zod:
import { McpServer } from '@modelcontextprotocol/server';
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';
Obiekt serwera przyjmuje nazwę i wersję:
const server = new McpServer({ name: 'dev-diary', version: '1.0.0' });
Każde narzędzie jest zarejestrowane pod nazwą, z konfiguracją oraz obsługownikiem. Oto log_work:
server.registerTool(
'log_work',
{
description: 'Append a timestamped entry to the developer diary...',
inputSchema: z.object({
text: z.string().min(1),
tags: z.array(z.string()).optional(),
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
}),
},
async ({ text, tags = [], date }) => {
// ...append to diary/YYYY-MM-DD.md...
return { content: [{ type: 'text', text: 'Logged.' }] };
},
);
Ostatni krok to połączenie przez stdio:
await server.connect(new StdioServerTransport());
To jest kompletny serwer. Opierają się na nim cztery koncepcje:
registerTool(name, config, handler)tworzy katalog narzędzi.inputSchemato schemat Zod. SDK konwertuje go na JSON Schema dla klienta i weryfikuje argumenty przed uruchomieniem obsługi.- Wartość zwracana, w formie
{ content: [{ type: 'text', text: '...' }] }, jest tym, co model LLM odczytuje jako wynik. - Transport,
StdioServerTransport(), umożliwia klientowi uruchomienie procesu i komunikację w formacie JSON-RPC przez stdin i stdout. Do użycia lokalnego nie są potrzebne żadne porty ani autoryzacja.
Opisy to instrukcje
To największa zmiana w sposobie myślenia. Opis narzędzia to nie dokumentacja API dla ludzi – to instrukcja, którą model odczytuje, aby zdecydować, kiedy i jak użyć narzędzia. Porównaj to z opisem w stylu dokumentacji:
// ❌ documentation-style
description: 'Appends an entry to the diary.'
z wersją w stylu promptu:
// ✅ prompt-style
description: 'Append a timestamped entry to the developer diary for today.
Use this whenever the user says they finished/did/fixed something and
wants it recorded.'
Drugi element informuje model, kiedy ma użyć danego narzędzia. Jego klauzula „użyj tego zawsze, gdy” przekształca przypadekowe stwierdzenie o wreszcie naprawieniu niestabilnego testu w wezwanie narzędzia, bez konieczności podawania wyraźnego polecenia. Opisy parametrów działają w ten sam sposób: wskazówka taka jak Optional labels, e.g. ["bugfix","api"] pokazuje poprawną formę poprzez przykład.
Narzędzia odpowiadają pytaniom, a nie operacjom
Pierwszy szkic z narzędziami CRUD (add_entry, update_entry, delete_entry) ma niewłaściwą strukturę. Nikt nie prosi dziennika o aktualizację wpisu numer 47 – pytają raczej o to, co stało się z błędem WebSocket z zeszłego tygodnia. Dlatego ostateczna wersja odpowiada zamierzeniom: search_diary znajduje informacje, daily_summary podsumowuje dzień, stats dostarcza motywujący przegląd, a log_work pełni rolę jedynego narzędzia do zapisywania. Żadne z czterech narzędzi się nie pokrywa i każde odpowiada na pytanie, które rzeczywiście zadaje programista. Zasada ogólna: jedno narzędzie na jedno pytanie użytkownika, a nie jedno na każdą operację nad danymi.
Demontaż bez edytora
Trudniejszym problemem jest pokazanie, jak działa serwer MCP. Połączenie Claude Desktop z robieniem skrínów rozmowy nie dowodzi niczego powtarzalnego, zależy od zamkniętej aplikacji i nie może być uruchomione w środowisku CI ani na komputerze czytelnika.
Cicha siła specyfikacji pomaga w tym przypadku: MCP to po prostu klient i serwer przez stdio. Nic nie przeszkadza drugiemu małemu programowi w połączeniu się w sposób podobny do edytora i obsłudze całego procesu po stronie serwera. Jego importy:
import { Client } from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
Tworzy serwer jako proces potomny:
const transport = new StdioClientTransport({
command: 'node',
args: ['dist/server.js'], // spawns the server as a child process
});
const client = new Client({ name: 'demo-client', version: '1.0.0' });
await client.connect(transport);
Następnie robi to, co robi kliencto stacjonarne w tle – wyświetla narzędzia i je uruchamia:
// Exactly what Claude Desktop does under the hood:
const { tools } = await client.listTools();
await client.callTool({ name: 'log_work', arguments: {
text: 'Fixed the race condition in the broadcast queue',
tags: ['bugfix', 'websocket'],
}});
Około pięćdziesiąt wierszy później npm run demo wyświetla pełną sesję protokołu jako dowód. Lista narzędzi:
=== 1. listTools ===
• log_work — Append a timestamped entry to the developer diary...
• search_diary — Full-text search across every entry...
• daily_summary — Everything logged on a given date...
• stats — Totals, active days, streaks, top tags...
Trzy wpisy:
=== 2. log_work x3 ===
Logged to 2026-08-22.md at 19:05 (tags: bugfix, websocket)
...
A widok statystyk:
=== 5. stats ===
📊 1 entries across 2 day(s)
🔥 Streak: 2 consecutive day(s)
🏷️ Top tags: #bugfix (1), #websocket (1)
Ponieważ jest to skrypt, pełni również funkcję testu integracyjnego. Po każdej zmianie na serwerze uruchamia się demo, a błędny proces nawiązywania połączenia lub zmienione schemat powodują wyraźny błąd. Demo, dokumentacja oraz zestaw testów tworzą jedno całość. Aby uzyskać wizualny interfejs, oficjalny Inspector, uruchamiany za pomocą npx @modelcontextprotocol/inspector node dist/server.js, otwiera interfejs internetowy umożliwiający ręczną obsługę narzędzi. Razem te dwa elementy obejmują zarówno proces rozwoju, jak i demonstrację.
Uwagi
- Użycie await na najwyższym poziomie może zakłócić proces budowania aplikacji. Dokumentacja SDK zawiera instrukcję
await server.connect(...)na poziomie modułu. Działa to poprawnie dla pakietów ESM ("type": "module"), natomiast powoduje poważny błąd podczas kompilacji TypeScript do CommonJS. Umieszczenie kodu startowego w funkcjimain()nie kosztuje nic i działa wszędzie. exactOptionalPropertyTypeskoliduje z schematami opcjonalnymi. W ścisłym trybie TypeScript przekazanietags: undefineddo polaz.string().optional()powoduje błędy. Należy złagodzić to ustawienie lub znormalizować argumenty na granicy obsługi.- Opisy zajmują zasoby kontekstowe. Każdy opis narzędzia trafia do modelu w każdej rozmowie, w której jest połączony serwer. Teksty marketingowe marnują tokeny i rozcieńczają użyteczne instrukcje, dlatego lepsze są krótkie, rozkazujące teksty skupione na celu.
- Brak stanu jest pomocny. Serwer nie przechowuje nic w pamięci i ponownie czyta Markdown przy każdym wywołaniu. Ręczne edycje pomiędzy wywołaniami nigdy nie generują przestarzałych danych, a dwa klienty w jednej folderze mogą dzielić się aktualną informacją bez kosztów. Odraczaj użycie cache’ów w narzędziach opartych na plikach, dopóki analiza wydajności tego nie uzasadni.
- Strefy czasowe mają wpływ nawet w grze.
new Date().toISOString().slice(11,16)zwraca wartość w formacie UTC. Osoba znajdująca się w strefie UTC-3 i logująca się o 18:00 zobaczy godzinę 21:05. Problem można rozwiązać jedną linijką kodu po jego zauważeniu.
Konfiguracja
W Claude Desktop, Cursor lub dowolnym klientze obsługującym MCP, konfiguracja (w formacie claude_desktop_config.json) wygląda następująco:
{
"mcpServers": {
"dev-diary": {
"command": "node",
"args": ["/absolute/path/to/dev-diary-mcp/dist/server.js"],
"env": { "DIARY_DIR": "/home/you/journal" }
}
}
}
Ustaw zmienną DIARY_DIR na folder w repozytorium plików typu dotfiles, a jako efekt uboczny powstaną same wersje dziennika. Po tygodniu prośba taka jak „streszcz moją tydzień z dziennika, pogrupowany według tagów” generuje raport stanu, którego nikt nie musiał pisać.
Dlaczego zwykły Markdown
Najbardziej kontrowersyjny wybór zasługuje na uzasadnienie. SQLite oferuje lepsze zapytania, ale ma nieprzejrzyste przechowywanie danych, natomiast JSON jest łatwy do parsowania, ale kiepsko radzi sobie z porównywaniem różnic. Markdown wygrał z czterech powodów:
- Człowiek to klient pierwszej klasy. Dziennik czyta się dobrze w edytorze, za pomocą grep lub w logach git, bez konieczności używania dodatkowych narzędzi.
- Git zapewnia kopie zapasowe i historię zmian.
git log diary/to ślad audytowy, którego żadna baza danych nie dostarcza domyślnie. - LLM bardzo dobrze radzą sobie z Markdowniem. To, co pisze model, odpowiada temu, co jest przechowywane, bez żadnych niezgodności w serializacji.
- Błędy są łagodne. Uszkodzony wiersz jest widoczny i można go naprawić ręcznie, czego nie da się zrobić w przypadku uszkodzonej strony SQLite.
Lekcja ta wykracza poza dzienniki: gdy zarówno ludzie, jak i modele językowe przetwarzają twoje dane, przechowuj je w formacie, którym obie strony już się posługują.
Uruchamianie
Klonuj, skompiluj i uruchom demo:
git clone https://github.com/rogeriolaa/dev-diary-mcp
cd dev-diary-mcp && npm install && npm run build && npm run demo
Następnie dodaj serwer do konfiguracji MCP edytora i pozwól asystentowi rozpocząć logowanie. Serwer, klient demonstracyjny oraz plik README razem stanowią około 400 linii, wliczając konfigurację.
Takie małe serwery to skuteczny sposób na naukę MCP: omawia się każdą koncepcję, wyniki są wykorzystywane codziennie, a zakres tematyczny pozostaje na tyle mały, by można go w pełni zrozumieć.