ucp について
ucpは、Universal Commerce Protocolの実装を支援する開発者向けスキルです。仕様に沿った初期設定、計画、コードの不足分析、スキャフォールド生成を行います。
主な機能
- 01UCP設定と仕様の初期化
- 02相談と実装ロードマップ
- 03ファイル単位の実装計画
- 04ASTによるコード不足分析
- 05UCP実装スキャフォールドの生成
活用シーン
- 01プロジェクトでUCP対応を開始
- 02既存のチェックアウトAPIをUCPに対応付け
- 03不足しているUCP要件を特定
- 04実装ファイルと工程を計画
MCP 対応
MCPをUCPのトランスポートとして設定でき、対応の要否を検討できます。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ vercel-labs/ agentic-commerce-skills --skill ucpインストール後、Claude Code で/ucpを入力して読み込まれたか確認します。
詳細
UCP スキル — Universal Commerce Protocol の実装
中核となる原則
- Edge ランタイムは使用しません — Node.js(デフォルト)または Bun(オプトイン)のランタイムのみを使用します
- 対話型のエラー処理 — 判断が曖昧な場合は、どのように進めるかユーザーに確認します
- 設定駆動 — すべての判断を
ucp.config.jsonに保存します - 仕様に基づく実装 — すべての実装で、正式な UCP 仕様を参照します
- Next.js の規約 — コードの構成には App Router のパターンに従います
- 詳細な分析 — ギャップを検出するために AST 解析とデータフローの追跡を使用します
仕様リポジトリの取り扱い
場所の優先順位
次の順に確認します:
./ucp/— ユーザーのローカルコピー(そのまま使用)./.ucp-spec/— 以前にクローンした仕様(更新して使用)- どちらも存在しない場合 — 新たにクローン
クローン手順
クローンが必要な場合:
git clone --depth 1 https://github.com/Universal-Commerce-Protocol/ucp.git .ucp-spec
HTTPS で失敗した場合は、SSH を試します:
git clone --depth 1 [email protected]:Universal-Commerce-Protocol/ucp.git .ucp-spec
更新手順
./.ucp-spec/ が存在する場合:
cd .ucp-spec && git pull && cd ..
Gitignore の管理
クローン後、.ucp-spec/ が .gitignore に含まれていることを確認します:
.gitignoreが存在する場合は読み込みます.ucp-spec/または.ucp-specがすでに記載されているか確認します- 記載されていない場合は、新しい行に
.ucp-spec/を追加します
仕様ファイルの場所(必要に応じて読み込み)
docs/specification/overview.md
docs/specification/checkout.md
docs/specification/checkout-rest.md
docs/specification/checkout-mcp.md
docs/specification/checkout-a2a.md
docs/specification/embedded-checkout.md
docs/specification/order.md
docs/specification/fulfillment.md
docs/specification/discount.md
docs/specification/buyer-consent.md
docs/specification/identity-linking.md
docs/specification/ap2-mandates.md
docs/specification/payment-handler-guide.md
docs/specification/tokenization-guide.md
spec/services/shopping/rest.openapi.json
spec/services/shopping/mcp.openrpc.json
spec/services/shopping/embedded.openrpc.json
spec/handlers/tokenization/openapi.json
spec/schemas/shopping/*
spec/discovery/profile_schema.json
設定ファイル
場所
プロジェクトルートの ./ucp.config.json
スキーマ
{
"$schema": "./ucp.config.schema.json",
"ucp_version": "2026-01-11",
"roles": ["business"],
"runtime": "nodejs",
"capabilities": {
"core": ["dev.ucp.shopping.checkout"],
"extensions": []
},
"transports": ["rest"],
"transport_priority": ["rest", "mcp", "a2a", "embedded"],
"payment_handlers": [],
"features": {
"ap2_mandates": false,
"identity_linking": false,
"multi_destination_fulfillment": false
},
"domain": "",
"existing_apis": {},
"policy_urls": {
"privacy": "",
"terms": "",
"refunds": "",
"shipping": ""
},
"scaffold_depth": "full",
"generated_files": [],
"answers": {},
"deployment": {
"platform": "vercel",
"region": "iad1",
"mcp": {
"enabled": false,
"max_duration": 60
}
}
}
フィールドの説明
| フィールド | 型 | 説明 |
|---|---|---|
ucp_version | string | UCP 仕様のバージョン(日付ベース) |
roles | string[] | 次のうち1つ以上:business、platform、payment_provider、host_embedded |
runtime | string | nodejs(デフォルト)または bun |
capabilities.core | string[] | 実装が必須の機能 |
capabilities.extensions | string[] | 実装する任意の拡張機能 |
transports | string[] | 有効にするトランスポート:rest、mcp、a2a、embedded |
transport_priority | string[] | トランスポートを実装する順序 |
payment_handlers | string[] | サポートする決済ハンドラー ID |
features.ap2_mandates | boolean | AP2 委任状への署名を有効にします |
features.identity_linking | boolean | OAuth ID の連携を有効にします |
features.multi_destination_fulfillment | boolean | 複数配送先への配送を有効にします |
domain | string | /.well-known/ucp に使用するビジネスドメイン |
existing_apis | object | 分析対象となる既存 API エンドポイントのマップ |
policy_urls | object | プライバシー、利用規約、返品、配送に関するポリシーの URL |
scaffold_depth | string | types | scaffolding | full |
generated_files | string[] | スキャフォールドによって作成されたファイル(追跡用) |
answers | object | 適格性確認の質問に対する回答(生データ) |
サブコマンド: (引数なし)
起動条件
ユーザーがサブコマンドを指定せずに /ucp を実行した場合
動作
利用可能なすべてのサブコマンドを一覧表示するヘルプを表示します:
UCP Skill — Universal Commerce Protocol Implementation
Available commands:
/ucp init — Initialize UCP in this project (clone spec, create config)
/ucp consult — Full consultation: answer qualifying questions, build roadmap
/ucp plan — Generate detailed implementation plan
/ucp gaps — Analyze existing code against UCP requirements
/ucp scaffold — Generate full working UCP implementation
/ucp validate — Validate implementation against UCP schemas
/ucp profile — Generate /.well-known/ucp discovery profile
/ucp test — Generate unit tests for UCP handlers
/ucp docs — Generate internal documentation
Typical workflow:
/ucp init → /ucp consult → /ucp plan → /ucp scaffold → /ucp profile → /ucp test → /ucp validate
Configuration: ./ucp.config.json
Spec location: ./ucp/ or ./.ucp-spec/
サブコマンド:init
起動条件
ユーザーが /ucp init を実行した場合
目的
プロジェクトで UCP を初期化します。仕様をクローンし、設定を作成して、必要な質問をします。
手順
ステップ 1:仕様リポジトリの確認/クローン
./ucp/が存在するか確認します- 存在する場合:「ローカルの UCP 仕様が ./ucp/ に見つかりました」
- 存在しない場合は、
./.ucp-spec/が存在するか確認します- 存在する場合:
git pullを実行して更新します - 存在しない場合:リポジトリをクローンします(「仕様リポジトリの取り扱い」を参照)
- 存在する場合:
- クローン後、
.ucp-spec/を.gitignoreに追加します
ステップ 2:既存の設定を確認
./ucp.config.jsonが存在するか確認します- 存在する場合は、次のように尋ねます:「設定ファイルが存在します。上書き、マージ、中止のどれにしますか?」
- 上書き:削除して新規作成します
- マージ:既存の値をデフォルトとして保持します
- 中止:初期化を停止します
Step 3: 必須項目を質問する(4つの質問)
Q1: どの役割を実装しますか?
- ビジネス(販売者としての記録主体)
- プラットフォーム(コンシューマーアプリまたはエージェント)
- 決済認証情報プロバイダー
- チェックアウトを埋め込むホスト
- 複数(具体的に指定)
ユーザーが複数の役割を選択した場合は、警告します:
"複数の役割を実装するのは一般的ではありません。通常はマーケットプレイス/アグリゲーターのシナリオで行われます。本当によろしいですか?"
Q2: どのランタイムを使用しますか?
- Node.js(推奨、安定)
- Bun(オプトイン、実験的)
注:ユーザーが Edge について言及した場合は、次のように応答します:
"UCP の実装では Edge ランタイムはサポートされていません。Node.js または Bun を選択してください。"
Q3: ビジネスのドメインは何ですか?
/.well-known/ucpをホストするドメイン- 例:
shop.example.com
Q4: リリース時にどのトランスポートが必要ですか?
- REST(推奨される基本構成)
- MCP(Model Context Protocol)
- A2A(エージェント間)
- 埋め込み(iframe チェックアウト)
Step 4: 設定ファイルを作成する
次の内容を含む ./ucp.config.json を作成します:
- 必須項目への回答
- その他のフィールドには妥当なデフォルト値
ucp_versionには仕様の最新バージョンを設定
Step 5: 完了メッセージを出力する
UCP initialized successfully!
Config: ./ucp.config.json
Spec: ./.ucp-spec/ (or ./ucp/)
Role: {role}
Domain: {domain}
Next steps:
/ucp consult — Complete full consultation (recommended)
/ucp plan — Skip to implementation planning
/ucp gaps — Analyze existing code first
サブコマンド:consult
トリガー
ユーザーが /ucp consult を実行します
目的
12個すべての適格性確認の質問を行い、設定を更新し、実装ロードマップを作成します。
前提条件
- 設定ファイルが存在している必要があります(先に
/ucp initを実行してください) - 仕様が利用可能である必要があります
手順
Step 1: 既存の設定を読み込む
./ucp.config.json を読み込み、既存の回答をデフォルトとして使用します。
ステップ 2:12 個の確認事項を順に確認する
各質問を確認します。設定ですでに回答されている場合は、現在の値を示し、確認または変更を求めます。
Q1:ビジネス側、プラットフォーム側、またはその両方を実装しますか?
- 設定の
rolesに対応付けます - 両方/複数の場合は、一般的ではないケースであることを伝えます
Q2:対象となる UCP のバージョンと機能/拡張機能は何ですか?
- 仕様から利用可能なバージョンを読み込みます
- 機能の選択肢を提示します:
- コア:
dev.ucp.shopping.checkout(必須) - 拡張機能:
dev.ucp.shopping.fulfillmentdev.ucp.shopping.discountdev.ucp.shopping.buyer_consentdev.ucp.shopping.ap2_mandatedev.ucp.shopping.orderdev.ucp.common.identity_linking
- コア:
Q3:どの決済ハンドラーが必要ですか?
- ウォレット(Apple Pay、Google Pay)
- PSP によるトークン化(Stripe、Adyen など)
- カスタムハンドラー
- まだなし(後で決定)
Q4:AP2 マンデートと署名鍵のインフラストラクチャが必要ですか?
- はい →
features.ap2_mandates: trueを設定します - いいえ →
features.ap2_mandates: falseを設定します - はいの場合は、「JWS 署名鍵を用意する必要があります(ES256 を推奨)」と説明します
Q5:フルフィルメントの選択肢と、複数グループ/複数配送先への対応が必要ですか?
- フルフィルメントは不要
- 配送先は 1 か所のみ
- 複数配送先に対応 →
features.multi_destination_fulfillment: trueを設定します
Q6:割引、購入者の同意取得、または ID リンクが必要ですか?
- 割引 →
dev.ucp.shopping.discountを拡張機能に追加します - 購入者の同意 →
dev.ucp.shopping.buyer_consentを拡張機能に追加します - ID リンク →
dev.ucp.common.identity_linkingを追加し、features.identity_linking: trueを設定します
Q7:UCP に対応付ける既存のチェックアウト API と注文 API は何ですか?
- 既存のエンドポイントパスを尋ねます
existing_apisオブジェクトに保存します- 例:
/api/checkout、/api/cart、/api/orders
Q8:必須のポリシー URL は何ですか?
- プライバシーポリシーの URL
- 利用規約の URL
- 返金ポリシーの URL
- 配送ポリシーの URL
policy_urlsオブジェクトに保存します
Q9:チェックアウトエンドポイントには、どの認証方式が必要ですか?
- なし(匿名チェックアウト)
- API キー
- OAuth 2.0
- セッションベース
answers.authentication_modelに保存します
Q10:注文 Webhook の受信先はどこで、必要なイベント配信頻度はどのくらいですか?
- 注文イベント用の Webhook URL
- 必要なイベントタイプ:
order.created、order.updated、order.fulfilled、order.canceled answers.webhook_configに保存します
Q11:リリース時に MCP、A2A、または埋め込み型チェックアウトへの対応が必要ですか?
transports配列を確認/更新しますtransport_priorityの順序を設定します
Q12:/.well-known/ucp をホストするビジネスドメインは何ですか?
domainフィールドを確認/更新します
ステップ 3:設定を更新する
すべての回答を ./ucp.config.json に書き込みます
ステップ 4:実装ロードマップを生成する
回答に基づいて、ロードマップを作成します。
UCP Implementation Roadmap
==========================
Role: Business (merchant)
Version: 2026-01-11
Domain: shop.example.com
Capabilities to implement:
✓ dev.ucp.shopping.checkout (core)
✓ dev.ucp.shopping.fulfillment
✓ dev.ucp.shopping.discount
○ dev.ucp.shopping.order
Transports (in order):
1. REST
2. MCP
Payment handlers:
- Stripe tokenization
Key implementation tasks:
1. Create /.well-known/ucp discovery profile
2. Implement checkout session endpoints (create, get, update, complete)
3. Implement fulfillment options logic
4. Implement discount code application
5. Set up payment handler integration
6. Implement order webhooks
7. Add MCP transport layer
Estimated files to create/modify: ~15-20
Run /ucp plan for detailed file-by-file plan.
サブコマンド:plan
トリガー
ユーザーが /ucp plan を実行します
目的
具体的なファイルと作業順序を含む、詳細な実装計画を生成します。
前提条件
- 設定ファイルが存在し、ヒアリングが完了していること
- 仕様を利用できること
手順
ステップ 1:設定と仕様を読み込む
./ucp.config.jsonを読み込みます- 機能/トランスポートに応じた関連仕様ファイルを読み込みます
ステップ 2:既存のコードベース構造を分析する
- Next.js のバージョンを検出します(App Router または Pages Router)
- 既存の API ルートを確認します
- 既存の lib/utils 構造を確認します
- 既存の型/スキーマを確認します
- パッケージマネージャーを特定します(npm、yarn、pnpm、bun)
ステップ 3:ファイル計画を生成する
各機能/トランスポートについて、作成/変更するファイルを列挙します。
出力例:
UCP Implementation Plan
=======================
Phase 1: Core Types and Schemas
-------------------------------
CREATE lib/ucp/types/checkout.ts
- CheckoutSession interface
- LineItem, Totals, Payment types
- Status enum
CREATE lib/ucp/types/index.ts
- Re-export all types
CREATE lib/ucp/schemas/checkout.ts
- Zod schemas for validation
Phase 2: Discovery Profile
--------------------------
CREATE app/.well-known/ucp/route.ts
- GET handler returning profile JSON
- Read capabilities from config
CREATE lib/ucp/profile.ts
- Profile generation logic
Phase 3: Checkout Endpoints (REST)
----------------------------------
CREATE app/api/ucp/checkout/route.ts
- POST: Create checkout session
- Capability negotiation logic
CREATE app/api/ucp/checkout/[id]/route.ts
- GET: Retrieve checkout
- PATCH: Update checkout
- POST: Complete checkout (action=complete)
CREATE lib/ucp/handlers/checkout.ts
- Business logic for checkout operations
- State machine implementation
Phase 4: Fulfillment Extension
------------------------------
CREATE lib/ucp/handlers/fulfillment.ts
- Fulfillment options calculation
- Destination validation
MODIFY lib/ucp/handlers/checkout.ts
- Integrate fulfillment into checkout response
Phase 5: Discount Extension
---------------------------
CREATE lib/ucp/handlers/discount.ts
- Discount code validation
- Applied discount calculation
MODIFY lib/ucp/handlers/checkout.ts
- Integrate discounts into checkout
Phase 6: Payment Integration
----------------------------
CREATE lib/ucp/handlers/payment.ts
- Payment handler registry
- payment_data processing
CREATE lib/ucp/handlers/stripe.ts
- Stripe-specific tokenization
Phase 7: Order Webhooks
-----------------------
CREATE lib/ucp/handlers/order.ts
- Order event emission
- Webhook signing (JWS)
CREATE lib/ucp/webhooks/sender.ts
- Webhook delivery with retries
Phase 8: MCP Transport (if enabled)
-----------------------------------
CREATE lib/ucp/transports/mcp.ts
- MCP tool definitions
- JSON-RPC handlers
Dependencies to install:
------------------------
zod — Schema validation
jose — JWS signing (if AP2/webhooks enabled)
Run /ucp scaffold to generate these files.
ステップ 4:計画を設定に保存する
スキャフォールドから参照できるよう、計画を answers.implementation_plan に保存します。
サブコマンド:gaps
トリガー
ユーザーが /ucp gaps を実行します
目的
AST 解析とデータフローの追跡を使用して、既存のコードベースを UCP の要件と照らし合わせて詳細に分析します。
前提条件
- 設定ファイルが存在すること(ロール/機能のコンテキスト用)
- 仕様を利用できること
手順
ステップ 1:コンテキストを読み込む
- 宣言された機能を確認するために設定を読み込みます
- 関連する仕様ファイルを読み込みます
ステップ 2:既存のコードを調べる
以下を検索します:
- API ルート(
app/api/**、pages/api/**) - チェックアウト関連のファイル(「checkout」、「cart」、「order」を検索)
- 決済処理コード
- Webhook の実装
ステップ 3:詳細分析(AST ベース)
関連するファイルごとに:
- AST を解析します
- チェックアウトオブジェクトのデータフローを追跡します
- 既存のパターンを特定します
UCP 要件に照らして分析します:
| 要件 | 状態 | 調査結果 |
|---|---|---|
/.well-known/ucp のディスカバリープロファイル | 不足 | ルートが見つかりません |
| チェックアウトセッションの作成 | 一部対応 | /api/checkout は見つかりましたが、UCP フィールドが不足しています |
| ステータスのライフサイクル | 不足 | ステータスの状態遷移がありません |
| ケイパビリティネゴシエーション | 不足 | UCP-Agent ヘッダーの処理がありません |
| 決済ハンドラーのサポート | 一部対応 | Stripe はありますが、UCP に準拠していません |
| レスポンスメタデータ(ucp オブジェクト) | 不足 | レスポンスに ucp フィールドが含まれていません |
ステップ 4:ギャップレポートを生成する
UCP Gap Analysis Report
=======================
Existing codebase: Next.js 14 (App Router)
Target role: Business
Target capabilities: checkout, fulfillment, discount
CRITICAL GAPS (must fix)
------------------------
[GAP-001] Missing discovery profile
- Required: /.well-known/ucp endpoint
- Status: NOT FOUND
- Fix: Create app/.well-known/ucp/route.ts
[GAP-002] Missing UCP response envelope
- Required: All responses must include `ucp` object with version/capabilities
- Found: app/api/checkout/route.ts returns raw checkout data
- Fix: Wrap responses with UCP metadata
[GAP-003] Missing capability negotiation
- Required: Read UCP-Agent header, compute intersection
- Found: No header processing in checkout routes
- Fix: Add middleware or handler logic
PARTIAL IMPLEMENTATIONS
-----------------------
[PARTIAL-001] Checkout session exists but non-compliant
- File: app/api/checkout/route.ts
- Missing: id, status, currency, totals.grand_total, links, payment fields
- Has: line_items (needs schema adjustment)
[PARTIAL-002] Payment integration exists
- File: lib/stripe.ts
- Issue: Direct Stripe API, not UCP payment_data flow
- Fix: Wrap with UCP payment handler abstraction
COMPLIANT AREAS
---------------
[OK] Policy URLs configured in existing checkout
[OK] HTTPS enforced
[OK] Idempotency key support in POST handlers
RECOMMENDATIONS
---------------
1. Start with /ucp scaffold to generate compliant structure
2. Migrate existing checkout logic into new handlers
3. Run /ucp validate after migration
Total: 3 critical gaps, 2 partial, 3 compliant
サブコマンド:scaffold
トリガー
ユーザーが /ucp scaffold を実行します
目的
設定と計画に基づいて、完全に動作する UCP 実装を生成します。
前提条件
- 設定ファイルが存在している必要があります
- 計画が存在している必要があります(先に
/ucp planを実行してください。そうでない場合、scaffold が計画を生成します)
手順
ステップ 1:スキャフォールドの深さを確認する
ユーザーに尋ねます:
「どのレベルのコード生成を希望しますか?」
- types:TypeScript インターフェースと Zod スキーマのみ
- scaffolding:ビジネスロジック用の TODO マーカーを含む構造
- full:完全に動作する実装(推奨)
選択内容を config.scaffold_depth に保存します
ステップ 2:依存関係を確認する
設定に基づいて必要なパッケージを特定します:
zod— 常に必要ですjose— AP2 で必須の場合、または Webhook 署名が有効な場合uuid— セッション ID の生成用
インストール前に尋ねます:
「以下のパッケージが必要です:zod、jose、uuid」 「今すぐインストールしますか?(npm install / bun add)」
はいの場合、適切なインストールコマンドを実行します。
ステップ 3:コードを生成する
計画に従ってファイルを生成します。ファイルごとに:
- 必要に応じて親ディレクトリを作成します
- ファイルの内容を書き込みます
config.generated_filesに記録します
コード生成テンプレート
lib/ucp/types/checkout.ts
/**
* UCP Checkout Types
* Generated by /ucp scaffold
* Spec: {spec_version}
*/
export type CheckoutStatus =
| 'incomplete'
| 'requires_escalation'
| 'ready_for_complete'
| 'complete_in_progress'
| 'completed'
| 'canceled';
export type MessageSeverity =
| 'recoverable'
| 'requires_buyer_input'
| 'requires_buyer_review';
export interface UCPMetadata {
version: string;
capabilities: string[];
}
export interface LineItem {
id: string;
name: string;
quantity: number;
unit_price: number;
total_price: number;
currency: string;
// Extension fields added based on config
}
export interface Totals {
subtotal: number;
tax: number;
shipping: number;
discount: number;
grand_total: number;
currency: string;
}
export interface PaymentInfo {
status: 'pending' | 'authorized' | 'captured' | 'failed';
handlers: PaymentHandler[];
amount_due: number;
currency: string;
}
export interface PaymentHandler {
id: string;
type: string;
config?: Record<string, unknown>;
}
export interface CheckoutMessage {
code: string;
severity: MessageSeverity;
message: string;
field?: string;
}
export interface CheckoutLinks {
self: string;
continue_url?: string;
privacy_policy: string;
terms_of_service: string;
refund_policy?: string;
shipping_policy?: string;
}
export interface CheckoutSession {
ucp: UCPMetadata;
id: string;
status: CheckoutStatus;
currency: string;
line_items: LineItem[];
totals: Totals;
payment: PaymentInfo;
links: CheckoutLinks;
messages: CheckoutMessage[];
expires_at: string;
created_at: string;
updated_at: string;
// Extension fields populated based on negotiated capabilities
buyer?: BuyerInfo;
fulfillment?: FulfillmentInfo;
discounts?: DiscountInfo;
}
export interface BuyerInfo {
email?: string;
phone?: string;
name?: string;
// consent fields if buyer_consent extension enabled
}
// Conditional types based on extensions...
lib/ucp/schemas/checkout.ts
/**
* UCP Checkout Zod Schemas
* Generated by /ucp scaffold
*/
import { z } from 'zod';
export const LineItemSchema = z.object({
id: z.string(),
name: z.string(),
quantity: z.number().int().positive(),
unit_price: z.number().int(), // minor units (cents)
total_price: z.number().int(),
currency: z.string().length(3),
});
export const TotalsSchema = z.object({
subtotal: z.number().int(),
tax: z.number().int(),
shipping: z.number().int(),
discount: z.number().int(),
grand_total: z.number().int(),
currency: z.string().length(3),
});
export const CreateCheckoutRequestSchema = z.object({
line_items: z.array(LineItemSchema).min(1),
currency: z.string().length(3),
buyer: z.object({
email: z.string().email().optional(),
phone: z.string().optional(),
}).optional(),
// Extension fields...
});
export const UpdateCheckoutRequestSchema = z.object({
line_items: z.array(LineItemSchema).optional(),
buyer: z.object({
email: z.string().email().optional(),
phone: z.string().optional(),
}).optional(),
// Extension fields...
});
export const CompleteCheckoutRequestSchema = z.object({
action: z.literal('complete'),
payment_data: z.record(z.unknown()),
});
export type CreateCheckoutRequest = z.infer<typeof CreateCheckoutRequestSchema>;
export type UpdateCheckoutRequest = z.infer<typeof UpdateCheckoutRequestSchema>;
export type CompleteCheckoutRequest = z.infer<typeof CompleteCheckoutRequestSchema>;
app/.well-known/ucp/route.ts
/**
* UCP Discovery Profile Endpoint
* GET /.well-known/ucp
* Generated by /ucp scaffold
*/
import { NextResponse } from 'next/server';
import { generateProfile } from '@/lib/ucp/profile';
export const runtime = 'nodejs'; // Edge runtime is not supported
export async function GET() {
const profile = generateProfile();
return NextResponse.json(profile, {
headers: {
'Cache-Control': 'public, max-age=3600',
'Content-Type': 'application/json',
},
});
}
lib/ucp/profile.ts
/**
* UCP Discovery Profile Generator
* Generated by /ucp scaffold
*/
import config from '@/../ucp.config.json';
export interface UCPProfile {
ucp: {
version: string;
services: Record<string, ServiceDefinition>;
capabilities: CapabilityDefinition[];
};
payment?: {
handlers: PaymentHandlerDefinition[];
};
signing_keys?: JsonWebKey[];
}
interface ServiceDefinition {
version: string;
spec: string;
rest?: { schema: string; endpoint: string };
mcp?: { schema: string; endpoint: string };
a2a?: { endpoint: string };
embedded?: { schema: string };
}
interface CapabilityDefinition {
name: string;
version: string;
spec: string;
schema: string;
extends?: string;
config?: Record<string, unknown>;
}
interface PaymentHandlerDefinition {
id: string;
type: string;
spec: string;
config_schema: string;
}
export function generateProfile(): UCPProfile {
const baseUrl = `https://${config.domain}`;
const profile: UCPProfile = {
ucp: {
version: config.ucp_version,
services: {
'dev.ucp.shopping': {
version: config.ucp_version,
spec: 'https://ucp.dev/spec/services/shopping',
...(config.transports.includes('rest') && {
rest: {
schema: 'https://ucp.dev/spec/services/shopping/rest.openapi.json',
endpoint: `${baseUrl}/api/ucp`,
},
}),
...(config.transports.includes('mcp') && {
mcp: {
schema: 'https://ucp.dev/spec/services/shopping/mcp.openrpc.json',
endpoint: `${baseUrl}/api/ucp/mcp`,
},
}),
...(config.transports.includes('a2a') && {
a2a: {
endpoint: `${baseUrl}/api/ucp/a2a`,
},
}),
...(config.transports.includes('embedded') && {
embedded: {
schema: 'https://ucp.dev/spec/services/shopping/embedded.openrpc.json',
},
}),
},
},
capabilities: buildCapabilities(config),
},
};
if (config.payment_handlers.length > 0) {
profile.payment = {
handlers: config.payment_handlers.map(buildHandlerDefinition),
};
}
return profile;
}
function buildCapabilities(config: typeof import('@/../ucp.config.json')): CapabilityDefinition[] {
const capabilities: CapabilityDefinition[] = [];
// Core checkout capability (always present)
capabilities.push({
name: 'dev.ucp.shopping.checkout',
version: config.ucp_version,
spec: 'https://ucp.dev/spec/capabilities/checkout',
schema: 'https://ucp.dev/spec/schemas/shopping/checkout.json',
});
// Add extensions based on config
for (const ext of config.capabilities.extensions) {
capabilities.push(buildExtensionCapability(ext, config));
}
return capabilities;
}
function buildExtensionCapability(
extension: string,
config: typeof import('@/../ucp.config.json')
): CapabilityDefinition {
// Map extension names to spec URLs
const extMap: Record<string, { spec: string; schema: string; extends?: string }> = {
'dev.ucp.shopping.fulfillment': {
spec: 'https://ucp.dev/spec/capabilities/fulfillment',
schema: 'https://ucp.dev/spec/schemas/shopping/fulfillment.json',
extends: 'dev.ucp.shopping.checkout',
},
'dev.ucp.shopping.discount': {
spec: 'https://ucp.dev/spec/capabilities/discount',
schema: 'https://ucp.dev/spec/schemas/shopping/discount.json',
extends: 'dev.ucp.shopping.checkout',
},
'dev.ucp.shopping.buyer_consent': {
spec: 'https://ucp.dev/spec/capabilities/buyer-consent',
schema: 'https://ucp.dev/spec/schemas/shopping/buyer-consent.json',
extends: 'dev.ucp.shopping.checkout',
},
'dev.ucp.shopping.order': {
spec: 'https://ucp.dev/spec/capabilities/order',
schema: 'https://ucp.dev/spec/schemas/shopping/order.json',
config: {
webhook_url: config.answers?.webhook_config?.url,
},
},
'dev.ucp.common.identity_linking': {
spec: 'https://ucp.dev/spec/capabilities/identity-linking',
schema: 'https://ucp.dev/spec/schemas/common/identity-linking.json',
},
};
const def = extMap[extension];
return {
name: extension,
version: config.ucp_version,
spec: def?.spec || '',
schema: def?.schema || '',
...(def?.extends && { extends: def.extends }),
...(def?.config && { config: def.config }),
};
}
function buildHandlerDefinition(handlerId: string): PaymentHandlerDefinition {
// Map known handlers
const handlerMap: Record<string, Omit<PaymentHandlerDefinition, 'id'>> = {
stripe: {
type: 'tokenization',
spec: 'https://ucp.dev/spec/handlers/stripe',
config_schema: 'https://ucp.dev/spec/handlers/stripe/config.json',
},
// Add more handlers as needed
};
const def = handlerMap[handlerId] || {
type: 'custom',
spec: '',
config_schema: '',
};
return { id: handlerId, ...def };
}
app/api/ucp/checkout/route.ts
/**
* UCP Checkout Session Endpoint
* POST /api/ucp/checkout - Create checkout session
* Generated by /ucp scaffold
*/
import { NextRequest, NextResponse } from 'next/server';
import { createCheckout } from '@/lib/ucp/handlers/checkout';
import { CreateCheckoutRequestSchema } from '@/lib/ucp/schemas/checkout';
import { negotiateCapabilities, parseUCPAgent } from '@/lib/ucp/negotiation';
import { wrapResponse, errorResponse } from '@/lib/ucp/response';
export const runtime = 'nodejs'; // Edge runtime is not supported
export async function POST(request: NextRequest) {
try {
// Parse UCP-Agent header for capability negotiation
const ucpAgent = parseUCPAgent(request.headers.get('UCP-Agent'));
// Negotiate capabilities
const negotiation = await negotiateCapabilities(ucpAgent?.profile);
// Parse and validate request body
const body = await request.json();
const parsed = CreateCheckoutRequestSchema.safeParse(body);
if (!parsed.success) {
return errorResponse(400, 'invalid_request', parsed.error.message);
}
// Get idempotency key
const idempotencyKey = request.headers.get('Idempotency-Key');
// Create checkout session
const checkout = await createCheckout(parsed.data, {
capabilities: negotiation.capabilities,
idempotencyKey,
});
return wrapResponse(checkout, negotiation, 201);
} catch (error) {
console.error('Checkout creation failed:', error);
return errorResponse(500, 'internal_error', 'Failed to create checkout session');
}
}
lib/ucp/handlers/checkout.ts
/**
* UCP Checkout Handler
* Core business logic for checkout operations
* Generated by /ucp scaffold
*/
import { randomUUID } from 'crypto';
import type {
CheckoutSession,
CheckoutStatus,
CreateCheckoutRequest,
UpdateCheckoutRequest,
} from '@/lib/ucp/types/checkout';
import config from '@/../ucp.config.json';
// In-memory store for demo - replace with your database
const checkoutStore = new Map<string, CheckoutSession>();
interface CreateCheckoutOptions {
capabilities: string[];
idempotencyKey?: string | null;
}
export async function createCheckout(
request: CreateCheckoutRequest,
options: CreateCheckoutOptions
): Promise<CheckoutSession> {
const id = randomUUID();
const now = new Date().toISOString();
const expiresAt = new Date(Date.now() + 30 * 60 * 1000).toISOString(); // 30 min
// Calculate totals
const subtotal = request.line_items.reduce((sum, item) => sum + item.total_price, 0);
const tax = calculateTax(subtotal); // Implement your tax logic
const shipping = 0; // Set by fulfillment extension
const discount = 0; // Set by discount extension
const checkout: CheckoutSession = {
ucp: {
version: config.ucp_version,
capabilities: options.capabilities,
},
id,
status: 'incomplete',
currency: request.currency,
line_items: request.line_items.map((item, index) => ({
...item,
id: item.id || `line_${index}`,
})),
totals: {
subtotal,
tax,
shipping,
discount,
grand_total: subtotal + tax + shipping - discount,
currency: request.currency,
},
payment: {
status: 'pending',
handlers: getPaymentHandlers(options.capabilities),
amount_due: subtotal + tax + shipping - discount,
currency: request.currency,
},
links: {
self: `https://${config.domain}/api/ucp/checkout/${id}`,
continue_url: `https://${config.domain}/checkout/${id}`,
privacy_policy: config.policy_urls.privacy,
terms_of_service: config.policy_urls.terms,
...(config.policy_urls.refunds && { refund_policy: config.policy_urls.refunds }),
...(config.policy_urls.shipping && { shipping_policy: config.policy_urls.shipping }),
},
messages: [],
expires_at: expiresAt,
created_at: now,
updated_at: now,
};
// Add buyer info if provided
if (request.buyer) {
checkout.buyer = request.buyer;
}
// Validate checkout state and set appropriate status
checkout.status = determineStatus(checkout);
checkout.messages = generateMessages(checkout);
// Store checkout
checkoutStore.set(id, checkout);
return checkout;
}
export async function getCheckout(id: string): Promise<CheckoutSession | null> {
return checkoutStore.get(id) || null;
}
export async function updateCheckout(
id: string,
request: UpdateCheckoutRequest,
capabilities: string[]
): Promise<CheckoutSession | null> {
const checkout = checkoutStore.get(id);
if (!checkout) return null;
// Check if checkout can be modified
if (['completed', 'canceled'].includes(checkout.status)) {
throw new Error('Checkout cannot be modified in current state');
}
// Apply updates
if (request.line_items) {
checkout.line_items = request.line_items;
recalculateTotals(checkout);
}
if (request.buyer) {
checkout.buyer = { ...checkout.buyer, ...request.buyer };
}
// Update metadata
checkout.updated_at = new Date().toISOString();
checkout.ucp.capabilities = capabilities;
checkout.status = determineStatus(checkout);
checkout.messages = generateMessages(checkout);
checkoutStore.set(id, checkout);
return checkout;
}
export async function completeCheckout(
id: string,
paymentData: Record<string, unknown>,
capabilities: string[]
): Promise<CheckoutSession | null> {
const checkout = checkoutStore.get(id);
if (!checkout) return null;
if (checkout.status !== 'ready_for_complete') {
throw new Error('Checkout is not ready for completion');
}
checkout.status = 'complete_in_progress';
checkout.updated_at = new Date().toISOString();
checkoutStore.set(id, checkout);
try {
// Process payment
await processPayment(checkout, paymentData);
checkout.status = 'completed';
checkout.payment.status = 'captured';
checkout.updated_at = new Date().toISOString();
checkoutStore.set(id, checkout);
// Emit order event if order capability enabled
if (capabilities.includes('dev.ucp.shopping.order')) {
await emitOrderCreated(checkout);
}
return checkout;
} catch (error) {
checkout.status = 'incomplete';
checkout.payment.status = 'failed';
checkout.messages.push({
code: 'payment_failed',
severity: 'recoverable',
message: error instanceof Error ? error.message : 'Payment processing failed',
});
checkout.updated_at = new Date().toISOString();
checkoutStore.set(id, checkout);
return checkout;
}
}
function determineStatus(checkout: CheckoutSession): CheckoutStatus {
// Check for missing required fields
const missingFields: string[] = [];
if (!checkout.buyer?.email) {
missingFields.push('buyer.email');
}
// Check fulfillment if extension enabled
if (checkout.fulfillment && !checkout.fulfillment.selected_option) {
missingFields.push('fulfillment.selected_option');
}
if (missingFields.length > 0) {
return 'incomplete';
}
// Check if buyer input needed
if (checkout.messages.some(m => m.severity === 'requires_buyer_input')) {
return 'requires_escalation';
}
return 'ready_for_complete';
}
function generateMessages(checkout: CheckoutSession): CheckoutSession['messages'] {
const messages: CheckoutSession['messages'] = [];
if (!checkout.buyer?.email) {
messages.push({
code: 'missing_email',
severity: 'recoverable',
message: 'Buyer email is required',
field: 'buyer.email',
});
}
return messages;
}
function recalculateTotals(checkout: CheckoutSession): void {
const subtotal = checkout.line_items.reduce((sum, item) => sum + item.total_price, 0);
checkout.totals.subtotal = subtotal;
checkout.totals.tax = calculateTax(subtotal);
checkout.totals.grand_total =
subtotal + checkout.totals.tax + checkout.totals.shipping - checkout.totals.discount;
checkout.payment.amount_due = checkout.totals.grand_total;
}
function calculateTax(subtotal: number): number {
// Implement your tax calculation logic
return Math.round(subtotal * 0.08); // Example: 8% tax
}
function getPaymentHandlers(capabilities: string[]): CheckoutSession['payment']['handlers'] {
// Return configured payment handlers
return config.payment_handlers.map(id => ({
id,
type: 'tokenization',
}));
}
async function processPayment(
checkout: CheckoutSession,
paymentData: Record<string, unknown>
): Promise<void> {
// Validate handler_id against advertised handlers
const handlerId = paymentData.handler_id as string;
if (!config.payment_handlers.includes(handlerId)) {
throw new Error(`Unknown payment handler: ${handlerId}`);
}
// Implement payment processing based on handler
// This is where you integrate with Stripe, etc.
}
async function emitOrderCreated(checkout: CheckoutSession): Promise<void> {
// Implement order webhook emission
// See lib/ucp/webhooks/sender.ts
}
lib/ucp/negotiation.ts
/**
* UCP Capability Negotiation
* Generated by /ucp scaffold
*/
import config from '@/../ucp.config.json';
interface UCPAgentInfo {
profile: string;
}
interface NegotiationResult {
capabilities: string[];
version: string;
}
/**
* Parse UCP-Agent header (RFC 8941 dictionary syntax)
* Example: profile="https://platform.example.com/.well-known/ucp"
*/
export function parseUCPAgent(header: string | null): UCPAgentInfo | null {
if (!header) return null;
const profileMatch = header.match(/profile="([^"]+)"/);
if (!profileMatch) return null;
return { profile: profileMatch[1] };
}
/**
* Negotiate capabilities between business and platform
*/
export async function negotiateCapabilities(
platformProfileUrl?: string
): Promise<NegotiationResult> {
// Business capabilities
const businessCapabilities = new Set([
...config.capabilities.core,
...config.capabilities.extensions,
]);
if (!platformProfileUrl) {
// No platform profile - return all business capabilities
return {
capabilities: Array.from(businessCapabilities),
version: config.ucp_version,
};
}
try {
// Fetch platform profile
const response = await fetch(platformProfileUrl, {
headers: { Accept: 'application/json' },
});
if (!response.ok) {
console.warn(`Failed to fetch platform profile: ${response.status}`);
return {
capabilities: Array.from(businessCapabilities),
version: config.ucp_version,
};
}
const platformProfile = await response.json();
// Validate namespace authority
const profileUrl = new URL(platformProfileUrl);
// Platform controls its own domain - trust it
// Compute intersection
const platformCapabilities = new Set(
platformProfile.ucp?.capabilities?.map((c: { name: string }) => c.name) || []
);
const intersection = [...businessCapabilities].filter(c =>
platformCapabilities.has(c)
);
// Version negotiation - accept platform version <= business version
const platformVersion = platformProfile.ucp?.version;
if (platformVersion && platformVersion > config.ucp_version) {
throw new Error('version_unsupported');
}
return {
capabilities: intersection,
version: config.ucp_version,
};
} catch (error) {
console.error('Capability negotiation failed:', error);
// Fall back to business capabilities
return {
capabilities: Array.from(businessCapabilities),
version: config.ucp_version,
};
}
}
lib/ucp/response.ts
/**
* UCP Response Helpers
* Generated by /ucp scaffold
*/
import { NextResponse } from 'next/server';
import type { NegotiationResult } from './negotiation';
/**
* Wrap a response with UCP metadata
*/
export function wrapResponse<T extends { ucp?: unknown }>(
data: T,
negotiation: NegotiationResult,
status: number = 200
): NextResponse {
// Ensure ucp metadata is present
const response = {
...data,
ucp: {
version: negotiation.version,
capabilities: negotiation.capabilities,
},
};
return NextResponse.json(response, { status });
}
/**
* Create an error response
*/
export function errorResponse(
status: number,
code: string,
message: string,
details?: Record<string, unknown>
): NextResponse {
return NextResponse.json(
{
error: {
code,
message,
...(details && { details }),
},
},
{ status }
);
}
MCP トランスポートのコードテンプレート(mcp-handler を使用)
設定で MCP トランスポートが有効になっている場合、以下のファイルも生成します。
MCP トランスポートの依存関係
npm install mcp-handler @modelcontextprotocol/[email protected] zod
重要: @modelcontextprotocol/[email protected] 以降を使用してください。それ以前のバージョンにはセキュリティ上の脆弱性があります。
app/api/mcp/[transport]/route.ts
/**
* UCP MCP Transport Endpoint
* Model Context Protocol server for UCP checkout operations
* Generated by /ucp scaffold
*
* Supports:
* - Streamable HTTP transport (direct client connection)
* - SSE transport (via mcp-remote bridge)
*
* @see https://github.com/vercel/mcp-handler
*/
import { createMcpHandler } from 'mcp-handler';
import { z } from 'zod';
import {
createCheckout,
getCheckout,
updateCheckout,
completeCheckout,
} from '@/lib/ucp/handlers/checkout';
import { negotiateCapabilities } from '@/lib/ucp/negotiation';
import { generateProfile } from '@/lib/ucp/profile';
import config from '@/../ucp.config.json';
export const runtime = 'nodejs'; // Edge runtime is not supported
const handler = createMcpHandler(
(server) => {
// =========================================================================
// UCP Discovery
// =========================================================================
server.registerTool(
'ucp_get_profile',
{
title: 'Get UCP Profile',
description: 'Retrieve the UCP discovery profile for this business. Returns supported capabilities, transports, and payment handlers.',
inputSchema: {},
},
async () => {
const profile = generateProfile();
return {
content: [
{
type: 'text',
text: JSON.stringify(profile, null, 2),
},
],
};
}
);
// =========================================================================
// Checkout Session Management
// =========================================================================
server.registerTool(
'ucp_create_checkout',
{
title: 'Create Checkout Session',
description: 'Create a new UCP checkout session with line items. Returns a checkout session with id, status, totals, and available payment handlers.',
inputSchema: {
line_items: z.array(
z.object({
id: z.string().optional().describe('Unique identifier for the line item'),
name: z.string().describe('Product name'),
quantity: z.number().int().positive().describe('Quantity ordered'),
unit_price: z.number().int().describe('Price per unit in minor units (cents)'),
total_price: z.number().int().describe('Total price for this line (quantity * unit_price)'),
currency: z.string().length(3).describe('ISO 4217 currency code'),
})
).min(1).describe('Array of items in the checkout'),
currency: z.string().length(3).describe('ISO 4217 currency code for the checkout'),
buyer: z.object({
email: z.string().email().optional().describe('Buyer email address'),
phone: z.string().optional().describe('Buyer phone number'),
name: z.string().optional().describe('Buyer full name'),
}).optional().describe('Buyer information'),
platform_profile_url: z.string().url().optional().describe('URL to the platform UCP profile for capability negotiation'),
},
},
async ({ line_items, currency, buyer, platform_profile_url }) => {
try {
// Negotiate capabilities
const negotiation = await negotiateCapabilities(platform_profile_url);
const checkout = await createCheckout(
{ line_items, currency, buyer },
{ capabilities: negotiation.capabilities }
);
return {
content: [
{
type: 'text',
text: JSON.stringify(checkout, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: {
code: 'checkout_creation_failed',
message: error instanceof Error ? error.message : 'Unknown error',
},
}),
},
],
isError: true,
};
}
}
);
server.registerTool(
'ucp_get_checkout',
{
title: 'Get Checkout Session',
description: 'Retrieve an existing checkout session by ID. Returns the current state including status, line items, totals, and messages.',
inputSchema: {
checkout_id: z.string().describe('The checkout session ID'),
},
},
async ({ checkout_id }) => {
const checkout = await getCheckout(checkout_id);
if (!checkout) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: {
code: 'checkout_not_found',
message: `Checkout session ${checkout_id} not found`,
},
}),
},
],
isError: true,
};
}
return {
content: [
{
type: 'text',
text: JSON.stringify(checkout, null, 2),
},
],
};
}
);
server.registerTool(
'ucp_update_checkout',
{
title: 'Update Checkout Session',
description: 'Update an existing checkout session. Can modify line items, buyer info, fulfillment selection, or discount codes.',
inputSchema: {
checkout_id: z.string().describe('The checkout session ID'),
line_items: z.array(
z.object({
id: z.string(),
name: z.string(),
quantity: z.number().int().positive(),
unit_price: z.number().int(),
total_price: z.number().int(),
currency: z.string().length(3),
})
).optional().describe('Updated line items (replaces existing)'),
buyer: z.object({
email: z.string().email().optional(),
phone: z.string().optional(),
name: z.string().optional(),
}).optional().describe('Updated buyer information (merged with existing)'),
fulfillment: z.object({
selected_option_id: z.string().optional().describe('ID of selected fulfillment option'),
destination: z.object({
address_line1: z.string(),
address_line2: z.string().optional(),
city: z.string(),
state: z.string().optional(),
postal_code: z.string(),
country: z.string().length(2),
}).optional().describe('Shipping destination address'),
}).optional().describe('Fulfillment selection (if fulfillment extension enabled)'),
discount_codes: z.array(z.string()).optional().describe('Discount codes to apply (if discount extension enabled)'),
platform_profile_url: z.string().url().optional().describe('Platform profile URL for capability negotiation'),
},
},
async ({ checkout_id, line_items, buyer, fulfillment, discount_codes, platform_profile_url }) => {
try {
const negotiation = await negotiateCapabilities(platform_profile_url);
const checkout = await updateCheckout(
checkout_id,
{ line_items, buyer },
negotiation.capabilities
);
if (!checkout) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: {
code: 'checkout_not_found',
message: `Checkout session ${checkout_id} not found`,
},
}),
},
],
isError: true,
};
}
return {
content: [
{
type: 'text',
text: JSON.stringify(checkout, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: {
code: 'checkout_update_failed',
message: error instanceof Error ? error.message : 'Unknown error',
},
}),
},
],
isError: true,
};
}
}
);
server.registerTool(
'ucp_complete_checkout',
{
title: 'Complete Checkout',
description: 'Complete a checkout session with payment. Checkout must be in ready_for_complete status. Returns the completed checkout or error messages.',
inputSchema: {
checkout_id: z.string().describe('The checkout session ID'),
payment_data: z.object({
handler_id: z.string().describe('Payment handler ID (must match one from checkout.payment.handlers)'),
token: z.string().optional().describe('Payment token from tokenization handler'),
instrument: z.record(z.unknown()).optional().describe('Payment instrument data'),
}).describe('Payment data from the selected payment handler'),
platform_profile_url: z.string().url().optional().describe('Platform profile URL'),
},
},
async ({ checkout_id, payment_data, platform_profile_url }) => {
try {
const negotiation = await negotiateCapabilities(platform_profile_url);
const checkout = await completeCheckout(
checkout_id,
payment_data,
negotiation.capabilities
);
if (!checkout) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: {
code: 'checkout_not_found',
message: `Checkout session ${checkout_id} not found`,
},
}),
},
],
isError: true,
};
}
return {
content: [
{
type: 'text',
text: JSON.stringify(checkout, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: {
code: 'checkout_completion_failed',
message: error instanceof Error ? error.message : 'Unknown error',
},
}),
},
],
isError: true,
};
}
}
);
// =========================================================================
// Fulfillment Extension Tools (if enabled)
// =========================================================================
if (config.capabilities.extensions.includes('dev.ucp.shopping.fulfillment')) {
server.registerTool(
'ucp_get_fulfillment_options',
{
title: 'Get Fulfillment Options',
description: 'Get available fulfillment/shipping options for a checkout. Requires a destination address.',
inputSchema: {
checkout_id: z.string().describe('The checkout session ID'),
destination: z.object({
address_line1: z.string(),
address_line2: z.string().optional(),
city: z.string(),
state: z.string().optional(),
postal_code: z.string(),
country: z.string().length(2).describe('ISO 3166-1 alpha-2 country code'),
}).describe('Shipping destination'),
},
},
async ({ checkout_id, destination }) => {
// Implementation would call fulfillment handler
return {
content: [
{
type: 'text',
text: JSON.stringify({
checkout_id,
destination,
options: [
{
id: 'standard',
name: 'Standard Shipping',
description: '5-7 business days',
price: 599,
currency: 'USD',
},
{
id: 'express',
name: 'Express Shipping',
description: '2-3 business days',
price: 1299,
currency: 'USD',
},
],
}, null, 2),
},
],
};
}
);
}
// =========================================================================
// Discount Extension Tools (if enabled)
// =========================================================================
if (config.capabilities.extensions.includes('dev.ucp.shopping.discount')) {
server.registerTool(
'ucp_validate_discount',
{
title: 'Validate Discount Code',
description: 'Validate a discount code before applying to checkout. Returns discount details or rejection reason.',
inputSchema: {
checkout_id: z.string().describe('The checkout session ID'),
code: z.string().describe('The discount code to validate'),
},
},
async ({ checkout_id, code }) => {
// Implementation would call discount handler
return {
content: [
{
type: 'text',
text: JSON.stringify({
valid: true,
code,
discount: {
type: 'percentage',
value: 10,
description: '10% off your order',
},
}, null, 2),
},
],
};
}
);
}
// =========================================================================
// Payment Handler Tools
// =========================================================================
server.registerTool(
'ucp_get_payment_handlers',
{
title: 'Get Payment Handlers',
description: 'Get available payment handlers for a checkout session. Use this to determine how to collect payment information.',
inputSchema: {
checkout_id: z.string().describe('The checkout session ID'),
},
},
async ({ checkout_id }) => {
const checkout = await getCheckout(checkout_id);
if (!checkout) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: {
code: 'checkout_not_found',
message: `Checkout session ${checkout_id} not found`,
},
}),
},
],
isError: true,
};
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
checkout_id,
amount_due: checkout.payment.amount_due,
currency: checkout.payment.currency,
handlers: checkout.payment.handlers,
}, null, 2),
},
],
};
}
);
},
{
// Server metadata
name: 'ucp-shopping',
version: config.ucp_version,
},
{
// Handler options
basePath: '/api/mcp',
maxDuration: 60,
verboseLogs: process.env.NODE_ENV === 'development',
}
);
export { handler as GET, handler as POST };
lib/ucp/transports/mcp-tools.ts
/**
* UCP MCP Tool Definitions
* Reusable tool schemas for MCP transport
* Generated by /ucp scaffold
*/
import { z } from 'zod';
// Common schemas
export const LineItemSchema = z.object({
id: z.string().optional(),
name: z.string(),
quantity: z.number().int().positive(),
unit_price: z.number().int(),
total_price: z.number().int(),
currency: z.string().length(3),
});
export const BuyerSchema = z.object({
email: z.string().email().optional(),
phone: z.string().optional(),
name: z.string().optional(),
});
export const AddressSchema = z.object({
address_line1: z.string(),
address_line2: z.string().optional(),
city: z.string(),
state: z.string().optional(),
postal_code: z.string(),
country: z.string().length(2),
});
export const PaymentDataSchema = z.object({
handler_id: z.string(),
token: z.string().optional(),
instrument: z.record(z.unknown()).optional(),
});
// Tool definitions for documentation
export const UCP_MCP_TOOLS = {
ucp_get_profile: {
description: 'Get UCP discovery profile',
input: {},
},
ucp_create_checkout: {
description: 'Create a new checkout session',
input: {
line_items: 'Array of line items (required)',
currency: 'ISO 4217 currency code (required)',
buyer: 'Buyer information (optional)',
platform_profile_url: 'Platform UCP profile URL (optional)',
},
},
ucp_get_checkout: {
description: 'Get checkout session by ID',
input: {
checkout_id: 'Checkout session ID (required)',
},
},
ucp_update_checkout: {
description: 'Update checkout session',
input: {
checkout_id: 'Checkout session ID (required)',
line_items: 'Updated line items (optional)',
buyer: 'Updated buyer info (optional)',
fulfillment: 'Fulfillment selection (optional)',
discount_codes: 'Discount codes to apply (optional)',
},
},
ucp_complete_checkout: {
description: 'Complete checkout with payment',
input: {
checkout_id: 'Checkout session ID (required)',
payment_data: 'Payment handler data (required)',
},
},
ucp_get_fulfillment_options: {
description: 'Get shipping options (fulfillment extension)',
input: {
checkout_id: 'Checkout session ID (required)',
destination: 'Shipping address (required)',
},
},
ucp_validate_discount: {
description: 'Validate discount code (discount extension)',
input: {
checkout_id: 'Checkout session ID (required)',
code: 'Discount code (required)',
},
},
ucp_get_payment_handlers: {
description: 'Get available payment handlers',
input: {
checkout_id: 'Checkout session ID (required)',
},
},
} as const;
Vercel へのデプロイ
デプロイ設定
vercel.json
{
"framework": "nextjs",
"regions": ["iad1"],
"functions": {
"app/api/mcp/[transport]/route.ts": {
"maxDuration": 60
},
"app/api/ucp/**/*.ts": {
"maxDuration": 30
}
},
"headers": [
{
"source": "/.well-known/ucp",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=3600"
},
{
"key": "Content-Type",
"value": "application/json"
}
]
}
]
}
環境変数
Vercel Dashboard → Settings → Environment Variables で次の値を設定します:
| 変数 | 必須 | 説明 |
|---|---|---|
UCP_DOMAIN | はい | 本番ドメイン(例:shop.example.com) |
UCP_SIGNING_KEY | AP2 の場合 | JWS 署名キー(PEM または JWK) |
STRIPE_SECRET_KEY | Stripe の場合 | Stripe API のシークレットキー |
next.config.js(MCP に最適化)
/** @type {import('next').NextConfig} */
const nextConfig = {
// Required for mcp-handler streaming
experimental: {
serverActions: {
bodySizeLimit: '2mb',
},
},
// Ensure proper headers for MCP
async headers() {
return [
{
source: '/api/mcp/:path*',
headers: [
{ key: 'Access-Control-Allow-Origin', value: '*' },
{ key: 'Access-Control-Allow-Methods', value: 'GET, POST, OPTIONS' },
{ key: 'Access-Control-Allow-Headers', value: 'Content-Type, Authorization' },
],
},
];
},
// Required for MCP streaming responses
async rewrites() {
return [];
},
};
module.exports = nextConfig;
MCP クライアントの設定
Claude Desktop/Cursor/Windsurf 向け
オプション 1:直接 HTTP(クライアントがストリーミング可能な HTTP に対応している場合) MCP クライアントの設定に追加します:
{
"mcpServers": {
"ucp-shopping": {
"url": "https://your-domain.vercel.app/api/mcp"
}
}
}
オプション 2:mcp-remote ブリッジ経由(stdio のみのクライアント向け)
{
"mcpServers": {
"ucp-shopping": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://your-domain.vercel.app/api/mcp"]
}
}
}
MCP デプロイのテスト
scripts/test-mcp.mjs
#!/usr/bin/env node
/**
* MCP Server Test Script
* Usage: node scripts/test-mcp.mjs [deployment-url]
*/
const deploymentUrl = process.argv[2] || 'http://localhost:3000';
async function testMcpServer() {
console.log(`Testing MCP server at ${deploymentUrl}/api/mcp\n`);
// Test 1: Get Profile
console.log('1. Testing ucp_get_profile...');
const profileResponse = await fetch(`${deploymentUrl}/api/mcp`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'tools/call',
params: {
name: 'ucp_get_profile',
arguments: {},
},
}),
});
const profileResult = await profileResponse.json();
console.log(' Profile:', profileResult.result ? 'OK' : 'FAILED');
// Test 2: Create Checkout
console.log('2. Testing ucp_create_checkout...');
const createResponse = await fetch(`${deploymentUrl}/api/mcp`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 2,
method: 'tools/call',
params: {
name: 'ucp_create_checkout',
arguments: {
line_items: [
{
name: 'Test Product',
quantity: 1,
unit_price: 1000,
total_price: 1000,
currency: 'USD',
},
],
currency: 'USD',
},
},
}),
});
const createResult = await createResponse.json();
console.log(' Create:', createResult.result ? 'OK' : 'FAILED');
// Parse checkout ID for subsequent tests
if (createResult.result?.content?.[0]?.text) {
const checkout = JSON.parse(createResult.result.content[0].text);
console.log(` Checkout ID: ${checkout.id}`);
console.log(` Status: ${checkout.status}`);
// Test 3: Get Checkout
console.log('3. Testing ucp_get_checkout...');
const getResponse = await fetch(`${deploymentUrl}/api/mcp`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 3,
method: 'tools/call',
params: {
name: 'ucp_get_checkout',
arguments: { checkout_id: checkout.id },
},
}),
});
const getResult = await getResponse.json();
console.log(' Get:', getResult.result ? 'OK' : 'FAILED');
}
console.log('\nMCP server tests completed.');
}
testMcpServer().catch(console.error);
Vercel デプロイのチェックリスト
Vercel へのデプロイで /ucp scaffold を実行する場合は、以下を確認してください。
- 関数のタイムアウトを設定した
vercel.jsonが作成されている - 環境変数が文書化されている
- MCP のストリーミング用に
next.config.jsが更新されている - MCP ルートが
app/api/mcp/[transport]/route.tsにある - テストスクリプトが
scripts/test-mcp.mjsにある - 必須変数を含めて
.env.exampleが更新されている
デプロイ後の確認
Vercel にデプロイした後:
-
ディスカバリープロファイルを確認:
curl https://your-domain.vercel.app/.well-known/ucp | jq . -
MCP エンドポイントをテスト:
node scripts/test-mcp.mjs https://your-domain.vercel.app -
MCP クライアントを設定: サーバーを Claude Desktop、Cursor、またはお好みの MCP クライアントに追加します。
-
クライアントで確認: AI に「利用可能な UCP ツールを一覧表示して」と尋ねます。チェックアウトツールが表示されるはずです。
生成後の手順
ステップ 4:設定を更新
生成後、ucp.config.json を更新します。
- 作成したすべてのファイルを
generated_files配列に追加します - 生成内容を反映するように
scaffold_depthを更新します
ステップ 5:概要を出力
UCP Scaffold Complete
=====================
Generated files:
CREATE lib/ucp/types/checkout.ts
CREATE lib/ucp/types/index.ts
CREATE lib/ucp/schemas/checkout.ts
CREATE lib/ucp/profile.ts
CREATE lib/ucp/negotiation.ts
CREATE lib/ucp/response.ts
CREATE lib/ucp/handlers/checkout.ts
CREATE app/.well-known/ucp/route.ts
CREATE app/api/ucp/checkout/route.ts
CREATE app/api/ucp/checkout/[id]/route.ts
MCP Transport (if enabled):
CREATE app/api/mcp/[transport]/route.ts
CREATE lib/ucp/transports/mcp-tools.ts
Vercel Deployment:
CREATE vercel.json
CREATE scripts/test-mcp.mjs
MODIFY next.config.js
Dependencies installed:
zod, jose, uuid
mcp-handler, @modelcontextprotocol/sdk (if MCP enabled)
Next steps:
1. Review generated code and customize business logic
2. Set environment variables in Vercel dashboard
3. Deploy: vercel --prod
4. Run /ucp profile to verify discovery profile
5. Run /ucp test to generate unit tests
6. Run /ucp validate to check compliance
7. Configure MCP client with deployment URL
サブコマンド:validate
トリガー
ユーザーが /ucp validate を実行します
目的
UCP JSON スキーマに照らして実装を検証します。
前提条件
- 実装が存在すること(先に
/ucp scaffoldを実行してください) - 仕様が利用可能であること
手順
ステップ 1:スキーマを読み込む
仕様から JSON スキーマを読み込みます。
spec/schemas/shopping/checkout.jsonspec/schemas/shopping/fulfillment.jsonspec/schemas/shopping/discount.jsonspec/discovery/profile_schema.json
ステップ 2:ディスカバリープロファイルを検証
/.well-known/ucpルートにリクエストを送信する(またはファイルを直接読み込む)profile_schema.jsonに照らして検証する
ステップ 3:レスポンスの形式を検証
実装済みの各エンドポイントについて:
- サンプルリクエストを生成する
- ハンドラーを実行する(モックモード)
- スキーマに照らしてレスポンスを検証する
ステップ 4:プロトコル要件を確認
以下を確認します。
- すべてのレスポンスに
ucpオブジェクトが含まれている - ステータス値が有効な列挙値である
- 金額が整数(最小単位)である
- 日付が RFC 3339 形式である
- リンクが絶対 HTTPS URL である
- ケイパビリティ名が逆 DNS 形式に従っている
ステップ 5:レポートを出力
UCP Validation Report
=====================
Discovery Profile: PASS
✓ Schema valid
✓ Required fields present
✓ Service definitions valid
✓ Capability specs accessible
Checkout Endpoints:
POST /api/ucp/checkout
✓ Response schema valid
✓ UCP metadata present
✓ Status enum valid
GET /api/ucp/checkout/[id]
✓ Response schema valid
✓ Idempotent
PATCH /api/ucp/checkout/[id]
✓ Response schema valid
✓ Partial update works
POST /api/ucp/checkout/[id] (complete)
✓ Response schema valid
✓ State transition correct
Protocol Requirements:
✓ Amounts in minor units
✓ Dates in RFC 3339
✓ HTTPS links
✓ Reverse-DNS capability names
Overall: PASS (24/24 checks)
サブコマンド:profile
トリガー
ユーザーが /ucp profile を実行します
目的
/.well-known/ucp ディスカバリープロファイル JSON を生成して表示します。
前提条件
- 設定が存在すること
手順
ステップ 1:プロファイルを生成
lib/ucp/profile.ts の generateProfile() 関数を使用します(または、まだスキャフォールドされていない場合はインラインで生成します)。
ステップ 2:プロファイルを表示
整形済み JSON を出力します。
{
"ucp": {
"version": "2026-01-11",
"services": {
"dev.ucp.shopping": {
"version": "2026-01-11",
"spec": "https://ucp.dev/spec/services/shopping",
"rest": {
"schema": "https://ucp.dev/spec/services/shopping/rest.openapi.json",
"endpoint": "https://shop.example.com/api/ucp"
}
}
},
"capabilities": [
{
"name": "dev.ucp.shopping.checkout",
"version": "2026-01-11",
"spec": "https://ucp.dev/spec/capabilities/checkout",
"schema": "https://ucp.dev/spec/schemas/shopping/checkout.json"
}
]
}
}
ステップ 3:ファイルへの書き込みを提案
「静的配信のために public/.well-known/ucp に書き込みますか。それとも動的ルートとして保持しますか?」
静的配信の場合:
public/.well-known/ucpを作成します(拡張子なし、JSON コンテンツ)- 注意:拡張子のないファイルには Next.js の設定が必要になる場合があります
サブコマンド:test
トリガー
ユーザーが /ucp test を実行します
目的
UCP ハンドラーのユニットテストを生成します。
前提条件
- 実装が存在すること
- テストフレームワークが検出されていること(Jest、Vitest など)
手順
ステップ 1:テストフレームワークを検出
以下を確認します。
jest.config.js/jest.config.ts→ Jestvitest.config.js/vitest.config.ts→ Vitestpackage.jsonのテストスクリプトに関するヒント
ステップ 2:テストファイルを生成
各ハンドラーに対応するテストファイルを生成します。
例:lib/ucp/handlers/tests/checkout.test.ts
/**
* UCP Checkout Handler Tests
* Generated by /ucp test
*/
import { describe, it, expect, beforeEach } from 'vitest'; // or jest
import {
createCheckout,
getCheckout,
updateCheckout,
completeCheckout,
} from '../checkout';
describe('UCP Checkout Handler', () => {
const validRequest = {
line_items: [
{
id: 'item_1',
name: 'Test Product',
quantity: 1,
unit_price: 1000,
total_price: 1000,
currency: 'USD',
},
],
currency: 'USD',
};
describe('createCheckout', () => {
it('creates a checkout with valid request', async () => {
const checkout = await createCheckout(validRequest, {
capabilities: ['dev.ucp.shopping.checkout'],
});
expect(checkout.id).toBeDefined();
expect(checkout.status).toBe('incomplete');
expect(checkout.currency).toBe('USD');
expect(checkout.line_items).toHaveLength(1);
expect(checkout.ucp.version).toBeDefined();
expect(checkout.ucp.capabilities).toContain('dev.ucp.shopping.checkout');
});
it('calculates totals correctly', async () => {
const checkout = await createCheckout(validRequest, {
capabilities: ['dev.ucp.shopping.checkout'],
});
expect(checkout.totals.subtotal).toBe(1000);
expect(checkout.totals.grand_total).toBeGreaterThanOrEqual(checkout.totals.subtotal);
});
it('sets expiration time', async () => {
const checkout = await createCheckout(validRequest, {
capabilities: ['dev.ucp.shopping.checkout'],
});
expect(checkout.expires_at).toBeDefined();
const expiresAt = new Date(checkout.expires_at);
expect(expiresAt.getTime()).toBeGreaterThan(Date.now());
});
it('includes required links', async () => {
const checkout = await createCheckout(validRequest, {
capabilities: ['dev.ucp.shopping.checkout'],
});
expect(checkout.links.self).toContain(checkout.id);
expect(checkout.links.privacy_policy).toBeDefined();
expect(checkout.links.terms_of_service).toBeDefined();
});
});
describe('getCheckout', () => {
it('retrieves existing checkout', async () => {
const created = await createCheckout(validRequest, {
capabilities: ['dev.ucp.shopping.checkout'],
});
const retrieved = await getCheckout(created.id);
expect(retrieved).toEqual(created);
});
it('returns null for non-existent checkout', async () => {
const retrieved = await getCheckout('non-existent-id');
expect(retrieved).toBeNull();
});
});
describe('updateCheckout', () => {
it('updates line items', async () => {
const created = await createCheckout(validRequest, {
capabilities: ['dev.ucp.shopping.checkout'],
});
const updated = await updateCheckout(
created.id,
{
line_items: [
{ ...validRequest.line_items[0], quantity: 2, total_price: 2000 },
],
},
['dev.ucp.shopping.checkout']
);
expect(updated?.totals.subtotal).toBe(2000);
});
it('updates buyer info', async () => {
const created = await createCheckout(validRequest, {
capabilities: ['dev.ucp.shopping.checkout'],
});
const updated = await updateCheckout(
created.id,
{ buyer: { email: '[email protected]' } },
['dev.ucp.shopping.checkout']
);
expect(updated?.buyer?.email).toBe('[email protected]');
});
it('transitions to ready_for_complete when requirements met', async () => {
const created = await createCheckout(validRequest, {
capabilities: ['dev.ucp.shopping.checkout'],
});
const updated = await updateCheckout(
created.id,
{ buyer: { email: '[email protected]' } },
['dev.ucp.shopping.checkout']
);
expect(updated?.status).toBe('ready_for_complete');
});
});
describe('status lifecycle', () => {
it('follows correct state transitions', async () => {
// incomplete -> ready_for_complete -> complete_in_progress -> completed
const checkout = await createCheckout(validRequest, {
capabilities: ['dev.ucp.shopping.checkout'],
});
expect(checkout.status).toBe('incomplete');
const updated = await updateCheckout(
checkout.id,
{ buyer: { email: '[email protected]' } },
['dev.ucp.shopping.checkout']
);
expect(updated?.status).toBe('ready_for_complete');
});
});
});
ステップ 3:概要を出力
Generated test files:
CREATE lib/ucp/handlers/__tests__/checkout.test.ts
CREATE lib/ucp/handlers/__tests__/fulfillment.test.ts
CREATE lib/ucp/__tests__/negotiation.test.ts
CREATE lib/ucp/__tests__/profile.test.ts
CREATE app/api/ucp/__tests__/checkout.route.test.ts
Run tests with: npm test (or bun test)
サブコマンド:docs
トリガー
ユーザーが /ucp docs を実行します
目的
UCP 統合に関する内部ドキュメントを生成します。
前提条件
- 設定が存在すること
- 実装が存在することが望ましい
手順
ステップ 1:情報を収集
- ケイパビリティ、トランスポート、ハンドラーについて設定を読み込む
- 生成されたファイルをスキャンする
- 正確な説明のために仕様ファイルを読み込む
ステップ 2:ドキュメントの生成
docs/ucp-integration.md を作成します:
# UCP Integration Documentation
## Overview
This codebase implements the Universal Commerce Protocol (UCP) version {version}.
**Role:** {role}
**Domain:** {domain}
**Transports:** {transports}
## Capabilities
### Core
- `dev.ucp.shopping.checkout` - Checkout session management
### Extensions
{list extensions with descriptions}
## API Endpoints
### Discovery
- `GET /.well-known/ucp` - UCP discovery profile
### Checkout (REST)
- `POST /api/ucp/checkout` - Create checkout session
- `GET /api/ucp/checkout/:id` - Get checkout session
- `PATCH /api/ucp/checkout/:id` - Update checkout session
- `POST /api/ucp/checkout/:id` (action=complete) - Complete checkout
## Checkout Status Lifecycle
incomplete → requires_escalation → ready_for_complete → complete_in_progress → completed ↘ canceled
## Payment Handlers
{list configured handlers with integration notes}
## Capability Negotiation
The platform sends their profile URL via `UCP-Agent` header:
UCP-Agent: profile="https://platform.example.com/.well-known/ucp"
The business fetches the platform profile, computes the capability intersection,
and includes the negotiated capabilities in every response.
## Configuration
Configuration is stored in `ucp.config.json` at the project root.
## Files
{list generated files with descriptions}
## Testing
Run unit tests:
```bash
npm test
検証
UCP スキーマに照らして実装を検証します:
# Using the skill
/ucp validate
#### Step 3: Output
生成されたドキュメント: CREATE docs/ucp-integration.md
ドキュメントには次の内容が含まれます:
- 機能の概要
- API エンドポイントのリファレンス
- ステータスのライフサイクル図
- 設定ガイド
- ファイルマニフェスト
---
## Error Handling
### Interactive Error Resolution
When encountering ambiguous situations, always ask the user:
1. **Missing config:** "Config file not found. Run /ucp init first, or create manually?"
2. **Spec fetch failed:** "Could not clone UCP spec. Check network/auth. Retry, use local path, or abort?"
3. **Conflicting files:** "File {path} already exists. Overwrite, merge, skip, or abort?"
4. **Unknown role:** "Role '{role}' not recognized. Did you mean: business, platform, payment_provider, or host_embedded?"
5. **Validation failure:** "Schema validation failed for {file}. Show details, attempt fix, or skip?"
### Error Codes
| Code | Meaning | Resolution |
|------|---------|------------|
| `CONFIG_NOT_FOUND` | ucp.config.json missing | Run /ucp init |
| `SPEC_NOT_FOUND` | Spec repo not available | Check network, clone manually |
| `INVALID_ROLE` | Unknown role in config | Fix config |
| `SCHEMA_INVALID` | Response doesn't match schema | Review generated code |
| `EDGE_RUNTIME_DETECTED` | Edge runtime used | Change to nodejs/bun |
---
## Next.js Conventions
### File Structure
project/ ├── app/ │ ├── .well-known/ │ │ └── ucp/ │ │ └── route.ts # ディスカバリープロファイル │ └── api/ │ └── ucp/ │ ├── checkout/ │ │ ├── route.ts # POST 作成 │ │ └── [id]/ │ │ └── route.ts # GET、PATCH、POST 完了 │ ├── mcp/ │ │ └── route.ts # MCP トランスポート(有効な場合) │ └── a2a/ │ └── route.ts # A2A トランスポート(有効な場合) ├── lib/ │ └── ucp/ │ ├── types/ │ │ ├── checkout.ts │ │ └── index.ts │ ├── schemas/ │ │ └── checkout.ts │ ├── handlers/ │ │ ├── checkout.ts │ │ ├── fulfillment.ts │ │ ├── discount.ts │ │ └── payment.ts │ ├── transports/ │ │ └── mcp.ts │ ├── profile.ts │ ├── negotiation.ts │ └── response.ts ├── docs/ │ └── ucp-integration.md ├── ucp.config.json └── .ucp-spec/ # クローンした仕様(gitignore 対象)
### Runtime Declaration
Every route file must include:
```typescript
export const runtime = 'nodejs'; // Edge runtime is not supported
または、Bun の場合:
export const runtime = 'nodejs'; // Bun-compatible Node.js runtime
App Router のパターン
- API Routes(
pages/api)ではなく、Route Handlers(route.ts)を使用します next/serverのNextRequestとNextResponseを使用します- リクエストボディの解析には
request.json()を await します - ヘッダーへのアクセスには
request.headers.get()を使用します
認証情報の取り扱い
必要なキー
- AP2 Mandates: JWS 署名キー(ES256)
- Webhook 署名: JWS 署名キー(ES256)
- ID のリンク: OAuth クライアント認証情報
認証情報の確認
機能で認証情報が必要な場合は、次のように尋ねます:
「AP2 mandates には JWS 署名キーが必要です(ES256 を推奨)。 既存のキーペアをお持ちですか。それとも、生成方法をご説明しましょうか?」
ユーザーが生成手順を必要とする場合:
# Generate ES256 key pair
openssl ecparam -genkey -name prime256v1 -noout -out private.pem
openssl ec -in private.pem -pubout -out public.pem
# Convert to JWK format (use jose library or online tool)
保存
認証情報は設定ファイルではなく、環境変数に保存します:
UCP_SIGNING_KEY- 秘密鍵(PEM または JWK)UCP_OAUTH_CLIENT_ID- OAuth クライアント IDUCP_OAUTH_CLIENT_SECRET- OAuth クライアントシークレット
バージョン履歴
- 2026-01-11:UCP 仕様の初版
- Skill v1.0:スキルの初回リリース
参考資料
- UCP 仕様リポジトリ:https://github.com/Universal-Commerce-Protocol/ucp.git
- UCP ドキュメント:
./ucp/docs/または./.ucp-spec/docs/を参照してください - JSON Schema Draft 2020-12
- RFC 8941(構造化フィールド値)
- RFC 3339(日付/時刻形式)
- RFC 8785(JSON 正準化スキーム)