ucp
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseUCP Skill — Universal Commerce Protocol Implementation
UCP Skill — 通用商务协议实现工具
Core Principles
核心原则
- Edge runtime is NOT USED — Only Node.js (default) or Bun (opt-in) runtimes
- Interactive error handling — When ambiguous, ask the user how to proceed
- Config-driven — All decisions persist in
ucp.config.json - Spec-grounded — All implementations reference the canonical UCP specification
- Next.js conventions — Follow App Router patterns for code organization
- Deep analysis — Use AST parsing and data flow tracing for gap detection
- 不使用Edge runtime — 仅支持Node.js(默认)或Bun(可选)运行时
- 交互式错误处理 — 遇到歧义场景时询问用户后续处理方式
- 配置驱动 — 所有决策都持久化存储在中
ucp.config.json - 基于规范实现 — 所有实现都参考官方UCP规范
- 遵循Next.js约定 — 代码组织遵循App Router模式
- 深度分析 — 使用AST解析和数据流追踪检测实现缺口
Spec Repository Handling
规范仓库处理
Location Priority
查找优先级
Check in this order:
- — User's local copy (use as-is)
./ucp/ - — Previously cloned spec (update it)
./.ucp-spec/ - Neither exists — Clone fresh
按以下顺序检查:
- — 用户本地副本(直接使用)
./ucp/ - — 之前克隆的规范(更新即可)
./.ucp-spec/ - 两者都不存在 — 全新克隆
Clone Procedure
克隆流程
When cloning is needed:
bash
git clone --depth 1 https://github.com/Universal-Commerce-Protocol/ucp.git .ucp-specIf HTTPS fails, try SSH:
bash
git clone --depth 1 git@github.com:Universal-Commerce-Protocol/ucp.git .ucp-spec需要克隆时执行:
bash
git clone --depth 1 https://github.com/Universal-Commerce-Protocol/ucp.git .ucp-spec如果HTTPS克隆失败,尝试SSH:
bash
git clone --depth 1 git@github.com:Universal-Commerce-Protocol/ucp.git .ucp-specUpdate Procedure
更新流程
When exists:
./.ucp-spec/bash
cd .ucp-spec && git pull && cd ..当存在时执行:
./.ucp-spec/bash
cd .ucp-spec && git pull && cd ..Gitignore Management
Gitignore管理
After cloning, ensure is in :
.ucp-spec/.gitignore- Read if it exists
.gitignore - Check if or
.ucp-spec/is already listed.ucp-spec - If not, append on a new line
.ucp-spec/
克隆完成后,确保已添加到:
.ucp-spec/.gitignore- 如果存在则读取内容
.gitignore - 检查或
.ucp-spec/是否已经在列表中.ucp-spec - 如果不存在,在新行追加
.ucp-spec/
Spec File Locations (read on demand)
规范文件位置(按需读取)
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.jsondocs/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.jsonConfiguration File
配置文件
Location
位置
./ucp.config.json项目根目录下的
./ucp.config.jsonSchema
Schema
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
}
}
}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
}
}
}Field Descriptions
字段说明
| Field | Type | Description |
|---|---|---|
| string | UCP spec version (date-based) |
| string[] | One or more of: |
| string | |
| string[] | Required capabilities to implement |
| string[] | Optional extensions to implement |
| string[] | Enabled transports: |
| string[] | Order to implement transports |
| string[] | Payment handler IDs to support |
| boolean | Enable AP2 mandate signing |
| boolean | Enable OAuth identity linking |
| boolean | Enable multi-destination shipping |
| string | Business domain for |
| object | Map of existing API endpoints to analyze |
| object | URLs for privacy, terms, refunds, shipping policies |
| string | |
| string[] | Files created by scaffold (for tracking) |
| object | Raw answers to qualifying questions |
| 字段 | 类型 | 说明 |
|---|---|---|
| string | UCP规范版本(基于日期命名) |
| string[] | 可选项: |
| string | |
| string[] | 需要实现的核心能力 |
| string[] | 需要实现的可选扩展能力 |
| string[] | 启用的传输协议: |
| string[] | 传输协议的实现顺序 |
| string[] | 需要支持的支付处理程序ID |
| boolean | 启用AP2 mandate签名 |
| boolean | 启用OAuth身份关联 |
| boolean | 启用多地址配送 |
| string | 用于 |
| object | 待分析的现有API端点映射 |
| object | 隐私政策、服务条款、退款政策、配送政策的URL |
| string | |
| string[] | 脚手架生成的文件(用于追踪) |
| object | 资格评估问题的原始回答 |
Sub-command: (no argument)
子命令:(无参数)
Trigger
触发方式
User runs with no sub-command
/ucp用户执行不带子命令的
/ucpBehavior
执行逻辑
Display help listing all available sub-commands:
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/显示所有可用子命令的帮助列表:
UCP Skill — 通用商务协议实现工具
可用命令:
/ucp init — 在当前项目中初始化UCP(克隆规范、创建配置)
/ucp consult — 全流程咨询:回答资格评估问题,生成实现路线图
/ucp plan — 生成详细的实现计划
/ucp gaps — 对照UCP要求分析现有代码的缺口
/ucp scaffold — 生成完整可运行的UCP实现代码
/ucp validate — 对照UCP schema验证实现是否合规
/ucp profile — 生成`/.well-known/ucp`发现配置文件
/ucp test — 为UCP处理程序生成单元测试
/ucp docs — 生成内部文档
典型工作流:
/ucp init → /ucp consult → /ucp plan → /ucp scaffold → /ucp profile → /ucp test → /ucp validate
配置文件:./ucp.config.json
规范位置:./ucp/ 或 ./.ucp-spec/Sub-command: init
子命令:init
Trigger
触发方式
User runs
/ucp init用户执行
/ucp initPurpose
用途
Bootstrap UCP in a project: clone spec, create config, ask essential questions.
在项目中初始化UCP:克隆规范、创建配置、询问基础问题
Procedure
执行流程
Step 1: Check/Clone Spec Repository
步骤1:检查/克隆规范仓库
- Check if exists
./ucp/- If yes: "Found local UCP spec at ./ucp/"
- If not, check if exists
./.ucp-spec/- If yes: Run to update
git pull - If no: Clone the repo (see Spec Repository Handling)
- If yes: Run
- After cloning, add to
.ucp-spec/.gitignore
- 检查是否存在
./ucp/- 存在则提示:"Found local UCP spec at ./ucp/"
- 如果不存在,检查是否存在
./.ucp-spec/- 存在则执行更新
git pull - 不存在则克隆仓库(参考规范仓库处理部分)
- 存在则执行
- 克隆完成后,将添加到
.ucp-spec/.gitignore
Step 2: Check for Existing Config
步骤2:检查现有配置
- Check if exists
./ucp.config.json - If yes, ask: "Config file exists. Overwrite, merge, or abort?"
- Overwrite: Delete and create fresh
- Merge: Keep existing values as defaults
- Abort: Stop init
- 检查是否存在
./ucp.config.json - 如果存在,询问:"配置文件已存在,要覆盖、合并还是终止操作?"
- 覆盖:删除现有文件并创建全新配置
- 合并:保留现有值作为默认值
- 终止:停止初始化流程
Step 3: Ask Essential Questions (4 questions)
步骤3:询问基础问题(共4个)
Q1: What role(s) are you implementing?
- Business (merchant of record)
- Platform (consumer app or agent)
- Payment credential provider
- Host embedding checkout
- Multiple (specify)
If user selects multiple roles, WARN:
"Implementing multiple roles is unusual. This is typically for marketplace/aggregator scenarios. Are you sure?"
Q2: What runtime will you use?
- Node.js (recommended, stable)
- Bun (opt-in, experimental)
NOTE: If user mentions Edge, respond:
"Edge runtime is not supported for UCP implementations. Please choose Node.js or Bun."
Q3: What is your business domain?
- The domain that will host
/.well-known/ucp - Example:
shop.example.com
Q4: Which transports do you need at launch?
- REST (recommended baseline)
- MCP (Model Context Protocol)
- A2A (Agent-to-Agent)
- Embedded (iframe checkout)
问题1:你要实现的角色是什么?
- 商家(记录交易主体)
- 平台(消费端应用或Agent)
- 支付凭证提供方
- 宿主嵌入结账页
- 多个角色(请说明)
如果用户选择多个角色,给出警告:
"实现多个角色属于非常规场景,通常仅适用于市场/聚合商场景,是否确认选择?"
问题2:你要使用什么运行时?
- Node.js(推荐,稳定)
- Bun(可选,实验性)
注意:如果用户提到Edge,回复:
"UCP实现不支持Edge runtime,请选择Node.js或Bun。"
问题3:你的业务域名是什么?
- 用于托管的域名
/.well-known/ucp - 示例:
shop.example.com
问题4:上线时需要哪些传输协议?
- REST(推荐的基础协议)
- MCP(Model Context Protocol)
- A2A(Agent-to-Agent)
- Embedded(iframe结账)
Step 4: Create Config File
步骤4:创建配置文件
Create with:
./ucp.config.json- Answers from essential questions
- Sensible defaults for other fields
- set to latest from spec
ucp_version
创建,包含:
./ucp.config.json- 基础问题的回答
- 其他字段的合理默认值
- 设置为规范中的最新版本
ucp_version
Step 5: Output Ready Message
步骤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 firstUCP初始化成功!
配置文件:./ucp.config.json
规范位置:./.ucp-spec/(或./ucp/)
角色:{role}
域名:{domain}
下一步操作:
/ucp consult — 完成全流程咨询(推荐)
/ucp plan — 直接生成实现计划
/ucp gaps — 先分析现有代码缺口Sub-command: consult
子命令:consult
Trigger
触发方式
User runs
/ucp consult用户执行
/ucp consultPurpose
用途
Walk through all 12 qualifying questions, update config, produce implementation roadmap.
引导用户回答全部12个资格评估问题,更新配置,生成实现路线图
Prerequisites
前置条件
- Config file must exist (run first)
/ucp init - Spec must be available
- 配置文件必须存在(先执行)
/ucp init - 规范必须可用
Procedure
执行流程
Step 1: Load Existing Config
步骤1:加载现有配置
Read and use existing answers as defaults.
./ucp.config.json读取,使用现有回答作为默认值
./ucp.config.jsonStep 2: Walk Through 12 Qualifying Questions
步骤2:引导回答12个资格评估问题
Ask each question. If already answered in config, show current value and ask to confirm or change.
Q1: Are we implementing the business side, the platform side, or both?
- Map to in config
roles - If both/multiple, warn about unusual scenario
Q2: Which UCP version and which capabilities/extensions are in scope?
- Read available versions from spec
- Present capability options:
- Core: (required)
dev.ucp.shopping.checkout - Extensions:
dev.ucp.shopping.fulfillmentdev.ucp.shopping.discountdev.ucp.shopping.buyer_consentdev.ucp.shopping.ap2_mandatedev.ucp.shopping.orderdev.ucp.common.identity_linking
- Core:
Q3: Which payment handlers do we need?
- Wallets (Apple Pay, Google Pay)
- PSP tokenization (Stripe, Adyen, etc.)
- Custom handler
- None yet (decide later)
Q4: Do we need AP2 mandates and signing key infrastructure?
- Yes → set
features.ap2_mandates: true - No → set
features.ap2_mandates: false - If yes, explain: "You'll need to provide JWS signing keys (ES256 recommended)"
Q5: Do we need fulfillment options and multi-group/multi-destination support?
- No fulfillment needed
- Single destination only
- Multi-destination support → set
features.multi_destination_fulfillment: true
Q6: Do we need discounts, buyer consent capture, or identity linking?
- Discounts → add to extensions
dev.ucp.shopping.discount - Buyer consent → add to extensions
dev.ucp.shopping.buyer_consent - Identity linking → add , set
dev.ucp.common.identity_linkingfeatures.identity_linking: true
Q7: What are the existing checkout and order APIs we should map to UCP?
- Ask for existing endpoint paths
- Store in object
existing_apis - Examples: ,
/api/checkout,/api/cart/api/orders
Q8: What are the required policy URLs?
- Privacy policy URL
- Terms of service URL
- Refund policy URL
- Shipping policy URL
- Store in object
policy_urls
Q9: What authentication model is required for checkout endpoints?
- None (anonymous checkout)
- API key
- OAuth 2.0
- Session-based
- Store in
answers.authentication_model
Q10: Who will receive order webhooks and what event cadence is required?
- Webhook URL for order events
- Event types needed: ,
order.created,order.updated,order.fulfilledorder.canceled - Store in
answers.webhook_config
Q11: Do we need to support MCP, A2A, or embedded checkout at launch?
- Confirm/update array
transports - Set order
transport_priority
Q12: What is the business domain that will host /.well-known/ucp?
- Confirm/update field
domain
逐个询问问题,如果配置中已有答案,显示当前值并询问用户是否确认或修改。
问题1:我们要实现业务端、平台端,还是两者都实现?
- 映射到配置中的字段
roles - 如果选择两者/多个角色,警告非常规场景
问题2:要使用哪个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
- 核心:
问题3:我们需要哪些支付处理程序?
- 电子钱包(Apple Pay、Google Pay)
- PSP令牌化(Stripe、Adyen等)
- 自定义处理程序
- 暂未确定(后续再选)
问题4:我们是否需要AP2 mandates和签名密钥基础设施?
- 是 → 设置
features.ap2_mandates: true - 否 → 设置
features.ap2_mandates: false - 如果选择是,说明:"你需要提供JWS签名密钥(推荐ES256算法)"
问题5:我们是否需要配送选项和多分组/多地址支持?
- 不需要配送
- 仅支持单地址
- 多地址支持 → 设置
features.multi_destination_fulfillment: true
问题6:我们是否需要折扣、买家同意采集或身份关联?
- 折扣 → 将添加到扩展能力
dev.ucp.shopping.discount - 买家同意 → 将添加到扩展能力
dev.ucp.shopping.buyer_consent - 身份关联 → 添加,设置
dev.ucp.common.identity_linkingfeatures.identity_linking: true
问题7:有哪些现有的结账和订单API需要映射到UCP?
- 询问现有端点路径
- 存储到对象
existing_apis - 示例:、
/api/checkout、/api/cart/api/orders
问题8:必填的政策URL有哪些?
- 隐私政策URL
- 服务条款URL
- 退款政策URL
- 配送政策URL
- 存储到对象
policy_urls
问题9:结账端点需要什么认证模式?
- 无(匿名结账)
- API密钥
- OAuth 2.0
- 基于会话
- 存储到
answers.authentication_model
问题10:谁会接收订单webhook,需要什么事件推送频率?
- 订单事件的webhook URL
- 需要的事件类型:、
order.created、order.updated、order.fulfilledorder.canceled - 存储到
answers.webhook_config
问题11:上线时是否需要支持MCP、A2A或嵌入式结账?
- 确认/更新数组
transports - 设置顺序
transport_priority
问题12:托管的业务域名是什么?
/.well-known/ucp- 确认/更新字段
domain
Step 3: Update Config
步骤3:更新配置
Write all answers to
./ucp.config.json将所有回答写入
./ucp.config.jsonStep 4: Generate Implementation Roadmap
步骤4:生成实现路线图
Based on answers, produce a roadmap:
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.基于回答生成路线图:
UCP实现路线图
==========================
角色:商家(商户)
版本:2026-01-11
域名:shop.example.com
待实现能力:
✓ dev.ucp.shopping.checkout(核心)
✓ dev.ucp.shopping.fulfillment
✓ dev.ucp.shopping.discount
○ dev.ucp.shopping.order
传输协议(按优先级):
1. REST
2. MCP
支付处理程序:
- Stripe令牌化
核心实现任务:
1. 创建`/.well-known/ucp`发现配置
2. 实现结账会话端点(创建、查询、更新、完成)
3. 实现配送选项逻辑
4. 实现折扣码应用逻辑
5. 搭建支付处理程序集成
6. 实现订单webhook
7. 添加MCP传输层
预计创建/修改文件数:~15-20
执行/ucp plan获取逐文件的详细计划。Sub-command: plan
子命令:plan
Trigger
触发方式
User runs
/ucp plan用户执行
/ucp planPurpose
用途
Generate detailed implementation plan with specific files and order of operations.
生成详细的实现计划,包含具体文件和操作顺序
Prerequisites
前置条件
- Config file must exist with completed consultation
- Spec must be available
- 配置文件必须存在且已完成咨询流程
- 规范必须可用
Procedure
执行流程
Step 1: Load Config and Spec
步骤1:加载配置和规范
- Read
./ucp.config.json - Read relevant spec files based on capabilities/transports
- 读取
./ucp.config.json - 基于能力/传输协议读取相关规范文件
Step 2: Analyze Existing Codebase Structure
步骤2:分析现有代码库结构
- Detect Next.js version (App Router vs Pages Router)
- Find existing API routes
- Find existing lib/utils structure
- Find existing types/schemas
- Identify package manager (npm, yarn, pnpm, bun)
- 检测Next.js版本(App Router vs Pages Router)
- 查找现有API路由
- 查找现有lib/utils结构
- 查找现有类型/schema
- 识别包管理器(npm、yarn、pnpm、bun)
Step 3: Generate File Plan
步骤3:生成文件计划
For each capability/transport, list files to create/modify.
Example output:
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.为每个能力/传输协议列出需要创建/修改的文件。
示例输出:
UCP实现计划
=======================
阶段1:核心类型和Schema
-------------------------------
创建 lib/ucp/types/checkout.ts
- CheckoutSession接口
- LineItem、Totals、Payment类型
- Status枚举
创建 lib/ucp/types/index.ts
- 重新导出所有类型
创建 lib/ucp/schemas/checkout.ts
- 用于验证的Zod schema
阶段2:发现配置
--------------------------
创建 app/.well-known/ucp/route.ts
- 返回配置JSON的GET处理程序
- 从配置读取能力
创建 lib/ucp/profile.ts
- 配置生成逻辑
阶段3:结账端点(REST)
----------------------------------
创建 app/api/ucp/checkout/route.ts
- POST:创建结账会话
- 能力协商逻辑
创建 app/api/ucp/checkout/[id]/route.ts
- GET:查询结账信息
- PATCH:更新结账信息
- POST:完成结账(action=complete)
创建 lib/ucp/handlers/checkout.ts
- 结账操作的业务逻辑
- 状态机实现
阶段4:配送扩展
------------------------------
创建 lib/ucp/handlers/fulfillment.ts
- 配送选项计算
- 地址验证
修改 lib/ucp/handlers/checkout.ts
- 将配送逻辑集成到结账响应
阶段5:折扣扩展
---------------------------
创建 lib/ucp/handlers/discount.ts
- 折扣码验证
- 适用折扣计算
修改 lib/ucp/handlers/checkout.ts
- 将折扣逻辑集成到结账流程
阶段6:支付集成
----------------------------
创建 lib/ucp/handlers/payment.ts
- 支付处理程序注册表
- payment_data处理逻辑
创建 lib/ucp/handlers/stripe.ts
- Stripe专属令牌化逻辑
阶段7:订单Webhook
-----------------------
创建 lib/ucp/handlers/order.ts
- 订单事件触发
- Webhook签名(JWS)
创建 lib/ucp/webhooks/sender.ts
- 带重试机制的Webhook投递
阶段8:MCP传输(如果启用)
-----------------------------------
创建 lib/ucp/transports/mcp.ts
- MCP工具定义
- JSON-RPC处理程序
待安装依赖:
------------------------
zod — Schema验证
jose — JWS签名(如果启用AP2/webhook)
执行/ucp scaffold生成这些文件。Step 4: Save Plan to Config
步骤4:保存计划到配置
Store the plan in for scaffold reference.
answers.implementation_plan将计划存储到供脚手架参考
answers.implementation_planSub-command: gaps
子命令:gaps
Trigger
触发方式
User runs
/ucp gaps用户执行
/ucp gapsPurpose
用途
Deep analysis of existing codebase against UCP requirements. Uses AST parsing and data flow tracing.
对照UCP要求深度分析现有代码库,使用AST解析和数据流追踪能力
Prerequisites
前置条件
- Config file should exist (for role/capability context)
- Spec must be available
- 配置文件应存在(提供角色/能力上下文)
- 规范必须可用
Procedure
执行流程
Step 1: Load Context
步骤1:加载上下文
- Read config for declared capabilities
- Read relevant spec files
- 读取配置中声明的能力
- 读取相关规范文件
Step 2: Discover Existing Code
步骤2:发现现有代码
Scan for:
- API routes (,
app/api/**)pages/api/** - Checkout-related files (search for "checkout", "cart", "order")
- Payment handling code
- Webhook implementations
扫描以下内容:
- API路由(、
app/api/**)pages/api/** - 结账相关文件(搜索"checkout"、"cart"、"order")
- 支付处理代码
- Webhook实现
Step 3: Deep Analysis (AST-based)
步骤3:深度分析(基于AST)
For each relevant file:
- Parse AST
- Trace data flow for checkout objects
- Identify existing patterns
Analyze against UCP requirements:
| Requirement | Status | Finding |
|---|---|---|
| Discovery profile at /.well-known/ucp | MISSING | No route found |
| Checkout session creation | PARTIAL | Found /api/checkout but missing UCP fields |
| Status lifecycle | MISSING | No status state machine |
| Capability negotiation | MISSING | No UCP-Agent header handling |
| Payment handler support | PARTIAL | Stripe exists but not UCP-compliant |
| Response metadata (ucp object) | MISSING | Responses don't include ucp field |
对每个相关文件:
- 解析AST
- 追踪结账对象的数据流
- 识别现有模式
对照UCP要求分析:
| 要求 | 状态 | 发现 |
|---|---|---|
| 缺失 | 未找到对应路由 |
| 结账会话创建 | 部分实现 | 找到 |
| 状态生命周期 | 缺失 | 无状态机实现 |
| 能力协商 | 缺失 | 无UCP-Agent header处理逻辑 |
| 支付处理程序支持 | 部分实现 | 已集成Stripe但不符合UCP规范 |
| 响应元数据(ucp对象) | 缺失 | 响应未包含ucp字段 |
Step 4: Generate Gap Report
步骤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 compliantUCP缺口分析报告
=======================
现有代码库:Next.js 14(App Router)
目标角色:商家
目标能力:结账、配送、折扣
严重缺口(必须修复)
------------------------
[GAP-001] 缺失发现配置
- 要求:`/.well-known/ucp`端点
- 状态:未找到
- 修复方案:创建app/.well-known/ucp/route.ts
[GAP-002] 缺失UCP响应封装
- 要求:所有响应必须包含带版本/能力的`ucp`对象
- 发现:app/api/checkout/route.ts返回原始结账数据
- 修复方案:用UCP元数据封装响应
[GAP-003] 缺失能力协商
- 要求:读取UCP-Agent header,计算能力交集
- 发现:结账路由中无header处理逻辑
- 修复方案:添加中间件或处理逻辑
部分实现
-----------------------
[PARTIAL-001] 结账会话已存在但不符合规范
- 文件:app/api/checkout/route.ts
- 缺失:id、status、currency、totals.grand_total、links、payment字段
- 存在:line_items(需要调整schema)
[PARTIAL-002] 支付集成已存在
- 文件:lib/stripe.ts
- 问题:直接调用Stripe API,未遵循UCP payment_data流程
- 修复方案:用UCP支付处理程序抽象层封装
合规区域
---------------
[OK] 现有结账中已配置政策URL
[OK] 已强制使用HTTPS
[OK] POST处理程序支持幂等键
建议
---------------
1. 先执行/ucp scaffold生成合规结构
2. 将现有结账逻辑迁移到新的处理程序中
3. 迁移完成后执行/ucp validate验证
总计:3个严重缺口,2个部分实现,3个合规区域Sub-command: scaffold
子命令:scaffold
Trigger
触发方式
User runs
/ucp scaffold用户执行
/ucp scaffoldPurpose
用途
Generate full working UCP implementation based on config and plan.
基于配置和计划生成完整可运行的UCP实现
Prerequisites
前置条件
- Config file must exist
- Plan should exist (run first, or scaffold will generate one)
/ucp plan
- 配置文件必须存在
- 计划应存在(先执行,否则脚手架会自动生成)
/ucp plan
Procedure
执行流程
Step 1: Confirm Scaffold Depth
步骤1:确认脚手架生成级别
Ask user:
"What level of code generation do you want?"
- types: TypeScript interfaces and Zod schemas only
- scaffolding: Structure with TODO markers for business logic
- full: Complete working implementation (recommended)
Store choice in
config.scaffold_depth询问用户:
"你需要什么级别的代码生成?"
- types:仅生成TypeScript接口和Zod schema
- scaffolding:生成带TODO标记的业务逻辑结构
- full:生成完整可运行的实现(推荐)
将选择存储到
config.scaffold_depthStep 2: Check Dependencies
步骤2:检查依赖
Identify required packages based on config:
- — Always needed
zod - — If AP2 mandates or webhook signing enabled
jose - — For session ID generation
uuid
Ask before installing:
"The following packages are required: zod, jose, uuid" "Install now? (npm install / bun add)"
If yes, run appropriate install command.
基于配置识别所需包:
- — 始终需要
zod - — 如果启用AP2 mandates或webhook签名
jose - — 用于会话ID生成
uuid
安装前询问:
"需要安装以下依赖:zod、jose、uuid" "是否立即安装?(npm install / bun add)"
如果同意,执行对应的安装命令
Step 3: Generate Code
步骤3:生成代码
Generate files according to plan. For each file:
- Create parent directories if needed
- Write file content
- Track in
config.generated_files
按照计划生成文件,对每个文件:
- 不存在父目录则创建
- 写入文件内容
- 在中追踪
config.generated_files
Code Generation Templates
代码生成模板
lib/ucp/types/checkout.ts
lib/ucp/types/checkout.ts
typescript
/**
* 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...typescript
/**
* 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
lib/ucp/schemas/checkout.ts
typescript
/**
* 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>;typescript
/**
* 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
app/.well-known/ucp/route.ts
typescript
/**
* 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',
},
});
}typescript
/**
* 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
lib/ucp/profile.ts
typescript
/**
* 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 };
}typescript
/**
* 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
app/api/ucp/checkout/route.ts
typescript
/**
* 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');
}
}typescript
/**
* 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
lib/ucp/handlers/checkout.ts
typescript
/**
* 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
}typescript
/**
* 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
lib/ucp/negotiation.ts
typescript
/**
* 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,
};
}
}typescript
/**
* 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
lib/ucp/response.ts
typescript
/**
* 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 }
);
}typescript
/**
* 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 Transport Code Templates (using mcp-handler)
MCP传输代码模板(使用mcp-handler)
When MCP transport is enabled in config, generate these additional files.
当配置中启用MCP传输时,生成这些额外文件。
Dependencies for MCP Transport
MCP传输依赖
bash
npm install mcp-handler @modelcontextprotocol/sdk@1.25.2 zodIMPORTANT: Use or later — earlier versions have security vulnerabilities.
@modelcontextprotocol/sdk@1.25.2bash
npm install mcp-handler @modelcontextprotocol/sdk@1.25.2 zod重要提示: 请使用或更高版本 — 早期版本存在安全漏洞。
@modelcontextprotocol/sdk@1.25.2app/api/mcp/[transport]/route.ts
app/api/mcp/[transport]/route.ts
typescript
/**
* 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 };typescript
/**
* 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
lib/ucp/transports/mcp-tools.ts
typescript
/**
* 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;typescript
/**
* 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 Deployment
Vercel部署
Deployment Configuration
部署配置
vercel.json
vercel.json
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"
}
]
}
]
}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"
}
]
}
]
}Environment Variables
环境变量
Set these in Vercel Dashboard → Settings → Environment Variables:
| Variable | Required | Description |
|---|---|---|
| Yes | Production domain (e.g., |
| If AP2 | JWS signing key (PEM or JWK) |
| If Stripe | Stripe API secret key |
在Vercel控制台 → 设置 → 环境变量中设置:
| 变量 | 必填 | 说明 |
|---|---|---|
| 是 | 生产域名(例如: |
| 启用AP2时必填 | JWS签名密钥(PEM或JWK格式) |
| 集成Stripe时必填 | Stripe API密钥 |
next.config.js (MCP-optimized)
next.config.js(MCP优化版)
javascript
/** @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;javascript
/** @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 Client Configuration
MCP客户端配置
For Claude Desktop / Cursor / Windsurf
适用于Claude Desktop / Cursor / Windsurf
Option 1: Direct HTTP (if client supports streamable HTTP)
Add to MCP client config:
json
{
"mcpServers": {
"ucp-shopping": {
"url": "https://your-domain.vercel.app/api/mcp"
}
}
}Option 2: Via mcp-remote bridge (for stdio-only clients)
json
{
"mcpServers": {
"ucp-shopping": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://your-domain.vercel.app/api/mcp"]
}
}
}选项1:直接HTTP(如果客户端支持流式HTTP)
添加到MCP客户端配置:
json
{
"mcpServers": {
"ucp-shopping": {
"url": "https://your-domain.vercel.app/api/mcp"
}
}
}选项2:通过mcp-remote桥接(适用于仅支持stdio的客户端)
json
{
"mcpServers": {
"ucp-shopping": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://your-domain.vercel.app/api/mcp"]
}
}
}Testing MCP Deployment
测试MCP部署
scripts/test-mcp.mjs
scripts/test-mcp.mjs
javascript
#!/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);javascript
#!/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 Deployment Checklist
Vercel部署检查清单
When running with Vercel deployment, verify:
/ucp scaffold- created with function timeouts
vercel.json - Environment variables documented
- updated for MCP streaming
next.config.js - MCP route at
app/api/mcp/[transport]/route.ts - Test script at
scripts/test-mcp.mjs - updated with required variables
.env.example
使用Vercel部署执行时,验证以下内容:
/ucp scaffold- 已创建带函数超时配置的
vercel.json - 已记录环境变量
- 已更新支持MCP流式传输
next.config.js - MCP路由位于
app/api/mcp/[transport]/route.ts - 测试脚本位于
scripts/test-mcp.mjs - 已更新添加必填变量
.env.example
Post-Deployment Verification
部署后验证
After deploying to Vercel:
-
Check discovery profile:bash
curl https://your-domain.vercel.app/.well-known/ucp | jq . -
Test MCP endpoint:bash
node scripts/test-mcp.mjs https://your-domain.vercel.app -
Configure MCP client: Add the server to Claude Desktop, Cursor, or your preferred MCP client.
-
Verify in client: Ask the AI to "list available UCP tools" — it should show the checkout tools.
部署到Vercel后:
-
检查发现配置:bash
curl https://your-domain.vercel.app/.well-known/ucp | jq . -
测试MCP端点:bash
node scripts/test-mcp.mjs https://your-domain.vercel.app -
配置MCP客户端: 将服务器添加到Claude Desktop、Cursor或你偏好的MCP客户端。
-
在客户端验证: 让AI"列出可用的UCP工具" — 应显示结账相关工具。
Post-Generation Steps
生成后步骤
Step 4: Update Config
步骤4:更新配置
After generation, update :
ucp.config.json- Add all created files to array
generated_files - Update to reflect what was generated
scaffold_depth
生成完成后,更新:
ucp.config.json- 将所有创建的文件添加到数组
generated_files - 更新反映生成的级别
scaffold_depth
Step 5: Output Summary
步骤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 URLUCP脚手架生成完成
=====================
生成的文件:
创建 lib/ucp/types/checkout.ts
创建 lib/ucp/types/index.ts
创建 lib/ucp/schemas/checkout.ts
创建 lib/ucp/profile.ts
创建 lib/ucp/negotiation.ts
创建 lib/ucp/response.ts
创建 lib/ucp/handlers/checkout.ts
创建 app/.well-known/ucp/route.ts
创建 app/api/ucp/checkout/route.ts
创建 app/api/ucp/checkout/[id]/route.ts
MCP传输(如果启用):
创建 app/api/mcp/[transport]/route.ts
创建 lib/ucp/transports/mcp-tools.ts
Vercel部署:
创建 vercel.json
创建 scripts/test-mcp.mjs
修改 next.config.js
已安装依赖:
zod, jose, uuid
mcp-handler, @modelcontextprotocol/sdk(如果启用MCP)
下一步操作:
1. 审查生成的代码,自定义业务逻辑
2. 在Vercel控制台设置环境变量
3. 部署:vercel --prod
4. 执行/ucp profile验证发现配置
5. 执行/ucp test生成单元测试
6. 执行/ucp validate检查合规性
7. 用部署URL配置MCP客户端Sub-command: validate
子命令:validate
Trigger
触发方式
User runs
/ucp validate用户执行
/ucp validatePurpose
用途
Validate the implementation against UCP JSON schemas.
对照UCP JSON schema验证实现是否合规
Prerequisites
前置条件
- Implementation must exist (run first)
/ucp scaffold - Spec must be available
- 实现必须存在(先执行)
/ucp scaffold - 规范必须可用
Procedure
执行流程
Step 1: Load Schemas
步骤1:加载Schema
Read JSON schemas from spec:
spec/schemas/shopping/checkout.jsonspec/schemas/shopping/fulfillment.jsonspec/schemas/shopping/discount.jsonspec/discovery/profile_schema.json
从规范中读取JSON schema:
spec/schemas/shopping/checkout.jsonspec/schemas/shopping/fulfillment.jsonspec/schemas/shopping/discount.jsonspec/discovery/profile_schema.json
Step 2: Validate Discovery Profile
步骤2:验证发现配置
- Make request to route (or read file directly)
/.well-known/ucp - Validate against
profile_schema.json
- 请求路由(或直接读取文件)
/.well-known/ucp - 对照验证
profile_schema.json
Step 3: Validate Response Shapes
步骤3:验证响应结构
For each implemented endpoint:
- Generate sample request
- Execute handler (mock mode)
- Validate response against schema
对每个实现的端点:
- 生成示例请求
- 执行处理程序(模拟模式)
- 对照schema验证响应
Step 4: Check Protocol Requirements
步骤4:检查协议要求
Verify:
- All responses include object
ucp - Status values are valid enum values
- Amounts are integers (minor units)
- Dates are RFC 3339 format
- Links are absolute HTTPS URLs
- Capability names follow reverse-DNS format
验证以下内容:
- 所有响应都包含对象
ucp - 状态值属于有效枚举值
- 金额为整数(最小单位)
- 日期为RFC 3339格式
- 链接为绝对HTTPS URL
- 能力名称遵循反向DNS格式
Step 5: Output Report
步骤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)UCP验证报告
=====================
发现配置:通过
✓ Schema有效
✓ 存在必填字段
✓ 服务定义有效
✓ 能力规范可访问
结账端点:
POST /api/ucp/checkout
✓ 响应Schema有效
✓ 存在UCP元数据
✓ 状态枚举有效
GET /api/ucp/checkout/[id]
✓ 响应Schema有效
✓ 幂等
PATCH /api/ucp/checkout/[id]
✓ 响应Schema有效
✓ 部分更新正常
POST /api/ucp/checkout/[id](完成)
✓ 响应Schema有效
✓ 状态转换正确
协议要求:
✓ 金额使用最小单位
✓ 日期为RFC 3339格式
✓ 使用HTTPS链接
✓ 能力名称为反向DNS格式
整体:通过(24/24检查项)Sub-command: profile
子命令:profile
Trigger
触发方式
User runs
/ucp profile用户执行
/ucp profilePurpose
用途
Generate and display the discovery profile JSON.
/.well-known/ucp生成并展示发现配置JSON
/.well-known/ucpPrerequisites
前置条件
- Config must exist
- 配置必须存在
Procedure
执行流程
Step 1: Generate Profile
步骤1:生成配置
Use the function from (or generate inline if not scaffolded yet).
generateProfile()lib/ucp/profile.ts使用中的函数(如果尚未生成脚手架则内联生成)
lib/ucp/profile.tsgenerateProfile()Step 2: Display Profile
步骤2:展示配置
Output the formatted JSON:
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"
}
]
}
}输出格式化的JSON:
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"
}
]
}
}Step 3: Offer to Write File
步骤3:提供写入文件选项
Ask: "Write this to for static serving, or keep as dynamic route?"
public/.well-known/ucpIf static:
- Create (no extension, JSON content)
public/.well-known/ucp - Note: May need Next.js config for extensionless files
询问:"是否将此内容写入用于静态托管,还是保留为动态路由?"
public/.well-known/ucp如果选择静态:
- 创建(无后缀,JSON内容)
public/.well-known/ucp - 注意:可能需要修改Next.js配置支持无后缀文件
Sub-command: test
子命令:test
Trigger
触发方式
User runs
/ucp test用户执行
/ucp testPurpose
用途
Generate unit tests for UCP handlers.
为UCP处理程序生成单元测试
Prerequisites
前置条件
- Implementation must exist
- Test framework detected (Jest, Vitest, etc.)
- 实现必须存在
- 已检测到测试框架(Jest、Vitest等)
Procedure
执行流程
Step 1: Detect Test Framework
步骤1:检测测试框架
Look for:
- /
jest.config.js→ Jestjest.config.ts - /
vitest.config.js→ Vitestvitest.config.ts - test script hints
package.json
查找以下内容:
- /
jest.config.js→ Jestjest.config.ts - /
vitest.config.js→ Vitestvitest.config.ts - 中的test脚本提示
package.json
Step 2: Generate Test Files
步骤2:生成测试文件
For each handler, generate corresponding test file.
为每个处理程序生成对应的测试文件
Example: lib/ucp/handlers/tests/checkout.test.ts
示例:lib/ucp/handlers/tests/checkout.test.ts
typescript
/**
* 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: 'test@example.com' } },
['dev.ucp.shopping.checkout']
);
expect(updated?.buyer?.email).toBe('test@example.com');
});
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: 'test@example.com' } },
['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: 'test@example.com' } },
['dev.ucp.shopping.checkout']
);
expect(updated?.status).toBe('ready_for_complete');
});
});
});typescript
/**
* 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: 'test@example.com' } },
['dev.ucp.shopping.checkout']
);
expect(updated?.buyer?.email).toBe('test@example.com');
});
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: 'test@example.com' } },
['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: 'test@example.com' } },
['dev.ucp.shopping.checkout']
);
expect(updated?.status).toBe('ready_for_complete');
});
});
});Step 3: Output Summary
步骤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)生成的测试文件:
创建 lib/ucp/handlers/__tests__/checkout.test.ts
创建 lib/ucp/handlers/__tests__/fulfillment.test.ts
创建 lib/ucp/__tests__/negotiation.test.ts
创建 lib/ucp/__tests__/profile.test.ts
创建 app/api/ucp/__tests__/checkout.route.test.ts
执行测试命令:npm test(或bun test)Sub-command: docs
子命令:docs
Trigger
触发方式
User runs
/ucp docs用户执行
/ucp docsPurpose
用途
Generate internal documentation for the UCP integration.
生成UCP集成的内部文档
Prerequisites
前置条件
- Config must exist
- Implementation ideally exists
- 配置必须存在
- 最好已有实现
Procedure
执行流程
Step 1: Gather Information
步骤1:收集信息
- Read config for capabilities, transports, handlers
- Scan generated files
- Read spec files for accurate descriptions
- 读取配置中的能力、传输协议、处理程序
- 扫描生成的文件
- 读取规范文件获取准确描述
Step 2: Generate Documentation
步骤2:生成文档
Create :
docs/ucp-integration.mdmarkdown
undefined创建:
docs/ucp-integration.mdmarkdown
undefinedUCP Integration Documentation
UCP集成文档
Overview
概述
This codebase implements the Universal Commerce Protocol (UCP) version {version}.
Role: {role}
Domain: {domain}
Transports: {transports}
本代码库实现了{version}版本的通用商务协议(UCP)。
角色: {role}
域名: {domain}
传输协议: {transports}
Capabilities
能力
Core
核心
- - Checkout session management
dev.ucp.shopping.checkout
- - 结账会话管理
dev.ucp.shopping.checkout
Extensions
扩展
{list extensions with descriptions}
{列出扩展能力及描述}
API Endpoints
API端点
Discovery
发现
- - UCP discovery profile
GET /.well-known/ucp
- - UCP发现配置
GET /.well-known/ucp
Checkout (REST)
结账(REST)
- - Create checkout session
POST /api/ucp/checkout - - Get checkout session
GET /api/ucp/checkout/:id - - Update checkout session
PATCH /api/ucp/checkout/:id - (action=complete) - Complete checkout
POST /api/ucp/checkout/:id
- - 创建结账会话
POST /api/ucp/checkout - - 查询结账会话
GET /api/ucp/checkout/:id - - 更新结账会话
PATCH /api/ucp/checkout/:id - (action=complete)- 完成结账
POST /api/ucp/checkout/:id
Checkout Status Lifecycle
结账状态生命周期
incomplete → requires_escalation → ready_for_complete → complete_in_progress → completed
↘ canceledincomplete → requires_escalation → ready_for_complete → complete_in_progress → completed
↘ canceledPayment Handlers
支付处理程序
{list configured handlers with integration notes}
{列出已配置的处理程序及集成说明}
Capability Negotiation
能力协商
The platform sends their profile URL via header:
UCP-AgentUCP-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.
平台通过 header发送其配置URL:
UCP-AgentUCP-Agent: profile="https://platform.example.com/.well-known/ucp"商家侧获取平台配置,计算能力交集,并在每个响应中包含协商后的能力。
Configuration
配置
Configuration is stored in at the project root.
ucp.config.json配置存储在项目根目录的中。
ucp.config.jsonFiles
文件列表
{list generated files with descriptions}
{列出生成的文件及描述}
Testing
测试
Run unit tests:
bash
npm test运行单元测试:
bash
npm testValidation
验证
Validate implementation against UCP schemas:
bash
undefined对照UCP schema验证实现合规性:
bash
undefinedUsing the skill
使用skill
/ucp validate
undefined/ucp validate
undefinedStep 3: Output
步骤3:输出
Generated documentation:
CREATE docs/ucp-integration.md
Documentation includes:
- Capability overview
- API endpoint reference
- Status lifecycle diagram
- Configuration guide
- File manifest已生成文档:
创建 docs/ucp-integration.md
文档包含:
- 能力概览
- API端点参考
- 状态生命周期图
- 配置指南
- 文件清单Error Handling
错误处理
Interactive Error Resolution
交互式错误解决
When encountering ambiguous situations, always ask the user:
- Missing config: "Config file not found. Run /ucp init first, or create manually?"
- Spec fetch failed: "Could not clone UCP spec. Check network/auth. Retry, use local path, or abort?"
- Conflicting files: "File {path} already exists. Overwrite, merge, skip, or abort?"
- Unknown role: "Role '{role}' not recognized. Did you mean: business, platform, payment_provider, or host_embedded?"
- Validation failure: "Schema validation failed for {file}. Show details, attempt fix, or skip?"
遇到歧义场景时,始终询问用户:
- 配置缺失: "未找到配置文件,先执行/ucp init还是手动创建?"
- 规范拉取失败: "无法克隆UCP规范,请检查网络/权限。要重试、使用本地路径还是终止?"
- 文件冲突: "文件{path}已存在,要覆盖、合并、跳过还是终止?"
- 未知角色: "角色'{role}'未识别,是否是:business、platform、payment_provider或host_embedded?"
- 验证失败: "{file}的schema验证失败,要显示详情、尝试修复还是跳过?"
Error Codes
错误码
| Code | Meaning | Resolution |
|---|---|---|
| ucp.config.json missing | Run /ucp init |
| Spec repo not available | Check network, clone manually |
| Unknown role in config | Fix config |
| Response doesn't match schema | Review generated code |
| Edge runtime used | Change to nodejs/bun |
| 码值 | 含义 | 解决方案 |
|---|---|---|
| 缺失ucp.config.json | 执行/ucp init |
| 规范仓库不可用 | 检查网络,手动克隆 |
| 配置中存在未知角色 | 修复配置 |
| 响应不符合schema | 审查生成的代码 |
| 使用了Edge runtime | 切换为nodejs/bun |
Next.js Conventions
Next.js约定
File Structure
文件结构
project/
├── app/
│ ├── .well-known/
│ │ └── ucp/
│ │ └── route.ts # Discovery profile
│ └── api/
│ └── ucp/
│ ├── checkout/
│ │ ├── route.ts # POST create
│ │ └── [id]/
│ │ └── route.ts # GET, PATCH, POST complete
│ ├── mcp/
│ │ └── route.ts # MCP transport (if enabled)
│ └── a2a/
│ └── route.ts # A2A transport (if enabled)
├── 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/ # Cloned spec (gitignored)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 supportedOr for Bun:
typescript
export const runtime = 'nodejs'; // Bun-compatible Node.js runtime每个路由文件必须包含:
typescript
export const runtime = 'nodejs'; // Edge runtime is not supported如果使用Bun:
typescript
export const runtime = 'nodejs'; // Bun-compatible Node.js runtimeApp Router Patterns
App Router模式
- Use Route Handlers () not API Routes (
route.ts)pages/api - Use and
NextRequestfromNextResponsenext/server - Await for body parsing
request.json() - Use for header access
request.headers.get()
- 使用Route Handlers()而非API Routes(
route.ts)pages/api - 使用导出的
next/server和NextRequestNextResponse - 使用解析请求体
await request.json() - 使用读取header
request.headers.get()
Credential Handling
凭证处理
Keys Required For
需要密钥的场景
- AP2 Mandates: JWS signing key (ES256)
- Webhook Signing: JWS signing key (ES256)
- Identity Linking: OAuth client credentials
- AP2 Mandates: JWS签名密钥(ES256)
- Webhook签名: JWS签名密钥(ES256)
- 身份关联: OAuth客户端凭证
Prompting for Credentials
凭证提示
When a feature requires credentials, ask:
"AP2 mandates require a JWS signing key (ES256 recommended). Do you have an existing key pair, or should I explain how to generate one?"
If user needs generation instructions:
bash
undefined当某个功能需要凭证时,询问用户:
"AP2 mandates需要JWS签名密钥(推荐ES256算法)。 你已有现成的密钥对,还是需要我说明生成方式?"
如果用户需要生成说明:
bash
undefinedGenerate ES256 key pair
生成ES256密钥对
openssl ecparam -genkey -name prime256v1 -noout -out private.pem
openssl ec -in private.pem -pubout -out public.pem
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)
转换为JWK格式(使用jose库或在线工具)
undefinedundefinedStorage
存储
Credentials should be stored in environment variables, not in config:
- - Private key (PEM or JWK)
UCP_SIGNING_KEY - - OAuth client ID
UCP_OAUTH_CLIENT_ID - - OAuth client secret
UCP_OAUTH_CLIENT_SECRET
凭证应存储在环境变量中,不要放在配置里:
- - 私钥(PEM或JWK格式)
UCP_SIGNING_KEY - - OAuth客户端ID
UCP_OAUTH_CLIENT_ID - - OAuth客户端密钥
UCP_OAUTH_CLIENT_SECRET
Version History
版本历史
- 2026-01-11: Initial UCP spec version
- Skill v1.0: Initial skill release
- 2026-01-11:UCP初始规范版本
- Skill v1.0:Skill初始版本
References
参考
- UCP Spec Repository: https://github.com/Universal-Commerce-Protocol/ucp.git
- UCP Documentation: See or
./ucp/docs/./.ucp-spec/docs/ - JSON Schema Draft 2020-12
- RFC 8941 (Structured Field Values)
- RFC 3339 (Date/Time Format)
- RFC 8785 (JSON Canonicalization Scheme)
- 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规范化方案)