npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

shopify-ops-mcp-client

v0.2.0

Published

OAuth-capable stdio bridge for a governed remote Shopify Ops MCP server.

Readme

shopify-ops-mcp-client

shopify-ops-mcp-client 是 Shopify Ops 远程 MCP 服务的轻量 stdio 桥接器。它把 MCP 协议消息通过 HTTPS 转发给中央 Streamable HTTP MCP 服务,并默认使用标准 MCP OAuth 自动管理 Access Token 和 Refresh Token。它不会保存 Shopify Admin token、店铺凭证或权限策略。

推荐方式:OAuth 自动刷新

安装到 MCP 宿主前,先在终端完成首次登录:

SHOPIFY_OPS_MCP_URL=https://mcp.example.com/mcp \
npx -y [email protected] login

命令会通过 RFC 9728 发现授权服务器,使用 Authorization Code + PKCE S256 打开浏览器,并在本机 loopback 地址接收回调。请登录 Web Console,核对客户端和权限后批准。系统无法打开浏览器时,完整授权 URL 会写到 stderr,可手动复制到浏览器;stdout 始终保留给 MCP stdio 协议。

登录后,MCP 宿主只需启动同一个桥接器,不要配置 SHOPIFY_OPS_MCP_TOKEN

SHOPIFY_OPS_MCP_URL=https://mcp.example.com/mcp \
npx -y [email protected]

桥接器会在 Access Token 到期前刷新,并保存服务端轮换后的 Refresh Token;收到 401 时最多刷新并重试一次。Refresh Token 失效或需要切换账号/授权时,重新运行 login。只有显式 login 命令会打开浏览器;普通桥接进程不会在启动或 401 恢复路径发起交互授权,没有可用凭据时会直接提示先登录。

OAuth 凭据默认保存在:

  • Linux/macOS:${XDG_CONFIG_HOME:-~/.config}/shopify-ops-mcp/oauth-credentials.json
  • Windows:%APPDATA%\shopify-ops-mcp\oauth-credentials.json

POSIX 系统中的凭据文件权限为 0600,新建的专用目录为 0700。可用绝对路径环境变量 SHOPIFY_OPS_MCP_OAUTH_CREDENTIALS_FILE 修改位置。默认回调端口为操作系统分配的随机端口;只有确实需要固定端口时才设置 SHOPIFY_OPS_MCP_OAUTH_CALLBACK_PORT。已动态注册的客户端可以在后续登录中复用同一个 Client ID,loopback 回调端口可以变化。

Refresh Token 和 Access Token 只由桥接器从专用凭据文件读取,不需要也不应放进宿主环境变量、提示词、MCP 工具参数或交给客户端 AI。子进程和服务端都无法安全修改 MCP 宿主父进程的环境变量,自动刷新更新的是凭据文件,不是 SHOPIFY_OPS_MCP_TOKEN

服务端需要发布 RFC 9728 Protected Resource Metadata 和 RFC 8414 Authorization Server Metadata,并支持 PKCE S256。默认使用 Dynamic Client Registration;已有公共 Client ID 时可设置 SHOPIFY_OPS_MCP_OAUTH_CLIENT_ID。授权服务器支持 Client ID Metadata Document 时,也可改设 SHOPIFY_OPS_MCP_OAUTH_CLIENT_METADATA_URL,两者不能同时配置。自定义 scope 使用 SHOPIFY_OPS_MCP_OAUTH_SCOPES

SHOPIFY_OPS_MCP_URL 默认是本地开发地址 http://127.0.0.1:3000/mcp。生产环境必须使用 HTTPS;HTTP 只允许 loopback 地址。

Codex 配置

先执行上面的 login,然后配置:

[mcp_servers.shopify_ops_mcp]
command = "npx"
args = ["-y", "[email protected]"]
startup_timeout_sec = 20
tool_timeout_sec = 120

[mcp_servers.shopify_ops_mcp.env]
SHOPIFY_OPS_MCP_URL = "https://mcp.example.com/mcp"

Claude Code 配置

先执行上面的 login,然后配置:

{
  "mcpServers": {
    "shopify-ops-mcp": {
      "command": "npx",
      "args": ["-y", "[email protected]"],
      "env": {
        "SHOPIFY_OPS_MCP_URL": "https://mcp.example.com/mcp"
      }
    }
  }
}

静态 Bearer 兼容方式

宿主或服务端暂不支持标准 OAuth 时,仍可显式配置受管 Token:

SHOPIFY_OPS_MCP_URL=https://mcp.example.com/mcp \
SHOPIFY_OPS_MCP_TOKEN=replace-with-managed-client-token \
npx -y [email protected]

