create-mcp-use-app 0.5.2 → 0.6.0-canary.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/templates/apps-sdk/README.md +144 -4
- package/dist/templates/apps-sdk/index.ts +72 -6
- package/dist/templates/apps-sdk/package.json +3 -0
- package/dist/templates/apps-sdk/public/fruits/apple.png +0 -0
- package/dist/templates/apps-sdk/public/fruits/apricot.png +0 -0
- package/dist/templates/apps-sdk/public/fruits/avocado.png +0 -0
- package/dist/templates/apps-sdk/public/fruits/banana.png +0 -0
- package/dist/templates/apps-sdk/public/fruits/blueberry.png +0 -0
- package/dist/templates/apps-sdk/public/fruits/cherries.png +0 -0
- package/dist/templates/apps-sdk/public/fruits/coconut.png +0 -0
- package/dist/templates/apps-sdk/public/fruits/grapes.png +0 -0
- package/dist/templates/apps-sdk/public/fruits/lemon.png +0 -0
- package/dist/templates/apps-sdk/public/fruits/mango.png +0 -0
- package/dist/templates/apps-sdk/public/fruits/orange.png +0 -0
- package/dist/templates/apps-sdk/public/fruits/pear.png +0 -0
- package/dist/templates/apps-sdk/public/fruits/pineapple.png +0 -0
- package/dist/templates/apps-sdk/public/fruits/plum.png +0 -0
- package/dist/templates/apps-sdk/public/fruits/strawberry.png +0 -0
- package/dist/templates/apps-sdk/public/fruits/watermelon.png +0 -0
- package/dist/templates/apps-sdk/resources/product-search-result/components/Accordion.tsx +40 -0
- package/dist/templates/apps-sdk/resources/product-search-result/components/AccordionItem.tsx +34 -0
- package/dist/templates/apps-sdk/resources/product-search-result/components/Carousel.tsx +65 -0
- package/dist/templates/apps-sdk/resources/product-search-result/components/CarouselItem.tsx +26 -0
- package/dist/templates/apps-sdk/resources/product-search-result/constants.ts +4 -0
- package/dist/templates/apps-sdk/resources/product-search-result/hooks/useCarouselAnimation.ts +78 -0
- package/dist/templates/apps-sdk/resources/product-search-result/types.ts +14 -0
- package/dist/templates/apps-sdk/resources/product-search-result/widget.tsx +61 -0
- package/dist/templates/apps-sdk/styles.css +104 -0
- package/package.json +1 -1
- package/dist/templates/apps-sdk/resources/display-weather.tsx +0 -103
|
@@ -7,11 +7,13 @@ An MCP server template with OpenAI Apps SDK integration for ChatGPT-compatible w
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
9
|
- **🤖 OpenAI Apps SDK**: Full compatibility with ChatGPT widgets
|
|
10
|
-
- **🎨
|
|
10
|
+
- **🎨 Official UI Components**: Integrated [OpenAI Apps SDK UI components](https://openai.github.io/apps-sdk-ui/) for consistent, accessible widgets
|
|
11
|
+
- **🛒 Ecommerce Widgets**: Complete ecommerce example with carousel, search, map, and order confirmation
|
|
11
12
|
- **🔄 Automatic Registration**: Widgets auto-register from `resources/` folder
|
|
12
13
|
- **📦 Props Schema**: Zod schema validation for widget props
|
|
13
14
|
- **🌙 Theme Support**: Dark/light theme detection via `useWidget` hook
|
|
14
15
|
- **🛠️ TypeScript**: Complete type safety
|
|
16
|
+
- **🔧 Widget Capabilities**: Full support for `callTool`, `sendFollowUpMessage`, and persistent state
|
|
15
17
|
|
|
16
18
|
## What's New: Apps SDK Integration
|
|
17
19
|
|
|
@@ -59,9 +61,13 @@ npm start
|
|
|
59
61
|
|
|
60
62
|
```
|
|
61
63
|
apps-sdk/
|
|
62
|
-
├── resources/
|
|
63
|
-
│
|
|
64
|
-
├──
|
|
64
|
+
├── resources/ # React widget components
|
|
65
|
+
│ ├── display-weather.tsx # Weather widget example
|
|
66
|
+
│ ├── ecommerce-carousel.tsx # Ecommerce product carousel
|
|
67
|
+
│ ├── product-search-result.tsx # Product search with filters
|
|
68
|
+
│ ├── stores-locations-map.tsx # Store locations map
|
|
69
|
+
│ └── order-confirmation.tsx # Order confirmation widget
|
|
70
|
+
├── index.ts # Server entry point (includes brand info tool)
|
|
65
71
|
├── package.json
|
|
66
72
|
├── tsconfig.json
|
|
67
73
|
└── README.md
|
|
@@ -153,6 +159,89 @@ const bgColor = theme === 'dark' ? 'bg-gray-900' : 'bg-white';
|
|
|
153
159
|
const textColor = theme === 'dark' ? 'text-gray-100' : 'text-gray-800';
|
|
154
160
|
```
|
|
155
161
|
|
|
162
|
+
## Official UI Components
|
|
163
|
+
|
|
164
|
+
This template uses the [OpenAI Apps SDK UI component library](https://openai.github.io/apps-sdk-ui/) for building consistent, accessible widgets. The library provides:
|
|
165
|
+
|
|
166
|
+
- **Button**: Primary, secondary, and outline button variants
|
|
167
|
+
- **Card**: Container component for content sections
|
|
168
|
+
- **Carousel**: Image and content carousel with transitions
|
|
169
|
+
- **Input**: Form input fields
|
|
170
|
+
- **Icon**: Consistent iconography
|
|
171
|
+
- **Transition**: Smooth animations and transitions
|
|
172
|
+
|
|
173
|
+
Import components like this:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
import {
|
|
177
|
+
Button,
|
|
178
|
+
Card,
|
|
179
|
+
Carousel,
|
|
180
|
+
CarouselItem,
|
|
181
|
+
Transition,
|
|
182
|
+
Icon,
|
|
183
|
+
Input,
|
|
184
|
+
} from '@openai/apps-sdk-ui';
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Ecommerce Widgets
|
|
188
|
+
|
|
189
|
+
This template includes a complete ecommerce example with four widgets:
|
|
190
|
+
|
|
191
|
+
### 1. Ecommerce Carousel (`ecommerce-carousel.tsx`)
|
|
192
|
+
|
|
193
|
+
A product carousel widget featuring:
|
|
194
|
+
- Title and description
|
|
195
|
+
- Carousel of product items with placeholder images
|
|
196
|
+
- Info button and Add to Cart button for each item
|
|
197
|
+
- Uses official Carousel, Card, Button, Icon, and Transition components
|
|
198
|
+
- Integrates with `callTool` for cart operations
|
|
199
|
+
- Persistent state management
|
|
200
|
+
|
|
201
|
+
### 2. Product Search Result (`product-search-result.tsx`)
|
|
202
|
+
|
|
203
|
+
A search results widget with:
|
|
204
|
+
- Search input with real-time filtering
|
|
205
|
+
- Price range filters and stock status filter
|
|
206
|
+
- Grid layout of product cards
|
|
207
|
+
- Uses `callTool` to perform searches
|
|
208
|
+
- Uses `sendFollowUpMessage` to update conversation
|
|
209
|
+
- Persistent filter state
|
|
210
|
+
|
|
211
|
+
### 3. Stores Locations Map (`stores-locations-map.tsx`)
|
|
212
|
+
|
|
213
|
+
A store locator widget featuring:
|
|
214
|
+
- Interactive map display (placeholder)
|
|
215
|
+
- List of store locations with details
|
|
216
|
+
- Distance calculation
|
|
217
|
+
- Get directions functionality
|
|
218
|
+
- Store details on click
|
|
219
|
+
- Uses `callTool` for directions and store info
|
|
220
|
+
|
|
221
|
+
### 4. Order Confirmation (`order-confirmation.tsx`)
|
|
222
|
+
|
|
223
|
+
An order confirmation widget with:
|
|
224
|
+
- Order summary and items list
|
|
225
|
+
- Shipping information
|
|
226
|
+
- Order status tracking
|
|
227
|
+
- Track order and view receipt actions
|
|
228
|
+
- Uses `callTool` for order tracking
|
|
229
|
+
|
|
230
|
+
## Brand Info Tool
|
|
231
|
+
|
|
232
|
+
The template includes a `get-brand-info` tool (normal MCP tool, not a widget) that returns brand information:
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
// Call the tool
|
|
236
|
+
await client.callTool('get-brand-info', {});
|
|
237
|
+
|
|
238
|
+
// Returns brand details including:
|
|
239
|
+
// - Company name, tagline, description
|
|
240
|
+
// - Mission and values
|
|
241
|
+
// - Contact information
|
|
242
|
+
// - Social media links
|
|
243
|
+
```
|
|
244
|
+
|
|
156
245
|
## Example: Weather Widget
|
|
157
246
|
|
|
158
247
|
The included `display-weather.tsx` widget demonstrates:
|
|
@@ -321,8 +410,59 @@ const { props } = useWidget();
|
|
|
321
410
|
const city = props.city;
|
|
322
411
|
```
|
|
323
412
|
|
|
413
|
+
## Using Widget Capabilities
|
|
414
|
+
|
|
415
|
+
The widgets in this template demonstrate the full capabilities of the Apps SDK:
|
|
416
|
+
|
|
417
|
+
### Calling Tools (`callTool`)
|
|
418
|
+
|
|
419
|
+
Widgets can call other MCP tools:
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
const { callTool } = useWidget();
|
|
423
|
+
|
|
424
|
+
const handleAction = async () => {
|
|
425
|
+
const result = await callTool('add-to-cart', {
|
|
426
|
+
productId: '123',
|
|
427
|
+
productName: 'Product Name',
|
|
428
|
+
price: 29.99
|
|
429
|
+
});
|
|
430
|
+
};
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### Sending Follow-up Messages (`sendFollowUpMessage`)
|
|
434
|
+
|
|
435
|
+
Widgets can send messages to the ChatGPT conversation:
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
const { sendFollowUpMessage } = useWidget();
|
|
439
|
+
|
|
440
|
+
await sendFollowUpMessage('Product added to cart successfully!');
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### Persistent State (`setState`)
|
|
444
|
+
|
|
445
|
+
Widgets can maintain state across interactions:
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
const { setState, state } = useWidget();
|
|
449
|
+
|
|
450
|
+
// Save state
|
|
451
|
+
await setState({ cart: [...cart, newItem] });
|
|
452
|
+
|
|
453
|
+
// Read state
|
|
454
|
+
const savedCart = state?.cart || [];
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
## Component Library Note
|
|
458
|
+
|
|
459
|
+
This template uses the [OpenAI Apps SDK UI component library](https://openai.github.io/apps-sdk-ui/). The exact component API may vary based on the library version. If you encounter import errors, check the [official documentation](https://openai.github.io/apps-sdk-ui/) for the correct component names and props.
|
|
460
|
+
|
|
461
|
+
If the official library is not available, you can replace the imports with custom React components or other UI libraries while maintaining the same widget structure.
|
|
462
|
+
|
|
324
463
|
## Learn More
|
|
325
464
|
|
|
465
|
+
- [OpenAI Apps SDK UI Components](https://openai.github.io/apps-sdk-ui/) - Official component library
|
|
326
466
|
- [MCP Documentation](https://modelcontextprotocol.io)
|
|
327
467
|
- [OpenAI Apps SDK](https://platform.openai.com/docs/apps)
|
|
328
468
|
- [mcp-use Documentation](https://docs.mcp-use.com)
|
|
@@ -11,20 +11,86 @@ const server = createMCPServer("test-app", {
|
|
|
11
11
|
* Just export widgetMetadata with description and Zod schema, and mcp-use handles the rest!
|
|
12
12
|
*
|
|
13
13
|
* It will automatically add to your MCP server:
|
|
14
|
-
* - server.tool('
|
|
15
|
-
* - server.resource('ui://widget/
|
|
14
|
+
* - server.tool('get-brand-info')
|
|
15
|
+
* - server.resource('ui://widget/get-brand-info')
|
|
16
16
|
*
|
|
17
17
|
* See docs: https://docs.mcp-use.com/typescript/server/ui-widgets
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* Add here
|
|
21
|
+
* Add here your standard MCP tools, resources and prompts
|
|
22
22
|
*/
|
|
23
|
+
|
|
24
|
+
// Fruits data for the API
|
|
25
|
+
const fruits = [
|
|
26
|
+
{ fruit: "mango", color: "bg-[#FBF1E1] dark:bg-[#FBF1E1]/10" },
|
|
27
|
+
{ fruit: "pineapple", color: "bg-[#f8f0d9] dark:bg-[#f8f0d9]/10" },
|
|
28
|
+
{ fruit: "cherries", color: "bg-[#E2EDDC] dark:bg-[#E2EDDC]/10" },
|
|
29
|
+
{ fruit: "coconut", color: "bg-[#fbedd3] dark:bg-[#fbedd3]/10" },
|
|
30
|
+
{ fruit: "apricot", color: "bg-[#fee6ca] dark:bg-[#fee6ca]/10" },
|
|
31
|
+
{ fruit: "blueberry", color: "bg-[#e0e6e6] dark:bg-[#e0e6e6]/10" },
|
|
32
|
+
{ fruit: "grapes", color: "bg-[#f4ebe2] dark:bg-[#f4ebe2]/10" },
|
|
33
|
+
{ fruit: "watermelon", color: "bg-[#e6eddb] dark:bg-[#e6eddb]/10" },
|
|
34
|
+
{ fruit: "orange", color: "bg-[#fdebdf] dark:bg-[#fdebdf]/10" },
|
|
35
|
+
{ fruit: "avocado", color: "bg-[#ecefda] dark:bg-[#ecefda]/10" },
|
|
36
|
+
{ fruit: "apple", color: "bg-[#F9E7E4] dark:bg-[#F9E7E4]/10" },
|
|
37
|
+
{ fruit: "pear", color: "bg-[#f1f1cf] dark:bg-[#f1f1cf]/10" },
|
|
38
|
+
{ fruit: "plum", color: "bg-[#ece5ec] dark:bg-[#ece5ec]/10" },
|
|
39
|
+
{ fruit: "banana", color: "bg-[#fdf0dd] dark:bg-[#fdf0dd]/10" },
|
|
40
|
+
{ fruit: "strawberry", color: "bg-[#f7e6df] dark:bg-[#f7e6df]/10" },
|
|
41
|
+
{ fruit: "lemon", color: "bg-[#feeecd] dark:bg-[#feeecd]/10" },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// API endpoint for fruits data
|
|
45
|
+
server.get("/api/fruits", (c) => {
|
|
46
|
+
return c.json(fruits);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Brand Info Tool - Returns brand information
|
|
23
50
|
server.tool({
|
|
24
|
-
name: "get-
|
|
25
|
-
description:
|
|
51
|
+
name: "get-brand-info",
|
|
52
|
+
description:
|
|
53
|
+
"Get information about the brand, including company details, mission, and values",
|
|
26
54
|
cb: async () => {
|
|
27
|
-
return {
|
|
55
|
+
return {
|
|
56
|
+
content: [
|
|
57
|
+
{
|
|
58
|
+
type: "text",
|
|
59
|
+
text: JSON.stringify(
|
|
60
|
+
{
|
|
61
|
+
name: "mcp-use",
|
|
62
|
+
tagline: "Build MCP servers with UI widgets in minutes",
|
|
63
|
+
description:
|
|
64
|
+
"mcp-use is a modern framework for building Model Context Protocol (MCP) servers with automatic UI widget registration, making it easy to create interactive AI tools and resources.",
|
|
65
|
+
founded: "2024",
|
|
66
|
+
mission:
|
|
67
|
+
"To simplify the development of MCP servers and make AI integration accessible for developers",
|
|
68
|
+
values: [
|
|
69
|
+
"Developer Experience",
|
|
70
|
+
"Simplicity",
|
|
71
|
+
"Performance",
|
|
72
|
+
"Open Source",
|
|
73
|
+
"Innovation",
|
|
74
|
+
],
|
|
75
|
+
contact: {
|
|
76
|
+
website: "https://mcp-use.com",
|
|
77
|
+
docs: "https://docs.mcp-use.com",
|
|
78
|
+
github: "https://github.com/mcp-use/mcp-use",
|
|
79
|
+
},
|
|
80
|
+
features: [
|
|
81
|
+
"Automatic UI widget registration",
|
|
82
|
+
"React component support",
|
|
83
|
+
"Full TypeScript support",
|
|
84
|
+
"Built-in HTTP server",
|
|
85
|
+
"MCP protocol compliance",
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
null,
|
|
89
|
+
2
|
|
90
|
+
),
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
};
|
|
28
94
|
},
|
|
29
95
|
});
|
|
30
96
|
|
|
@@ -27,11 +27,14 @@
|
|
|
27
27
|
"deploy": "mcp-use deploy"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
+
"@openai/apps-sdk-ui": "^0.1.0",
|
|
31
|
+
"@tanstack/react-query": "^5.0.0",
|
|
30
32
|
"cors": "^2.8.5",
|
|
31
33
|
"express": "^4.18.0",
|
|
32
34
|
"mcp-use": "workspace:*",
|
|
33
35
|
"react": "^19.2.0",
|
|
34
36
|
"react-dom": "^19.2.0",
|
|
37
|
+
"react-router": "^7.9.6",
|
|
35
38
|
"tailwindcss": "^4.0.0",
|
|
36
39
|
"zod": "^4.1.12"
|
|
37
40
|
},
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { AccordionItem } from "./AccordionItem";
|
|
3
|
+
|
|
4
|
+
export interface AccordionItemData {
|
|
5
|
+
question: string;
|
|
6
|
+
answer: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface AccordionProps {
|
|
10
|
+
items: AccordionItemData[];
|
|
11
|
+
title?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const Accordion: React.FC<AccordionProps> = ({
|
|
15
|
+
items,
|
|
16
|
+
title = "Can fruit be cute?",
|
|
17
|
+
}) => {
|
|
18
|
+
const [openAccordionIndex, setOpenAccordionIndex] = useState<number | null>(
|
|
19
|
+
null
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="p-8 pt-4 border-t border-gray-200 dark:border-gray-800 mt-4">
|
|
24
|
+
<h3 className="heading-lg mb-4">{title}</h3>
|
|
25
|
+
<div className="rounded-lg border border-gray-200 dark:border-gray-800 overflow-hidden">
|
|
26
|
+
{items.map((item, index) => (
|
|
27
|
+
<AccordionItem
|
|
28
|
+
key={index}
|
|
29
|
+
question={item.question}
|
|
30
|
+
answer={item.answer}
|
|
31
|
+
isOpen={openAccordionIndex === index}
|
|
32
|
+
onToggle={() =>
|
|
33
|
+
setOpenAccordionIndex(openAccordionIndex === index ? null : index)
|
|
34
|
+
}
|
|
35
|
+
/>
|
|
36
|
+
))}
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Animate } from "@openai/apps-sdk-ui/components/Transition";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import type { AccordionItemProps } from "../types";
|
|
4
|
+
|
|
5
|
+
export const AccordionItem: React.FC<AccordionItemProps> = ({
|
|
6
|
+
question,
|
|
7
|
+
answer,
|
|
8
|
+
isOpen,
|
|
9
|
+
onToggle,
|
|
10
|
+
}) => {
|
|
11
|
+
return (
|
|
12
|
+
<div className="border-b border-gray-200 dark:border-gray-800 last:border-b-0">
|
|
13
|
+
<button
|
|
14
|
+
type="button"
|
|
15
|
+
onClick={onToggle}
|
|
16
|
+
className="w-full flex items-center justify-between p-4 text-left hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors"
|
|
17
|
+
>
|
|
18
|
+
<span className="font-medium text-gray-900 dark:text-gray-100">
|
|
19
|
+
{question}
|
|
20
|
+
</span>
|
|
21
|
+
<span className="text-xl text-gray-500 dark:text-gray-400 transition-transform duration-200">
|
|
22
|
+
{isOpen ? "−" : "+"}
|
|
23
|
+
</span>
|
|
24
|
+
</button>
|
|
25
|
+
<Animate enter={{ y: 0, delay: 150, duration: 450 }} exit={{ y: -8 }}>
|
|
26
|
+
{isOpen && (
|
|
27
|
+
<div key="content" className="pb-4 text-secondary px-4">
|
|
28
|
+
{answer}
|
|
29
|
+
</div>
|
|
30
|
+
)}
|
|
31
|
+
</Animate>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Animate } from "@openai/apps-sdk-ui/components/Transition";
|
|
2
|
+
import { useQuery } from "@tanstack/react-query";
|
|
3
|
+
import React, { useRef } from "react";
|
|
4
|
+
import { CarouselItem } from "./CarouselItem";
|
|
5
|
+
import { useCarouselAnimation } from "../hooks/useCarouselAnimation";
|
|
6
|
+
|
|
7
|
+
interface CarouselProps {
|
|
8
|
+
mcpUrl: string | undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const Carousel: React.FC<CarouselProps> = ({ mcpUrl }) => {
|
|
12
|
+
const carouselContainerRef = useRef<HTMLDivElement>(null);
|
|
13
|
+
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
14
|
+
|
|
15
|
+
// Fetch fruits from the API using React Query
|
|
16
|
+
const {
|
|
17
|
+
data: items,
|
|
18
|
+
isLoading,
|
|
19
|
+
error,
|
|
20
|
+
} = useQuery({
|
|
21
|
+
queryKey: ["fruits"],
|
|
22
|
+
queryFn: async () => {
|
|
23
|
+
const response = await fetch(`${mcpUrl}/api/fruits`);
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
throw new Error("Failed to fetch fruits");
|
|
26
|
+
}
|
|
27
|
+
return response.json() as Promise<
|
|
28
|
+
Array<{ fruit: string; color: string }>
|
|
29
|
+
>;
|
|
30
|
+
},
|
|
31
|
+
enabled: !!mcpUrl, // Only run query if mcpUrl is available
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Carousel animation with pointer tracking
|
|
35
|
+
useCarouselAnimation(carouselContainerRef, scrollContainerRef);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
ref={scrollContainerRef}
|
|
40
|
+
className="carousel-scroll-container w-full overflow-x-auto overflow-y-visible pl-8"
|
|
41
|
+
>
|
|
42
|
+
<div ref={carouselContainerRef} className="overflow-visible">
|
|
43
|
+
{isLoading ? (
|
|
44
|
+
<div className="flex items-center justify-center p-8">
|
|
45
|
+
<p className="text-secondary">Loading fruits...</p>
|
|
46
|
+
</div>
|
|
47
|
+
) : error ? (
|
|
48
|
+
<div className="flex items-center justify-center p-8">
|
|
49
|
+
<p className="text-red-500">Failed to load fruits</p>
|
|
50
|
+
</div>
|
|
51
|
+
) : (
|
|
52
|
+
<Animate className="flex gap-4">
|
|
53
|
+
{items?.map((item: { fruit: string; color: string }) => (
|
|
54
|
+
<CarouselItem
|
|
55
|
+
key={item.fruit}
|
|
56
|
+
fruit={item.fruit}
|
|
57
|
+
color={item.color}
|
|
58
|
+
/>
|
|
59
|
+
))}
|
|
60
|
+
</Animate>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Image } from "mcp-use/react";
|
|
2
|
+
import React from "react";
|
|
3
|
+
|
|
4
|
+
export interface CarouselItemProps {
|
|
5
|
+
fruit: string;
|
|
6
|
+
color: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const CarouselItem: React.FC<CarouselItemProps> = ({ fruit, color }) => {
|
|
10
|
+
return (
|
|
11
|
+
<div
|
|
12
|
+
className={`carousel-item size-52 rounded-xl border border-gray-200 dark:border-gray-800 ${color}`}
|
|
13
|
+
>
|
|
14
|
+
<div className="carousel-item-bg">
|
|
15
|
+
<Image src={"/fruits/" + fruit + ".png"} alt={fruit} />
|
|
16
|
+
</div>
|
|
17
|
+
<div className="carousel-item-content">
|
|
18
|
+
<Image
|
|
19
|
+
src={"/fruits/" + fruit + ".png"}
|
|
20
|
+
alt={fruit}
|
|
21
|
+
className="w-24 h-24 object-contain"
|
|
22
|
+
/>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { useEffect, type RefObject } from "react";
|
|
2
|
+
|
|
3
|
+
export function useCarouselAnimation(
|
|
4
|
+
carouselContainerRef: RefObject<HTMLDivElement | null>,
|
|
5
|
+
scrollContainerRef: RefObject<HTMLDivElement | null>
|
|
6
|
+
): void {
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
let lastPointerX = 0;
|
|
9
|
+
let lastPointerY = 0;
|
|
10
|
+
|
|
11
|
+
const updateItems = () => {
|
|
12
|
+
const container = carouselContainerRef.current;
|
|
13
|
+
if (!container) return;
|
|
14
|
+
|
|
15
|
+
const articles =
|
|
16
|
+
container.querySelectorAll<HTMLElement>(".carousel-item");
|
|
17
|
+
|
|
18
|
+
articles.forEach((article) => {
|
|
19
|
+
const rect = article.getBoundingClientRect();
|
|
20
|
+
const centerX = rect.left + rect.width / 2;
|
|
21
|
+
const centerY = rect.top + rect.height / 2;
|
|
22
|
+
const relativeX = lastPointerX - centerX;
|
|
23
|
+
const relativeY = lastPointerY - centerY;
|
|
24
|
+
const x = relativeX / (rect.width / 2);
|
|
25
|
+
const y = relativeY / (rect.height / 2);
|
|
26
|
+
|
|
27
|
+
// Calculate distance from cursor to center of item
|
|
28
|
+
const distance = Math.sqrt(
|
|
29
|
+
relativeX * relativeX + relativeY * relativeY
|
|
30
|
+
);
|
|
31
|
+
// Use a larger max distance to make the effect work across gaps
|
|
32
|
+
const maxDistance = Math.max(rect.width, rect.height) * 2;
|
|
33
|
+
const normalizedDistance = Math.min(distance / maxDistance, 1);
|
|
34
|
+
|
|
35
|
+
// Closer items get higher opacity and scale
|
|
36
|
+
// Use exponential falloff for smoother transition
|
|
37
|
+
const proximity = Math.pow(1 - normalizedDistance, 2);
|
|
38
|
+
const opacity = 0.1 + proximity * 0.3; // Range from 0.1 to 0.4
|
|
39
|
+
const scale = 2.0 + proximity * 2.0; // Range from 2.0 to 4.0
|
|
40
|
+
|
|
41
|
+
article.style.setProperty("--pointer-x", x.toFixed(3));
|
|
42
|
+
article.style.setProperty("--pointer-y", y.toFixed(3));
|
|
43
|
+
article.style.setProperty("--icon-opacity", opacity.toFixed(3));
|
|
44
|
+
article.style.setProperty("--icon-scale", scale.toFixed(2));
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const handlePointerMove = (event: { clientX: number; clientY: number }) => {
|
|
49
|
+
lastPointerX = event.clientX;
|
|
50
|
+
lastPointerY = event.clientY;
|
|
51
|
+
updateItems();
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const handleScroll = () => {
|
|
55
|
+
updateItems();
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
document.addEventListener("pointermove", handlePointerMove);
|
|
59
|
+
window.addEventListener("scroll", handleScroll, true);
|
|
60
|
+
|
|
61
|
+
const scrollContainer = scrollContainerRef.current;
|
|
62
|
+
if (scrollContainer) {
|
|
63
|
+
scrollContainer.addEventListener("scroll", handleScroll);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Initial update
|
|
67
|
+
updateItems();
|
|
68
|
+
|
|
69
|
+
return () => {
|
|
70
|
+
document.removeEventListener("pointermove", handlePointerMove);
|
|
71
|
+
window.removeEventListener("scroll", handleScroll, true);
|
|
72
|
+
const container = scrollContainerRef.current;
|
|
73
|
+
if (container) {
|
|
74
|
+
container.removeEventListener("scroll", handleScroll);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}, [carouselContainerRef, scrollContainerRef]);
|
|
78
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const propSchema = z.object({
|
|
4
|
+
query: z.string().describe("The search query"),
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
export type ProductSearchResultProps = z.infer<typeof propSchema>;
|
|
8
|
+
|
|
9
|
+
export type AccordionItemProps = {
|
|
10
|
+
question: string;
|
|
11
|
+
answer: string;
|
|
12
|
+
isOpen: boolean;
|
|
13
|
+
onToggle: () => void;
|
|
14
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { AppsSDKUIProvider } from "@openai/apps-sdk-ui/components/AppsSDKUIProvider";
|
|
2
|
+
import { QueryClientProvider } from "@tanstack/react-query";
|
|
3
|
+
import { McpUseProvider, useWidget } from "mcp-use/react";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { Link } from "react-router";
|
|
6
|
+
import { Accordion } from "./components/Accordion";
|
|
7
|
+
import { Carousel } from "./components/Carousel";
|
|
8
|
+
import { queryClient } from "./constants";
|
|
9
|
+
import type { ProductSearchResultProps } from "./types";
|
|
10
|
+
import { propSchema } from "./types";
|
|
11
|
+
import "../../styles.css";
|
|
12
|
+
|
|
13
|
+
export const widgetMetadata = {
|
|
14
|
+
description:
|
|
15
|
+
"Display product search results with filtering, state management, and tool interactions",
|
|
16
|
+
inputs: propSchema,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const ProductSearchResult: React.FC = () => {
|
|
20
|
+
const { props, mcp_url } = useWidget<ProductSearchResultProps>();
|
|
21
|
+
|
|
22
|
+
console.log(props);
|
|
23
|
+
|
|
24
|
+
const accordionItems = [
|
|
25
|
+
{
|
|
26
|
+
question: "Demo of the autosize feature",
|
|
27
|
+
answer:
|
|
28
|
+
"This is a demo of the autosize feature. The widget will automatically resize to fit the content, as supported by the OpenAI apps sdk https://developers.openai.com/apps-sdk/build/mcp-server/",
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<McpUseProvider debugger viewControls autoSize>
|
|
34
|
+
<AppsSDKUIProvider linkComponent={Link}>
|
|
35
|
+
<div className="relative bg-white dark:bg-black border border-gray-200 dark:border-gray-800 rounded-3xl">
|
|
36
|
+
<div className="p-8">
|
|
37
|
+
<h5 className="text-secondary mb-1">Apps SDK Template</h5>
|
|
38
|
+
<h2 className="heading-xl mb-3">Lovely Little Fruit Shop</h2>
|
|
39
|
+
<p className="text-md">
|
|
40
|
+
Start building your ChatGPT widget this this mcp-use template. It
|
|
41
|
+
features the openai apps sdk ui components, dark/light theme
|
|
42
|
+
support, actions like callTool and sendFollowUpMessage, and more.
|
|
43
|
+
</p>
|
|
44
|
+
</div>
|
|
45
|
+
<Carousel mcpUrl={mcp_url} />
|
|
46
|
+
<Accordion items={accordionItems} />
|
|
47
|
+
</div>
|
|
48
|
+
</AppsSDKUIProvider>
|
|
49
|
+
</McpUseProvider>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const ProductSearchResultWithProvider: React.FC = () => {
|
|
54
|
+
return (
|
|
55
|
+
<QueryClientProvider client={queryClient}>
|
|
56
|
+
<ProductSearchResult />
|
|
57
|
+
</QueryClientProvider>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export default ProductSearchResultWithProvider;
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
@import "tailwindcss";
|
|
2
|
+
@import "@openai/apps-sdk-ui/css";
|
|
3
|
+
|
|
4
|
+
/* Configure Tailwind v4 to scan for classes */
|
|
5
|
+
@source "../node_modules/@openai/apps-sdk-ui";
|
|
6
|
+
@source "./**/*.{ts,tsx,js,jsx,html}";
|
|
7
|
+
|
|
8
|
+
@theme {
|
|
9
|
+
/* Enable class-based dark mode */
|
|
10
|
+
--color-scheme: light dark;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@variant dark (&:where(.dark, .dark *));
|
|
2
14
|
|
|
3
15
|
/* Custom styles */
|
|
4
16
|
body {
|
|
@@ -9,3 +21,95 @@ body {
|
|
|
9
21
|
-webkit-font-smoothing: antialiased;
|
|
10
22
|
-moz-osx-font-smoothing: grayscale;
|
|
11
23
|
}
|
|
24
|
+
|
|
25
|
+
h1,
|
|
26
|
+
h2,
|
|
27
|
+
h3,
|
|
28
|
+
h4,
|
|
29
|
+
h5,
|
|
30
|
+
h6 {
|
|
31
|
+
@apply text-gray-900 dark:text-white;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
p {
|
|
35
|
+
@apply text-gray-600 dark:text-gray-400;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
a {
|
|
39
|
+
@apply text-blue-600 dark:text-blue-400;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Carousel scroll container with padding for blur effect */
|
|
43
|
+
.carousel-scroll-container {
|
|
44
|
+
padding-bottom: 2rem;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* Hover effect styles for carousel items */
|
|
48
|
+
.carousel-item {
|
|
49
|
+
position: relative;
|
|
50
|
+
container-type: size;
|
|
51
|
+
cursor: pointer;
|
|
52
|
+
transition-property: translate, scale;
|
|
53
|
+
transition-duration: 0.12s;
|
|
54
|
+
transition-timing-function: cubic-bezier(0.645, 0.045, 0.355, 1);
|
|
55
|
+
-webkit-tap-highlight-color: transparent;
|
|
56
|
+
overflow: hidden;
|
|
57
|
+
border-radius: 12px;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.carousel-item:active {
|
|
61
|
+
translate: 0 1px;
|
|
62
|
+
scale: 0.99;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.carousel-item-content {
|
|
66
|
+
position: relative;
|
|
67
|
+
z-index: 2;
|
|
68
|
+
width: 100%;
|
|
69
|
+
height: 100%;
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
justify-content: center;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.carousel-item-bg {
|
|
76
|
+
position: absolute;
|
|
77
|
+
inset: 0;
|
|
78
|
+
display: grid;
|
|
79
|
+
place-items: center;
|
|
80
|
+
transform: translateZ(0);
|
|
81
|
+
filter: blur(28px) saturate(5) brightness(1.3);
|
|
82
|
+
translate: calc(var(--pointer-x, -10) * 50cqi)
|
|
83
|
+
calc(var(--pointer-y, -10) * 50cqh);
|
|
84
|
+
scale: var(--icon-scale, 3.4);
|
|
85
|
+
opacity: var(--icon-opacity, 0.25);
|
|
86
|
+
will-change: transform, filter, opacity;
|
|
87
|
+
pointer-events: none;
|
|
88
|
+
z-index: 1;
|
|
89
|
+
transition:
|
|
90
|
+
opacity 0.26s ease-out,
|
|
91
|
+
scale 0.26s ease-out;
|
|
92
|
+
border-radius: inherit;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.carousel-item-bg img {
|
|
96
|
+
width: 100px;
|
|
97
|
+
user-select: none;
|
|
98
|
+
-webkit-user-drag: none;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.carousel-item::after {
|
|
102
|
+
content: "";
|
|
103
|
+
position: absolute;
|
|
104
|
+
pointer-events: none;
|
|
105
|
+
inset: 0px;
|
|
106
|
+
border-radius: inherit;
|
|
107
|
+
border: 3px solid transparent;
|
|
108
|
+
backdrop-filter: blur(0px) saturate(4.2) brightness(2.5) contrast(2.5);
|
|
109
|
+
mask:
|
|
110
|
+
linear-gradient(#fff 0 100%) border-box,
|
|
111
|
+
linear-gradient(#fff 0 100%) padding-box;
|
|
112
|
+
mask-composite: exclude;
|
|
113
|
+
z-index: 3;
|
|
114
|
+
transform: translateZ(0);
|
|
115
|
+
}
|
package/package.json
CHANGED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import { useWidget } from "mcp-use/react";
|
|
4
|
-
import "../styles.css";
|
|
5
|
-
|
|
6
|
-
/*
|
|
7
|
-
* Apps SDK widget
|
|
8
|
-
* Just export widgetMetadata with description and Zod schema, and mcp-use handles the rest!
|
|
9
|
-
* See docs: https://docs.mcp-use.com/typescript/server/ui-widgets
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
const propSchema = z.object({
|
|
13
|
-
city: z.string().describe("The city to display weather for"),
|
|
14
|
-
weather: z
|
|
15
|
-
.enum(["sunny", "rain", "snow", "cloudy"])
|
|
16
|
-
.describe("The weather condition"),
|
|
17
|
-
temperature: z
|
|
18
|
-
.number()
|
|
19
|
-
.min(-20)
|
|
20
|
-
.max(50)
|
|
21
|
-
.describe("The temperature in Celsius"),
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
export const widgetMetadata = {
|
|
25
|
-
description: "Display weather for a city",
|
|
26
|
-
inputs: propSchema,
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
type WeatherProps = z.infer<typeof propSchema>;
|
|
30
|
-
|
|
31
|
-
const WeatherWidget: React.FC = () => {
|
|
32
|
-
// Use the useWidget hook to get props from OpenAI Apps SDK
|
|
33
|
-
const { props, theme } = useWidget<WeatherProps>();
|
|
34
|
-
|
|
35
|
-
console.log(props);
|
|
36
|
-
|
|
37
|
-
const { city, weather, temperature } = props;
|
|
38
|
-
const getWeatherIcon = (weatherType: string) => {
|
|
39
|
-
switch (weatherType?.toLowerCase()) {
|
|
40
|
-
case "sunny":
|
|
41
|
-
return "☀️";
|
|
42
|
-
case "rain":
|
|
43
|
-
return "🌧️";
|
|
44
|
-
case "snow":
|
|
45
|
-
return "❄️";
|
|
46
|
-
case "cloudy":
|
|
47
|
-
return "☁️";
|
|
48
|
-
default:
|
|
49
|
-
return "🌤️";
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const getWeatherColor = (weatherType: string) => {
|
|
54
|
-
switch (weatherType?.toLowerCase()) {
|
|
55
|
-
case "sunny":
|
|
56
|
-
return "from-yellow-400 to-orange-500";
|
|
57
|
-
case "rain":
|
|
58
|
-
return "from-blue-400 to-blue-600";
|
|
59
|
-
case "snow":
|
|
60
|
-
return "from-blue-100 to-blue-300";
|
|
61
|
-
case "cloudy":
|
|
62
|
-
return "from-gray-400 to-gray-600";
|
|
63
|
-
default:
|
|
64
|
-
return "from-gray-300 to-gray-500";
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
// Theme-aware styling
|
|
69
|
-
const bgColor = theme === "dark" ? "bg-gray-900" : "bg-white";
|
|
70
|
-
const textColor = theme === "dark" ? "text-gray-100" : "text-gray-800";
|
|
71
|
-
const subtextColor = theme === "dark" ? "text-gray-400" : "text-gray-600";
|
|
72
|
-
|
|
73
|
-
return (
|
|
74
|
-
<div
|
|
75
|
-
className={`max-w-sm mx-auto ${bgColor} rounded-xl shadow-lg overflow-hidden`}
|
|
76
|
-
>
|
|
77
|
-
<div
|
|
78
|
-
className={`h-32 bg-gradient-to-br ${getWeatherColor(weather)} flex items-center justify-center`}
|
|
79
|
-
>
|
|
80
|
-
<div className="text-6xl">{getWeatherIcon(weather)}</div>
|
|
81
|
-
</div>
|
|
82
|
-
|
|
83
|
-
<div className="p-6">
|
|
84
|
-
<div className="text-center">
|
|
85
|
-
<h2 className={`text-2xl font-bold ${textColor} mb-2`}>{city}</h2>
|
|
86
|
-
<div className="flex items-center justify-center space-x-4">
|
|
87
|
-
<span className={`text-4xl font-light ${textColor}`}>
|
|
88
|
-
{temperature}°
|
|
89
|
-
</span>
|
|
90
|
-
<div className="text-right">
|
|
91
|
-
<p className={`text-lg font-medium ${subtextColor} capitalize`}>
|
|
92
|
-
{weather}
|
|
93
|
-
</p>
|
|
94
|
-
<p className={`text-sm ${subtextColor}`}>Current</p>
|
|
95
|
-
</div>
|
|
96
|
-
</div>
|
|
97
|
-
</div>
|
|
98
|
-
</div>
|
|
99
|
-
</div>
|
|
100
|
-
);
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
export default WeatherWidget;
|