MCP TypeScript SDK, Supabase Edge Functions के साथ paired, आपको एक hosted, globally-distributed server देता है जो एक AI client और आपके database के बीच बैठता है। AI client को कभी raw database access नहीं मिलता। Supabase deploy guide के अनुसार, इस stack के लिए recommended transport WebStandardStreamableHTTPServerTransport है। यह एकमात्र implementation है जो नीचे cover किया गया है। आपको नीचे क्या मिलता है: एक synthetic read-only product catalogue, paginated query tools, RLS-enforced two-user isolation, एक Inspector transcript walkthrough, और deploying और versioning पर notes।
Server और data flow
MCP एक AI client (Claude Code, Claude Desktop, Cursor) और आपके backend के बीच बैठता है। Client आपके server को HTTP पर JSON-RPC बोलता है। आपका server tools expose करता है। Tools typed functions हैं जिन्हें client call कर सकता है। Server decide करता है कि वे tools क्या touch कर सकते हैं।
Supabase के लिए, वह boundary critical है। जैसा कि UI Bakery का breakdown कहता है: AI client को आपके database तक unlimited direct access नहीं मिलना चाहिए। आप tools design करते हैं, आप queries define करते हैं, और Supabase Row Level Security (RLS) caller की identity के आधार पर per-row access को handle करता है।
Data flow कुछ इस तरह दिखता है:
- Claude Code आपके Edge Function URL को एक JSON-RPC
tools/callrequest भेजता है। - Edge Function उसे receive करता है,
Authorizationheader से JWT को extract करता है, और एक Supabase client बनाता है जो उस identity को scope किया जाता है। - Tool एक query चलाता है। Table पर RLS policies उन rows को filter करती हैं जिन्हें caller देख सकता है।
- Result एक JSON-RPC response के रूप में serialize किया जाता है।
ध्यान दें कि उस chain में कोई service-role key नहीं है। एक service-role client RLS को पूरी तरह bypass कर देता है, इसलिए अगर आप एक caller-facing tool में एक use करते हैं, तो आपकी RLS policies सिर्फ decoration हैं। Service-role key को background admin tasks के लिए ही रखें।
Synthetic sample data
यहाँ के examples एक synthetic products table का उपयोग करते हैं जिसमें तीन columns हैं: id (uuid), owner_id (uuid, auth.users को foreign key), और name (text)। दो synthetic users, alice और bob, प्रत्येक के पास rows का एक disjoint set है। यह setup explicitly illustrative है और किसी real client project से derived नहीं है।
create table public.products (
id uuid primary key default gen_random_uuid(),
owner_id uuid references auth.users(id),
name text not null
);
इसे उनके respective UUIDs का उपयोग करके प्रत्येक user के लिए, कहते हैं, दस rows के साथ seed करें।
Pagination के साथ एक small tool surface define करें
Narrow tools wide ones से बेहतर हैं। एक tool जो एक पूरी table return करता है वह एक liability है। Pagination payloads को predictable रखता है और Edge Function response limits को hit करने से बचाता है।
इस server के लिए tool surface deliberately small है:
- list_products: caller के owned products का एक
pagereturn करता है, page औरpage_sizeparameters को accept करता है, default में page 1 with 20 rows per page है। - get_product:
idके आधार पर एक single product return करता है, gracefully fail करता है अगर caller उसे own नहीं करता है।
बस। दो टूल्स। आप कोई क्वेरी इंजन नहीं बना रहे हैं। अगर आप Supabase पर बड़ी डायरेक्टरी-स्टाइल रीड लेयर को स्ट्रक्चर करने का तरीका समझना चाहते हैं, तो पहले 25,000-पेज की डायरेक्टरी पोस्ट पढ़ने लायक है।
Zod के साथ स्कीमा परिभाषित करना
MCP TypeScript SDK इनपुट वेलिडेशन के लिए Zod का उपयोग करता है। आपकी टूल परिभाषाएं कुछ इस तरह दिखती हैं (सचित्र):
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 आपके हैंडलर से पहले वेलिडेट करता है। अगर क्लाइंट गलत डेटा भेजता है तो उसे टाइपड एरर वापस मिलता है, रनटाइम एक्सेप्शन नहीं। यह अच्छी प्रैक्टिस है शुरुआत से ही बनाना।
TypeScript सर्वर को इंप्लीमेंट और रन करें
Supabase डिप्लॉय गाइड @modelcontextprotocol/sdk@1.25.3 को WebStandardStreamableHTTPServerTransport के साथ उपयोग करता है। लेखन के समय यह पिन किया गया संस्करण है। शिप करने से पहले वर्तमान डॉक्स चेक करें।
स्कैफोल्डिंग
mkdir my-mcp-server && cd my-mcp-server
supabase init
supabase functions new mcp
आपका फंक्शन supabase/functions/mcp/index.ts पर रहता है। डिफॉल्ट कंटेंट को इसी तरह कुछ से बदलें (सचित्र स्ट्रक्चर):
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)
})
यहां दो चीजें ध्यान देने लायक हैं। पहली, हर रिक्वेस्ट के लिए एक ताज़ा McpServer और ट्रांसपोर्ट इंस्टेंस वह स्टेटलेस पैटर्न है जो Edge Function रनटाइम उम्मीद करता है। दूसरी, Supabase क्लाइंट को SUPABASE_ANON_KEY से बनाया जाता है, service-role की से नहीं, और JWT रिक्वेस्ट हेडर में भेजा जाता है ताकि RLS कॉलर की आइडेंटिटी देख सके।
स्थानीय रूप से चलाना
supabase start
supabase functions serve --no-verify-jwt mcp
आपका सर्वर http://localhost:54321/functions/v1/mcp पर उपलब्ध है। स्थानीय टेस्टिंग के लिए --no-verify-jwt फ्लैग ठीक है। प्रोडक्शन में JWT वेरिफिकेशन चालू रखेंगे।
कॉलर्स को ऑथेंटिकेट करें और रो एक्सेस लागू करें
यहीं पर ज्यादातर ट्यूटोरियल गलत हो जाते हैं। वे service-role की तक पहुंचते हैं क्योंकि यह आसान है। लेकिन यह RLS को बायपास करता है और मतलब है कि आपका टूल सरफेस, कितना भी संकीर्ण हो, हर रो में एडमिन-लेवल रीड एक्सेस रखता है। कॉलर-फेसिंग सर्वर के लिए यह स्वीकार्य नहीं है।

