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.
Files changed (57) hide show
  1. package/README.md +40 -21
  2. package/index.js +51 -0
  3. package/package.json +11 -89
  4. package/starter/.claude/settings.local.json +23 -0
  5. package/starter/.env.example +1 -0
  6. package/starter/README.md +26 -0
  7. package/starter/components.json +24 -0
  8. package/starter/package.json +42 -0
  9. package/starter/src/flows/list-pokemons.flow.ts +129 -0
  10. package/starter/src/server.ts +169 -0
  11. package/starter/src/web/PokemonList.tsx +126 -0
  12. package/starter/src/web/components/blog-post-card.tsx +288 -0
  13. package/starter/src/web/components/blog-post-list.tsx +291 -0
  14. package/starter/src/web/components/payment-methods.tsx +201 -0
  15. package/starter/src/web/components/table.tsx +478 -0
  16. package/starter/src/web/components/ui/.gitkeep +0 -0
  17. package/starter/src/web/components/ui/button.tsx +62 -0
  18. package/starter/src/web/components/ui/checkbox.tsx +30 -0
  19. package/starter/src/web/globals.css +98 -0
  20. package/starter/src/web/hooks/.gitkeep +0 -0
  21. package/starter/src/web/lib/utils.ts +6 -0
  22. package/starter/src/web/root.tsx +36 -0
  23. package/starter/src/web/tsconfig.json +3 -0
  24. package/starter/tsconfig.json +25 -0
  25. package/starter/tsconfig.web.json +24 -0
  26. package/starter/vite.config.ts +37 -0
  27. package/assets/monorepo/README.md +0 -52
  28. package/assets/monorepo/api-package.json +0 -9
  29. package/assets/monorepo/api-readme.md +0 -50
  30. package/assets/monorepo/manifest.yml +0 -34
  31. package/assets/monorepo/root-package.json +0 -15
  32. package/assets/monorepo/web-package.json +0 -10
  33. package/assets/monorepo/web-readme.md +0 -9
  34. package/assets/standalone/README.md +0 -50
  35. package/assets/standalone/api-package.json +0 -9
  36. package/assets/standalone/manifest.yml +0 -34
  37. package/bin/dev.cmd +0 -3
  38. package/bin/dev.js +0 -5
  39. package/bin/run.cmd +0 -3
  40. package/bin/run.js +0 -5
  41. package/dist/commands/index.d.ts +0 -65
  42. package/dist/commands/index.js +0 -480
  43. package/dist/index.d.ts +0 -1
  44. package/dist/index.js +0 -1
  45. package/dist/utils/GetBackendFileContent.d.ts +0 -1
  46. package/dist/utils/GetBackendFileContent.js +0 -21
  47. package/dist/utils/GetLatestPackageVersion.d.ts +0 -1
  48. package/dist/utils/GetLatestPackageVersion.js +0 -5
  49. package/dist/utils/UpdateExtensionJsonFile.d.ts +0 -6
  50. package/dist/utils/UpdateExtensionJsonFile.js +0 -8
  51. package/dist/utils/UpdatePackageJsonFile.d.ts +0 -18
  52. package/dist/utils/UpdatePackageJsonFile.js +0 -21
  53. package/dist/utils/UpdateSettingsJsonFile.d.ts +0 -4
  54. package/dist/utils/UpdateSettingsJsonFile.js +0 -6
  55. package/dist/utils/helpers.d.ts +0 -1
  56. package/dist/utils/helpers.js +0 -11
  57. package/oclif.manifest.json +0 -47
package/README.md CHANGED
@@ -1,38 +1,57 @@
1
1
  # create-manifest
2
2
 
3
- The `create manifest` create a new project with [Manifest](https://manifest.build).
3
+ Create a new Manifest MCP server project with a single command.
4
+
5
+ ## Usage
4
6
 
5
7
  ```bash
6
- npx create-manifest@latest
8
+ npx create-manifest my-app
7
9
  ```
8
10
 
9
- This will create a folder named my-project and install Manifest inside it.
10
- If you leave out the name, the CLI will ask you for it during setup.
11
+ This will:
11
12
 
12
- You can add a flag to set up rules for your AI code Editor:
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
- - `--cursor` for **Cursor**
15
- - `--copilot` for **GitHub Copilot**
16
- - `--windsurf` for **Windsurf**
18
+ ## What's Included
17
19
 
18
- You can also use `yarn create manifest`.
20
+ The starter template includes:
19
21
 
20
- ## Develop
22
+ - MCP server setup with Express
23
+ - TypeScript configuration
24
+ - Hot reload with Nodemon
25
+ - Vite for building web components
21
26
 
22
- ```bash
23
- npm install
27
+ ## Requirements
24
28
 
25
- # Run from a test folder to prevent messing with project files.
26
- mkdir test-folder
27
- cd test-folder
28
- ../bin/dev.js
29
- ```
29
+ - Node.js 22+
30
30
 
31
- However due to the monorepo workspace structure, the launch script will fail as the path to the node modules folder is different than when served. This is normal.
31
+ ## After Creation
32
32
 
33
- ## Publish
33
+ Your project will be running at `http://localhost:3000`.
34
+
35
+ To restart the dev server later:
34
36
 
35
37
  ```bash
36
- npm run build
37
- npm publish
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": "1.3.4",
4
- "author": "Manifest",
5
- "description": "Create a new Manifest backend",
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": "./bin/run.js"
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
- "/bin",
65
- "/dist",
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
- "cursor",
78
- "AI-powered coding",
79
- "backend for LLMs",
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
- "types": "dist/index.d.ts",
97
- "exports": "./lib/index.js",
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
+ })