← Alle Playbooks
Playbook· build

Publishing an MCP server step by step

You've built an MCP server. Now someone else has to be able to install it without asking you. Here are the ten steps from 'works on my laptop' to 'npx -y your-server'.

Building an MCP server is one thing. Distributing it so someone else installs it in 30 seconds is another. Most repos get stuck in "build" because distribution feels annoying. It isn't, once you've done the path once.

Assumption: you already have a working local stdio MCP server (path: /path/to/your-server with package.json + src/index.ts). Now you want to publish to npm plus GitHub. By the end anyone can wire it in with npx -y your-server.

1. Naming decision

The npm package name has to be unique and the pattern is always mcp-{topic} (with dash, lowercase). Existing examples: mcp-academy, mcp-personal-suite, mcp-stateless-migrator, mcp-spec-migrator.

Search npm for whether your name is free:

npm view mcp-your-name

If that returns 404 not found, the name is free. If it shows a version number, find another name.

A tip: don't use an acronym only you understand. mcp-cli-buddy is better than mcp-cb.

2. Tidy package.json

These are the mandatory fields for a published MCP package:

{
  "name": "mcp-your-name",
  "version": "0.1.0",
  "description": "One sentence of what the server does.",
  "license": "MIT",
  "author": "Your Name <email>",
  "homepage": "https://github.com/yourusername/mcp-your-name#readme",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/yourusername/mcp-your-name.git"
  },
  "bugs": "https://github.com/yourusername/mcp-your-name/issues",
  "keywords": ["mcp", "model-context-protocol", "claude", "your-topic"],
  "type": "module",
  "main": "dist/index.js",
  "bin": {
    "mcp-your-name": "dist/index.js"
  },
  "files": [
    "dist/**/*",
    "README.md",
    "LICENSE"
  ],
  "engines": {
    "node": ">=20"
  },
  "scripts": {
    "build": "tsc -p tsconfig.json",
    "prepublishOnly": "npm run build"
  }
}

The bin field is the most important. With it, npx -y mcp-your-name works without install. The prepublishOnly hook ensures dist/ is rebuilt before each npm publish.

3. Set the shebang in src/index.ts

So npx can directly run your script, the first line in src/index.ts must be:

#!/usr/bin/env node

That's it. No blank line before, no comment. The shebang must be exactly at the start otherwise the executable doesn't work.

4. tsconfig aimed at build output

{
  "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"]
}

declaration: true also builds .d.ts files. Important if someone uses your package as a library. Less critical for pure MCP servers, never hurts.

5. Write the README (three sections)

A good MCP README has exactly three required sections:

# mcp-your-name

One sentence of what it does.

## Install

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

Add to Claude Desktop or Claude Code config, restart.

## Tools

- `tool_one(param)`: what it does
- `tool_two(param)`: what it does

## Example prompts

> "Ask mcp-your-name about X"
> "Use tool_one with parameter Y"

That's all that's needed for the first install. More can be added, less can't. If someone reads your README and isn't installed in two minutes, the README is too long.

6. Local test with npm pack

BEFORE you publish, simulate the install locally:

npm run build
npm pack
# Creates mcp-your-name-0.1.0.tgz
cd /tmp
npm install /path/to/mcp-your-name-0.1.0.tgz
ls node_modules/mcp-your-name/dist/

Check dist/index.js is there and the shebang is at the top. If that's missing, you have a build problem you fix now, not after publish.

A tip: npm pack shows you the list of files going into the package. If tests/ or .env are in there, you defined files: too broadly in package.json. Correct.

7. npm account + login

If you don't have an npm account: npmjs.com/signup. Then locally:

npm login
npm whoami

If you have two-factor auth enabled (you should): npm asks for OTP on login AND on publish. Required for serious packages.

8. First publish

npm publish --access=public

The --access=public is required if your package name is unscoped (@org/package). Default is private which doesn't work on free accounts and throws a cryptic error.

Verify:

npm view mcp-your-name
# Now shows 0.1.0

Plus: wire it into Claude Desktop config and restart. claude mcp list must show your server.

9. GitHub repo + tag

Push the source to GitHub:

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

Make a GitHub release (gh release create v0.1.0 --notes "Initial release"). That gives your package discoverability via GitHub search and makes version history traceable for users.

Optional but recommended: GitHub Actions for automatic publish-on-tag with npm provenance. Pattern is in the studiomeyer-io repos.

10. Distribution beyond npm

npm is the mandatory spot, not the only one. Three other platforms where MCP servers get found:

  • awesome-mcp-servers on GitHub: open a PR, list your repo under the right category.
  • MCPize Marketplace (mcpize.com): create a listing, gives discoverability for cloud users.
  • Glama (glama.ai/mcp/servers): aggregator, free entry.

These three are free, one-time 30 minutes of work, bring the first users. Posting in Reddit r/ClaudeAI or r/mcp also works if your server solves a specific pain.

What you should not do: post in beginner groups "Hey I built an MCP server, check it out". That's spam and doesn't land. Posting only works when you have a concrete use-case story.

OAuth refresh patterns for HTTP servers

If your server is HTTP instead of stdio and uses OAuth, there's no avoiding token refresh. Access tokens are short-lived (typically 1 hour), and if your user hits your server once a week, the token will have expired between sessions.

In practice: on the first OAuth flow your server gets access_token plus refresh_token. You store both. When the next tool request comes in and the access token has expired, you make a refresh call to the OAuth provider (grant_type=refresh_token), get a new access token (ideally also a new refresh token, best practice against token replay), store both, and transparently retry the tool request.

What came as bug fixes in Claude Code v2.1.118 and you should know as an MCP server author:

  • Token expiry detection. Your server must actually refresh on 401 or 403, not silently fail. Claude Code now reliably detects when an MCP server returns "401 token expired" and triggers the refresh client-side.
  • Keychain race conditions. When your server is hit in parallel (two tool calls at once), it must not happen that both trigger the refresh and overwrite each other's tokens. Mutex or atomic token updates are mandatory.
  • Refresh token rotation. Some OAuth providers (Google, GitHub) hand out a new refresh token on every refresh. If you keep using the old one, after a few refreshes you have an invalid token. Always store the new one.

Pseudo-code pattern for your 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);  // with mutex so no double-refresh
    res = await fetch(endpoint, { headers: { Authorization: `Bearer ${token}` } });
  }

  return res;
}

For the mutex logic in a multi-tenant setup: per tenant an in-memory lock map (Node.js Promise-based) or Redis lock if you scale horizontally. For single-tenant servers a simple let refreshing: Promise<string> | null pattern is enough.

What you should not do: store refresh tokens in the frontend or browser localStorage. Refresh tokens belong server-side, always. In the frontend only the short-lived access token remains.

Next steps

After the first publish comes the next hurdle: version maintenance. Patch releases (0.1.1) when you fix bugs, minor (0.2.0) on new tools, major (1.0.0) on API changes. Stick to SemVer, otherwise you frustrate users who have your server in a production setup.