Publicar um servidor MCP no npm e no PyPI passo a passo

Publique um servidor MCP nos dois registros para que qualquer cliente o inicie com npx ou uvx. Configure package.json e pyproject.toml, verifique o tarball, teste no MCP Inspector, publique com publicação confiável no GitHub Actions e adicione ferramentas de imagem e vídeo por meio de uma API.

Publicar um servidor MCP no npm e no PyPI passo a passo
Cristian Da Conceicao
Fundador do Picasso IA

Você criou um servidor MCP e ele roda no seu notebook. Agora um colega, ou um desconhecido na internet, quer que ele funcione com uma única linha de configuração: sem git clone, sem etapa de build, sem a conversa do tipo "qual versão do Node você está usando?". Isso é exatamente o que um registro oferece. Publique no npm e os clientes iniciam seu servidor com npx. Publique no PyPI e eles o iniciam com uvx. Faça as duas coisas e qualquer cliente MCP, do Claude Desktop ao Cursor e ao VS Code, consegue executar seu servidor apenas com um nome de pacote.

Este tutorial segue o caminho em ordem: estrutura do repositório, package.json, pyproject.toml, testes locais, a primeira publicação manual e um workflow do GitHub Actions que envia os dois pacotes a partir de uma única tag. Todos os passos partem do princípio de que você tem um servidor stdio que já roda localmente.

💡 Antes de começar: você precisa do Node 18+ para o npm ou do Python 3.10+ para o PyPI, além de contas gratuitas no npmjs.com e no pypi.org. Ative a autenticação em duas etapas nas duas, já que ambos os registros a exigem de quem publica.

Por que publicar nos dois registros

A maioria dos servidores MCP começa em uma linguagem, geralmente TypeScript ou Python, e permanece nela. Isso funciona até que alguém de outro ambiente queira testar o seu. Uma equipe de dados em Python não vai instalar Node para rodar uma ferramenta, e uma equipe de front-end não vai montar um virtualenv. Publicar nos dois registros elimina essa desculpa.

Duas caixas de correio de madeira desgastada, uma verde e uma azul-marinho, lado a lado em uma estrada rural enevoada ao nascer do sol

Dois públicos, um servidor

Veja como os dois caminhos se comparam lado a lado:

npmPyPI
Comando de execuçãonpx -y your-packageuvx your-package
SDK oficial@modelcontextprotocol/sdkmcp (inclui FastMCP)
Manifestopackage.jsonpyproject.toml
O que é enviadoUm tarball gerado a partir de dist/Um arquivo de código-fonte mais uma wheel
Comando de publicaçãonpm publishuv publish ou twine upload
Autenticação no CIPublicação confiável ou token granularPublicação confiável ou token de API

A configuração mais limpa é uma implementação por linguagem com um único contrato de ferramentas compartilhado. Nomes de ferramentas, schemas de entrada e descrições ficam idênticos nos dois pacotes, então um prompt que funciona com a versão do npm se comporta da mesma forma com a versão do PyPI. Mantenha esse contrato em um pequeno arquivo JSON no repositório e faça o CI comparar as duas builds com ele.

Resista ao atalho de um wrapper Python simples que chama npx por linha de comando. Ele funciona até que o usuário não tenha o Node instalado, e então falha com um erro que ninguém consegue entender de relance.

Escolha a estrutura do pacote

Um único repositório com duas pastas mantém o processo de lançamento simples:

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

Vista de cima de uma bancada de carvalho com duas fileiras organizadas de caixas de papelão desmontadas ao lado de uma régua de aço e um rolo de barbante

Cada pasta é um pacote próprio, com seu próprio manifesto. O workflow de lançamento usa depois working-directory para gerar cada um separadamente, para que nada vaze de um lado para o outro.

Defina o nome uma vez, confira duas vezes

