apps/www/_blog/2025-12-17-building-chatgpt-apps-with-supabase.mdx
ChatGPT is no longer just a chat app. OpenAI recently launched the 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, 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.
<Admonition type="note" label="ChatGPT Apps Marketplace Now Open">OpenAI has launched the ChatGPT Apps Marketplace, 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 to learn how to get your app listed.
</Admonition>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:
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.
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:
Supabase Edge Functions are a good fit for MCP servers for several reasons.
You will build a ChatGPT app that explores Supabase databases. The app provides four tools:
Each tool returns both data and a React widget. ChatGPT users see interactive interfaces, not just text responses.
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
Start by creating a new project with mcp-use:
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:
supabase init
supabase login
supabase link --project-ref YOUR_PROJECT_ID
You can find your project ID in your Supabase dashboard under Project Settings.
Open index.ts and set up your MCP server:
//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',
})
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.
├── 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 and the widget docs.
mcp-use SDK supports two widget patterns.
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.
// 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>
</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.
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.
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:
// 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.
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.
Prerequisites: Docker must be running before deployment
# Verify Docker is running, otherwise install it
docker info
mcp-use includes a deployment script that handles everything:
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
Check out the Official Developer Mode Guide to enable it in your ChatGPT profile.
To use your app in ChatGPT:
/mcp at the end.Open a new ChatGPT App:
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.
You now have a working ChatGPT app powered by Supabase Edge Functions. Here are some ways to extend it:
For more details on mcp-use, see the mcp-use documentation or the mcp-use GitHub repository. For more on Supabase Edge Functions, see the Edge Functions guide.
For the full implementation, please visit or clone the repository: github.com/mcp-use/supabase-mcp-server