Large Language ModelsGenerate imagesGenerate videos
Publish an MCP Server to npm and PyPI Step by Step
Ship one MCP server to both registries so any client can start it with npx or uvx. Set up package.json and pyproject.toml, check the tarball, test in MCP Inspector, publish through trusted publishing in GitHub Actions, and add image and video tools through an API.
You built an MCP server and it runs on your laptop. Now a teammate, or a stranger on the internet, wants it running with one line of config: no git clone, no build step, no "which Node version are you on?" thread. That is exactly what a registry gives you. Publish to npm and clients start your server with npx. Publish to PyPI and they start it with uvx. Do both and every MCP client, from Claude Desktop to Cursor to VS Code, can launch your server from nothing but a package name.
This walkthrough follows the path in order: repo layout, package.json, pyproject.toml, local tests, the first manual publish, and a GitHub Actions workflow that ships both packages from a single tag. Every step assumes a stdio server that already runs locally.
💡 Before you start: you need Node 18+ for npm or Python 3.10+ for PyPI, plus free accounts on npmjs.com and pypi.org. Turn on two-factor authentication on both, since both registries expect it from anyone who publishes.
Why Ship to Both Registries
Most MCP servers start in one language, usually TypeScript or Python, and stay there. That works until someone on a different stack wants to try yours. A Python data team will not install Node to run a tool, and a front-end team will not set up a virtualenv. Publishing to both registries removes the excuse.
Two Audiences, One Server
Here is how the two paths compare side by side:
npm
PyPI
Run command
npx -y your-package
uvx your-package
Official SDK
@modelcontextprotocol/sdk
mcp (includes FastMCP)
Manifest
package.json
pyproject.toml
What gets uploaded
A tarball built from dist/
A source archive plus a wheel
Publish command
npm publish
uv publish or twine upload
Auth in CI
Trusted publishing or granular token
Trusted publishing or API token
The cleanest setup is one implementation per language with one shared tool contract. Tool names, input schemas and descriptions stay identical in both packages, so a prompt that works against the npm build behaves the same against the PyPI build. Keep that contract in a small JSON file in the repo and have CI compare both builds against it.
Resist the shortcut of a thin Python wrapper that shells out to npx. It works until the user has no Node installed, and then it fails with an error nobody can read at a glance.
Pick a Package Layout
A single repo with two folders keeps the release story simple:
Each folder is its own package with its own manifest. The release workflow later uses working-directory to build them separately, so nothing leaks from one side to the other.
Name It Once, Check It Twice
Pick one name and use it on both registries. Users remember it, and search results line up.
npm: lowercase, URL-safe, no spaces. A scoped name like @yourscope/my-mcp-server avoids collisions and works fine for MCP servers.
PyPI: names are case-insensitive and treat -, _ and . as the same character, so My_MCP.Server and my-mcp-server collide.
Availability:npm view my-mcp-server returns a 404 when the name is free. On PyPI, open pypi.org/project/my-mcp-server/ and a 404 means the same.
💡 Check both names before you write the README around one. Finding out on publish day that the name is taken costs an afternoon of renaming.
Write a README That Works as Docs
Both registries render your README as the package page, and for many users it is the only documentation they read. Put four things in it, in this order:
One sentence on what the server does
A copy-paste client config for npx and another for uvx
A table of tools with one line each
Every environment variable the server reads, marked required or optional
Publish the npm Package
Set Up package.json
Three fields decide whether npx works at all: bin, files and the shebang in your entry file.
{
"name": "@yourscope/my-mcp-server",
"version": "0.1.0",
"description": "MCP server that does one useful thing",
"type": "module",
"bin": { "my-mcp-server": "dist/index.js" },
"files": ["dist", "README.md", "LICENSE"],
"engines": { "node": ">=18" },
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build"
},
"dependencies": { "@modelcontextprotocol/sdk": "^1.0.0" },
"license": "MIT"
}
bin maps the command name to the compiled entry file. Without it, npx has nothing to run.
files is a whitelist. Only dist/, the README and the license go into the tarball.
prepublishOnly rebuilds right before every publish, so you never ship stale output.
The shebang#!/usr/bin/env node must be the first line of src/index.ts. TypeScript keeps it in the compiled file.
Check the Tarball Before Sending
Run npm pack --dry-run and read the file list it prints. You want dist/, the README, the license and package.json. You do not want .env, test fixtures, source maps you did not intend to share, or a stray node_modules.
💡 A leaked .env is the most common mistake in a first publish, and a published version cannot be taken back. The files whitelist is your safety net, so keep it.
Run the First Publish
npm login
npm publish --access public
Scoped packages default to private, which is why --access public matters on the first publish. Enter your two-factor code when asked. Then prove it works from a different folder:
cd $(mktemp -d)
npx -y @yourscope/my-mcp-server
The process should start and wait for input on stdin. That silence is correct, because a stdio server speaks only when a client talks to it.
Publish the PyPI Package
Write pyproject.toml
Python packaging is one file. This version uses hatchling as the build backend:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "my-mcp-server"
version = "0.1.0"
description = "MCP server that does one useful thing"
readme = "README.md"
requires-python = ">=3.10"
license = { text = "MIT" }
dependencies = ["mcp>=1.0"]
[project.scripts]
my-mcp-server = "my_mcp_server.server:main"
The [project.scripts] table is the equivalent of bin. It creates a command on install. If the script name matches the package name, uvx my-mcp-server just works. If it differs, run uvx --from my-mcp-server script-name.
A minimal server using FastMCP from the official Python SDK:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("my-mcp-server")
@mcp.tool()
def ping() -> str:
"""Return pong so clients can confirm the server is alive."""
return "pong"
def main() -> None:
mcp.run() # stdio transport by default
Build and Check the Files
cd python
uv build
uvx twine check dist/*
uv build writes two files into dist/: a source archive (.tar.gz) and a wheel (.whl). Wheels install fast because nothing needs compiling, which is why uvx and pip prefer them. twine check confirms the README renders as the package page, so you catch broken metadata before PyPI does.
Try TestPyPI, Then Publish
TestPyPI is a separate site with separate accounts and separate tokens. It exists so a failed first upload costs you nothing.
The extra index matters. Your dependencies, including mcp, live on the real PyPI, and TestPyPI does not carry them. Once the test install runs, publish for real:
uv publish --token <pypi-token>
uvx my-mcp-server
💡 Versions are permanent on both registries. PyPI never accepts the same filename twice, even after a deletion, and npm refuses to reuse a published version number. When something is wrong, bump the version and publish again.
Test Before You Ship
Run It in MCP Inspector
The MCP Inspector opens a local web page where you list tools, fill in arguments and read raw responses. Point it at the built output, then at the package exactly as a user would run it:
The second command matters most. It exercises the installed package instead of your working tree, so missing files and wrong entry points show up here and not in a bug report.
Keep stdout Clean
On stdio, stdout carries the protocol. A stray console.log() or print() injects text into the JSON-RPC stream and the client disconnects with a parse error. Send every log line to stderr instead: console.error() in Node, and print(..., file=sys.stderr) or the logging module in Python.
Test the Client Config
This is the config your users will paste. Try both entries in a real client:
Run the pre-publish checklist from a clean shell in a temporary folder. A global install or a nearby node_modules can hide a missing file for weeks.
npm pack --dry-run lists only what you intend to ship
twine check dist/* passes with no warnings
Inspector lists every tool through npx and through uvx
Nothing writes to stdout except protocol messages
The README config blocks match what you just tested
Automate and Version Releases
Trusted Publishing, No Stored Tokens
Both registries let a GitHub Actions workflow publish through OpenID Connect. You register your repository and workflow file on the registry side once, and the registry trusts that exact workflow from then on. No long-lived token sits in your repo secrets, so there is nothing to leak or rotate. The workflow only needs id-token: write permission.
On PyPI, add a trusted publisher in the project's publishing settings. A brand-new project can use a pending publisher, so the first release can come from CI as well. On npm, add the trusted publisher in the package settings. The setup screens change now and then, so follow the current prompts.
One Tag, Two Publishes
Pushing a tag like v0.1.0 triggers both jobs in parallel:
The npm install -g npm@latest step makes sure the npm CLI is recent enough for trusted publishing. Add a needs: step with your test job if you want a red build to block the release.
Semver That Clients Respect
MCP clients and the agents behind them depend on your tool names and input schemas, so treat those as your public API:
Change
Version bump
Fix a bug, no schema change
Patch (0.1.1)
Add a new tool or an optional argument
Minor (0.2.0)
Rename or remove a tool, or add a required argument
Major (1.0.0)
Bump both manifests to the same number in one commit, then tag it. A tiny script that edits package.json and pyproject.toml together prevents the classic mismatch where npm sits at 1.2.0 and PyPI at 1.1.0. Users who want stability can pin a major in their config, for example @yourscope/my-mcp-server@1.
Once both packages are live, you can also list the server in the official MCP Registry. It verifies ownership by reading an mcpName field in package.json and a matching mcp-name: line in the PyPI README, then publishes metadata with the mcp-publisher command. The registry is still evolving, so read its current docs before you rely on the exact format.
Add Image and Video Tools
A published server becomes more useful the moment it can make something. Image and video generation are the most requested tools, and the PicassoIA API makes them a short addition. The base URL is https://api.picassoia.com/v1, authentication is a Bearer token that starts with pia_sk_, and jobs are asynchronous: you create a prediction, poll it, then read the result.
Check the API docs for each model's exact input fields, because the shape of output and the accepted parameters differ from model to model.
Pass the token through the client config. Never bake a token into the package. Read it from the environment and let each user set it in their MCP config:
💡 Read the current plan requirements on the PicassoIA API page before you promise free usage in your README. Pricing wording can change, and your users will hold you to what you wrote.
Draft release notes with an LLM. A language model can handle the chore that makes people skip changelogs. Here is a quick workflow with Claude Sonnet 5:
Run git log v0.1.0..HEAD --oneline and copy the output.
Open the model page on PicassoIA and paste the log with a one-line instruction: group the changes into Added, Changed and Fixed, in plain language.
Tell it which changes touch tool names or schemas, so those are labeled as breaking.
Read the result against the diff and paste it into the GitHub release.
For a second opinion on your pyproject.toml or workflow file, GPT 5.6 Sol is a good reviewer for coding tasks.
Try It Yourself on Picasso IA
Your package page deserves a real hero image and a short demo clip, not a screenshot of a terminal. Create the hero with Picasso IA Image, tidy details with Picasso IA Image Editor Pro, then animate the final frame into a short clip with Picasso IA Video.
A simple first experiment:
Write a 40-word prompt describing a calm developer desk in morning light
Generate three variations and keep the sharpest one
Save it as the hero image in your README
Animate it for the release announcement
Browse every available model at picassoia.com/en/all-models, pick one that fits your style, and publish something worth opening. Your first release is one tag away.