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: Build Native Apps Inside ChatGPT

    10 minutes reading

    OpenAI Apps SDK: Build Native Apps Inside ChatGPT
    🇫🇷 This post is also available in french

    Imagine being able to order food, book a ride, or interact with your APIs directly from ChatGPT—without ever leaving the conversation. That's exactly what the freshly announced OpenAI Apps SDK delivers.

    ChatGPT is moving beyond a text-based assistant—it's becoming an application platform, much like the App Store for iOS or the Play Store for Android.

    Why ChatGPT Apps?

    The OpenAI Apps SDK turns ChatGPT from a simple conversational assistant into an app platform. With more than 100 million weekly active users, ChatGPT now represents an unprecedented distribution channel for businesses.

    This transformation opens up significant commercial opportunities. Developers can now create native apps that seamlessly integrate with ChatGPT’s user experience, giving them direct access to an engaged audience.

    Like any emerging technology, the Apps SDK has its limitations. OpenAI is very clear about the use-cases to avoid:

    • Long, complex content (a regular website is better)
    • Complicated, multi-step workflows (ChatGPT isn't optimized for these)
    • Advertising or unsolicited promotional content (it degrades the user experience)
    • Displaying sensitive information directly in the conversation

    Technical Architecture: MCP at the Core

    The Heart of the Architecture: MCP

    Good news for developers: the Apps SDK doesn't reinvent the wheel. It’s built on top of the Model Context Protocol (MCP), an open standard created by Anthropic and adopted by OpenAI.

    What does this mean in practice? The code you write for ChatGPT today could work with other MCP clients tomorrow. That’s unusual in the AI ecosystem and great news for the longevity of your work.

    MCP defines three essential primitives:

    1. List tools: your server exposes available tools and their schemas
    2. Call tools: ChatGPT invokes your tools with the right parameters
    3. Return components: each tool can return an HTML interface to render

    The benefit? Your connector will work automatically on ChatGPT web, mobile, and any future MCP-compatible clients. Build once, deploy everywhere.

    Choosing Your Stack: TypeScript or Python?

    You can use the official MCP SDKs:

    TypeScript (definitely our favorite ❤️):

    • @modelcontextprotocol/sdk
    • Ideal if you already use Node.js/React
    • Natively integrates with your existing web components
    • Great developer tooling with TypeScript, Zod, and the modern JS ecosystem

    Python:

    • modelcontextprotocol/python-sdk
    • Includes FastMCP to get up and running quickly
    • Perfect for data science or machine learning teams
    • Handy if your backend is already in Python

    For the rest of this article, we'll focus on TypeScript—the stack we recommend and use at Premier Octet.

    Hands-on Implementation

    Let's build a concrete ChatGPT app together! We'll create a Premier Octet blog article widget—from the server to the React component.

    Step 1: Set Up the MCP Server

    First, we need to create a server that exposes our tools. Here's the starter code:

    import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
    import { z } from 'zod'
    import { readFileSync } from 'node:fs'
    
    // Initialize the MCP server
    const server = new McpServer({
      name: 'premier-octet-blog',
      version: '1.0.0',
    })
    
    // Load the compiled React component assets
    const BLOG_JS = readFileSync('web/dist/blog-widget.js', 'utf8')
    const BLOG_CSS = readFileSync('web/dist/blog-widget.css', 'utf8')
    
    // Register the UI resource (HTML template)
    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(),
        },
      ],
    }))
    

    Nothing complicated: we initialize the server, load our compiled React bundle, and register it as a resource. The key is mimeType: 'text/html+skybridge'—this signals to ChatGPT: "here is a UI to display." Skybridge is ChatGPT's sandboxed runtime.

    Step 2: Define a Tool

    Now, let's create a tool that ChatGPT can invoke. You define what your app can do—then ChatGPT decides when to use it based on the conversation's context.

    server.registerTool(
      'show_blog_articles',
      {
        title: 'Display Premier Octet blog articles',
        description:
          'Use this tool when the user wants to see the latest Premier Octet blog articles or search by topic.',
        inputSchema: {
          type: 'object',
          properties: {
            category: {
              type: 'string',
              description: 'Article category (e.g., react, typescript, openai)',
            },
            limit: { type: 'number', description: 'Number of articles to display (default: 6)' },
          },
          required: [],
        },
        _meta: {
          'openai/outputTemplate': 'ui://widget/blog-articles.html',
          'openai/widgetAccessible': true, // Enables calls from the component
          'openai/toolInvocation/invoking': 'Loading articles...',
          'openai/toolInvocation/invoked': 'Articles displayed',
        },
      },
      async ({ category, limit = 6 }, context) => {
        // Fetch articles from your API
        const articles = await fetchBlogArticles({ category, limit })
    
        return {
          // Structured data for both the model AND the component
          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 || 'all',
          },
          // Text for the conversation transcript
          content: [
            {
              type: 'text',
              text: `Here ${articles.length} article${
                articles.length > 1 ? 's' : ''
              } from the Premier Octet blog${category ? ` about "${category}"` : ''}.`,
            },
          ],
          // Private metadata (invisible to the model, available to your UI)
          _meta: {
            fullArticles: articles, // Complete data for the UI
            searchQuery: category,
          },
        }
      }
    )
    

    Notice how we clearly separate data:

    • structuredContent: what the model AND the component can see (keep this concise!)
    • content: text shown within the conversation
    • _meta: private data, invisible to the model but available to your UI

    Step 3: Build the React Component

    Already using React components? Good news—reuse them! The only difference is, they run in a sandboxed iframe, talking to ChatGPT through the window.openai API.

    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() {
      // Get the initial data provided by the tool
      const initialData = window.openai?.toolOutput as BlogData
      const [articles, setArticles] = useState(initialData)
    
      // Persist local state in ChatGPT
      const saveState = async (newArticles: BlogData) => {
        setArticles(newArticles)
        await window.openai?.setWidgetState?.({
          version: 1,
          articles: newArticles,
          lastModified: Date.now(),
        })
      }
    
      // Call a tool from inside the component
      const searchArticles = async (category: string) => {
        await window.openai?.callTool('show_blog_articles', {
          category,
          limit: 6,
        })
      }
    
      // Handle layout changes
      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>Premier Octet Articles</h2>
            <div className="category-filters">
              <button onClick={() => searchArticles('')}>All</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>
      )
    }
    
    // Mount the component
    createRoot(document.getElementById('blog-root')!).render(<BlogWidget />)
    

    This component demonstrates the key concepts:

    • Reading initial data via toolOutput
    • Persisting state with setWidgetState (so state survives re-renders)
    • Calling tools from within the component via callTool (e.g., for category filtering)
    • Adapting the layout (inline vs fullscreen)
    • User interface with filters and an article grid

    Step 4: The window.openai API

    The window.openai API bridges your component and ChatGPT:

    // Data
    window.openai.toolInput // Input arguments to the tool
    window.openai.toolOutput // Tool response (structuredContent)
    window.openai.widgetState // Persisted state between renders
    
    // 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 & context
    window.openai.displayMode // "inline" | "pip" | "fullscreen"
    window.openai.maxHeight // Max available height
    window.openai.locale // "fr-FR", "en-US"...
    window.openai.theme // "light" | "dark"
    

    These methods cover 90% of what you'll need. The rest is in the official docs for advanced cases.

    Step 5: Secure with OAuth 2.1

    If your app accesses user data (which it likely will), you'll need authentication. The good news: the Apps SDK supports OAuth 2.1 end to end, including automatic token verification.

    import { FastMCP } from '@modelcontextprotocol/sdk/server/fastmcp.js'
    
    // Configure authentication
    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'],
      },
    })
    
    // Verify tokens on each call
    mcp.registerTool(
      'show_blog_articles',
      {
        /* ... */
      },
      async ({ projectId }, { token }) => {
        // Token is automatically validated
        const userId = token.subject
        const hasAccess = token.scopes.includes('blog:read')
    
        if (!hasAccess) {
          throw new Error('Insufficient permissions')
        }
    
        // Load user data
        return await loadUserBoard(userId, projectId)
      }
    )
    

    To speed up this process, use Better Auth. It supports OAuth 2.1, dynamic client registration, and native scope management.

    Making Your App Discoverable

    Getting your app discovered by users is crucial.

    The Different Paths to Your App

    ChatGPT users can discover and use your app in several ways:

    1. Explicit mention: "Show me Premier Octet blog articles"
    2. Conversational discovery: the model picks your app based on context
    3. Directory: app directory with metadata and screenshots
    4. Launcher: "+" button in the composer

    Your metadata quality really matters. ChatGPT selects your tool by analyzing its description. Compare:

    // ✅ Good: action-oriented, contextual description
    description: 'Use this tool when the user wants to view Premier Octet blog articles.'
    
    // ❌ Bad: too vague—the model won't know when to use it
    description: 'A tool to display articles.'
    

    Security and Privacy

    A ChatGPT app can access sensitive user data. OpenAI imposes strict security standards, and you should too.

    Core Principles

    OpenAI enforces strict standards:

    1. Least privilege: only request the permissions (scopes) you truly need
    2. Explicit consent: users must understand what they're allowing
    3. Defense in depth: always validate everything server-side, even if it's from the model
    4. Data minimization: only collect what's strictly necessary

    Component Sandboxing

    Components run in an iframe under a strict Content Security Policy (CSP):

    // Set your 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'],
            },
          },
        },
      ],
    }))
    

    Why TypeScript Is Your Best Friend

    As highlighted earlier: TypeScript is the optimal choice for building with the Apps SDK. Here’s why:

    Practical Advantages

    1. Type safety: Zod + TypeScript helps prevent bugs. Your server and client schemas always match.
    2. Reusability: use your existing React components with zero effort.
    3. Blazing-fast tooling: esbuild compiles in milliseconds, TypeScript spots errors before runtime.
    4. Isomorphism: one language, shared types server/client, zero friction.

    Example: Shared Types

    // 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>
    
    // Server
    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 }
      }
    )
    
    // React client
    function BlogWidget() {
      const blogData = window.openai?.toolOutput as BlogData
      // TypeScript guarantees blogData.articles exists!
    }
    

    You get the idea: define a type once, use it everywhere. No duplication or out-of-sync headaches.

    UX as a Success Factor

    Great tech is useless if the user experience stinks. OpenAI’s design guidelines are comprehensive. Here’s what to remember:

    The Three Display Modes

    The Apps SDK lets you choose how your app is presented. Each mode has its use:

    1. Inline: a lightweight card in the conversation—perfect for simple actions (e.g., confirm a booking)
    2. Picture-in-Picture: a floating window—ideal for ongoing content (video, games)
    3. Fullscreen: an immersive view—best for complex editing or exploration, with the ChatGPT composer embedded
    function BlogWidget() {
      const displayMode = window.openai?.displayMode
    
      if (displayMode === 'fullscreen') {
        return <FullscreenBlogView />
      }
    
      return (
        <InlineCard>
          <button
            onClick={() => {
              window.openai?.requestDisplayMode({ mode: 'fullscreen' })
            }}
          >
            Open full screen
          </button>
        </InlineCard>
      )
    }
    

    Switching modes should be seamless. Make sure to test all three.

    Key Design Principles

    OpenAI highlights 5 fundamental principles. They may sound obvious, but many apps ignore them in practice:

    • Conversational: your app should extend ChatGPT, not interrupt it.
    • Intelligent: adapt your interface to the conversation's context.
    • Simple: one clear action per interaction—don't overload users.
    • Responsive: it must be fast. If your tool takes 3 seconds, it's too long.
    • Accessible: support dark mode, text sizes, screen readers—do it right.

    Keep these five pillars in mind throughout your development.

    Testing and Debugging

    MCP Inspector: Your Best Friend

    Before even touching ChatGPT, test locally. The MCP Inspector is indispensable:

    npx @modelcontextprotocol/inspector@latest
    # Point to http://localhost:3000/mcp
    

    Inspector lets you:

    • List all exposed tools
    • Call tools with parameters
    • View JSON responses
    • Preview component rendering

    Automated Tests

    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')
      })
    })
    

    Debugging in ChatGPT

    In developer mode:

    1. Open your browser DevTools
    2. Components are shown within an iframe
    3. Errors appear in the console
    4. Use console.log in your components

    Real-World Use Cases: Get Inspired

    OpenAI provides a demo app, Pizzaz, with several examples:

    • Pizzaz List: ranked restaurant lists
    • Pizzaz Map: interactive Mapbox map
    • Pizzaz Carousel: horizontal gallery
    • Pizzaz Video: video player with timeline
    • Pizzaz Album: place detail view

    Check out the source code

    In Conclusion

    The Apps SDK is transforming ChatGPT from a chatbot to a conversational app platform.

    Technically, it’s solid. MCP architecture, TypeScript support, and React integration put it within reach of any modern web team. Strict guidelines, though demanding, ensure a consistent user experience.

    If you’re thinking of building a ChatGPT app—or just exploring—let’s talk.

    👉 Useful resources:

    👋

    À découvrir également

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

    Discuter de votre projet openai