SHOPIFY_OPS_MCP_TOKEN 存在时优先使用静态 Bearer,不会进入 OAuth 登录或自动刷新。不要把 Token 放在命令行参数中,因为参数可能出现在进程列表或 shell 历史里。每个 MCP 客户端或安装实例应使用服务器单独签发的 Token;Token binding 决定它能访问哪些店铺以及每个店铺的工具权限。请根据 Web Console 的过期时间和倒计时在到期前手动轮换、更新环境变量并重启宿主。

桥接器会原样转发客户端的 MCP initialize 请求,因此中央服务器可以记录原始 clientInfo。返回宿主时会保留服务端 instructions,并补充审批描述语言和工作流模板编写提示;其他协议消息仍原样转发。授权身份仍来自服务器签发的 Token;clientInfo 只用于诊断和追踪,不能授予权限。

工作流模板编写

当用户要求创建或修改工作流模板时,客户端初始化提示会要求 AI 每次先调用 workflow_get_template_authoring_rules,以服务端当前返回的 schema、限制和版本规则为准;随后可用 workflow_list_templatesworkflow_get_template 查找已有模板作为参考。客户端不得自行补充 topic、行为步骤、输入/输出映射、条件、重试或其他默认行为,完整定义通过 workflow_publish_template 创建;相同 key 的再次发布会生成不可变新版本。

这是运行时指引,不是客户端内置并缓存的一份模板 schema,因此服务端规则升级后不需要重新发布客户端。如果所需工作流工具没有出现在工具列表中,客户端应说明缺少对应 MCP 权限,而不是猜测格式或绕过治理。

任务式审批

这个包是协议桥接器,不读取 Codex、Claude Code 等宿主里的原始用户消息,也无法从当前一次 tools/call 推断未来还会调用哪些工具。因此它不会猜测任务描述、自动拼接相邻调用,或在本地保存任务正文。

需要人工审批的多步操作应由 AI 或宿主客户端先明确询问用户:审批任务的 description 和每个有序 action 的 purpose 要使用什么语言。用户回答前不要调用 task_submit;回答后,两类人类可读描述应统一使用所选语言,toolName 和结构化 input 不做翻译。随后一次提交自然语言任务描述和有序、精确的 actions。每个 action 必须提供简短、具体的 purpose,单独说明该 Tool Call 要完成什么,不能只重复通用工具名称。Web Console 中的一次批准覆盖这份计划里的多个 Tool Call;批准后由服务端按顺序执行,客户端不要再逐个调用其中的工具。每个 action 仍会重新经过店铺授权、Policy、保护暂停、幂等和审计链。

任务描述用于向审批人解释意图,实际授权边界是 Console 中展示的工具、输入预览、顺序和 planHash。计划变化时应重新提交任务,不应在同一个已审批任务下追加调用。

推荐调用顺序:

  1. AI 明确询问用户审批任务描述和有序 action 目的要使用什么语言,并等待用户选择。
  2. AI 完成计划,确定所有工具、准确输入和执行顺序,使用所选语言撰写 description 和每个 purpose
  3. 调用一次 task_submit,为每个 action 提供简短、具体的 purpose,为每个写 action 提供独立的 idempotencyKey,并使用稳定的 clientTaskKey 标识这份逻辑计划。
  4. 从返回值读取 task.idtask.planHashtask.statustask.executionStatusstatus: "approval_required" 表示整份计划正在等待一次人工审批。
  5. 等待审批时保留 task.id,随后调用 task_get 查询任务和逐 action 状态。task_get 只能读取同一受管 MCP 客户端创建的任务。
  6. 不要在等待期间直接调用计划内的工具。若工具、输入、目的或顺序有变化,应使用新的 clientTaskKey 重新调用 task_submit

下面的 TypeScript 示例使用已连接的 MCP SDK Client。桥接包本身只负责 stdio 与远程 HTTP 之间的协议转发,不额外提供 Shopify 业务 SDK。

import type { Client } from "@modelcontextprotocol/sdk/client/index.js";

type TaskStatus =
  | "pending"
  | "approved"
  | "not_required"
  | "rejected"
  | "expired";
type ExecutionStatus =
  | "not_started"
  | "running"
  | "succeeded"
  | "partial"
  | "failed";
type ActionStatus =
  | "planned"
  | "running"
  | "succeeded"
  | "cached"
  | "failed"
  | "skipped";

interface GovernedTask {
  id: string;
  planHash: string;
  description: string;
  status: TaskStatus;
  executionStatus: ExecutionStatus;
  executionError?: string;
  actions: Array<{
    id: string;
    purpose: string;
    toolName: string;
    executionStatus: ActionStatus;
    executionError?: string;
  }>;
}

