Building a Production-Ready MCP Server with TypeScript, Stripe, and Supabase
Most Model Context Protocol (MCP) tutorials show you how to build an echo server, a dice roller, or a weather lookup. That's fine for learning the basics, but what happens when you want to connect an AI agent to your actual stack—your payment processor, your database, your auth system?
The gap between "hello world" and production is wide. This tutorial bridges it. You'll build a typed MCP server in TypeScript that exposes real Stripe and Supabase operations, with full type safety from your tools all the way to the LLM.
What Is MCP and Why TypeScript?
The Model Context Protocol is Anthropic's open standard for connecting AI assistants to external data sources and tools. Instead of hardcoding integrations or relying on fragile prompt injection, MCP lets you expose structured tools that Claude (or any MCP-compatible client) can discover and invoke.
TypeScript is the natural choice for production MCP servers:
- Type safety end-to-end: Your tool schemas, validation logic, and external API calls all share the same type definitions
- Autocomplete for tool definitions: No more guessing at JSON Schema syntax
- Runtime safety with Zod: Validate LLM-generated parameters before they hit your database or payment APIs
- Ecosystem fit: Both Stripe and Supabase have excellent TypeScript SDKs
The official @modelcontextprotocol/sdk supports TypeScript natively, making setup straightforward.
Architecture: Tools, Resources, and Type Safety
A well-designed MCP server separates concerns into three layers:
- Schema layer: Zod schemas that define and validate tool inputs
- Service layer: Business logic that calls Stripe, Supabase, etc.
- MCP layer: Tool registration and request handling
Here's the skeleton:
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import Stripe from "stripe";
import { createClient } from "@supabase/supabase-js";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-12-18.acacia",
});
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_KEY!
);
const server = new Server(
{
name: "payment-stack-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
Building Type-Safe Tools
Let's implement a tool that creates a Stripe checkout session and logs it to Supabase. Start with the Zod schema:
const CreateCheckoutSchema = z.object({
priceId: z.string().startsWith("price_"),
customerId: z.string().startsWith("cus_").optional(),
successUrl: z.string().url(),
cancelUrl: z.string().url(),
});
type CreateCheckoutInput = z.infer<typeof CreateCheckoutSchema>;
Now register the tool with MCP:
server.setRequestHandler("tools/list", async () => ({
tools: [
{
name: "create_checkout",
description: "Create a Stripe checkout session and log it to Supabase",
inputSchema: {
type: "object",
properties: {
priceId: {
type: "string",
description: "Stripe price ID (e.g., price_1A2B3C)",
},
customerId: {
type: "string",
description: "Optional existing Stripe customer ID",
},
successUrl: { type: "string" },
cancelUrl: { type: "string" },
},
required: ["priceId", "successUrl", "cancelUrl"],
},
},
],
}));
Implement the handler with full type safety:
server.setRequestHandler("tools/call", async (request) => {
if (request.params.name === "create_checkout") {
// Validate with Zod
const input = CreateCheckoutSchema.parse(request.params.arguments);
// Create Stripe session
const session = await stripe.checkout.sessions.create({
line_items: [{ price: input.priceId, quantity: 1 }],
mode: "payment",
success_url: input.successUrl,
cancel_url: input.cancelUrl,
customer: input.customerId,
});
// Log to Supabase
const { error } = await supabase.from("checkout_sessions").insert({
session_id: session.id,
customer_id: input.customerId,
amount_total: session.amount_total,
currency: session.currency,
status: session.status,
created_at: new Date(session.created * 1000).toISOString(),
});
if (error) throw new Error(`Supabase error: ${error.message}`);
return {
content: [
{
type: "text",
text: JSON.stringify({
sessionId: session.id,
url: session.url,
status: session.status,
}),
},
],
};
}
throw new Error(`Unknown tool: ${request.params.name}`);
});
Key Patterns for Production
1. Validate everything: Never trust LLM-generated inputs. Use Zod schemas before touching external APIs.
2. Return structured data: Instead of plain text responses, return JSON that the LLM can parse reliably.
3. Handle errors gracefully: Wrap external API calls in try-catch blocks and return actionable error messages:
try {
const session = await stripe.checkout.sessions.create(...);
} catch (err) {
if (err instanceof Stripe.errors.StripeInvalidRequestError) {
return {
content: [{ type: "text", text: `Invalid Stripe request: ${err.message}` }],
isError: true,
};
}
throw err;
}
4. Use environment-specific configs: Keep separate Stripe keys and Supabase projects for development, staging, and production.
5. Log tool invocations: Track which tools are called, with what inputs, and whether they succeed. This is critical for debugging and abuse detection.
Running Your MCP Server
Start the server with stdio transport:
const transport = new StdioServerTransport();
await server.connect(transport);
Then configure your MCP client (like Claude Desktop) to launch it:
{
"mcpServers": {
"payment-stack": {
"command": "node",
"args": ["dist/index.js"],
"env": {
"STRIPE_SECRET_KEY": "sk_test_...",
"SUPABASE_URL": "https://xxx.supabase.co",
"SUPABASE_SERVICE_KEY": "eyJh..."
}
}
}
}
The Takeaway
Building a production MCP server isn't about memorizing the protocol—it's about applying the same patterns you'd use in any production TypeScript service: strong typing, input validation, error handling, and observability.
The real power of MCP shows up when you connect it to your actual infrastructure. A typed server that exposes Stripe, Supabase, your auth provider, and your internal APIs gives Claude (or any MCP client) safe, structured access to operations that previously required human intervention.
Start with one or two tools that automate a painful workflow—refunding a charge, provisioning a test user, querying recent errors. Build type safety in from day one. You'll have a foundation you can extend for years.
The full code and setup instructions are available in the @modelcontextprotocol/create-typescript-server template. Clone it, add your tools, and ship something real.