Skip to main content
Glama

ChatGPT App with OAuth2 + MCP + Privy

by Jahnik

ChatGPT App with OAuth2 + MCP + Privy

A complete ChatGPT App implementation using the OpenAI Apps SDK (MCP), with OAuth2 authentication via Privy.io.

๐Ÿ—๏ธ Architecture

  • Backend: Express + MCP Server (TypeScript/Bun)

  • OAuth UI: React + Privy + React Router

  • Widgets: React components (rendered in ChatGPT)

  • Auth: OAuth2 with PKCE + Privy.io

  • Package Manager: Bun

๐Ÿ“ Project Structure

mcp2/ โ”œโ”€โ”€ src/ โ”‚ โ”œโ”€โ”€ server/ # Express + MCP server โ”‚ โ”‚ โ”œโ”€โ”€ oauth/ # OAuth2 endpoints โ”‚ โ”‚ โ”œโ”€โ”€ mcp/ # MCP tools & resources โ”‚ โ”‚ โ”œโ”€โ”€ api/ # Backend API integration โ”‚ โ”‚ โ””โ”€โ”€ middleware/ # Auth middleware โ”‚ โ”œโ”€โ”€ client/ # OAuth authorization UI โ”‚ โ””โ”€โ”€ widgets/ # ChatGPT widget components โ”œโ”€โ”€ dist/ โ”‚ โ”œโ”€โ”€ client/ # Built OAuth UI โ”‚ โ”œโ”€โ”€ widgets/ # Built widget bundles โ”‚ โ””โ”€โ”€ server/ # Compiled server โ””โ”€โ”€ package.json

๐Ÿš€ Quick Start

Prerequisites

  • Bun installed

  • Privy.io account and app created

  • OpenSSL (for generating JWT keys)

1. Install Bun

curl -fsSL https://bun.sh/install | bash

2. Install Dependencies

bun install

3. Generate JWT Keys

# Generate RSA key pair for JWT signing openssl genrsa -out private-key.pem 2048 openssl rsa -in private-key.pem -pubout -out public-key.pem # Base64 encode for .env echo "JWT_PRIVATE_KEY=$(cat private-key.pem | base64)" echo "JWT_PUBLIC_KEY=$(cat public-key.pem | base64)" # Clean up PEM files rm private-key.pem public-key.pem

4. Configure Environment

cp .env.example .env # Edit .env with your values: # - PRIVY_APP_ID (from Privy dashboard) # - PRIVY_APP_SECRET (from Privy dashboard) # - JWT_PRIVATE_KEY (from step 3) # - JWT_PUBLIC_KEY (from step 3) # - BACKEND_API_URL (your existing backend)

5. Build & Run

IMPORTANT: Widgets must be built before starting the server!

# First time: Build widgets (required!) bun run build:widgets # Then start development server bun run dev

The server will start at http://localhost:3002

๐Ÿ”ง Development

Understanding the Widget Build Process

โš ๏ธ Key Point: bun run dev does NOT automatically build widgets. You must build them separately!

There are three development workflows:

Option 1: Manual Build (Recommended for first-time setup)

# 1. Build widgets once bun run build:widgets # 2. Start server with auto-reload bun run dev # 3. Rebuild widgets manually when you change widget code bun run build:widgets

Option 2: Watch Mode (Recommended for active widget development)

# Terminal 1: Build widgets in watch mode (auto-rebuilds on changes) bun run dev:widgets # Terminal 2: Run server with auto-reload bun run dev

Option 3: Run Everything (Most convenient)

# Runs both server AND widget watch mode simultaneously bun run dev:all

Other Development Commands

# Type check bun run type-check # Run tests bun test # Build everything for production bun run build

Project Configuration

