create-manifest 1.3.4 → 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/README.md +40 -21
- package/index.js +51 -0
- package/package.json +11 -89
- package/starter/.claude/settings.local.json +23 -0
- package/starter/.env.example +1 -0
- package/starter/README.md +26 -0
- package/starter/components.json +24 -0
- package/starter/package.json +42 -0
- package/starter/src/flows/list-pokemons.flow.ts +129 -0
- package/starter/src/server.ts +169 -0
- package/starter/src/web/PokemonList.tsx +126 -0
- package/starter/src/web/components/blog-post-card.tsx +288 -0
- package/starter/src/web/components/blog-post-list.tsx +291 -0
- package/starter/src/web/components/payment-methods.tsx +201 -0
- package/starter/src/web/components/table.tsx +478 -0
- package/starter/src/web/components/ui/.gitkeep +0 -0
- package/starter/src/web/components/ui/button.tsx +62 -0
- package/starter/src/web/components/ui/checkbox.tsx +30 -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 +25 -0
- package/starter/tsconfig.web.json +24 -0
- package/starter/vite.config.ts +37 -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
package/README.md
CHANGED
|
@@ -1,38 +1,57 @@
|
|
|
1
1
|
# create-manifest
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Create a new Manifest MCP server project with a single command.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
4
6
|
|
|
5
7
|
```bash
|
|
6
|
-
npx create-manifest
|
|
8
|
+
npx create-manifest my-app
|
|
7
9
|
```
|
|
8
10
|
|
|
9
|
-
This will
|
|
10
|
-
If you leave out the name, the CLI will ask you for it during setup.
|
|
11
|
+
This will:
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
1. Create a new directory `my-app`
|
|
14
|
+
2. Copy the starter template
|
|
15
|
+
3. Install dependencies
|
|
16
|
+
4. Start the development server
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
- `--copilot` for **GitHub Copilot**
|
|
16
|
-
- `--windsurf` for **Windsurf**
|
|
18
|
+
## What's Included
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
The starter template includes:
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
- MCP server setup with Express
|
|
23
|
+
- TypeScript configuration
|
|
24
|
+
- Hot reload with Nodemon
|
|
25
|
+
- Vite for building web components
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
npm install
|
|
27
|
+
## Requirements
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
mkdir test-folder
|
|
27
|
-
cd test-folder
|
|
28
|
-
../bin/dev.js
|
|
29
|
-
```
|
|
29
|
+
- Node.js 22+
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
## After Creation
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
Your project will be running at `http://localhost:3000`.
|
|
34
|
+
|
|
35
|
+
To restart the dev server later:
|
|
34
36
|
|
|
35
37
|
```bash
|
|
36
|
-
|
|
37
|
-
npm
|
|
38
|
+
cd my-app
|
|
39
|
+
npm run dev
|
|
38
40
|
```
|
|
41
|
+
|
|
42
|
+
## Available Scripts
|
|
43
|
+
|
|
44
|
+
Inside the created project, you can run:
|
|
45
|
+
|
|
46
|
+
- `npm run dev` - Start development server with hot reload
|
|
47
|
+
- `npm run build` - Build for production
|
|
48
|
+
- `npm start` - Run production build
|
|
49
|
+
|
|
50
|
+
## Learn More
|
|
51
|
+
|
|
52
|
+
- [Manifest Documentation](https://manifest.build)
|
|
53
|
+
- [MCP Protocol](https://modelcontextprotocol.io)
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { cpSync, readFileSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { join, dirname } from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { execSync, spawn } from 'node:child_process'
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
|
|
10
|
+
const projectName = process.argv[2]
|
|
11
|
+
|
|
12
|
+
if (!projectName) {
|
|
13
|
+
console.error('Please provide a project name:')
|
|
14
|
+
console.error(' npx create-manifest my-app')
|
|
15
|
+
process.exit(1)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const targetDir = join(process.cwd(), projectName)
|
|
19
|
+
const starterDir = join(__dirname, 'starter')
|
|
20
|
+
|
|
21
|
+
console.log(`\nCreating a new Manifest project in ${targetDir}...\n`)
|
|
22
|
+
|
|
23
|
+
// Copy starter folder to target directory
|
|
24
|
+
cpSync(starterDir, targetDir, { recursive: true })
|
|
25
|
+
|
|
26
|
+
// Update package.json with the project name
|
|
27
|
+
const packageJsonPath = join(targetDir, 'package.json')
|
|
28
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
|
|
29
|
+
packageJson.name = projectName
|
|
30
|
+
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n')
|
|
31
|
+
|
|
32
|
+
console.log('Installing dependencies...\n')
|
|
33
|
+
|
|
34
|
+
// Install dependencies
|
|
35
|
+
execSync('npm install', {
|
|
36
|
+
cwd: targetDir,
|
|
37
|
+
stdio: 'inherit'
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
console.log('\nStarting development server...\n')
|
|
41
|
+
|
|
42
|
+
// Run dev script
|
|
43
|
+
const dev = spawn('npm', ['run', 'dev'], {
|
|
44
|
+
cwd: targetDir,
|
|
45
|
+
stdio: 'inherit',
|
|
46
|
+
shell: true
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
dev.on('close', (code) => {
|
|
50
|
+
process.exit(code)
|
|
51
|
+
})
|
package/package.json
CHANGED
|
@@ -1,99 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-manifest",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"homepage": "https://manifest.build",
|
|
7
|
-
"license": "MIT",
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "Create a new Manifest project",
|
|
5
|
+
"type": "module",
|
|
8
6
|
"bin": {
|
|
9
|
-
"create-manifest": "./
|
|
10
|
-
},
|
|
11
|
-
"scripts": {
|
|
12
|
-
"build": "shx rm -rf dist && tsc -b",
|
|
13
|
-
"watch": "tsc -b --watch",
|
|
14
|
-
"postpack": "shx rm -f oclif.manifest.json",
|
|
15
|
-
"posttest": "npm run lint",
|
|
16
|
-
"prepack": "npm run build && oclif manifest && oclif readme",
|
|
17
|
-
"prepare": "npm run build",
|
|
18
|
-
"version": "oclif readme && git add README.md"
|
|
19
|
-
},
|
|
20
|
-
"oclif": {
|
|
21
|
-
"bin": "create-manifest",
|
|
22
|
-
"dirname": "create-manifest",
|
|
23
|
-
"commands": {
|
|
24
|
-
"strategy": "single",
|
|
25
|
-
"target": "./dist/commands/index.js"
|
|
26
|
-
}
|
|
27
|
-
},
|
|
28
|
-
"plugins": [
|
|
29
|
-
"@oclif/plugin-help",
|
|
30
|
-
"@oclif/plugin-plugins"
|
|
31
|
-
],
|
|
32
|
-
"topicSeparator": " ",
|
|
33
|
-
"topics": {
|
|
34
|
-
"create": {
|
|
35
|
-
"description": "Create a new Manifest backend"
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
|
-
"dependencies": {
|
|
39
|
-
"@inquirer/prompts": "^7.5.1",
|
|
40
|
-
"@oclif/core": "^4",
|
|
41
|
-
"@oclif/plugin-help": "^6",
|
|
42
|
-
"@oclif/plugin-plugins": "^5",
|
|
43
|
-
"axios": "^1.9.0",
|
|
44
|
-
"chalk": "^5.4.1",
|
|
45
|
-
"fs": "^0.0.1-security",
|
|
46
|
-
"jsonc-parser": "^3.3.1",
|
|
47
|
-
"ora": "^8.2.0",
|
|
48
|
-
"path": "^0.12.7",
|
|
49
|
-
"tree-kill": "^1.2.2",
|
|
50
|
-
"url": "^0.11.4"
|
|
51
|
-
},
|
|
52
|
-
"devDependencies": {
|
|
53
|
-
"@oclif/prettier-config": "^0.2.1",
|
|
54
|
-
"@oclif/test": "^4",
|
|
55
|
-
"oclif": "^4.17.46",
|
|
56
|
-
"shx": "^0.4.0",
|
|
57
|
-
"ts-node": "^10.9.2",
|
|
58
|
-
"typescript": "^5"
|
|
59
|
-
},
|
|
60
|
-
"engines": {
|
|
61
|
-
"node": ">=18.0.0"
|
|
7
|
+
"create-manifest": "./index.js"
|
|
62
8
|
},
|
|
63
9
|
"files": [
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"/assets",
|
|
67
|
-
"/oclif.manifest.json"
|
|
10
|
+
"index.js",
|
|
11
|
+
"starter"
|
|
68
12
|
],
|
|
69
|
-
"main": "",
|
|
70
|
-
"repository": {
|
|
71
|
-
"type": "git",
|
|
72
|
-
"url": "git+https://github.com/mnfst/manifest.git"
|
|
73
|
-
},
|
|
74
|
-
"bugs": "https://github.com/mnfst/manifest/issues",
|
|
75
13
|
"keywords": [
|
|
76
14
|
"manifest",
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"AI code tool",
|
|
81
|
-
"backend for vibe coding",
|
|
82
|
-
"backend for modern workflows",
|
|
83
|
-
"vibe coding",
|
|
84
|
-
"vibe coding backend",
|
|
85
|
-
"vibe coding api",
|
|
86
|
-
"AI assisted coding",
|
|
87
|
-
"AI coding",
|
|
88
|
-
"windsurf",
|
|
89
|
-
"copilot",
|
|
90
|
-
"micro-backend",
|
|
91
|
-
"backend",
|
|
92
|
-
"headless",
|
|
93
|
-
"install",
|
|
94
|
-
"rest"
|
|
15
|
+
"mcp",
|
|
16
|
+
"create",
|
|
17
|
+
"cli"
|
|
95
18
|
],
|
|
96
|
-
"
|
|
97
|
-
"
|
|
98
|
-
"type": "module"
|
|
19
|
+
"author": "MNFST, Inc.",
|
|
20
|
+
"license": "MIT"
|
|
99
21
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(cat:*)",
|
|
5
|
+
"Bash(npm install:*)",
|
|
6
|
+
"Bash(npm --version:*)",
|
|
7
|
+
"Bash(node --version:*)",
|
|
8
|
+
"Bash(npm cache clean:*)",
|
|
9
|
+
"Bash(npm ls:*)",
|
|
10
|
+
"Bash(npm run build:*)",
|
|
11
|
+
"WebFetch(domain:github.com)",
|
|
12
|
+
"WebFetch(domain:raw.githubusercontent.com)",
|
|
13
|
+
"WebSearch",
|
|
14
|
+
"WebFetch(domain:xmcp.dev)",
|
|
15
|
+
"WebFetch(domain:dev.to)",
|
|
16
|
+
"WebFetch(domain:www.skybridge.tech)",
|
|
17
|
+
"WebFetch(domain:gadget.dev)",
|
|
18
|
+
"Bash(curl:*)",
|
|
19
|
+
"Bash(pkill:*)",
|
|
20
|
+
"Bash(npm run dev:*)"
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
PORT=3000
|
|
@@ -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
|
+
```
|
|
@@ -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,129 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
|
+
import { getWidgetHTML, type ViteHandle } 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
|
+
viteHandle: ViteHandle
|
|
49
|
+
): void {
|
|
50
|
+
const uiVersion = 'v1'
|
|
51
|
+
const resourceUri = `ui://pokemon-list.html?${uiVersion}`
|
|
52
|
+
|
|
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)
|
|
57
|
+
|
|
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
|
+
})
|
|
69
|
+
|
|
70
|
+
server.registerTool(
|
|
71
|
+
'listPokemons',
|
|
72
|
+
{
|
|
73
|
+
title: 'Pokemon List',
|
|
74
|
+
description: `Display a list of Pokemon in an interactive carousel.
|
|
75
|
+
|
|
76
|
+
WHEN TO USE:
|
|
77
|
+
- When the user asks to see a list of Pokemon
|
|
78
|
+
- When the user wants to browse Pokemon
|
|
79
|
+
- When the user asks "show me some Pokemon"
|
|
80
|
+
|
|
81
|
+
The tool fetches Pokemon data from the PokeAPI and displays them in a beautiful carousel format.`,
|
|
82
|
+
inputSchema: z.object({
|
|
83
|
+
limit: z
|
|
84
|
+
.number()
|
|
85
|
+
.min(1)
|
|
86
|
+
.max(50)
|
|
87
|
+
.optional()
|
|
88
|
+
.default(12)
|
|
89
|
+
.describe('Number of Pokemon to fetch (1-50, default: 12)')
|
|
90
|
+
}),
|
|
91
|
+
_meta: {
|
|
92
|
+
'openai/outputTemplate': resourceUri,
|
|
93
|
+
'openai/toolInvocation/invoking': 'Fetching Pokemon...',
|
|
94
|
+
'openai/toolInvocation/invoked': 'Pokemon list ready!'
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
async (args: { limit?: number }) => {
|
|
98
|
+
console.log('listPokemons called with args:', JSON.stringify(args))
|
|
99
|
+
const limit = args.limit ?? 12
|
|
100
|
+
console.log('Using limit:', limit)
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const pokemons = await fetchPokemons(limit)
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
content: [
|
|
107
|
+
{
|
|
108
|
+
type: 'text' as const,
|
|
109
|
+
text: `Found ${pokemons.length} Pokemon! Browse through the carousel to see them all.`
|
|
110
|
+
}
|
|
111
|
+
],
|
|
112
|
+
structuredContent: {
|
|
113
|
+
pokemons
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch (error) {
|
|
117
|
+
return {
|
|
118
|
+
content: [
|
|
119
|
+
{
|
|
120
|
+
type: 'text' as const,
|
|
121
|
+
text: `Failed to fetch Pokemon: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
122
|
+
}
|
|
123
|
+
],
|
|
124
|
+
isError: true
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
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 {
|
|
8
|
+
getWidgets,
|
|
9
|
+
getWidgetHTML,
|
|
10
|
+
type ViteHandle
|
|
11
|
+
} from 'vite-plugin-chatgpt-widgets'
|
|
12
|
+
import type { ViteDevServer } from 'vite'
|
|
13
|
+
import { registerPokemonFlow } from './flows/list-pokemons.flow.js'
|
|
14
|
+
|
|
15
|
+
const isDev = process.env.NODE_ENV !== 'production'
|
|
16
|
+
const port = Number(process.env.PORT) || 3000
|
|
17
|
+
|
|
18
|
+
// Session storage
|
|
19
|
+
interface SessionData {
|
|
20
|
+
transport: StreamableHTTPServerTransport
|
|
21
|
+
}
|
|
22
|
+
const sessions = new Map<string, SessionData>()
|
|
23
|
+
|
|
24
|
+
// Vite handle for dynamic widget content
|
|
25
|
+
let viteHandle: ViteHandle
|
|
26
|
+
|
|
27
|
+
function createServer() {
|
|
28
|
+
const server = new McpServer({
|
|
29
|
+
name: 'Manifest Starter',
|
|
30
|
+
version: '0.0.1'
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// Pass viteHandle so flows can fetch fresh widget content
|
|
34
|
+
registerPokemonFlow(server, viteHandle)
|
|
35
|
+
|
|
36
|
+
return server
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function main() {
|
|
40
|
+
const app = express()
|
|
41
|
+
|
|
42
|
+
// Create Vite dev server in development mode
|
|
43
|
+
let viteDevServer: ViteDevServer | null = null
|
|
44
|
+
|
|
45
|
+
if (isDev) {
|
|
46
|
+
const { createServer } = await import('vite')
|
|
47
|
+
viteDevServer = await createServer({
|
|
48
|
+
server: { middlewareMode: true },
|
|
49
|
+
appType: 'custom'
|
|
50
|
+
})
|
|
51
|
+
// Use Vite's middleware for HMR and asset serving
|
|
52
|
+
app.use(viteDevServer.middlewares)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Set vite handle for dynamic widget content
|
|
56
|
+
if (isDev && viteDevServer) {
|
|
57
|
+
viteHandle = { devServer: viteDevServer }
|
|
58
|
+
console.log('Vite dev server ready - widgets will be served dynamically')
|
|
59
|
+
} else {
|
|
60
|
+
viteHandle = { manifestPath: 'dist/web/.vite/manifest.json' }
|
|
61
|
+
console.log('Using production manifest for widgets')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Log available widgets
|
|
65
|
+
const widgets = await getWidgets('src/web', viteHandle)
|
|
66
|
+
for (const widget of widgets) {
|
|
67
|
+
console.log(` - ${widget.name} (${widget.source})`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
app.use(express.json())
|
|
71
|
+
|
|
72
|
+
// CORS headers for all responses
|
|
73
|
+
app.use((_req, res, next) => {
|
|
74
|
+
res.header('Access-Control-Allow-Origin', '*')
|
|
75
|
+
res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS')
|
|
76
|
+
res.header(
|
|
77
|
+
'Access-Control-Allow-Headers',
|
|
78
|
+
'Content-Type, Accept, Authorization, mcp-session-id'
|
|
79
|
+
)
|
|
80
|
+
res.header(
|
|
81
|
+
'Access-Control-Expose-Headers',
|
|
82
|
+
'mcp-session-id, WWW-Authenticate'
|
|
83
|
+
)
|
|
84
|
+
next()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
// Handle CORS preflight
|
|
88
|
+
app.options('/mcp', (_req, res) => {
|
|
89
|
+
res.sendStatus(204)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
app.post('/mcp', async (req, res, next) => {
|
|
93
|
+
const sessionId = req.headers['mcp-session-id'] as string | undefined
|
|
94
|
+
|
|
95
|
+
// Existing session
|
|
96
|
+
if (sessionId && sessions.has(sessionId)) {
|
|
97
|
+
const session = sessions.get(sessionId)!
|
|
98
|
+
await session.transport
|
|
99
|
+
.handleRequest(req as unknown as IncomingMessage, res, req.body)
|
|
100
|
+
.catch(next)
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// New session - create transport and server
|
|
105
|
+
const transport = new StreamableHTTPServerTransport({
|
|
106
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
107
|
+
enableJsonResponse: true
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const server = createServer()
|
|
111
|
+
|
|
112
|
+
transport.onclose = () => {
|
|
113
|
+
const sid = (transport as unknown as { sessionId?: string }).sessionId
|
|
114
|
+
if (sid) sessions.delete(sid)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
await server.connect(transport as Transport)
|
|
118
|
+
await transport
|
|
119
|
+
.handleRequest(req as unknown as IncomingMessage, res, req.body)
|
|
120
|
+
.catch(next)
|
|
121
|
+
|
|
122
|
+
// Store session after successful initialization
|
|
123
|
+
const newSessionId = (transport as unknown as { sessionId?: string })
|
|
124
|
+
.sessionId
|
|
125
|
+
if (newSessionId) {
|
|
126
|
+
sessions.set(newSessionId, { transport })
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
// Handle GET for SSE streams
|
|
131
|
+
app.get('/mcp', async (req, res, next) => {
|
|
132
|
+
const sessionId = req.headers['mcp-session-id'] as string | undefined
|
|
133
|
+
|
|
134
|
+
if (!sessionId || !sessions.has(sessionId)) {
|
|
135
|
+
res.status(400).json({ error: 'Invalid or missing session ID' })
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const session = sessions.get(sessionId)!
|
|
140
|
+
await session.transport
|
|
141
|
+
.handleRequest(req as unknown as IncomingMessage, res)
|
|
142
|
+
.catch(next)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
// Handle DELETE for session cleanup
|
|
146
|
+
app.delete('/mcp', async (req, res) => {
|
|
147
|
+
const sessionId = req.headers['mcp-session-id'] as string | undefined
|
|
148
|
+
|
|
149
|
+
if (sessionId && sessions.has(sessionId)) {
|
|
150
|
+
const session = sessions.get(sessionId)!
|
|
151
|
+
await session.transport.close()
|
|
152
|
+
sessions.delete(sessionId)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
res.status(204).end()
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
app.listen(port, '0.0.0.0', () => {
|
|
159
|
+
console.log(`MCP server listening on http://localhost:${port}/mcp`)
|
|
160
|
+
if (isDev) {
|
|
161
|
+
console.log('Vite HMR enabled - widget changes will hot reload')
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
main().catch((err) => {
|
|
167
|
+
console.error('Failed to start server:', err)
|
|
168
|
+
process.exit(1)
|
|
169
|
+
})
|