7 octobre 2025
OpenAI Apps SDK : développer des apps natives dans ChatGPT

12 minutes de lecture

Imaginez proposer à vos utilisateurs de commander un repas, réserver un trajet ou interagir avec vos APIs directement dans ChatGPT, sans jamais quitter la conversation. C'est exactement ce que permet l'Apps SDK d'OpenAI, fraîchement annoncé.
ChatGPT ne se contente plus d'être un assistant textuel : il devient une plateforme d'applications, comparable à l'App Store pour iOS ou au Play Store pour Android.
Les Apps ChatGPT : pourquoi ?
L'Apps SDK d'OpenAI transforme ChatGPT d'un simple assistant conversationnel en une plateforme d'applications. Avec plus de 100 millions d'utilisateurs actifs hebdomadaires, ChatGPT représente désormais un canal de distribution sans précédent pour les entreprises.
Cette transformation ouvre des perspectives commerciales. Les développeurs peuvent désormais créer des applications natives qui s'intègrent parfaitement dans l'expérience utilisateur de ChatGPT, offrant un accès direct à une audience engagée.
Comme toute technologie émergente, l'Apps SDK a ses contraintes. OpenAI est très clair sur les cas d'usage à éviter :
- Des contenus longs et complexes (mieux vaut un site web classique)
- Des workflows multi-étapes complexes (ChatGPT n'est pas optimisé pour ça)
- De la publicité ou du contenu promotionnel non sollicité (ça dégrade l'expérience utilisateur)
- L'affichage d'informations sensibles directement dans la conversation
L'architecture technique : MCP au cœur
Le protocole au cœur de l'architecture : MCP
Si vous êtes développeur, voici la bonne nouvelle : l'Apps SDK ne réinvente pas la roue. Elle s'appuie sur le Model Context Protocol, un standard ouvert créé par Anthropic et adopté par OpenAI.
Qu'est-ce que cela signifie concrètement ? Que le code que vous écrivez aujourd'hui pour ChatGPT pourrait fonctionner demain avec d'autres clients MCP. C'est rare dans l'écosystème IA, et c'est une excellente nouvelle pour pérenniser vos développements.
MCP définit trois primitives essentielles :
- List tools : votre serveur expose les outils disponibles avec leurs schémas
- Call tools : ChatGPT invoque vos outils avec les arguments appropriés
- Return components : chaque outil peut retourner une interface HTML à rendre
L'avantage ? Votre connecteur fonctionnera automatiquement sur ChatGPT web, mobile, et potentiellement sur tous les futurs clients compatibles MCP. Vous construisez une fois, vous déployez partout.
Choisir sa stack : TypeScript ou Python ?
Pour développer votre app, vous pouvez utiliser les SDKs MCP officiels :
TypeScript (notre préféré bien sûr ❤️) :
- @modelcontextprotocol/sdk
- Parfait si vous avez déjà une équipe Node.js/React
- S'intègre naturellement avec vos composants web existants
- Excellent outillage avec TypeScript, Zod, et l'écosystème moderne
Python :
- modelcontextprotocol/python-sdk
- Inclut FastMCP pour démarrer rapidement
- Idéal pour les équipes data science ou machine learning
- Pratique si votre backend est déjà en Python
Pour la suite de cet article, nous allons nous concentrer sur TypeScript, car c'est la stack que nous privilégions chez Premier Octet.
Implémentation technique : passons à la pratique
Construisons ensemble une app ChatGPT concrète. Nous allons créer un widget d'articles de blog Premier Octet, du serveur au composant React.
Étape 1 : Monter le serveur MCP
La première étape consiste à créer un serveur qui expose nos outils. Voici le code de démarrage :
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { z } from 'zod'
import { readFileSync } from 'node:fs'
// Initialiser le serveur MCP
const server = new McpServer({
name: 'premier-octet-blog',
version: '1.0.0',
})
// Charger les assets compilés du composant React
const BLOG_JS = readFileSync('web/dist/blog-widget.js', 'utf8')
const BLOG_CSS = readFileSync('web/dist/blog-widget.css', 'utf8')
// Enregistrer la ressource UI (template HTML)
server.registerResource('blog-widget', 'ui://widget/blog-articles.html', {}, async () => ({
contents: [
{
uri: 'ui://widget/blog-articles.html',
mimeType: 'text/html+skybridge',
text: `
<div id="blog-root"></div>
${BLOG_CSS ? `<style>${BLOG_CSS}</style>` : ''}
<script type="module">${BLOG_JS}</script>
`.trim(),
},
],
}))
Rien de compliqué ici : on initialise le serveur, on charge notre bundle React compilé, et on l'enregistre comme ressource. Le mimeType: 'text/html+skybridge'
est crucial : c'est le signal qui dit à ChatGPT "voici une interface à afficher". Skybridge est le runtime sandboxé de ChatGPT.
Étape 2 : Définir un outil
Maintenant, créons un outil que ChatGPT pourra invoquer. Vous définissez ce que votre app peut faire, et ChatGPT décide quand l'utiliser en fonction du contexte de la conversation.
server.registerTool(
'show_blog_articles',
{
title: 'Afficher les articles du blog Premier Octet',
description:
"Utilisez cet outil quand l'utilisateur veut découvrir les derniers articles du blog Premier Octet ou rechercher des articles par thème",
inputSchema: {
type: 'object',
properties: {
category: {
type: 'string',
description: "Catégorie d'articles (ex: react, typescript, openai)",
},
limit: { type: 'number', description: "Nombre d'articles à afficher (défaut: 6)" },
},
required: [],
},
_meta: {
'openai/outputTemplate': 'ui://widget/blog-articles.html',
'openai/widgetAccessible': true, // Permet les appels depuis le composant
'openai/toolInvocation/invoking': 'Chargement des articles...',
'openai/toolInvocation/invoked': 'Articles affichés',
},
},
async ({ category, limit = 6 }, context) => {
// Récupérer les articles depuis votre API
const articles = await fetchBlogArticles({ category, limit })
return {
// Données structurées pour le modèle ET le composant
structuredContent: {
articles: articles.map((article) => ({
id: article.id,
title: article.title,
excerpt: article.excerpt,
date: article.date,
author: article.author,
tags: article.tags,
slug: article.slug,
})),
total: articles.length,
category: category || 'tous',
},
// Texte pour le transcript de conversation
content: [
{
type: 'text',
text: `Voici ${articles.length} articles du blog Premier Octet${
category ? ` sur le thème "${category}"` : ''
}.`,
},
],
// Métadonnées privées (invisibles au modèle)
_meta: {
fullArticles: articles, // Données complètes pour le UI
searchQuery: category,
},
}
}
)
Remarquez comment nous séparons clairement les données :
structuredContent
: ce que le modèle ET le composant voient (gardez ça concis !)content
: le texte qui apparaîtra dans la conversation_meta
: les données privées, invisibles au modèle mais disponibles pour votre UI
Étape 3 : Construire le composant React
Vous développez déjà des composants React ? Bonne nouvelle : vous pouvez les utiliser ! La seule différence, c'est qu'ils tournent dans une iframe sandboxée et communiquent avec ChatGPT via l'API window.openai
.
import React, { useEffect, useState } from 'react'
import { createRoot } from 'react-dom/client'
interface BlogData {
articles: Array<{
id: string
title: string
excerpt: string
date: string
author: string
tags: string[]
slug: string
}>
total: number
category: string
}
function BlogWidget() {
// Récupérer les données initiales du tool
const initialData = window.openai?.toolOutput as BlogData
const [articles, setArticles] = useState(initialData)
// Persister l'état local dans ChatGPT
const saveState = async (newArticles: BlogData) => {
setArticles(newArticles)
await window.openai?.setWidgetState?.({
version: 1,
articles: newArticles,
lastModified: Date.now(),
})
}
// Appeler un outil depuis le composant
const searchArticles = async (category: string) => {
await window.openai?.callTool('show_blog_articles', {
category,
limit: 6,
})
}
// Gérer les changements de layout
const displayMode = window.openai?.displayMode || 'inline'
const maxHeight = window.openai?.maxHeight
return (
<div
style={{ maxHeight }}
className={displayMode === 'fullscreen' ? 'fullscreen-layout' : 'inline-layout'}
>
<div className="blog-header">
<h2>Articles Premier Octet</h2>
<div className="category-filters">
<button onClick={() => searchArticles('')}>Tous</button>
<button onClick={() => searchArticles('react')}>React</button>
<button onClick={() => searchArticles('typescript')}>TypeScript</button>
<button onClick={() => searchArticles('openai')}>OpenAI</button>
</div>
</div>
<div className="articles-grid">
{articles.articles.map((article) => (
<article key={article.id} className="article-card">
<h3>{article.title}</h3>
<p className="article-excerpt">{article.excerpt}</p>
<div className="article-meta">
<span className="author">{article.author}</span>
<span className="date">{new Date(article.date).toLocaleDateString('fr-FR')}</span>
</div>
<div className="article-tags">
{article.tags.map((tag) => (
<span key={tag} className="tag">
{tag}
</span>
))}
</div>
</article>
))}
</div>
</div>
)
}
// Monter le composant
createRoot(document.getElementById('blog-root')!).render(<BlogWidget />)
Ce composant illustre les concepts clés :
- Lecture des données initiales depuis
toolOutput
- Persistance de l'état avec
setWidgetState
(pour que l'état survive aux re-rendus) - Appels d'outils depuis le composant avec
callTool
pour filtrer par catégorie - Adaptation au layout (inline vs fullscreen)
- Interface utilisateur avec filtres et grille d'articles
Étape 4 : l'API window.openai
L'API window.openai
est votre pont entre le composant et ChatGPT :
// Données
window.openai.toolInput // Arguments passés à l'outil
window.openai.toolOutput // Réponse de l'outil (structuredContent)
window.openai.widgetState // État persisté entre les rendus
// Actions
await window.openai.setWidgetState({
/* state */
})
await window.openai.callTool('tool_name', {
/* args */
})
await window.openai.sendFollowupTurn({ prompt: '...' })
await window.openai.requestDisplayMode({ mode: 'fullscreen' })
// Layout & contexte
window.openai.displayMode // "inline" | "pip" | "fullscreen"
window.openai.maxHeight // Hauteur max disponible
window.openai.locale // "fr-FR", "en-US"...
window.openai.theme // "light" | "dark"
Ces méthodes couvrent 90% de vos besoins. Le reste de la documentation officielle complètera pour les cas avancés.
Étape 5 : sécuriser avec OAuth 2.1
Si votre app accède à des données utilisateur (ce qui sera souvent le cas), vous devez implémenter l'authentification. Bonne nouvelle : l'Apps SDK supporte OAuth 2.1 de bout en bout, avec vérification automatique des tokens.
import { FastMCP } from '@modelcontextprotocol/sdk/server/fastmcp.js'
// Configurer l'authentification
const mcp = new FastMCP({
name: 'secure-blog',
auth: {
issuerUrl: 'https://your-tenant.auth0.com',
resourceServerUrl: 'https://api.example.com/mcp',
requiredScopes: ['blog:read', 'blog:write'],
},
})
// Vérifier les tokens sur chaque appel
mcp.registerTool(
'show_blog_articles',
{
/* ... */
},
async ({ projectId }, { token }) => {
// Le token est automatiquement vérifié
const userId = token.subject
const hasAccess = token.scopes.includes('blog:read')
if (!hasAccess) {
throw new Error('Insufficient permissions')
}
// Charger les données de l'utilisateur
return await loadUserBoard(userId, projectId)
}
)
Pour accélérer l'implémentation, utilisez Better Auth. Il supporte OAuth 2.1, l'enregistrement dynamique des clients, et la gestion des scopes nativement.
Rendre votre app découvrable
La découverte de votre app par les utilisateurs est un enjeu crucial.
Les différents chemins vers votre app
ChatGPT offre plusieurs façons aux utilisateurs de découvrir et d'utiliser votre app :
- Mention explicite : "Montre-moi les articles du blog de Premier Octet"
- Découverte conversationnelle : le modèle choisit votre app selon le contexte
- Directory : répertoire des apps avec métadonnées et captures d'écran
- Launcher : bouton + dans le composer
La qualité de vos métadonnées fait toute la différence. ChatGPT décide d'appeler votre outil en analysant sa description. Comparez :
// ✅ Bon : description action-oriented et contextuelle
description: "Utilisez cet outil quand l'utilisateur veut visualiser les articles du blog de Premier Octet"
// ❌ Mauvais : trop vague, le modèle ne saura pas quand l'utiliser
description: 'Un outil pour afficher des articles'
Sécurité et vie privée
Une app ChatGPT a accès à des données utilisateur sensibles. OpenAI impose des standards stricts de sécurité, et vous devriez en faire autant.
Les principes de base
OpenAI impose des standards stricts :
- Principe du moindre privilège : ne demandez que les scopes nécessaires
- Consentement explicite : les utilisateurs doivent comprendre ce qu'ils autorisent
- Défense en profondeur : validez tout côté serveur, même si le modèle l'a fourni
- Minimisation des données : ne collectez que ce qui est strictement nécessaire
Sandboxing des composants
Les composants s'exécutent dans une iframe avec CSP stricte :
// Définir votre CSP
server.registerResource('blog-widget', 'ui://widget/blog-articles.html', {}, async () => ({
contents: [
{
uri: 'ui://widget/blog-articles.html',
mimeType: 'text/html',
text: componentHtml,
_meta: {
'openai/widgetCSP': {
connect_domains: ['https://api.example.com'],
resource_domains: ['https://cdn.example.com'],
},
},
},
],
}))
Pourquoi TypeScript est votre meilleur allié
On en a parlé au début, mais revenons-y : TypeScript est vraiment le choix optimal pour l'Apps SDK. Voici pourquoi :
Les avantages concrets
- Type safety : Zod + TypeScript vous évitent les bugs. Vos schémas serveur et client sont toujours cohérents.
- Réutilisabilité : vos composants React existants fonctionnent directement.
- Tooling performant : esbuild compile en millisecondes, TypeScript détecte les erreurs avant l'exécution
- Isomorphisme : un seul langage, des types partagés serveur/client, zéro friction
Exemple : types partagés
// shared/types.ts
import { z } from 'zod'
export const ArticleSchema = z.object({
id: z.string(),
title: z.string(),
excerpt: z.string(),
date: z.string(),
author: z.string(),
tags: z.array(z.string()),
slug: z.string(),
})
export const BlogDataSchema = z.object({
articles: z.array(ArticleSchema),
total: z.number(),
category: z.string(),
})
export type Article = z.infer<typeof ArticleSchema>
export type BlogData = z.infer<typeof BlogDataSchema>
// Serveur
server.registerTool(
'show_blog_articles',
{
inputSchema: z.object({
category: z.string().optional(),
limit: z.number().optional(),
}),
},
async ({ category, limit = 6 }) => {
const blogData: BlogData = await fetchBlogArticles({ category, limit })
return { structuredContent: blogData }
}
)
// Client React
function BlogWidget() {
const blogData = window.openai?.toolOutput as BlogData
// TypeScript sait que blogData.articles existe !
}
Vous voyez l'idée : un type défini une fois, utilisé partout. Zéro duplication, zéro désynchronisation.
L'UX comme critère de succès
Une excellente tech ne suffit pas si l'expérience utilisateur est mauvaise. OpenAI a publié des design guidelines très complètes. Voici ce qu'il faut retenir.
Les trois modes d'affichage
L'Apps SDK vous permet de choisir comment votre app s'affiche. Chaque mode a son usage :
- Inline : une carte légère dans la conversation — parfait pour une action simple (confirmer une réservation)
- Picture-in-Picture : une fenêtre flottante qui reste visible — idéal pour du contenu continu (vidéo, jeu)
- Fullscreen : une vue immersive avec le composer ChatGPT intégré — pour de l'édition complexe ou de l'exploration
function BlogWidget() {
const displayMode = window.openai?.displayMode
if (displayMode === 'fullscreen') {
return <FullscreenBlogView />
}
return (
<InlineCard>
<button
onClick={() => {
window.openai?.requestDisplayMode({ mode: 'fullscreen' })
}}
>
Ouvrir en plein écran
</button>
</InlineCard>
)
}
Le passage d'un mode à l'autre doit être fluide. Testez bien les trois cas.
Les principes de design à respecter
OpenAI insiste sur 5 principes fondamentaux. Ils peuvent sembler évidents, mais en pratique, beaucoup d'apps les ignorent :
- Conversationnel : votre app doit prolonger ChatGPT, pas créer une rupture
- Intelligent : utilisez le contexte de la conversation pour adapter l'interface
- Simple : une action claire par interaction — ne surchargez pas
- Responsive : ça doit être rapide. Si votre tool met 3 secondes à répondre, c'est trop
- Accessible : dark mode, tailles de texte, lecteurs d'écran — faites les choses bien
Gardez ces 5 principes constamment à l'esprit lors du développement.
Tester et débugger efficacement
MCP Inspector : votre meilleur ami
Avant même de toucher à ChatGPT, vous devez tester localement. Le MCP Inspector est l'outil indispensable :
npx @modelcontextprotocol/inspector@latest
# Pointer vers http://localhost:3000/mcp
L'inspector permet de :
- Lister tous les outils exposés
- Appeler les outils avec des paramètres
- Visualiser les réponses JSON
- Tester le rendu des composants
Tests automatisés
import { describe, it, expect } from 'vitest'
import { server } from './server.js'
describe('Blog Articles Tool', () => {
it('should return articles data', async () => {
const result = await server.callTool('show_blog_articles', {
category: 'react',
limit: 3,
})
expect(result.structuredContent).toHaveProperty('articles')
expect(result.structuredContent.articles).toBeInstanceOf(Array)
expect(result.structuredContent.total).toBeGreaterThan(0)
})
it('should filter by category', async () => {
const result = await server.callTool('show_blog_articles', {
category: 'typescript',
})
expect(result.structuredContent.category).toBe('typescript')
})
})
Debug dans ChatGPT
En mode développeur :
- Ouvrir les DevTools du navigateur
- Les composants s'affichent dans une iframe
- Les erreurs apparaissent dans la console
- Utiliser
console.log
dans vos composants
Cas d'usage réels : inspiration
OpenAI fournit l'app de démonstration Pizzaz avec plusieurs exemples :
- Pizzaz List : liste classée de restaurants
- Pizzaz Map : carte interactive Mapbox
- Pizzaz Carousel : galerie horizontale
- Pizzaz Video : lecteur vidéo avec timeline
- Pizzaz Album : vue détaillée d'un lieu
Pour conclure
L'Apps SDK transforme ChatGPT : d'un chatbot à une plateforme d'applications conversationnelles.
Techniquement, c'est bien pensé. L'architecture MCP, le support TypeScript, et l'intégration React rendent le développement accessible à toute équipe web moderne. Les guidelines strictes, bien que contraignantes, garantissent une expérience utilisateur cohérente.
Si vous envisagez de construire une app ChatGPT, ou simplement d'explorer le sujet, parlons-en ensemble.
👉 Ressources utiles :
👋