pizzaz-mcp 1.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/.vscode/settings.json +3 -0
- package/README.md +139 -0
- package/build-all.mts +188 -0
- package/docs/DEPLOYMENT_GUIDE.md +226 -0
- package/package.json +41 -0
- package/render.yaml +12 -0
- package/server/server.ts +400 -0
- package/src/index.css +39 -0
- package/src/media-queries.ts +15 -0
- package/src/pizzaz/Inspector.jsx +109 -0
- package/src/pizzaz/Sidebar.jsx +165 -0
- package/src/pizzaz/index.jsx +295 -0
- package/src/pizzaz/map.css +707 -0
- package/src/pizzaz/markers.json +104 -0
- package/src/pizzaz-albums/AlbumCard.jsx +45 -0
- package/src/pizzaz-albums/FilmStrip.jsx +30 -0
- package/src/pizzaz-albums/FullscreenViewer.jsx +43 -0
- package/src/pizzaz-albums/albums.json +112 -0
- package/src/pizzaz-albums/index.jsx +153 -0
- package/src/pizzaz-carousel/PlaceCard.jsx +40 -0
- package/src/pizzaz-carousel/index.jsx +121 -0
- package/src/pizzaz-list/index.jsx +115 -0
- package/src/pizzaz-shop/index.tsx +1482 -0
- package/src/types.ts +103 -0
- package/src/use-display-mode.ts +6 -0
- package/src/use-max-height.ts +5 -0
- package/src/use-openai-global.ts +37 -0
- package/src/use-widget-props.ts +14 -0
- package/src/use-widget-state.ts +46 -0
- package/tailwind.config.ts +7 -0
- package/tsconfig.app.json +24 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +13 -0
- package/vite-env.d.ts +1 -0
- package/vite.config.mts +232 -0
package/README.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Pizzaz MCP Server
|
|
2
|
+
|
|
3
|
+
A Model Context Protocol (MCP) server that serves pizza UI widgets for ChatGPT's Apps SDK. Deployed on Render to bypass proxy restrictions, using OpenAI's exact component architecture, styling system, and design tokens.
|
|
4
|
+
|
|
5
|
+
## Live URL
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
https://pizzaz-mcp.onrender.com/mcp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## What It Does
|
|
12
|
+
|
|
13
|
+
- Exposes 5 pizza widgets via MCP tools: map, carousel, list, albums, shop
|
|
14
|
+
- Uses `@openai/apps-sdk-ui` components (Button, Image) exactly as OpenAI does
|
|
15
|
+
- Tailwind CSS v4 + OpenAI design tokens for theming
|
|
16
|
+
- Each widget is a self-contained HTML file served from `/assets/`
|
|
17
|
+
- Auto-deploys to Render on every push to `main`
|
|
18
|
+
|
|
19
|
+
## Project Structure
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
pizzaz-mcp/
|
|
23
|
+
├── src/ # React widget source (OpenAI SDK architecture)
|
|
24
|
+
│ ├── index.css # Tailwind + @openai/apps-sdk-ui/css
|
|
25
|
+
│ ├── types.ts # OpenAI globals / widget types
|
|
26
|
+
│ ├── use-openai-global.ts # Hook: reads window.openai globals
|
|
27
|
+
│ ├── use-widget-props.ts # Hook: reads toolOutput props
|
|
28
|
+
│ ├── use-widget-state.ts # Hook: reads/writes widget state
|
|
29
|
+
│ ├── use-display-mode.ts # Hook: pip | inline | fullscreen
|
|
30
|
+
│ ├── use-max-height.ts # Hook: max height constraint
|
|
31
|
+
│ ├── media-queries.ts # Shared media query helpers
|
|
32
|
+
│ ├── pizzaz/ # Pizza map widget + markers data
|
|
33
|
+
│ ├── pizzaz-carousel/ # Pizza carousel widget
|
|
34
|
+
│ ├── pizzaz-list/ # Pizza list widget
|
|
35
|
+
│ ├── pizzaz-albums/ # Pizza albums widget
|
|
36
|
+
│ └── pizzaz-shop/ # Pizza shop / cart widget
|
|
37
|
+
├── server/
|
|
38
|
+
│ └── server.ts # MCP server (SSE transport)
|
|
39
|
+
├── assets/ # Build output (committed to git)
|
|
40
|
+
│ ├── pizzaz.html
|
|
41
|
+
│ ├── pizzaz-carousel.html
|
|
42
|
+
│ ├── pizzaz-list.html
|
|
43
|
+
│ ├── pizzaz-albums.html
|
|
44
|
+
│ ├── pizzaz-shop.html
|
|
45
|
+
│ └── *.js / *.css # Hashed bundles
|
|
46
|
+
├── build-all.mts # Vite multi-entry build script
|
|
47
|
+
├── vite.config.mts # Vite dev server config
|
|
48
|
+
├── tailwind.config.ts # Tailwind content paths
|
|
49
|
+
├── tsconfig.json # TypeScript project references
|
|
50
|
+
├── package.json # Dependencies + scripts
|
|
51
|
+
└── render.yaml # Render deployment config
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Architecture
|
|
55
|
+
|
|
56
|
+
The app follows OpenAI's exact widget architecture from `openai-apps-sdk-examples`:
|
|
57
|
+
|
|
58
|
+
- Components use `@openai/apps-sdk-ui/components/Button` and `Image` directly
|
|
59
|
+
- CSS uses Tailwind v4 with `@import "@openai/apps-sdk-ui/css"` for design tokens
|
|
60
|
+
- Hooks (`useWidgetProps`, `useWidgetState`, `useOpenAiGlobal`) match the SDK pattern
|
|
61
|
+
- Each widget entry is `src/<name>/index.{tsx,jsx}` — Vite builds each to `assets/<name>.js` + `assets/<name>.css`
|
|
62
|
+
- The build script generates `assets/<name>.html` with absolute URLs pointing to the Render deployment
|
|
63
|
+
- The MCP server reads those HTML files at startup and serves them via `text/html+skybridge` MIME type
|
|
64
|
+
|
|
65
|
+
## Available MCP Tools
|
|
66
|
+
|
|
67
|
+
| Tool | Widget | Description |
|
|
68
|
+
|------|--------|-------------|
|
|
69
|
+
| `pizza-map` | pizzaz | Interactive pizza map of SF |
|
|
70
|
+
| `pizza-carousel` | pizzaz-carousel | Scrollable pizza place cards |
|
|
71
|
+
| `pizza-list` | pizzaz-list | Ranked list of pizzerias |
|
|
72
|
+
| `pizza-albums` | pizzaz-albums | Album-style pizza gallery |
|
|
73
|
+
| `pizza-shop` | pizzaz-shop | Full cart + checkout experience |
|
|
74
|
+
|
|
75
|
+
## Quick Start
|
|
76
|
+
|
|
77
|
+
### Local Development
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npm install
|
|
81
|
+
npm run dev
|
|
82
|
+
# Open http://localhost:4444
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Build
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npm run build
|
|
89
|
+
# Outputs to assets/
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Deploy to Render
|
|
93
|
+
|
|
94
|
+
1. Push to GitHub
|
|
95
|
+
2. On [Render Dashboard](https://dashboard.render.com/): New → Web Service → connect repo
|
|
96
|
+
3. Render auto-detects `render.yaml` and deploys
|
|
97
|
+
4. Add MCP connector in ChatGPT: `https://pizzaz-mcp.onrender.com/mcp`
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
git add .
|
|
101
|
+
git commit -m "Deploy"
|
|
102
|
+
git push origin main
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Using in ChatGPT
|
|
106
|
+
|
|
107
|
+
1. Go to https://chatgpt.com
|
|
108
|
+
2. Settings → Beta Features → enable Developer Mode
|
|
109
|
+
3. Add MCP connector: `https://pizzaz-mcp.onrender.com/mcp`
|
|
110
|
+
4. Try: "Show me the pizza carousel" or "Open the Pizzaz shop"
|
|
111
|
+
|
|
112
|
+
## Theming
|
|
113
|
+
|
|
114
|
+
Theming is handled by `@openai/apps-sdk-ui`'s CSS custom properties. The `src/index.css` imports the full token set:
|
|
115
|
+
|
|
116
|
+
```css
|
|
117
|
+
@import "tailwindcss";
|
|
118
|
+
@import "@openai/apps-sdk-ui/css";
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
To customize colors, override the CSS variables in `src/index.css`:
|
|
122
|
+
|
|
123
|
+
```css
|
|
124
|
+
:root {
|
|
125
|
+
--color-background-primary-solid: #ff6b35;
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Notes
|
|
130
|
+
|
|
131
|
+
- Render free tier spins down after 15 min of inactivity — first request takes ~30s
|
|
132
|
+
- The MCP endpoint is `GET /mcp` (SSE) and `POST /mcp/messages` (messages)
|
|
133
|
+
- Build output is committed to git so Render only needs `npm install && npm start`
|
|
134
|
+
|
|
135
|
+
## References
|
|
136
|
+
|
|
137
|
+
- [OpenAI Apps SDK Examples](https://github.com/openai/openai-apps-sdk-examples)
|
|
138
|
+
- [MCP Protocol](https://modelcontextprotocol.io/)
|
|
139
|
+
- [Render Docs](https://render.com/docs)
|
package/build-all.mts
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { build, type InlineConfig, type Plugin } from "vite";
|
|
2
|
+
import react from "@vitejs/plugin-react";
|
|
3
|
+
import fg from "fast-glob";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import crypto from "crypto";
|
|
7
|
+
import pkg from "./package.json" with { type: "json" };
|
|
8
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
9
|
+
|
|
10
|
+
const entries = fg.sync("src/**/index.{tsx,jsx}");
|
|
11
|
+
const outDir = "assets";
|
|
12
|
+
|
|
13
|
+
const PER_ENTRY_CSS_GLOB = "**/*.{css,pcss,scss,sass}";
|
|
14
|
+
const PER_ENTRY_CSS_IGNORE = "**/*.module.*".split(",").map((s) => s.trim());
|
|
15
|
+
const GLOBAL_CSS_LIST = [path.resolve("src/index.css")];
|
|
16
|
+
|
|
17
|
+
const targets: string[] = [
|
|
18
|
+
"pizzaz",
|
|
19
|
+
"pizzaz-carousel",
|
|
20
|
+
"pizzaz-list",
|
|
21
|
+
"pizzaz-albums",
|
|
22
|
+
"pizzaz-shop",
|
|
23
|
+
];
|
|
24
|
+
const builtNames: string[] = [];
|
|
25
|
+
|
|
26
|
+
function wrapEntryPlugin(
|
|
27
|
+
virtualId: string,
|
|
28
|
+
entryFile: string,
|
|
29
|
+
cssPaths: string[]
|
|
30
|
+
): Plugin {
|
|
31
|
+
return {
|
|
32
|
+
name: `virtual-entry-wrapper:${entryFile}`,
|
|
33
|
+
resolveId(id) {
|
|
34
|
+
if (id === virtualId) return id;
|
|
35
|
+
},
|
|
36
|
+
load(id) {
|
|
37
|
+
if (id !== virtualId) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const cssImports = cssPaths
|
|
42
|
+
.map((css) => `import ${JSON.stringify(css)};`)
|
|
43
|
+
.join("\n");
|
|
44
|
+
|
|
45
|
+
return `
|
|
46
|
+
${cssImports}
|
|
47
|
+
export * from ${JSON.stringify(entryFile)};
|
|
48
|
+
|
|
49
|
+
import * as __entry from ${JSON.stringify(entryFile)};
|
|
50
|
+
export default (__entry.default ?? __entry.App);
|
|
51
|
+
|
|
52
|
+
import ${JSON.stringify(entryFile)};
|
|
53
|
+
`;
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fs.rmSync(outDir, { recursive: true, force: true });
|
|
59
|
+
|
|
60
|
+
for (const file of entries) {
|
|
61
|
+
const name = path.basename(path.dirname(file));
|
|
62
|
+
if (targets.length && !targets.includes(name)) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const entryAbs = path.resolve(file);
|
|
67
|
+
const entryDir = path.dirname(entryAbs);
|
|
68
|
+
|
|
69
|
+
// Collect CSS for this entry using the glob(s) rooted at its directory
|
|
70
|
+
const perEntryCss = fg.sync(PER_ENTRY_CSS_GLOB, {
|
|
71
|
+
cwd: entryDir,
|
|
72
|
+
absolute: true,
|
|
73
|
+
dot: false,
|
|
74
|
+
ignore: PER_ENTRY_CSS_IGNORE,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Global CSS (Tailwind, etc.), only include those that exist
|
|
78
|
+
const globalCss = GLOBAL_CSS_LIST.filter((p) => fs.existsSync(p));
|
|
79
|
+
|
|
80
|
+
// Final CSS list (global first for predictable cascade)
|
|
81
|
+
const cssToInclude = [...globalCss, ...perEntryCss].filter((p) =>
|
|
82
|
+
fs.existsSync(p)
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const virtualId = `\0virtual-entry:${entryAbs}`;
|
|
86
|
+
|
|
87
|
+
const createConfig = (): InlineConfig => ({
|
|
88
|
+
plugins: [
|
|
89
|
+
wrapEntryPlugin(virtualId, entryAbs, cssToInclude),
|
|
90
|
+
tailwindcss(),
|
|
91
|
+
react(),
|
|
92
|
+
{
|
|
93
|
+
name: "remove-manual-chunks",
|
|
94
|
+
outputOptions(options) {
|
|
95
|
+
if ("manualChunks" in options) {
|
|
96
|
+
delete (options as any).manualChunks;
|
|
97
|
+
}
|
|
98
|
+
return options;
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
esbuild: {
|
|
103
|
+
jsx: "automatic",
|
|
104
|
+
jsxImportSource: "react",
|
|
105
|
+
target: "es2022",
|
|
106
|
+
},
|
|
107
|
+
build: {
|
|
108
|
+
target: "es2022",
|
|
109
|
+
outDir,
|
|
110
|
+
emptyOutDir: false,
|
|
111
|
+
chunkSizeWarningLimit: 2000,
|
|
112
|
+
minify: "esbuild",
|
|
113
|
+
cssCodeSplit: false,
|
|
114
|
+
rollupOptions: {
|
|
115
|
+
input: virtualId,
|
|
116
|
+
output: {
|
|
117
|
+
format: "es",
|
|
118
|
+
entryFileNames: `${name}.js`,
|
|
119
|
+
inlineDynamicImports: true,
|
|
120
|
+
assetFileNames: (info) =>
|
|
121
|
+
(info.name || "").endsWith(".css")
|
|
122
|
+
? `${name}.css`
|
|
123
|
+
: `[name]-[hash][extname]`,
|
|
124
|
+
},
|
|
125
|
+
preserveEntrySignatures: "allow-extension",
|
|
126
|
+
treeshake: true,
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
console.group(`Building ${name} (react)`);
|
|
132
|
+
await build(createConfig());
|
|
133
|
+
console.groupEnd();
|
|
134
|
+
builtNames.push(name);
|
|
135
|
+
console.log(`Built ${name}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const outputs = fs
|
|
139
|
+
.readdirSync("assets")
|
|
140
|
+
.filter((f) => f.endsWith(".js") || f.endsWith(".css"))
|
|
141
|
+
.map((f) => path.join("assets", f))
|
|
142
|
+
.filter((p) => fs.existsSync(p));
|
|
143
|
+
|
|
144
|
+
const h = crypto
|
|
145
|
+
.createHash("sha256")
|
|
146
|
+
.update(pkg.version, "utf8")
|
|
147
|
+
.digest("hex")
|
|
148
|
+
.slice(0, 4);
|
|
149
|
+
|
|
150
|
+
console.group("Hashing outputs");
|
|
151
|
+
for (const out of outputs) {
|
|
152
|
+
const dir = path.dirname(out);
|
|
153
|
+
const ext = path.extname(out);
|
|
154
|
+
const base = path.basename(out, ext);
|
|
155
|
+
const newName = path.join(dir, `${base}-${h}${ext}`);
|
|
156
|
+
|
|
157
|
+
fs.renameSync(out, newName);
|
|
158
|
+
console.log(`${out} -> ${newName}`);
|
|
159
|
+
}
|
|
160
|
+
console.groupEnd();
|
|
161
|
+
|
|
162
|
+
console.log("new hash: ", h);
|
|
163
|
+
|
|
164
|
+
const defaultBaseUrl = "https://pizzaz-mcp.onrender.com";
|
|
165
|
+
const baseUrlCandidate = process.env.BASE_URL?.trim() ?? "";
|
|
166
|
+
const baseUrlRaw = baseUrlCandidate.length > 0 ? baseUrlCandidate : defaultBaseUrl;
|
|
167
|
+
const normalizedBaseUrl = baseUrlRaw.replace(/\/+$/, "") || defaultBaseUrl;
|
|
168
|
+
console.log(`Using BASE_URL ${normalizedBaseUrl} for generated HTML`);
|
|
169
|
+
|
|
170
|
+
for (const name of builtNames) {
|
|
171
|
+
const dir = outDir;
|
|
172
|
+
const hashedHtmlPath = path.join(dir, `${name}-${h}.html`);
|
|
173
|
+
const liveHtmlPath = path.join(dir, `${name}.html`);
|
|
174
|
+
const html = `<!doctype html>
|
|
175
|
+
<html>
|
|
176
|
+
<head>
|
|
177
|
+
<script type="module" src="${normalizedBaseUrl}/${name}-${h}.js"></script>
|
|
178
|
+
<link rel="stylesheet" href="${normalizedBaseUrl}/${name}-${h}.css">
|
|
179
|
+
</head>
|
|
180
|
+
<body>
|
|
181
|
+
<div id="${name}-root"></div>
|
|
182
|
+
</body>
|
|
183
|
+
</html>
|
|
184
|
+
`;
|
|
185
|
+
fs.writeFileSync(hashedHtmlPath, html, { encoding: "utf8" });
|
|
186
|
+
fs.writeFileSync(liveHtmlPath, html, { encoding: "utf8" });
|
|
187
|
+
console.log(`${liveHtmlPath}`);
|
|
188
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# Pizza Carousel MCP Deployment Guide
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
Your pizza carousel with dual styling is ready to deploy! The MCP server is already configured to serve both carousel variants that users can switch between using commands in ChatGPT.
|
|
5
|
+
|
|
6
|
+
## What's Already Configured
|
|
7
|
+
|
|
8
|
+
### MCP Server Tools
|
|
9
|
+
The server (`src/server.ts`) already has two tools configured:
|
|
10
|
+
|
|
11
|
+
1. **`pizza-carousel-default`** - Shows carousel with OpenAI default styling
|
|
12
|
+
- Green color scheme (#10a37f)
|
|
13
|
+
- 12px button border radius
|
|
14
|
+
- System fonts
|
|
15
|
+
|
|
16
|
+
2. **`pizza-carousel-custom`** - Shows carousel with custom branding
|
|
17
|
+
- Cyan color scheme (#00caff)
|
|
18
|
+
- 20px button border radius
|
|
19
|
+
- Custom fonts
|
|
20
|
+
|
|
21
|
+
### File Structure
|
|
22
|
+
```
|
|
23
|
+
assets/
|
|
24
|
+
├── tokens/
|
|
25
|
+
│ ├── default-tokens.css ✅ Ready
|
|
26
|
+
│ └── custom-tokens.css ✅ Ready
|
|
27
|
+
├── pizzaz-carousel-default.html ✅ Ready
|
|
28
|
+
└── pizzaz-carousel-custom.html ✅ Ready
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Static Asset Serving
|
|
32
|
+
The server is configured to serve static files from `/assets/` path, which means:
|
|
33
|
+
- Token CSS files are accessible at `/assets/tokens/default-tokens.css`
|
|
34
|
+
- Token CSS files are accessible at `/assets/tokens/custom-tokens.css`
|
|
35
|
+
- HTML files correctly reference tokens with relative path: `href="tokens/default-tokens.css"`
|
|
36
|
+
|
|
37
|
+
## Deployment Steps
|
|
38
|
+
|
|
39
|
+
### 1. Start the MCP Server Locally
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm start
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Expected output:
|
|
46
|
+
```
|
|
47
|
+
Pizzaz MCP server listening on http://localhost:8000
|
|
48
|
+
SSE stream: GET http://localhost:8000/mcp
|
|
49
|
+
Message post endpoint: POST http://localhost:8000/mcp/messages?sessionId=...
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 2. Test the Server Locally
|
|
53
|
+
|
|
54
|
+
Open a new terminal and test the endpoints:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Test that token files are accessible
|
|
58
|
+
curl http://localhost:8000/assets/tokens/default-tokens.css
|
|
59
|
+
curl http://localhost:8000/assets/tokens/custom-tokens.css
|
|
60
|
+
|
|
61
|
+
# Test that HTML files load
|
|
62
|
+
curl http://localhost:8000/assets/pizzaz-carousel-default.html
|
|
63
|
+
curl http://localhost:8000/assets/pizzaz-carousel-custom.html
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
All should return the file contents without 404 errors.
|
|
67
|
+
|
|
68
|
+
### 3. Configure ChatGPT to Use Your MCP Server
|
|
69
|
+
|
|
70
|
+
You need to add your MCP server to ChatGPT's configuration:
|
|
71
|
+
|
|
72
|
+
1. Open ChatGPT settings
|
|
73
|
+
2. Navigate to MCP servers configuration
|
|
74
|
+
3. Add your server with the URL: `http://localhost:8000/mcp`
|
|
75
|
+
|
|
76
|
+
Or if deploying to a cloud service (like Render.com):
|
|
77
|
+
- Use the public URL provided by your hosting service
|
|
78
|
+
- Example: `https://your-app-name.onrender.com/mcp`
|
|
79
|
+
|
|
80
|
+
### 4. Deploy to Render.com (Optional)
|
|
81
|
+
|
|
82
|
+
Your `render.yaml` is already configured. To deploy:
|
|
83
|
+
|
|
84
|
+
1. **Push to GitHub:**
|
|
85
|
+
```bash
|
|
86
|
+
git add .
|
|
87
|
+
git commit -m "Add pizza carousel with dual styling"
|
|
88
|
+
git push origin main
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
2. **Connect to Render:**
|
|
92
|
+
- Go to https://render.com
|
|
93
|
+
- Create new Web Service
|
|
94
|
+
- Connect your GitHub repository
|
|
95
|
+
- Render will automatically detect `render.yaml` and deploy
|
|
96
|
+
|
|
97
|
+
3. **Get your public URL:**
|
|
98
|
+
- Render will provide a URL like: `https://pizzaz-mcp.onrender.com`
|
|
99
|
+
- Use this URL in ChatGPT: `https://pizzaz-mcp.onrender.com/mcp`
|
|
100
|
+
|
|
101
|
+
## How Users Will Interact in ChatGPT
|
|
102
|
+
|
|
103
|
+
Once deployed, users can interact with your pizza carousel in ChatGPT:
|
|
104
|
+
|
|
105
|
+
### Example User Commands:
|
|
106
|
+
|
|
107
|
+
**Show default styled carousel:**
|
|
108
|
+
```
|
|
109
|
+
Show me the pizza carousel with default styling
|
|
110
|
+
```
|
|
111
|
+
or
|
|
112
|
+
```
|
|
113
|
+
Use the pizza-carousel-default tool
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Show custom styled carousel:**
|
|
117
|
+
```
|
|
118
|
+
Show me the pizza carousel with custom branding
|
|
119
|
+
```
|
|
120
|
+
or
|
|
121
|
+
```
|
|
122
|
+
Use the pizza-carousel-custom tool
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Switch between styles:**
|
|
126
|
+
```
|
|
127
|
+
Now show it with custom styling instead
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### What Happens:
|
|
131
|
+
1. User types a command in ChatGPT
|
|
132
|
+
2. ChatGPT calls your MCP server tool (`pizza-carousel-default` or `pizza-carousel-custom`)
|
|
133
|
+
3. Server returns the HTML widget with the appropriate token file imported
|
|
134
|
+
4. ChatGPT renders the carousel in the conversation
|
|
135
|
+
5. User sees the styled carousel and can interact with it (hover, scroll)
|
|
136
|
+
|
|
137
|
+
## Verification Checklist
|
|
138
|
+
|
|
139
|
+
Before deploying, verify:
|
|
140
|
+
|
|
141
|
+
- [x] Both HTML files exist in `assets/` directory
|
|
142
|
+
- [x] Both token CSS files exist in `assets/tokens/` directory
|
|
143
|
+
- [x] HTML files correctly reference token files with relative paths
|
|
144
|
+
- [x] MCP server has both tools configured
|
|
145
|
+
- [x] Server serves static assets from `/assets/` path
|
|
146
|
+
- [ ] Server starts without errors (`npm start`)
|
|
147
|
+
- [ ] Token files are accessible via HTTP
|
|
148
|
+
- [ ] HTML files are accessible via HTTP
|
|
149
|
+
- [ ] ChatGPT can connect to your MCP server
|
|
150
|
+
- [ ] Both carousel tools work in ChatGPT
|
|
151
|
+
|
|
152
|
+
## Testing in ChatGPT
|
|
153
|
+
|
|
154
|
+
After deployment, test both tools:
|
|
155
|
+
|
|
156
|
+
1. **Test default carousel:**
|
|
157
|
+
- Ask: "Show me the pizza carousel with default styling"
|
|
158
|
+
- Verify: Green buttons, 12px border radius
|
|
159
|
+
- Check: All 5 pizzas display correctly
|
|
160
|
+
- Test: Hover effects and scrolling work
|
|
161
|
+
|
|
162
|
+
2. **Test custom carousel:**
|
|
163
|
+
- Ask: "Show me the pizza carousel with custom branding"
|
|
164
|
+
- Verify: Cyan buttons, 20px border radius
|
|
165
|
+
- Check: All 5 pizzas display correctly
|
|
166
|
+
- Test: Hover effects and scrolling work
|
|
167
|
+
|
|
168
|
+
3. **Test switching:**
|
|
169
|
+
- Ask: "Now show the default version"
|
|
170
|
+
- Verify: Carousel switches to default styling
|
|
171
|
+
- Ask: "Now show the custom version"
|
|
172
|
+
- Verify: Carousel switches to custom styling
|
|
173
|
+
|
|
174
|
+
## Troubleshooting
|
|
175
|
+
|
|
176
|
+
### Token files not loading (404 errors)
|
|
177
|
+
**Problem:** HTML shows unstyled content
|
|
178
|
+
**Solution:**
|
|
179
|
+
- Verify token files exist in `assets/tokens/`
|
|
180
|
+
- Check server logs for 404 errors
|
|
181
|
+
- Ensure server is serving static assets from `/assets/` path
|
|
182
|
+
|
|
183
|
+
### Styles not applying
|
|
184
|
+
**Problem:** Carousel appears but with wrong colors
|
|
185
|
+
**Solution:**
|
|
186
|
+
- Open browser DevTools in ChatGPT
|
|
187
|
+
- Check Network tab for CSS file loading
|
|
188
|
+
- Verify CSS variables are defined in token files
|
|
189
|
+
- Check that HTML uses `var(--variable-name)` syntax
|
|
190
|
+
|
|
191
|
+
### Server won't start
|
|
192
|
+
**Problem:** `npm start` fails
|
|
193
|
+
**Solution:**
|
|
194
|
+
- Check port 8000 is not already in use
|
|
195
|
+
- Verify all dependencies are installed: `npm install`
|
|
196
|
+
- Check for TypeScript errors in `src/server.ts`
|
|
197
|
+
|
|
198
|
+
### ChatGPT can't connect
|
|
199
|
+
**Problem:** MCP server not accessible from ChatGPT
|
|
200
|
+
**Solution:**
|
|
201
|
+
- Verify server is running and accessible
|
|
202
|
+
- Check firewall settings
|
|
203
|
+
- If using localhost, ensure ChatGPT can access local servers
|
|
204
|
+
- If deployed, verify public URL is correct
|
|
205
|
+
|
|
206
|
+
## Next Steps
|
|
207
|
+
|
|
208
|
+
1. ✅ **Verification Complete** - All automated checks passed
|
|
209
|
+
2. ⏳ **Local Testing** - Start server and test locally
|
|
210
|
+
3. ⏳ **Deploy** - Push to GitHub and deploy to Render
|
|
211
|
+
4. ⏳ **ChatGPT Integration** - Configure ChatGPT to use your MCP server
|
|
212
|
+
5. ⏳ **User Testing** - Test both carousel variants in ChatGPT
|
|
213
|
+
|
|
214
|
+
## Summary
|
|
215
|
+
|
|
216
|
+
Your pizza carousel implementation is complete and ready for deployment! The MCP server is configured to serve both styling variants, and users will be able to switch between them using natural language commands in ChatGPT.
|
|
217
|
+
|
|
218
|
+
**Key Features:**
|
|
219
|
+
- ✅ Two distinct styling themes (default and custom)
|
|
220
|
+
- ✅ Design token system for easy theming
|
|
221
|
+
- ✅ All 5 pizzas with complete data
|
|
222
|
+
- ✅ Interactive hover effects and smooth scrolling
|
|
223
|
+
- ✅ MCP server tools configured and ready
|
|
224
|
+
- ✅ Static asset serving for token CSS files
|
|
225
|
+
|
|
226
|
+
**Ready to deploy!** 🚀
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pizzaz-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Pizzaz MCP server — themed pizza UI widgets for ChatGPT Apps SDK, deployed on Render",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite --config vite.config.mts",
|
|
8
|
+
"build": "tsx ./build-all.mts",
|
|
9
|
+
"serve": "serve -s ./assets -p 4444 --cors",
|
|
10
|
+
"start": "tsx server/server.ts",
|
|
11
|
+
"tsc": "tsc -b"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
15
|
+
"@openai/apps-sdk-ui": "^0.2.1",
|
|
16
|
+
"@types/react": "^19.1.12",
|
|
17
|
+
"@types/react-dom": "^19.1.9",
|
|
18
|
+
"clsx": "^2.1.1",
|
|
19
|
+
"embla-carousel": "^8.0.0",
|
|
20
|
+
"embla-carousel-react": "^8.0.0",
|
|
21
|
+
"framer-motion": "^12.23.12",
|
|
22
|
+
"lucide-react": "^0.536.0",
|
|
23
|
+
"react": "^19.1.1",
|
|
24
|
+
"react-dom": "^19.1.1",
|
|
25
|
+
"react-router-dom": "^7.8.2",
|
|
26
|
+
"zod": "^3.23.8"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@tailwindcss/vite": "^4.1.11",
|
|
30
|
+
"@types/node": "^24.3.0",
|
|
31
|
+
"@vitejs/plugin-react": "^4.5.2",
|
|
32
|
+
"autoprefixer": "10.4.21",
|
|
33
|
+
"fast-glob": "^3.3.3",
|
|
34
|
+
"postcss": "8.5.6",
|
|
35
|
+
"serve": "^14.2.4",
|
|
36
|
+
"tailwindcss": "4.1.11",
|
|
37
|
+
"tsx": "^4.20.4",
|
|
38
|
+
"typescript": "^5.9.2",
|
|
39
|
+
"vite": "^7.1.1"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/render.yaml
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
services:
|
|
2
|
+
- type: web
|
|
3
|
+
name: pizzaz-mcp
|
|
4
|
+
runtime: node
|
|
5
|
+
plan: free
|
|
6
|
+
buildCommand: npm install && BASE_URL=https://pizzaz-mcp.onrender.com npm run build
|
|
7
|
+
startCommand: npm start
|
|
8
|
+
envVars:
|
|
9
|
+
- key: NODE_ENV
|
|
10
|
+
value: production
|
|
11
|
+
- key: PORT
|
|
12
|
+
value: 10000
|