Przejdź do głównej zawartości

Tworzenie własnego serwera MCP

Własny serwer MCP to mały program, który udostępnia wewnętrzne API, bazę danych albo CLI jako narzędzia, które mogą wywoływać Claude Code, Codex i Cursor. We wrześniu 2026 budujesz go na SDK v2 dla TypeScriptu (@modelcontextprotocol/server) albo na pakiecie PyPI mcp 2.x i uruchamiasz punktem wejścia, który obsługuje zarówno specyfikację MCP 2026-07-28, jak i starszych klientów.

Ta strona jest dla programisty, który odpowiada za wewnętrzny system, i dla tech leada, który decyduje, czy serwer trafi do zespołu. Flagi funkcji siedzą za wewnętrznym API, historia wdrożeń leży w usłudze, o której nikt spoza firmy nie słyszał, a agent nie sięga ani do jednego, ani do drugiego. Wklejasz więc JSON do czatu i to ty jesteś integracją. Własny serwer usuwa ten krok. Trudny nie jest protokół, bo ten obsługuje SDK. Trudny jest kontrakt narzędzia, którego model używa poprawnie, i dowód, że tak jest.

  • Działający serwer na @modelcontextprotocol/server 2.1.0, uruchamiany przez serveStdio, który odpowiada zarówno klientowi negocjującemu spec 2026-07-28 (Claude Code robi to domyślnie z serwerami HTTP), jak i klientowi, który nadal używa starszego protokołu (Codex 0.157.1).
  • Skill mcp-builder zainstalowany w twoim agencie i prompt, który każe mu generować szkielet na SDK v2 zamiast na przykładach v1, z którymi przychodzi.
  • Plik testów i dwa polecenia MCP Inspectora, które dowodzą kontraktu narzędzi w obu erach protokołu, więc nikt nie musi recenzować serwera linijka po linijce.
  • Rejestrację w Claude Code, Codeksie i Cursorze oraz pomiar kosztu kontekstu przed i po.
  • Trzy sposoby wydania (prywatny npm, Streamable HTTP dla zespołu, publiczny MCP Registry przez mcp-publisher) i listę kontrolną do zatwierdzenia.

Sprawdzone 2026-09-26 na @modelcontextprotocol/server 2.1.0, @modelcontextprotocol/node 2.1.0, @modelcontextprotocol/express 2.0.1, PyPI mcp 2.2.0, @modelcontextprotocol/inspector 2.8.0, Claude Code 2.1.283 i codex-cli 0.157.1. Każdy blok kodu na tej stronie został skompilowany i uruchomiony na tych wersjach. W środowisku testowym nie było binarki Cursora, więc kroki dla Cursora nie są zweryfikowane.

Buduj serwer tylko wtedy, gdy agent potrzebuje dostępu na żywo do czegoś, do czego inaczej nie sięgnie. Wiele próśb „potrzebujemy serwera MCP” to w rzeczywistości brakująca instrukcja albo brakujący skrypt.

Agent potrzebuje…ZbudujDlaczego
Danych lub akcji na żywo za uwierzytelnieniem: flagi funkcji, historia wdrożeń, system incydentówSerwer MCPTypowane wejścia, jedno poświadczenie trzymane przez serwer, działa w każdym agencie
Procedury: jak pisać migrację, wasza lista kontrolna wydaniaSkill (tworzenie własnych skilli)Kosztuje kontekst tylko wtedy, gdy się uruchamia; nie ma procesu do utrzymania
Polecenia, które już istnieje: kubectl, gh, cele makeNic nowego albo skill opisujący polecenieAgent już uruchamia polecenia powłoki; serwer dodaje drugą drogę do tego samego
Czegoś, co robi gotowy serwer: GitHub, Postgres, SentryZainstaluj ten serwer (najlepsze serwery MCP)Nie chcesz utrzymywać cudzego wrappera API

Tech leadzi, którzy podejmują tę decyzję za cały zespół, powinni przeczytać wewnętrzne serwery MCP: buduj tylko brakujący interfejs, gdzie są opisane odpowiedzialność za serwer i kryteria jego wycofania.

Zainstaluj skill mcp-builder i pozwól agentowi wygenerować szkielet

