← Back to Blog

Part1: From Zero to SaaS ChatGPT App Tutorial Series

Nicholas Dickey

Part one of a four part video tutorial series on creating a SaaS ChatGPT App from scratch using vibe engineering methods and tools. In the first video we create a Skybridge HTML widget for our app - ChatVault using Cursor Code Agent and OpenAI Apps SDK.

Part1: From Zero to SaaS ChatGPT App Tutorial Series

From Zero to SaaS ChatGPT App – Part 1

Building the ChatVault Skybridge Widget With Vibe Engineering

This is Part 1 of a four-part video tutorial series:
Vibe Engineering From Zero to SaaS ChatGPT App.

In this series, we go all the way from:

  • “I have an idea for a ChatGPT App”
    to
  • “I have a real SaaS product with users, Stripe billing, CI/CD, and a maintainable codebase.”

Part 1 focuses on one thing:

Build a fully working Skybridge ChatGPT widget app called ChatVault, driven entirely by an AI code agent, using a method I call Vibe Engineering.

The companion video for Part 1 is about 1 hour 30 minutes. https://www.youtube.com/watch?v=l9eHFLzo1uo This blog post is the written summary:

  • What we built
  • Why we built it this way
  • How the prompts are structured
  • Where the repo lives
  • The exact prompts you can paste into Cursor if you want to ride the beast yourself.

What is a Skybridge ChatGPT Widget App?

Before we talk about Vibe Engineering or ChatVault, we need to anchor vocabulary.

OpenAI’s new Apps SDK lets you build ChatGPT Apps that can:

  • Expose tools (MCP-style actions)
  • Expose resources
  • Render HTML widgets inside ChatGPT conversations

Behind the scenes, this uses:

  • OpenAI-MCP / MCP-UI – a protocol for tools and UI
  • Skybridge – the mechanism that hosts HTML widgets inside ChatGPT, connects them to MCP tools, and lets them call back into the host

A Skybridge ChatGPT Widget App is basically:

  1. An MCP server that speaks the OpenAI-MCP protocol:
    • Handles initialize, tools/list, tools/call, resources/list, resources/read, etc.
  2. One or more UI resources, typically HTML+JS+CSS, referenced by a ui://... URI.
  3. A widget rendered in an iframe inside ChatGPT that:
    • Calls MCP tools via window.openai.callTool() (or equivalent)
    • Reads resources from the MCP server
    • Adapts to ChatGPT’s theme (light/dark)
    • Can show logs and errors inside itself

In our case:

  • The MCP server is written in Node (but we don’t manually write that code — the AI agent does).
  • The widget is a ChatVault UI that lets you browse saved chats, like a browser history.
  • Everything is wired through Apps SDK + MCP-UI + Skybridge so it runs inside ChatGPT.

What Is Vibe Engineering?

Most people start with vibe coding:

You open an AI coding assistant, describe what you want, and keep asking for fixes until the demo looks about right.

Vibe coding is fantastic for:

  • Quickly exploring ideas
  • Whipping up demos
  • Building internal tools that might never be touched again
  • Pitch prototypes and proof-of-concept UI

But it has a big weakness:
it does not scale to things you want to maintain, ship, or monetize.

That’s where Vibe Engineering comes in.

Vibe Coding vs. Vibe Engineering

Vibe Coding is great for:

  • Prototypes and experiments
  • One-off scripts
  • Quick throwaway integrations
  • UI sketches
  • “What if we tried this?” experiments

If the code will never see production, vibe coding is perfect.

Vibe Engineering is for:

  • Small, well-defined applets and services that need to be correct
  • Things you want to take to commercial release
  • Software that must be tested, deployed, and maintained
  • Pieces of a larger SaaS (widgets, MCP servers, microservices)
  • Anything that needs to be changed safely six months from now