Server: src/server/index.ts

  • OAuth endpoints: /authorize, /token, /.well-known/*

  • MCP endpoint: /mcp

  • Health check: /health

OAuth UI: src/client/src/App.tsx

  • Authorization page with Privy login

  • Consent screen

  • Built with Vite + React + React Router

Widgets: src/widgets/src/

  • ListView: Interactive list with actions

  • Built as standalone bundles

  • Communicate via window.openai API

๐Ÿงช Testing

Test with MCP Inspector

# Terminal 1: Run server bun run dev # Terminal 2: Run MCP Inspector bunx @modelcontextprotocol/inspector http://localhost:3002/mcp

Test with ngrok

# Expose local server ngrok http 3002 # Copy the HTTPS URL (e.g., https://abc123.ngrok.app) # Use this URL in ChatGPT Settings โ†’ Connectors

Connect to ChatGPT

  1. Enable Developer Mode:

    • ChatGPT Settings โ†’ Apps & Connectors โ†’ Advanced settings

    • Enable "Developer mode"

  2. Create Connector:

    • Settings โ†’ Connectors โ†’ Create

    • Name: "Your App Name"

    • Description: "What your app does"

    • Connector URL: https://your-server.com/mcp (or ngrok URL)

  3. Test OAuth Flow:

    • Start a new ChatGPT conversation

    • Click + โ†’ More โ†’ Select your connector

    • You'll be redirected to /authorize

    • Log in with Privy

    • Grant consent

    • ChatGPT receives OAuth token

  4. Test Tools:

    • Ask ChatGPT: "Show me my items"

    • The get-items tool will be called

    • Widget will render in ChatGPT

๐Ÿ“ฆ Production Build

# Build everything bun run build # Run production server bun run start # Or preview locally bun run preview

Docker Deployment

# Build image docker build -t chatgpt-app . # Run container docker run -p 3000:3000 --env-file .env chatgpt-app

Deploy to Fly.io

# Install flyctl curl -L https://fly.io/install.sh | sh # Create app fly launch # Set secrets fly secrets set PRIVY_APP_ID=xxx fly secrets set PRIVY_APP_SECRET=xxx fly secrets set JWT_PRIVATE_KEY=xxx fly secrets set JWT_PUBLIC_KEY=xxx fly secrets set BACKEND_API_URL=xxx # Deploy fly deploy

๐Ÿ” OAuth2 Flow

  1. ChatGPT redirects user to /authorize?client_id=...&code_challenge=...

  2. Server serves React UI (Privy login)

  3. User authenticates with Privy

  4. Frontend shows consent screen

  5. User approves, server generates authorization code

  6. Frontend redirects back to ChatGPT with code

  7. ChatGPT exchanges code for access token at /token

  8. Server validates PKCE, issues JWT

  9. ChatGPT uses JWT for /mcp requests

๐ŸŽจ Adding New Tools

1. Define Tool in src/server/mcp/tools.ts

{ name: 'my-new-tool', description: 'What the tool does', inputSchema: { type: 'object', properties: { param: { type: 'string' } }, required: ['param'] } }

2. Implement Handler

async function handleMyNewTool(args: any, auth: any) { // Validate auth // Call backend API // Return structured response }

3. Link to Widget (Optional)

_meta: { 'openai/outputTemplate': 'ui://widget/my-widget.html', }

๐ŸŽจ Adding New Widgets

1. Create Widget Component

mkdir -p src/widgets/src/MyWidget

2. Build Widget

// src/widgets/src/MyWidget/index.tsx import React from 'react'; import ReactDOM from 'react-dom/client'; import { MyWidget } from './MyWidget'; const root = ReactDOM.createRoot(document.getElementById('root')!); root.render(<MyWidget />);

3. Configure Vite

// Update src/widgets/vite.config.ts build: { lib: { entry: { 'my-widget': 'src/MyWidget/index.tsx' } } }

4. Register Resource

// src/server/mcp/resources.ts await registerMyWidget(server, widgetPath);

๐Ÿ“š Environment Variables

Variable

Description

Required

PRIVY_APP_ID

Your Privy app ID

โœ…

PRIVY_APP_SECRET

Your Privy app secret

โœ…

VITE_PRIVY_APP_ID

Privy app ID (for frontend)

โœ…

JWT_PRIVATE_KEY

Base64-encoded RSA private key

โœ…

JWT_PUBLIC_KEY

Base64-encoded RSA public key

โœ…

SERVER_BASE_URL

Your server URL

โœ…

BACKEND_API_URL

Your existing backend URL

โœ…

PORT

Server port (default: 3000)

โŒ

NODE_ENV

Environment (development/production)

โŒ

๐Ÿ› Troubleshooting

Widgets not loading

# Build widgets first bun run build:widgets # Restart server bun run dev

OAuth flow fails

  • Check SERVER_BASE_URL matches your actual URL

  • Verify Privy app ID is correct

  • Check JWT keys are properly base64-encoded

  • Ensure redirect URI is registered in ChatGPT

Token validation fails

  • Verify JWT keys are correct (public/private pair)

  • Check token hasn't expired (1 hour default)

  • Ensure aud claim matches your server URL

MCP Inspector can't connect

# Ensure server is running bun run dev # Try: bunx @modelcontextprotocol/inspector http://localhost:3002/mcp

๐Ÿ“– Resources

๐Ÿ“ License

MIT

๐Ÿค Contributing

Contributions welcome! Please open an issue or PR.

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/Jahnik/mcp2'

If you have feedback or need assistance with the MCP directory API, please join our Discord server