Publier un serveur MCP sur npm et PyPI, étape par étape

Publiez un même serveur MCP sur les deux registres pour que n’importe quel client le lance avec npx ou uvx. Configurez package.json et pyproject.toml, vérifiez le tarball, testez avec MCP Inspector, publiez via la publication de confiance dans GitHub Actions, puis ajoutez des outils d’image et de vidéo via une API.

Publier un serveur MCP sur npm et PyPI, étape par étape
Cristian Da Conceicao
Fondateur de Picasso IA

Vous avez créé un serveur MCP et il tourne sur votre ordinateur. Maintenant, un collègue, ou un inconnu sur internet, veut le faire tourner avec une seule ligne de configuration : pas de git clone, pas d’étape de build, pas de fil de discussion sur « quelle version de Node utilisez-vous ? ». C’est exactement ce qu’offre un registre. Publiez sur npm et les clients lanceront votre serveur avec npx. Publiez sur PyPI et ils le lanceront avec uvx. Si vous faites les deux, chaque client MCP, de Claude Desktop à Cursor en passant par VS Code, peut lancer votre serveur à partir d’un simple nom de package.

Ce guide suit le chemin dans l’ordre : structure du dépôt, package.json, pyproject.toml, tests en local, première publication manuelle, puis un workflow GitHub Actions qui livre les deux packages à partir d’un seul tag. Chaque étape suppose un serveur stdio qui fonctionne déjà en local.

💡 Avant de commencer : il vous faut Node 18+ pour npm ou Python 3.10+ pour PyPI, ainsi que des comptes gratuits sur npmjs.com et pypi.org. Activez l’authentification à deux facteurs sur les deux, car les deux registres l’exigent de tous ceux qui publient.

Pourquoi publier sur les deux registres

La plupart des serveurs MCP sont écrits dans un seul langage, généralement TypeScript ou Python, et y restent. Cela fonctionne jusqu’au jour où quelqu’un travaillant avec un autre environnement technique veut essayer le vôtre. Une équipe data en Python n’installera pas Node pour faire tourner un outil, et une équipe front-end ne mettra pas en place un environnement virtuel. Publier sur les deux registres supprime cet obstacle.

Deux boîtes aux lettres en bois patiné, l’une verte et l’autre bleu marine, côte à côte sur un chemin de campagne brumeux au lever du soleil

Deux publics, un seul serveur

Voici la comparaison des deux parcours côte à côte :

npmPyPI
Commande d’exécutionnpx -y your-packageuvx your-package
SDK officiel@modelcontextprotocol/sdkmcp (inclut FastMCP)
Manifestepackage.jsonpyproject.toml
Ce qui est envoyéUn tarball construit à partir de dist/Une archive source plus un wheel
Commande de publicationnpm publishuv publish ou twine upload
Authentification en CIPublication de confiance ou jeton granulairePublication de confiance ou jeton d’API

La configuration la plus propre est une implémentation par langage avec un seul contrat d’outils partagé. Les noms des outils, les schémas d’entrée et les descriptions restent identiques dans les deux packages, si bien qu’un prompt qui fonctionne avec la version npm se comporte de la même façon avec la version PyPI. Conservez ce contrat dans un petit fichier JSON du dépôt et faites comparer les deux builds à ce fichier par la CI.

Résistez à la tentation d’un simple wrapper Python qui appelle npx. Cela fonctionne jusqu’à ce que l’utilisateur n’ait pas Node installé, et il obtient alors une erreur que personne ne peut déchiffrer d’un coup d’œil.

Choisir la structure du package

Un seul dépôt avec deux dossiers simplifie la gestion des releases :

my-mcp-server/
  README.md
  LICENSE
  node/
    package.json
    tsconfig.json
    src/index.ts
  python/
    pyproject.toml
    src/my_mcp_server/__init__.py
    src/my_mcp_server/server.py
  .github/workflows/release.yml

Vue en plongée d’un établi en chêne avec deux rangées bien ordonnées de cartons plats, à côté d’une règle en acier et d’une ficelle

Chaque dossier est un package à part entière, avec son propre manifeste. Le workflow de publication utilisera plus tard working-directory pour les construire séparément, afin que rien ne passe d’un côté à l’autre.

Nommez une fois, vérifiez deux fois

Choisissez un seul nom et utilisez-le sur les deux registres. Les utilisateurs s’en souviendront, et les résultats de recherche seront cohérents.

  • npm : minuscules, compatible avec les URL, sans espaces. Un nom avec scope comme @yourscope/my-mcp-server évite les collisions et convient parfaitement aux serveurs MCP.
  • PyPI : les noms ne tiennent pas compte de la casse et traitent -, _ et . comme le même caractère, donc My_MCP.Server et my-mcp-server entrent en collision.
  • Disponibilité : npm view my-mcp-server renvoie une erreur 404 lorsque le nom est libre. Sur PyPI, ouvrez pypi.org/project/my-mcp-server/ : une erreur 404 signifie la même chose.

