Recipe-Inhalt ist auf Englisch. Englisches Original lesen →
← Alle Recipes
Phase 9 · Distribution·7 steps

Publish to npm, bin field, files allowlist, npx -y

Make your server installable via 'claude mcp add foo -s user -- npx -y your-package'. The four package.json fields that decide whether it works for users.

7 steps0%
Du liest ohne Account. Mit Login speichern wir Step-Fortschritt + Notes.

Publish to npm, bin field, files allowlist, npx -y

npx -y your-package is the lowest-friction install path for an MCP server. User runs claude mcp add foo -- npx -y your-package, npm fetches the latest version, runs your binary, done. For that to work, four package.json fields need to be exactly right. Get any of them wrong and users hit confusing errors.

Schritt 1: The four fields that matter

{
  "name": "your-mcp-server",
  "version": "1.0.0",
  "description": "What your MCP server does in one sentence.",
  "type": "module",
  "main": "dist/server.js",
  "bin": {
    "your-mcp-server": "dist/server.js"
  },
  "files": [
    "dist",
    "README.md",
    "LICENSE"
  ],
  "engines": {
    "node": ">=18"
  },
  "keywords": [
    "mcp",
    "model-context-protocol",
    "claude",
    "claude-code",
    "claude-desktop",
    "cursor",
    "your-domain"
  ]
}

Each field's job:

  • bin, npm symlinks your binary into node_modules/.bin/. npx -y your-package then finds it. Without this, npx fails with command not found.
  • files, allowlist of what gets included in the published tarball. Without this, npm includes everything not matched by .npmignore, your .git, node_modules, .env, tests/ all get published. Allowlist > denylist.
  • type: "module", tells Node to use ESM. Required for top-level await and modern imports. Without this, your import statements fail at runtime.
  • engines.node, npm warns users on incompatible versions. MCP SDK needs Node 18+; some features want 20+.

Schritt 2: The shebang (don't forget)

Your dist/server.js must start with:

#!/usr/bin/env node

Without it, npx runs your file via the wrong shell and errors with unexpected token import. With it, npx knows to use Node.

If your build is tsc, the shebang must be in your source src/server.ts. TypeScript preserves it through compilation:

#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
// ...

After build, verify:

head -1 dist/server.js
# → #!/usr/bin/env node

Schritt 3: Make the binary executable

chmod +x dist/server.js

tsc doesn't preserve the executable bit. Add this to your build step:

"scripts": {
  "build": "tsc && chmod +x dist/server.js"
}

npm publish includes the executable bit in the tarball, so users get it executable when they install.

Schritt 4: Pre-publish dry-run

npm pack --dry-run

Output shows exactly what will be published:

npm notice 📦  [email protected]
npm notice Tarball Contents
npm notice 1.2kB README.md
npm notice 543B  LICENSE
npm notice 12kB  dist/server.js
npm notice 4kB   dist/lib/logger.js
npm notice ...
npm notice Tarball Details
npm notice name:          your-mcp-server
npm notice version:       1.0.0
npm notice filename:      your-mcp-server-1.0.0.tgz
npm notice package size:  18.3 kB
npm notice unpacked size: 92.1 kB
npm notice total files:   23

What to check:

  • No .env, .git, tests/, node_modules in the file list. If they show up, your files allowlist is wrong.
  • dist/server.js is included.
  • Total size < 1MB for a typical MCP server. If it's bigger, you probably accidentally included node_modules or build artifacts.

Schritt 5: Publish

First publish:

npm login
npm publish --access public  # only needed for scoped packages (@your-org/foo)

For scoped packages, --access public is required on first publish (default is private).

Subsequent publishes:

npm version patch  # 1.0.0 → 1.0.1
npm publish

npm version patch bumps the version, creates a git tag, and commits. Push the tag separately:

git push origin main --tags

Schritt 6: Confirm the install works

After publish, test the install path your users will use:

# Fresh terminal, no project context
npx -y your-mcp-server@latest --version
# → 1.0.0

# In Claude Code:
claude mcp add my-test -s user -- npx -y your-mcp-server
claude mcp list
# → my-test  ✓ stdio  npx -y your-mcp-server

Open Claude Code, send a message, the server should start and respond.

If it doesn't work in the fresh-install case, your real users won't be able to install it either.

Schritt 7: Verify

Run academy_validate_step. The validator checks npm view your-package version returns a version (i.e., the package exists on npm registry).

For the package quality, also run:

npm view your-mcp-server
# Confirms metadata: description, keywords, dependencies, repository link

If repository isn't set, npm shows an unhelpful default. Add to package.json:

"repository": {
  "type": "git",
  "url": "git+https://github.com/your-org/your-mcp-server.git"
}

Common traps

  • Missing shebang, npx errors with unexpected token import. Add #!/usr/bin/env node at the top of src/server.ts.
  • Missing chmod +x, npx errors with permission denied. Add chmod +x dist/server.js to build.
  • bin path doesn't match files allowlist, npx errors with cannot find module dist/server.js. The path in bin must point at a file included in files.
  • type: "module" missing, runtime import errors. Always include for ESM projects.
  • @your-org/your-server published as private by accident, npm publish without --access public defaults to private for scoped packages. Use --access public on first publish.
  • Forgetting to bump version, npm publish errors with cannot publish over existing version. npm version patch bumps + tags + commits in one step.
  • Including dev dependencies in dependencies, bloats install size, slows npx -y. Move to devDependencies.
  • Including dist/ in git, pollutes diffs. Add dist/ to .gitignore and rely on npm publish to include it via files.

What good looks like

npm publish from a clean checkout finishes in 30 seconds. npx -y your-mcp-server@latest --version from a fresh terminal prints the right version. claude mcp add my-test -s user -- npx -y your-mcp-server adds the server, claude mcp list shows it, sending a message in Claude triggers a tool call. Users can copy-paste your README install snippet without reading more than 2 lines.

That's the bar. Anything more friction than npx -y your-package and you'll lose half your potential installs to "I'll set this up later", which means never.

Client-Check · auf Deinem Rechner ausführen
cat package.json 2>/dev/null | python3 -c "import json,sys; p=json.load(sys.stdin); print(\"name:\", p.get(\"name\")); print(\"version:\", p.get(\"version\")); print(\"private:\", p.get(\"private\"))" 2>/dev/null || echo "no package.json"
Erwartet: name + version are set, private is false (or omitted).
Falls hängen geblieben: Remove `"private": true`, set a unique name, then `npm publish --access public`.
Blue-green deploys, zero-downtGitHub OSS, trust signals that