Vibe Engineering keeps the fun and speed of AI coding, but adds:

  1. Version control discipline – every serious change is committed; you can always roll back.
  2. Test coverage – tests mirror your requirements and catch regressions.
  3. CI/CD mindset – even small modules are built and tested automatically.
  4. Prompt engineering as architecture – prompts are structured, reusable, and define the process.
  5. Context management - maintaining the code agent context.
  6. Small, composable units – each widget/server/action is small enough for an AI agent to reason about.

Why Vibe Engineering is a Perfect Match for a ChatGPT Widget App like ChatVault

A Skybridge widget app like ChatVault, separated from any backend concerns. is:

  • Small enough for an AI agent to implement end-to-end
  • Structured enough (MCP protocol, tools, resources, widget HTML) that tests can be meaningful
  • Valuable enough that correctness, debuggability, and maintainability matter

ChatVault is exactly that kind of project:

A ChatGPT widget that lets you browse and review your past chats like a browser history:
  • A list of saved chats
  • Each chat has turns (prompt + response)
  • Click to expand, collapse, and copy content
  • All rendered inside ChatGPT

This is not a throwaway demo:

  • It serves as a reusable pattern for other widget apps.
  • It will later be connected to backend storage, user accounts, and Stripe plans.
  • It will be used as a teaching project for SaaS architecture.

So we apply Vibe Engineering:

  • The AI agent does the coding.
  • We drive with carefully structured prompts.
  • We insist on tests, logging, and observability.
  • We treat this as the first brick in a real SaaS app, not a weekend toy.

The ChatVault Project (Part 1 Scope)

In Part 1, we build:

  • A ChatVault Skybridge widget that:
    • Takes a list of “saved chats”
    • Displays them like a history list
    • Lets the user expand a chat to see all turns
    • Truncates long text with ellipses, but reveals full content when clicked
    • Provides “copy to clipboard” buttons with a five-second green checkmark confirmation
    • Has a collapsible debug panel at the bottom
    • Detects and adapts to dark mode
  • An MCP server with four actions:
    • saveChat
    • loadChats
    • searchChat
    • browseSavedChats
  • For Part 1:
    • loadChats returns hard-coded sample data (fake chats)
    • saveChat and searchChat are dummy placeholders
    • browseSavedChats is the action that opens the widget
    • loadChats accepts a userId parameter but the widget doesn’t set it yet (wiring user identity comes later)
  • A test harness using Jest, running end-to-end against the real MCP server.

All of this is done by driving Cursor’s Code Agent with structured prompts — not by hand-writing TypeScript.

Tools and Prerequisites

For Part 1, you’ll need:

  • Git – for cloning the tutorial repo and letting the AI make commits.
  • GitHub account – to host your own copy if you want.
  • Ngrok – to expose your local MCP server to ChatGPT Dev Mode.
  • Cursor – these prompts are tuned for Cursor’s Code Agent.
  • ChatGPT Plus – so you can enable Developer Mode and register your own app.

Starter Repo

The series uses a starter repo:

GitHub: https://github.com/findexar/chatvault-tutorial

Inside, for Part 1, you’ll find:

  • prompts/part1/openai-AppsSDK-prompt.mdgeneric Apps SDK + MCP + widget prompts
  • prompts/part1/chatVaultPrompts.mdChatVault-specific prompts

You can:

  1. Clone the repo and follow along exactly
    or
  2. Ignore the repo and just copy/paste the prompts below into your own Cursor workspace.

This blog includes the full prompt texts in code blocks for option (2).

High-Level Flow of the Part 1 Video (1h 30m)

