create-manifest 1.3.4 → 2.0.0
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/README.md +40 -21
- package/index.js +51 -0
- package/package.json +11 -89
- package/starter/.claude/settings.local.json +21 -0
- package/starter/.env.example +1 -0
- package/starter/@/components/table.tsx +478 -0
- package/starter/@/components/ui/button.tsx +62 -0
- package/starter/@/components/ui/checkbox.tsx +30 -0
- package/starter/README-DEV.md +167 -0
- package/starter/components.json +24 -0
- package/starter/package.json +42 -0
- package/starter/src/flows/list-pokemons.flow.ts +131 -0
- package/starter/src/server.ts +165 -0
- package/starter/src/web/PokemonList.tsx +125 -0
- package/starter/src/web/components/blog-post-card.tsx +286 -0
- package/starter/src/web/components/blog-post-list.tsx +291 -0
- package/starter/src/web/components/ui/.gitkeep +0 -0
- package/starter/src/web/components/ui/button.tsx +62 -0
- package/starter/src/web/globals.css +98 -0
- package/starter/src/web/hooks/.gitkeep +0 -0
- package/starter/src/web/lib/utils.ts +6 -0
- package/starter/src/web/root.tsx +36 -0
- package/starter/src/web/tsconfig.json +3 -0
- package/starter/tsconfig.json +21 -0
- package/starter/tsconfig.web.json +24 -0
- package/starter/vite.config.ts +36 -0
- package/assets/monorepo/README.md +0 -52
- package/assets/monorepo/api-package.json +0 -9
- package/assets/monorepo/api-readme.md +0 -50
- package/assets/monorepo/manifest.yml +0 -34
- package/assets/monorepo/root-package.json +0 -15
- package/assets/monorepo/web-package.json +0 -10
- package/assets/monorepo/web-readme.md +0 -9
- package/assets/standalone/README.md +0 -50
- package/assets/standalone/api-package.json +0 -9
- package/assets/standalone/manifest.yml +0 -34
- package/bin/dev.cmd +0 -3
- package/bin/dev.js +0 -5
- package/bin/run.cmd +0 -3
- package/bin/run.js +0 -5
- package/dist/commands/index.d.ts +0 -65
- package/dist/commands/index.js +0 -480
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/utils/GetBackendFileContent.d.ts +0 -1
- package/dist/utils/GetBackendFileContent.js +0 -21
- package/dist/utils/GetLatestPackageVersion.d.ts +0 -1
- package/dist/utils/GetLatestPackageVersion.js +0 -5
- package/dist/utils/UpdateExtensionJsonFile.d.ts +0 -6
- package/dist/utils/UpdateExtensionJsonFile.js +0 -8
- package/dist/utils/UpdatePackageJsonFile.d.ts +0 -18
- package/dist/utils/UpdatePackageJsonFile.js +0 -21
- package/dist/utils/UpdateSettingsJsonFile.d.ts +0 -4
- package/dist/utils/UpdateSettingsJsonFile.js +0 -6
- package/dist/utils/helpers.d.ts +0 -1
- package/dist/utils/helpers.js +0 -11
- package/oclif.manifest.json +0 -47
|
@@ -0,0 +1,167 @@
|
|
|
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
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "new-york",
|
|
4
|
+
"rsc": false,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "src/web/globals.css",
|
|
9
|
+
"baseColor": "neutral",
|
|
10
|
+
"cssVariables": true,
|
|
11
|
+
"prefix": ""
|
|
12
|
+
},
|
|
13
|
+
"iconLibrary": "lucide",
|
|
14
|
+
"aliases": {
|
|
15
|
+
"components": "@/components",
|
|
16
|
+
"utils": "@/lib/utils",
|
|
17
|
+
"ui": "@/components/ui",
|
|
18
|
+
"lib": "@/lib",
|
|
19
|
+
"hooks": "@/hooks"
|
|
20
|
+
},
|
|
21
|
+
"registries": {
|
|
22
|
+
"@manifest": "https://ui.manifest.build/r/{name}.json"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "manfiest-starter",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "index.js",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "tsx src/server.ts",
|
|
8
|
+
"dev:watch": "tsx --watch src/server.ts",
|
|
9
|
+
"build": "tsc && vite build",
|
|
10
|
+
"start": "NODE_ENV=production node dist/server.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [],
|
|
13
|
+
"author": "MNFST, Inc.",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"description": "Manifest starter project for ChatGPT",
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@tailwindcss/vite": "^4.1.8",
|
|
18
|
+
"@types/express": "^5.0.6",
|
|
19
|
+
"@types/react": "^19.1.8",
|
|
20
|
+
"@types/react-dom": "^19.1.6",
|
|
21
|
+
"@vitejs/plugin-react": "^4.5.2",
|
|
22
|
+
"tailwindcss": "^4.1.8",
|
|
23
|
+
"tsx": "^4.21.0",
|
|
24
|
+
"typescript": "^5.9.3",
|
|
25
|
+
"vite": "^7.3.0"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
29
|
+
"@radix-ui/react-checkbox": "^1.3.3",
|
|
30
|
+
"@radix-ui/react-slot": "^1.2.4",
|
|
31
|
+
"class-variance-authority": "^0.7.1",
|
|
32
|
+
"clsx": "^2.1.1",
|
|
33
|
+
"dotenv": "^17.2.3",
|
|
34
|
+
"express": "^5.1.0",
|
|
35
|
+
"lucide-react": "^0.511.0",
|
|
36
|
+
"react": "^19.1.0",
|
|
37
|
+
"react-dom": "^19.1.0",
|
|
38
|
+
"tailwind-merge": "^3.3.1",
|
|
39
|
+
"vite-plugin-chatgpt-widgets": "^0.0.10",
|
|
40
|
+
"zod": "^3.25.76"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
|
+
import type { WidgetInfo } from 'vite-plugin-chatgpt-widgets'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
|
|
5
|
+
export interface Pokemon {
|
|
6
|
+
id: number
|
|
7
|
+
name: string
|
|
8
|
+
image: string
|
|
9
|
+
types: string[]
|
|
10
|
+
height: number
|
|
11
|
+
weight: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function fetchPokemons(limit: number = 12): Promise<Pokemon[]> {
|
|
15
|
+
const response = await fetch(
|
|
16
|
+
`https://pokeapi.co/api/v2/pokemon?limit=${limit}`
|
|
17
|
+
)
|
|
18
|
+
const data = await response.json()
|
|
19
|
+
|
|
20
|
+
const pokemons = await Promise.all(
|
|
21
|
+
data.results.map(async (pokemon: { name: string; url: string }) => {
|
|
22
|
+
const detailResponse = await fetch(pokemon.url)
|
|
23
|
+
const detail = await detailResponse.json()
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
id: detail.id,
|
|
27
|
+
name: detail.name,
|
|
28
|
+
image:
|
|
29
|
+
detail.sprites.other['official-artwork'].front_default ||
|
|
30
|
+
detail.sprites.front_default,
|
|
31
|
+
types: detail.types.map(
|
|
32
|
+
(t: { type: { name: string } }) => t.type.name
|
|
33
|
+
),
|
|
34
|
+
height: detail.height,
|
|
35
|
+
weight: detail.weight
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
return pokemons
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Registers the Pokemon List flow with the MCP server.
|
|
45
|
+
*/
|
|
46
|
+
export function registerPokemonFlow(
|
|
47
|
+
server: McpServer,
|
|
48
|
+
widgets: WidgetInfo[]
|
|
49
|
+
): void {
|
|
50
|
+
const uiVersion = 'v1'
|
|
51
|
+
const resourceUri = `ui://pokemon-list.html?${uiVersion}`
|
|
52
|
+
|
|
53
|
+
// Find the PokemonList widget
|
|
54
|
+
const pokemonWidget = widgets.find((w) => w.name === 'PokemonList')
|
|
55
|
+
|
|
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
|
+
}))
|
|
71
|
+
|
|
72
|
+
server.registerTool(
|
|
73
|
+
'listPokemons',
|
|
74
|
+
{
|
|
75
|
+
title: 'Pokemon List',
|
|
76
|
+
description: `Display a list of Pokemon in an interactive carousel.
|
|
77
|
+
|
|
78
|
+
WHEN TO USE:
|
|
79
|
+
- When the user asks to see a list of Pokemon
|
|
80
|
+
- When the user wants to browse Pokemon
|
|
81
|
+
- When the user asks "show me some Pokemon"
|
|
82
|
+
|
|
83
|
+
The tool fetches Pokemon data from the PokeAPI and displays them in a beautiful carousel format.`,
|
|
84
|
+
inputSchema: z.object({
|
|
85
|
+
limit: z
|
|
86
|
+
.number()
|
|
87
|
+
.min(1)
|
|
88
|
+
.max(50)
|
|
89
|
+
.optional()
|
|
90
|
+
.default(12)
|
|
91
|
+
.describe('Number of Pokemon to fetch (1-50, default: 12)')
|
|
92
|
+
}),
|
|
93
|
+
_meta: {
|
|
94
|
+
'openai/outputTemplate': resourceUri,
|
|
95
|
+
'openai/toolInvocation/invoking': 'Fetching Pokemon...',
|
|
96
|
+
'openai/toolInvocation/invoked': 'Pokemon list ready!'
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
async (args: { limit?: number }) => {
|
|
100
|
+
console.log('listPokemons called with args:', JSON.stringify(args))
|
|
101
|
+
const limit = args.limit ?? 12
|
|
102
|
+
console.log('Using limit:', limit)
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const pokemons = await fetchPokemons(limit)
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
content: [
|
|
109
|
+
{
|
|
110
|
+
type: 'text' as const,
|
|
111
|
+
text: `Found ${pokemons.length} Pokemon! Browse through the carousel to see them all.`
|
|
112
|
+
}
|
|
113
|
+
],
|
|
114
|
+
structuredContent: {
|
|
115
|
+
pokemons
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
return {
|
|
120
|
+
content: [
|
|
121
|
+
{
|
|
122
|
+
type: 'text' as const,
|
|
123
|
+
text: `Failed to fetch Pokemon: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
124
|
+
}
|
|
125
|
+
],
|
|
126
|
+
isError: true
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import 'dotenv/config'
|
|
2
|
+
import type { IncomingMessage } from 'node:http'
|
|
3
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
4
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
|
|
5
|
+
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
|
|
6
|
+
import express from 'express'
|
|
7
|
+
import { getWidgets, type WidgetInfo } from 'vite-plugin-chatgpt-widgets'
|
|
8
|
+
import type { ViteDevServer } from 'vite'
|
|
9
|
+
import { registerPokemonFlow } from './flows/list-pokemons.flow.js'
|
|
10
|
+
|
|
11
|
+
const isDev = process.env.NODE_ENV !== 'production'
|
|
12
|
+
const port = Number(process.env.PORT) || 3000
|
|
13
|
+
|
|
14
|
+
// Session storage
|
|
15
|
+
interface SessionData {
|
|
16
|
+
transport: StreamableHTTPServerTransport
|
|
17
|
+
}
|
|
18
|
+
const sessions = new Map<string, SessionData>()
|
|
19
|
+
|
|
20
|
+
// Widget storage - populated at startup
|
|
21
|
+
let widgets: WidgetInfo[] = []
|
|
22
|
+
|
|
23
|
+
function createServer() {
|
|
24
|
+
const server = new McpServer({
|
|
25
|
+
name: 'Manifest Starter',
|
|
26
|
+
version: '0.0.1'
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
registerPokemonFlow(server, widgets)
|
|
30
|
+
|
|
31
|
+
return server
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function main() {
|
|
35
|
+
const app = express()
|
|
36
|
+
|
|
37
|
+
// Create Vite dev server in development mode
|
|
38
|
+
let viteDevServer: ViteDevServer | null = null
|
|
39
|
+
|
|
40
|
+
if (isDev) {
|
|
41
|
+
const { createServer } = await import('vite')
|
|
42
|
+
viteDevServer = await createServer({
|
|
43
|
+
server: { middlewareMode: true },
|
|
44
|
+
appType: 'custom'
|
|
45
|
+
})
|
|
46
|
+
// Use Vite's middleware for HMR and asset serving
|
|
47
|
+
app.use(viteDevServer.middlewares)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Load widgets
|
|
51
|
+
if (isDev && viteDevServer) {
|
|
52
|
+
widgets = await getWidgets('src/web', { devServer: viteDevServer })
|
|
53
|
+
console.log(`Loaded ${widgets.length} widget(s) in development mode`)
|
|
54
|
+
} 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`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Log available widgets
|
|
62
|
+
for (const widget of widgets) {
|
|
63
|
+
console.log(` - ${widget.name} (${widget.source})`)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
app.use(express.json())
|
|
67
|
+
|
|
68
|
+
// CORS headers for all responses
|
|
69
|
+
app.use((_req, res, next) => {
|
|
70
|
+
res.header('Access-Control-Allow-Origin', '*')
|
|
71
|
+
res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS')
|
|
72
|
+
res.header(
|
|
73
|
+
'Access-Control-Allow-Headers',
|
|
74
|
+
'Content-Type, Accept, Authorization, mcp-session-id'
|
|
75
|
+
)
|
|
76
|
+
res.header(
|
|
77
|
+
'Access-Control-Expose-Headers',
|
|
78
|
+
'mcp-session-id, WWW-Authenticate'
|
|
79
|
+
)
|
|
80
|
+
next()
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// Handle CORS preflight
|
|
84
|
+
app.options('/mcp', (_req, res) => {
|
|
85
|
+
res.sendStatus(204)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
app.post('/mcp', async (req, res, next) => {
|
|
89
|
+
const sessionId = req.headers['mcp-session-id'] as string | undefined
|
|
90
|
+
|
|
91
|
+
// Existing session
|
|
92
|
+
if (sessionId && sessions.has(sessionId)) {
|
|
93
|
+
const session = sessions.get(sessionId)!
|
|
94
|
+
await session.transport
|
|
95
|
+
.handleRequest(req as unknown as IncomingMessage, res, req.body)
|
|
96
|
+
.catch(next)
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// New session - create transport and server
|
|
101
|
+
const transport = new StreamableHTTPServerTransport({
|
|
102
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
103
|
+
enableJsonResponse: true
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const server = createServer()
|
|
107
|
+
|
|
108
|
+
transport.onclose = () => {
|
|
109
|
+
const sid = (transport as unknown as { sessionId?: string }).sessionId
|
|
110
|
+
if (sid) sessions.delete(sid)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await server.connect(transport as Transport)
|
|
114
|
+
await transport
|
|
115
|
+
.handleRequest(req as unknown as IncomingMessage, res, req.body)
|
|
116
|
+
.catch(next)
|
|
117
|
+
|
|
118
|
+
// Store session after successful initialization
|
|
119
|
+
const newSessionId = (transport as unknown as { sessionId?: string })
|
|
120
|
+
.sessionId
|
|
121
|
+
if (newSessionId) {
|
|
122
|
+
sessions.set(newSessionId, { transport })
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// Handle GET for SSE streams
|
|
127
|
+
app.get('/mcp', async (req, res, next) => {
|
|
128
|
+
const sessionId = req.headers['mcp-session-id'] as string | undefined
|
|
129
|
+
|
|
130
|
+
if (!sessionId || !sessions.has(sessionId)) {
|
|
131
|
+
res.status(400).json({ error: 'Invalid or missing session ID' })
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const session = sessions.get(sessionId)!
|
|
136
|
+
await session.transport
|
|
137
|
+
.handleRequest(req as unknown as IncomingMessage, res)
|
|
138
|
+
.catch(next)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
// Handle DELETE for session cleanup
|
|
142
|
+
app.delete('/mcp', async (req, res) => {
|
|
143
|
+
const sessionId = req.headers['mcp-session-id'] as string | undefined
|
|
144
|
+
|
|
145
|
+
if (sessionId && sessions.has(sessionId)) {
|
|
146
|
+
const session = sessions.get(sessionId)!
|
|
147
|
+
await session.transport.close()
|
|
148
|
+
sessions.delete(sessionId)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
res.status(204).end()
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
app.listen(port, '0.0.0.0', () => {
|
|
155
|
+
console.log(`MCP server listening on http://localhost:${port}/mcp`)
|
|
156
|
+
if (isDev) {
|
|
157
|
+
console.log('Vite HMR enabled - widget changes will hot reload')
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
main().catch((err) => {
|
|
163
|
+
console.error('Failed to start server:', err)
|
|
164
|
+
process.exit(1)
|
|
165
|
+
})
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import { BlogPostList } from '@/components/blog-post-list'
|
|
3
|
+
import type { BlogPost } from '@/components/blog-post-card'
|
|
4
|
+
|
|
5
|
+
interface Pokemon {
|
|
6
|
+
id: number
|
|
7
|
+
name: string
|
|
8
|
+
image: string
|
|
9
|
+
types: string[]
|
|
10
|
+
height: number
|
|
11
|
+
weight: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface StructuredContent {
|
|
15
|
+
pokemons: Pokemon[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function pokemonToBlogPost(pokemon: Pokemon): BlogPost {
|
|
19
|
+
const capitalizedName =
|
|
20
|
+
pokemon.name.charAt(0).toUpperCase() + pokemon.name.slice(1)
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
id: String(pokemon.id),
|
|
24
|
+
title: capitalizedName,
|
|
25
|
+
excerpt: `Height: ${pokemon.height / 10}m | Weight: ${pokemon.weight / 10}kg`,
|
|
26
|
+
coverImage: pokemon.image,
|
|
27
|
+
author: {
|
|
28
|
+
name: `#${String(pokemon.id).padStart(3, '0')}`,
|
|
29
|
+
avatar: pokemon.image
|
|
30
|
+
},
|
|
31
|
+
publishedAt: new Date().toISOString(),
|
|
32
|
+
readTime: pokemon.types.join(', '),
|
|
33
|
+
tags: pokemon.types.map((t) => t.charAt(0).toUpperCase() + t.slice(1)),
|
|
34
|
+
category: pokemon.types[0]
|
|
35
|
+
? pokemon.types[0].charAt(0).toUpperCase() + pokemon.types[0].slice(1)
|
|
36
|
+
: 'Normal'
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default function PokemonList() {
|
|
41
|
+
const [posts, setPosts] = useState<BlogPost[]>([])
|
|
42
|
+
const [loading, setLoading] = useState(true)
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
console.log('window.openai:', window.openai)
|
|
46
|
+
console.log('structuredContent:', window.openai?.content?.structuredContent)
|
|
47
|
+
|
|
48
|
+
// Get structured content from OpenAI
|
|
49
|
+
if (window.openai?.content?.structuredContent) {
|
|
50
|
+
const content = window.openai.content
|
|
51
|
+
.structuredContent as StructuredContent
|
|
52
|
+
console.log('Using structuredContent, pokemons count:', content.pokemons?.length)
|
|
53
|
+
if (content.pokemons) {
|
|
54
|
+
setPosts(content.pokemons.map(pokemonToBlogPost))
|
|
55
|
+
}
|
|
56
|
+
setLoading(false)
|
|
57
|
+
} else {
|
|
58
|
+
// Fallback: fetch directly if not in OpenAI context
|
|
59
|
+
console.log('No structuredContent, falling back to direct fetch')
|
|
60
|
+
fetchPokemons()
|
|
61
|
+
}
|
|
62
|
+
}, [])
|
|
63
|
+
|
|
64
|
+
async function fetchPokemons() {
|
|
65
|
+
try {
|
|
66
|
+
const response = await fetch(
|
|
67
|
+
'https://pokeapi.co/api/v2/pokemon?limit=12'
|
|
68
|
+
)
|
|
69
|
+
const data = await response.json()
|
|
70
|
+
|
|
71
|
+
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()
|
|
76
|
+
|
|
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
|
+
}
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
setPosts(pokemons.map(pokemonToBlogPost))
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error('Failed to fetch Pokemon:', error)
|
|
96
|
+
} finally {
|
|
97
|
+
setLoading(false)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const handleReadMore = (post: BlogPost) => {
|
|
102
|
+
const pokemonName = post.title.toLowerCase()
|
|
103
|
+
window.openai?.sendFollowUpMessage?.(`Tell me more about ${pokemonName}`)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (loading) {
|
|
107
|
+
return (
|
|
108
|
+
<div className="flex items-center justify-center p-8">
|
|
109
|
+
<div className="text-muted-foreground">Loading Pokemon...</div>
|
|
110
|
+
</div>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div className="p-4">
|
|
116
|
+
<BlogPostList
|
|
117
|
+
posts={posts}
|
|
118
|
+
variant="carousel"
|
|
119
|
+
showAuthor={true}
|
|
120
|
+
showCategory={true}
|
|
121
|
+
onReadMore={handleReadMore}
|
|
122
|
+
/>
|
|
123
|
+
</div>
|
|
124
|
+
)
|
|
125
|
+
}
|