Wrap an existing MCP tool with Toolgate so it handles pricing, fallback, idempotency, recovery, and traces — without rewriting your tool. Start local-first, add payment rails later.
All imports use the public package API (@tkorkmaz/toolgate).
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { ToolGate, createMcpAdapter, usd } from "@tkorkmaz/toolgate";
const server = new McpServer({ name: "my-tools", version: "1.0.0" });
const gate = new ToolGate({ publisherKey: "tg_local_demo" });
const mcp = createMcpAdapter(gate, {
// Map an MCP session to a Toolgate caller identity.
getCallerId: (_args, extra) => extra?.sessionId ?? "demo-user",
// Attach Toolgate metadata to responses (default true).
includeMeta: true,
});
mcp.paidTool("premium_search", {
description: "AI-powered search with deep analysis.",
price: usd("0.05"),
onPaymentFailed: "fallback",
inputSchema: {
type: "object",
properties: { query: { type: "string" } },
required: ["query"],
},
handler: async (args) => ({ results: [`deep results for ${args.query}`] }),
fallback: async (args) => ({ results: [`basic result for ${args.query}`] }),
});
// Register the wrapped tool(s) with your MCP server.
mcp.registerAll(server);If you omit fallback, the tool defaults to onPaymentFailed: "block" and returns a
payment_required response instead of a degraded result.
When the caller can't pay and a fallback is defined, the tool still returns a successful
MCP result — the degraded payload plus a notice appended to the content:
⚡ This is a basic result. Top up your balance for the full premium version.
No charge is made. With onPaymentFailed: "block" (no fallback) the call returns
isError: true with a "Payment required" message, the amount, accepted rails, and a top-up URL.
On a normal/fallback result (includeMeta: true):
On a payment_required (402) result:
{
"_meta": {
"toolgate": {
"paymentRequired": true,
"amount": 0.05,
"currency": "usd",
"acceptedRails": ["stripe"],
"topUpUrl": "https://your-topup-endpoint.example/pay?..."
}
}
}Agents that understand Toolgate can read _meta.toolgate to decide whether to top up and retry.
You do not need Stripe, x402, or MPP to develop and test. Use the in-memory ledger and credit callers directly:
import { InMemoryLedger, usd } from "@tkorkmaz/toolgate";
const ledger = new InMemoryLedger();
const gate = new ToolGate({ publisherKey: "tg_local_demo", ledger });
await ledger.credit("demo-user", usd("1.00"), {
source: "manual",
reference: "dev-credit",
});When you're ready for real money, add a rail adapter (StripeRailAdapter, X402RailAdapter,
or MppRailAdapter) to the ToolGate config and switch the ledger to DbLedger. See the
README "Advanced payment rails" and "Production status" sections for maturity and limitations.
For a runnable, no-env demo of the full lifecycle:
npm run example:local
{ "_meta": { "toolgate": { "paid": true, // false when served from fallback "isFallback": false, "receipt": { // null when nothing was charged "amount": 0.05, "currency": "usd", "rail": "prepaid", "balanceAfter": 0.95 }, "metrics": { "durationMs": 12 } } } }