The video walks through these stages, all using Vibe Engineering:

  1. Prepare the environment
    • Clone chatvault-tutorial
    • Open it in Cursor
    • Review the two prompt files for Part 1
  2. Prompt0 (from openai-AppsSDK-prompt.md): Clone SDK
    • AI clones the official openai-apps-sdk-examples repo
    • Detaches it from its original Git history
    • Renames pizzaz_server_nodemcp_server
    • Cleans out Python and other unused directories
  3. Prompt1: Refactor SDK Example
    • AI turns the pizza example into a generic Apps SDK + MCP + widget project:
      • Single widget target
      • Inlines widget assets (HTML+JS+CSS)
      • Switches to HTTP POST MCP transport (no SSE/NDJSON)
      • Adds a structured MCP server with correct handlers
      • Adds in-widget debug logging
  4. Prompt2: Install + Server + Ngrok + Logging
    • AI:
      • Installs dependencies
      • Creates a single command to start the MCP server
      • Adds a script or shell helper for ngrok
      • Adds detailed logging on /mcp and MCP handlers
  5. Prompt3: Jest + E2E Tests
    • AI sets up Jest
    • Creates initial end-to-end tests for the future browse action
    • Ensures tests hit the live MCP server via /mcp with proper JSON-RPC
  6. Prompt4 (from chatVaultPrompts.md): ChatVault-specific behavior
    • AI:
      • Defines saveChat, loadChats, searchChat, browseSavedChats on the MCP server
      • Implements the ChatVault widget’s UI (history list, expand/collapse, copy, debug panel)
      • Wires browseSavedChats to return the widget
      • Makes loadChats a hard-coded paged fetch of sample chats
      • Ensures dark mode support
    • Then, it revisits Prompt3 and updates Jest tests to cover the real ChatVault behavior.
  7. Prompt5 (from chatVaultPrompts.md): Isolated widget test on port 4444
    • AI sets up:
      • A simple static server (e.g., npx serve assets -l 4444)
      • A local URL like http://localhost:4444/chat-vault.html
    • You test:
      • That the widget loads in a normal browser (outside ChatGPT)
      • That basic UI interactions work
      • That the widget behaves sensibly when window.openai is missing
  8. Register in ChatGPT Dev Mode
    • Start the MCP server
    • Start ngrok
    • Register a new app in ChatGPT Dev Mode pointing at your MCP endpoint
    • Trigger browseSavedChats
    • See the ChatVault widget render inside ChatGPT with the fake chat data

All of this is accomplished by feeding prompts and logs back and forth with the AI agent — a full vibe engineering session.

Full Generic Prompts (Part 1)

For readers who don’t want to start from the GitHub repo and would rather drive everything with prompts, here’s the full generic prompt file for Part 1.

prompts/part1/openai-AppsSDK-prompt.md

Title: Generic OpenAI Apps SDK Vibe Coding PROMPTS

Prerequisites:

- git
- ngrok
- Cursor (these prompts are tuned for running inside the Cursor editor)

This document defines **generic prompts** for building an Apps SDK + MCP + widget project.
Project-specific behavior (tools, data model, widget UI) should be defined in that project's own prompt file as its **Prompt4** (or similar).

## Engineering Principles (for all prompts)

- **Verify, don’t guess**: When behavior depends on external systems (Apps SDK, Skybridge, ChatGPT host, MCP spec), consult the latest docs or run a minimal experiment before changing code. Do not rely solely on heuristics or assumptions.
- **Prefer build-time solutions over runtime surgery**: When you need a different bundle shape (modules vs. classic scripts, different entrypoints, etc.), favor changing the bundler/build config (for example, Vite/Rollup/Webpack) over ad-hoc string manipulation of compiled JS/HTML. If you must transform compiled output, keep it small, explicit, and tested.
- **Test in both isolated and host contexts**: Validate widgets both in a standalone browser page (pure UI/runtime behavior) and in the full Apps SDK + MCP + Skybridge context (protocol, tools, resources). Localize bugs to one context before changing both.
- **Design for graceful degradation and bounded behavior**: Widgets and servers should detect missing capabilities (for example, `window.openai`, MCP endpoints) and fail in a controlled, observable way (clear error, no infinite retries or unbounded logging), rather than hanging or degrading the host UI.

project name - `${PROJECT_NAME}`

Prompt0: Clone SDK