Escolha um nome e use-o nos dois registros. Os usuários se lembram dele, e os resultados de busca ficam alinhados.

  • npm: minúsculas, seguro para URL, sem espaços. Um nome com escopo como @yourscope/my-mcp-server evita colisões e funciona bem para servidores MCP.
  • PyPI: os nomes não diferenciam maiúsculas de minúsculas e tratam -, _ e . como o mesmo caractere, então My_MCP.Server e my-mcp-server entram em conflito.
  • Disponibilidade: npm view my-mcp-server retorna um 404 quando o nome está livre. No PyPI, abra pypi.org/project/my-mcp-server/ e um 404 significa o mesmo.

💡 Verifique os dois nomes antes de escrever o README em torno de um deles. Descobrir no dia da publicação que o nome já está em uso custa uma tarde de renomeação.

Escreva um README que funcione como documentação

Os dois registros exibem seu README como página do pacote, e para muitos usuários ele é a única documentação que leem. Coloque quatro coisas nele, nesta ordem:

  1. Uma frase sobre o que o servidor faz
  2. Uma configuração de cliente para copiar e colar para npx e outra para uvx
  3. Uma tabela de ferramentas com uma linha para cada uma
  4. Todas as variáveis de ambiente que o servidor lê, marcadas como obrigatórias ou opcionais

Publique o pacote npm

Configure o package.json

Três campos determinam se npx funciona de fato: bin, files e o shebang no arquivo de entrada.

