Web Summer Camp 2026 Schedule Server
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Web Summer Camp 2026 Schedule ServerWhat talks are scheduled for Friday afternoon?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
MCP Apps Workshop — Web Summer Camp 2026 schedule
A reference implementation for the workshop "Building MCP Apps: Branded Experiences Inside ChatGPT" — a full ChatGPT app built on the OpenAI Apps SDK (which rides on top of the Model Context Protocol).
The finished app surfaces the Web Summer Camp 2026 JavaScript track schedule inside ChatGPT and lets attendees buy a conference ticket, all through natural language.
This repo is the workshop answer key. Each block has a matching
checkpoint/0Nbranch — check out that branch to see the finished state after block N, or diff against the previous branch to see exactly what changed.
What you'll build
By the end you have an MCP server that ChatGPT talks to over HTTP, exposing:
Tool | Kind | What it does |
| data | Returns the schedule as JSON so the model can answer questions. |
| render | Same data, but attaches a widget so ChatGPT paints the UI. |
| mutation | Buys an early-bird or standard pass. OAuth-gated in production. |
…and a widget rendered inside ChatGPT that groups the schedule by day, highlights time slots with tabular numerals, and swaps a Buy ticket CTA for a You're going ✓ confirmation once the mutation lands.
Related MCP server: Centia MCP Server
Workshop map
Block | Focus | Branch | Command |
1 | Warmup + environment gate |
|
|
2 | Build the MCP server |
|
|
3 | Connect to ChatGPT via ngrok |
|
|
4 | Ship the widget |
|
|
5 | Add the |
|
|
6 | OAuth for the mutation |
|
|
Quickstart
nvm use # Node 20+
npm install
cp .env.example .env # OPENROUTER_API_KEY unlocks block-1 warmup
npm run doctor # verifies Node, ngrok, and that the server boots
npm run warmup # first block — stream tokens from an LLMThen jump to a checkpoint:
git checkout checkpoint/02 # or 03, 04, 05, or main for the full app
npm run devEverything runs with zero secrets by default (AUTH_MODE=noauth).
OAuth is strictly additive — set AUTH_MODE=oauth plus the IDP_* env
vars from .env.example to switch it on.
Repo layout
server/ MCP server
index.ts HTTP entry, mounts POST /mcp
http.ts CORS + JSON helpers
store.ts In-memory data + seed
schemas.ts Zod schemas shared across tools
widget.ts Registers the widget as an MCP App resource
tools/ One file per tool
auth/ JWKS Bearer verification (checkpoint/06)
web/
public/ The widget HTML — served to ChatGPT as-is
scripts/
doctor.ts Pre-flight environment check
warmup.ts Block-1 streaming warmupConnect to ChatGPT
The Apps SDK is in preview. You enable Developer Mode once, then treat every change to the server as a "reload the connector" cycle.
Run the server locally.
npm run dev # tsx watch, PORT=8787Expose it publicly. ChatGPT needs to reach your server, so tunnel through ngrok:
ngrok http 8787Copy the
https://…ngrok.appURL. Your connector URL is that +/mcp.Enable Developer Mode in ChatGPT once (Settings → Advanced → Developer Mode). This unlocks the "Create connector" flow.
Create the connector. Settings → Apps and Connectors → Create. Name it (e.g. "WSC Schedule — local"), paste the ngrok URL with
/mcp, leave auth as No authentication, and Create.Enable the connector in a chat. In a new chat, click + → toggle the connector on. Then ask "what's on the JavaScript track Friday?" or "when is Tejas's talk?".
The refresh-after-every-change rule
ChatGPT caches tool descriptors and widget HTML aggressively. Whenever you change server code:
Save (tsx watch restarts the server).
In ChatGPT, click the connector chip in the composer → Refresh.
In stubborn cases, start a new chat.
If you change the widget HTML without changing its ui:// URI, ChatGPT
may serve the cached copy indefinitely. Bump the URI (e.g.
ui://widget/schedule-v2.html) when you want to force a hard reload — the
Apps SDK docs recommend this pattern explicitly.
Verify without ChatGPT
# Health
curl -s http://localhost:8787/ | jq
# Initialize
curl -s -X POST http://localhost:8787/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0.0.0"}}}' | jq
# tools/list
curl -s -X POST http://localhost:8787/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | jqOr use the MCP Inspector — its Auth panel is invaluable when you get to block 6.
How the widget talks to the server
The widget lives inside an iframe that ChatGPT hands to your MCP server. It never fetches your backend directly — it can't, because the user's Bearer token isn't in the iframe. Everything goes through the host:
structuredContent
MCP server ────────────────────────► window.openai.toolOutput
│
│ read on mount +
│ on 'openai:set_globals'
▼
widget renders
│
│ user clicks "Buy standard"
▼
window.openai.callTool(
'buy_ticket', { tier }
)
│
ChatGPT forwards │
MCP server ◄──────────────────────────┘
│
└─► tool handler writes to store, returns updated structuredContent
│
└────► resolves widget's callTool promise
│
▼
widget swaps returned data into state,
re-renders in the "you're going" branchThree details from web/public/schedule-widget.html worth calling out:
The subscription. We read
window.openai.toolOutputon boot, then listen for theopenai:set_globalscustom event to reactively pick up new toolOutput (or new theme, or new displayMode) without polling. This is the vanilla equivalent of the SDK'suseOpenAiGlobalhook.The mutation returns the whole schedule.
buy_ticketreturns the same shape asrender_schedule— schedule + ticket. The widget just overwrites state with the returned object; there's no diffing or merge logic to get wrong.The gotcha. In OAuth mode the widget cannot make an authenticated fetch to your MCP server — the user token doesn't cross the iframe boundary. Always route mutations through
window.openai.callTooland let the host attach the token.
The OAuth path
AUTH_MODE=noauth is fine for the whole workshop up to block 5. Block 6
switches on OAuth for the mutation only; the schedule tools stay open so
"list my talks" still works before login.
AUTH_MODE=oauth \
IDP_KIND=stytch \
IDP_ISSUER=https://your-project.stytch.com \
IDP_AUDIENCE=https://your-mcp.example.com \
IDP_REQUIRED_SCOPE=tickets.write \
MCP_RESOURCE_URL=https://your-mcp.example.com \
npm run devUnder the hood, on every request:
HTTP peeks at the JSON body. If it isn't a
tools/callfor a gated tool (currently justbuy_ticket), no auth is required.The Bearer token is extracted from
Authorization: Bearer ….JWKS is fetched and cached from
IDP_JWKS_URI(defaults to${issuer}/.well-known/jwks.json).jose.jwtVerifyverifies signature, issuer, audience, and expiry in one call.Scopes are checked against
IDP_REQUIRED_SCOPE. We look atscope(space-delimited),scp,scopes, andpermissions— every IdP formats them differently.The
subclaim becomes the ticket owner.store.buyTicket(subject, …)uses it as the key, so each authenticated user gets their own ticket state.Missing / invalid / expired token → 401. The response carries a
WWW-Authenticate: Bearer resource_metadata="…"header pointing at/.well-known/oauth-protected-resource, which tells ChatGPT which auth server to send the user to.ChatGPT walks the discovery chain. Protected Resource metadata → authorization server metadata → OAuth 2.1 + PKCE authorization code flow → the user logs in → a fresh token attaches to the next
tools/calland the mutation runs.
IdP tenant setup
You need a tenant that speaks OAuth 2.1 with authorization code + PKCE and publishes JWKS. Both adapters do; the setup steps differ.
Create a Stytch Consumer project (Live or Test).
Connected Apps → Create new app. Name it "WSC Schedule".
Add the redirect URI ChatGPT uses:
https://chatgpt.com/connector_platform_oauth_redirect.Custom scopes → add
tickets.writewith a description like "Buy conference tickets".Grab Project ID and Public token — you'll set these in
.env:AUTH_MODE=oauth IDP_KIND=stytch IDP_ISSUER=https://<your-project>.stytch.com IDP_AUDIENCE=https://<your-mcp-domain> # your server's public origin IDP_REQUIRED_SCOPE=tickets.write MCP_RESOURCE_URL=https://<your-mcp-domain>Add a test user in Stytch. That user is who you'll log in as when ChatGPT prompts you.
Restart the server. On boot you should see:
[auth] oauth enabled · idp=Stytch · issuer=… · scope=tickets.write.In ChatGPT, refresh the connector. Try "buy me an early-bird ticket" → ChatGPT surfaces the Stytch login → after login the tool runs and the widget shows the confirmation card.
Full guide: Stytch's Apps SDK walkthrough.
Create an Auth0 tenant.
Applications → Regular Web Application for the ChatGPT client. Add the redirect URI:
https://chatgpt.com/connector_platform_oauth_redirect.Applications → APIs → Create API for this MCP server:
Identifier:
https://<your-mcp-domain>(this becomes theaudclaim →IDP_AUDIENCE).Signing algorithm: RS256.
Add a permission
tickets.write.
In your
.env:AUTH_MODE=oauth IDP_KIND=auth0 IDP_ISSUER=https://<your-tenant>.us.auth0.com/ IDP_AUDIENCE=https://<your-mcp-domain> IDP_REQUIRED_SCOPE=tickets.write MCP_RESOURCE_URL=https://<your-mcp-domain>Add a test user in Auth0.
Restart the server; you should see the Auth0 label at boot.
Refresh the connector in ChatGPT and try to buy.
Full guide: Auth0's MCP AI docs.
Troubleshooting
502 in the connector wizard. The wizard hit an unimplemented route
and ngrok/your proxy returned 502 instead of 404. Our server serves
explicit 404 · Cache-Control: no-store for
/.well-known/oauth-* in noauth mode to avoid this. If you see 502
anyway, tail ngrok — it's usually your server crashed, not the wizard.
Tool doesn't appear in ChatGPT. Click the connector chip → Refresh. If it still doesn't appear, start a new chat. The tool list is cached per-conversation.
Widget stays on the old version. ChatGPT caches widget HTML by
ui:// URI. Bump the URI (e.g. schedule-v2.html → schedule-v3.html)
and the host re-fetches. server/widget.ts isolates the URI so this is
a one-line change.
401 loop in oauth mode. Check the boot log — bad IDP_ISSUER or
IDP_AUDIENCE values won't fail at boot but will fail at verify time.
The MCP Inspector's Auth panel shows the exact 401 body,
including the WWW-Authenticate challenge.
"Insufficient scope" after login. The test user has a token, but
the token doesn't carry tickets.write. Grant the scope to the user in
your IdP, or change IDP_REQUIRED_SCOPE.
The Apps SDK is in preview and moves fast. If anything above diverges from the docs, follow the docs and open an issue.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/TejasQ/mcp-apps-workshop'
If you have feedback or need assistance with the MCP directory API, please join our Discord server