Le SDK TypeScript MCP, associé à Supabase Edge Functions, vous donne un serveur hébergé et distribué mondialement qui s'interpose entre un client IA et votre base de données. Le client IA n'accède jamais directement à la base de données brute. Selon le guide de déploiement Supabase, le transport recommandé pour cette pile est WebStandardStreamableHTTPServerTransport. C'est la seule implémentation couverte ci-dessous. Ce que vous obtenez ci-dessous : un catalogue de produits synthétique en lecture seule, des outils de requête paginés, une isolation de deux utilisateurs appliquée par RLS, une transcription de l'Inspector, et des notes sur le déploiement et la gestion des versions.
Le flux de données du serveur
MCP s'interpose entre un client IA (Claude Code, Claude Desktop, Cursor) et votre backend. Le client parle JSON-RPC par HTTP à votre serveur. Votre serveur expose des outils. Les outils sont des fonctions typées que le client peut appeler. Le serveur décide ce que ces outils peuvent toucher.
Pour Supabase, cette limite est critique. Comme l'explique le résumé d'UI Bakery : le client IA ne doit pas accéder sans limites directement à votre base de données. Vous concevez les outils, vous définissez les requêtes, et Supabase Row Level Security (RLS) gère l'accès par ligne en fonction de l'identité de l'appelant.
Le flux de données ressemble à ceci :
- Claude Code envoie une requête JSON-RPC
tools/callà l'URL de votre Edge Function. - L'Edge Function la reçoit, extrait le JWT de l'en-tête
Authorization, et crée un client Supabase limité à cette identité. - L'outil exécute une requête. Les politiques RLS sur la table filtrent les lignes que l'appelant peut voir.
- Le résultat est sérialisé en réponse JSON-RPC.
Remarquez qu'il n'y a pas de clé service-role dans cette chaîne. Un client service-role contourne entièrement RLS, donc si vous en utilisez un dans un outil face aux appelants, vos politiques RLS sont purement décoratives. Gardez la clé service-role pour les tâches administratives d'arrière-plan uniquement.
Données synthétiques d'exemple
Les exemples ici utilisent une table de produits synthétiques avec trois colonnes : id (uuid), owner_id (uuid, clé étrangère vers auth.users), et name (text). Deux utilisateurs synthétiques, alice et bob, possèdent chacun un ensemble disjoint de lignes. Cette configuration est explicitement illustrative et ne provient pas d'un vrai projet client.
create table public.products (
id uuid primary key default gen_random_uuid(),
owner_id uuid references auth.users(id),
name text not null
);
Peuplez-la avec, disons, dix lignes pour chaque utilisateur en utilisant leurs UUID respectifs.
Définir une surface d'outils réduite avec pagination
Les outils restreints sont meilleurs que les outils larges. Un outil qui retourne une table entière est un risque. La pagination garde les charges utiles prévisibles et évite de dépasser les limites de réponse des Edge Functions.
La surface d'outils pour ce serveur est volontairement petite :
list_products: retourne unepagede produits appartenant à l'appelant, accepte les paramètres page etpage_size, par défaut page 1 avec 20 lignes par page.get_product: retourne un seul produit parid, échoue correctement si l'appelant ne le possède pas.
C'est tout. Deux outils. Vous ne construisez pas un moteur de requêtes. Si vous voulez comprendre comment structurer une couche de lecture de style annuaire plus grande sur Supabase, la publication de 25 000 pages sur l'annuaire vaut le coup d'être lue en premier.
Définir le schéma avec Zod
Le SDK TypeScript MCP utilise Zod pour la validation des entrées. Vos définitions d'outils ressemblent à peu près à ceci (structure illustrative) :
import { z } from 'npm:zod'
``
const ListProductsInput = z.object({
page: z.number().int().min(1).default(1),
page_size: z.number().int().min(1).max(100).default(20),
})
``
const GetProductInput = z.object({
id: z.string().uuid(),
})
Zod valide avant l'exécution de votre gestionnaire. Le client reçoit une erreur typée plutôt qu'une exception d'exécution s'il envoie des données invalides. C'est un comportement sain à mettre en place dès le départ.
Implémenter et exécuter le serveur TypeScript
Le guide de déploiement Supabase utilise @modelcontextprotocol/sdk@1.25.3 avec WebStandardStreamableHTTPServerTransport. C'est la version figée au moment de la rédaction. Consultez la documentation actuelle avant de déployer.
Scaffolding
mkdir my-mcp-server && cd my-mcp-server
supabase init
supabase functions new mcp
Votre fonction se trouve à supabase/functions/mcp/index.ts. Remplacez le contenu par défaut par quelque chose de ce genre (structure illustrative) :
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
import { McpServer } from 'npm:@modelcontextprotocol/sdk@1.25.3/server/mcp.js'
import { WebStandardStreamableHTTPServerTransport } from 'npm:@modelcontextprotocol/sdk@1.25.3/server/streamableHttp.js'
import { createClient } from 'npm:@supabase/supabase-js@2'
import { z } from 'npm:zod'
``
Deno.serve(async (req) => {
const authHeader = req.headers.get('Authorization') ?? ''
const jwt = authHeader.replace('Bearer ', '')
``
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')!,
{ global: { headers: { Authorization: Bearer ${jwt} } } }
)
``
const server = new McpServer({ name: 'products-mcp', version: '1.0.0' })
``
server.tool('list_products', 'List products owned by the caller',
{ page: z.number().int().min(1).default(1), page_size: z.number().int().min(1).max(100).default(20) },
async ({ page, page_size }) => {
const from = (page - 1) * page_size
const { data, error } = await supabase
.from('products')
.select('id, name')
.range(from, from + page_size - 1)
if (error) return { content: [{ type: 'text', text: Error: ${error.message} }] }
return { content: [{ type: 'text', text: JSON.stringify(data) }] }
}
)
``
server.tool('get_product', 'Get a single product by id',
{ id: z.string().uuid() },
async ({ id }) => {
const { data, error } = await supabase
.from('products')
.select('id, name')
.eq('id', id)
.maybeSingle()
if (error) return { content: [{ type: 'text', text: Error: ${error.message} }] }
if (!data) return { content: [{ type: 'text', text: 'Not found or access denied' }] }
return { content: [{ type: 'text', text: JSON.stringify(data) }] }
}
)
``
const transport = new WebStandardStreamableHTTPServerTransport({ path: '/mcp' })
await server.connect(transport)
return transport.handleRequest(req)
})
Deux points à noter ici. D'abord, une nouvelle instance McpServer et transport par requête est le modèle sans état que le runtime Edge Function attend. Ensuite, le client Supabase est créé avec SUPABASE_ANON_KEY, pas la clé service-role, et le JWT est transféré dans l'en-tête de requête pour que RLS connaisse l'identité de l'appelant.
Exécution en local
supabase start
supabase functions serve --no-verify-jwt mcp
Votre serveur est disponible à http://localhost:54321/functions/v1/mcp. Le flag --no-verify-jwt convient pour les tests locaux. Vous voudrez activer la vérification JWT en production.
Authentifier les appelants et appliquer l'accès aux lignes
C'est là que la plupart des tutoriels se trompent. Ils recourent à la clé service-role parce que c'est plus simple. Mais cela contourne RLS et signifie que votre surface d'outils, aussi restreinte soit-elle, a un accès en lecture de niveau administrateur à chaque ligne. C'est inacceptable pour un serveur accessible aux appelants.