interface TaskSubmitResult {
  status: "approval_required" | "rejected" | "running" | "completed" | "failed";
  created: boolean;
  task: GovernedTask;
}

interface TaskGetResult {
  found: boolean;
  task?: GovernedTask;
}

async function callStructured<T>(
  client: Client,
  name: string,
  args: Record<string, unknown>
): Promise<T> {
  const result = await client.callTool({ name, arguments: args });
  if (result.isError || !result.structuredContent) {
    throw new Error(
      `MCP tool ${name} failed: ${JSON.stringify(result.content)}`
    );
  }
  return result.structuredContent as unknown as T;
}

async function submitOrderReview(client: Client): Promise<TaskSubmitResult> {
  return callStructured<TaskSubmitResult>(client, "task_submit", {
    storeId: "dev-store",
    clientTaskKey: "order-review-1001-v1",
    description: "记录订单复核结论,并移除临时风险标签。",
    actions: [
      {
        actionId: "write-review-note",
        purpose: "记录复核结论",
        toolName: "shopify_append_order_note",
        input: {
          idempotencyKey: "order-review-1001-note-v1",
          orderId: "1001",
          noteFragment: "Review completed."
        }
      },
      {
        actionId: "remove-risk-tag",
        purpose: "移除已经处理的临时标签",
        toolName: "shopify_remove_order_tags",
        input: {
          idempotencyKey: "order-review-1001-tag-v1",
          orderId: "1001",
          tags: ["needs-review"]
        }
      }
    ]
  });
}

async function waitForTask(
  client: Client,
  taskId: string
): Promise<GovernedTask> {
  for (;;) {
    const result = await callStructured<TaskGetResult>(client, "task_get", {
      taskId
    });
    if (!result.found || !result.task) {
      throw new Error(`Task ${taskId} is not visible to this MCP client.`);
    }

    const task = result.task;
    if (
      task.executionStatus === "succeeded" ||
      task.executionStatus === "partial" ||
      task.executionStatus === "failed" ||
      task.status === "rejected" ||
      task.status === "expired"
    ) {
      return task;
    }

    await new Promise((resolve) => setTimeout(resolve, 2_000));
  }
}

export async function runOrderReview(client: Client): Promise<GovernedTask> {
  const submitted = await submitOrderReview(client);
  const finalTask =
    submitted.task.executionStatus === "succeeded" ||
    submitted.task.executionStatus === "partial" ||
    submitted.task.executionStatus === "failed" ||
    submitted.task.status === "rejected" ||
    submitted.task.status === "expired"
      ? submitted.task
      : await waitForTask(client, submitted.task.id);

  if (finalTask.executionStatus !== "succeeded") {
    const failedActions = finalTask.actions.filter(
      (action) => action.executionStatus === "failed"
    );
    throw new Error(
      `Task ${finalTask.id} ended as ${finalTask.status}/${finalTask.executionStatus}: ` +
        JSON.stringify(failedActions)
    );
  }

  return finalTask;
}

状态处理建议:

  • task_submit.status 是本次提交的便捷摘要:approval_required 表示等待整任务审批,running 表示正在执行,completed 表示已经成功完成,rejected 表示任务被 Policy、审批人或过期状态拒绝,failed 表示执行失败或只完成了部分 action。
  • 最终结果以 task.statustask.executionStatustask.actions 为准。executionStatus: "succeeded" 是整体成功;partialfailed 应检查每个 action 的 executionErrorrejectedexpired 不应重试原计划中的单个工具。
  • action 的 succeededcached 都表示该 action 已安全完成;skipped 通常表示任务被拒绝,或前序 action 失败后停止执行。
  • clientTaskKey 在同一客户端身份下具有幂等语义:相同 key 与相同计划会返回已有任务;相同 key 搭配不同计划会返回 task_idempotency_conflict。不要用随机重试创建重复计划。
  • 示例采用固定间隔便于说明。生产客户端应增加最大等待时间、退避、取消信号和网络错误重试,并把 task.id 持久化到当前会话或工作项中,而不是把 Token 或完整敏感输入写入日志。

发布

在仓库根目录执行:

npm run pack:client
npm run publish:client

以上命令从仓库根目录执行。客户端 publishConfig 已固定为 npm 官方 registry 的 public package;prepack 会自动生成 dist/,发布清单只包含编译后的桥接器和本 README。根目录服务器设置为 private,并有拒绝发布脚本作为额外保护。

License

MIT © 2026 neilx2ye