AccueilClients

Applications et sites

  • Application métiersIntranet, back-office...
  • Applications mobilesAndroid & iOS
  • Sites InternetSites marketings et vitrines
  • Expertises techniques

  • React
  • Expo / React Native
  • Next.js
  • Node.js
  • Directus
  • TypeScript
  • Open SourceBlogContactEstimer

    7 octobre 2025

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

    12 minutes de lecture

    OpenAI Apps SDK : développer des apps natives dans ChatGPT
    🇺🇸 This post is also available in english

    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 :

    1. List tools : votre serveur expose les outils disponibles avec leurs schémas
    2. Call tools : ChatGPT invoque vos outils avec les arguments appropriés
    3. 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 :

    1. Mention explicite : "Montre-moi les articles du blog de Premier Octet"
    2. Découverte conversationnelle : le modèle choisit votre app selon le contexte
    3. Directory : répertoire des apps avec métadonnées et captures d'écran
    4. 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 :

    1. Principe du moindre privilège : ne demandez que les scopes nécessaires
    2. Consentement explicite : les utilisateurs doivent comprendre ce qu'ils autorisent
    3. Défense en profondeur : validez tout côté serveur, même si le modèle l'a fourni
    4. 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

    1. Type safety : Zod + TypeScript vous évitent les bugs. Vos schémas serveur et client sont toujours cohérents.
    2. Réutilisabilité : vos composants React existants fonctionnent directement.
    3. Tooling performant : esbuild compile en millisecondes, TypeScript détecte les erreurs avant l'exécution
    4. 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 :

    1. Inline : une carte légère dans la conversation — parfait pour une action simple (confirmer une réservation)
    2. Picture-in-Picture : une fenêtre flottante qui reste visible — idéal pour du contenu continu (vidéo, jeu)
    3. 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 :

    1. Ouvrir les DevTools du navigateur
    2. Les composants s'affichent dans une iframe
    3. Les erreurs apparaissent dans la console
    4. 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

    Code source disponible

    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 :

    👋

    À découvrir également

    Premier Octet vous accompagne dans le développement de vos projets avec openai

    Discuter de votre projet openai