Dział zatytułowany „Zainstaluj skill mcp-builder i pozwól agentowi wygenerować szkielet”

Skill mcp-builder od Anthropic prowadzi agenta przez research, implementację, testy w MCP Inspectorze i zestaw pytań ewaluacyjnych. Działa we wszystkich trzech agentach przez CLI skills (1.7.0).

Okno terminala
npx skills add anthropics/skills --skill mcp-builder -a claude-code

Możesz też zainstalować go jako część pluginu: /plugin marketplace add anthropics/skills, a potem /plugin install example-skills@anthropic-agent-skills. Ten plugin instaluje jeszcze 11 innych przykładowych skilli, więc wybierz instalację pojedynczego skilla.

Porównaj to, co wygenerował agent, z tym wzorcem. To najmniejszy serwer, który poprawnie wykonuje zadanie, z odczytem flag w osobnym module, żeby łatwo podmienić go na prawdziwe wywołanie API.

  1. Utwórz projekt i zainstaluj SDK.

    Okno terminala
    mkdir feature-flags-mcp && cd feature-flags-mcp
    npm init -y && npm pkg set type=module
    npm install @modelcontextprotocol/server zod
    npm install -D typescript @types/node vitest @modelcontextprotocol/client

    v2 wymaga Node.js 20 lub nowszego i Zoda 4.2 lub nowszego. Z Zodem 4.0 albo 4.1 SDK przełącza się na własny konwerter i gubi twoje opisy z .describe() w schemacie, który widzi model.

  2. Napisz fabrykę serwera w src/build-server.ts.

    src/build-server.ts
    import { McpServer } from '@modelcontextprotocol/server';
    import { z } from 'zod';
    import { getFeatureFlags } from './flags.js';
    export function buildServer() {
    const server = new McpServer({ name: 'feature-flags', version: '1.0.0' });
    server.registerTool(
    'get_feature_flags',
    {
    description:
    'Return the feature flags that are ON in one environment. Use before changing code behind a flag.',
    inputSchema: z.object({
    environment: z.enum(['dev', 'staging', 'production']).describe('Deployment environment'),
    }),
    annotations: { readOnlyHint: true },
    },
    async ({ environment }) => {
    try {
    const flags = await getFeatureFlags(environment);
    return { content: [{ type: 'text', text: JSON.stringify(flags, null, 2) }] };
    } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    return { isError: true, content: [{ type: 'text', text: `Flag lookup failed: ${message}` }] };
    }
    },
    );
    return server;
    }

    src/flags.ts eksportuje getFeatureFlags(environment), które wywołuje wasze API flag. Osobny moduł pozwala testom go podmienić.

  3. Uruchom serwer w src/server.ts.

    src/server.ts
    #!/usr/bin/env node
    import { serveStdio } from '@modelcontextprotocol/server/stdio';
    import { buildServer } from './build-server.js';
    serveStdio(buildServer);

    serveStdio przypina jedną instancję serwera do każdego połączenia. Odpowiada na otwarcie w erze 2026-07-28 i, domyślnie, na initialize z ery 2025, więc ta sama binarka działa z klientem w każdej z dwóch er protokołu.

  4. Dodaj tsconfig.json i zbuduj projekt.

    tsconfig.json
    {
    "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "types": ["node"]
    },
    "include": ["src"]
    }
    Okno terminala
    npx tsc

    Build tworzy dist/server.js, na który wskazuje każda konfiguracja klienta poniżej.

Trzy szczegóły decydują o tym, czy model dobrze używa narzędzia. Opis to jedyne, co model czyta przy wyborze narzędzia, więc napisz, co zwraca i kiedy go wywołać. z.enum zamienia zgadywanie („prod”?) w błąd walidacji, który model potrafi poprawić. A isError: true pozwala modelowi zobaczyć porażkę i zareagować, zamiast dostać pustą treść i wymyślić odpowiedź.

Udowodnij, że serwer działa, bez czytania każdej linii

Dział zatytułowany „Udowodnij, że serwer działa, bez czytania każdej linii”

Pytanie przy przeglądzie serwera MCP nie brzmi „czy ten kod jest ładny?”, tylko „czy model dostaje właściwe narzędzia i czy każde narzędzie bezpiecznie zawodzi?”. Odpowiadają na nie trzy sprawdzenia, które trwają sekundy.

