shop-mcp
Servidor MCP somente leitura de catálogo e estoque da Shopify para Claude. Apenas biblioteca padrão do Python, sem dependências, sem SDK MCP.
Documentação
shop-mcp
Um servidor Model Context Protocol que permite a um agente LLM responder perguntas sobre o catálogo e o estoque de uma loja Shopify — via stdio, a partir de um único arquivo, usando apenas a biblioteca padrão do Python.
Sem SDK MCP. Sem requests. Sem cliente GraphQL. python3 shop_mcp.py é toda a
instalação.
$ python3 shop_mcp.py --self-test
all green: 200 assertions
Esse comando não precisa de credenciais nem de rede. É o objetivo do repositório: a camada de protocolo e a camada de ferramentas são ambas exercitadas de verdade, porque o transporte Shopify é substituído em uma junção, em vez de ser simulado na fronteira.
Instalado a partir do PyPI, o mesmo comando reporta 183, e a diferença de dezessete asserções é um fato de empacotamento, não uma verificação mais fraca:
$ uvx --from shop-mcp shop-mcp --self-test
all green: 183 assertions
manifest.json (5 asserções), llms-install.md (11) e README.md (1) são
deliberadamente não incluídos em site-packages — o entry_point do manifesto
nomeia um caminho de bundle que não existe em uma cópia instalada, então empacotá-lo
faria uma instalação correta falhar. Todos os três grupos pulam em vez de falhar quando
seu arquivo está ausente, e é por isso que a contagem muda e o veredito não.
Clone o repositório para executar todos os 200.
Este servidor é o exemplo trabalhado, não uma linha de produtos. Eu construo o mesmo formato — um servidor MCP via stdio sobre dados que você já tem, com uma suíte de autoteste e prova de que a suíte detecta defeitos injetados — como um trabalho de preço fixo: hello532.github.io/services.html, ou coolun.337@gmail.com. Nada nesta página precisa ser pago; issues e PRs são bem-vindos de qualquer forma.
Por que escrever o protocolo à mão
Porque os modos de falha de um servidor MCP via stdio são todos invisíveis localmente e todos fatais em um host. Cada um abaixo é um defeito real que este arquivo é construído para não ter, e cada um tem uma asserção que o nomeia:
- Um diagnóstico no stdout. Um único
print()corrompe a próxima análise do cliente. Nada parece errado quando você executa o servidor sozinho. Todo diagnóstico aqui vai para o stderr, e um teste afirma que o stdout permanece vazio em bytes durante uma sessão completa. - Responder a uma notificação.
notifications/initializednão temid, então uma resposta a ela é uma mensagem sem solicitação pendente. Clientes estritos tratam isso como uma violação de protocolo e encerram a conexão. id: 0lido como uma notificação.if msg.get("id")é falso para zero, então um cliente que numera solicitações a partir de zero tem sua primeira chamada silenciosamente descartada. Presença, não veracidade.- Falhas de ferramenta enviadas como erros JSON-RPC. Um erro JSON-RPC é para uma solicitação
malformada. Uma ferramenta que executou e falhou deve retornar um resultado normal com
isError: truee o motivo como texto — caso contrário, o modelo nunca vê a mensagem e não pode corrigir seus próprios argumentos. - Ecoar um
protocolVersiondesconhecido. Se um cliente pede uma revisão que o servidor não conhece, concordar com ela deixa ambos os lados acreditando que uma especificação está em uso que nenhum implementa. Isso recai para2025-03-26, o padrão da própria especificação, e diz isso. - Imprimir a resposta de forma bonita. JSON indentado contém novas linhas, e nova linha é o delimitador de quadro. Uma mensagem se torna várias quebradas.
Ferramentas
| ferramenta | responde |
|---|---|
search_products | "o que vendemos que corresponde a X" — identidade e estoque total |
get_product | um produto completo, cada variante com SKU, preço, estoque |
check_inventory | estoque para um SKU por local: disponível, comprometido, em mãos |
low_stock_report | variantes em ou abaixo de um limite, do menor para o maior |
Quatro ferramentas, escolhidas porque cada uma responde a uma pergunta que um dono de loja realmente faz. Uma superfície maior seria fácil e tornaria o modelo pior em escolher.
A correção que não é protocolo
Três das asserções cobrem erros que produzem respostas confiantemente erradas, que são piores do que erros:
- Um SKU sem aspas.
sku:SH 1é uma consulta diferente desku:"SH 1". A primeira corresponde silenciosamente às variantes erradas e reporta seu estoque como se fosse seu. SKUs são citados e aspas internas escapadas. - Uma quantidade nula lida como zero. Shopify retorna
nullpara uma variante que não rastreia inventário. Coagido para0, aparece em todo relatório de reposição para sempre. Não rastreado e fora de estoque são fatos diferentes e permanecem diferentes. scan_exhausted.low_stock_reportescaneia um número limitado de variantes. Se a varredura atingir seu limite, "nada está baixo" é indistinguível de "não olhei longe o suficiente" — então o resultado diz qual foi, e o modelo pode dizer isso também.
Além das regras de transporte que qualquer cliente Shopify precisa e a maioria ignora: uma resposta
GraphQL THROTTLED é um 200 e deve ser repetida, não lida como sucesso; um 401 deve
não ser repetido, porque esperar não corrigirá um token ruim; o backoff deve realmente
crescer.
Verificado, e não verificado
Verificado, pelo autoteste, em cada execução: 200 asserções cobrindo o
handshake, enquadramento, manipulação de notificações, presença de id, mapeamento de erros, rigor
de esquema, política de repetição e backoff, citação de SKU, manipulação de quantidade nula,
limites de limite e exaustão de varredura. Formas de fio foram tiradas do
mcp oficial do SDK Python do types.py (LATEST_PROTOCOL_VERSION,
CallToolResult, ServerCapabilities), não da memória.
Não verificado: isso nunca foi executado contra uma loja Shopify ao vivo. Não há credencial neste repositório e nenhuma sessão de API registrada. As consultas GraphQL do Shopify Admin são escritas para o esquema documentado, e cada caminho de código ao redor delas é testado contra um substituto de transporte — mas a ida e volta contra uma loja real é não comprovada, e os substitutos de teste são meu modelo do comportamento da Shopify, não a Shopify.
Essa distinção é a honesta, e é a mesma linha traçada em
gpt-ads-feed. Um README que a confunde
está pedindo para ser confiado na coisa errada.
Asserções que podem falhar
mutation_test.sh injeta defeitos conhecidos em cópias do código-fonte e afirma
que --self-test fica vermelho para cada um, nomeando qual asserção o pegou. Também sinaliza
um NO-OP EDIT quando um padrão de busca ficou desatualizado — porque uma mutação que
não se aplica não testa nada enquanto parece verde, que é o modo de falha que
torna uma suíte pior do que inútil: confiável e vazia.
Encontrou fraquezas reais na suíte em sua primeira execução, e todas as três tinham o mesmo formato: o defeito foi detectado, mas por uma exceção em vez de por uma asserção nomeada, então a mensagem não explicava nada e toda asserção depois dela nunca rodou.
Duas eram um KeyError: 'result' não tratado, de indexar uma resposta que o
defeito tinha transformado em um erro JSON-RPC. Corrigido roteando o acesso ao resultado através
de um guarda de formato, então o mesmo defeito agora reporta a tool crash returns a result, so the loop survives: reply is a JSON-RPC error {'code': -32603, ...} e as
três asserções seguintes ainda reportam seu próprio veredito.
A terceira era uma chamada de configuração nua — S.Tools(c).search_products(...), presente
apenas para tornar a asserção abaixo dela significativa. Quando o ramo de limitação foi
desabilitado, ela levantou, abortando o teste antes que essa asserção rodasse. Corrigido com
completes(), o inverso exato de raises(): o defeito agora reporta
a 200-with-THROTTLED is survivable, not a hard failure: raised ShopifyError: Throttled [THROTTLED], nomeando a regra e mantendo a causa.
Mais três defeitos surgiram apenas quando o servidor foi empacotado como um bundle .mcpb
e lançado da maneira que um host o lança, o que nenhum teste jamais tinha feito:
- O código lia
SHOPIFY_SHOP; este README e o manifesto do bundle ambos diziam aos usuários para exportarSHOPIFY_SHOP_DOMAIN. Qualquer um seguindo a documentação obteve um servidor permanentemente não configurado. Cada uma das 180 asserções passou, porque nenhuma delas comparou o código com a documentação. tools/listretornava[]até que credenciais existissem, então um host viu um servidor vazio e o reportou como quebrado — e a mensagem legível no store is configured emtools/callera inalcançável, já que nada estava listado para chamar. O docstring acima desse código afirmava o requisito oposto, e o teste abaixo dele afirmava o defeito:eq(tools, [], ...). A lista nunca dependeu de credenciais;descriptors()não tocava nenhum estado de instância e agora é umstaticmethod.--self-testfoi anunciado no docstring do módulo, mas travou dentro do bundle, que enviava apenas o arquivo do servidor. O bundle agora envia a suíte.
A primeira correção então quebrou o harness de uma maneira que vale registrar. A nova
asserção falhou quando README.md estava ausente, e o harness copiava apenas dois
arquivos, então disparou dentro de todo mutante. A execução ainda imprimiu 17 caught,
mas seis deles foram creditados a README.md is present em vez de seus próprios
rótulos: seis asserções reais poderiam estar mortas com a suíte ainda verde.
Um README ausente é um fato de empacotamento, não um defeito de código. A comparação
de suporte agora roda contra o docstring do módulo, que viaja com o
código-fonte, e o harness copia o README para que a verificação cruzada seja real.
Todas as 23 mutações são pegas por uma asserção que nomeia o que quebrou, e cada uma é creditada ao seu próprio rótulo.
Use
Instalado a partir do PyPI — nada para clonar:
export SHOPIFY_SHOP_DOMAIN=your-shop.myshopify.com
export SHOPIFY_ADMIN_TOKEN=shpat_... # read_products, read_inventory
uvx shop-mcp # or: pip install shop-mcp && shop-mcp
Claude Desktop / qualquer host MCP:
{
"mcpServers": {
"shop": {
"command": "uvx",
"args": ["shop-mcp"],
"env": {
"SHOPIFY_SHOP_DOMAIN": "your-shop.myshopify.com",
"SHOPIFY_ADMIN_TOKEN": "shpat_..."
}
}
}
}
De um clone em vez disso, quando você quiser ler o código-fonte antes de executá-lo — que é o ponto de um único arquivo sem dependências, e a única maneira de obter a suíte completa de 200 asserções:
python3 shop_mcp.py --self-test # 200 here, 183 installed; see above
python3 shop_mcp.py
{ "command": "python3", "args": ["/absolute/path/to/shop_mcp.py"] }
Sem credenciais definidas, ainda completa um handshake e serve tools/list,
então retorna isError com a variável ausente nomeada. Um host que não pode ler
tools/list reporta "servidor quebrado" e envia você procurando no lugar errado.
MIT.