create-manifest 2.0.0 → 2.0.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/package.json +1 -1
- package/starter/.claude/settings.local.json +3 -1
- package/starter/README.md +26 -0
- package/starter/src/flows/list-pokemons.flow.ts +17 -19
- package/starter/src/server.ts +15 -11
- package/starter/src/web/PokemonList.tsx +22 -21
- package/starter/src/web/components/blog-post-card.tsx +3 -1
- package/starter/src/web/components/payment-methods.tsx +201 -0
- package/starter/tsconfig.json +5 -1
- package/starter/vite.config.ts +2 -1
- package/starter/@/components/ui/button.tsx +0 -62
- package/starter/README-DEV.md +0 -167
- /package/starter/{@ → src/web}/components/table.tsx +0 -0
- /package/starter/{@ → src/web}/components/ui/checkbox.tsx +0 -0
package/package.json
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Manifest Starter
|
|
2
|
+
|
|
3
|
+
## Development
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm run dev
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Exposing with ngrok
|
|
10
|
+
|
|
11
|
+
To test widgets in ChatGPT, you need to expose your local server via ngrok:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
ngrok http 3000
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The Vite config already allows `.ngrok-free.dev` and `.ngrok.io` hosts in `server.allowedHosts`.
|
|
18
|
+
|
|
19
|
+
Update `baseUrl` in `vite.config.ts` with your ngrok URL:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
chatGPTWidgetPlugin({
|
|
23
|
+
widgetsDir: 'src/web',
|
|
24
|
+
baseUrl: 'https://your-subdomain.ngrok-free.dev'
|
|
25
|
+
})
|
|
26
|
+
```
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
|
-
import type
|
|
2
|
+
import { getWidgetHTML, type ViteHandle } from 'vite-plugin-chatgpt-widgets'
|
|
3
3
|
import { z } from 'zod'
|
|
4
4
|
|
|
5
5
|
export interface Pokemon {
|
|
@@ -45,29 +45,27 @@ async function fetchPokemons(limit: number = 12): Promise<Pokemon[]> {
|
|
|
45
45
|
*/
|
|
46
46
|
export function registerPokemonFlow(
|
|
47
47
|
server: McpServer,
|
|
48
|
-
|
|
48
|
+
viteHandle: ViteHandle
|
|
49
49
|
): void {
|
|
50
50
|
const uiVersion = 'v1'
|
|
51
51
|
const resourceUri = `ui://pokemon-list.html?${uiVersion}`
|
|
52
52
|
|
|
53
|
-
//
|
|
54
|
-
|
|
53
|
+
// Register resource that fetches fresh widget content on each request
|
|
54
|
+
server.registerResource('pokemon-list', resourceUri, {}, async () => {
|
|
55
|
+
// Fetch fresh widget HTML from Vite (enables HMR in dev mode)
|
|
56
|
+
const { content } = await getWidgetHTML('PokemonList', viteHandle)
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
_meta: { 'openai/widgetPrefersBorder': false }
|
|
68
|
-
}
|
|
69
|
-
]
|
|
70
|
-
}))
|
|
58
|
+
return {
|
|
59
|
+
contents: [
|
|
60
|
+
{
|
|
61
|
+
uri: resourceUri,
|
|
62
|
+
mimeType: 'text/html+skybridge',
|
|
63
|
+
text: content,
|
|
64
|
+
_meta: { 'openai/widgetPrefersBorder': false }
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
})
|
|
71
69
|
|
|
72
70
|
server.registerTool(
|
|
73
71
|
'listPokemons',
|
package/starter/src/server.ts
CHANGED
|
@@ -4,7 +4,11 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
|
4
4
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
|
|
5
5
|
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
|
|
6
6
|
import express from 'express'
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
getWidgets,
|
|
9
|
+
getWidgetHTML,
|
|
10
|
+
type ViteHandle
|
|
11
|
+
} from 'vite-plugin-chatgpt-widgets'
|
|
8
12
|
import type { ViteDevServer } from 'vite'
|
|
9
13
|
import { registerPokemonFlow } from './flows/list-pokemons.flow.js'
|
|
10
14
|
|
|
@@ -17,8 +21,8 @@ interface SessionData {
|
|
|
17
21
|
}
|
|
18
22
|
const sessions = new Map<string, SessionData>()
|
|
19
23
|
|
|
20
|
-
//
|
|
21
|
-
let
|
|
24
|
+
// Vite handle for dynamic widget content
|
|
25
|
+
let viteHandle: ViteHandle
|
|
22
26
|
|
|
23
27
|
function createServer() {
|
|
24
28
|
const server = new McpServer({
|
|
@@ -26,7 +30,8 @@ function createServer() {
|
|
|
26
30
|
version: '0.0.1'
|
|
27
31
|
})
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
// Pass viteHandle so flows can fetch fresh widget content
|
|
34
|
+
registerPokemonFlow(server, viteHandle)
|
|
30
35
|
|
|
31
36
|
return server
|
|
32
37
|
}
|
|
@@ -47,18 +52,17 @@ async function main() {
|
|
|
47
52
|
app.use(viteDevServer.middlewares)
|
|
48
53
|
}
|
|
49
54
|
|
|
50
|
-
//
|
|
55
|
+
// Set vite handle for dynamic widget content
|
|
51
56
|
if (isDev && viteDevServer) {
|
|
52
|
-
|
|
53
|
-
console.log(
|
|
57
|
+
viteHandle = { devServer: viteDevServer }
|
|
58
|
+
console.log('Vite dev server ready - widgets will be served dynamically')
|
|
54
59
|
} else {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
})
|
|
58
|
-
console.log(`Loaded ${widgets.length} widget(s) from production build`)
|
|
60
|
+
viteHandle = { manifestPath: 'dist/web/.vite/manifest.json' }
|
|
61
|
+
console.log('Using production manifest for widgets')
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
// Log available widgets
|
|
65
|
+
const widgets = await getWidgets('src/web', viteHandle)
|
|
62
66
|
for (const widget of widgets) {
|
|
63
67
|
console.log(` - ${widget.name} (${widget.source})`)
|
|
64
68
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react'
|
|
2
2
|
import { BlogPostList } from '@/components/blog-post-list'
|
|
3
3
|
import type { BlogPost } from '@/components/blog-post-card'
|
|
4
|
+
import { PaymentMethods } from './components/payment-methods'
|
|
4
5
|
|
|
5
6
|
interface Pokemon {
|
|
6
7
|
id: number
|
|
@@ -49,7 +50,10 @@ export default function PokemonList() {
|
|
|
49
50
|
if (window.openai?.content?.structuredContent) {
|
|
50
51
|
const content = window.openai.content
|
|
51
52
|
.structuredContent as StructuredContent
|
|
52
|
-
console.log(
|
|
53
|
+
console.log(
|
|
54
|
+
'Using structuredContent, pokemons count:',
|
|
55
|
+
content.pokemons?.length
|
|
56
|
+
)
|
|
53
57
|
if (content.pokemons) {
|
|
54
58
|
setPosts(content.pokemons.map(pokemonToBlogPost))
|
|
55
59
|
}
|
|
@@ -63,31 +67,27 @@ export default function PokemonList() {
|
|
|
63
67
|
|
|
64
68
|
async function fetchPokemons() {
|
|
65
69
|
try {
|
|
66
|
-
const response = await fetch(
|
|
67
|
-
'https://pokeapi.co/api/v2/pokemon?limit=12'
|
|
68
|
-
)
|
|
70
|
+
const response = await fetch('https://pokeapi.co/api/v2/pokemon?limit=12')
|
|
69
71
|
const data = await response.json()
|
|
70
72
|
|
|
71
73
|
const pokemons = await Promise.all(
|
|
72
|
-
data.results.map(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const detail = await detailResponse.json()
|
|
74
|
+
data.results.map(async (pokemon: { name: string; url: string }) => {
|
|
75
|
+
const detailResponse = await fetch(pokemon.url)
|
|
76
|
+
const detail = await detailResponse.json()
|
|
76
77
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
78
|
+
return {
|
|
79
|
+
id: detail.id,
|
|
80
|
+
name: detail.name,
|
|
81
|
+
image:
|
|
82
|
+
detail.sprites.other['official-artwork'].front_default ||
|
|
83
|
+
detail.sprites.front_default,
|
|
84
|
+
types: detail.types.map(
|
|
85
|
+
(t: { type: { name: string } }) => t.type.name
|
|
86
|
+
),
|
|
87
|
+
height: detail.height,
|
|
88
|
+
weight: detail.weight
|
|
89
89
|
}
|
|
90
|
-
)
|
|
90
|
+
})
|
|
91
91
|
)
|
|
92
92
|
|
|
93
93
|
setPosts(pokemons.map(pokemonToBlogPost))
|
|
@@ -113,6 +113,7 @@ export default function PokemonList() {
|
|
|
113
113
|
|
|
114
114
|
return (
|
|
115
115
|
<div className="p-4">
|
|
116
|
+
<PaymentMethods />
|
|
116
117
|
<BlogPostList
|
|
117
118
|
posts={posts}
|
|
118
119
|
variant="carousel"
|
|
@@ -109,7 +109,9 @@ export function BlogPostCard({
|
|
|
109
109
|
)}
|
|
110
110
|
<div className="text-xs">
|
|
111
111
|
<p className="font-medium">{post.author.name}</p>
|
|
112
|
-
<p className="text-white/60">
|
|
112
|
+
<p className="text-white/60">
|
|
113
|
+
{formatDate(post.publishedAt)}
|
|
114
|
+
</p>
|
|
113
115
|
</div>
|
|
114
116
|
</div>
|
|
115
117
|
)}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { Button } from '@/components/ui/button'
|
|
2
|
+
import { cn } from '@/lib/utils'
|
|
3
|
+
import { CreditCard, Lock, Plus } from 'lucide-react'
|
|
4
|
+
import { useState } from 'react'
|
|
5
|
+
|
|
6
|
+
export interface PaymentMethod {
|
|
7
|
+
id: string
|
|
8
|
+
type: 'card' | 'apple_pay' | 'google_pay' | 'paypal'
|
|
9
|
+
brand?: 'visa' | 'mastercard' | 'amex' | 'cb'
|
|
10
|
+
last4?: string
|
|
11
|
+
isDefault?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PaymentMethodsProps {
|
|
15
|
+
methods?: PaymentMethod[]
|
|
16
|
+
amount?: number
|
|
17
|
+
currency?: string
|
|
18
|
+
selectedMethodId?: string
|
|
19
|
+
onSelectMethod?: (methodId: string) => void
|
|
20
|
+
onAddCard?: () => void
|
|
21
|
+
onPay?: (methodId: string) => void
|
|
22
|
+
isLoading?: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const defaultMethods: PaymentMethod[] = [
|
|
26
|
+
{ id: '1', type: 'card', brand: 'visa', last4: '4242' },
|
|
27
|
+
{
|
|
28
|
+
id: '2',
|
|
29
|
+
type: 'card',
|
|
30
|
+
brand: 'mastercard',
|
|
31
|
+
last4: '8888',
|
|
32
|
+
isDefault: true
|
|
33
|
+
},
|
|
34
|
+
{ id: '3', type: 'apple_pay' }
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
const BrandLogo = ({ brand }: { brand?: string }) => {
|
|
38
|
+
switch (brand) {
|
|
39
|
+
case 'visa':
|
|
40
|
+
return (
|
|
41
|
+
<svg viewBox="0 0 48 32" className="h-5 w-auto">
|
|
42
|
+
<rect width="48" height="32" rx="4" fill="#fff" stroke="#e5e5e5" />
|
|
43
|
+
<g transform="translate(5, 10) scale(0.15)">
|
|
44
|
+
<polygon points="116.145,95.719 97.858,95.719 109.296,24.995 127.582,24.995" fill="#00579f" />
|
|
45
|
+
<path d="M182.437,26.724c-3.607-1.431-9.328-3.011-16.402-3.011c-18.059,0-30.776,9.63-30.854,23.398c-0.15,10.158,9.105,15.8,16.027,19.187c7.075,3.461,9.48,5.72,9.48,8.805c-0.072,4.738-5.717,6.922-10.982,6.922c-7.301,0-11.213-1.126-17.158-3.762l-2.408-1.13l-2.559,15.876c4.289,1.954,12.191,3.688,20.395,3.764c19.188,0,31.68-9.481,31.828-24.153c0.073-8.051-4.814-14.22-15.35-19.261c-6.396-3.236-10.313-5.418-10.313-8.729c0.075-3.01,3.313-6.093,10.533-6.093c5.945-0.151,10.313,1.278,13.622,2.708l1.654,0.751l2.487-15.272z" fill="#00579f" />
|
|
46
|
+
<path d="M206.742,70.664c1.506-4.063,7.301-19.788,7.301-19.788c-0.076,0.151,1.503-4.138,2.406-6.771l1.278,6.094c0,0,3.463,16.929,4.215,20.465c-2.858,0-11.588,0-15.2,0zm22.573-45.669l-14.145,0c-4.362,0-7.676,1.278-9.558,5.868l-27.163,64.855l19.188,0c0,0,3.159-8.729,3.838-10.609c2.105,0,20.771,0,23.479,0c0.525,2.483,2.182,10.609,2.182,10.609l16.932,0l-14.753-70.723z" fill="#00579f" />
|
|
47
|
+
<path d="M82.584,24.995l-17.909,48.227l-1.957-9.781c-3.311-11.286-13.695-23.548-25.283-29.645l16.404,61.848l19.338,0l28.744-70.649l-19.337,0z" fill="#00579f" />
|
|
48
|
+
<path d="M48.045,24.995l-29.422,0l-0.301,1.429c22.951,5.869,38.151,20.016,44.396,37.02l-6.396-32.523c-1.053-4.517-4.289-5.796-8.277-5.926z" fill="#faa61a" />
|
|
49
|
+
</g>
|
|
50
|
+
</svg>
|
|
51
|
+
)
|
|
52
|
+
case 'mastercard':
|
|
53
|
+
return (
|
|
54
|
+
<svg viewBox="0 0 48 32" className="h-5 w-auto">
|
|
55
|
+
<rect width="48" height="32" rx="4" fill="#fff" stroke="#e5e5e5" />
|
|
56
|
+
<g transform="translate(7, 5) scale(0.22)">
|
|
57
|
+
<rect x="60.4" y="25.7" width="31.5" height="56.6" fill="#FF5F00" />
|
|
58
|
+
<path d="M62.4,54c0-11,5.1-21.5,13.7-28.3c-15.6-12.3-38.3-9.6-50.6,6.1C13.3,47.4,16,70,31.7,82.3c13.1,10.3,31.4,10.3,44.5,0C67.5,75.5,62.4,65,62.4,54z" fill="#EB001B" />
|
|
59
|
+
<path d="M134.4,54c0,19.9-16.1,36-36,36c-8.1,0-15.9-2.7-22.2-7.7c15.6-12.3,18.3-34.9,6-50.6c-1.8-2.2-3.8-4.3-6-6c15.6-12.3,38.3-9.6,50.5,6.1C131.7,38.1,134.4,45.9,134.4,54z" fill="#F79E1B" />
|
|
60
|
+
</g>
|
|
61
|
+
</svg>
|
|
62
|
+
)
|
|
63
|
+
case 'amex':
|
|
64
|
+
return (
|
|
65
|
+
<svg viewBox="0 0 48 32" className="h-5 w-auto">
|
|
66
|
+
<rect width="48" height="32" rx="4" fill="#006FCF" />
|
|
67
|
+
<path
|
|
68
|
+
d="M10 12h4l.8 2 .8-2h4v8h-3v-5l-1.3 3h-2l-1.3-3v5h-2v-8zm14 0h6v2h-3v1h3v2h-3v1h3v2h-6v-8zm8 0h3l2 3 2-3h3l-3.5 4 3.5 4h-3l-2-3-2 3h-3l3.5-4-3.5-4z"
|
|
69
|
+
fill="white"
|
|
70
|
+
/>
|
|
71
|
+
</svg>
|
|
72
|
+
)
|
|
73
|
+
case 'cb':
|
|
74
|
+
return (
|
|
75
|
+
<svg viewBox="0 0 48 32" className="h-5 w-auto">
|
|
76
|
+
<rect width="48" height="32" rx="4" fill="#1E4B9E" />
|
|
77
|
+
<rect x="4" y="10" width="18" height="12" rx="2" fill="#49A942" />
|
|
78
|
+
<text x="28" y="20" fill="white" fontSize="10" fontWeight="bold">
|
|
79
|
+
CB
|
|
80
|
+
</text>
|
|
81
|
+
</svg>
|
|
82
|
+
)
|
|
83
|
+
default:
|
|
84
|
+
return <CreditCard className="h-5 w-5 text-muted-foreground" />
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const MethodIcon = ({ method }: { method: PaymentMethod }) => {
|
|
89
|
+
if (method.type === 'apple_pay') {
|
|
90
|
+
return (
|
|
91
|
+
<div className="h-5 w-8 rounded bg-black flex items-center justify-center">
|
|
92
|
+
<img src="/images/apple-pay.svg" alt="Apple Pay" className="h-2 w-auto invert" />
|
|
93
|
+
</div>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
if (method.type === 'google_pay') {
|
|
97
|
+
return (
|
|
98
|
+
<svg viewBox="0 0 48 32" className="h-5 w-auto">
|
|
99
|
+
<rect width="48" height="32" rx="4" fill="#fff" stroke="#ddd" />
|
|
100
|
+
<text x="8" y="20" fontSize="10" fontWeight="500" fill="#5F6368">
|
|
101
|
+
G Pay
|
|
102
|
+
</text>
|
|
103
|
+
</svg>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
if (method.type === 'paypal') {
|
|
107
|
+
return (
|
|
108
|
+
<svg viewBox="0 0 48 32" className="h-5 w-auto">
|
|
109
|
+
<rect width="48" height="32" rx="4" fill="#003087" />
|
|
110
|
+
<text x="8" y="20" fontSize="9" fontWeight="bold" fill="#fff">
|
|
111
|
+
PayPal
|
|
112
|
+
</text>
|
|
113
|
+
</svg>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
return <BrandLogo brand={method.brand} />
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function PaymentMethods({
|
|
120
|
+
methods = defaultMethods,
|
|
121
|
+
amount = 279.0,
|
|
122
|
+
currency = 'EUR',
|
|
123
|
+
selectedMethodId,
|
|
124
|
+
onSelectMethod,
|
|
125
|
+
onAddCard,
|
|
126
|
+
onPay,
|
|
127
|
+
isLoading = false
|
|
128
|
+
}: PaymentMethodsProps) {
|
|
129
|
+
const [selected, setSelected] = useState(
|
|
130
|
+
selectedMethodId || methods.find((m) => m.isDefault)?.id || methods[0]?.id
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
const handleSelect = (methodId: string) => {
|
|
134
|
+
setSelected(methodId)
|
|
135
|
+
onSelectMethod?.(methodId)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const formatCurrency = (value: number) => {
|
|
139
|
+
return new Intl.NumberFormat('en-US', {
|
|
140
|
+
style: 'currency',
|
|
141
|
+
currency
|
|
142
|
+
}).format(value)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const getMethodLabel = (method: PaymentMethod) => {
|
|
146
|
+
if (method.type === 'apple_pay') return 'Apple Pay'
|
|
147
|
+
if (method.type === 'google_pay') return 'Google Pay'
|
|
148
|
+
if (method.type === 'paypal') return 'PayPal'
|
|
149
|
+
return `•••• ${method.last4}`
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div className="w-full rounded-md sm:rounded-lg bg-card p-2 space-y-4">
|
|
154
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
155
|
+
{methods.map((method) => (
|
|
156
|
+
<button
|
|
157
|
+
key={method.id}
|
|
158
|
+
onClick={() => handleSelect(method.id)}
|
|
159
|
+
className={cn(
|
|
160
|
+
'inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-sm transition-colors',
|
|
161
|
+
selected === method.id
|
|
162
|
+
? 'border-foreground ring-1 ring-foreground'
|
|
163
|
+
: 'border-border hover:border-foreground/50'
|
|
164
|
+
)}
|
|
165
|
+
>
|
|
166
|
+
<MethodIcon method={method} />
|
|
167
|
+
<span>{getMethodLabel(method)}</span>
|
|
168
|
+
{method.isDefault && (
|
|
169
|
+
<span className="rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground">
|
|
170
|
+
Default
|
|
171
|
+
</span>
|
|
172
|
+
)}
|
|
173
|
+
</button>
|
|
174
|
+
))}
|
|
175
|
+
<button
|
|
176
|
+
onClick={onAddCard}
|
|
177
|
+
className="inline-flex items-center gap-1.5 rounded-full border border-dashed border-border px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
178
|
+
>
|
|
179
|
+
<Plus className="h-4 w-4" />
|
|
180
|
+
Add
|
|
181
|
+
</button>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<div className="flex flex-col-reverse sm:flex-row sm:items-center sm:justify-between gap-2">
|
|
185
|
+
<span className="flex items-center justify-center sm:justify-start gap-1.5 text-xs text-muted-foreground">
|
|
186
|
+
<Lock className="h-3 w-3" />
|
|
187
|
+
Secure encrypted transaction
|
|
188
|
+
</span>
|
|
189
|
+
<Button
|
|
190
|
+
size="sm"
|
|
191
|
+
className="w-full sm:w-auto"
|
|
192
|
+
onClick={() => selected && onPay?.(selected)}
|
|
193
|
+
disabled={!selected || isLoading}
|
|
194
|
+
>
|
|
195
|
+
<Lock className="mr-1.5 h-3.5 w-3.5" />
|
|
196
|
+
{isLoading ? 'Processing...' : `Pay ${formatCurrency(amount)}`}
|
|
197
|
+
</Button>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
)
|
|
201
|
+
}
|
package/starter/tsconfig.json
CHANGED
package/starter/vite.config.ts
CHANGED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import * as React from "react"
|
|
2
|
-
import { Slot } from "@radix-ui/react-slot"
|
|
3
|
-
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
-
|
|
5
|
-
import { cn } from "@/lib/utils"
|
|
6
|
-
|
|
7
|
-
const buttonVariants = cva(
|
|
8
|
-
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
9
|
-
{
|
|
10
|
-
variants: {
|
|
11
|
-
variant: {
|
|
12
|
-
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
13
|
-
destructive:
|
|
14
|
-
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
15
|
-
outline:
|
|
16
|
-
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
|
17
|
-
secondary:
|
|
18
|
-
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
19
|
-
ghost:
|
|
20
|
-
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
|
21
|
-
link: "text-primary underline-offset-4 hover:underline",
|
|
22
|
-
},
|
|
23
|
-
size: {
|
|
24
|
-
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
25
|
-
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
|
26
|
-
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
|
27
|
-
icon: "size-9",
|
|
28
|
-
"icon-sm": "size-8",
|
|
29
|
-
"icon-lg": "size-10",
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
defaultVariants: {
|
|
33
|
-
variant: "default",
|
|
34
|
-
size: "default",
|
|
35
|
-
},
|
|
36
|
-
}
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
function Button({
|
|
40
|
-
className,
|
|
41
|
-
variant = "default",
|
|
42
|
-
size = "default",
|
|
43
|
-
asChild = false,
|
|
44
|
-
...props
|
|
45
|
-
}: React.ComponentProps<"button"> &
|
|
46
|
-
VariantProps<typeof buttonVariants> & {
|
|
47
|
-
asChild?: boolean
|
|
48
|
-
}) {
|
|
49
|
-
const Comp = asChild ? Slot : "button"
|
|
50
|
-
|
|
51
|
-
return (
|
|
52
|
-
<Comp
|
|
53
|
-
data-slot="button"
|
|
54
|
-
data-variant={variant}
|
|
55
|
-
data-size={size}
|
|
56
|
-
className={cn(buttonVariants({ variant, size, className }))}
|
|
57
|
-
{...props}
|
|
58
|
-
/>
|
|
59
|
-
)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export { Button, buttonVariants }
|
package/starter/README-DEV.md
DELETED
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
# Manifest MCP Server - Developer Guide
|
|
2
|
-
|
|
3
|
-
This is an MCP (Model Context Protocol) server template for building AI-powered flows.
|
|
4
|
-
|
|
5
|
-
## Project Structure
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
src/
|
|
9
|
-
├── server.ts # Express + MCP server setup
|
|
10
|
-
├── flows/ # MCP flows (tools, resources)
|
|
11
|
-
│ └── gameboy.flow.ts # Example flow
|
|
12
|
-
└── web/ # Web components (UI widgets)
|
|
13
|
-
└── gameboy-player/ # Example web component
|
|
14
|
-
├── gameboy-player.html
|
|
15
|
-
├── gameboy-player.ts
|
|
16
|
-
└── gameboy-player.css
|
|
17
|
-
scripts/
|
|
18
|
-
└── build-web.ts # Vite build script for web components
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## Getting Started
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
# Install dependencies
|
|
25
|
-
npm install
|
|
26
|
-
|
|
27
|
-
# Start development server (with hot reload)
|
|
28
|
-
npm run dev
|
|
29
|
-
|
|
30
|
-
# Build for production
|
|
31
|
-
npm run build
|
|
32
|
-
|
|
33
|
-
# Run production build
|
|
34
|
-
npm start
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## Environment Variables
|
|
38
|
-
|
|
39
|
-
Copy `.env.example` to `.env` and configure:
|
|
40
|
-
|
|
41
|
-
| Variable | Default | Description |
|
|
42
|
-
|----------|---------|-------------|
|
|
43
|
-
| `PORT` | `3000` | Server port |
|
|
44
|
-
|
|
45
|
-
## Creating Flows
|
|
46
|
-
|
|
47
|
-
Flows register tools and resources with the MCP server. See `src/flows/gameboy.flow.ts` for a complete example.
|
|
48
|
-
|
|
49
|
-
### Basic Flow Structure
|
|
50
|
-
|
|
51
|
-
```typescript
|
|
52
|
-
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
|
|
53
|
-
import { z } from "zod"
|
|
54
|
-
|
|
55
|
-
export function registerMyFlow(server: McpServer): void {
|
|
56
|
-
// Register a tool
|
|
57
|
-
server.registerTool(
|
|
58
|
-
"myTool",
|
|
59
|
-
{
|
|
60
|
-
title: "My Tool",
|
|
61
|
-
description: "Does something useful",
|
|
62
|
-
inputSchema: z.object({
|
|
63
|
-
input: z.string().describe("The input parameter")
|
|
64
|
-
})
|
|
65
|
-
},
|
|
66
|
-
async (args) => {
|
|
67
|
-
return {
|
|
68
|
-
content: [{ type: "text", text: `Result: ${args.input}` }]
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
// Register a resource (UI widget)
|
|
74
|
-
server.registerResource("my-widget", "ui://my-widget.html", {}, async () => ({
|
|
75
|
-
contents: [
|
|
76
|
-
{
|
|
77
|
-
uri: "ui://my-widget.html",
|
|
78
|
-
mimeType: "text/html+skybridge",
|
|
79
|
-
text: "<html>...</html>"
|
|
80
|
-
}
|
|
81
|
-
]
|
|
82
|
-
}))
|
|
83
|
-
}
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
### Registering Your Flow
|
|
87
|
-
|
|
88
|
-
Add your flow to `src/server.ts`:
|
|
89
|
-
|
|
90
|
-
```typescript
|
|
91
|
-
import { registerMyFlow } from "./flows/my.flow.js"
|
|
92
|
-
|
|
93
|
-
function createServer() {
|
|
94
|
-
const server = new McpServer({
|
|
95
|
-
name: "My MCP Server",
|
|
96
|
-
version: "0.0.1"
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
registerMyFlow(server)
|
|
100
|
-
|
|
101
|
-
return server
|
|
102
|
-
}
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
## Creating Web Components
|
|
106
|
-
|
|
107
|
-
Web components are built with Vite and bundled into single HTML files.
|
|
108
|
-
|
|
109
|
-
1. Create a folder in `src/web/` with your component files
|
|
110
|
-
2. Add the component to `scripts/build-web.ts`
|
|
111
|
-
3. Reference the built HTML in your flow
|
|
112
|
-
|
|
113
|
-
### Build Configuration
|
|
114
|
-
|
|
115
|
-
Edit `scripts/build-web.ts` to add new web components:
|
|
116
|
-
|
|
117
|
-
```typescript
|
|
118
|
-
const components = [
|
|
119
|
-
'gameboy-player',
|
|
120
|
-
'my-new-component' // Add your component here
|
|
121
|
-
]
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
## MCP Endpoints
|
|
125
|
-
|
|
126
|
-
The server exposes the following endpoints:
|
|
127
|
-
|
|
128
|
-
| Method | Endpoint | Description |
|
|
129
|
-
|--------|----------|-------------|
|
|
130
|
-
| `POST` | `/mcp` | Create session / Send messages |
|
|
131
|
-
| `GET` | `/mcp` | SSE stream for server events |
|
|
132
|
-
| `DELETE` | `/mcp` | Close session |
|
|
133
|
-
|
|
134
|
-
Sessions are managed via the `mcp-session-id` header.
|
|
135
|
-
|
|
136
|
-
## Development Tips
|
|
137
|
-
|
|
138
|
-
- The server uses Nodemon for hot reload during development
|
|
139
|
-
- Web components are rebuilt on file changes
|
|
140
|
-
- Use `console.log()` for debugging - output appears in the terminal
|
|
141
|
-
|
|
142
|
-
## Testing with ChatGPT
|
|
143
|
-
|
|
144
|
-
1. Deploy your server to a public URL (or use ngrok for local testing)
|
|
145
|
-
2. Add the MCP server URL in ChatGPT settings
|
|
146
|
-
3. Your tools will be available in the chat
|
|
147
|
-
|
|
148
|
-
## Available Scripts
|
|
149
|
-
|
|
150
|
-
| Script | Description |
|
|
151
|
-
|--------|-------------|
|
|
152
|
-
| `npm run dev` | Start dev server with hot reload |
|
|
153
|
-
| `npm run build` | Build TypeScript and web components |
|
|
154
|
-
| `npm run build:web` | Build only web components |
|
|
155
|
-
| `npm start` | Run production build |
|
|
156
|
-
|
|
157
|
-
## Dependencies
|
|
158
|
-
|
|
159
|
-
- `@modelcontextprotocol/sdk` - MCP protocol implementation
|
|
160
|
-
- `express` - HTTP server
|
|
161
|
-
- `zod` - Schema validation
|
|
162
|
-
- `dotenv` - Environment variables
|
|
163
|
-
- `vite` - Web component bundling
|
|
164
|
-
|
|
165
|
-
## License
|
|
166
|
-
|
|
167
|
-
MIT
|
|
File without changes
|
|
File without changes
|