AT&T购物MCP服务器
A完整 模型上下文协议(MCP)应用程序 使Claude能够充当AT&T购物助理的服务器 交互式可视化UI应用程序.
使用官方 @modelcontextprotocol/ext-apps SDK。
______________________________________________________________________
目录
______________________________________________________________________
概述
这是什么?
MCP服务器将Claude转变为功能齐全的AT&T零售助理,能够:
- 浏览带有可视产品卡的手机、配件和计划
- 使用颜色/存储选择管理购物车
- 互联网服务的合格地址(光纤与空中互联网)
- 通过客户类型验证处理结账
- 申请促销折扣
主要特点
| 特性 | 描述 |
|---|---|
| 视觉产品目录 | 带有产品卡片、图像和评级的旋转木马UI |
| 颜色和储存选择 | 具有动态定价的交互式样本 |
| 地址资格 | 基于邮政编码的光纤与互联网航空 |
| 客户类型验证 | 新客户结账规则与现有客户结账规则 |
| BYOD计划 | 携带自己的设备计划,无需电话 |
| 促销代码 | 百分比和固定折扣 |
______________________________________________________________________
建筑
高级体系结构
┌─────────────────────────────────────────────────────────────────────────────┐
│ CLAUDE (MCP HOST) │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────────┐ │
│ │ User Message │───▶│ Tool Selection │───▶│ Response Generation │ │
│ │ "Show phones" │ │ get_phones │ │ + UI Rendering │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ MCP SERVER (Node.js) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Tools │ │ Resources │ │ Catalog │ │ State │ │
│ │ │ │ │ │ │ │ │ │
│ │ • get_phones │ │ • UI HTML │ │ • Products │ │ • Carts │ │
│ │ • get_plans │ │ • Bundled JS │ │ • Plans │ │ • Orders │ │
│ │ • add_to_cart│ │ │ │ • Internet │ │ • Promos │ │
│ │ • checkout │ │ │ │ • Promotions │ │ • Addresses │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Transport Layer │ │
│ │ HTTP (:3001/mcp) ←─────────────────────────→ stdio (--stdio) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ UI APP (Sandboxed iframe) │
│ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ mcp-app.html + mcp-app.ts (bundled by Vite) │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Render │ │ Events │ │ Tool Calls │ │ State │ │ │
│ │ │ Products │ │ (clicks) │ │ (via MCP) │ │ (local) │ │ │
│ │ │ Carousel │ │ Colors │ │ add_to_cart │ │ cartState │ │ │
│ │ │ Cards │ │ Storage │ │ get_cart │ │ currentPage│ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘MCP应用程序模式
1. Claude calls tool (e.g., get_phones)
2. Tool has _meta.ui.resourceUri pointing to UI resource
3. Server returns data + UI resource reference
4. Claude fetches UI HTML and renders in sandboxed iframe
5. UI uses @modelcontextprotocol/ext-apps to call tools
6. UI receives tool results and updates display通信流
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Claude │ │ Server │ │ UI │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
│ 1. get_phones() │ │
│───────────────────▶│ │
│ │ │
│ 2. JSON + UI ref │ │
│◀───────────────────│ │
│ │ │
│ 3. Fetch UI HTML │ │
│───────────────────▶│ │
│ │ │
│ 4. HTML resource │ │
│◀───────────────────│ │
│ │ │
│ 5. Render iframe │ │
│────────────────────────────────────────▶│
│ │ │
│ │ 6. ontoolresult │
│ │◀───────────────────│
│ │ │
│ │ 7. UI updates │
│ │───────────────────▶│
│ │ │
│ 8. User clicks "Add to Cart" │
│◀────────────────────────────────────────│
│ │ │
│ 9. add_to_cart() │ │
│───────────────────▶│ │
│ │ │______________________________________________________________________
项目结构
att-mcp-server/
├── main.ts # Entry point - HTTP & stdio transports
├── server.ts # MCP server - tools, resources, business logic
├── mcp-app.html # UI template - HTML structure & CSS
├── src/
│ └── mcp-app.ts # UI logic - TypeScript for interactivity
├── data/
│ ├── catalog.xlsx # Product database (Excel)
│ ├── carts.json # Shopping cart state
│ └── orders.json # Completed orders
├── dist/ # Build output
│ ├── main.js # Compiled server
│ └── mcp-app.html # Bundled single-file UI
├── package.json
├── tsconfig.json # UI TypeScript config
├── tsconfig.server.json # Server TypeScript config
└── vite.config.ts # Vite bundler config文件责任
| 文件 | 目的 |
|---|---|
main.ts | HTTP服务器设置、传输处理、请求路由 |
server.ts | 工具定义、业务逻辑、目录加载、状态管理 |
mcp-app.html | UI结构、CSS样式、吐司通知 |
src/mcp-app.ts | UI交互性、事件处理程序、MCP通信 |
data/catalog.xlsx | 产品/计划/促销数据(Excel格式) |
______________________________________________________________________
数据模型
产品(手机/配件)
interface Product {
product_id: string; // "ATT-IP17PM"
name: string; // "iPhone 17 Pro Max"
category: string; // "Phones" | "Accessories"
subcategory: string; // "Flagship" | "Case" | "Charger"
price: number; // Base price: 1199
monthly_price?: number; // Optional installment price
stock: number; // Inventory count
description: string; // Product description
brand: string; // "Apple" | "Samsung" | "Google"
rating: number; // 4.8 (out of 5)
color?: string; // Pipe-delimited: "Black|Silver|Gold"
storage?: string; // Pipe-delimited: "128GB|256GB|512GB"
storage_prices?: string; // Price increments: "128GB:0|256GB:100|512GB:200"
ranking?: number; // Sort order (lower = first)
}无线计划
interface Plan {
plan_id: string; // "PLAN-PREM"
name: string; // "AT&T Unlimited Premium"
category: string; // "Postpaid" | "BYOD" | "Family"
price_monthly: number; // 85
description: string; // Plan description
data_limit: string; // "Unlimited Premium" | "50GB"
hotspot: string; // "60GB" | "None"
streaming: string; // "4K UHD" | "HD 1080p" | "SD 480p"
features: string; // Comma-separated features
popular?: boolean; // Show "Popular" badge
requires_phone?: boolean;// New customers need phone
}互联网计划
interface InternetPlan {
plan_id: string; // "INT-F500"
name: string; // "AT&T Fiber 500"
category: string; // "Fiber" | "Internet Air"
price_monthly: number; // 65
speed_down: string; // "500 Mbps"
speed_up: string; // "500 Mbps"
description: string; // Plan description
features: string; // Comma-separated features
popular?: boolean; // Show "Recommended" badge
requires_qualification?: boolean; // Needs address check
}购物车商品
interface CartItem {
id: string; // Product/plan ID
name: string; // Display name with options
price: number; // Price (with storage increment)
quantity: number; // Quantity
type: "product" | "plan" | "internet";
color?: string; // Selected color
storage?: string; // Selected storage
}购物车状态
interface Cart {
items: CartItem[];
promo_code: string | null;
}
// Calculated cart summary
interface CartSummary {
items: CartItem[];
item_count: number;
subtotal: number;
discount: number;
promo_code: string | null;
tax: number; // 8.25%
shipping: number; // Free over $35
total: number;
}地址资格
interface QualificationState {
address: string;
zip: string;
fiber_available: boolean;
qualified_at: string; // ISO timestamp
}
// Qualification rules:
// - ZIP prefixes 90-95, 10-12, 20-22 → Fiber available
// - All other ZIPs → Internet Air only______________________________________________________________________
工具参考
交互式UI工具
这些工具返回JSON数据和指向可视化UI资源的链接。
| 工具 | 说明 | 参数 |
|---|---|---|
get_phones | 使用轮播UI浏览电话目录 | brand?, max_price?, foldable?, limit? |
get_accessories | 浏览配件目录 | brand?, max_price?, limit? |
get_wireless_plans | 比较无线计划 | category?, max_price? |
get_internet_plans | 浏览互联网计划(需要资格) | min_speed?, user_id? |
get_cart | 使用结账UI查看购物车 | user_id? |
get_inventory_summary | 管理员库存仪表板 | - |
行动工具
这些工具执行操作并返回文本响应。
| 工具 | 说明 | 参数 |
|---|---|---|
search_products | 按关键字搜索目录 | query?, category?, brand?, min_price?, max_price?, limit? |
add_to_cart | 将商品添加到购物车 | product_id, product_type?, quantity?, color?, storage?, user_id? |
remove_from_cart | 从购物车中删除商品 | product_id, user_id? |
apply_promo | 应用促销代码 | promo_code, user_id? |
clear_cart | 清空购物车 | user_id? |
check_address | 互联网的合格地址 | address, zip, city?, state?, user_id? |
checkout | 完成购买 | shipping_address, is_new_customer, user_id? |
get_promotions | 列出活动促销代码 | - |
工具注册模式
// Interactive tool with UI
registerAppTool(
server,
"get_phones",
{
title: "Phone Catalog",
description: "Browse AT&T phones with visual interface",
inputSchema: {
brand: z.string().optional(),
max_price: z.number().optional(),
},
_meta: { ui: { resourceUri: "app://att-shopping/phone-browser" } },
},
async (args) => {
const products = await getProducts(args);
return { content: [{ type: "text", text: JSON.stringify(products) }] };
}
);
// Standard tool without UI
server.tool(
"add_to_cart",
"Add item to cart",
{
product_id: z.string(),
color: z.string().optional(),
storage: z.string().optional(),
},
async (args) => {
const result = await addToCart(args);
return { content: [{ type: "text", text: formatResponse(result) }] };
}
);______________________________________________________________________
UI组件
组件层次结构
App Container (#app)
├── Header
│ ├── Title & Count
│ └── Page Navigation Info
├── Carousel Container
│ ├── Prev Button (‹)
│ ├── Carousel Wrapper
│ │ └── Carousel Track (CSS transform)
│ │ ├── Product Card 1
│ │ ├── Product Card 2
│ │ └── ...
│ └── Next Button (›)
├── Page Dots
│ ├── Dot 1
│ ├── Dot 2
│ └── ...
├── Cart Badge (floating)
│ └── Item Count
└── Toast Container (notifications)产品卡片结构
Flagship
Apple
iPhone 17 Pro Max
Cosmic Orange
Storage
256GB
512GB +$200
$1,199
$33.31/mo × 36
⭐⭐⭐⭐⭐ 4.9
✓ In Stock
🛒 Add to Cart
旋转木马导航
const ITEMS_PER_PAGE = 3;
let currentPage = 0;
function setupCarousel(totalItems: number): void {
const totalPages = Math.ceil(totalItems / ITEMS_PER_PAGE);
const track = document.getElementById("carousel-track");
const prevBtn = document.getElementById("prev-btn");
const nextBtn = document.getElementById("next-btn");
function updateCarousel(): void {
// Move track using CSS transform
const offset = currentPage * ITEMS_PER_PAGE * CARD_WIDTH;
track.style.transform = `translateX(-${offset}px)`;
// Update button states
prevBtn.disabled = currentPage === 0;
nextBtn.disabled = currentPage >= totalPages - 1;
// Update page dots
updatePageDots();
}
prevBtn.addEventListener("click", () => {
if (currentPage > 0) {
currentPage--;
updateCarousel();
}
});
nextBtn.addEventListener("click", () => {
if (currentPage = {
"Cosmic Orange": "#FF6B35",
"Space Black": "#1d1d1f",
"Natural Titanium": "#9a9a9f",
"Ultramarine": "#2851A3",
"Titanium Black": "#3d3d3d",
// ... 50+ colors
};
function getColorHex(colorName: string): string {
return COLOR_MAP[colorName] || "#cccccc";
}
function setupColorSwatches(): void {
document.querySelectorAll(".color-swatch").forEach(swatch => {
swatch.addEventListener("click", (e) => {
const card = target.closest(".product-card");
const colorName = target.dataset.color;
// Update active state
card.querySelectorAll(".color-swatch").forEach(s =>
s.classList.remove("active"));
target.classList.add("active");
// Update color name display
card.querySelector(".color-name").textContent = colorName;
// Store selection for add-to-cart
card.dataset.selectedColor = colorName;
});
});
}动态存储定价
function setupStorageOptions(): void {
document.querySelectorAll(".storage-option").forEach(option => {
option.addEventListener("click", (e) => {
const button = e.currentTarget as HTMLElement;
const card = button.closest(".product-card") as HTMLElement;
const storage = button.dataset.storage;
const priceIncrement = parseInt(button.dataset.priceIncrement || "0");
// Update active state
card.querySelectorAll(".storage-option").forEach(s =>
s.classList.remove("active"));
button.classList.add("active");
// Calculate new price
const basePrice = parseFloat(card.dataset.basePrice);
const newPrice = basePrice + priceIncrement;
const newMonthly = newPrice / 36;
// Update price display
card.querySelector(".product-price").textContent =
`$${newPrice.toLocaleString()}`;
card.querySelector(".product-monthly").textContent =
`$${newMonthly.toFixed(2)}/mo × 36`;
// Store selection
card.dataset.selectedStorage = storage;
});
});
}______________________________________________________________________
业务逻辑
地址资格流程
┌─────────────────────────────────────────────────────────────────┐
│ Address Qualification │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ User asks about internet service │
│ "I want home internet at 123 Main St, 90210" │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Claude calls check_address tool │
│ { address: "123 Main St", zip: "90210" } │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Server checks ZIP prefix │
│ 90xxx = West Coast = Fiber Available ✓ │
│ (Other ZIPs = Internet Air only) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Store qualification in cache │
│ qualificationCache["default"] = { │
│ address: "123 Main St", │
│ zip: "90210", │
│ fiber_available: true │
│ } │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Claude calls get_internet_plans │
│ Server returns only Fiber plans (user is qualified) │
│ UI shows Fiber product cards │
└─────────────────────────────────────────────────────────────────┘纤维鉴定规则
const FIBER_ZIPS = ['90', '91', '92', '93', '94', '95', // West Coast
'10', '11', '12', // NY Metro
'20', '21', '22']; // DC Area
function isFiberAvailable(zip: string): boolean {
const prefix = zip.substring(0, 2);
return FIBER_ZIPS.includes(prefix);
}检查验证流程
┌─────────────────────────────────────────────────────────────────┐
│ Checkout Validation │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 1. Check customer type │
│ is_new_customer: true/false │
└─────────────────────────────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ NEW CUSTOMER │ │ EXISTING CUSTOMER │
│ │ │ │
│ Postpaid plan? │ │ No restrictions │
│ → Must have phone │ │ Can buy phone only │
│ │ │ Can buy plan only │
│ Phone purchase? │ │ │
│ → Must have plan │ │ │
│ (Postpaid or BYOD) │ │ │
│ │ │ │
│ BYOD plan? │ │ │
│ → No phone required │ │ │
└─────────────────────────┘ └─────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 2. Validate shipping address │
│ Required: name, street, city, state, zip │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 3. Create order │
│ - Generate order ID │
│ - Calculate totals │
│ - Clear cart │
│ - Save to orders.json │
└─────────────────────────────────────────────────────────────────┘验证码
async function checkout(args: {
shipping_address: ShippingAddress;
is_new_customer: boolean;
user_id?: string;
}) {
const cart = await getCart(args.user_id);
const { plans } = await loadCatalog();
const hasPhone = cart.items.some(i => i.type === "product");
const hasPostpaidPlan = cart.items.some(i => {
if (i.type === "plan") {
const plan = plans.find(p => p.plan_id === i.id);
return plan?.category === "Postpaid";
}
return false;
});
const hasBYODPlan = cart.items.some(i => {
if (i.type === "plan") {
const plan = plans.find(p => p.plan_id === i.id);
return plan?.category === "BYOD";
}
return false;
});
// New customer validation
if (args.is_new_customer) {
if (hasPostpaidPlan && !hasPhone) {
return {
success: false,
message: "New customers must purchase a phone with Unlimited plans",
validation_error: "PHONE_REQUIRED"
};
}
if (hasPhone && !hasPostpaidPlan && !hasBYODPlan) {
return {
success: false,
message: "New customers must select a wireless plan",
validation_error: "PLAN_REQUIRED"
};
}
}
// Proceed with order...
}定价计算
// Storage-based pricing
function calculatePrice(basePrice: number, storage: string, storagePrices: string): number {
if (!storage || !storagePrices) return basePrice;
const priceMap = storagePrices.split('|').reduce((acc, item) => {
const [size, increment] = item.split(':');
acc[size.trim()] = parseInt(increment) || 0;
return acc;
}, {} as Record);
return basePrice + (priceMap[storage] || 0);
}
// Cart totals
function calculateCartTotals(items: CartItem[], promoCode: string | null) {
const subtotal = items.reduce((sum, i) => sum + i.price * i.quantity, 0);
let discount = 0;
if (promoCode) {
const promo = findPromo(promoCode);
if (promo && subtotal >= promo.min_order) {
discount = promo.type === "percent"
? subtotal * (promo.value / 100)
: promo.value;
}
}
const tax = (subtotal - discount) * 0.0825; // 8.25% tax
const shipping = subtotal >= 35 ? 0 : 7.99; // Free over $35
return {
subtotal: round(subtotal),
discount: round(discount),
tax: round(tax),
shipping: round(shipping),
total: round(subtotal - discount + tax + shipping)
};
}______________________________________________________________________
设置和部署
先决条件
- Node.js 18+
- npm 9+
安装
# Clone or extract project
cd att-mcp-server
# Install dependencies
npm install
# Build (compiles TypeScript + bundles UI)
npm run build
# Start server
npm run serve部署选项
选项1:ngrok(快速测试)
# Terminal 1: Start server
npm run serve
# Terminal 2: Create tunnel
ngrok http 3001
# Use the ngrok URL in Claude connector settings选项2:Cloudflare隧道(生产)
# Start server
npm run serve
# Create tunnel
npx cloudflared tunnel --url http://localhost:3001
# Use the cloudflared URL in Claude connector settings选项3:克劳德桌面(本地)
增添 ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"att-shopping": {
"command": "node",
"args": ["/path/to/att-mcp-server/dist/main.js", "--stdio"]
}
}
}环境变量
| 变量 | 默认值 | 描述 |
|---|---|---|
PORT | 3001 | HTTP服务器端口 |
______________________________________________________________________
API 参考
HTTP端点
| 端点 | 方法 | 描述 |
|---|---|---|
/ | GET | 服务器信息页面 |
/mcp | POST | MCP JSON-RPC端点 |
/health | GET | 健康检查 |
MCP JSON-RPC
// Request
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_phones",
"arguments": { "brand": "Apple", "limit": 10 }
}
}
// Response
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{ "type": "text", "text": "[{\"product_id\":\"ATT-IP17PM\",...}]" }
]
}
}资源URI
| URI | 描述 |
|---|---|
app://att-shopping/phone-browser | 电话目录用户界面 |
app://att-shopping/accessory-browser | 配件目录UI |
app://att-shopping/plan-browser | 无线计划UI |
app://att-shopping/internet-browser | 互联网计划UI |
app://att-shopping/cart | 购物车用户界面 |
app://att-shopping/inventory-dashboard | 库存管理界面 |
______________________________________________________________________
开发指南
构建命令
# Full build (TypeScript + UI bundle)
npm run build
# Development mode (watch + auto-restart)
npm start
# Type checking only
npx tsc --noEmit
# Server compilation only
npx tsc -p tsconfig.server.json
# UI bundle only
npx vite build添加新产品
- 编辑
data/catalog.xlsx→ 产品介绍 - 添加包含所有必填字段的行
- 重新启动服务器(目录已缓存)
添加新工具
// In server.ts
// 1. Define business logic function
async function myNewFunction(args: MyArgs): Promise {
// Implementation
}
// 2. Register tool
server.tool(
"my_new_tool",
"Description for Claude",
{
param1: z.string().describe("Parameter description"),
param2: z.number().optional(),
},
async (args) => {
const result = await myNewFunction(args);
return { content: [{ type: "text", text: JSON.stringify(result) }] };
}
);添加新UI视图
- 在中添加渲染功能
src/mcp-app.ts:
function renderMyView(data: MyData): string {
return `
...
...
`;
}- 添加视图类型:
type ViewType = "phones" | "accessories" | ... | "myview";- 更新渲染开关:
switch (currentView) {
case "myview": html = renderMyView(currentData as MyData); break;
}- 添加检测
ontoolresult:
if (data.myViewIdentifier) {
currentView = "myview";
}测试
# Manual testing with curl
curl -X POST http://localhost:3001/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# Test specific tool
curl -X POST http://localhost:3001/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_phones","arguments":{}}}'______________________________________________________________________
产品目录摘要
手机
| 品牌 | 型号 | 价格范围 |
|---|---|---|
| 苹果 | iPhone 17 Pro Max、17 Pro、17 Air、17、16系列、15系列、14 | 699美元-1599美元 |
| 三星 | Galaxy S25 Ultra、S25+、S25、S24系列、Z Fold6、Z Flip6、A54 | 449美元-1899美元 |
| 谷歌 | Pixel 9 Pro XL、9 Pro、9、8 Pro、8 | 699美元-1099美元 |
无线计划
| 类别 | 计划 | 价格范围 |
|---|---|---|
| 延期 | 高级、额外、入门 | 每月65-85美元 |
| BYOD | 5GB、15GB、无限、无限+ | 30-50美元/月 |
| 家庭 | 4线 | 160美元/月 |
互联网计划
| 类别 | 计划 | 价格范围 |
|---|---|---|
| 光纤 | 300、500、1个GIG、2个GIG | 55-110美元/月 |
| 空中互联网 | 标准,加 | 55-75美元/月 |
促销代码
| 代码 | 折扣 | 最低 |
|---|---|---|
| ATT20 | 20% off | $100 |
| 新闻50 | 50美元折扣 | 50美元 |
| 免费 | 免运费 | $35 |
______________________________________________________________________
许可证
MIT许可证-有关详细信息,请参阅许可证文件。
______________________________________________________________________