1. Sprawdź kontrakt w obu erach protokołu. CLI MCP Inspectora łączy się starszym protokołem, jeśli nie powiesz inaczej, więc uruchom je dwa razy. Polecenie serwera podaj najpierw: --tool-arg przyjmuje wiele wartości i połyka wszystko, co stoi za nim.

Okno terminala
npx @modelcontextprotocol/inspector --cli node dist/server.js --protocol-era modern --method tools/list --strict
npx @modelcontextprotocol/inspector --cli node dist/server.js --protocol-era legacy \
--method tools/call --tool-name get_feature_flags --tool-arg environment=staging

--strict zgłasza problemy z przenośnością schematów narzędzi i kończy się kodem 6, jeśli któryś jest błędem, więc nadaje się na bramkę w CI. npx @modelcontextprotocol/inspector bez --cli otwiera interfejs w przeglądarce.

2. Przetestuj kontrakt narzędzi w procesie. createMcpHandler obsługuje 2026-07-28 przez funkcję fetch, więc test może sterować prawdziwym serwerem bez gniazda i bez procesu potomnego:

test/server.test.ts
import { describe, expect, it } from 'vitest';
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
import { createMcpHandler } from '@modelcontextprotocol/server';
import { buildServer } from '../src/build-server.js';
async function connect() {
const handler = createMcpHandler(buildServer);
const transport = new StreamableHTTPClientTransport(new URL('http://test.local/mcp'), {
fetch: (url, init) => handler.fetch(new Request(url, init)),
});
const client = new Client({ name: 'test', version: '0.0.0' }, { versionNegotiation: { mode: { pin: '2026-07-28' } } });
await client.connect(transport);
return client;
}
describe('feature-flags MCP server', () => {
it('exposes exactly the tools the agent should see', async () => {
const { tools } = await (await connect()).listTools();
expect(tools.map((t) => t.name)).toEqual(['get_feature_flags']);
});
it('rejects an unknown environment as a tool error, not a crash', async () => {
const result = await (await connect()).callTool({ name: 'get_feature_flags', arguments: { environment: 'qa' } });
expect(result.isError).toBe(true);
});
});

Pierwszy test robi się czerwony, gdy ktoś niechcący doda narzędzie zapisujące. Drugi, gdy zepsuje się walidacja albo obsługa błędów. Dodaj po jednym teście ścieżki szczęśliwej na każde narzędzie, z podmienionym getFeatureFlags.

3. Oceń, czy model z niego korzysta. Testy dowodzą kontraktu, ale nie dowodzą, że model wybiera właściwe narzędzie. mcp-builder ma fazę ewaluacji, w której powstaje 10 realistycznych pytań wymagających twoich narzędzi. Uruchamiaj je w świeżej sesji po każdej zmianie opisu lub schematu i trzymaj plik z pytaniami w repozytorium.

Używaj ścieżki bezwzględnej: klienci uruchamiają serwer z własnego katalogu roboczego, a nie z twojego projektu. Sekrety przekazuj jako zmienne środowiskowe z powłoki albo menedżera sekretów, nigdy jako wartości wpisane w commitowany plik.

Okno terminala
claude mcp add feature-flags -e FLAGS_API_URL="$FLAGS_API_URL" -e FLAGS_API_TOKEN="$FLAGS_API_TOKEN" \
-- node /abs/path/feature-flags-mcp/dist/server.js
claude mcp get feature-flags

claude mcp get sprawdza stan serwera i wypisuje Connected, gdy proces startuje i odpowiada. Domyślny zakres to local (tylko ty, tylko ten projekt). Użyj -s project, żeby zapisać .mcp.json dla zespołu; współpracownicy zatwierdzają go wtedy raz. .mcp.json trafia do repozytorium, więc w tym przypadku nie przekazuj tokenu przez -e, tylko ustaw go w środowisku każdego inżyniera. Przy wolnym systemie źródłowym zwiększ MCP_TOOL_TIMEOUT (w milisekundach) w środowisku, z którego uruchamiasz Claude Code.