Prepare the Apps SDK examples (spec reference)

1. Clone from Apps SDK

- Check out the official Apps SDK examples:
- `git clone https://github.com/openai/openai-apps-sdk-examples.git`
- This cloned repository will be modified in place to become `${PROJECT_NAME}` (starting in Prompt1)
- Treat the `pizzaz_server_node` example as the **canonical reference implementation** for:
- MCP JSON-RPC request/response shapes
- The `initialize`, `tools/list`, `tools/call`, `resources/list`, and `resources/read` handler behavior
- The way UI resources and widget templates are exposed (including `ui://...` IDs and how they resolve to HTTP URIs)
- Build process structure and file organization

2. Detach from GitHub repository

- Remove the `.git` directory to detach from the original repository
- This makes it a standalone project (a new repository will be created in a later video)
- After detaching, initialize a new Git repository for `${PROJECT_NAME}` and publish it to your own GitHub account:
- `git init`
- `git add .`
- `git commit -m "Initial commit for ${PROJECT_NAME} based on OpenAI Apps SDK examples"`
- Create an empty repository on GitHub (for example, `https://github.com/<USER>/${PROJECT_NAME}`)
- `git remote add origin git@github.com:<USER>/${PROJECT_NAME}.git` (or the HTTPS URL if you prefer)
- `git push -u origin main` (or `master`, depending on your default branch)

3. Rename server directory

- Rename `pizzaz_server_node/` to `mcp_server/`
- Update any references in build scripts or configuration files
- rename project directory
- remove python directories

Prompt1: Refactor SDK Example

Starting from the cloned SDK examples repository, refactor it into `${PROJECT_NAME}` by making the following changes:

Requirements (non-negotiable):

1. Widget foundation

- Choose one widget from the SDK example as the foundation (the project-specific Prompt4 will specify which one)
- Rename that widget's source directory in `src/` to match your project's widget name (as specified in Prompt4)
- In `build-all.mts`, remove all other widgets from the `targets` array, keeping only your project's widget (as specified in Prompt4)
- The widget will be built to `assets/${WIDGET_NAME}.html`, `assets/${WIDGET_NAME}.js`, and `assets/${WIDGET_NAME}.css` (where `${WIDGET_NAME}` is specified in Prompt4)
- Remove all remaining pizza directories from the project, todo and utils too.

1a. Widget tool calling format

- When widgets call MCP tools from within the widget (using `window.openai.callTool()`), handle response metadata formats correctly:
- **ChatGPT format**: When using `window.openai.callTool()`, ChatGPT returns tool response data in `result.meta` (not `result._meta`)
- **MCP format**: The MCP server returns data in `result._meta` in the JSON-RPC response, but ChatGPT transforms this to `result.meta` when exposing it to widgets
- Widget code should check `result.meta` first, then fall back to `result._meta` for backwards compatibility
- Example: If the MCP server returns `_meta: { chats: [...], pagination: {...} }`, the widget should access it via `result.meta.chats` when called via `window.openai.callTool()`

1b. Widget debug logging (recommended)

- Because widget iframes may not expose their console in all environments, add a small in-widget **debug log panel**:
- Maintain a `logs` state array and a `log(level, msg, meta?)` helper that also best-effort mirrors to `console`.
- Log at least: widget mount, theme detection, MCP API detection, all `window.openai.callTool` / `window.tools.call` attempts, response-shape decisions, and errors.
- Render logs in a collapsible panel at the bottom of the widget so issues can be diagnosed even when the iframe console is not visible.

2. MCP server structure

- Keep the `widgets` array structure in `mcp_server/src/server.ts` (for easy expansion later)
- Replace the widgets array with a single entry for your project's widget (name, templateUri, and metadata as specified in Prompt4)
- Keep all handler patterns identical to the SDK example (`tools/list`, `tools/call`, `resources/list`, `resources/read`)
- When in doubt, follow the SDK example structure exactly

