This tutorial project is a ChatGPT App built with the Apps SDK and an MCP server. It demonstrates the “three kinds of state” model from the Apps SDK guide (Managing State):
- Business data (authoritative): tasks + notifications stored in Postgres (
Supabase). - UI state (ephemeral): widget state (selected task, which view is open) stored via
window.openai.setWidgetState. - Cross-session state (durable): per-user scoping using the host-provided anonymized id
_meta["openai/subject"]persisted asuser_subjectin the DB.
- Task management: Create, read, update, delete tasks; view them on a
Kanbanboard. - Notifications:
- Slack (optional): schedule messages or send instantly.
- ChatGPT Tasks: create a reminder intent and let the widget post a follow-up prompt for ChatGPT scheduling.
- Docker Desktop (required for
Supabaselocal) SupabaseCLI (for local dev):- macOS:
brew install supabase/tap/supabase
- macOS:
- Python: 3.11+
- uv (Python package manager):
brew install uv Node.js+npm(for building the widget bundles)
git clone https://github.com/hollaugo/tutorials.git
cd tutorials/task-manager-app
uv sync
cd web
npm install
npm run buildStart Supabase and run migrations:
cd task-manager-app
supabase start
supabase db resetOr run the helper script (recommended):
cd task-manager-app
./SUPABASE_START.shsupabase db resetapplies all SQL files insupabase/migrations/(and then runssupabase/seed.sql).- Studio will be available at
http://127.0.0.1:54323.
If you prefer a hosted DB:
- Create a
Supabaseproject. - Get your Postgres connection string (Pooler/Direct works; direct is simplest).
- Put it in
.env.localasDATABASE_URL=....
Create task-manager-app/.env.local (it is ignored by git) and set at least:
- Local DB default (if using
Supabaselocal):DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres
Optional (Slack notifications):
SLACK_BOT_TOKEN=xoxb-...SLACK_DEFAULT_CHANNEL=#general(or any channel the bot is in)
Tip: you can start from the template:
cp .env.example .env.localThis project can run in multi-user OAuth mode where ChatGPT authenticates users through Auth0 and the MCP server issues its own short-lived access tokens (scoped to the authenticated user).
Add the following to .env.local:
TASK_MANAGER_REQUIRE_AUTH=1TASK_MANAGER_OAUTH_MODE=auth0TASK_MANAGER_PUBLIC_URL=https://<your-ngrok-domain>AUTH0_DOMAIN=<your-tenant-domain>(e.g.dev-xxxx.us.auth0.com)AUTH0_CLIENT_ID=<your-auth0-app-client-id>AUTH0_CLIENT_SECRET=<your-auth0-app-client-secret>
- Create an Auth0 Application (Regular Web App works well for this tutorial).
- Ensure the Application has Allowed Callback URLs that include:
https://<your-ngrok-domain>/auth/callbackhttps://chatgpt.com/connector_platform_oauth_redirecthttps://platform.openai.com/apps-manage/oauth(recommended for app review/submission)
Notes:
- ChatGPT will attempt dynamic client registration against your MCP server; this project enables it automatically when
TASK_MANAGER_REQUIRE_AUTH=1. - Your Auth0 “API / Resource Server” identifier (audience) is still useful for RBAC and scope modeling, but this tutorial flow uses Auth0 primarily as the upstream identity provider.
cd task-manager-app
./START.sh- This script:
- loads
.env.local(if present) - defaults
DATABASE_URLtoSupabaselocal if missing - starts the MCP server on port 8000
- loads
In a separate terminal:
ngrok http 8000Copy the HTTPS forwarding URL and append /mcp:
- Connector URL:
https://<your-ngrok-domain>/mcp
Important: when your public tunnel URL changes, you must update:
.env.local:TASK_MANAGER_PUBLIC_URL=https://<your-ngrok-domain>- Auth0 Allowed Callback URLs:
https://<your-ngrok-domain>/auth/callback
FastMCP may reject unknown Host headers when fronted by ngrok. For local dev, add one of these to .env.local:
- Disable host header validation (dev-only):
TASK_MANAGER_DISABLE_DNS_REBINDING=1
- Or allowlist your
ngrokhost:TASK_MANAGER_ALLOWED_HOSTS=<your-ngrok-domain>
cd task-manager-app
./START.sh --inspectorThis runs the MCP Inspector in STDIO mode so you can call tools without ChatGPT.
To send Slack messages:
- Create a Slack App + Bot token.
- Use the example manifest in
slack-app-manifest.example.jsonas a starting point. - Required scopes (bot):
chat:write(post messages + schedule messages)channels:join(lets the bot join channels; still invite it if needed)chat:write.public(optional; can help but is not a guarantee for posting everywhere)
- Invite the bot to the channel you want to post in:
- In Slack:
/invite @YourBotName
- In Slack:
If you see not_in_channel, the bot needs to be added to that channel.
This tutorial does not require Slack Events or Interactivity (no request URLs needed). The MCP server only uses Slack Web API calls.
If you add events later, you’ll need to run a separate web endpoint and update Slack request URLs to your ngrok HTTPS domain.
Tasks:
- “Show my task board”
- “Create a task: Draft blog post due tomorrow at 5pm”
- “Move my Draft blog post task to In Progress”
Notifications:
- “Create a Slack notification to remind me in 1 hour: ‘Standup notes’”
- “Send a Slack message now: ‘Hello from Task Manager’”
- “Create a ChatGPT task reminder for tomorrow at 9am: ‘Pay rent’”
- Business state (DB)
supabase/migrations/*(schema + constraints)mcp_server/tools/tasks.py,mcp_server/tools/notifications.py(read/write + return authoritative snapshots)
- Per-user identity
mcp_server/state.py(openai/subject→user_subject)server.py(extract client meta and passuser_subjectinto tools)
- UI state
web/src/hooks.ts(useWidgetState,useToolOutput)web/src/ui/TaskBoardComponent.tsx(widget state + local UI state + refresh policy)
If supabase start fails with something like “port is already allocated”, another local supabase project is running.
- Stop all local
supabaseprojects:
supabase stop --all- Or change the db port in
supabase/config.tomlfor this project.