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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-manifest",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "Create a new Manifest project",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,7 +15,9 @@
15
15
  "WebFetch(domain:dev.to)",
16
16
  "WebFetch(domain:www.skybridge.tech)",
17
17
  "WebFetch(domain:gadget.dev)",
18
- "Bash(curl:*)"
18
+ "Bash(curl:*)",
19
+ "Bash(pkill:*)",
20
+ "Bash(npm run dev:*)"
19
21
  ]
20
22
  }
21
23
  }
@@ -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 { WidgetInfo } from 'vite-plugin-chatgpt-widgets'
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
- widgets: WidgetInfo[]
48
+ viteHandle: ViteHandle
49
49
  ): void {
50
50
  const uiVersion = 'v1'
51
51
  const resourceUri = `ui://pokemon-list.html?${uiVersion}`
52
52
 
53
- // Find the PokemonList widget
54
- const pokemonWidget = widgets.find((w) => w.name === 'PokemonList')
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
- if (!pokemonWidget) {
57
- console.warn('PokemonList widget not found - skipping flow registration')
58
- return
59
- }
60
-
61
- server.registerResource('pokemon-list', resourceUri, {}, async () => ({
62
- contents: [
63
- {
64
- uri: resourceUri,
65
- mimeType: 'text/html+skybridge',
66
- text: pokemonWidget.content,
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',
@@ -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 { getWidgets, type WidgetInfo } from 'vite-plugin-chatgpt-widgets'
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
- // Widget storage - populated at startup
21
- let widgets: WidgetInfo[] = []
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
- registerPokemonFlow(server, widgets)
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
- // Load widgets
55
+ // Set vite handle for dynamic widget content
51
56
  if (isDev && viteDevServer) {
52
- widgets = await getWidgets('src/web', { devServer: viteDevServer })
53
- console.log(`Loaded ${widgets.length} widget(s) in development mode`)
57
+ viteHandle = { devServer: viteDevServer }
58
+ console.log('Vite dev server ready - widgets will be served dynamically')
54
59
  } else {
55
- widgets = await getWidgets('src/web', {
56
- manifestPath: 'dist/web/.vite/manifest.json'
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('Using structuredContent, pokemons count:', content.pokemons?.length)
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
- async (pokemon: { name: string; url: string }) => {
74
- const detailResponse = await fetch(pokemon.url)
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
- return {
78
- id: detail.id,
79
- name: detail.name,
80
- image:
81
- detail.sprites.other['official-artwork'].front_default ||
82
- detail.sprites.front_default,
83
- types: detail.types.map(
84
- (t: { type: { name: string } }) => t.type.name
85
- ),
86
- height: detail.height,
87
- weight: detail.weight
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">{formatDate(post.publishedAt)}</p>
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
+ }
@@ -16,6 +16,10 @@
16
16
  "verbatimModuleSyntax": true,
17
17
  "isolatedModules": true,
18
18
  "moduleDetection": "force",
19
- "skipLibCheck": true
19
+ "skipLibCheck": true,
20
+ "baseUrl": ".",
21
+ "paths": {
22
+ "@/*": ["./src/web/*"]
23
+ }
20
24
  }
21
25
  }
@@ -31,6 +31,7 @@ export default defineConfig({
31
31
  server: {
32
32
  cors: {
33
33
  origin: true
34
- }
34
+ },
35
+ allowedHosts: ['.ngrok-free.dev', '.ngrok.io']
35
36
  }
36
37
  })
@@ -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 }
@@ -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