3. Transport: Replace SSE with HTTP streaming

- Replace the SSE transport (`SSEServerTransport`, `GET /mcp`, `POST /mcp/messages`) with HTTP streaming (using standard HTTP POST requests, not SSE)
- Expose a single `POST /mcp` endpoint that:
- Uses the `@modelcontextprotocol/sdk` `Server` instance internally for all MCP behavior
- Handles JSON-RPC over HTTP: ChatGPT sends **one JSON-RPC request per HTTP POST** (not batches or NDJSON)
- For **ALL responses** (including `initialize`, `tools/list`, `tools/call`, `resources/list`, `resources/read`, and notifications):
- Set `Content-Type: application/json` header (NOT `application/x-ndjson`)
- Send a single JSON-RPC response object and immediately end the HTTP response
- **DO NOT use** NDJSON format or batch multiple responses
- Manually dispatch JSON-RPC requests and format responses
- For `initialize` requests: manually construct response with `capabilities: { resources: {}, tools: {} }` (both keys must be present) and set `mcp-session-id` header in the response
- For notifications (requests without an `id` field, like `notifications/initialized`): respond with HTTP `204 No Content` and set `mcp-session-id` header
- For all other requests: parse the JSON-RPC request body as a single JSON object (not NDJSON), dispatch to the appropriate handler, write the JSON-RPC response, and end the HTTP connection
- Keep session management pattern similar to the SDK example
- Keep everything else from the SDK example structure (file organization, build process, etc.)

**Implementation notes:**

- Parse request body as: `const requestData = JSON.parse(body)` (single JSON object, not splitting by newlines for NDJSON)
- Set response headers once at the start: `res.setHeader("Content-Type", "application/json")` (for ALL responses)
- Write response and end immediately: `writeJsonRpcResponse(res, id, result, error); res.end()`
- Each HTTP POST request gets exactly one response that completes before the next request
- "HTTP streaming" here refers to using standard HTTP POST (vs SSE), not NDJSON/batched responses

3a. Asset inlining for single-port deployment

- Implement asset inlining to make widgets self-contained and work with a single ngrok port
- When reading widget HTML from `assets/${WIDGET_NAME}.html`, process it to inline all external assets:
- Create a `localizeWidgetAssets(html: string, assetsDir: string): string` function that:
1. Reads the HTML string
2. Uses regex to find all `<script src="...">` tags pointing to JS files in the assets directory
3. For each script tag: read the JS file from `assets`, escape `</script>` sequences in the JS content, and replace the `<script src="...">` tag with an inline `<script>` tag containing the file contents
- If the JS bundle is ESM (for example, Vite output with `import`/`export`), preserve module semantics by using `<script type="module">…</script>` for the inlined script instead of a plain `<script>…</script>`.
4. Uses regex to find all `<link rel="stylesheet" href="...">` tags pointing to CSS files in the assets directory
5. For each stylesheet link: read the CSS file from `assets/` and replace the `<link>` tag with an inline `<style>` tag containing the file contents
6. Returns the processed HTML with all assets inlined
- Apply `localizeWidgetAssets()` when reading widget HTML (before storing it in the widget definition or returning it from `resources/read`)
- The processed HTML should be stored in the widget object so it's ready to return when `resources/read` is called
- Result: Widget HTML returned by `resources/read` should be self-contained with all JS and CSS inlined, requiring no external asset requests and enabling single-port deployment via ngrok
- Optional: Add a `GET /assets/*` route to serve raw assets for development/debugging (controlled by an env var like `INLINE_WIDGET_ASSETS=false`)

4. Project metadata

- Update server name in `mcp_server/src/server.ts`: `name: "${PROJECT_NAME}"`
- Update `package.json` name: `"${PROJECT_NAME}"`
- Keep all dependencies identical to the SDK example

---

Prompt2: Install + Server + Ngrok + Logging