Le modèle montré ci-dessus (passer le JWT de l'appelant, utiliser la clé anon) signifie que Supabase Auth évalue le jeton et définit le contexte auth.uid() dans Postgres pour chaque requête. Votre politique RLS peut alors l'utiliser :
alter table public.products enable row level security;
``
create policy "owners can read own products"
on public.products
for select
using (owner_id = auth.uid());
Maintenant, le JWT d'alice ne peut retourner que les lignes où owner_id correspond à son UID. bob qui appelle get_product avec l'un des ID de produit d'Alice obtient Not found or access denied, pas une erreur, pas les données d'Alice. C'est le comportement correct.
Pour une étude plus approfondie de la rédaction de politiques RLS, le guide Supabase RLS couvre les modèles courants et les pièges en détail.
Vérification d'isolation d'accès (illustrative)
La vérification à deux utilisateurs est simple à scripter. Obtenez un JWT pour Alice (via supabase.auth.signInWithPassword), appelez list_products, confirmez que tous les owner_id retournés correspondent à l'UID d'Alice. Répétez avec le JWT de Bob. Confirmez zéro contamination croisée. Si vos politiques sont correctes, ceci passe par construction. Si vous avez fait l'erreur service-role, elle apparaîtra ici car les deux utilisateurs verront toutes les lignes.
Si votre projet Supabase porte déjà des besoins complexes de fetching de données Next.js aux côtés de ceci, l'équipe à /solutions/nextjs-supabase-development/ peut vous aider à structurer la couche base de données sans les raccourcis service-role qui créent ces problèmes.
Connecter Claude Code et vérifier les cas d'erreur
Une fois votre fonction exécutée localement, ajoutez-la à Claude Code :
claude mcp add products-mcp -t http http://localhost:54321/functions/v1/mcp
Ensuite, depuis une session Claude Code, vous pouvez appeler vos outils directement. Vous devriez voir list_products et get_product dans la liste des outils disponibles. Si ce n'est pas le cas, exécutez claude mcp list pour confirmer que le serveur s'est enregistré correctement.
Walkthrough de la transcription de l'Inspector
MCP Inspector (npx @modelcontextprotocol/inspector) vous donne une interface navigateur pour exercer les outils sans client complet. Comme indiqué dans le guide mcp-lite, vous collez l'URL de votre endpoint dans l'Inspector et il découvre vos outils automatiquement.
Une session illustrative d'Inspector pour list_products :
Request:
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "list_products",
"arguments": { "page": 1, "page_size": 5 }
},
"id": 1
}
``
Response (Alice's JWT):
{
"result": {
"content": [{ "type": "text", "text": "[{\"id\":\"...\",\"name\":\"Widget A\"}...]" }]
}
}
Testez maintenant les cas d'erreur intentionnellement. Appelez get_product avec un UUID valide appartenant à Bob, en utilisant le JWT d'Alice. Vous devriez obtenir Not found or access denied. Appelez list_products avec page_size: 200 (au-dessus de votre limite de 100). Zod devrait le rejeter avant que la requête ne s'exécute. Les deux sont des comportements attendus, et tous deux méritent d'être confirmés avant votre déploiement.
Que se passe-t-il quand le JWT est absent ?
Sans en-tête Authorization, votre appel createClient transmet un JWT vide. Supabase le traite comme le rôle anon. Si votre politique RLS n'accorde pas explicitement l'accès au rôle anon, la requête retourne zéro lignes. Ce qui est correct. L'outil retourne un tableau vide, pas une erreur. Vous pourriez préférer retourner un message « unauthenticated » explicite. De toute façon, décidez intentionnellement plutôt que de le découvrir en production.
Déployer, observer et versionner le serveur
Le déploiement est une commande unique :
supabase functions deploy mcp
Votre fonction obtient une URL stable à https://<project-ref>.supabase.co/functions/v1/mcp. Définissez les variables d'environnement dans le tableau de bord Supabase (Settings > Edge Functions > Secrets) : SUPABASE_URL et SUPABASE_ANON_KEY. Le runtime de la fonction injecte ceux-ci au moment de l'invocation.
Observabilité
Supabase fournit des journaux Edge Function intégrés dans le tableau de bord. Filtrez par nom de fonction. Vous verrez les temps de démarrage à froid, la durée d'exécution et les erreurs non interceptées. Pour une observabilité plus structurée, écrivez console.log JSON à partir de vos gestionnaires d'outils. Le flux de journalisation le récupère.
Pour un usage en production, pensez à :
- Enregistrer le nom de l'outil et l'UID de l'appelant (pas le JWT complet) par requête.
- Définir un délai d'expiration sur vos requêtes Supabase pour qu'une requête lente ne consume pas votre budget d'exécution Edge Function.
- Retourner des formes d'erreur déterministes pour que le client IA puisse gérer les défaillances de manière cohérente.
Versioning
Les Edge Functions n'ont pas de versioning intégré. L'approche pratique est basée sur le chemin : déployez une fonction v2 en tant qu'Edge Function séparée (supabase functions new mcp-v2), testez-la indépendamment, puis mettez à jour votre enregistrement Claude Code. Les anciens clients peuvent continuer à pointer vers l'URL v1 jusqu'à ce que vous les basculiez.
Si vous construisez une pile MCP de production avec plusieurs serveurs, le post production MCP stack couvre la sélection de serveurs et les décisions architecturales qui dépassent le cadre ici.
Checklist de déploiement
- Supprimez
--no-verify-jwtdes commandes serve locales avant de déployer. - Confirmez que
SUPABASE_ANON_KEYest défini dans les secrets du tableau de bord, pas service-role. - Exécutez votre vérification d'isolation d'accès à deux utilisateurs contre l'URL de production avant d'annoncer l'endpoint.
- Définissez un plafond
max_rowsdans chaque outil pour éviter les requêtes illimitées. - Vérifiez la version du SDK épinglée dans votre import (
@modelcontextprotocol/sdk@1.25.3dans la documentation Supabase au moment de la rédaction) par rapport aux versions actuelles et mettez à jour si nécessaire.
FAQ
Puis-je utiliser mcp-lite à la place du SDK officiel ?
Oui. La documentation Supabase indique explicitement que vous pouvez utiliser mcp-lite ou mcp-handler comme alternatives au SDK officiel. Le WebStandardStreamableHTTPServerTransport utilisé ci-dessus provient du SDK officiel, mais mcp-lite est plus léger et sans dépendances. Choisissez-en un et tenez-vous-y. Le guide mcp-lite montre la commande scaffold : npm create mcp-lite@latest. L'architecture et le comportement de RLS décrits ci-dessus s'appliquent indépendamment du framework que vous choisissez.
Ai-je besoin d'un plan Supabase payant pour déployer des Edge Functions ?
Les Edge Functions sont disponibles sur l'offre gratuite avec certaines limites sur les invocations et le temps d'exécution. Consultez la page de tarification Supabase actuelle pour les chiffres. Pour un outil interne à faible trafic, l'offre gratuite suffit généralement. Pour tout ce qui fait face à la production avec un vrai volume de requêtes, les limites du plan Pro sont plus appropriées.
Que faire si mon serveur MCP doit écrire des données au lieu de simplement les lire ?
Ajoutez des outils qui exécutent des requêtes insert, update ou delete. Le modèle RLS s'applique toujours : votre politique contrôle les lignes que l'appelant peut modifier en fonction de auth.uid(). La principale considération supplémentaire est l'idempotence. Si le client IA réessaie un appel d'outil échoué, voulez-vous que l'insertion s'exécute deux fois ? L'utilisation d'un upsert avec une clé primaire stable ou la vérification de l'existence avant l'insertion sont les atténuations standard.
Puis-je exécuter cela localement sans compte Supabase ?
Oui, supabase start exécute la pile Supabase complète localement via Docker, y compris Postgres, Auth et Edge Functions. Vous obtenez une SUPABASE_URL et une SUPABASE_ANON_KEY locales à partir de la sortie CLI. Votre fonction et vos politiques RLS fonctionnent exactement comme elles le feront en production. La seule différence est l'émetteur JWT, qui est l'instance GoTrue locale plutôt que l'Auth cloud de Supabase.
Comment gérer la limitation de débit sur les outils ?
Les Edge Functions n'ont pas de limitation de débit intégrée par appelant. Les options pratiques sont : une table Postgres qui enregistre les comptes d'appels par UID par fenêtre de temps (vérifiez et incrémentez à l'intérieur de l'outil), ou une passerelle API en amont (la propre passerelle API de Supabase gère une partie de cela au niveau du projet). Pour la plupart des outils internes, atteindre la limite d'invocation d'Edge Function sur un projet gratuit est la première contrainte que vous rencontrerez, pas un abus délibéré.
La mise en garde la plus importante de tout ce qui précède : si vous utilisez une clé service-role dans un outil accessible aux appelants, vos politiques RLS ne font rien. La clé anon plus le transfert JWT n'est pas un avantage supplémentaire, c'est le mécanisme qui rend l'isolation des appelants possible.
