AIエージェントを使ったECや SaaSの契約フローについて、PoCやディスカッションが出てき始めていますね。今回は簡単にAIによるEC体験を検証できるMCPサーバーをStripeとCloudflareで作ってみましたので、紹介します。
このデモで実現していること
AI チャットに「〇〇を探して購入したい」と伝えると、ClaudeやCursorなどのエージェントが以下のフローを自律的に行ってくれます。
- 商品をキーワード検索して候補を提示
- 商品詳細を取得してスペックや価格を確認
- Stripe Checkout のリンクを生成して決済ページに案内
Stripeとの連携はMCPサーバーを使っています。バックエンドは Cloudflare Workers 上で動く Hono アプリで実装しました。
- デモ MCP サーバー: https://stripe-mcp-server.wp-kyoto.workers.dev/
- GitHub リポジトリ: hideokamoto/demo-stripe-mcp-app-agent-commerce-like
アーキテクチャ概観
AI ホスト(Cursor / Claude Desktop など)
│ MCP プロトコル(Streamable HTTP)
▼
Hono アプリ on Cloudflare Workers
├── /mcp ← MCP エンドポイント
└── /checkout/success|cancel
│
▼ Stripe API
商品データ / Checkout Session
MCP サーバーは @modelcontextprotocol/sdk で実装し、@hono/mcp の StreamableHTTPTransport で Hono アプリに乗せています。さらに @modelcontextprotocol/ext-apps を使って ツール実行結果をリッチな HTML カタログ UI として返しています。
Cursor 等のクライアントへの接続設定
Cursor の MCP 設定ファイルに以下を追加するだけで、すぐにデモを試せます。
{
"mcpServers": {
"demo-commerce": {
"url": "https://stripe-mcp-server.wp-kyoto.workers.dev/mcp"
}
}
}
実装されている MCP ツール

サーバーは 3 つのツールを提供しています。
1. search_products — 商品を検索する
キーワードなしで全商品一覧、キーワードを指定すれば名前・説明・メタデータで絞り込んで返します。
import { z } from "zod";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
import { getContext } from "hono/context-storage";
import { createStripeClient } from "../lib/stripe.js";
import { Env } from "../types.js";
import { toProductData } from "../lib/product-mapper.js";
import { RESOURCE_URIS } from "../resources.js";
export function registerSearchProducts(server: McpServer): void {
registerAppTool(
server,
"search_products",
{
title: "商品を検索",
description:
"ストアの商品を検索します。キーワードなしで全商品一覧、キーワード指定で絞り込み。結果はカタログUIに表示されます。",
inputSchema: {
query: z
.string()
.optional()
.describe("検索キーワード(商品名やタグ)"),
limit: z
.number()
.int()
.min(1)
.max(100)
.default(10)
.describe("取得件数(デフォルト10)"),
},
_meta: {
ui: {
resourceUri: RESOURCE_URIS.catalog,
},
},
},
async ({ query, limit }) => {
const c = getContext<{ Bindings: Env }>();
const stripe = createStripeClient(c.env.STRIPE_SECRET_KEY);
// autoPagingToArray で 100 件上限を回避して全件取得
const [allProducts, allPrices] = await Promise.all([
stripe.products.list({ active: true }).autoPagingToArray({ limit: 10000 }),
stripe.prices.list({ active: true, currency: "jpy" }).autoPagingToArray({ limit: 10000 }),
]);
// default_price を優先しつつ各商品の最適な価格を選択
const pricesByProduct = new Map<string, (typeof allPrices)[0]>();
const pricesByProductId = new Map<string, (typeof allPrices)[0][]>();
for (const price of allPrices) {
const productId =
typeof price.product === "string" ? price.product : price.product.id;
if (!pricesByProductId.has(productId)) {
pricesByProductId.set(productId, []);
}
pricesByProductId.get(productId)!.push(price);
}
for (const product of allProducts) {
const prices = pricesByProductId.get(product.id) || [];
if (product.default_price) {
const defaultPriceId =
typeof product.default_price === "string"
? product.default_price
: product.default_price.id;
const defaultPrice = prices.find((p) => p.id === defaultPriceId);
if (defaultPrice) {
pricesByProduct.set(product.id, defaultPrice);
continue;
}
}
if (prices.length > 0) {
pricesByProduct.set(product.id, prices[0]);
}
}
let products = allProducts;
if (query) {
const lowerQuery = query.toLowerCase();
products = products.filter((p) => {
const nameMatch = p.name.toLowerCase().includes(lowerQuery);
const descMatch = p.description?.toLowerCase().includes(lowerQuery) ?? false;
const tagsMatch = Object.values(p.metadata ?? {}).some((v) =>
v.toLowerCase().includes(lowerQuery)
);
return nameMatch || descMatch || tagsMatch;
});
}
const items = products
.filter((p) => pricesByProduct.has(p.id))
.slice(0, limit)
.map((p) => toProductData(p, pricesByProduct.get(p.id)!));
return {
content: [{ type: "text", text: JSON.stringify(items) }],
structuredContent: { items },
};
}
);
}
結果は _meta.ui.resourceUri を介してカタログ UI として Cursor 上にレンダリングされます。