ऊपर दिखाया गया पैटर्न (कॉलर का JWT पास करें, anon की उपयोग करें) मतलब है कि Supabase Auth टोकन का मूल्यांकन करता है और हर क्वेरी के लिए Postgres के अंदर auth.uid() कॉन्टेक्स्ट सेट करता है। फिर आपकी RLS पॉलिसी उसका उपयोग कर सकती है:
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());
अब alice का JWT केवल उन रोज़ को रिटर्न कर सकता है जहां owner_id उसके UID से मेल खाता है। bob यदि alice की किसी product ID के साथ get_product को कॉल करता है तो उसे Not found या access denied मिलता है, एरर नहीं, न ही alice का डेटा। यह सही व्यवहार है।
RLS पॉलिसीज़ लिखने के बारे में गहरे समझ के लिए, Supabase RLS गाइड सामान्य पैटर्न और नुकसान को विस्तार से कवर करता है।
एक्सेस आइसोलेशन चेक (सचित्र)
दो-यूजर चेक सीधे स्क्रिप्ट करना आसान है। alice के लिए JWT प्राप्त करें (supabase.auth.signInWithPassword के माध्यम से), list_products को कॉल करें, कन्फर्म करें कि सभी रिटर्नड owner_id वैल्यूज़ alice के UID से मेल खाती हैं। bob के साथ दोहराएं। जीरो क्रॉस-कंटेमिनेशन कन्फर्म करें। अगर आपकी पॉलिसीज़ सही हैं, तो यह निर्माण से पास होता है। अगर आपने service-role की गलती की है, तो यह यहां सामने आएगी क्योंकि दोनों यूजर्स को सभी रोज़ दिखेंगी।
अगर आपकी Supabase प्रोजेक्ट पहले से ही इसके साथ-साथ जटिल Next.js डेटा फेचिंग की जरूरतें रखती है, तो /solutions/nextjs-supabase-development/ की टीम डेटाबेस लेयर को उन service-role शॉर्टकट्स के बिना स्ट्रक्चर करने में मदद दे सकती है जो ये समस्याएं बनाते हैं।
Claude Code को कनेक्ट करें और त्रुटि मामलों को सत्यापित करें
एक बार जब आपका फंक्शन स्थानीय रूप से चल रहा हो, तो इसे Claude Code में जोड़ें:
claude mcp add products-mcp -t http http://localhost:54321/functions/v1/mcp
फिर, एक Claude Code सेशन से, आप अपने टूल्स को सीधे कॉल कर सकते हैं। आपको उपलब्ध टूल सूची में list_products और get_product दिखना चाहिए। यदि नहीं, तो claude mcp list चलाएं यह पुष्टि करने के लिए कि सर्वर सही ढंग से पंजीकृत हुआ है।
Inspector ट्रांसक्रिप्ट वॉकथ्रू
MCP Inspector (npx @modelcontextprotocol/inspector) आपको एक ब्राउज़र UI देता है ताकि आप पूर्ण क्लाइंट के बिना टूल्स का उपयोग कर सकें। जैसा कि mcp-lite गाइड में नोट किया गया है, आप अपने endpoint URL को Inspector में पेस्ट करते हैं और यह आपके टूल्स को स्वचालित रूप से खोजता है।
list_products के लिए एक उदाहरण Inspector सेशन:
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\"}...]" }]
}
}
अब त्रुटि मामलों को जानबूझकर परीक्षण करें। Alice के JWT का उपयोग करके Bob के अंतर्गत आने वाले एक मान्य UUID के साथ get_product को कॉल करें। आपको Not found or access denied मिलना चाहिए। page_size: 200 के साथ list_products को कॉल करें (आपकी 100 की सीमा से ऊपर)। Zod को क्वेरी चलने से पहले इसे अस्वीकार करना चाहिए। दोनों अपेक्षित व्यवहार हैं, और आप तैनात करने से पहले दोनों की पुष्टि करना चाहते हैं।
JWT अनुपस्थित होने पर क्या होता है?
Authorization हेडर के बिना, आपका createClient कॉल एक खाली JWT पास करता है। Supabase इसे anon भूमिका के रूप में मानता है। यदि आपकी RLS नीति स्पष्ट रूप से anon भूमिका को एक्सेस प्रदान नहीं करती है, तो क्वेरी शून्य पंक्तियां लौटाती है। जो सही है। टूल एक त्रुटि नहीं, एक खाली सरणी लौटाता है। आप एक स्पष्ट "unauthenticated" संदेश लौटाना पसंद कर सकते हैं। किसी भी तरह से, उत्पादन में इसकी खोज करने के बजाय जानबूझकर निर्णय लें।
सर्वर को तैनात करें, निरीक्षण करें और संस्करण दें
तैनाती एक एकल कमांड है:
supabase functions deploy mcp
आपका फंक्शन https://<project-ref>.supabase.co/functions/v1/mcp पर एक स्थिर URL प्राप्त करता है। Supabase डैशबोर्ड में पर्यावरण चर सेट करें (Settings > Edge Functions > Secrets): SUPABASE_URL और SUPABASE_ANON_KEY। फंक्शन रनटाइम आह्वान के समय उन्हें इंजेक्ट करता है।
दृश्यमानता
Supabase डैशबोर्ड में अंतर्निहित Edge Function लॉग प्रदान करता है। फंक्शन नाम के अनुसार फ़िल्टर करें। आपको कोल्ड स्टार्ट समय, निष्पादन अवधि और कोई भी अनहोंडल्ड त्रुटि दिखाई देगी। अधिक संरचित दृश्यशीलता के लिए, अपने टूल हैंडलर्स के अंदर से console.log JSON लिखें। लॉग स्ट्रीम इसे उठाती है।
उत्पादन उपयोग के लिए, निम्नलिखित पर विचार करें:
- प्रति अनुरोध टूल नाम और कॉलर UID (पूर्ण JWT नहीं) को लॉग करना।
- अपने Supabase क्वेरीज़ पर एक टाइमआउट सेट करना ताकि एक धीमी क्वेरी आपके Edge Function निष्पादन बजट को न जला दे।
- निर्धारक त्रुटि आकार लौटाना ताकि AI क्लाइंट विफलताओं को सुसंगत रूप से संभाल सके।
संस्करण निर्धारण
Edge Functions में अंतर्निहित संस्करण निर्धारण नहीं है। व्यावहारिक दृष्टिकोण पाथ-आधारित है: एक v2 फंक्शन को एक अलग Edge Function के रूप में तैनात करें (supabase functions new mcp-v2), इसे स्वतंत्र रूप से परीक्षण करें, फिर अपने Claude Code पंजीकरण को अपडेट करें। पुराने क्लाइंट तब तक v1 URL की ओर इशारा करते रह सकते हैं जब तक आप उन्हें काट न दें।
यदि आप कई सर्वरों के साथ एक उत्पादन MCP स्टैक बना रहे हैं, तो production MCP stack post सर्वर चयन और आर्किटेक्चर निर्णयों को कवर करता है जो यहां स्कोप से बाहर हैं।
तैनाती चेकलिस्ट
- तैनात करने से पहले स्थानीय serve कमांड से
--no-verify-jwtहटाएं। - पुष्टि करें कि
SUPABASE_ANON_KEYडैशबोर्ड सीक्रेट्स में सेट है, service-role में नहीं। - उत्पादन URL के विरुद्ध अपनी दो-उपयोगकर्ता पहुंच अलगाव जांच चलाएं और फिर endpoint की घोषणा करें।
- असीमित प्रश्नों को रोकने के लिए हर tool में
max_rowscap सेट करें। - आपके import में pinned किए गए SDK संस्करण (
@modelcontextprotocol/sdk@1.25.3लिखने के समय Supabase docs में) को वर्तमान releases के विरुद्ध जांचें और यदि आवश्यक हो तो अपडेट करें।
FAQ
क्या मैं आधिकारिक SDK की जगह mcp-lite का उपयोग कर सकता हूं?
हां। Supabase docs स्पष्ट रूप से कहते हैं कि आप आधिकारिक SDK के विकल्प के रूप में mcp-lite या mcp-handler का उपयोग कर सकते हैं। ऊपर दिया गया WebStandardStreamableHTTPServerTransport आधिकारिक SDK से आता है, लेकिन mcp-lite हल्का है और zero-dependency है। एक चुनें और उसे adopt करें। mcp-lite guide scaffold command दिखाता है: npm create mcp-lite@latest। ऊपर described architecture और RLS behavior आप कौन सा framework चुनते हैं इस बात की परवाह किए बिना लागू होता है।
Edge Functions को deploy करने के लिए क्या मुझे paid Supabase plan की जरूरत है?
Edge Functions आमंत्रण और execution time पर कुछ limits के साथ free tier पर उपलब्ध हैं। संख्याओं के लिए वर्तमान Supabase pricing page देखें। कम ट्रैफिक वाले internal tool के लिए, free tier आमतौर पर ठीक है। असली query volume के साथ production-facing किसी भी चीज़ के लिए, Pro plan की limits अधिक उपयुक्त हैं।
अगर मेरा MCP server केवल पढ़ना नहीं है, बल्कि डेटा write करने की जरूरत है, तो क्या करूं?
ऐसे tools जोड़ें जो insert, update, या delete queries चलाएं। RLS pattern अभी भी लागू होता है: आपकी policy नियंत्रित करती है कि caller auth.uid() के आधार पर कौन सी rows को modify कर सकते हैं। मुख्य अतिरिक्त विचार idempotency है। अगर AI client एक failed tool call को फिर से try करता है, तो क्या आप insert को दो बार चलना चाहते हैं? Stable primary key के साथ upsert का उपयोग करना या insert करने से पहले existence की जांच करना standard mitigations हैं।
क्या मैं Supabase account के बिना इसे locally चला सकता हूं?
हां, supabase start Docker के माध्यम से पूरे Supabase stack को locally चलाता है, जिसमें Postgres, Auth, और Edge Functions शामिल हैं। आप CLI output से एक local SUPABASE_URL और SUPABASE_ANON_KEY प्राप्त करते हैं। आपका function और RLS policies बिल्कुल वैसे ही काम करते हैं जैसे वे production में करेंगे। एकमात्र अंतर JWT issuer है, जो Supabase के cloud Auth की बजाय local GoTrue instance है।
मैं tools पर rate limiting को कैसे संभालूं?
Edge Functions के पास built-in per-caller rate limiting नहीं है। व्यावहारिक options हैं: एक Postgres table जो UID per time window पर call counts को record करता है (tool के अंदर check और increment करें), या एक upstream API gateway (Supabase का अपना API gateway project level पर इसका कुछ हिस्सा handle करता है)। अधिकांश internal tools के लिए, free-tier project पर Edge Function invocation limit को hit करना पहली constraint है जिसका आप सामना करेंगे, जानबूझकर abuse नहीं।
ऊपर सब कुछ से एक सबसे तीव्र सावधानी: अगर आप caller-facing tool में service-role key का उपयोग करते हैं, तो आपकी RLS policies कुछ नहीं करती हैं। anon key plus JWT forwarding एक nice-to-have नहीं है, यह वह mechanism है जो caller isolation को काम करता है।