Install all dependencies, start the MCP server, and create an `ngrok` script that shares the server port. Saturate the MCP path, including tools and resources, with `console.log` statements so we can observe end-to-end traffic.

Non-negotiables:

- Ensure the MCP server can be started with a single command (for example, `npm start`).
- Add a script (or simple shell file) that starts `ngrok http <PORT>` against the MCP server port.
- Add detailed logging to:
- The `/mcp` HTTP handler (incoming request body, handshake chunk, response chunk, errors).
- Each MCP method handler (`initialize`, `tools/list`, `tools/call`, `resources/list`, `resources/get`) logging `id`, `method`, and `params`.

---

Prompt3: Jest + e2e tests for project-specific browse action

Add Jest and create end-to-end tests to verify the project-specific **browse** action and its skybridge MCP-UI behavior.

Notes:

- The exact action name and behavior (for example, `browseSavedChats`, `browseSavedItems`, or similar) is defined in the **project-specific Prompt4**.
- When Prompt4 is implemented (adding real tools, resources, and widget behavior), you **must revisit and update these tests** so they:
- Exercise the MCP action end-to-end via the **real MCP server** (no mocks), calling `/mcp` exactly as the Apps SDK would.
- Validate that the skybridge widget can load and render the project’s browse view using the MCP resource and tools defined in Prompt4.
- Emit enough logging and assertions that we can prove coverage of the live MCP server implementation (including both the HTTP layer and the JSON-RPC handlers).
- Treat the **OpenAI MCP / Apps SDK examples as the spec for behavior and shapes**: tests should assert that JSON-RPC envelopes, method names, and tool/resource/result shapes remain compatible with the current examples, and treat any drift from those examples as a failing test to fix rather than an acceptable change.
- Include a **module-usage test for the widget bundle**: write a Jest/e2e test that calls the live MCP server’s `/mcp` endpoint to perform a `resources/read` on the widget template URI (for example, `ui://widget/chat-vault.html`), and assert that the returned HTML text contains a `<script type="module">` tag (or other host-supported construct) for loading the widget bundle. This ensures the inlined widget bundle preserves whatever module semantics the host supports (for example, ES modules in Skybridge) and can safely use modern tooling (for example, Vite + React) inside the ChatGPT widget iframe.
- After Prompt4 and Prompt5 are implemented, extend these tests (or add new ones) so they also cover the concrete widget behavior and failure modes described in Prompt5 (isolated widget on port 4444), in addition to the protocol-level assertions in this prompt.

ChatVault-Specific Prompts (Part 1)

Now the project-specific prompts for ChatVault.

prompts/part1/chatVaultPrompts.md

Title: ChatVault – Apps SDK / MCP Vibe Coding PROMPTS

project name - apps-sdk-tutorial-part1 (ChatVault)

This project uses the **generic Apps SDK prompts** defined in:

- `prompts/openai-AppsSDK-prompt.md`

Use that file for:

- **Prompt1**: Init Prompt (baseline stack, MCP server, widget, build pipeline).
- **Prompt2**: Install everything, start server, ngrok script, and logging on the MCP path.
- **Prompt3**: Jest + e2e tests for the project-specific browse action.

This file defines the **ChatVault-specific behavior** as Prompt4.

## Engineering Principles (ChatVault-specific)

- **Align with the generic prompts**: All work here inherits the engineering principles from `openai-AppsSDK-prompt.md` (verify, build-time over surgery, dual-context testing, graceful degradation). Do not introduce project-specific shortcuts that violate those principles.
- **Use Prompts 4 and 5 together**: Treat Prompt4 (MCP + widget behavior) and Prompt5 (isolated widget on port 4444) as a pair—when implementing or changing the widget, validate behavior both in isolation and through the live MCP server before assuming an issue is “host-side”.
- **Design for observability**: The ChatVault widget should surface its state clearly (loading vs. empty vs. error) and keep debug logging bounded and human-readable so future debugging sessions don’t require speculative changes.

---

