关于 ucp
ucp 是面向开发者的 Universal Commerce Protocol 实现技能,帮助项目按规范建立 UCP 支持,涵盖初始化、规划、代码缺口分析与脚手架生成。
主要功能
- 01初始化 UCP 配置与规范
- 02提供咨询与实施路线图
- 03生成文件级实施计划
- 04基于 AST 分析代码缺口
- 05生成 UCP 实现脚手架
适用场景
- 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[] | 以下一个或多个角色: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 身份关联 |
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是否存在 - 如果存在,询问:“配置文件已存在。要覆盖、合并还是中止?”
- 覆盖:删除并重新创建
- 合并:保留现有值作为默认值
- 中止:停止初始化
步骤 3:询问关键问题(4 个问题)
Q1:你要实现哪些角色?
- 商家(记录商家)
- 平台(消费者应用或 Agent)
- 支付凭证提供方
- 嵌入结账功能的宿主
- 多个(请注明)
如果用户选择多个角色,请警告:
“实现多个角色并不常见。通常适用于市场平台/聚合器场景。你确定吗?”
Q2:你将使用哪种运行时?
- Node.js(推荐,稳定)
- Bun(选择启用,实验性)
注意:如果用户提到 Edge,请回复:
“UCP 实现不支持 Edge 运行时。请选择 Node.js 或 Bun。”
Q3:你的业务域名是什么?
- 将托管
/.well-known/ucp的域名 - 示例:
shop.example.com
Q4:你计划在发布时使用哪些传输方式?
- REST(推荐的基线方案)
- MCP(模型上下文协议)
- A2A(Agent 到 Agent)
- 嵌入式(iframe 结账)
步骤 4:创建配置文件
创建 ./ucp.config.json,其中包含:
- 关键问题的答案
- 其他字段的合理默认值
- 将
ucp_version设置为规范中的最新版本
步骤 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) - 规范必须可用
流程
步骤 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:是否需要配送选项以及多组/多目的地支持?
- 无需配送
- 仅支持单一目的地
- 支持多目的地 → 设置
features.multi_destination_fulfillment: true
Q6:是否需要折扣、买家同意信息收集或身份关联?
- 折扣 → 将
dev.ucp.shopping.discount添加到扩展中 - 买家同意 → 将
dev.ucp.shopping.buyer_consent添加到扩展中 - 身份关联 → 添加
dev.ucp.common.identity_linking,并设置features.identity_linking: true
Q7:需要映射到 UCP 的现有结账和订单 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
用途
根据 UCP 要求深入分析现有代码库。使用 AST 解析和数据流追踪。
前置条件
- 配置文件应存在(以提供角色/能力上下文)
- 规范必须可用
流程
步骤 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 控制面板 → 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 - 已记录环境变量
- 已更新
next.config.js以支持 MCP 流式传输 - 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 模式
- 使用 Route Handlers(
route.ts),不要使用 API Routes(pages/api) - 使用
next/server中的NextRequest和NextResponse - 使用
await request.json()解析请求正文 - 使用
request.headers.get()访问请求头
凭据处理
所需密钥
- **AP2 Mandates:**JWS 签名密钥(ES256)
- **Webhook 签名:**JWS 签名密钥(ES256)
- **身份关联:**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:Skill 初始版本
参考资料
- 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 规范化方案)