shop-mcp

Servidor MCP de solo lectura para el catálogo y stock de Shopify, para Claude. Solo biblioteca estándar de Python, sin dependencias, sin SDK de MCP.

Documentación

shop-mcp

Un servidor de Protocolo de Contexto de Modelo que permite a un agente LLM responder preguntas sobre el catálogo y el stock de una tienda Shopify — a través de stdio, desde un solo archivo, usando solo la biblioteca estándar de Python.

Sin SDK de MCP. Sin requests. Sin cliente GraphQL. python3 shop_mcp.py es toda la instalación.

$ python3 shop_mcp.py --self-test
all green: 200 assertions

Ese comando no necesita credenciales ni red. Es el propósito del repositorio: la capa de protocolo y la capa de herramientas se ejercitan de verdad, porque el transporte de Shopify se reemplaza en una costura en lugar de simularse en el límite.

Instalado desde PyPI, el mismo comando reporta 183, y la diferencia de diecisiete aserciones es un hecho de empaquetado, no una verificación más débil:

$ uvx --from shop-mcp shop-mcp --self-test
all green: 183 assertions

manifest.json (5 aserciones), llms-install.md (11) y README.md (1) se excluyen deliberadamente de site-packages — el entry_point del manifiesto nombra una ruta de paquete que no existe en una copia instalada, por lo que empaquetarlo haría fallar una instalación correcta. Los tres grupos se omiten en lugar de fallar cuando su archivo está ausente, por eso el número cambia y el veredicto no. Clona el repositorio para ejecutar las 200.

Este servidor es el ejemplo trabajado, no una línea de productos. Construyo la misma forma — un servidor MCP de stdio sobre datos que ya tienes, con una suite de autocomprobación y prueba de que la suite detecta defectos inyectados — como trabajo a precio fijo: hello532.github.io/services.html, o coolun.337@gmail.com. Nada en esta página requiere pago; los problemas y las solicitudes de extracción son bienvenidos de cualquier manera.

Por qué escribir el protocolo a mano

Porque los modos de fallo de un servidor MCP de stdio son todos invisibles localmente y todos fatales en un host. Cada uno de los siguientes es un defecto real que este archivo está construido para no tener, y cada uno tiene una aserción que lo nombra:

  • Un diagnóstico en stdout. Un print() perdido corrompe el siguiente análisis del cliente. Nada parece mal cuando ejecutas el servidor tú mismo. Cada diagnóstico aquí va a stderr, y una prueba afirma que stdout permanece vacío en bytes durante una sesión completa.
  • Responder a una notificación. notifications/initialized no tiene id, por lo que una respuesta a ella es un mensaje sin solicitud pendiente. Los clientes estrictos lo tratan como una violación del protocolo y cortan la conexión.
  • id: 0 leído como notificación. if msg.get("id") es falso para cero, por lo que un cliente que numera solicitudes desde cero tiene su primera llamada silenciosamente descartada. Presencia, no veracidad.
  • Fallos de herramientas enviados como errores JSON-RPC. Un error JSON-RPC es para una solicitud malformada. Una herramienta que se ejecutó y falló debe devolver un resultado normal con isError: true y la razón como texto — de lo contrario, el modelo nunca ve el mensaje y no puede corregir sus propios argumentos.
  • Eco de un protocolVersion desconocido. Si un cliente solicita una revisión que el servidor no conoce, aceptarla deja a ambos lados creyendo que se usa una especificación que ninguno implementa. Esto retrocede a 2025-03-26, el valor predeterminado de la propia especificación, y lo dice.
  • Impresión bonita de la respuesta. El JSON indentado contiene nuevas líneas, y la nueva línea es el delimitador de marco. Un mensaje se convierte en varios rotos.

Herramientas

herramientaresponde
search_products"qué vendemos que coincida con X" — identidad y stock total
get_productun producto completo, cada variante con SKU, precio, stock
check_inventorystock para un SKU por ubicación: disponible, comprometido, en mano
low_stock_reportvariantes en o por debajo de un umbral, las más bajas primero

Cuatro herramientas, elegidas porque cada una responde una pregunta que un dueño de tienda realmente hace. Una superficie más amplia sería fácil y haría que el modelo fuera peor al elegir.

La corrección que no es protocolo

Tres de las aserciones cubren errores que producen respuestas confiadamente incorrectas, que son peores que los errores:

  • Un SKU sin comillas. sku:SH 1 es una consulta diferente de sku:"SH 1". La primera coincide silenciosamente con las variantes equivocadas y reporta su stock como si fuera tuyo. Los SKU se citan y las comillas internas se escapan.
  • Una cantidad nula leída como cero. Shopify devuelve null para una variante que no rastrea inventario. Coaccionado a 0, aparece en cada informe de reposición para siempre. No rastreado y agotado son hechos diferentes y permanecen diferentes.
  • scan_exhausted. low_stock_report escanea un número limitado de variantes. Si el escaneo alcanza su límite, "nada está bajo" es indistinguible de "no miré lo suficiente" — por lo que el resultado dice cuál fue, y el modelo puede decirlo también.

Más las reglas de transporte que cualquier cliente de Shopify necesita y la mayoría omite: una respuesta GraphQL de THROTTLED es un 200 y debe reintentarse, no leerse como éxito; un 401 no debe reintentarse, porque esperar no arreglará un token malo; la retrocesión debe crecer realmente.

Verificado, y no verificado

