A remote Cloudflare MCP server boilerplate with user authentication and Stripe for paid tools.
This project helps you create your own remote MCP server on Cloudflare with user login and payment options. You don't need to be a technical expert to get it running.
[!NOTE]
This project is now free to use and open source. If you want to support me, just follow me on X @iannuttall and subscribe to my newsletter.
git clone https://github.com/iannuttall/mcp-boilerplate.git
cd mcp-boilerplate
npm install
npm install -g wrangler
npx wrangler kv namespace create "OAUTH_KV"
Note: you can't use a different name for this database. It has to be "OAUTH_KV".
id
and preview_id
valueswrangler.jsonc
file in the project folder"kv_namespaces": [
"kv_namespaces": [
{
"binding": "OAUTH_KV",
"id": "paste-your-id-here",
"preview_id": "paste-your-preview-id-here"
}
]
cp .dev.vars.example .dev.vars
.dev.vars
file in your code editorhttp://localhost:8787/callback/google
.dev.vars
file:GOOGLE_CLIENT_ID="paste-your-client-id-here"
GOOGLE_CLIENT_SECRET="paste-your-client-secret-here"
Once you've completed this step, you can proceed directly to Step 5 if you don't need GitHub login.
If you prefer to use GitHub for login instead of Google:
http://localhost:8787
http://localhost:8787/callback/github
.dev.vars
file:GITHUB_CLIENT_ID="paste-your-client-id-here"
GITHUB_CLIENT_SECRET="paste-your-client-secret-here"
src/index.ts
import { GoogleHandler } from "./auth/google-handler";
import { GitHubHandler } from "./auth/github-handler";
defaultHandler: GoogleHandler as any,
defaultHandler: GitHubHandler as any,
After completing either Step 4a or 4b, proceed to Step 5.
sk_test_
)price_
).dev.vars
file:STRIPE_SECRET_KEY="sk_test_your-key-here"
STRIPE_SUBSCRIPTION_PRICE_ID="price_your-price-id-here"
STRIPE_METERED_PRICE_ID="your-stripe-metered-price-id"
This boilerplate includes a tool (check_user_subscription_status
) that can provide your end-users with a link to their Stripe Customer Billing Portal. This portal allows them to manage their subscriptions, such as canceling them or, if you configure it, switching between different plans.
Initial Setup (Important):
By default, the Stripe Customer Billing Portal might not be fully configured in your Stripe account, especially in the test environment.
check_user_subscription_status
tool (e.g., via MCP Inspector, or by triggering it through an AI assistant).billingPortal.message
contains an error like: "Could not generate a link to the customer billing portal: No configuration provided and your test mode default configuration has not been created. Provide a configuration or create your default by saving your customer portal settings in test mode at https://dashboard.stripe.com/test/settings/billing/portal."https://dashboard.stripe.com/test/settings/billing/portal
) and save your portal settings in Stripe. This activates the portal for your test environment. You'll need to do a similar check and configuration for your live environment.Once activated, the check_user_subscription_status
tool will provide a direct link in the billingPortal.url
field of its JSON response, which your users can use.
Allowing Users to Switch Plans (Optional):
By default, the billing portal allows users to cancel their existing subscriptions. If you offer multiple subscription products for your MCP server and want to allow users to switch between them:
https://dashboard.stripe.com/settings/billing/portal
for live mode, or https://dashboard.stripe.com/test/settings/billing/portal
for test mode).This configuration empowers your users to manage their subscriptions more flexibly directly through the Stripe-hosted portal.
Make sure your .dev.vars
file has all these values:
BASE_URL="http://localhost:8787"
COOKIE_ENCRYPTION_KEY="generate-a-random-string-at-least-32-characters"
GOOGLE_CLIENT_ID="your-google-client-id"
GOOGLE_CLIENT_SECRET="your-google-client-secret"
STRIPE_SECRET_KEY="your-stripe-secret-key"
STRIPE_SUBSCRIPTION_PRICE_ID="your-stripe-price-id"
STRIPE_METERED_PRICE_ID="your-stripe-metered-price-id"
For the COOKIE_ENCRYPTION_KEY
, you can generate a random string with this command:
openssl rand -hex 32
npx wrangler dev
http://localhost:8787
http://localhost:8787/sse
You can test your server by connecting to it with an AI assistant:
http://localhost:8787/sse
Or with Claude Desktop:
{
"mcpServers": {
"my_server": {
"command": "npx",
"args": [
"mcp-remote",
"http://localhost:8787/sse"
]
}
}
}
Or with MCP Inspector:
npx @modelcontextprotocol/inspector@0.11.0
[!WARNING]
The latest version of MCP Inspector is 0.12.0 but using npx @modelcontextprotocol/inspector@latest doesn't work right now. Working on it.
http://localhost:8787/sse
When you're ready to make your server available online:
npx wrangler deploy
https://your-worker-name.your-account.workers.dev
3a. Update your Google OAuth settings:
https://your-worker-name.your-account.workers.dev/callback/google
.3b. Update your GitHub OAuth App settings: (optional)
https://your-worker-name.your-account.workers.dev/callback/github
npx wrangler secret put BASE_URL
npx wrangler secret put COOKIE_ENCRYPTION_KEY
npx wrangler secret put GOOGLE_CLIENT_ID
npx wrangler secret put GOOGLE_CLIENT_SECRET
npx wrangler secret put STRIPE_SECRET_KEY
npx wrangler secret put STRIPE_SUBSCRIPTION_PRICE_ID
npx wrangler secret put STRIPE_METERED_PRICE_ID
For the BASE_URL
, use your Cloudflare URL: https://your-worker-name.your-account.workers.dev
You can easily create your own AI tools by adding new files to the src/tools
folder. The project comes with examples of both free and paid tools.
To create a free tool (one that users can access without payment):
src/tools
folder (for example: myTool.ts
)add.ts
example:import { z } from "zod";
import { experimental_PaidMcpAgent as PaidMcpAgent } from "@stripe/agent-toolkit/cloudflare";
export function myTool(agent: PaidMcpAgent<Env, any, any>) {
const server = agent.server;
// @ts-ignore
server.tool(
"my_tool_name", // The tool name
"This tool does something cool.", // Description of what your tool does
{ // Input parameters
input1: z.string(), // Parameter definitions using Zod
input2: z.number() // E.g., strings, numbers, booleans
},
async ({ input1, input2 }: { input1: string; input2: number }) => ({
// The function that runs when the tool is called
content: [{ type: "text", text: `You provided: ${input1} and ${input2}` }],
})
);
}
myTool
)my_tool_name
)src/tools/index.ts
:// Add this line with your other exports
export * from './myTool';
src/index.ts
:// Inside the init() method, add:
tools.myTool(this);
You can create tools that require payment in three ways: recurring subscriptions, metered usage, or one-time payments.
This option is suitable if you want to charge users a recurring fee (e.g., monthly) for access to a tool or a suite of tools.
Stripe Setup for Subscription Billing:
price_xxxxxxxxxxxxxx
). This is the ID you will use for STRIPE_SUBSCRIPTION_PRICE_ID
in your .dev.vars
file and when registering the tool.Tool Implementation:
src/tools
folder (for example: mySubscriptionTool.ts
)subscriptionAdd.ts
example:import { z } from "zod";
import { experimental_PaidMcpAgent as PaidMcpAgent } from "@stripe/agent-toolkit/cloudflare";
import { REUSABLE_PAYMENT_REASON } from "../helpers/constants";
export function mySubscriptionTool(
agent: PaidMcpAgent<Env, any, any>,
env?: { STRIPE_SUBSCRIPTION_PRICE_ID: string; BASE_URL: string }
) {
const priceId = env?.STRIPE_SUBSCRIPTION_PRICE_ID || null;
const baseUrl = env?.BASE_URL || null;
if (!priceId || !baseUrl) {
throw new Error("Stripe Price ID and Base URL must be provided for paid tools");
}
agent.paidTool(
"my_subscription_tool_name", // The tool name
{
// Input parameters
input1: z.string(), // Parameter definitions using Zod
input2: z.number(), // E.g., strings, numbers, booleans
},
async ({ input1, input2 }: { input1: string; input2: number }) => ({
// The function that runs when the tool is called
content: [
{ type: "text", text: `You provided: ${input1} and ${input2}` },
],
}),
{
priceId, // Uses the Stripe price ID for a subscription product
successUrl: `${baseUrl}/payment/success`,
paymentReason: REUSABLE_PAYMENT_REASON, // General reason shown to user
}
);
}
mySubscriptionTool
)my_subscription_tool_name
)src/tools/index.ts
:// Add this line with your other exports
export * from './mySubscriptionTool';
src/index.ts
:// Inside the init() method, add:
tools.mySubscriptionTool(this, {
STRIPE_SUBSCRIPTION_PRICE_ID: this.env.STRIPE_SUBSCRIPTION_PRICE_ID, // Ensure this matches a subscription Price ID
BASE_URL: this.env.BASE_URL
});
This option is suitable if you want to charge users based on how much they use an MCP tool.
Stripe Setup for Metered Billing:
price_xxxxxxxxxxxxxx
).metered_add_usage
) that you'll use in your tool's code. You can usually set this up under the "Usage" tab of your product or when defining the metered price.Tool Implementation:
src/tools
folder (e.g., myMeteredTool.ts
).meteredAdd.ts
example:import { z } from "zod";
import { experimental_PaidMcpAgent as PaidMcpAgent } from "@stripe/agent-toolkit/cloudflare";
import { METERED_TOOL_PAYMENT_REASON } from "../helpers/constants"; // You might want a specific constant
export function myMeteredTool(
agent: PaidMcpAgent<Env, any, any>,
env?: { STRIPE_METERED_PRICE_ID: string; BASE_URL: string }
) {
const priceId = env?.STRIPE_METERED_PRICE_ID || null;
const baseUrl = env?.BASE_URL || null;
if (!priceId || !baseUrl) {
throw new Error("Stripe Metered Price ID and Base URL must be provided for metered tools");
}
agent.paidTool(
"my_metered_tool_name", // The tool name
{
// Input parameters
a: z.number(),
b: z.number(),
},
async ({ a, b }: { a: number; b: number }) => {
// The function that runs when the tool is called
// IMPORTANT: Business logic for your tool
const result = a + b; // Example logic
return {
content: [{ type: "text", text: String(result) }],
};
},
{
checkout: {
success_url: `${baseUrl}/payment/success`,
line_items: [
{
price: priceId, // Uses the Stripe Price ID for a metered product
},
],
mode: 'subscription', // Metered plans are usually set up as subscriptions
},
paymentReason:
"METER INFO: Details about your metered usage. E.g., Your first X uses are free, then $Y per use. " +
METERED_TOOL_PAYMENT_REASON, // Customize this message
meterEvent: "your_meter_event_name_from_stripe", // ** IMPORTANT: Use the event name from your Stripe meter setup **
// e.g., "metered_add_usage"
}
);
}
myMeteredTool
).my_metered_tool_name
).meterEvent
to match the event name you configured in your Stripe meter.paymentReason
to clearly explain the metered billing to the user.src/tools/index.ts
:// Add this line with your other exports
export * from './myMeteredTool';
src/index.ts
:// Inside the init() method, add:
tools.myMeteredTool(this, {
STRIPE_METERED_PRICE_ID: this.env.STRIPE_METERED_PRICE_ID, // Ensure this matches your metered Price ID
BASE_URL: this.env.BASE_URL
});
This option is suitable if you want to charge users a single fee for access to a tool, rather than a recurring subscription or usage-based metering.
Stripe Setup for One-Time Payments:
price_xxxxxxxxxxxxxx
). This is the ID you will use for a new environment variable, for example, STRIPE_ONE_TIME_PRICE_ID
.Tool Implementation:
src/tools
folder (for example: myOnetimeTool.ts
).onetimeAdd.ts
example:import { z } from "zod";
import { experimental_PaidMcpAgent as PaidMcpAgent } from "@stripe/agent-toolkit/cloudflare";
import { REUSABLE_PAYMENT_REASON } from "../helpers/constants"; // Or a more specific reason
export function myOnetimeTool(
agent: PaidMcpAgent<Env, any, any>, // Adjust AgentProps if needed
env?: { STRIPE_ONE_TIME_PRICE_ID: string; BASE_URL: string }
) {
const priceId = env?.STRIPE_ONE_TIME_PRICE_ID || null;
const baseUrl = env?.BASE_URL || null;
if (!priceId || !baseUrl) {
throw new Error("Stripe One-Time Price ID and Base URL must be provided for this tool");
}
agent.paidTool(
"my_onetime_tool_name", // The tool name
{
// Input parameters
input1: z.string(), // Parameter definitions using Zod
},
async ({ input1 }: { input1: string }) => ({
// The function that runs when the tool is called
content: [
{ type: "text", text: `You processed: ${input1}` },
],
}),
{
checkout: { // Defines a one-time payment checkout session
success_url: `${baseUrl}/payment/success`,
line_items: [
{
price: priceId, // Uses the Stripe Price ID for a one-time payment product
quantity: 1,
},
],
mode: 'payment', // Specifies this is a one-time payment, not a subscription
},
paymentReason: "Enter a clear reason for this one-time charge. E.g., 'Unlock premium feature X for a single use.'", // Customize this message
}
);
}
myOnetimeTool
).my_onetime_tool_name
).checkout.mode
is set to 'payment'
.paymentReason
to clearly explain the one-time charge to the user.src/tools/index.ts
:// Add this line with your other exports
export * from './myOnetimeTool';
src/index.ts
:// Inside the init() method, add:
tools.myOnetimeTool(this, {
STRIPE_ONE_TIME_PRICE_ID: this.env.STRIPE_ONE_TIME_PRICE_ID, // Ensure this matches your one-time payment Price ID
BASE_URL: this.env.BASE_URL
});
STRIPE_ONE_TIME_PRICE_ID
to your .dev.vars
file and Cloudflare secrets:.dev.vars
:STRIPE_ONE_TIME_PRICE_ID="price_your-onetime-price-id-here"
And for production:
npx wrangler secret put STRIPE_ONE_TIME_PRICE_ID
You can create different paid tools with different Stripe products (subscription or metered) by creating additional price IDs in your Stripe dashboard and passing them as environment variables.
When a user tries to access a paid tool without having purchased it:
The basic setup above is all you need to get started. The built-in Stripe integration verifies payments directly when users try to access paid tools - it checks both one-time payments and subscriptions automatically.
Webhooks are completely optional but could be useful for more complex payment scenarios in the future, like:
If you ever want to add webhook support:
http://localhost:8787/webhooks/stripe
https://your-worker-name.your-account.workers.dev/webhooks/stripe
.dev.vars
:STRIPE_WEBHOOK_SECRET="whsec_your-webhook-secret-here"
npx wrangler secret put STRIPE_WEBHOOK_SECRET
If you encounter any bugs or have issues with the boilerplate, please submit an issue on the GitHub repository. Please note that this project is provided as-is and does not include direct support.
iannuttall/mcp-boilerplate
May 7, 2025
July 7, 2025
TypeScript