2. get_product — 商品詳細を取得する
カタログから気になった商品の ID を渡すと、詳細情報を商品詳細 UI として返します。

default_price が設定されている場合はそれを優先し、なければアクティブな価格を自動で取得します。

3. create_checkout — 決済リンクを生成する
購入する商品の price_id と数量を渡すと、Stripe Checkout Session を作成して URL を返します。
import { z } from "zod";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { getContext } from "hono/context-storage";
import { createStripeClient } from "../lib/stripe.js";
import { Env } from "../types.js";
export function registerCreateCheckout(server: McpServer): void {
server.registerTool(
"create_checkout",
{
title: "決済リンクを生成",
description:
"選択された商品の決済リンクを生成します。ユーザーはこのリンクをブラウザで開いて購入手続きを完了します。",
inputSchema: {
items: z
.array(
z.object({
price_id: z.string().min(1).describe("StripeのPrice ID"),
quantity: z.number().int().min(1).describe("購入数量"),
})
)
.min(1)
.describe("購入する商品のリスト"),
success_url: z
.string()
.optional()
.refine(
(val) => {
if (!val) return true;
return val.startsWith("/");
},
{ message: "success_url must be a relative path starting with /" }
)
.describe("決済成功後のリダイレクト先URL(相対パスのみ)"),
cancel_url: z
.string()
.optional()
.refine(
(val) => {
if (!val) return true;
return val.startsWith("/");
},
{ message: "cancel_url must be a relative path starting with /" }
)
.describe("決済キャンセル後のリダイレクト先URL(相対パスのみ)"),
},
},
async ({ items, success_url, cancel_url }) => {
const c = getContext<{ Bindings: Env }>();
const stripe = createStripeClient(c.env.STRIPE_SECRET_KEY);
const baseUrl = new URL(c.req.url).origin;
const session = await stripe.checkout.sessions.create({
mode: "payment",
line_items: items.map((item) => ({
price: item.price_id,
quantity: item.quantity,
})),
success_url: success_url
? `${baseUrl}${success_url}`
: `${baseUrl}/checkout/success`,
cancel_url: cancel_url
? `${baseUrl}${cancel_url}`
: `${baseUrl}/checkout/cancel`,
});
if (!session.url) {
throw new Error("Checkout session was created but URL is missing");
}
return {
content: [
{
type: "text",
text: `決済リンクが生成されました。以下のURLをブラウザで開いて購入手続きを完了してください:\n\n${session.url}`,
},
],
structuredContent: { url: session.url, sessionId: session.id },
};
}
);
}
ユーザーはそのリンクをブラウザで開くだけで決済を完了できます。

決済ページ自体はStripe Checkoutです。実際に組み込みする際は、サイトやアプリのCustomer情報と紐づけるなどすることで、よりシームレスな体験も作れそうです。

Honoで構築しているので、完了ページの実装もそのままJSXでやれました。

自分でデプロイする場合
リポジトリをクローンして Cloudflare Workers にデプロイするだけです。
# 依存関係インストール
pnpm install
# Stripe シークレットキーを設定
wrangler secret put STRIPE_SECRET_KEY
# デプロイ
pnpm run deploy
シークレットキーはダッシュボードからも登録できます。

デプロイ後、wrangler.jsonc の name に対応した *.workers.dev URL が MCP エンドポイントになります。
まとめ
今回のデモは「MCP サーバー = AI エージェントのバックエンド API」として Stripe を接続することで、自然言語だけでショッピング体験を完結させる可能性を示しています。
@modelcontextprotocol/ext-apps の App Tool 機能を使うことで、ツールの実行結果をただのテキストではなく インタラクティブな HTML UI としてエージェントホストに表示できる点も注目です。エージェントコマース・AI ショッピングアシスタントを検討している方は、ぜひリポジトリを参考にしてみてください。
- GitHub: hideokamoto/demo-stripe-mcp-app-agent-commerce-like
- ライブデモ MCP:
https://stripe-mcp-server.wp-kyoto.workers.dev/mcp