storefront-mcp について
storefront-mcpは、AIエージェントに商品カタログを公開しつつ、バックオフィス操作を制限したいチーム向けのEC用MCPサーバーテンプレートです。ツールはアダプターの実装内容に応じて提供され、公開操作と認証が必要な機密操作を分離し、書き込み操作にも制御を設けています。
主な機能
- 01商品カタログ用ツールの公開
- 02公開ツールと機密ツールの分離
- 03認証情報によるバックオフィス制限
- 04書き込みツールのドライモード起動
- 05カタログアダプターの選択または実装
- 06stdioとHTTPに対応
活用シーン
- 01エージェントによる商品検索と詳細確認
- 02制御付きチェックアウトの実行
- 03認証済み呼び出し元への注文・売上データ提供
- 04アダプターを通じた独自カタログの提供
MCP 対応
ECカタログと運用ツールをMCPクライアントに公開します。公開ツールと機密ツールを分け、後者には認証を求めます。stdioとHTTPに対応します。
設定
ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。
npx -y storefront-mcpCATALOG_ADAPTERWhich catalog adapter to use: "memory" (bundled demo catalog, no setup) or "woocommerce" (Store API skeleton).MCP_SECRETシークレットUnlocks the 5 sensitive back-office tools (stock, orders, sales). On stdio the presence of this variable in the server process env unlocks them; leave it unset to expose only the 6 public catalog tools.MCP_SERVER_NAMEOverride the advertised server name.ローカルサーバーにはマシン上の実行環境(Node.js 18 以上など)が必要です。設定ファイルの場所はクライアントごとに異なります。
ツール
6掲載時のツール一覧 · 2 週間前に更新
| パラメーター | 型 | 必須 | 説明 |
|---|---|---|---|
| limit | number | いいえ | 最大結果数(デフォルトは 8、最大 25) |
| query | string | はい | 検索するフリーテキスト(例:"sketch marker warm gray") |
詳細
storefront-mcp
EC ストアフロント向けの MCP サーバーテンプレートです。 AI エージェントには商品カタログを公開し、バックオフィスはあなただけが利用できます。
(Español más abajo / スペイン語は下記をご覧ください。)
クイックスタート(30 秒)
npx storefront-mcp
これにより、stdio 経由で MCP サーバーが起動し、デモ用カタログ(同梱の memory アダプター)を 8 つの公開ツールで提供します。6 つの読み取りツールに加え、2 つの書き込みツールがあり、書き込みツールはドライモードで起動します。すべてのチェックを実行しますが、何も作成しません。MCP 設定(claude_desktop_config.json、または claude mcp add storefront -- npx storefront-mcp)に次の内容を追加すると、Claude Desktop または Claude Code に接続できます。
{
"mcpServers": {
"storefront": {
"command": "npx",
"args": ["storefront-mcp"]
}
}
}
バックオフィス用の 5 つのツールも必要ですか?stdio には HTTP ヘッダーがないため、アクセス制御にはサーバープロセスの環境変数に MCP_SECRET が存在するかどうかを使います。プロセスを起動するユーザーが、そのプロセスが実行されるマシンを管理します。
{
"mcpServers": {
"storefront": {
"command": "npx",
"args": ["storefront-mcp"],
"env": { "MCP_SECRET": "anything-non-empty" }
}
}
}
curl を使う場合は、npx storefront-mcp --http 8787 を実行すると、localhost 上のプレーン HTTP で同じ JSON-RPC コントラクトを提供します。実際の Authorization: Bearer <MCP_SECRET> チェック(下記の Next.js ルートと同じ動作)に加え、オプトイン形式の確認ページを /api/stock-alert/confirm で利用できます。
npx storefront-mcp --http 8787 &
curl -s http://127.0.0.1:8787/ -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
CATALOG_ADAPTER でアダプターを選択します(デフォルトは memory、Store API のスケルトンを使う場合は woocommerce)。独自のカタログを提供するには、アダプターを作成してください(下記参照)。CLI、Next.js ルート、レジストリエントリー(server.json)は、すべて同じツール定義と権限境界を再利用します。
これは何ですか
Model Context Protocol サーバーを Next.js App Router のルートとしてパッケージ化したもので、オンラインストアを AI エージェント(Claude、カスタム GPT、エージェントフレームワークなど、Streamable HTTP 経由で MCP を利用するものすべて)に公開します。13 個のツールを定義し、アダプターが実際に応答できるツールだけを公開します。
| 公開読み取り(認証不要) | 公開書き込み(保護あり) | 機密情報(Bearer トークン) |
|---|---|---|
search_products | create_checkout | get_stock_bulk |
get_product | subscribe_stock_alert | get_top_products |
get_variant_chart | get_recent_orders | |
list_variant_charts | get_order_status | |
get_promotions | get_sales_summary | |
get_quote |
search_products と get_product だけは常に利用できます。それ以外はすべて機能として提供されます。アダプターのメソッドを実装するとツールが表示され、実装しなければ、そのツールはデプロイ環境に存在しません。ガードレール 18 を参照してください。
書き込みツールは、意図的に公開されています。人間に代わってエージェントが購入することが目的だからです。そのため、保護にはトークンではなく動作上の制約を使います。これらのツールはドライモードで起動します。ガードレールを参照してください。
本番環境のストアフロントサーバーから切り出し、ストア固有の要素をすべて取り除いて、すっきりしたアダプターインターフェースに置き換えています。
なぜ必要か
AI エージェントは販売チャネルになりつつあります。誰かがアシスタントに「近くで在庫のある、温かみのあるグレーのアルコールマーカーを探して」と尋ねたとき、選ばれるのは、エージェントが実際に検索できるストアです。構造化された検索、実際の在庫状況、決済リンク付きの見積もりを提供できるストアです。公開 MCP エンドポイントを使えば、独自のドメインで、独自のデータを使い、独自のルールに従って、その会話にあなたのストアを登場させることができます。
中核となる設計:権限の分離
エージェントは店頭を見ることはできますが、業務の実態を見ることはできません。
各ツールは 公開 または 機密 のいずれかで、境界はプロトコル層(Next.js のルートとスタンドアロン CLI で共有される src/lib/protocol.ts)で二重に強制されます。
tools/list— 有効なAuthorization: Bearer <MCP_SECRET>ヘッダーがない場合、返されるのは公開ツールのみです。機密ツールは単にロックされるのではなく、表示されません。tools/call— それでも機密ツールの名前を推測した呼び出し元には、データ処理コードが実行される前に JSON-RPC エラー-32001が返されます。
チェックは フェイルクローズ 方式です。MCP_SECRET 環境変数が設定されていない場合、機密ツールは全員に対してブロックされます。「何も設定されていないからすべて許可」というモードはありません。トークンの比較は定時間で行われます。
トランスポートに関する補足:HTTP(Next.js のルートおよび --http モード)では、リモートの呼び出し元は信頼できないため、ゲートには Bearer ヘッダーを使います。stdio(npx storefront-mcp)ではヘッダーはありません。クライアントとサーバーは同じマシンを共有するため、ゲートとなるのはサーバープロセスの環境変数に MCP_SECRET が存在するかどうかです。同じ境界を、それぞれのトランスポートに実際に存在する信頼境界で強制しています。
データ層にも同じ分離があります。CatalogAdapter インターフェイスが扱うのは公開されているストアフロントのデータだけで、任意の OpsAdapter(注文、売上、正確な在庫数)は別の契約です。これは実装しないこともでき、その場合、機密ツールは誰にも一切通知されません。Ops の実装では、顧客の個人情報(PII)を匿名化する必要があります。明細項目に含まれるのは商品名、数量、価格であり、認証の有無にかかわらず、メールアドレス、住所、電話番号は含まれません。
2つ目の設計:書き込みツールが拒否すべきこと
読み取りツールが誤っていれば、不正確な情報を伝えます。書き込みツールが誤っていれば、在庫がない商品を販売したり、見知らぬ人にメールの一斉送信を浴びせたりすることになります。しかも、マシンの速度で、リトライループの中で、誰もその場にいないままです。
そのため、create_checkout と subscribe_stock_alert で重要なのは、何をするかではありません。何を拒否するかです。そして、第三者を守るための 拒否条件は設定で変更できません。処理を完全に無効にすること(ドライモード、キルスイッチ)はできますが、処理を有効にしたままチェックだけを外すことはできません。
以下では、各ガードレールの後に それがなければ何が起きるか を示しています。参考にすべきなのはこの部分です。ツール自体は、午後のうちに書ける数百行のコードにすぎません。
チェックアウト
1. 在庫状況はカタログではなく、在庫情報のソースを基に確認します。
「公開されていて購入可能であること」と「在庫があること」は別の問いです。ほとんどの E コマースシステムでは、それぞれ別のシステム(CMS と ERP/POS)が回答します。これがない場合: ツールは各明細を販売可能なカタログのインデックスと照合し、価格しか扱わない価格計算コードに渡します。その経路のどこでも在庫の問い合わせは行われません。エージェントは在庫が2個しかない商品を50個注文し、実際の注文と支払い可能なリンクを受け取ります。
2. 「分からない」は「在庫がない」とまったく同じようにブロックします。 在庫状況は三値で表します:{units: n, verified: true}、{units: 0, verified: true}、{units: null, verified: false}。これがない場合: 結果が数値としてモデル化されるため、失敗時には必ず 0(実際の販売を気付かれないままブロックする)か、「問題ないと仮定する」(実体のない商品を販売する)のどちらかになります。実際にあり得る「分からない」ケースは3つあります。バリアントが在庫管理システムに紐付けられていない、在庫スナップショットに該当行がない、バックエンドが停止している、のいずれも在庫数ゼロではありません。人に請求する前であれば、不明と在庫切れは同じように扱うべきです。
3. 上限チェックや在庫チェックの前に、行は SKU ではなく UNIT POOL ごとに集約されます。 前半がなければ: 1 行あたり 50 個という上限は飾りにすぎません。同じ SKU が数量 50 で 20 行あれば合計 1,000 個になり、どの行も「上限内」に収まるためです。後半がなければ――そして、これは単純な重複排除をすり抜けるケースです――{slug: "notebook-a4", qty: 50} と {sku: "NB-A4", qty: 50} は、1 つの製品と1つの在庫を表すにもかかわらず、別々のキーになります。各行は同じ 60 個の在庫に照らしてチェックされ、どちらも通過し、ストアは 100 個を販売してしまいます。この製品の同一性を解決できるのは在庫アダプターだけです。そのため、AvailabilityRow は pool フィールドを持ち、すべての集計上限をプールごとに測定します。アダプターからプールが返されない場合は、2 つの行が別物だと確認できたふりをせず、その旨をレスポンスで伝えます。
4. 無効な数量は拒否し、決して修正しません。
Math.min(Math.max(Math.floor(Number(qty) || 1), 1), 50) は入力のサニタイズに見えます。これがなければ: {qty: 0} は、エージェントにとっては「これを削除する」という意味ですが、人間に 1 個分の代金を請求する数量に変わってしまいます。また、負の数、NaN、小数は、実在しない販売を生み出します。金銭を扱うツールでは、サニタイズとは拒否して理由を説明することです。入力をもっともらしい値に書き換えるのは、意図の捏造です。
5. 価格は呼び出し元から決して受け付けず、見積もりはリクエストと照合します。 入力スキーマに価格フィールドはなく、注文を作成する前に、サーバーはカタログがリクエストされた数量に対して価格を設定したことと、unit_price × qty == line_total であることを確認します。前半がなければ: 割引ポリシーは呼び出し元が入力する内容次第になります。後半がなければ: 数量はカートから、金額は見積もりから渡されますが、両者を照合するものはありません。そのため、「親切にも」40 個を 10 個に制限する価格情報源によって、ほかのすべてのガードレールが問題なくても、40 個分を 10 個分の金額で請求する注文が作成されます。見積もりが行を拒否することは許されますが、尋ねられた内容とは違う問いに答えることは許されません。
6. 注文の作成前に在庫を確保します。確保できない場合、ライブチェックアウトを拒否します。 これはステートレスなチェックでは実現できないガードレールです。ポイント 1〜3 はすべて過去の状態を説明しています。数値を読み取るだけです。10 件の同時呼び出しがそれぞれ「残り 4 個」と読み取り、すべてのチェックに通過して、注文を作成すると、1 つもルールに違反せずに 4 個の在庫に対して 40 個を販売してしまいます。これを防げるのは在庫情報源でのアトミックな減算だけです。そのため、InventoryAdapter.reserve() はチェックの後、注文の前に実行されます。また、アダプターが確保に対応していないデプロイ環境では、オペレーターが CHECKOUT_UNRESERVED=allow を設定し、リスクを文書で了承しない限り、ライブ注文は作成されません。これがなければ: 「過剰販売を防ぐ」という主張は、リクエストが一度に 1 件だけの場合に限って成り立ちます。それは、この言葉の意味するところではありません。その後 createCheckout が失敗した場合は、確保した在庫を解放します。
7. 拒否には根拠を添えます。 成功時も失敗時も、すべての行に stock_available と stock_verified が返されます。これがなければ: 正確な在庫数を報告するツールはトークンで制限されています(バックオフィスのデータであるため)。そのため、公開エージェントは何も診断できず、やみくもに再試行し、人間にはでっち上げた理由を伝えることになります。権限境界によってエージェントに診断ツールへのアクセスを認めないのであれば、書き込みツールには診断を提供する義務があります。
8. 1 行でも問題があれば、カート全体をブロックします。 これがなければ: 人間に届くのは「たまたま通過した商品のみ」の注文ですが、そのようなカートを求めた人はいません。
9. お客様に伝える金額が、お客様がお支払いになる金額です。 税、送料、割引はアダプター側の処理です。そのため、CheckoutReceipt.total は明細の小計と異なる場合があります。異なる場合は、矛盾する2つの金額を返すのではなく、レスポンスでそれぞれが何を示すのか(amount_to_pay、charges)を明示します。これがないと: subtotal だけで終わる金額の契約では、税金が価格に含まれ、配送が無料であることを暗黙に仮定してしまいます。EU や米国の実装者は、バックエンドで税を加算すると、同じレスポンスで報告した小計と合計が一致しなくなり、加算しなければ請求額が不足します。
再入荷通知
10. 外部に作用する処理には、サーバー側の業務上の前提条件が必要です。 商品が実際に在庫切れの場合に限りメールを送信します。在庫数が確認できる場合は在庫数がゼロであること、確認できない場合はカタログで独立して在庫切れと判定されていることを確認します。これがないと: 呼び出し元が指定したアドレスにメールを送信する公開ツールは、第三者に向けたメール砲台になります。{email: [email protected], slug: <anything>} で tools/call を繰り返すと、正当なメールに見えるメッセージが何千通もあなたのドメインから送信され、クレジットと送信者の評価を消耗し、店舗に連絡したことのない相手にまで届きます。(おまけに、在庫切れになっていない商品の誤った「再入荷しました」通知も防げます。)
11. 購読だけでなく、送信にも重複排除を適用します。失敗時は安全側に倒します。 「このメールボックスはすでに購読済みか」と「このメールボックスに、この商品についてすでにメールを送り、返信がないか」は別の問いです。後者がないと: 重要なケースでは前者だけでは何も防げません。攻撃者は確認しないため、同じ呼び出しを3回行うとメールが3通送られますが、どれも技術的には重複した購読ではありません。送信前に両方を照会し、どちらか一方でも失敗したら何も送信しません。バックエンドが停止している場合、それを送信許可に変えてはなりません。
12. クォータは受信者のメールボックス単位で設定し、その永続性はアダプターに依存します。 呼び出し元の制限とは無関係に、メールボックスごとに1時間あたり3通の確認メールを許可します。メールボックス単位でないと: 文字列そのものをキーにしても、クォータとして機能しません。受信トレイの表記は無数にあるためです。victim@、victim+1@、victim+2@、v.i.c.t.i.m@、VICTIM@ はすべて同じ Gmail アカウントに届き、それぞれが新たに3通分の枠を得ます。永続性がないと: メモリ内のカウンターはコールドスタートでリセットされ、インスタンス間で分かれ、再デプロイで消えるため、上限は書面上だけのものになります。NotifyAdapter.countOptInEmails を実装すれば、上限はストレージ上でカウントされる実際の制限になります。実装しない場合、ツール自身のレスポンスには、維持できない数値を約束する代わりに quota_enforcement: "best_effort" と表示されます。
13. 自動送信メールを、呼び出し元が指定したアドレスに送ってはなりません。 ウェブのチェックアウトでは、お客様と管理者にメールを送信します。MCP ツールでは、customer_email は エージェント が入力したものです。これがないと: 「注文確認」を書き込みツールに組み込むことで、再入荷通知ツールで閉じたばかりのメール砲台を再び開くことになります。ルール:ツール入力として渡されたアドレスに送信してよいのは、ダブルオプトインの確認メール1通だけです。それ以外のメールには、事前に意図を確認する必要があります。その確認に当たるのが支払いです。(このテンプレートは運用担当者にはメールを送信しません。注文通知を送る場合は、自分のアダプターから、自分の環境変数に指定したアドレスに送信してください。draft.customer_email には決して送らないでください。)
14. 適切に構築されたトークンによるダブルオプトイン。トークンとして十分な長さのキーも必要です。 v1.<payload>.<hmac> を使い、有効期限を署名対象のペイロード内に含め、タイミングセーフな比較を行い、署名シークレットを最低32文字に設定します。シークレットがない、または短すぎる場合はフェイルクローズし、直接登録する代わりにツールは「オプトインを利用できません」と応答します。これがない場合: 署名のないトークンは偽造されます。有効期限をトークンの隣に保存すると無視されます。署名を === で比較すると、1バイトずつ漏えいします。環境変数がないと、誰でも入れる状態になります。また、STOCK_ALERT_SIGNING_SECRET=x は「空ではない」というチェックを通過してしまい、誰でも任意のアドレスに対する有効なトークンを計算して自分で POST できるようになります。誰もオプトインしていないのに、ダブルオプトインが成立する状態です。外部に表示するメッセージは「シークレットがない」「形式が不正」「署名が不正」で共通にし、「有効期限切れ」だけを区別します。これはユーザーにとって対処可能で、攻撃者にとっては役に立たない情報だからです。
15. 確認用 URL がなければ、メールも送信しません。 これがない場合: ほかの処理がすべてフェイルクローズする中で、唯一フェイルオープンする箇所になります。署名シークレットが設定されていてもサイト URL が設定されていないと、以前のツールはそのままメールを送信していました。確認リンクは、運用者が所有していないドメイン https://example.com を指し、宛先自身のアドレスを含む署名済みトークンをクエリ文字列に載せて、第三者に渡していました。リンク切れは問題です。顧客のアドレスを含むリンク切れが、他人のドメイン上にあるのはさらに問題です。
16. GET は表示のみ、POST は書き込みを行い、確認ページには確認対象を明記します。 確認ページは GET では一切書き込みを行わず、フォームの本文にトークンを含む POST のみがデータを永続化します。これがない場合: 企業のメールゲートウェイ(Safe Links、URL Defense、デスクトップ AV、クライアントのプリフェッチ機能)は、配信時にすべてのメッセージ内のすべてのリンクを取得します。被害者のアドレスに確認メールを送ると、その人が勤務する企業のセキュリティスキャナーがサブスクリプションを有効にしてしまいます。ダブルオプトインで防ごうとしていた第三者によるオプトインが、裏口から再び入り込むのです。スキャナーはリンクをたどりますが、フォームは送信しません。この考え方は、メールのリンクから実行されるあらゆる操作に当てはまります。確認、キャンセル、承認、登録解除などです。また、ページには製品名と具体的なバリエーションも表示します。製品ラインへの登録と、その製品の特定の色への登録は別のサブスクリプションであり、その違いを省いた同意画面は同意とは言えません。書き込みは冪等なので、二度クリックしても問題なく完了し、サポートへの問い合わせにはなりません。
両方のツール
17. デフォルトではドライモードで、ツールごとに無効化できます。 CHECKOUT_MODE と
STOCK_ALERT_MODE は、明示的に live に設定しない限り dry です。off にすると、
そのツールは tools/list から完全に除外されます。ドライモードで呼び出すと、すべての
チェックを実行したうえで、実行されるはずだった内容を正確に回答します。これには「これは
BLOCKED になっていたはずです。その理由はこちらです」も含まれます。これがない場合:
デプロイするだけで有効になる書き込みツールにはリハーサルの機会がなく、初回の実呼び出しが
本番環境で、実際の金銭を扱うものになります。live という完全一致の文字列だけが
ライブモードに切り替えるため、入力ミスをしても何も実行されません。
18. 正直に応答できないツールは公開しません。読み取りツールも対象です。
tools/list は最初から最後まで機能に応じて制御されます。getQuote も get_quote も
公開されません。OpsAdapter がなければ、認証済みの呼び出し元であってもバックオフィス
ツールは公開されません。CheckoutAdapter、InventoryAdapter、価格設定のいずれかが
なければ、create_checkout は公開されません。公開されていないツールは、理由を問わず
すべて同じ方法で -32601 を返します。これがない場合: スタブを出荷することになります。
以前、WooCommerce アダプターは応答できないメソッドを3つ実装していました。インターフェースを
満たすためだけに、listBrands は [] を返し、getColorCard は null を返し、
getQuote はすべての明細を拒否していました。さらにサーバーは、この3つすべてを匿名の
エージェントに公開していました。常に空のグラフ一覧、常に「見つかりません」と返す検索、
常に失敗する見積もりです。エージェントが二度も迷い込む行き止まりは、ツールが存在しない
よりも悪いものです。
19. 制限の説明は、実際の制限と一致しています。 エラーには両方のツール名を記載し、 クォータを共有していると明示します。ツールの説明にも同じ内容を記載し、デプロイ環境で 呼び出し元を識別できない場合についても説明します。これがない場合: 2つのツールを交互に 使うエージェントは、告知されたより早く制限に達し、カウンターはツールごとだと判断して 再試行します。その結果、レート制限が、止めるはずのトラフィックを発生させます。MCP サーバー では、ツールの説明とエラー文字列がエージェント向け API です。誤った制限の説明は、再試行の コストとなります。
20. レート制限のキーを呼び出し元が選ぶことはできません。 以下を参照してください。
レートリミッターについて(率直な説明)
「IP でレート制限する」場合、たいてい見落としがちな問題が2つあり、2つ目はほとんど触れられません。
キー。 x-forwarded-for の 最初の エントリを信頼してはいけないことは、誰もが知っています。
見落とされがちなのは、転送ヘッダーを書き込むのは プロキシ だという点です。前段にプロキシがない場合――VPS での node server.js、単独の next start、proxy_set_header のない nginx、パブリックポートを持つコンテナなど――最後のホップも含め、ヘッダー全体は呼び出し元が入力した文字列です。X-Forwarded-For: 203.0.113.1, .2, .3… をローテーションすれば、リクエストごとに新しいバケットが作られ、レートリミッターは何もしません。そのため、このサーバーは、エッジが書き込むヘッダーを指定しない限り、どの転送ヘッダーも信頼しません。
TRUSTED_PROXY_HEADER=x-vercel-forwarded-for # Vercel
TRUSTED_PROXY_HEADER=cf-connecting-ip # Cloudflare
TRUSTED_PROXY_HEADER=x-storefront-client-ip # the bundled WordPress proxy
ヘッダーを指定することは、2つのことを表明することになります。エッジがそのヘッダーを 上書き すること、そしてオリジンに到達できる経路がほかにないことです。オリジンにパブリックから到達できる場合、そのヘッダーは発見した誰でも偽装できます。まずオリジンを保護してください(デプロイ保護、ファイアウォール、mTLS)。何も指定されていない場合、--http サーバーは偽装できない TCP ピアアドレスを使用します。一方、サーバーレスの Fetch ハンドラーには問い合わせるソケットがないため、呼び出し元は 特定されません。呼び出し元を特定できないデプロイには、守れない呼び出し元ごとの制限を約束するのではなく、プロセス全体で毎分60件の書き込み上限を適用します。(共有の毎分5件にはしません。すべての呼び出し元をひとつの小さなバケットにまとめると、レートリミッターが自分の顧客に対するサービス拒否攻撃になり、修正対象のバグよりも深刻です。)
ストア。 同梱のカウンターは、1つのプロセスのメモリ内にある Map です。サーバーレスでは、N 個のウォームインスタンス = N 個の独立したクォータとなり、コールドスタートでリセットされ、再デプロイで消去されます。これはエージェントのループを抑えるためのものであり、WAF でも不正利用対策でもありません。そのことは、チームが実際には持っていない上限を示すのではなく、src/lib/ratelimit.ts に明記されています。rateLimited(key, max, windowMs) は呼び出し形式がひとつだけの、小さな同期ポートなので、Redis/KV/Durable Objects への切り替えは機械的に行えます。カウンターを何かに頼る前に切り替えてください。
だからこそ、重要な制限は 作用 に結び付けられています。フェイルクローズの可用性、アトミックな予約、在庫切れの事前条件、送信時点での重複排除、ダブルオプトイン、そしてキルスイッチです。これらはカウンターではなくデータによって強制されるため、実行中のインスタンス数にかかわらず機能します。受信者ごとのメールクォータはその中間に位置します。NotifyAdapter が countOptInEmails を実装していれば永続的に機能し、そうでなければベストエフォートとなります。ツールの応答にはどちらの状態かが示されるため、推測する必要はありません。
書き込みツールの設定
| Env var | デフォルト | 説明 |
|---|---|---|
CHECKOUT_MODE | dry | create_checkout の off | dry | live |
STOCK_ALERT_MODE | dry | subscribe_stock_alert の off | dry | live |
CHECKOUT_UNRESERVED | refuse | ライブモードで、在庫アダプターがユニットを 確保 できない場合の動作:refuse(注文しない)または allow(同時呼び出しで過剰販売が起こり得ることを許容し、すべてのレシートにその旨を記載) |
STOCK_ALERT_SIGNING_SECRET | (未設定) | オプトインリンク用の HMAC キー。32文字以上。未設定または短すぎる場合 ⇒ リンクを発行できず、ツールは要求を拒否します。openssl rand -hex 32 |
STOCK_ALERT_CONFIRM_URL | ${NEXT_PUBLIC_SITE_URL}/api/stock-alert/confirm | 確認ページの場所。どちらも設定されていない場合、ライブモードでは何も送信しません |
STOCK_ALERT_PAGE_LOCALE | en | ページの言語(en | es)。顧客が目にする画面の言語です |
TRUSTED_PROXY_HEADER | (未設定) | クライアント IP を設定する、エッジが書き込むヘッダーの名前。未設定 ⇒ 転送ヘッダーは無視されます |
ツールは、アクティブなアダプターがその機能を提供している場合にのみ公開されるため、上記の設定によって、アダプターが対応できないツールが再び有効になることはありません。
60 秒でガードレールを試す
同梱の玩具カタログは、すべての状態が現れるように構成されています。
chromaflow-classic-set-12 の在庫は 4 個です。CF-G09 はカタログに登録されたバリエーションですが、
在庫行はありません。fieldbook-sketch-a5 は在庫 0 個の状態で in_stock として公開されています
(これがまさに過剰販売の状態です)。そして fieldbook-sketch-a4 は通常のケースで、
スラッグと SKU FB-A4 のどちらからも参照できる在庫 60 個の商品が 1 つあります。
npx storefront-mcp --http 8787 &
# Consolidation + fail-closed: two lines of 3 for a product with 4 units.
# Per line each one "fits". Together they do not.
curl -s http://127.0.0.1:8787/ -H 'content-type: application/json' -d '{
"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"create_checkout","arguments":{
"items":[{"slug":"chromaflow-classic-set-12","qty":3},{"slug":"chromaflow-classic-set-12","qty":3}]}}}'
# → blocked, qty_requested 6, merged_from_input_lines 2, stock_available 4
# Same product, two spellings, one pile of units: 50 by slug + 50 by SKU
# against 60. Both lines "fit" on their own; the pool is what gets checked.
curl -s http://127.0.0.1:8787/ -H 'content-type: application/json' -d '{
"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"create_checkout","arguments":{
"items":[{"slug":"fieldbook-sketch-a4","qty":50},{"sku":"FB-A4","qty":50}]}}}'
# → blocked: "only 60 unit(s) available and 100 requested across the lines that
# share this stock", plus shares_stock_with on every line
# Unverifiable stock blocks like zero:
# "items":[{"sku":"CF-G09","qty":1}] → stock_verified:false, blocked
# Quantities are rejected, not repaired:
# "items":[{"slug":"fieldbook-sketch-a5","qty":0}] → items_invalid_qty, qty_received 0
ウェブエンドポイントとしてのクイックスタート(2 分)
独自のドメインから MCP を提供するには(デプロイ可能な Next.js ルートを使用します)。
git clone <this repo> && cd storefront-mcp
npm install
npm run dev
以上です。デフォルトの memory アダプターは、
examples/toy-catalog.json の玩具カタログ(架空の店舗「Demo Art Supply」)を提供します。お試しください。
# descriptor
curl http://localhost:3000/api/mcp
# list tools (public only — no token sent)
curl -s http://localhost:3000/api/mcp -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# search
curl -s http://localhost:3000/api/mcp -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"search_products","arguments":{"query":"leather dye"}}}'
# a sensitive tool without a token → -32001
curl -s http://localhost:3000/api/mcp -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_sales_summary","arguments":{}}}'
# now with the token
export MCP_SECRET=$(openssl rand -hex 32) # also set it in .env.local and restart
curl -s http://localhost:3000/api/mcp -H 'content-type: application/json' \
-H "authorization: Bearer $MCP_SECRET" \
-d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"get_sales_summary","arguments":{}}}'
Claude Code に接続するには、claude mcp add --transport http my-store http://localhost:3000/api/mcp を実行します。
デプロイする場合は、プラットフォームが設定するヘッダー(Vercel では
x-vercel-forwarded-for、Cloudflare の背後では cf-connecting-ip)を
TRUSTED_PROXY_HEADER に設定してください。そうしないと、書き込みクォータが
インスタンス全体で 1 つの上限になります。詳しくは
レートリミッターについての率直な説明をご覧ください。
独自のアダプターを作成する
プロトコル層がデータに直接触れることはありません。src/lib/adapter.ts で定義されたインターフェースを呼び出します。必須メソッドは2つです。それ以外はすべて機能であり、どのツールを利用できるかは機能によって決まります。
CatalogAdapter(必須) —searchProductsとgetProduct。必要なのはこの2つだけです。仕様上、公開データとして扱います。返すデータはすべて誰でも読み取れるものと想定してください。同じインターフェースで、以下の機能も任意で実装できます。getPromotions→get_promotionsgetQuote→get_quote(また、create_checkoutの前提条件です)listVariantCharts+getVariantChart→ 2つのチャートツール。両方をまとめて通知するか、どちらも通知しないでください。「バリアントチャート」とは、カラー、サイズ、粒度、焙煎度、容量など、バリアントごとに在庫が管理される製品ラインを指します。カタログにそのような軸がなければ、実装しないでください。スタブにする必要もありません。
OpsAdapter—getStockBulk、getTopProducts、getRecentOrders、getOrderStatus、getSalesSummary。トークンで保護されたツールを有効にします。InventoryAdapter— 明細ごとに{units, verified, reason, pool}を返すgetAvailability(refs)。これは「現時点で何個あるか」を示す情報源であり、読み取りツールが使うものと同じでなければなりません。任意ですが、重要な機能としてreserve(req)/release(id)があります。これがない場合、ライブチェックアウトは拒否されます(ガードレール 6)。CheckoutAdapter—createCheckout(draft)。InventoryAdapterおよびgetQuoteと併せて実装した場合にのみ通知されます。NotifyAdapter—isSubscribed/sendOptInEmail/confirmSubscriptionと、受信者ごとの上限を実際に適用する、任意のcountOptInEmails。
必須の2つのメソッドだけを実装したアダプターも、期待どおりに動作します。それ以外は通知されません。これはサポート対象の構成であり、機能が低下した構成ではありません。
手順:
src/lib/adapters/memory.ts(リファレンス実装)を新しいファイルにコピーし、データベース / API / ERP に接続します。src/lib/adapters/index.tsに登録し、CATALOG_ADAPTER環境変数で選択します。- コントラクトの誠実性に関するルールを守ります。
- 在庫状況を確認できなかった場合は
units: null/stock: nullを返します。数値を捏造せず、0 にフォールバックしないでください。 - すべての在庫状況の行に
poolを返します。これは、その明細が在庫を引き当てる在庫群を識別するものです。商品のスラッグと SKU は同じプールに解決される必要があります。そうでないと、同じ商品が異なる2つの方法で注文された場合、同じ在庫に対して二重に確認されます。 reserveは、1回のアトミックな操作(UPDATE … WHERE (on_hand − reserved) >= :qty)として実装し、全件成功か全件失敗とします。ロールバック用にreleaseも実装します。- すべての在庫回答を1つの情報源から提供し、書き込み処理が、顧客に表示された情報とは異なる情報を使って検証しないようにします。
- すでに別の場所で確保されている在庫(オープン中の引き当て、他の販売チャネルなど)を差し引きます。「棚にある」ことは「この顧客に販売できる」ことを意味しません。
isSubscribedは失敗時にfalseを返すのではなく、例外をスローするようにします。呼び出し元は安全側に倒し、何も送信しません。- 呼び出しごとのタイムアウトを設定し、バックエンドのハングがエージェントのハングではなく、注記の表示につながるようにします。
get_quoteでは課金せず、getQuoteは価格を設定した各明細について、渡されたslug/skuをそのまま返すようにします。また、要求された数量に対する価格を設定するか、その明細を拒否してください。チェックアウト処理では両者を比較し、一致しない場合はカートを拒否します。
- 在庫状況を確認できなかった場合は
WooCommerce のスケルトン(src/lib/adapters/woocommerce.ts)が含まれています。公開 Store API を基盤とし、カタログ関連の3つのツール(search_products、get_product、get_promotions)だけを実装しており、スタブはありません。TODO ブロックには、残りの各コントラクトが WooCommerce のインストール環境に求めるものが記されています。そこには、バリアントごとの在庫の扱いと、在庫をどのように確保するかという、こちらでは決められない2つの判断事項も含まれています。
ディスカバリー:見つけてもらうために
エージェントは、見つけられるものしか呼び出せません。discovery/ にある2つのアーティファクトは、テンプレートです。
/.well-known/mcp.json— 機械可読な記述子(discovery/well-known-mcp.json。{{DOMAIN}}を置き換え、public/.well-known/mcp.jsonから配信してください)。公開ツールのみを記載し、デプロイで実際に告知するものだけを含めてください。/llms.txt— 人間/LLMが読めるサイトガイド(discovery/llms-txt-snippet.md)。エージェント向けのポリシーセクションが含まれています。販売を確定する前に在庫を再確認すること、見積もりでは料金が発生しないこと、stock: nullは不明を意味すること、小計ではなく見積もりのamount_to_payを使用することが記載されています。
また、GET /api/mcp は JSON 記述子を返すため、エンドポイントにアクセスした人がその内容を把握できます。
公式の MCP Registry では、リポジトリのルートにある server.json がマニフェストです。stdio トランスポートを使用する storefront-mcp npm パッケージを参照しているため、レジストリクライアントは npx で実行できます。先に npm に公開してください。レジストリは公開済みの tarball 内にある mcpName を検証するため、npm にまだ存在しないバージョンを登録すると、実体のないエントリが作成されます。.github/workflows/publish-mcp-registry.yml のワークフローは、実行前にこれを確認します。
MCP でのスキル
サーバーには、プロトコル経由でスキルとして提供される、サーバーの使用手順も含まれています(SEP-2640、拡張機能 io.modelcontextprotocol/skills)。この拡張機能を実装したホストは、skills/list でスキルを検出し、resources/read で skill://storefront-mcp/SKILL.md を読み込み、エントリーに含まれる sha256 ダイジェストを検証します。実装していないホストには、同じファイルが通常のリソースとして表示されるため、問題は起きません。
この拡張機能はプロトコルリビジョン 2026-07-28 に基づいて仕様化されているため、サーバーは現在 server/discover に応答し、そのリビジョンをネゴシエートします(ステートレスで、各リクエストの _meta にバージョンが付与され、すべての結果に resultType があり、リストには ttlMs/cacheScope が含まれます)。古いクライアントは引き続き 2025-06-18 または 2025-11-25 の initialize を使うため、違いに気づきません(このリビジョンにはハンドシェイクがないため、initialize が 2026-07-28 を返すことはありません)。server/discover の応答には、スキルを指す instructions も含まれているため、この拡張機能を実装していないホスト(現在の Claude Code など)でも、モデルが判断した場合はリソースとして読み取ります。
2026-07-28 に対応するということは、このリビジョンがサーバーに拒否を指示するリクエストも拒否するということです。このサーバーが提供していないプロトコルバージョンを指定した _meta(対応バージョン一覧とともに -32022)、clientCapabilities のない最新形式のリクエスト(-32602)、および HTTP で本文に対応するヘッダー MCP-Protocol-Version、Mcp-Method、Mcp-Name がない場合や本文と矛盾する場合(-32020、HTTP 400)が該当します。最新形式のリクエストで未知のメソッドが指定された場合は 404 になります。_meta を指定しないリクエスト(旧リビジョン)は、これまでどおり処理されます。
SKILL.md は、デプロイ環境の実際の設定に基づき、呼び出し元ごとに生成されます。呼び出し元に対して tools/list が通知するツールを正確に列挙し、各書き込みツールの現在のモード(ドライまたはライブ)と、エージェントの権限の範囲(購入の明示的な依頼がなければ注文せず、その目的で提供された宛先がなければアラートを送信しない)を記載します。また、オペレーターセッションでは、利用できるバックオフィス用ツールも記載します。匿名の呼び出し元向けのスキルには、それらは記載されません。そのため、設定が変わるとダイジェストも変わります。これが意図した動作であり、ダイジェストを固定したホストは、その際にスキルを再読み込みします。
拡張機能に対応するクライアント(設定ファイルから接続する mcpc 0.7 以降など。MCP ホストであればどれでも対応します)で試してみてください。
cat > mcp.json <<'EOF'
{ "mcpServers": { "shop": { "command": "npx", "args": ["-y", "storefront-mcp"] } } }
EOF
npx -y @apify/mcpc connect mcp.json:shop @shop
npx -y @apify/mcpc @shop skills-list
npx -y @apify/mcpc @shop skills-get storefront-mcp
npm test は CLI をビルドし、両方のトランスポートをエンドツーエンドでテストします。宣言されたケイパビリティ、skills/list と skills/get の一致、エントリーと同じフィールドを持つ YAML としてのフロントマターの解析(検証用ホストで行う必要があります)、配信されたバイト列とのダイジェストとサイズの一致、スキル内のツール名と tools/list が通知するツール名の双方向での完全な一致、匿名の呼び出し元向けのスキルにバックオフィスが記載されないことを検証します。また、上記の 2026-07-28 における拒否動作と、JSON-RPC リクエストではない本文(null、数値、空のバッチ)もテストします。これらは実行されず、代わりに -32600 で応答します。
WordPress ドメインから MCP を提供する
ストアフロントが WordPress/WooCommerce で動作していても、MCP サーバーが別の場所(例:Vercel)にデプロイされている場合、wordpress-proxy/mcp-proxy.php は mu-plugin として、上流サーバーへのプロキシを通じて https://yourshop.com/api/mcp を提供します。
- 優先度 0 で
initにフックし(WordPress のルーティングより先に応答します)、 - POST 本文と
Authorizationヘッダーを変更せずに転送します(権限の分離は上流サーバーが適用します)。 - 実際のクライアントアドレスを
X-Storefront-Client-IPで転送し、呼び出し元が送信した値は上書きします。 - CORS プリフライトを処理し、GET には読みやすい記述子を返します。
- ペイロードのサイズを 256 KB に制限します。
- 上流サーバーで障害が発生した場合は JSON-RPC エラーオブジェクトを返します。クライアントはプログラムであるため、HTML のエラーページは返しません。
インストールするには、ファイルを wp-content/mu-plugins/ に配置し、wp-config.php で STOREFRONT_MCP_UPSTREAM を定義します。その後、上流サーバーで TRUSTED_PROXY_HEADER=x-storefront-client-ip を設定してください。設定しない場合、すべてのリクエストが WordPress サーバーのアドレスを付けて届き、書き込みクォータはストア全体で 1 分あたり 5 回になります。そのため、ループするエージェントが 1 ついるだけで、すべての顧客がチェックアウトできなくなります。上流サーバーにプロキシ経由以外でアクセスできない場合に限り、このヘッダーを信頼してください。上流サーバーが公開されている場合、見つけた人なら誰でも自分でこのヘッダーを書き換えられます。
Shopify の MCP だけではだめですか?
Shopify を利用している場合、Shopify はすでに各ストアに汎用の search_catalog のようなツールを備えたホスト型 MCP エンドポイントを提供しており、これは優れています。そちらを使ってください。このテンプレートは、Shopify が対応していないケースを対象としています。
- Shopify を利用していない場合 — WooCommerce、カスタムスタック、ヘッドレス構成、なぜか今でも動いている 2009 年製の ERP など。
- 差別化要因が、プラットフォームでは決して生成できないツールである場合。 ここで取り上げる例は
get_variant_chartです。商品のラインナップ全体の完全なバリエーション表を、バリエーションごとのリアルタイム在庫とともに取得します。どのストアでも「このマーカーを販売しています」とは言えますが、独自の在庫システムを連携させたストアだけが「色 W3 は今在庫がありますが、色 R21 はありません」と言えます。バリエーションごとのこの回答が売上につながりますが、汎用的なプラットフォームのツールにはないドメイン知識が必要です。 - 権限を分離したバックオフィスが必要な場合 — 同じエンドポイントが、トークンを使えば、あなたには「今月の売れ筋商品は何ですか?」と答え、エージェントにはショップの店頭だけを見せます。
- 実際に安全性を確保できる書き込みツールが必要な場合。 ホスト型プラットフォームでは、チェックアウトツールが何を確認するかはプラットフォームが決めます。ここでは拒否条件がリポジトリ内にあり、レビューできます。また、第三者を保護する条件は変更できません。
リポジトリの構成
src/lib/protocol.ts protocol core (JSON-RPC, auth boundary, capability gating, dispatch)
src/app/api/mcp/route.ts Next.js transport (Streamable HTTP + Bearer)
src/cli/cli.ts standalone transport: `npx storefront-mcp` (stdio, or --http + confirm page)
src/lib/tools.ts tool definitions, built from what the adapter can answer
src/lib/adapter.ts the five adapter contracts + types
src/lib/commerce.ts create_checkout / subscribe_stock_alert — the guard rails
src/lib/availability.ts tri-state availability, pool identity, blocksWrite()
src/lib/cart.ts consolidate first, then measure limits; reject bad quantities
src/lib/money.ts currency-aware rounding (not everything has two decimals)
src/lib/email.ts address vs mailbox: canonical keys for quota and dedupe
src/lib/optin.ts signed double opt-in tokens (exp inside payload, fail-closed)
src/lib/optin-page.ts the confirmation page: GET renders, POST writes (framework-free)
src/lib/ratelimit.ts rateLimited(key,max,window) + what an in-memory limiter is NOT
src/lib/client-ip.ts caller identity: no forwarding header is trusted unless declared
src/lib/write-mode.ts off | dry | live per tool, + the unreserved-checkout policy
src/lib/errors.ts ToolCallError (quota / capability refusals)
src/app/api/stock-alert/confirm/route.ts Next.js transport for the confirmation page
src/lib/adapters/memory.ts reference adapter (all five contracts, one stock source, real holds)
src/lib/adapters/woocommerce.ts Store API skeleton — implements only what it can answer
src/lib/adapters/index.ts adapter registry (env CATALOG_ADAPTER)
examples/toy-catalog.json the demo data, incl. a separate `inventory` section
server.json MCP Registry manifest (registry.modelcontextprotocol.io)
tsconfig.build.json compiles lib + cli to dist/ for the npm bin
discovery/ /.well-known/mcp.json + llms.txt templates
wordpress-proxy/mcp-proxy.php mu-plugin to serve MCP under your WP domain
2.0 での破壊的変更
get_color_card→get_variant_chart(引数brand→chart)、list_brands→list_variant_charts。以前の名前は契約ではなく、単一ストアのドメインを表していました。ペイロードではcolors→variants、hex→ オプションのswatch_hexに名前が変わりました。CatalogAdapterで必須になったのはsearchProductsとgetProductのみです。ほかの 4 つのメソッドはオプションで、それぞれが対応するツールの有効化条件になります。AvailabilityRow.poolが追加されました。これを返さないアダプターも引き続き動作しますが、スラッグと SKU の衝突を検出できません(ガードレール 3)。- 本番環境の
create_checkoutでは、CHECKOUT_UNRESERVED=allowを設定しない限り、InventoryAdapter.reserveが必要です(ガードレール 6)。 TRUSTED_PROXY_HEADERで指定されていない限り、転送ヘッダーは無視されます(ガードレール 20)。STOCK_ALERT_SIGNING_SECRETは 32 文字以上である必要があります。また、本番環境のアラートでは、確認 URL の設定が必須になりました。
ライセンス
Apache-2.0 — LICENSE と NOTICE を参照してください。
storefront-mcp(スペイン語)
オンラインストア向け MCP サーバーテンプレート。 AI エージェントにはカタログを見せ、あなたの運営情報はあなただけが見られます。
30 秒で始める
npx storefront-mcp
これにより、デモ用カタログ(memory アダプター)を備えた MCP サーバーが stdio で起動し、公開ツール 8 個を提供します。内訳は読み取り用 6 個と書き込み用 2 個で、書き込み用ツールは ドライモード で起動します(すべての検証を実行しますが、何も作成しません)。Claude Desktop
または Claude Code に接続するには、これを MCP 設定に追加するか、
claude mcp add storefront -- npx storefront-mcp を実行してください。
{
"mcpServers": {
"storefront": {
"command": "npx",
"args": ["storefront-mcp"]
}
}
}
バックヤード用ツール 5 個も必要ですか?stdio には HTTP ヘッダーが存在しないため、サーバープロセスの環境変数に MCP_SECRET が存在することが鍵になります(プロセスを起動する人が、そのプロセスの実行マシンの管理者です)。
{
"mcpServers": {
"storefront": {
"command": "npx",
"args": ["storefront-mcp"],
"env": { "MCP_SECRET": "cualquier-valor-no-vacio" }
}
}
}
curl を使いますか?npx storefront-mcp --http 8787 を実行すると、localhost 上で同じ JSON-RPC 契約を HTTP 経由で提供します。Authorization: Bearer <MCP_SECRET> の実際の検証も行います(Next.js のルートと同じ動作です)。さらに、/api/stock-alert/confirm にオプトイン確認ページも提供します。アダプターは CATALOG_ADAPTER で選択します(デフォルトは memory、Store API の骨組みには woocommerce)。
概要
オンラインストアを AI エージェント(Claude、カスタム GPT、エージェントフレームワーク — Streamable HTTP 上の任意の MCP クライアント)に公開する、Next.js のルート(App Router)としてパッケージ化された MCP サーバーです。13 個のツールを定義し、アダプターが実際に応答できるサブセットを公開します。
- 公開読み取り用 6 個:
search_products、get_product、get_variant_chart、list_variant_charts、get_promotions、get_quote。 - ガードレール付きの公開書き込み用 2 個:
create_checkoutとsubscribe_stock_alert。 - トークンで保護された機密性の高いツール 5 個:
get_stock_bulk、get_top_products、get_recent_orders、get_order_status、get_sales_summary。
常に利用できるのは search_products と get_product だけです。それ以外はすべて機能として扱われます。アダプターのメソッドを実装するとツールが公開され、実装しなければ、そのツールはデプロイ環境に存在しません(ガードレール 18 を参照)。
書き込み用ツールは意図的に公開されています。人に代わってエージェントが購入することこそが目的だからです。そのため、保護はトークンではなく動作によって行います。これらのツールはドライモードで起動します。 ガードレールを参照してください。
これは本番環境のストアサーバーから抽出したもので、そのストアに固有の要素をすべて取り除き、アダプターインターフェースに置き換えています。
背景
AI エージェントは販売チャネルになりつつあります。誰かがアシスタントに「在庫のある暖色系のグレーのマーカーを探して」と頼んだとき、エージェントが実際に照会できるストアが選ばれます。構造化された検索、実際の在庫状況、支払いリンク付きの見積もりを提供できるストアです。公開 MCP エンドポイントは、その会話に登場する方法です。自分のドメインで、自分のデータとルールを使って実現できます。
中核となる設計:権限の分離
エージェントはショーケースを見ることはできますが、運用には決してアクセスできません。
各ツールは公開または機密であり、プロトコル層で境界を二重に適用します。
tools/list— 有効なAuthorization: Bearer <MCP_SECRET>がなければ、公開ツールのみが返されます。機密ツールはブロックされるのではなく、見えなくなります。tools/call— 機密ツールの名前を推測した相手には、データ処理コードが実行される前に JSON-RPC エラー-32001が返されます。
チェックはフェイルクローズです。環境で MCP_SECRET が定義されていない場合、機密ツールはすべてのユーザーに対してブロックされます。「何も設定していないから、すべて公開される」というモードはありません。トークンの比較は定時間で行われます。
トランスポートによる違いがあります。HTTP(Next.js のルートと --http モード)では、外部から呼び出す相手は信頼できないため、キーは Bearer ヘッダーです。stdio(npx storefront-mcp)ではヘッダーがありません。クライアントとサーバーが同じマシンを共有するため、キーはプロセスの環境に MCP_SECRET が存在することです。同じ境界を、各トランスポートが実際に持つ信頼境界に適用しています。
同じ分離はデータ層にもあります。CatalogAdapter が扱うのは公開されたショーケースのデータだけで、OpsAdapter(注文、売上、正確な在庫数)は別の契約です。これは実装しないこともできます。その場合、機密ツールは誰にも公開されません。ops の実装では顧客情報を匿名化する必要があります。アイテムには名前、数量、価格を含められますが、認証の背後であってもメールアドレス、住所、電話番号は含めません。
2つ目の設計:書き込みツールが拒否すべきこと
読み取りツールの誤りは、不正確な情報を伝えるだけです。書き込みツールの誤りは、存在しない在庫を販売したり、メール砲を第三者に向けたりします。マシンの速度で、再試行ループの中、誰にも見られずに実行されます。
だからこそ、create_checkout と subscribe_stock_alert で重要なのは何をするかではなく、何を拒否するかです。そして、第三者を保護する拒否動作は設定で無効にできません。効果全体を無効にすること(ドライモード、キルスイッチ)はできますが、効果を残したまま検証を取り除くことはできません。
各ガードレールには、それがないと何が壊れるかが示されています。参考にする価値があるのはそこです。ツール自体は、誰でも午後のうちに書ける数百行ほどのコードです。
チェックアウト
1. 在庫状況はカタログではなく、在庫データに照らして確認します。「公開されていて購入可能」と「在庫がある」は別の問いであり、ほとんどの e コマーススタックでは、別々のシステム(CMS と ERP/POS)がそれぞれに回答します。*これがない場合:*ツールは各明細を販売可能なカタログインデックスと照合し、価格しか知らない見積もりシステムに渡します。その間、在庫確認は一度も行われません。エージェントが在庫2個の商品を50個注文すると、実際の注文と、決済可能な支払いリンクが作成されます。
2.「わからない」も「在庫なし」と同じようにブロックします。在庫状況は3つの状態を取ります。{units: n, verified: true}、{units: 0, verified: true}、{units: null, verified: false} です。*これがない場合:*結果は数値としてモデル化され、すべてのエラーが 0 に変換され(正当な販売を黙ってブロックする)、あるいは「在庫があると仮定」されます(存在しない商品を販売することになります)。実際に起こりうる「わからない」の3つのケース、すなわち在庫にマッピングされていないバリエーション、スナップショットに行がない商品、バックエンドの停止は、いずれもゼロではありません。誰かに請求する前であれば、未知と在庫切れは同じ扱いです。
3. 明細行は、上限チェックや在庫チェックの前に統合し、書き方ではなく同じ在庫プールごとに統合します。 前半がなければ: 1 明細行あたり 50 個という上限は飾りにすぎません。同じ SKU を数量 50 で 20 行並べれば合計 1,000 個になりますが、各行は「上限内」です。後半がなければ — これは単純な重複排除では防げないケースです — {slug: "cuaderno-a4", qty: 50} と {sku: "CU-A4", qty: 50} は、1 つの商品と1 つの在庫プールに対する別々のキーです。各行は同じ 60 個に対してチェックされるため、どちらも通過し、店舗は 100 個を販売してしまいます。この商品の同一性を解決できるのは在庫アダプターだけなので、AvailabilityRow には pool フィールドがあり、集計できるすべての上限はプール単位で測定されます。アダプターがそれを返さない場合、2 つの異なる商品だと確認できたふりをするのではなく、その旨を応答で伝えます。
4. 無効な数量は拒否し、補正しません。
Math.min(Math.max(Math.floor(Number(qty) || 1), 1), 50) は、入力のサニタイズのように見えます。これがなければ: エージェントにとって「これを取り除いて」という意味の {qty: 0} が、購入者に請求される 1 個に変わります。また、負の数、NaN、小数は架空の販売数に変わります。課金を行う tool では、サニタイズとは拒否して説明することです。入力をもっともらしい値に書き換えるのは、意図を捏造することになります。
5. 価格は呼び出し元から受け取らず、見積もりを注文内容と照合します。 入力スキーマに価格フィールドはなく、注文を作成する前にサーバーは、カタログが要求された数量を見積もったことと、unit_price × qty == line_total であることを確認します。前半がなければ: 割引ポリシーは呼び出し元が書いた内容次第になります。後半がなければ: 数量はカートから送られ、金額は見積もりから送られますが、両者を照合するものはありません。そのため、40 個を 10 個に「調整」する見積もり機能によって、他のすべてのガードレールが有効なまま、40 個分を請求して 10 個だけ注文することになります。見積もりは明細行を拒否してもかまいませんが、尋ねられた内容とは別の質問に答えてはいけません。
6. 注文が存在する前に在庫を予約するか、ライブチェックアウトを拒否します。 これは、ステートレスな検証では実現できないガードレールです。ポイント 1 から 3 は過去について説明しています。つまり、ある数値を読み取ります。同時に 10 件の呼び出しが「残り 4 個」を読み取り、すべての検証を通過して 10 件すべてが注文を作成すると、どのルールにも違反しないまま、4 個の在庫に対して 40 個を販売してしまいます。それを防げるのは在庫の保存元でのアトミックな減算だけなので、InventoryAdapter.reserve() は検証と注文の間で実行されます。また、アダプターが予約に対応していないデプロイでは、オペレーターが CHECKOUT_UNRESERVED=allow を設定してリスクを明示的に受け入れない限り、ライブ注文は作成されません。これがなければ: 「過剰販売を防ぐ」という主張が成り立つのは、一度に 1 件のリクエストだけです。それはこの言葉の意味するところではありません。その後 createCheckout が失敗した場合は、予約が解放されます。
7. 判定結果には根拠も添えます。 各明細行には、通過したかどうかにかかわらず、stock_available と stock_verified が返されます。これがなければ: 在庫数を正確に返す tool はトークン認証の先にあります(バックヤードのデータです)。そのため、一般公開されているエージェントは何も診断できず、あてずっぽうに再試行し、購入者に理由をでっち上げて伝えることになります。権限の境界によってエージェントが診断用 tool を使えないようにするなら、書き込み用 tool が解決済みの診断情報を返す必要があります。
8. 不正な明細行が 1 つでもあれば、カート全体をブロックします。 これがなければ: 購入者には「たまたま通過した商品だけ」の注文が届きます。それは誰も注文していないカートです。
9. お客様に伝える金額が、お客様の支払う金額です。 税金、送料、割引はアダプター側の処理なので、CheckoutReceipt.total は明細行の小計と異なる場合があります。異なる場合は、矛盾する金額を2つ返すのではなく、どちらがどの金額なのか(amount_to_pay、charges)をレスポンスで明示します。
これがない場合: subtotal で終わる金額の契約では、税金込みの価格と送料無料を暗黙に前提とします。EU や米国でこれを実装する人は、バックエンドで税金を加算することになり、その場合、合計金額が同じレスポンスで報告した小計と一致しなくなります。あるいは、税金を加算せず、請求額が不足します。
再入荷通知
10. 外部への作用には、サーバーで検証済みのビジネス上の前提条件が必要です。 商品が本当に在庫切れの場合にのみメールを送ります。在庫数がゼロであることを確認するか、在庫数を確認できない場合は、カタログが独立して在庫切れと示している必要があります。これがない場合: 呼び出し元が選んだアドレスにメールを送る公開ツールは、第三者を狙うメール砲になります。tools/call を {email: [email protected], slug: <cualquiera>} でループさせるだけで、あなたのドメインから、誰もショップに連絡していない相手に、見栄えの整ったメールが何千通も送信され、クレジットと送信者の評判を消費します。(おまけ:在庫切れになったことのない商品に対する誤った「再入荷しました!」も防げます。)
11. サブスクリプションだけでなく、メール送信を重複排除し、フェイルクローズにします。 「このアドレスはすでに登録済みか?」と「このアドレスにこの商品についてすでにメールを送り、その後何も行われていないか?」は別々の問いです。後者がない場合: 攻撃者は登録を確定しないため、前者では重要なケースを防げません。同じ呼び出しを3回行うと、メールが3通送信されますが、技術的には重複登録ではありません。どちらの確認も送信前に行い、どちらかに失敗した場合は何も送信しません。バックエンドの停止が、送信許可に変わることはありません。
12. クォータは受信者のアドレスをキーとし、アダプターが維持できる期間にわたって適用します。 呼び出し元ごとの制限とは別に、アドレスごとに1時間あたり3通の確認メールを評価します。アドレス単位の制限がない場合: 文字列をそのままキーにしても、クォータとは言えません。アドレスには無限の表記方法があるためです。victima@、victima+1@、victima+2@、v.i.c.t.i.m.a@、VICTIMA@ はすべて同じ Gmail アカウントに届き、それぞれ新たに3通分のクォータが与えられます。永続性がない場合: メモリー上のカウンターはコールドスタートでリセットされ、インスタンス間で分散され、再デプロイ時に消去されるため、上限は書面上にしか存在しません。NotifyAdapter.countOptInEmails を実装すれば、制限はストレージ上のカウントになります。実装しない場合、ツールのレスポンスは、維持できない数値を約束するのではなく、quota_enforcement: "best_effort" を返します。
13. 自動メールを、呼び出し元が選んだアドレスに送信してはいけません。 Web のチェックアウトでは、お客様と管理者にメールを送ります。MCP ツールでは、customer_email を入力するのは エージェント です。これがない場合: 書き込みツールに「注文確認」を組み込むと、もう一方のツールが閉じたばかりのメール砲を、まさに再び開くことになります。ルール:公開ツールの入力として渡されたアドレスに送信できるのは、ダブルオプトインのメッセージのみです。それ以外のメールには、事前の意思表示の証明が必要です。支払いがその証明となります。(このテンプレートは、運営担当者にはメールを送りません。注文通知が必要な場合は、あなた自身のアダプターから、あなた自身の環境にあるアドレスへ送信してください。draft.customer_email には絶対に送信しないでください。)
14. 正しく構成されたトークンによるダブルオプトイン。鍵として機能する鍵も含みます。 v1.<payload>.<hmac> を使用し、有効期限は署名対象のペイロードの内部に含め、時間定数で比較し、署名シークレットは最低32文字にします。シークレットがない、または短すぎる場合はフェイルクローズにします(ツールは直接登録するのではなく、「オプトインを利用できません」と応答します)。*これがない場合:*署名のないトークンは偽造されます。トークンの内部ではなく横に保存された有効期限は無視されます。署名に対する === はバイト単位で情報を漏らします。環境変数がないと、門戸が開け放たれます。そして STOCK_ALERT_SIGNING_SECRET=x は「空でない」というチェックを通過するため、誰でも任意のアドレスに対する有効なトークンを計算して POST で送信でき、誰もオプトインしていないのにダブルオプトインが成立します。外部向けには、「シークレットなし」「形式不正」「署名無効」はすべて同じメッセージにします。「期限切れ」だけは区別します。本人にとって対応可能で、攻撃者にとっては役に立たないためです。
15. 確認 URL がなければメールを送信しません。 *これがない場合:*ほかのすべてがフェイルクローズするフローで、唯一フェイルオープンになります。署名シークレットが設定されていて、サイト URL が未設定でも、ツールはメールを送信し、https://example.com への確認リンクを含めていました。これは運用者が管理していないドメインであり、宛先のアドレスを含む署名付きトークンがクエリ文字列に入った状態で、第三者に送られます。リンク切れは困ります。別の人が管理するドメイン上のリンク切れで、顧客のアドレスまで含まれているなら、さらに問題です。
**16. GET は表示し、POST は書き込みます。そしてページには、何を確認するのかを明記します。**確認ページは GET では何も書き込みません。フォーム本文にトークンを含めた POST だけが、データを永続化します。*これがない場合:*企業のメールゲートウェイ(Safe Links、URL Defense、デスクトップウイルス対策ソフト、クライアントのプリフェッチ機能)は、メールの配信時にメッセージ内のすべての URL に GET リクエストを送ります。被害者のアドレスに確認メールを送ると、本人の会社のセキュリティスキャナーが登録を有効にしてしまいます。ダブルオプトインが防ごうとしていた、本人以外によるオプトインが裏口から入り込むのです。スキャナーはリンクをたどりますが、フォームは送信しません。このルールは、メール内のリンクから実行されるあらゆる操作に当てはまります。確認、キャンセル、承認、登録解除などです。また、このページには商品名と具体的なバリエーションも表示します。商品シリーズへの登録と、そのシリーズの特定の音色1つへの登録は別の登録であり、その違いを明示しない同意画面は同意とは言えないためです。書き込みはべき等なので、ダブルクリックしても成功として扱われ、サポートへの問い合わせにはなりません。
2つのツール
17. デフォルトは dry で、ツールごとにキルスイッチがあります。 CHECKOUT_MODE と
STOCK_ALERT_MODE は、明示的に live に設定しない限り dry です。
off にすると、ツールは tools/list から完全に除外されます。dry での呼び出しでは
すべての検証を実行し、何が実行されるはずだったかを正確に返します。これには
「これは BLOCKED になっていたはずです。その理由は……」も含まれます。これがないと:
書き込みツールが、デプロイされているというだけで自動的に有効になり、試運転の
機会がなくなります。初回の実呼び出しが、本番環境でお金を扱う呼び出しになります。
live という完全一致の文字列だけが live に進むため、タイプミスがあっても何も
実行されません。
18. 正直な応答ができないツールは、読み取りツールも含めて公開しません。 tools/list は
エンドツーエンドで機能の有無に応じて制御されます。getQuote がなければ
get_quote もありません。OpsAdapter がなければ、認証済みの呼び出し元であっても
バックオフィスのツールはありません。CheckoutAdapter、InventoryAdapter、見積もり
ツールのいずれかがなければ、create_checkout もありません。公開されていないツールは、
理由を問わず常に -32601 を返します。これがないと: スタブを呼び出すことになります。
WooCommerce アダプターには、応答できないメソッドが3つ実装されていました。listBrands
は [] を返し、getColorCard は null を返し、getQuote は常にエラーになっていました。
これはインターフェースを満たすためだけの実装でしたが、サーバーは匿名のエージェントに
3つすべてを公開していました。常に空のカード一覧、常に「見つかりません」と返す検索、
常に失敗する見積もりです。エージェントが二度入り込む袋小路は、ツールが存在しない
状態よりも悪いです。
19. 制限の説明は、実際の制限と一致させます。 エラーでは2つのツールを挙げ、 上限を共有していることを伝えます。説明文も同じ内容にし、デプロイ環境が呼び出し元を 特定できない場合も含めます。これがないと: 2つのツールを交互に使うエージェントは、 説明された上限より早く制限に達します。カウンターがツールごとにあると判断して再試行し、 レート制限が、本来抑えるはずだったトラフィックを生み出します。MCP サーバーでは、 説明文とエラーメッセージがエージェントに見える API です。意味が誤って説明されていれば、 再試行という形で代償を払うことになります。
20. レート制限のキーを呼び出し元に選ばせません。 以下を参照してください。
レートリミッターについて(正直な説明)
「IPで制限する」場合に問題が二つあります。二つ目は、ほとんど言及されません。
キー。 x-forwarded-for の最初の値を信用してはいけないことは、誰もが知っています。見落とされがちなのは、転送用ヘッダーを書き込むのはプロキシであり、前段にプロキシがない場合――VPSでの node server.js、そのままの next start、proxy_set_header を設定していない nginx、公開ポートを持つコンテナなど――最後のホップも含め、ヘッダー全体が呼び出し元の書いた文字列だということです。X-Forwarded-For: 203.0.113.1, .2, .3… を次々と変えれば、リクエストごとに新しいバケットが使われ、レートリミッターは何もしません。そのため、このサーバーはエッジが書き込むヘッダーを指定しない限り、どの転送用ヘッダーも信用しません。
TRUSTED_PROXY_HEADER=x-vercel-forwarded-for # Vercel
TRUSTED_PROXY_HEADER=cf-connecting-ip # Cloudflare
TRUSTED_PROXY_HEADER=x-storefront-client-ip # el proxy de WordPress incluido
ヘッダーを指定するということは、二つの点を保証することになります。エッジがそのヘッダーを上書きすること、そしてほかのものがオリジンに到達しないことです。オリジンにインターネットから直接アクセスできる場合、そのヘッダーは見つけた人なら誰でも偽装できます。まずオリジンを保護してください(デプロイ時の保護、ファイアウォール、mTLS)。何も指定されていない場合、--http サーバーは偽装できない TCP ピアアドレスを使います。一方、Fetch 型のサーバーレスハンドラーには参照できるソケットがないため、呼び出し元は特定できません。そのため、呼び出し元ごとに守れるはずのない制限を約束するのではなく、特定できないデプロイではプロセス全体に毎分60回の書き込み上限を適用します。(共有で5回/分ではありません。全員を小さなバケットに押し込むと、レートリミッターが自分のクライアントに対するサービス拒否攻撃になり、解決しようとしていた問題より悪い事態になります。)
ストレージ。 組み込みカウンターは、1プロセスのメモリ上にある Map です。サーバーレスでは、稼働中のインスタンスがN個なら、独立した枠もN個という意味になります。コールドスタートでリセットされ、再デプロイで消去されます。これはエージェントのループへの歯止めであり、WAFでも不正利用対策でもありません。そのため、チームが実際には持っていない上限として見せるのではなく、src/lib/ratelimit.ts にその旨を記載しています。rateLimited(key, max, windowMs) は呼び出し形式が一つだけの小さな同期ポートなので、Redis/KV/Durable Objects への置き換えは機械的に行えます。カウンターを何らかの用途で信頼する前に、置き換えてください。
そのため、重要な制限は処理の効果に適用されています。可用性に対するフェイルクローズ、原子的な予約、在庫切れ時の事前条件、送信時点での重複排除、ダブルオプトイン、キルスイッチです。これらはカウンターではなくデータベースが適用するため、インスタンスの数にかかわらず機能します。宛先ごとのメール枠はその中間です。NotifyAdapter が countOptInEmails を実装していれば永続的に適用され、そうでなければベストエフォートです。どちらに当たるかは、推測させるのではなくツールの応答で伝えます。
書き込みツールの設定
| 変数 | デフォルト | 説明 |
|---|---|---|
CHECKOUT_MODE | dry | create_checkout の off | dry | live |
STOCK_ALERT_MODE | dry | subscribe_stock_alert の off | dry | live |
CHECKOUT_UNRESERVED | refuse | live モードで、在庫アダプターがユニットを予約できない場合の動作です。refuse(注文を作成しない)または allow(同時呼び出しによる過剰販売の可能性を許容します。各レシートにその旨が記載されます) |
STOCK_ALERT_SIGNING_SECRET | (値なし) | オプトインリンク用の HMAC キーです。32 文字以上が必要です。設定されていないか短すぎる場合、リンクを発行できず、ツールは拒否します。openssl rand -hex 32 |
STOCK_ALERT_CONFIRM_URL | ${NEXT_PUBLIC_SITE_URL}/api/stock-alert/confirm | 確認ページの URL です。どちらも設定されていない場合、live モードでは何も送信されません |
STOCK_ALERT_PAGE_LOCALE | en | ページの言語(en | es)です。クライアントに表示される唯一の画面です |
TRUSTED_PROXY_HEADER | (値なし) | エッジがクライアントの IP を設定するヘッダー名です。未設定の場合 ⇒ 転送ヘッダーは無視されます |
ツールは、アクティブなアダプターがその機能を提供している場合にのみ公開されます。そのため、上記の設定によって、アダプターが対応できないツールが有効になることはありません。
60 秒でガードレールをテストする
サンプルカタログは、すべての状態が表示されるように構成されています。
chromaflow-classic-set-12 のユニット数は 4 です。CF-G09 はカタログに登録されていますが、在庫行がありません。fieldbook-sketch-a5 は 0 ユニットの状態で in_stock として公開されており、これは過剰販売の典型的なケースです。そして fieldbook-sketch-a4 は通常のケースで、60 ユニットある商品をスラッグと SKU FB-A4 のどちらでも指定できます。
npx storefront-mcp --http 8787 &
# Consolidación + fail-closed: dos líneas de 3 para un producto con 4 unidades.
# Por línea cada una "cabe". Juntas no.
curl -s http://127.0.0.1:8787/ -H 'content-type: application/json' -d '{
"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"create_checkout","arguments":{
"items":[{"slug":"chromaflow-classic-set-12","qty":3},{"slug":"chromaflow-classic-set-12","qty":3}]}}}'
# → bloqueado, qty_requested 6, merged_from_input_lines 2, stock_available 4
# El mismo producto escrito de dos formas, una sola pila de unidades:
# 50 por slug + 50 por SKU contra 60. Por separado cada línea "cabe";
# lo que se verifica es el pozo.
curl -s http://127.0.0.1:8787/ -H 'content-type: application/json' -d '{
"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"create_checkout","arguments":{
"items":[{"slug":"fieldbook-sketch-a4","qty":50},{"sku":"FB-A4","qty":50}]}}}'
# → bloqueado: "only 60 unit(s) available and 100 requested across the lines
# that share this stock", más shares_stock_with en cada línea
# Stock no verificable bloquea igual que cero:
# "items":[{"sku":"CF-G09","qty":1}] → stock_verified:false, blocked
# Las cantidades se rechazan, no se corrigen:
# "items":[{"slug":"fieldbook-sketch-a5","qty":0}] → items_invalid_qty, qty_received 0
Web エンドポイントとして起動する(2 分)
独自ドメインから MCP を提供するには(デプロイ可能な Next.js ルートです):
git clone <este repo> && cd storefront-mcp
npm install
npm run dev
これで完了です。デフォルトの memory アダプターが、架空の店舗のサンプルカタログ examples/toy-catalog.json を提供します。英語のセクションにある curl コマンドは、そのまま使えます。
デプロイする場合は、プラットフォームが設定するヘッダー(Vercel では x-vercel-forwarded-for、Cloudflare の背後では cf-connecting-ip)を TRUSTED_PROXY_HEADER に設定してください。設定しないと、書き込み枠がインスタンス全体で共通の上限になります。レートリミッターについて(率直な説明)を参照してください。
独自のアダプターを作成する
プロトコル層がデータに直接触れることはありません。src/lib/adapter.ts のインターフェースを呼び出します。必須のメソッドは2つだけです。それ以外はすべて機能であり、どのツールを提供するかは機能によって決まります。
CatalogAdapter(必須)—searchProductsとgetProductが必須です。必須事項はこれだけです。定義上、公開されます。同じインターフェース内のオプション機能:getPromotions→get_promotions、getQuote→get_quote(create_checkoutの前提条件でもあります)、listVariantCharts+getVariantChart→ バリエーション表の2つのツール。この2つはセットで提供するか、どちらも提供しないかのどちらかです。「バリエーション表」とは、色、サイズ、挽き目、焙煎度、容量など、バリエーションごとに在庫を管理する商品ラインのことです。カタログにそのような軸がなければ、実装する必要はありません。スタブにする必要もありません。OpsAdapter— トークンで保護された5つのツールを有効にします。InventoryAdapter— 商品ごとに{units, verified, reason, pool}を返すgetAvailability(refs)。これは「現在の在庫数」の情報源であり、読み取り用ツールも同じ情報源を使う必要があります。オプションですが重要な機能として、reserve(req)/release(id)があります。これらがないと、ライブチェックアウトは拒否されます(ガードレール6)。CheckoutAdapter—createCheckout(draft)。InventoryAdapterとgetQuoteがそろっている場合にのみ提供されます。NotifyAdapter—isSubscribed/sendOptInEmail/confirmSubscriptionに加え、受信者ごとの送信枠を実際の上限にするオプションのcountOptInEmails。
必須の2つのメソッドだけを実装したアダプターでも、問題なく動作します。それ以外の機能は提供されないだけです。これはサポート対象の構成であり、機能が劣る構成ではありません。
src/lib/adapters/memory.ts を参考にコピーし、データベースまたは API に接続して、src/lib/adapters/index.ts に登録してください。契約上の誠実さを守るためのルール:在庫を確認できなかった場合は units: null / stock: null を返します(数値をでっち上げたり、0 に置き換えたりしてはいけません)。在庫状況の各行で pool を返してください。これは、その行で消費される在庫ユニット群の識別子です。同じ商品を示す slug と SKU は同じ在庫プールに解決されなければなりません。そうしないと、異なる形式で指定された同一商品が、同じ在庫に対して2回確認されてしまいます。reserve は全件成功または全件失敗となる単一のアトミック操作(UPDATE … WHERE (on_hand − reserved) >= :qty)として実装し、ロールバック用に release も実装してください。在庫の応答はすべて単一の情報源から返し、書き込み処理が、顧客に表示したものとは異なる情報を使って検証しないようにします。他の場所ですでに確保されている分(有効な予約や他の販売チャネルなど)は差し引いてください。isSubscribed でエラーが発生した場合は false を返すのではなく、例外を発生させて、呼び出し元が安全側に失敗するようにしてください。すべての外部呼び出しにタイムアウトを設定してください。また、get_quote は決して課金しません。見積もりを返し、受け取った slug / sku をそのまま返し、依頼された数量を見積もるか、その行を拒否してください。チェックアウトでは両者を比較し、数量が一致しないカートを拒否するためです。
WooCommerce(Store API)用のスケルトンが含まれています。実際に応答できる search_products、get_product、get_promotions だけを実装しており、何もスタブにしていません。TODO の各項目には、残りの契約を満たすために WooCommerce の各インストールで必要となる内容が記されています。これには、誰も代わりに決められない2つの事項、すなわちバリエーションごとの在庫の扱いと、ユニットの予約方法も含まれます。
ディスカバリー
discovery/ にテンプレートがあります。/.well-known/mcp.json(機械可読なディスクリプターです。{{DOMAIN}} を置き換え、public/.well-known/ から配信してください)と、エージェント向けポリシーを記述した /llms.txt 用のスニペットです。また、GET /api/mcp は JSON ディスクリプターを返します。デプロイで実際に公開するツールだけを一覧にしてください。
公式 MCP レジストリでは、server.json がマニフェストです。まず npm に公開してください。レジストリは公開済み tarball 内の mcpName を検証するため、npm にまだ存在しないバージョンを登録すると、参照先のないエントリーが残ります。.github/workflows/publish-mcp-registry.yml のワークフローは実行前にこれを検証します。
MCP を超えるスキル
サーバーは、プロトコルで提供されるスキルとして、独自の使用手順も配信します(SEP-2640、拡張機能 io.modelcontextprotocol/skills)。この拡張機能を実装しているホストは、skills/list でスキルを検出し、resources/read で skill://storefront-mcp/SKILL.md を読み込み、エントリーに含まれる sha256 ダイジェストを検証します。拡張機能を実装していないホストには、同じファイルが一般的なリソースとして表示されます。何も壊れません。
この拡張機能はプロトコルの 2026-07-28 リビジョンに基づいて仕様が定められているため、サーバーは server/discover に応答し、そのリビジョンをネゴシエートするようになりました(ステートレスで、各リクエストの _meta にバージョンを指定し、各結果に resultType を含め、リストには ttlMs/cacheScope を含めます)。以前のクライアントは、引き続き 2025-06-18 または 2025-11-25 を指定して initialize を使用でき、影響はありません(initialize が 2026-07-28 を返すことはありません。このリビジョンにはハンドシェイクがないためです)。さらに、server/discover の応答にはスキルを指す instructions も含まれているため、拡張機能を備えていないホスト(現時点では Claude Code)も、モデルが判断したときにリソースとして同じスキルを読み込めます。
2026-07-28 に対応するということは、このリビジョンで拒否するよう定められているリクエストを拒否することでもあります。このサーバーが提供していないプロトコルバージョンが _meta に指定されている場合(-32022。対応バージョンの一覧を含みます)、最新形式のリクエストに clientCapabilities がない場合(-32602)、そして HTTP でミラー用ヘッダー MCP-Protocol-Version、Mcp-Method、Mcp-Name が欠落しているか、本文と矛盾している場合(-32020、HTTP 400)です。最新形式のリクエストに不明なメソッドが含まれている場合は 404 になります。_meta を付けないリクエスト(以前のリビジョン)は、これまでどおり処理されます。
SKILL.md は、デプロイの実際の設定に基づいて呼び出し元ごとに生成されます。呼び出し元に tools/list が通知するツールを正確に記載し、各書き込みツールの現在のモード(dry または live)を示します。また、エージェントに許可される範囲(明示的な購入依頼なしのチェックアウトは禁止、通知はそのための宛先が提供された場合に限る)と、オペレーターセッションで利用できるバックオフィス用ツールを記載します。匿名の呼び出し元向けのスキルに、バックオフィス用ツールが記載されることはありません。このため、設定が変更されるとダイジェストも変わります。それが意図した動作です。ダイジェストを固定したホストは、その時点で再度読み込みます。
拡張機能に対応したクライアントで試してください(mcpc 0.7 以降は、ほかの MCP ホストと同様に、設定ファイルから接続します)。
cat > mcp.json <<'EOF'
{ "mcpServers": { "tienda": { "command": "npx", "args": ["-y", "storefront-mcp"] } } }
EOF
npx -y @apify/mcpc connect mcp.json:tienda @tienda
npx -y @apify/mcpc @tienda skills-list
npx -y @apify/mcpc @tienda skills-get storefront-mcp
npm test は CLI をビルドし、2 種類のトランスポートをエンドツーエンドでテストします。宣言された機能、skills/list と skills/get の一致、エントリーと同じフィールドに YAML として解析されるフロントマター(ホストが行うべき検証)、配信されたバイト列と一致するダイジェストおよびサイズ、tools/list が通知するツールとスキルの記載内容が両方向で完全に一致すること、そして匿名の呼び出し元向けのスキルにバックオフィス用ツールが記載されないことを確認します。また、上記の 2026-07-28 に関する拒否動作と、JSON-RPC リクエストではない本文(null、数値、空のバッチ)もテストします。これらは実行されず、-32600 で応答されます。
WordPress のドメイン配下で MCP を運用する
ストアが WordPress/WooCommerce で稼働していても、MCP サーバーが別の場所にある場合、wordpress-proxy/mcp-proxy.php は
https://tutienda.com/api/mcp を公開し、上流サーバーにプロキシする mu-plugin です。優先度 0 で init
にフックし、Authorization ヘッダーを変更せずに転送し、クライアントの実 IP を
X-Storefront-Client-IP で転送します(呼び出し元が送信した値は上書きします)。CORS のプリフライトを処理し、GET にはディスクリプターを返し、ペイロードを 256 KB に制限します。また、上流サーバーで障害が発生した場合は、HTML ページではなく JSON-RPC エラーを返します。インストールするには、ファイルを
wp-content/mu-plugins/ にコピーし、wp-config.php で
STOREFRONT_MCP_UPSTREAM を定義します。
その後、上流サーバーで TRUSTED_PROXY_HEADER=x-storefront-client-ip を設定します。設定しないと、すべてのリクエストが WordPress サーバーのアドレスから届くため、書き込み枠がストア全体で 1 分あたり 5 回になり、ループ状態になったエージェントが 1 つあるだけで、すべての顧客がチェックアウトできなくなります。上流サーバーにプロキシ以外からアクセスできない場合に限り、このヘッダーを信頼してください。上流サーバーにパブリックからアクセスできる場合、見つけた人なら誰でも自分でこのヘッダーを書き込めます。
Shopify の MCP をそのまま使わない理由は?
Shopify を利用している場合、Shopify は各ストアに汎用の search_catalog を備えた MCP エンドポイントを提供しており、問題なく動作します。それを使ってください。このテンプレートは、そのエンドポイントで対応できないもの、つまり Shopify 以外のストア(WooCommerce、独自スタック、ヘッドレス)を対象としています。特に、どのプラットフォームも代わりに生成してくれないツールが対象です。ここで取り上げる例は get_variant_chart です。商品ラインの全バリエーションを、バリエーションごとのリアルタイム在庫とともに返す機能です。どのストアでも「このマーカーを販売しています」とは言えます。しかし、自社の在庫を連携しているストアだけが、「色調 W3 は現在在庫があり、R21 はありません」と答えられます。バリエーションごとのこの回答が販売につながりますが、汎用ツールには備わっていません。
もう 1 つの理由は、自信を持って提供できる書き込みツールです。ホスト型プラットフォームでは、チェックアウト時に何を検証するかがプラットフォームによって決められます。ここでは、拒否の条件がリポジトリ内にあり、レビュー可能です。また、第三者を保護する条件は設定で変更できません。
2.0 の破壊的変更
get_color_card→get_variant_chart(引数brand→chart)、およびlist_brands→list_variant_charts。旧名称は契約ではなく、ストアのドメインを表していました。ペイロードではcolors→variants、hex→swatch_hex(省略可能)に名称が変更されました。CatalogAdapterで必須となるのはsearchProductsとgetProductのみになりました。他の 4 つのメソッドは省略可能で、それぞれが対応するツールの有効化条件になります。AvailabilityRow.poolが追加されました。これを返さないアダプターも引き続き動作しますが、slug/SKU の衝突を検出できません(ガードレール 3)。- ライブ環境の
create_checkoutでは、CHECKOUT_UNRESERVED=allowを設定しない限り、InventoryAdapter.reserveが必須になりました(ガードレール 6)。 - 転送ヘッダーは、
TRUSTED_PROXY_HEADERで指定されていない限り無視されます(ガードレール 20)。 STOCK_ALERT_SIGNING_SECRETは 32 文字以上である必要があります。また、ライブ環境でアラートを使用するには、確認 URL の設定が必須になりました。
ライセンス
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。