💡 Vérifiez les deux noms avant de rédiger le README autour de l’un d’eux. Découvrir le jour de la publication que le nom est déjà pris coûte une après-midi de renommage.

Rédigez un README qui sert de documentation

Les deux registres affichent votre README comme page du package, et pour beaucoup d’utilisateurs c’est la seule documentation qu’ils lisent. Incluez-y quatre éléments, dans cet ordre :

  1. Une phrase sur ce que fait le serveur
  2. Une configuration client à copier-coller pour npx et une autre pour uvx
  3. Un tableau des outils, avec une ligne par outil
  4. Chaque variable d’environnement lue par le serveur, indiquée comme obligatoire ou facultative

Publier le package npm

Configurer package.json

Trois champs déterminent si npx fonctionne réellement : bin, files et le shebang de votre fichier d’entrée.

{
  "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 associe le nom de la commande au fichier d’entrée compilé. Sans lui, npx n’a rien à exécuter.
  • files est une liste blanche. Seuls dist/, le README et la licence entrent dans le tarball.
  • prepublishOnly relance le build juste avant chaque publication, afin de ne jamais livrer une sortie périmée.
  • Le shebang #!/usr/bin/env node doit être la première ligne de src/index.ts. TypeScript le conserve dans le fichier compilé.

Gros plan de mains emballant une petite boîte en carton avec du papier kraft et une enveloppe scellée à la cire sur une table d’atelier

Vérifier le tarball avant l’envoi

Lancez npm pack --dry-run et lisez la liste de fichiers qu’il affiche. Vous voulez dist/, le README, la licence et package.json. Vous ne voulez pas de .env, de fixtures de test, de source maps que vous n’aviez pas prévu de partager, ni d’un node_modules qui traîne.

💡 Un .env divulgué est l’erreur la plus courante lors d’une première publication, et une version publiée ne peut pas être retirée. La liste blanche files est votre filet de sécurité, gardez-la.

Effectuer la première publication

npm login
npm publish --access public

Les packages avec scope sont privés par défaut, c’est pourquoi --access public est indispensable lors de la première publication. Saisissez votre code à deux facteurs lorsqu’il vous est demandé. Puis vérifiez que cela fonctionne depuis un autre dossier :

cd $(mktemp -d)
npx -y @yourscope/my-mcp-server

Le processus doit démarrer et attendre une entrée sur stdin. Ce silence est normal, car un serveur stdio ne parle que lorsqu’un client lui adresse la parole.

Publier le package PyPI

Rédiger pyproject.toml

Le packaging Python tient dans un seul fichier. Cette version utilise hatchling comme backend de build :

[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"

La table [project.scripts] équivaut à bin. Elle crée une commande à l’installation. Si le nom du script correspond au nom du package, uvx my-mcp-server fonctionne directement. S’il diffère, exécutez uvx --from my-mcp-server script-name.

Un serveur minimal utilisant FastMCP, issu du SDK Python officiel :

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

Mains marquées par le temps en train de centrer une roue de vélo en acier dans un atelier de cycles, les rayons rayonnant à travers le cadre

Construire et vérifier les fichiers

cd python
uv build
uvx twine check dist/*

uv build écrit deux fichiers dans dist/ : une archive source (.tar.gz) et un wheel (.whl). Les wheels s’installent rapidement, car rien n’a besoin d’être compilé, ce qui explique pourquoi uvx et pip les privilégient. twine check confirme que le README s’affiche correctement comme page du package, de sorte que vous repérez des métadonnées cassées avant que PyPI ne le fasse.

Essayer TestPyPI, puis publier

TestPyPI est un site distinct, avec des comptes et des jetons distincts. Il existe pour qu’un premier envoi raté ne vous coûte rien.

uv publish --publish-url https://test.pypi.org/legacy/ --token <testpypi-token>
pip install --index-url https://test.pypi.org/simple/ \
  --extra-index-url https://pypi.org/simple/ my-mcp-server

L’index supplémentaire est important. Vos dépendances, y compris mcp, se trouvent sur le vrai PyPI, et TestPyPI ne les héberge pas. Une fois que l’installation de test fonctionne, publiez pour de bon :

uv publish --token <pypi-token>
uvx my-mcp-server

💡 Les versions sont définitives sur les deux registres. PyPI n’accepte jamais deux fois le même nom de fichier, même après une suppression, et npm refuse de réutiliser un numéro de version déjà publié. En cas de problème, incrémentez la version et publiez à nouveau.

Tester avant de livrer

Profil d’un horloger examinant un mouvement mécanique à la loupe sur un établi

Lancer MCP Inspector

MCP Inspector ouvre une page web locale où vous listez les outils, renseignez les arguments et lisez les réponses brutes. Pointez-le vers le build généré, puis vers le package exactement comme le lancerait un utilisateur :

npx @modelcontextprotocol/inspector node dist/index.js
npx @modelcontextprotocol/inspector uvx my-mcp-server

La seconde commande est la plus importante. Elle teste le package installé plutôt que votre dossier de travail, de sorte que les fichiers manquants et les mauvais points d’entrée apparaissent ici et non dans un rapport de bug.

Garder stdout propre

En stdio, stdout transporte le protocole. Un console.log() ou un print() égaré injecte du texte dans le flux JSON-RPC, et le client se déconnecte avec une erreur d’analyse. Envoyez plutôt chaque ligne de log vers stderr : console.error() en Node, et print(..., file=sys.stderr) ou le module logging en Python.

Tester la configuration client

C’est la configuration que vos utilisateurs vont coller. Essayez les deux entrées dans un client réel :

{
  "mcpServers": {
    "my-server-npm": {
      "command": "npx",
      "args": ["-y", "@yourscope/my-mcp-server"]
    },
    "my-server-pypi": {
      "command": "uvx",
      "args": ["my-mcp-server"]
    }
  }
}

Lancez la checklist de pré-publication depuis un shell propre dans un dossier temporaire. Une installation globale ou un node_modules situé à proximité peut masquer un fichier manquant pendant des semaines.

  • npm pack --dry-run ne liste que ce que vous voulez livrer
  • twine check dist/* passe sans avertissement
  • Inspector liste chaque outil via npx et via uvx
  • Rien n’écrit sur stdout en dehors des messages du protocole
  • Les blocs de configuration du README correspondent exactement à ce que vous venez de tester

Automatiser et versionner les releases

Vue en contre-plongée de colis en papier kraft glissant sur un convoyeur à rouleaux en acier vers une porte de chargement dans une salle d’emballage lumineuse

Publication de confiance, sans jeton stocké

Les deux registres permettent à un workflow GitHub Actions de publier via OpenID Connect. Vous enregistrez votre dépôt et le fichier de workflow côté registre, une seule fois, et le registre fait confiance à ce workflow précis à partir de là. Aucun jeton de longue durée ne réside dans les secrets de votre dépôt, il n’y a donc rien à divulguer ni à renouveler. Le workflow n’a besoin que de la permission id-token: write.

Sur PyPI, ajoutez un éditeur de confiance dans les paramètres de publication du projet. Un projet tout neuf peut utiliser un publisher en attente, afin que la première release puisse elle aussi provenir de la CI. Sur npm, ajoutez l’éditeur de confiance dans les paramètres du package. Les écrans de configuration évoluent de temps à autre, suivez donc les instructions actuelles.

Un tag, deux publications

Pousser un tag comme v0.1.0 déclenche les deux jobs en parallèle :

name: release
on:
  push:
    tags: ["v*"]

permissions:
  id-token: write
  contents: read

jobs:
  npm:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: node
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 24
          registry-url: https://registry.npmjs.org
      - run: npm install -g npm@latest
      - run: npm ci
      - run: npm publish --provenance --access public

  pypi:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v5
      - run: uv build
        working-directory: python
      - uses: pypa/gh-action-pypi-publish@release/v1
        with:
          packages-dir: python/dist

L’étape npm install -g npm@latest s’assure que le CLI npm est assez récent pour la publication de confiance. Ajoutez une étape needs: avec votre job de test si vous voulez qu’un build en échec bloque la release.

Un semver que les clients respectent

Les clients MCP et les agents qui les utilisent dépendent de vos noms d’outils et de vos schémas d’entrée, traitez-les donc comme votre API publique :

ChangementIncrément de version
Corriger un bug, sans modifier le schémaPatch (0.1.1)
Ajouter un nouvel outil ou un argument facultatifMineure (0.2.0)
Renommer ou supprimer un outil, ou ajouter un argument obligatoireMajeure (1.0.0)

Mettez les deux manifestes au même numéro dans un seul commit, puis taguez-le. Un petit script qui modifie package.json et pyproject.toml ensemble évite le classique décalage où npm affiche 1.2.0 et PyPI 1.1.0. Les utilisateurs qui veulent de la stabilité peuvent figer une version majeure dans leur configuration, par exemple @yourscope/my-mcp-server@1.

Escalier en pierre ensoleillé gravissant un jardin en terrasses sur une colline, avec trois paliers distincts

Une fois les deux packages en ligne, vous pouvez aussi référencer le serveur dans le registre officiel MCP. Celui-ci vérifie la propriété en lisant un champ mcpName dans package.json et une ligne mcp-name: correspondante dans le README PyPI, puis publie les métadonnées avec la commande mcp-publisher. Le registre évolue encore, lisez donc sa documentation actuelle avant de vous fier au format exact.

Ajouter des outils d’image et de vidéo

Un serveur publié devient plus utile dès qu’il peut créer quelque chose. La génération d’images et de vidéos figure parmi les outils les plus demandés, et l’API PicassoIA permet de les ajouter en quelques lignes. L’URL de base est https://api.picassoia.com/v1, l’authentification se fait par jeton Bearer commençant par pia_sk_, et les tâches sont asynchrones : vous créez une prédiction, vous l’interrogez, puis vous lisez le résultat.

Quatre modèles sont disponibles via l’API : Picasso IA Image, Picasso IA Image Editor Pro, Picasso IA Video et Seedance 2.5 Lite, qui ajoute du son à ses clips. Un compte peut exécuter 5 prédictions simultanément, et les prompts peuvent atteindre 4 000 caractères.

Studio photo avec un appareil hybride sur trépied face à un vase en céramique devant un fond de papier gris

Appelez l’API depuis un outil. Cette fonction TypeScript crée une prédiction sur Picasso IA Image et interroge l’API jusqu’à la fin du traitement :

const BASE = "https://api.picassoia.com/v1";
const headers = {
  Authorization: `Bearer ${process.env.PICASSOIA_API_TOKEN}`,
  "Content-Type": "application/json",
};

export async function generateImage(prompt: string): Promise<string> {
  const created = await fetch(
    `${BASE}/models/picassoia/picassoia-image/predictions`,
    { method: "POST", headers, body: JSON.stringify({ input: { prompt } }) }
  ).then((r) => r.json());

  while (true) {
    const p = await fetch(`${BASE}/predictions/${created.id}`, { headers })
      .then((r) => r.json());
    if (p.status === "succeeded") {
      return Array.isArray(p.output) ? p.output[0] : p.output;
    }
    if (p.status === "failed" || p.status === "canceled") {
      throw new Error(p.error ?? p.status);
    }
    await new Promise((resolve) => setTimeout(resolve, 2000));
  }
}

Consultez la documentation de l’API pour connaître les champs de saisie exacts de chaque modèle, car la structure de output et les paramètres acceptés varient d’un modèle à l’autre.

Transmettez le jeton via la configuration client. Ne jamais intégrer un jeton dans le package. Lisez-le depuis l’environnement et laissez chaque utilisateur le renseigner dans sa configuration MCP :

{
  "mcpServers": {
    "my-server": {
      "command": "npx",
      "args": ["-y", "@yourscope/my-mcp-server"],
      "env": { "PICASSOIA_API_TOKEN": "pia_sk_your_token_here" }
    }
  }
}

💡 Consultez les conditions d’accès actuelles sur la page de l’API PicassoIA avant de promettre une utilisation gratuite dans votre README. La formulation des tarifs peut changer, et et vos utilisateurs vous demanderont des comptes sur ce que vous avez écrit.

Rédigez les notes de version avec un LLM. Un grand modèle de langage peut prendre en charge la corvée qui pousse les gens à négliger les changelogs. Voici un workflow rapide avec Claude Sonnet 5 :

  1. Lancez git log v0.1.0..HEAD --oneline et copiez le résultat.
  2. Ouvrez la page du modèle sur PicassoIA et collez le journal avec une consigne d’une ligne : regroupez les changements en Ajouts, Modifications et Corrections, en langage clair.
  3. Indiquez-lui quels changements touchent aux noms d’outils ou aux schémas, afin qu’ils soient signalés comme ruptures.
  4. Relisez le résultat en le comparant au diff, puis collez-le dans la release GitHub.

Pour un second avis sur votre pyproject.toml ou votre fichier de workflow, GPT 5.6 Sol est un bon relecteur pour les tâches de programmation.

Essayez par vous-même sur Picasso IA

Votre page de package mérite une vraie image d’en-tête et un court extrait de démonstration, pas une capture d’écran de terminal. Créez l’image d’en-tête avec Picasso IA Image, peaufinez les détails avec Picasso IA Image Editor Pro, puis animez la dernière image en un court clip avec Picasso IA Video.

Plan par-dessus l’épaule d’une jeune femme souriant à son ordinateur portable sur un bureau ensoleillé, à côté d’un carnet de croquis et d’un appareil photo argentique

Une première expérience simple :

  • Rédigez un prompt de 40 mots décrivant un bureau de développeur calme sous la lumière du matin
  • Générez trois variantes et gardez la plus nette
  • Enregistrez-la comme image d’en-tête dans votre README
  • Animez-la pour l’annonce de la release

Parcourez tous les modèles disponibles sur picassoia.com/en/all-models, choisissez celui qui correspond à votre style et publiez quelque chose qui vaut la peine d’être ouvert. Votre première release n’est qu’à un tag de distance.

Partager cet article

Choisissez votre langue