{
  "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 associa o nome do comando ao arquivo de entrada compilado. Sem ele, npx não tem nada para executar.
  • files é uma lista de permissões. Apenas dist/, o README e a licença entram no tarball.
  • prepublishOnly recompila logo antes de cada publicação, então você nunca envia uma saída desatualizada.
  • O shebang #!/usr/bin/env node precisa ser a primeira linha de src/index.ts. O TypeScript mantém essa linha no arquivo compilado.

Close de mãos embalando uma pequena caixa de papelão com papel kraft e um envelope selado com lacre de cera sobre uma mesa de oficina

Confira o tarball antes de enviar

Execute npm pack --dry-run e leia a lista de arquivos que ele imprime. Você quer ver dist/, o README, a licença e package.json. Você não quer ver .env, fixtures de teste, source maps que não pretendia compartilhar, nem um node_modules perdido.

💡 Um .env vazado é o erro mais comum em uma primeira publicação, e uma versão publicada não pode ser retirada. A lista de permissões files é sua rede de segurança, então mantenha-a.

Faça a primeira publicação

npm login
npm publish --access public

Pacotes com escopo são privados por padrão, por isso --access public é importante na primeira publicação. Digite o código da autenticação em duas etapas quando for solicitado. Depois, comprove que funciona a partir de outra pasta:

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

O processo deve iniciar e aguardar entrada no stdin. Esse silêncio é o comportamento correto, porque um servidor stdio só fala quando um cliente se comunica com ele.

Publique o pacote PyPI

Escreva o pyproject.toml

O empacotamento em Python é feito em um único arquivo. Esta versão usa hatchling como 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"

A tabela [project.scripts] equivale a bin. Ela cria um comando na instalação. Se o nome do script for igual ao nome do pacote, uvx my-mcp-server funciona direto. Se for diferente, execute uvx --from my-mcp-server script-name.

Um servidor mínimo usando FastMCP do SDK oficial de Python:

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

Mãos desgastadas alinhando uma roda de bicicleta de aço em uma oficina, com os raios se estendendo pelo quadro

Gere e verifique os arquivos

cd python
uv build
uvx twine check dist/*

uv build grava dois arquivos em dist/: um arquivo de código-fonte (.tar.gz) e uma wheel (.whl). As wheels instalam rápido porque nada precisa ser compilado, e por isso uvx e pip as preferem. twine check confirma que o README é exibido corretamente como página do pacote, assim você descobre metadados quebrados antes que o PyPI os descubra.

Teste no TestPyPI e depois publique

O TestPyPI é um site separado, com contas e tokens separados. Ele existe para que um primeiro envio com falha não custe nada.

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

O índice extra é importante. Suas dependências, incluindo mcp, ficam no PyPI real, e o TestPyPI não as tem. Quando a instalação de teste funcionar, publique de verdade:

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

💡 As versões são permanentes nos dois registros. O PyPI nunca aceita o mesmo nome de arquivo duas vezes, mesmo depois de uma exclusão, e o npm recusa reutilizar um número de versão já publicado. Quando algo estiver errado, aumente a versão e publique de novo.

Teste antes de enviar

Perfil lateral de um relojoeiro inspecionando um mecanismo com lupa em uma bancada

Execute no MCP Inspector

O MCP Inspector abre uma página web local onde você lista ferramentas, preenche argumentos e lê as respostas brutas. Aponte-o para a saída gerada e depois para o pacote exatamente como um usuário o executaria:

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

O segundo comando é o mais importante. Ele testa o pacote instalado, e não a sua pasta de trabalho, então arquivos ausentes e pontos de entrada errados aparecem aqui e não em um relatório de bug.

Mantenha o stdout limpo

No stdio, o stdout transporta o protocolo. Um console.log() ou print() perdido injeta texto no fluxo JSON-RPC, e o cliente se desconecta com um erro de parse. Envie cada linha de log para o stderr: console.error() no Node, e print(..., file=sys.stderr) ou o módulo logging em Python.

Teste a configuração do cliente

Esta é a configuração que seus usuários vão colar. Teste as duas entradas em um cliente real:

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

Execute a checklist pré-publicação a partir de um shell limpo em uma pasta temporária. Uma instalação global ou um node_modules próximo pode esconder um arquivo ausente por semanas.

  • npm pack --dry-run lista apenas o que você pretende enviar
  • twine check dist/* passa sem avisos
  • O Inspector lista todas as ferramentas por meio de npx e por meio de uvx
  • Nada escreve no stdout além de mensagens do protocolo
  • Os blocos de configuração do README correspondem ao que você acabou de testar

Automatize e versione os lançamentos

Vista em ângulo baixo de pacotes de papel kraft deslizando por uma esteira de rolos de aço em direção a uma porta de carga em uma sala de embalagem iluminada

Publicação confiável, sem tokens armazenados

Os dois registros permitem que um workflow do GitHub Actions publique por meio de OpenID Connect. Você registra o repositório e o arquivo do workflow no lado do registro uma única vez, e a partir daí o registro confia naquele workflow exato. Nenhum token de longa duração fica nos segredos do repositório, então não há nada para vazar ou rotacionar. O workflow só precisa da permissão id-token: write.

No PyPI, adicione um publicador confiável nas configurações de publicação do projeto. Um projeto novo pode usar um publicador pendente, para que a primeira versão também possa vir do CI. No npm, adicione o publicador confiável nas configurações do pacote. As telas de configuração mudam de tempos em tempos, então siga os passos atuais.

Uma tag, duas publicações

Enviar uma tag como v0.1.0 dispara os dois jobs em paralelo:

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

A etapa npm install -g npm@latest garante que a CLI do npm seja recente o bastante para a publicação confiável. Adicione uma etapa needs: com seu job de testes se quiser que uma build com falha bloqueie o lançamento.

Semver que os clientes respeitam

Os clientes MCP e os agentes por trás deles dependem dos nomes das ferramentas e dos schemas de entrada, então trate-os como sua API pública:

MudançaIncremento de versão
Corrigir um bug, sem alterar o schemaPatch (0.1.1)
Adicionar uma nova ferramenta ou um argumento opcionalMinor (0.2.0)
Renomear ou remover uma ferramenta, ou adicionar um argumento obrigatórioMajor (1.0.0)

Atualize os dois manifestos para o mesmo número em um único commit e, depois, crie a tag. Um pequeno script que edita package.json e pyproject.toml juntos evita o desencontro clássico em que o npm está em 1.2.0 e o PyPI em 1.1.0. Quem quer estabilidade pode fixar a versão major na configuração, por exemplo @yourscope/my-mcp-server@1.

Escadaria de pedra iluminada pelo sol subindo um jardim em terraços com três patamares distintos

Depois que os dois pacotes estiverem no ar, você também pode listar o servidor no MCP Registry oficial. Ele verifica a propriedade lendo um campo mcpName em package.json e uma linha mcp-name: correspondente no README do PyPI, depois publica os metadados com o comando mcp-publisher. O registro ainda está evoluindo, então leia a documentação atual antes de depender do formato exato.

Adicione ferramentas de imagem e vídeo

Um servidor publicado fica mais útil no momento em que consegue gerar algo. Geração de imagens e vídeos são as ferramentas mais pedidas, e a API do PicassoIA torna a inclusão rápida. A URL base é https://api.picassoia.com/v1, a autenticação é um token Bearer que começa com pia_sk_, e os trabalhos são assíncronos: você cria uma predição, consulta o status e depois lê o resultado.

Quatro modelos estão disponíveis pela API: Picasso IA Image, Picasso IA Image Editor Pro, Picasso IA Video e Seedance 2.5 Lite, que adiciona áudio aos seus clipes. Uma conta pode executar 5 predições ao mesmo tempo, e os prompts podem chegar a 4.000 caracteres.

Estúdio de fotografia com uma câmera sem espelho em um tripé apontada para um vaso de cerâmica diante de um fundo de papel cinza

Chame a API a partir de uma ferramenta. Este auxiliar em TypeScript cria uma predição no Picasso IA Image e consulta até que ela termine:

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));
  }
}

Consulte a documentação da API para os campos de entrada exatos de cada modelo, porque o formato de output e os parâmetros aceitos mudam de um modelo para outro.

Passe o token pela configuração do cliente. Nunca embuta um token no pacote. Leia-o do ambiente e deixe cada usuário defini-lo na própria configuração MCP:

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

💡 Leia os requisitos atuais de planos na página da API do PicassoIA antes de prometer uso gratuito no seu README. A redação sobre preços pode mudar, e seus usuários vão cobrar o que você escreveu.

Rascunhe as notas de lançamento com um LLM. Um modelo de linguagem pode cuidar da tarefa que faz as pessoas pularem os changelogs. Aqui está um fluxo rápido com o Claude Sonnet 5:

  1. Execute git log v0.1.0..HEAD --oneline e copie a saída.
  2. Abra a página do modelo no PicassoIA e cole o log com uma instrução de uma linha: agrupe as mudanças em Added, Changed e Fixed, em linguagem simples.
  3. Diga a ele quais mudanças afetam nomes de ferramentas ou schemas, para que sejam marcadas como incompatíveis.
  4. Compare o resultado com o diff e cole-o na release do GitHub.

Para uma segunda opinião sobre o seu pyproject.toml ou o arquivo de workflow, o GPT 5.6 Sol é um bom revisor para tarefas de programação.

Experimente você mesmo no Picasso IA

A página do seu pacote merece uma imagem de destaque de verdade e um clipe de demonstração curto, não uma captura de tela de um terminal. Crie a imagem de destaque com o Picasso IA Image, refine os detalhes com o Picasso IA Image Editor Pro e depois anime o quadro final em um clipe curto com o Picasso IA Video.

Plano por cima do ombro de uma jovem sentada em uma mesa ensolarada, sorrindo para o notebook ao lado de um caderno de esboços e uma câmera analógica

Um primeiro experimento simples:

  • Escreva um prompt de 40 palavras descrevendo uma mesa de desenvolvedor calma sob luz da manhã
  • Gere três variações e fique com a mais nítida
  • Salve-a como imagem de destaque no seu README
  • Anime-a para o anúncio do lançamento

Veja todos os modelos disponíveis em picassoia.com/en/all-models, escolha um que combine com o seu estilo e publique algo que valha a pena abrir. Seu primeiro lançamento está a uma tag de distância.

Compartilhe este artigo

Escolha seu idioma