---
title: Building ChatGPT Apps with Supabase Edge Functions and mcp-use
description: >-
  Learn how to build a ChatGPT app that connects to your Supabase database using
  mcp-use and Edge Functions. Create interactive widgets for schema exploration,
  data viewing, and SQL queries.
author: pedro_rodrigues
date: '2025-12-17'
tags:
  - ai
  - mcp
  - edge-functions
  - tutorial
categories:
  - developers
---
ChatGPT is no longer just a chat app. OpenAI recently launched the [Apps SDK](https://developers.openai.com/apps-sdk/), which turns ChatGPT into a platform where developers can build interactive applications. These apps run inside ChatGPT and can display custom interfaces, connect to databases, and perform real actions.

In this guide, you will learn how to build a ChatGPT app that connects to your Supabase database. You will use [mcp-use](https://mcp-use.com), an open source SDK that makes it easy to deploy MCP servers on Supabase Edge Functions. By the end, you will have an app that lets ChatGPT users explore your database schema, view table data, and run SQL queries, all through interactive widgets.

> **NOTE**
>
> OpenAI has launched the [ChatGPT Apps Marketplace](https://chatgpt.com/apps), where users can discover and use apps built by developers. If you build an app following this guide, you can submit it to the marketplace. Check out the [App Submission Guidelines](https://developers.openai.com/apps-sdk/app-submission-guidelines) to learn how to get your app listed.

## What are ChatGPT apps?

ChatGPT apps extend what ChatGPT can do. Instead of just answering questions, ChatGPT can now show interactive interfaces and connect to external services.

Every ChatGPT app has two parts:

- **MCP server.** This is your backend. It defines the tools and capabilities your app provides. When a user asks ChatGPT to do something, ChatGPT calls your MCP server to get the data or perform the action.
- **Web component.** This is your frontend. It renders inside ChatGPT as an iframe. Your MCP server can return UI metadata that tells ChatGPT which component to display and what data to pass to it.

The MCP server and web component work together. ChatGPT acts as the bridge. When a user asks a question, ChatGPT figures out which tool to call. Your MCP server runs the tool and returns both data and UI instructions. ChatGPT then renders your component with the data.

This architecture is powerful because it separates what your app can do from how it looks. Your MCP server handles all the logic. Your components handle all the display. ChatGPT handles the conversation.

## What is mcp-use?

mcp-use is an open source TypeScript SDK for building MCP servers. It solves a specific problem: the official MCP SDK uses Express, which depends on Node.js features that do not work in edge environments like Supabase Edge Functions.

mcp-use uses Hono instead of Express. Hono is a lightweight web framework designed for edge runtimes. This means your MCP server can run on Supabase Edge Functions, Cloudflare Workers, and other serverless platforms.

Here is what mcp-use gives you:

- **Edge runtime support.** Works on Deno, Cloudflare Workers, and Supabase Edge Functions.
- **OpenAI Apps SDK integration.** Built-in support for ChatGPT app widgets and UI components.
- **Supabase Auth integration.** Native support for authenticating users with Supabase Auth.
- **Full MCP compliance.** Scores 100/100 on the official MCP conformance tests.

## Why Supabase Edge Functions?

Supabase Edge Functions are a good fit for MCP servers for several reasons.

- **Everything in one place.** Your database, authentication, and server code all live in the same Supabase project. Your MCP server can query your database directly. It can check user permissions using Supabase Auth. There is no need to manage separate services or configure connections between them.
- **Fast cold starts.** Edge Functions run on Deno and start quickly. When ChatGPT calls your MCP server, users do not wait for a container to spin up.
- **Built-in authentication.** Supabase Auth works out of the box with mcp-use. You can require users to log in before using your app. You can check their roles and permissions. You can scope data access to specific users.
- **Simple deployment.** A single command deploys your MCP server. Supabase handles scaling, HTTPS, and infrastructure.

## What you will build

You will build a ChatGPT app that explores Supabase databases. The app provides four tools:

- **List tables.** Shows all tables in your database with an interactive schema explorer widget.
- **Show table.** Displays data from a specific table with sorting and filtering.
- **Execute SQL.** Runs read-only SQL queries and shows results with syntax highlighting.
- **Supabase status.** Shows the current status of Supabase services and recent incidents.

Each tool returns both data and a React widget. ChatGPT users see interactive interfaces, not just text responses.

## Full implementation

This guide provides the key points for building a ChatGPT app. For the full implementation, please visit or clone the repository: [github.com/mcp-use/supabase-mcp-server](https://github.com/mcp-use/supabase-mcp-server)

## Project setup

Start by creating a new project with mcp-use:

```bash
npx create-mcp-use-app my-supabase-app --template apps-sdk
cd my-supabase-app
npm install
```

This creates a project with everything you need: the mcp-use SDK, widget templates, and build configuration for Supabase Edge Functions.

Note: the default `apps-sdk` template includes a "fruit shop" demo to show how widgets work. **You will need to modify or replace these files** to align with the Supabase explorer tools described in this guide.

Next, initialize Supabase in your project:

```bash
supabase init
supabase login
supabase link --project-ref YOUR_PROJECT_ID
```

You can find your project ID in your Supabase dashboard under Project Settings.

## Building the MCP server

Open `index.ts` and set up your MCP server:

```tsx
//index.ts
import { MCPServer, object, text, widget, error } from 'mcp-use/server'

const server = new MCPServer({
  name: 'supabase-explorer',
  version: '1.0.0',
  description: 'A Supabase MCP server with Apps SDK widgets',
})
```

## Adding UI widgets

Widgets can be organized in two ways: **single-file widgets** or **folder-based widgets**. Choose the organization style that best fits your widget's complexity.

```bash
├── index.ts                # Main server file using mcp-use
├── resources/              # React widget components
│   ├── components/         # Reusable UI components
│   ├── schema-explorer/    # Schema explorer widget (in this article)
│   ├── table-viewer/       # Table viewer widget
│   └── query-results/      # Query results widget
│   └── supabase-status.tsx # 1 file widget
└── package.json            # Dependencies
```

See the widgets [implementation here](https://github.com/mcp-use/supabase-mcp-server/tree/main/resources) and the [widget docs](https://mcp-use.com/docs/typescript/server/ui-widgets).

### Two types of widgets

mcp-use SDK supports two widget patterns.

1. Display-only widgets
1. Display and return data widgets

### 1. Display-only widgets

**Display-only widgets** do not fetch data on the server. They receive tool parameters as props and render UI. The supabase-status (`resources/supabase-status.tsx`) widget works this way. It fetches status data client-side from the Supabase status API in the React component.

You don't need to register the widget as tool in `index.ts` because it doesn't need custom logic on the tool side, and mcp-use automatically registers all the **display-only widgets in** `resources/` folder as MCP tools.

```tsx
// resources/supabase-status.tsx
import { McpUseProvider, useWidget, type WidgetMetadata } from "mcp-use/react";
import React, { useEffect, useState } from "react";
import z from "zod/v4";
import "./styles.css";

// Tool args that will be passed to the widget props
const propSchema = z.object({
  daysBack: z.number().default(7).describe("Number of days back to show incidents"),
});

// Define widget metadata - auto-generates tool
export const widgetMetadata: WidgetMetadata = {
  description: "Display Supabase service status and recent incidents from the status page",
  props: propSchema,
  exposeAsTool: true, // Important for display-only widgets, that are auto registered
  annotations: { readOnlyHint: true },
  appsSdkMetadata: {
    "openai/widgetCSP": {
      connect_domains: ["https://status.supabase.com"],
      resource_domains: ["https://*.supabase.com"],
    },
  },
};

// Your widget component
const SupabaseStatusWidget: React.FC = () => {
  // Get props from mcp-use hook: useWidget -> Everything you need in one hook!
  const { props, isPending } = useWidget<z.infer<typeof propSchema>>();
  const [incidents, setIncidents] = useState<Incident[]>([]);

  useEffect(() => {
    const fetchStatus = async () => {
      try {
        setLoading(true);
        const response = await fetch("https://status.supabase.com/history.rss");
        // ...
        // Fetch APIs from the widget
      }
    };
    fetchStatus();
  }, [props.daysBack]);

  return (
    <McpUseProvider viewControls="fullscreen" autoSize>
      <Card className="relative p-6 rounded-3xl w-full">
        <div className="mb-6">
          <div className="flex items-center justify-between mb-2">
            <div>
              <h2 className="text-3xl font-bold text-[hsl(var(--foreground))] mb-1">
                Supabase Status
              </h2>
              <p className="text-sm text-[hsl(var(--foreground-muted))]">
                Last {props.daysBack} days
              </p>
            </div>
          </div>
        </div>
        {/* Full component implementation: https://github.com/mcp-use/supabase-mcp-server/blob/main/resources/supabase-status.tsx */}
      </Card>
    </McpUseProvider>
  );
};

export default SupabaseStatusWidget;
```

As we defined in the widgetMetadata props `daysBack`, the MCP tool expects an argument specifying the number of days to retrieve incidents from the LLM.

![Supabase status widget in ChatGPT](/images/blog/2025-12-17-building-chatgpt-apps-with-supabase/chatgpt-chat-1.png)

### 2. Display and return data widgets

**Display and return data widgets** fetch data server-side and pass it to both the LLM and the widget. The list-tables tool works this way. The server queries the database, returns data to ChatGPT for reasoning, and passes the same data to the widget for display.

This separation is useful. Sometimes you want to show the user more than you tell the LLM. Sometimes you want to tell the LLM more than you show the user. You control both independently.

You need to register the widget as a tool in `index.ts`. The following section goes through it.

## Adding tools

Tools define what your app can do. Each tool has a name, parameters, and a callback function. For tools that return UI widgets, as in our case, we need to specify the `widget` argument and set the widget name from the component in `resources/`, in this case: "schema-explorer".

Here is the **list-tables** tool that returns the `schema-explorer` widget:

```tsx
// index.ts
server.tool(
  {
    name: 'list-tables',
    description: 'List all tables in your Supabase database',
    schema: z.object({
      schemas: z.array(z.string()).optional().describe('Schemas to include (default: all)'),
    }),
    widget: {
      name: 'schema-explorer',
      invoking: 'Loading database tables...',
      invoked: 'Tables loaded successfully',
    },
    annotations: { readOnlyHint: true },
  },
  async ({ schemas }) => {
    try {
      const result = await supabaseClient?.callTool('list_tables', { schemas: ['public'] })
      const content = result?.content[0]
      let tables: any[] = []

      if (content?.type === 'text') {
        const data = extractJsonFromResponse(content?.text ?? '')
        tables = Array.isArray(data) ? data : []
      }

      return widget({
        // Props passed to the React component in /resources folder
        props: {
          tables,
          schemas: schemas || ['public'],
        },
        // Output returned by the MCP tool to the LLM (what the LLM sees)
        output: object({
          tables: tables.map((table) => ({
            name: table.name,
          })),
        }),
      })
    } catch (err) {
      return error(`Error listing tables: ${err instanceof Error ? err.message : String(err)}`)
    }
  }
)
```

The tool queries your database for table information. It returns both text content for the LLM and widget metadata for the UI. ChatGPT uses the text content to understand the data. The widget renders in the chat for the user.

![Schema explorer widget showing database tables](/images/blog/2025-12-17-building-chatgpt-apps-with-supabase/chatgpt-chat-2.png)

## Deploying to Supabase

Supabase Edge Functions provide a great place to host your server. They are fast, scalable, and easy to manage.

For the complete step-by-step guide, see our [Supabase deployment documentation](https://mcp-use.com/docs/typescript/server/deployment-supabase#supabase).

Prerequisites: Docker must be running before deployment

```bash
# Verify Docker is running, otherwise install it
docker info
```

mcp-use includes a deployment script that handles everything:

```bash
curl -fsSL https://url.mcp-use.com/supabase | bash
```

This script checks your authentication, builds your application, sets environment variables, and deploys to Supabase Edge Functions.

After deployment, your MCP server is live at:

```
https://<YOUR_PROJECT_ID>.supabase.co/functions/v1/<YOUR_FUNCTION_NAME>/mcp
```

## Connecting to ChatGPT

Check out the [Official Developer Mode Guide](https://platform.openai.com/docs/guides/developer-mode) to enable it in your ChatGPT profile.

To use your app in ChatGPT:

1. Go to your ChatGPT profile settings
1. **Enable developer mode:** Go to [Settings → Apps & Connectors](https://chatgpt.com/#settings/Connectors) → **Advanced → Developer mode**.
1. Click on "Create App" to add the MCP server and enter your Edge Function URL with `/mcp` at the end.

![Adding Supabase MCP server to ChatGPT](/images/blog/2025-12-17-building-chatgpt-apps-with-supabase/add-supabase-mcp-server.png)

Open a new ChatGPT App:

1. Click the "+" button to add a new connector
1. Select "More" and then select the MCP Server you just added.

Now you can start a new chat and use your tools. Ask ChatGPT to list your tables, show data from a specific table, or run a SQL query. ChatGPT will call your MCP server and display the interactive widgets.

For example, with the execute-sql tool, you can ask ChatGPT to write queries, execute them, and display the results.

![SQL query results in ChatGPT](/images/blog/2025-12-17-building-chatgpt-apps-with-supabase/chatgpt-chat-3.png)

## Next steps

You now have a working ChatGPT app powered by Supabase Edge Functions. Here are some ways to extend it:

- Add more tools that interact with your specific database tables
- Build custom widgets for your data types
- Implement write operations with proper authorization checks
- Add tool chaining so ChatGPT can combine multiple operations

For more details on mcp-use, see the [mcp-use documentation](https://mcp-use.com/docs/typescript/server/deployment-supabase) or the [mcp-use GitHub repository](https://github.com/mcp-use/mcp-use). For more on Supabase Edge Functions, see the [Edge Functions guide](https://supabase.com/docs/guides/functions).

For the full implementation, please visit or clone the repository: [github.com/mcp-use/supabase-mcp-server](https://github.com/mcp-use/supabase-mcp-server)