Prompt4 (ChatVault-specific app/widget prompt):
--- Note: this is where the user defines the actual widget component(s) and MCP actions for ChatVault.

This is the MCP server we want to build: actions: `saveChat`, `loadChats`, `searchChat` and `browseSavedChats`.

- A **Chat** is `{ title, timestamp, turns[{ prompt, response }] }`.
- `browseSavedChats` returns the widget we are creating in this project.
- `loadChats` is a paged fetch, hardcoded with example data for this project.
- `saveChat` and `searchChat` are dummy functions for this project.
- The widget is like a Chrome history browser, internally calling `loadChats` via skybridge (window.openai.toolCall). It is a list of chats.When user clicks on a chat, it opens up, showing all the saved prompts and responses. The prompts and responses are truncated with ellipses by default, but when clicked, they show in full. Each has copy to clipboard button which changes to green checkmark for 5 secs when clicked.
- `loadChats` should have `userId` as a parameter, but we are not setting it inside the widget.
- at the bottom of the widget, add collapsible debug panel. Add logging to widget and show in the debug panel. Load widget initi and calling loadChats thoroughly.

Note: The widget must detect and adapt to dark mode (use `data-theme` attribute, CSS variables, and `dark:` Tailwind classes).

After you implement these ChatVault-specific tools, resources, and widget behavior for Prompt4, go back to **Prompt3** in `openai-AppsSDK-prompt.md` and **update the Jest + e2e tests** so they cover the actual live MCP server (including the new `browseSavedChats` behavior) end-to-end via `/mcp`. When doing so, take into account the isolated widget behavior and failure modes you validated in Prompt5 so tests exercise both protocol-level correctness and real widget behavior.

---

Prompt5 (Isolated ChatVault widget test on port 4444):

- Set up a simple static server to serve the built widget assets from the ChatVault project root:
- From the tutorial root:
- `cd /home/nick/chatvault-tutorial/chat-vault-part1`
- `npx serve assets -l 4444`
- Verify that the ChatVault widget can be loaded in isolation in a regular browser (outside ChatGPT) by opening:
- `http://localhost:4444/chat-vault.html`
- Use this isolated page to:
- Confirm that the widget HTML, JS, and CSS are valid and that React mounts successfully.
- Exercise basic UI interactions (header rendering, empty-history state, expand/collapse of turns, debug panel toggle) **without** requiring `window.openai.callTool`.
- Diagnose widget-only issues (for example, runtime errors, host-API absence, or layout/styling problems) independently of MCP transport and ChatGPT’s hosting behavior.
- Verify that the widget handles missing or delayed host APIs (for example, `window.openai`) in a bounded, observable way (clear error or retry message, no infinite retries or unbounded logging), and that purely local interactions (such as debug panel toggling) remain responsive.

What We Achieved in ~1.5 Hours Using Vibe Engineering

In about 1.5 hours of focused Vibe Engineering, riding the AI agent instead of hand-writing code, we:

  • Bootstrapped an Apps SDK + MCP + widget project from the official examples
  • Refactored it into a clean, single-widget ChatVault project
  • Implemented a robust HTTP Streaming -based MCP server with proper JSON-RPC handling
  • Inlined widget assets to support a single server deployment
  • Added structured logging across the MCP path
  • Set up Jest end-to-end tests hitting /mcp for real
  • Designed and built the ChatVault widget UI:
    • Chat history list
    • Expand/collapse per chat and per turn
    • Copy-to-clipboard UX
    • Dark mode support
    • Debug panel
  • Confirmed the widget runs both:
    • In isolation on http://localhost:4444/...
    • Inside ChatGPT via Skybridge and Dev Mode

This is the foundation of the four-part series. In later parts, we’ll:

  • Add real data persistence and backend logic (Part 2)
  • Turn ChatVault into a real SaaS with users and Stripe (Part 3)
  • Put it all under CI/CD and deploy it like a grown-up product (Part 4)