Shopify MCP Demo
Shopify MCP 演示
在 ChatGPT 或 Claude 中浏览真实的 Shopify 商店,并使用 Cashfree 完成支付—— 包括商品目录、购物车、OTP 登录、已保存地址和支付,全程无需离开对话界面。
"show me shirts from the store"
↓ SearchProducts
product grid ⇄ product detail
└────────┬───────┘
↓
cart → phone → OTP → address → payment
↓
Cashfree → order summary每次支付流程最终都会停留在同一个界面上:显示已购买的商品及其数量和价格,以及订单 ID 和状态。Cashfree 确认资金已转移,但由于它从未接触 Shopify 购物车,因此无法得知购物车中的具体内容。
支付后再次搜索,小部件会启动全新的购物会话——新的购物车,不会保留之前的收据。在一次对话中购买两次也能正常工作。
工作原理
该服务器同时扮演三个角色:
作为 AI 主机的 MCP 服务器,暴露一个面向模型的工具(
SearchProducts)和一个 widget 资源作为 Shopify 的 MCP 客户端,通过 JSON-RPC 与
https://{SHOP_DOMAIN}/api/ucp/mcp通信——无需认证,店铺域名就是全部配置作为 Cashfree 的 REST 客户端,用于创建订单、OTP 登录、获取已保存地址和查询订单状态
React widget 掌控整个购物流程。只有商品搜索会触及模型;之后的所有操作都是 widget 到服务器的通信,这保证了流程的确定性。
浏览分为两个界面。网格视图为每个商品展示一张卡片,包含价格范围和可选规格数量;点击卡片进入详情界面,显示商品描述、规格选择器和"加入购物车"按钮。单一规格的商品可以直接从卡片加入购物车,已在购物车中的商品会显示数量调节器——这样常见操作只需一次点击,只有真正需要选择时才进入详情界面。两个界面都会显示购物车中的商品数量徽标,这两个数量来自同一个函数。
Related MCP server: Shopify Agentic MCP Gateway
设置
npm install
cp .env.example .env # set SHOP_DOMAIN and the Cashfree keys
npm run build
npm start暴露服务(ngrok http 8787),在 .env 中设置 SERVER_URL 为公网地址,重启服务,然后在主机中添加 <public-origin>/mcp 作为连接器。
只需重启即可——无需重新构建。公网地址在启动时读取,并在每次提供 widget 资源时注入到 widget HTML 中。
变量 | 用途 |
| 店铺地址。修改此行并重启即可切换店铺。 |
| 每次 UCP 调用都需要。Shopify 的公开示例配置文件在演示中即可使用。 |
|
|
| 控制台 → 开发者 → API 密钥。 |
| Cashfree 将买家重定向到的地址。默认为一个稳定的托管页面。 |
| 使用隧道时的公网地址。 |
| 默认为 8787。 |
|
|
运行时的预期表现
支付功能正常,但已保存的银行卡除外。 主机根据 MCP 注释来限制支付工具。cashfree-here 提供真实的注释——对于扣款工具使用 { readOnlyHint: false, destructiveHint: true }——主机据此拒绝调度:模型形成意图,主机预取工具的 widget 模板,但永远不会收到 tools/call 请求。
设置 PAYMENT_ANNOTATIONS=readonly 会将其覆盖为 { readOnlyHint: true, destructiveHint: false },此时五个工具中有四个可以调度:UPI、网银、托管结账、新卡——以及一旦交接完成后,已保存的银行卡也可以。
这个标志仅用于测量,并非解决方案。它让一个实际转移资金的工具声称自己什么都不做,这恰恰是对用于捕获此类行为的精确控制机制的欺骗。该标志默认关闭,首次使用时打印警告,且不得用于生产环境。
当工具被阻止时,widget 会显示提示并提供 Cashfree 链接,该链接可以正常使用。在标签页中完成支付后返回,widget 会确认订单状态。
交接延迟,但不会丢失——"被阻止"的判断可能不准确。 此文件之前提到 widget 到模型的交接大约有一半概率失败。但一次捕获的会话显示并非如此:CheckoutTool 在 widget 两次尝试(2 × 4秒)后被声明为已阻止,买家点击了 Cashfree 链接,但 tools/call 最终还是到达了——大约在点击后 2-4 秒,所以端到端大约 12-20 秒。服务器在 37 毫秒内处理完毕。延迟的每一毫秒都来自上游。
sendFollowUpMessage 不会要求主机运行工具。它发送一条用户消息,并在该消息送达后解析,因此 widget 的确认窗口从入队时开始计时,然后等待主机基础设施完成一次完整的模型推理——而 4 秒的超时从未针对此场景进行校准。
后果比界面响应慢更严重:买家在外部链接上完成支付时,同一个 payment_session_id 的 Cashfree widget 正在他们背后渲染。一个订单对应两个活跃的支付界面。DISPATCH_ATTEMPTS = 2 会发送两次后续消息,因此可能出现两个 widget。
"一半概率失败"的结论是我们自己的 bug。 MCP Apps 传输只提供一个 postMessage 通道,但有两个 App 实例被打开:useMcpApp 构建并连接一个用于渲染,getClientPlatform() 构建并连接另一个用于支付交接。握手过程存在竞争,失败的一方之后对所有请求都回答 "未连接"——买家选择支付方式后,tools/call 永远不会到达服务器。这种抛硬币式的交接正是双向竞争的表现,而非主机不稳定。该 hook 现在订阅共享客户端,connect() 是幂等的,卸载时不再对生命周期超过 React 树的单例调用 close()。
自修复以来,每次调度都首次成功。attemptsFor() 在 MCP Apps 主机上已返回 1,因此重试仅适用于 ChatGPT,它使用 LegacyOpenAiClient 且从未出现此竞争——没有测量数据支持移除重试逻辑,因此保留。
UPI 在 ₹1,00,000 以上会失败,提示"此订单不支持该支付方式"——这是 UPI 的单笔交易限额,通过二分法验证:₹99,600(成功)/ ₹100,800(失败)。网银和托管结账的限额更高。购物车没有上限,因此点击几次 + 就可能超出限额而没有任何警告。
在两个主机中,重新加载主机窗口都是安全的。 购物车、结账步骤、已保存地址和支付界面都会恢复;购物车内容从持久化状态恢复而非重新获取,因此在 Claude 中重新加载不会对 Shopify 产生任何调用。ChatGPT 不会重新传递工具结果,因此只需一次 search_catalog 调用来重建商品网格,无需更多操作。在四个流程中测量,包含多个步骤的重新加载:18 次上游调用,没有重复。
构建 ID 会显示在支付界面上。 主机缓存 widget 实例,缓存的实例与当前实例无法区分——多次调试都针对已删除的代码进行。如果构建 ID 与运行中的服务器不匹配,说明你看到的是过期的 widget。
重新构建不会影响浏览器,新建对话也不会。 widget URI 包含构建 ID,因此每次构建都是不同的资源,但这仍然不够:Claude 在重新构建和新建对话后仍然提供缓存的 widget,日志中完全没有 resources/read 记录。两次调试都针对从未执行的检测代码。强制重新读取的唯一可靠方法是断开并重新连接连接器。在信任任何看到的内容之前,请检查日志中是否有 POST /mcp (resources/read ui://widget/shopify-store-<build>.html)。
重新连接仍然不是全部:主机缓存的工具元数据可能引用早期构建的 URI,因此即使是新建的对话也可能请求此服务器不再拥有的构建 ID。服务器会为任何构建 ID 提供响应——见下文"对 widget URI 进行版本管理以使其失效"——但 resources/read 行中的 ID 是主机的,不一定是运行中服务器的。支付界面上的 window.__BUILD__ 才能告诉你哪个 bundle 正在执行。
已知限制
卡输入在 Claude 中无法渲染,且上游不会修复。 Cashfree Elements 将其 PCI 字段挂载为嵌套的跨域 iframe。 Claude 执行
frame-src 'self' blob: data:策略,并忽略 UI 资源声明的frameDomains,因此字段加载为空白、不可点击的框。这是策略问题,而非传输中的 bug:一位 Anthropic 工程师在 2026-04-09 于 claude-ai-mcp#40 中声明,出于安全原因不允许嵌套 iframe,后续两次询问“是永久还是临时?”未获答复。connectDomains和resourceDomains在四月份已修复并可正常工作——只有框架被阻止,因此从 widget 到该服务器的fetch请求正常。Stripe 遇到了同样的问题:其 MCP Apps 文档通过
app.openLink()打开托管结账页面,而不是嵌入卡字段。这与这里的CheckoutTool形态相同,该工具目前可用,也是 Claude 上推荐的卡片处理路径。保持对话内卡输入是可行的——使用直接发布到该服务器的纯
<input>字段(无 iframe),这正是cashfree-here在提交55da139替换为 Elements 之前的功能。它通过我们的服务器传输原始 PAN(属于 SAQ-D 范畴),且 3DS 仍然会重定向出去,因此它只解决了表单问题,而非流程问题。未实现;是产品决策,而非技术问题。已保存卡支付在 Cashfree 内失败。
CardPaymentTool能正确获取和列出已保存卡,但使用已保存卡支付时返回HTTP 500 {"message":"Internal Server Error"},来自/pg/orders/sessions/js。已验证为单一订单问题:UPI 和网银使用相同的payment_session_id和请求头均返回 200;只有payment_method.card.instrument_id分支返回 500,无论是否提供 CVV。通用 500 是 Cashfree 侧的错误——其验证错误是带具体信息的 400。需要 Cashfree 解决。UPI 在超过 ₹1,00,000 限额时仍被提供,并在 Cashfree 侧失败,而不是在选择器中禁用。
cashfree-here已原地打补丁。 两个修复位于同级结账项目,而非此仓库:useReconciliation.start()现在会清除之前的轮询计时器,且支付成功通知每笔订单只触发一次。没有这些修复,已支付订单会每隔几秒持续向聊天发送“Payment completed successfully”,永不停息。所选地址未绑定到订单。 买家选择地址后,Cashfree 未被告知。未解决;需要 OCC 团队给出答案。
未创建 Shopify 订单。 订单仅存在于 Cashfree 中。创建订单需要 Shopify Admin API 凭证,本项目有意避免使用。
会话存储为内存型。 服务器重启会丢失正在进行的结账流程。
优惠和优惠券功能已推迟。 两个 API 均已验证并记录在
docs/cashfree-occ-api.md中;仅缺少 UI。仅支持 INR,与演示商店一致。
给任何扩展此项目的人的建议
以下发现花了大量时间验证。每项都经过测量,而非假设。
Shopify
仅针对
/api/ucp/mcp。旧版/api/mcp在 2026-08-31 后已弃用,且每个响应都明确说明。已发布的文档在三处有误:错误颠倒了目录/购物车端点划分,并将购物车商品记录为
merchandise_id,而 UCP 要求line_items[].item.id。此处的类型来自src/lib/ucp/__fixtures__/中捕获的负载,而非文档。update_cart是声明式的——每次发送完整的期望商品集合;删除商品通过省略某行来实现。货币使用最小单位,且货币单位在购物车级别保存一次。
formatMoney从Intl获取小数位数,因此零小数位货币不会被分割。受密码保护的商店仍能提供目录、购物车和结账链接。只有浏览商店首页会进入密码页面。
search_catalog返回产品描述为 HTML,且变体携带其维度作为options: [{ name, label }]。在传递给 React 之前,描述已在normalise.ts中简化为纯文本:这是商店控制的内容,渲染在与收集 OTP 相同的屏幕上,其中任何格式都不值得引入注入风险。单一变体产品仍有一个选项——Shopify 的
{ name: "Title", label: "Default Title" }占位符。渲染它会在没有选择项的产品下显示“1 个标题”。
Cashfree
x-chxs-id就是来自创建订单的payment_session_id。这要求订单在登录前存在。伪造的 ID 会返回payment_session_id_invalid。OCC 调用只需三个请求头。捕获的请求中的浏览器指纹信息——设备 ID、Forter 令牌、Cookie、来源——均未强制执行。
地址需要合并后的地址行长度为 10–185 个字符。更短会返回 400,且 UI 中无提示,除非主动检查。
地址创建响应是
{ shipping_address, billing_address },而非列表。将其解析为列表会在成功时返回空数组。/api/orders/:id代理 Cashfree 的原始响应体,因为cashfree-here的核对解析该格式。
Widget
一张卡片是一个产品;购物车中的一行是一个变体,两者并不对应。 将一件 T 恤的三种颜色合并到一张卡片中,对于浏览是合理的,但对于步进器来说模棱两可——在包含一件红色和一件蓝色的卡片下点击减号,需要猜测减去哪一件。卡片会统计该产品有多少个变体在购物车中,只有当数量恰好为一时才显示步进器;否则显示总数徽章,并引导买家进入详情页面。拒绝操作比移除他们未选择的东西更安全。
详情页面本身不持有任何状态。 所选产品和变体存储在 widget 状态中,因为 widget 会在买家滚动时重新挂载(见下文),本地
useState会丢失选择。当新的searchId出现时,它们会被清除,否则上次搜索的产品会在新结果上重新打开。
此主机
在指责平台之前,先检查
Access-Control-Allow-Headers。 此文件曾声称来自小部件 iframe 的 GET 请求永远不会到达服务器。实际上它们会到达。cashfree-here在其对账 GET 请求中发送了ngrok-skip-browser-warning,这使得请求变为预检请求;我们的允许列表中没有包含该头部,因此浏览器拒绝了预检,GET 请求从未被发送。对账随后报告"无法验证支付状态",并在已支付的订单上显示支付失败。有两件事让它看起来像是平台壁垒:日志中从未出现任何 GET 请求,并且预检请求被当作噪音过滤掉了——因此被拒绝的请求和从未发出的请求无法区分。现在
/api/*上的预检请求会被记录。基于这个错误诊断构建了仅 POST 的端点(
/api/pay/addresses/list、/api/orders/status)。它们能工作,但并非必要。只有模型调用的工具调用才会让宿主渲染该工具的
outputTemplate。callTool运行处理程序但不渲染任何内容。window.open在小部件 iframe 中被阻止,而宿主的外部打开导航会离开当前页面并在支付过程中杀死 MCP 连接。普通的<a target="_blank">是唯一有效的方式。MCP 传输是无状态的(
sessionIdGenerator: undefined)。在每次请求构建新服务器时分配会话 ID,会导致initialize之后的所有操作失败并显示"服务器未初始化"。小部件状态比小部件本身存活得更久,因此每个工具结果都必须带有日期。 宿主在整个对话中保持状态,并从中重新填充每个新小部件。因此,支付后的搜索会唤醒并持有
screen: "checkout",对"显示衬衫"的请求会返回之前的收据——而添加的下一个商品会落入 Shopify 已经完成的购物车中。SearchProducts现在每次调用都会打上searchId戳记,小部件在遇到未显示过的 ID 时会重置。无论谁拥有状态,重置就必须清除它。
useCart和useCheckoutFlow在挂载时初始化自身,并且永远不会重新读取传递给它们的内容,因此仅清除小部件状态毫无作用,它们会在一个渲染周期后直接写回其陈旧值。会话现在以searchId为键,因此 React 会丢弃它们。在渲染期间推导重置,而不是在 effect 中。 Effect 会先绘制旧屏幕;一个想要裤子的买家会先看到上一个订单的"已收到付款"出现,然后被替换。
没有东西能对活动小部件之间的写入进行排序。 对话中的每个早期小部件都保持运行,并写入同一个全局
localStorage键。revision计数器可以防止陈旧快照在实例内部替换较新的快照;但它不能对跨实例的写入进行排序,因为每个实例都会递增自己的计数器。按对话键控状态可以正确解决这个问题。小部件的重新挂载频率远高于表面所见,CORS 预检就是判断依据。 Claude 会在买家滚动时销毁并重新创建小部件 iframe,并从其自身缓存提供 HTML——因此不会出现
resources/read,重新挂载在日志中不可见。暴露这一点的是OPTIONS /api/shop/cart:预检是按文档缓存的,因此新的预检意味着新的文档。在 22:48:29 和 22:52:25 测量到,期间除了滚动什么也没发生。小部件内部的每个锁存器、ref 和观察者都会随之消亡,因此"在挂载时加载一次"不是速率限制——而是每次滚动、每个小部件的速率。IntersectionObserver无法告诉你小部件是否在屏幕上。 上述明显的修复是仅在可见时获取。但这不起作用:在嵌套浏览上下文中使用 null 根时,观察者会以该 iframe 的视口为基准,而不是宿主页面的视口。每个滚动到视线之外的小部件都报告自己完全可见。构建、测试、测量、删除——三个小部件仍然在每次重新加载时获取。缓存宿主不会返回的内容。 由于重新挂载频繁而非罕见,任何在挂载时重新获取的内容都会被不断重新获取:三个活动小部件意味着每次宿主重新加载时都会重新加载三个购物车,Shopify 最终返回了
429 速率限制超出。现在购物车主体与购物车 ID 和时间戳一起持久化,仅在超过 TTL 时重新获取。一个三流程会话从 19-20 次上游调用减少到 13 次,而两次重新加载原本需要六次调用,现在为零。TTL 最初设为 30 秒,但未能阻止任何情况:在三个流程中测量,购物车最后一次获取与下一次挂载之间的间隔分别为 32 秒、41 秒、41 秒、42 秒、53 秒、80 秒和 143 秒——每一次都超过了窗口。现在 TTL 为 10 分钟。购物车主体仅用于显示;数量变更会从服务器重新获取,支付价格来自 Shopify 的购物车,因此无法支付过时的金额。
对小部件 URI 进行版本控制会使其失效,因此要为每个构建提供服务。 URI 携带构建 ID(见下文)以绕过宿主缓存。代价是重新构建会使对话中已有的每个小部件所使用的 ID 失效:宿主重新读取它记住的 URI,服务器返回
-32602 资源未找到,这些小部件会显示"商店无法加载"。现在为ui://widget/shopify-store-{build}.html提供了一个ResourceTemplate,为已失效的 ID 提供当前包,从而升级它们而不是使其失效。注意,这在新对话中也会发生——宿主缓存的工具元数据仍然包含旧的 URI。CSP 警告可能涉及你从未选择提供的内容。 MCPJam 报告每次工具调用时
https://cdn.openai.com被阻止。这二十个引用来自katex.min.css中的@font-face规则,由 Apps SDK 的./css桶引入,用于这个小部件不会渲染的数学公式。声明该域会使 Shopify 和 Cashfree 小部件在 Claude 内部依赖 OpenAI 的 CDN;改为按路径导入其他六个样式表则移除了该引用和 21KB 的 CSS。另请注意,MCP Apps CSP 模型没有scriptSrc——只有connectDomains、resourceDomains和frameDomains——因此脚本源投诉不是服务器能够处理的。对可流式 HTTP 的 GET 分支返回 405 而非 404。 传输是无状态的,因此没有服务器到客户端的流可以打开。MCPJam 在一次会话中打开了该分支 97 次,并在未中止的情况下收到了 404,因此这并非导致其失败的原因;更严格的客户端据报告会在
initialize之前放弃。404 被解读为"没有这样的端点",这是一个不同且错误的答案。重新加载的成本因宿主而异,而 ChatGPT 是付出代价的一方。 在 Claude 中,重新加载现在没有成本:状态和购物车主体都从存储中恢复。在 ChatGPT 中,目录不会重新传递,因此
useProducts会向此服务器请求——每次重新加载一次search_catalog,没有其他操作。
端点
路径 | 用途 |
| 面向 AI 宿主的基于 HTTP 的 MCP |
| 针对 Shopify 创建/更新购物车 |
| 为重新加载后未重新传递工具结果的宿主恢复目录 |
| 创建 Cashfree 订单,价格来自 Shopify 购物车 |
| OTP 登录 |
| 已保存地址:读取和创建 |
| 支付工具处理程序是否实际运行? |
| 用于我们自己的验证屏幕的订单状态 |
| 供 |
日志
每个请求都会记录方法、路径、状态和持续时间;MCP 调用会命名方法和工具,资源读取会命名 URI——当每个宿主调用看起来都相同时,仅凭 POST /mcp 本身是无法阅读的。
13:59:48.201 → POST /mcp (tools/call SearchProducts) 200 328ms
13:59:52.884 → POST /api/shop/cart 200 904ms
14:00:03.117 → POST /api/pay/order 200 1026ms
14:00:09.640 ✗ POST /api/pay/addresses 502 121ms时间戳的存在是因为仅凭持续时间无法测量两个请求之间的间隔,而当支付调度延迟到达时,这是唯一重要的问题。
测试
npm test # watch
npm run test:run
npm run type-check测试位于它们所覆盖的代码旁边。src/lib/ucp/__fixtures__/ 下的夹具是真实捕获的 Shopify 响应,因此形状变更会导致测试失败而非演示失败。Cashfree 夹具是手写的并经过编辑——其会话令牌不得提交。
文档
docs/cashfree-occ-api.md— OCC 合约,已在线验证。不在 Cashfree 的已发布文档中。docs/spikes/2026-08-12-occ-spike.md— 技术验证所测量的内容。docs/superpowers/specs/— 每个里程碑的设计规范。docs/superpowers/plans/— 它们所变成的逐任务实现计划。
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables interaction with Shopify store data (products, customers, orders) via GraphQL, providing comprehensive tools for store management through Claude.873MIT
- AlicenseAqualityFmaintenanceEnables AI agents to autonomously browse inventory, negotiate terms, manage carts, and execute secure payments on Shopify stores using standardized protocols. It provides a bridge for LLMs to handle the entire commerce lifecycle from discovery to order tracking through a verifiable mandate chain.52MIT
- FlicenseNot gradedqualityFmaintenanceEnables AI assistants to query and interact with Shopify store data via the Storefront API, including products, collections, carts, and customer information.9
- FlicenseNot gradedqualityDmaintenanceAI-powered Shopify Admin via Claude + MCP, enabling full store management through conversation including products, collections, analytics, and bulk operations.
Related MCP Connectors
Manage your Savanto store from your AI: catalog, content, prompts, and analytics, by chat.
Manage your NanoCart store from any AI agent: products, orders, coupons, subscribers, reports.
Amazon brand, seller, niche & buy-box intelligence inside your own Claude or ChatGPT.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/droiddevgeeks/shopify-mcp-demo'
If you have feedback or need assistance with the MCP directory API, please join our Discord server