Sprawdź koszt kontekstu. Każdy schemat narzędzia siedzi w kontekście agenta. W Claude Code uruchom /context przed dodaniem serwera i po nim i porównaj wiersz narzędzi MCP. Claude Code i Codex domyślnie odraczają schematy narzędzi przez wyszukiwanie narzędzi (tool search), więc jeden mały serwer kosztuje niewiele, ale koszt rośnie z każdym narzędziem. Dziesięć wąskich narzędzi to gorszy wybór niż trzy dobrze opisane. Zobacz ograniczanie kosztu tokenów MCP.

Narzędzie to granica wejścia: argumenty wybiera model, a może je wybrać także prompt injection ukryty w czymś, co agent przeczytał wcześniej. Dwa wzorce pokrywają większość wewnętrznych serwerów.

src/ops-tools.ts
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import type { McpServer } from '@modelcontextprotocol/server';
import { z } from 'zod';
const execFileAsync = promisify(execFile);
export function registerOpsTools(server: McpServer) {
server.registerTool(
'list_deployments',
{
description: 'List the most recent deployments of one service, newest first.',
inputSchema: z.object({
service: z.string().regex(/^[a-z0-9-]{1,64}$/),
limit: z.number().int().min(1).max(50).default(10),
}),
annotations: { readOnlyHint: true },
},
async ({ service, limit }) => {
const url = new URL('/deployments', process.env.DEPLOY_API_URL);
url.searchParams.set('service', service);
url.searchParams.set('limit', String(limit));
const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.DEPLOY_API_TOKEN}` } });
if (!res.ok) {
return { isError: true, content: [{ type: 'text', text: `Deploy API returned HTTP ${res.status} for ${service}` }] };
}
return { content: [{ type: 'text', text: JSON.stringify(await res.json(), null, 2) }] };
},
);
server.registerTool(
'kubectl_get',
{
description: 'Read Kubernetes resources as JSON. Read-only: never creates, edits or deletes.',
inputSchema: z.object({
resource: z.enum(['pods', 'services', 'deployments', 'ingresses']),
namespace: z.string().regex(/^[a-z0-9-]{1,63}$/).default('default'),
}),
annotations: { readOnlyHint: true },
},
async ({ resource, namespace }) => {
try {
const { stdout } = await execFileAsync('kubectl', ['get', resource, '-n', namespace, '-o', 'json'], {
timeout: 15_000,
maxBuffer: 5 * 1024 * 1024,
});
return { content: [{ type: 'text', text: stdout }] };
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return { isError: true, content: [{ type: 'text', text: `kubectl failed: ${message}` }] };
}
},
);
}

Bezpieczne czyni je kształt, a nie komentarze. URL i searchParams kodują wejście, zamiast doklejać je do ścieżki. execFile z tablicą argumentów nie uruchamia powłoki, a z.enum i wyrażenie regularne są listą dozwolonych wartości. Serwer trzyma jeden token konta usługowego o wąskim zakresie; agent nigdy go nie widzi. Wywołaj registerOpsTools(server) w buildServer(), żeby testy objęły również te narzędzia.

Zacznij od narzędzi tylko do odczytu. Narzędzie zapisujące dodawaj dopiero wtedy, gdy zatwierdzi je recenzent, daj mu własne wąskie wejście i nie ustawiaj readOnlyHint, żeby klienci pytali o zgodę. Modelowanie zagrożeń opisuje strona bezpieczeństwo MCP.

Serwery MCP udostępniają trzy prymitywy. Większości wewnętrznych serwerów wystarczą narzędzia.

PrymitywRejestrowany przezDo czegoZachowanie klientów
Narzędzie (tool)registerToolWszystko, co model sam postanawia wywołać: odczyty, wyszukiwania, akcjeObsługiwane przez wszystkich trzech agentów
Zasób (resource)registerResource (v2 wymaga argumentu z metadanymi; przekaż {}, jeśli ich nie masz)Dokumenty, które dołącza użytkownik: runbooki, schematyKlienci różnią się tym, jak zasoby trafiają do modelu; sprawdź każdego agenta, zanim zaczniesz na tym polegać
PromptregisterPromptPowtarzalny przepływ, który użytkownik uruchamia z nazwy, np. szablon postmortemuKlienci różnią się tym, jak pokazują prompty; często lepiej sprawdzi się skill

W razie wątpliwości zrób z tego narzędzie. Model może wywołać narzędzie sam; zasób albo prompt zwykle musi wybrać człowiek.

Serwer stdio działa na maszynie każdego inżyniera, z poświadczeniami każdego inżyniera. Gdy kilka osób albo zadanie CI ma współdzielić jeden serwer z jednym kontem usługowym, wystaw Streamable HTTP. createMcpHandler obsługuje 2026-07-28 i, domyślnie, bezstanowe żądania z ery 2025 z tego samego endpointu.

src/http.ts
import { createMcpExpressApp } from '@modelcontextprotocol/express';
import { toNodeHandler } from '@modelcontextprotocol/node';
import { createMcpHandler } from '@modelcontextprotocol/server';
import { buildServer } from './build-server.js';
const handler = createMcpHandler(buildServer);
const app = createMcpExpressApp({ host: '0.0.0.0', allowedHosts: ['flags-mcp.internal.example.com'] });
app.all('/mcp', toNodeHandler(handler));
app.listen(3000, '0.0.0.0');

Do tego zainstaluj adaptery oraz @types/express jako zależność deweloperską, żeby tsc skompilował src/http.ts: npm install @modelcontextprotocol/express @modelcontextprotocol/node express && npm install -D @types/express. allowedHosts odrzuca każdy inny nagłówek Host kodem HTTP 403, co blokuje DNS rebinding. To nie jest uwierzytelnianie: zanim ktokolwiek spoza twojego laptopa dostanie się do endpointu, postaw go za proxy świadomym tożsamości albo bramką, albo dodaj weryfikację tokenu bearer. Serwer tworzy świeżą instancję na każde żądanie, więc nie ma stanu sesji, który zginie, gdy load balancer przeniesie żądanie.

Zamiast polecenia zarejestruj URL: claude mcp add --transport http feature-flags https://flags-mcp.internal.example.com/mcp albo codex mcp add feature-flags --url https://flags-mcp.internal.example.com/mcp --bearer-token-env-var FLAGS_MCP_TOKEN.

Serwer, który już działa na SDK v1 albo trzyma stan sesji, wymaga migracji, a nie przepisania od zera: zobacz MCP 2026-07-28: co się zmieniło i jak zmigrować serwer.

Wydaj serwer: prywatny npm, wspólny serwer HTTP albo publiczny MCP Registry

Dział zatytułowany „Wydaj serwer: prywatny npm, wspólny serwer HTTP albo publiczny MCP Registry”
OdbiorcyWydaj jakoKlienci uruchamiają
Twoja firmaPakiet w prywatnym rejestrze npm, z wpisem binnpx -y @acme/feature-flags-mcp
Twoja firma, jedno wspólne poświadczenieSerwer HTTP z poprzedniej sekcji w kontenerze za waszą bramkąURL
WszyscyPubliczny pakiet npm plus wpis w oficjalnym MCP Registrynpx -y z twoim pakietem, znalezionym przez klientów obsługujących rejestr

Oficjalny MCP Registry przechowuje tylko metadane, więc pakiet musi najpierw być publiczny. Rejestr to nadal wersja preview (API zamrożone na v0.1 od 2025-10-24; możliwe są zmiany niekompatybilne i resety danych). 2026-09-26 zawierał 36 176 serwerów w version=latest (API rejestru, pełna paginacja). Nigdy nie publikuj tam wewnętrznego serwera; do kuratorowania wewnętrznych serwerów prowadź prywatny katalog, jak opisuje rejestry, allowlisty i bramki MCP.

  1. Dodaj mcpName do package.json. Przy uwierzytelnianiu przez GitHub musi zaczynać się od io.github.<twój-użytkownik-lub-organizacja>/.

    package.json (fragment)
    {
    "name": "@acme/feature-flags-mcp",
    "version": "1.0.0",
    "mcpName": "io.github.acme/feature-flags",
    "type": "module",
    "bin": { "feature-flags-mcp": "dist/server.js" },
    "files": ["dist"]
    }
  2. Opublikuj pakiet: npm publish --access public.

  3. Zainstaluj publisher i utwórz server.json.

    Okno terminala
    brew install mcp-publisher
    mcp-publisher init

    Ustaw name na tę samą wartość co mcpName, a oba pola version na wersję opublikowaną w npm.

  4. Zwaliduj, zaloguj się i opublikuj.

    Okno terminala
    mcp-publisher validate
    mcp-publisher login github
    mcp-publisher publish

claude mcp serve uruchamia samo Claude Code jako serwer MCP na stdio, dzięki czemu inny klient MCP może korzystać z jego narzędzi. To nie jest sposób na zbudowanie własnego serwera, a Codex 0.157.1 nie ma odpowiednika. Sięgnij po to, gdy narzędzie, którego już używasz, obsługuje MCP i ma korzystać z narzędzi plikowych i powłoki Claude Code; działa ono z uprawnieniami, na które pozwala dana konfiguracja Claude Code.

Klient pokazuje serwer jako niedziałający albo połączony bez narzędzi. Coś pisze na stdout. Przy stdio stdout jest kanałem protokołu. Klienci na SDK v2 pomijają linie, które nie są JSON-em, ale nie kontrolujesz, który klient się połączy, więc przenieś wszystkie logi na stderr (console.error) i sprawdź zależności, które wypisują bannery. Potem uruchom polecenie Inspectora z tools/list z tej strony.

Codex widzi narzędzia, a Claude Code nie (albo odwrotnie). Serwer odpowiada tylko w jednej erze protokołu. Serwer v2 uruchomiony przez server.connect(new StdioServerTransport()) obsługuje tylko protokół z ery 2025. Przełącz się na serveStdio(buildServer) albo createMcpHandler(buildServer) i ponownie uruchom Inspectora w obu erach.

Każde wywołanie nie przechodzi walidacji albo brakuje opisów pól. Masz Zoda 3 albo Zoda 4.0–4.1, albo przekazałeś surowy kształt ({ env: z.string() }) zbudowany inną kopią Zoda. Zainstaluj zod@^4.2.0 i opakowuj schematy w z.object().

„Cannot find module” przy starcie. Klient uruchamia serwer z innego katalogu albo w package.json brakuje "type": "module". Użyj ścieżki bezwzględnej do dist/server.js i przebuduj projekt.

Narzędzie wisi, a potem agent się poddaje. System źródłowy jest wolny. Ustaw limit czasu na każdym fetch i execFile, żeby narzędzie zwracało isError zamiast wisieć, a potem zwiększ limit klienta, jeśli praca naprawdę trwa długo: MCP_TOOL_TIMEOUT w Claude Code, tool_timeout_sec w Codeksie.

Model wywołuje złe narzędzie albo zmyśla argumenty. Opis jest mglisty albo dwa narzędzia się pokrywają. Przepisz opis tak, żeby mówił, co zwraca i kiedy go użyć, połącz pokrywające się narzędzia i ponownie uruchom pytania ewaluacyjne.

mcp-publisher publish odrzuca serwer. name w server.json nie zgadza się z mcpName, przestrzeń nazw nie pasuje do konta, na które się zalogowałeś, albo wersja nie jest jeszcze opublikowana w npm. Popraw niezgodność i ponownie uruchom mcp-publisher validate.

Tech lead zatwierdza serwer na podstawie dowodów, a nie lektury handlerów:

  • npm run verify przechodzi w CI: sprawdzenie typów, testy i przebieg Inspectora z --strict w obu erach protokołu.
  • Test przypina dokładną listę narzędzi, a każde narzędzie bez readOnlyHint jest na zatwierdzonej liście narzędzi zapisujących.
  • Serwer używa jednego poświadczenia konta usługowego o wąskim zakresie, pobieranego ze środowiska; żaden sekret nie leży w commitowanym pliku.
  • Każde wywołanie systemu źródłowego ma limit czasu, a każda porażka zwraca isError.
  • Pytania ewaluacyjne przechodzą w świeżej sesji głównego agenta zespołu.
  • Serwer ma nazwanego właściciela i warunek wycofania.

Stan na 2026-09-26: modelcontextprotocol/inspector ma 11,0 tys. gwiazdek na GitHubie, a mcp-builder ma 118 601 instalacji łącznie według zewnętrznego zrzutu skills.sh (LinklyAI/best-skills, z datą 2026-09-26; źródło wtórne, bo samego skills.sh nie udało się odczytać).