← Alle Playbooks
Playbook· build

MCP-Server publishen Schritt für Schritt

Du hast einen MCP-Server gebaut. Jetzt soll ihn jemand anders installieren können ohne Dich zu fragen. Hier sind die zehn Schritte von 'läuft auf meinem Laptop' zu 'npx -y dein-server'.

Einen MCP-Server bauen ist die eine Sache. Ihn so distributieren dass jemand anders ihn in 30 Sekunden installiert, ist die andere. Die meisten Repos bleiben in der "build" Phase hängen weil Distribution nervig wirkt. Ist sie nicht, wenn Du den Pfad einmal gemacht hast.

Annahme: Du hast einen funktionierenden lokalen stdio-MCP-Server (Pfad: /path/to/dein-server mit package.json + src/index.ts). Jetzt willst Du ihn auf npm publizieren plus auf GitHub. Am Ende kann jeder ihn mit npx -y dein-server einbinden.

1. Naming-Decision treffen

Der npm-Paket-Name muss eindeutig sein und der Pattern ist immer mcp-{thema} (mit Bindestrich, kleinbuchstaben). Beispiele die existieren: mcp-academy, mcp-academy-arena, mcp-personal-suite, mcp-multi-channel.

Suche bei npm ob Dein Name frei ist:

npm view mcp-dein-name

Wenn das 404 not found zurueckgibt, ist der Name frei. Wenn es eine Versionsnummer zeigt, suche einen anderen Namen.

Ein Tipp: Verwende kein Akronym das nur Du verstehst. mcp-cli-buddy ist besser als mcp-cb.

2. package.json aufraeumen

Das hier sind die Pflicht-Felder für ein veroeffentlichtes MCP-Paket:

{
  "name": "mcp-dein-name",
  "version": "0.1.0",
  "description": "Ein Satz was der Server tut.",
  "license": "MIT",
  "author": "Dein Name <email>",
  "homepage": "https://github.com/deinusername/mcp-dein-name#readme",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/deinusername/mcp-dein-name.git"
  },
  "bugs": "https://github.com/deinusername/mcp-dein-name/issues",
  "keywords": ["mcp", "model-context-protocol", "claude", "dein-thema"],
  "type": "module",
  "main": "dist/index.js",
  "bin": {
    "mcp-dein-name": "dist/index.js"
  },
  "files": [
    "dist/**/*",
    "README.md",
    "LICENSE"
  ],
  "engines": {
    "node": ">=20"
  },
  "scripts": {
    "build": "tsc -p tsconfig.json",
    "prepublishOnly": "npm run build"
  }
}

Das bin-Feld ist das wichtigste. Damit funktioniert npx -y mcp-dein-name ohne Install. Der prepublishOnly-Hook stellt sicher dass dist/ vor jedem npm publish neu gebaut wird.

3. Shebang in src/index.ts setzen

Damit npx Dein Script direkt ausführen kann, muss die erste Zeile in src/index.ts sein:

#!/usr/bin/env node

Nur das. Keine Leerzeile davor, kein Kommentar. Der Shebang muss exakt am Anfang stehen sonst funktioniert das executable nicht.

4. tsconfig auf Build-Output ausrichten

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "outDir": "dist",
    "rootDir": "src",
    "declaration": true,
    "sourceMap": true,
    "strict": true,
    "esModuleInterop": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "tests"]
}

Das declaration: true baut auch .d.ts Files mit. Wichtig falls jemand Dein Paket als Library nutzen will. Bei reinen MCP-Servern weniger kritisch, schadet aber nie.

5. README schreiben (drei Sektionen)

Ein gutes MCP-README hat genau drei Pflicht-Sektionen:

# mcp-dein-name

Ein Satz was es tut.

## Install