Verificado, por la autocomprobación, en cada ejecución: 200 aserciones que cubren el apretón de manos, el enmarcado, el manejo de notificaciones, la presencia de id, el mapeo de errores, la estrictez del esquema, la política de reintento y retrocesión, las comillas de SKU, el manejo de cantidad nula, los límites de umbral y el agotamiento del escaneo. Las formas de cable se tomaron del mcp oficial del SDK de Python de types.py (LATEST_PROTOCOL_VERSION, CallToolResult, ServerCapabilities), no de memoria.

No verificado: esto nunca se ha ejecutado contra una tienda Shopify en vivo. No hay credenciales en este repositorio ni sesión de API registrada. Las consultas GraphQL de Shopify Admin están escritas según el esquema documentado, y cada ruta de código alrededor de ellas se prueba contra un doble de transporte — pero el viaje de ida y vuelta contra una tienda real no está probado, y los dobles de prueba son mi modelo del comportamiento de Shopify, no Shopify.

Esa distinción es la honesta, y es la misma línea trazada en gpt-ads-feed. Un README que la difumina pide que se confíe en lo incorrecto.

Aserciones que pueden fallar

mutation_test.sh inyecta defectos conocidos en copias del código fuente y afirma que --self-test se pone rojo para cada uno, nombrando qué aserción lo atrapó. También marca un NO-OP EDIT cuando un patrón de búsqueda se ha vuelto obsoleto — porque una mutación que no se aplica no prueba nada mientras se ve verde, que es el modo de fallo que hace que una suite sea peor que inútil: confiable y vacía.

Encontró debilidades reales en la suite en su primera ejecución, y las tres tenían la misma forma: el defecto se detectaba, pero por una excepción en lugar de una aserción nombrada, por lo que el mensaje no explicaba nada y cada aserción después nunca se ejecutaba.

Dos fueron un KeyError: 'result' no manejado, por indexar una respuesta que el defecto había convertido en un error JSON-RPC. Se arregló enrutando el acceso a resultados a través de un guardián de forma, por lo que el mismo defecto ahora reporta un fallo de herramienta devuelve un resultado, por lo que el bucle sobrevive: la respuesta es un error JSON-RPC {'code': -32603, ...} y las tres aserciones siguientes cada una reportan su propio veredicto.

La tercera fue una llamada de configuración desnuda — S.Tools(c).search_products(...), presente solo para hacer significativa la aserción debajo. Cuando la rama de limitación se deshabilitó, se elevó, abortando la prueba antes de que esa aserción se ejecutara. Se arregló con completes(), el inverso exacto de raises(): el defecto ahora reporta un 200-con-THROTTLED es sobrevivible, no un fallo duro: se elevó ShopifyError: Throttled [THROTTLED], nombrando la regla y manteniendo la causa.

Tres defectos más surgieron solo cuando el servidor se empaquetó como un paquete .mcpb y se lanzó de la manera en que un host lo lanza, lo que ninguna prueba había hecho nunca:

  1. El código leía SHOPIFY_SHOP; este README y el manifiesto del paquete ambos decían a los usuarios que exportaran SHOPIFY_SHOP_DOMAIN. Cualquiera que siguiera los documentos obtenía un servidor permanentemente no configurado. Cada una de las 180 aserciones pasó, porque ninguna comparaba el código con los documentos.
  2. tools/list devolvía [] hasta que existieran credenciales, por lo que un host veía un servidor vacío y lo reportaba roto — y el mensaje legible no hay tienda configurada en tools/call era inalcanzable, ya que no había nada listado para llamar. El docstring sobre ese código declaraba el requisito opuesto, y la prueba debajo afirmaba el defecto: eq(tools, [], ...). La lista nunca dependía de credenciales; descriptors() no tocaba ningún estado de instancia en absoluto, y ahora es un staticmethod.
  3. --self-test se anunciaba en el docstring del módulo pero se estrellaba dentro del paquete, que enviaba solo el archivo del servidor. El paquete ahora envía la suite.

El primer arreglo luego rompió el arnés de una manera que vale la pena registrar. La nueva aserción falló cuando README.md estaba ausente, y el arnés copiaba solo dos archivos, por lo que se disparó dentro de cada mutante. La ejecución aún imprimió 17 caught, pero seis de esos se acreditaron a README.md is present en lugar de sus propias etiquetas: seis aserciones reales podrían haber estado muertas con la suite aún verde. Un README faltante es un hecho de empaquetado, no un defecto de código. La comparación de carga ahora se ejecuta contra el docstring del módulo, que viaja con el código fuente, y el arnés copia el README para que la verificación cruzada sea real.

Las 23 mutaciones son atrapadas por una aserción que nombra qué se rompió, y cada una se acredita a su propia etiqueta.

Úsalo

Instalado desde PyPI — nada que 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 / cualquier host MCP:

{
  "mcpServers": {
    "shop": {
      "command": "uvx",
      "args": ["shop-mcp"],
      "env": {
        "SHOPIFY_SHOP_DOMAIN": "your-shop.myshopify.com",
        "SHOPIFY_ADMIN_TOKEN": "shpat_..."
      }
    }
  }
}

Desde un clon en su lugar, cuando quieras leer el código fuente antes de ejecutarlo — que es el punto de un solo archivo sin dependencias, y la única manera de obtener la suite completa de 200 aserciones:

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"] }

Sin credenciales establecidas, aún completa un apretón de manos y sirve tools/list, luego devuelve isError con la variable faltante nombrada. Un host que no puede leer tools/list reporta "servidor roto" y te envía a buscar en el lugar equivocado.

MIT.