\`\`\`json
{
  "mcpServers": {
    "dein-name": {
      "command": "npx",
      "args": ["-y", "mcp-dein-name"]
    }
  }
}
\`\`\`

In Claude Desktop oder Claude Code config einfuegen, neu starten.

## Tools

- `tool_one(param)`: Was es tut
- `tool_two(param)`: Was es tut

## Beispiel-Prompts

> "Frag mcp-dein-name nach X"
> "Nutze tool_one mit Parameter Y"

Das ist alles was beim ersten Install gebraucht wird. Mehr kann hinzu, weniger nicht. Wenn jemand Deinen README liest und nicht in zwei Minuten installiert hat, ist die README zu lang.

6. Lokal mit npm pack testen

BEVOR Du publishest, simulier den Install lokal:

npm run build
npm pack
# Erzeugt mcp-dein-name-0.1.0.tgz
cd /tmp
npm install /pfad/zu/mcp-dein-name-0.1.0.tgz
ls node_modules/mcp-dein-name/dist/

Schau ob dist/index.js da ist und der Shebang oben drin steht. Wenn das fehlt, hast Du ein Build-Problem das Du jetzt fixt, nicht erst nach dem publish.

Ein Tipp: npm pack zeigt Dir die Liste der Files die in das Paket kommen. Wenn da tests/ oder .env mit drin sind, hast Du files: in package.json zu weit definiert. Korrigieren.

7. npm-Account anlegen + Login

Wenn Du noch keinen npm-Account hast: npmjs.com/signup. Dann lokal:

npm login
# Email, Passwort, OTP
npm whoami
# Sollte Deinen Username zeigen

Falls Du Two-Factor-Auth aktiviert hast (solltest Du): npm fragt nach OTP beim Login UND beim Publish. Pflicht für ernst gemeinte Pakete.

8. Erster Publish

npm publish --access=public

Das --access=public ist Pflicht wenn Dein Paket-Name nicht scoped ist (@org/paket). Standard ist private was bei Free-Accounts nicht geht und einen kryptischen Fehler wirft.

Verifikation:

npm view mcp-dein-name
# Zeigt jetzt 0.1.0

Plus: Im Claude-Desktop-Config einbinden und neu starten. claude mcp list muss Deinen Server zeigen.

9. GitHub-Repo + Tag

Push den Source nach GitHub:

git init
git add .
git commit -m "Initial commit: mcp-dein-name v0.1.0"
gh repo create deinusername/mcp-dein-name --public --source=. --push
git tag v0.1.0
git push --tags

Ein Release auf GitHub machen (gh release create v0.1.0 --notes "Initial release"). Das gibt Deinem Paket Diskoverbarkeit über GitHub-Search und macht die Versions-History für User nachvollziehbar.

Optional aber empfohlen: GitHub Actions für automatischen Publish-on-Tag mit npm provenance. Pattern findest Du in den studiomeyer-io Repos.

10. Distribution beyond npm

npm ist die Pflicht-Stelle, aber nicht die einzige. Drei weitere Plattformen wo MCP-Server gefunden werden:

  • awesome-mcp-servers auf GitHub: PR aufmachen, Dein Repo unter passende Kategorie eintragen.
  • MCPize Marketplace (mcpize.com): Listing eintragen, gibt Discoverability für Cloud-User.
  • Glama (glama.ai/mcp/servers): Aggregator, freier Eintrag.

Diese drei kostenlos, einmalig 30 Minuten Arbeit, bringen die ersten User. Reddit r/ClaudeAI oder r/mcp posten ist auch wirksam falls Dein Server eine spezifische Pain löst.

Was Du nicht machen sollst: in Anfaengergruppen "Hey ich hab einen MCP-Server gebaut, schaut mal" posten. Das ist Spam und kommt nicht an. Posten geht nur wenn Du eine konkrete Use-Case-Geschichte hast.

OAuth-Refresh-Patterns für HTTP-Server

Wenn Dein Server HTTP statt stdio ist und OAuth nutzt, kommst Du nicht um eine Sache herum: Token-Refresh. Access-Tokens haben kurze Lebensdauer (typischerweise 1 Stunde), und wenn Dein User einmal pro Woche Deinen Server nutzt, expired der Token zwischen den Sessions garantiert.

In der Praxis: Beim ersten OAuth-Flow bekommt Dein Server access_token plus refresh_token. Du speicherst beide. Wenn die nächste Tool-Anfrage reinkommt und der Access-Token abgelaufen ist, machst Du einen Refresh-Call zum OAuth-Provider (grant_type=refresh_token), bekommst einen neuen Access-Token (idealerweise auch einen neuen Refresh-Token, Best Practice gegen Token-Replay), speicherst beide, und retryst die Tool-Anfrage transparent.

Was bei Claude Code v2.1.118 als Bug-Fix kam und Du als MCP-Server-Autor wissen solltest:

  • Token-Expiry-Erkennung. Dein Server muss bei einem 401 oder 403 wirklich refreshen, nicht stumm fehlschlagen. Claude Code erkennt jetzt zuverlässig wenn ein MCP-Server "401 token expired" zurückgibt und triggert den Refresh client-seitig.
  • Keychain-Race-Conditions. Wenn Dein Server parallel angesprochen wird (zwei Tool-Calls gleichzeitig), darf es nicht passieren dass beide den Refresh anstoßen und sich gegenseitig die Tokens überschreiben. Mutex oder atomare Token-Updates sind Pflicht.
  • Refresh-Token-Rotation. Manche OAuth-Provider (Google, GitHub) geben bei jedem Refresh einen neuen Refresh-Token mit. Wenn Du den alten weiter benutzt, hast Du nach ein paar Refreshes einen ungültigen Token. Immer den neuen speichern.

Pattern in Pseudo-Code für Deinen Tool-Handler:

async function callApi(userId: string, endpoint: string) {
  let token = await getToken(userId);
  let res = await fetch(endpoint, { headers: { Authorization: `Bearer ${token}` } });

  if (res.status === 401) {
    token = await refreshToken(userId);  // mit Mutex damit kein Doppel-Refresh
    res = await fetch(endpoint, { headers: { Authorization: `Bearer ${token}` } });
  }

  return res;
}

Für die Mutex-Logik im Multi-Tenant-Setup: pro Tenant eine in-memory Lock-Map (Node.js Promise-basiert) oder Redis-Lock falls Du horizontal skalierst. Bei Single-Tenant Server reicht ein einfacher let refreshing: Promise<string> | null Pattern.

Was Du nicht machen solltest: Refresh-Tokens im Frontend speichern oder im Browser-LocalStorage. Refresh-Tokens gehören server-side, immer. Im Frontend bleibt nur der kurzlebige Access-Token.

Nächste Schritte

Nach dem ersten Publish kommt die nächste Hurde: Versions-Pflege. Patch-Releases (0.1.1) wenn Du Bugs fixt, Minor (0.2.0) bei neuen Tools, Major (1.0.0) bei API-Änderungen. Halte Dich an SemVer, sonst frustriest Du User die Deinen Server in einem Production-Setup haben.