@powforge/mcp-tool-l402
v0.1.0
Published
L402 Lightning payment middleware for Model Context Protocol (MCP) tool handlers. Wrap any MCP tool callback with a pay-per-call Lightning gate backed by LNBits. Returns valid MCP CallToolResult envelopes on both the payment-required and paid paths so the
Downloads
140
Maintainers
Readme
@powforge/mcp-tool-l402
L402 Lightning payment middleware for Model Context Protocol (MCP) tool handlers. Wrap any MCP tool callback with a pay-per-call Lightning gate backed by LNBits. Returns valid MCP CallToolResult envelopes on both the payment-required and paid paths so the wire format stays spec-compliant.
npm i @powforge/mcp-tool-l402Why
You run an MCP server. You want some tools to be free (list_files) and others to cost a few sats per call (search_archive, generate_image, run_inference). This package gives you a single function-wrapper that adds an L402 gate to any tool callback you're already passing to server.registerTool(...) or server.tool(...).
The wrapper does the right thing on the wire:
- No proof in args → mints a Lightning invoice via LNBits and returns a
CallToolResultwithisError: trueand a JSON envelope describing how to pay. - Proof present + paid → runs your handler and returns the result as a valid
CallToolResult. - Proof present + unpaid → returns
CallToolResult{ error: 'invoice_not_paid', payment_hash, next_step }. - LNBits down → returns
CallToolResult{ error: 'payment_provider_unavailable' | 'payment_verifier_unavailable', detail }.
The agent on the other side parses the JSON envelope from content[0].text, pays the invoice, then re-calls the tool with args._paymentProof = "<payment_hash>".
Quickstart
const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
const { z } = require('zod');
const { wrapMcpToolWithL402 } = require('@powforge/mcp-tool-l402');
const server = new McpServer({ name: 'paid-search', version: '0.1.0' });
server.registerTool(
'search',
{
description: 'Search the archive (10 sats per call)',
inputSchema: {
query: z.string(),
_paymentProof: z.string().optional(), // L402 payment hash
},
},
wrapMcpToolWithL402(
async ({ query }) => {
// Your real handler. Payment is already verified by the time
// this runs. The _paymentProof key is stripped from args.
const hits = await searchArchive(query);
return { content: [{ type: 'text', text: JSON.stringify(hits) }] };
},
{
lnbitsUrl: process.env.LNBITS_URL,
lnbitsApiKey: process.env.LNBITS_INVOICE_KEY,
satsAmount: 10,
},
),
);Factory form
If you prefer to keep tool metadata next to the wrapper, use createL402McpTool:
const { createL402McpTool } = require('@powforge/mcp-tool-l402');
const tool = createL402McpTool({
name: 'search',
description: 'Search the archive',
handler: async ({ query }) => `hits for ${query}`,
satsAmount: 10,
lnbitsUrl: process.env.LNBITS_URL,
lnbitsApiKey: process.env.LNBITS_INVOICE_KEY,
});
server.tool(tool.name, tool.description, tool.callback);Config
| Key | Required | Default | Notes |
|---|---|---|---|
| lnbitsUrl | yes (unless injecting fns) | — | e.g. https://lnbits.example |
| lnbitsApiKey | yes (unless injecting fns) | — | invoice/read key (NOT admin) |
| satsAmount | no | 10 | invoice amount per call |
| memo | no | 'mcp-tool-l402' | LNBits invoice memo |
| proofKey | no | '_paymentProof' | args key the wrapper reads for proof |
| fetchImpl | no | globalThis.fetch | inject for tests / non-Node runtimes |
| createInvoiceFn | no | LNBits-backed | inject to bypass LNBits entirely |
| checkPaidFn | no | LNBits-backed | inject to bypass LNBits entirely |
| state | no | new in-memory map | inject _createPaymentState() to share the paid-cache across wrappers |
402 envelope (embedded as JSON text in content[0].text)
{
"error": "payment_required",
"invoice": "lnbc100n1...",
"payment_hash": "abc...",
"sats": 10,
"next_step": "Pay the invoice, then re-call this tool with the payment_hash in args._paymentProof."
}The result isError field is set to true so MCP clients can branch on it without parsing the envelope.
Why a payment_hash and not a 32-byte preimage
This adapter accepts the LNBits payment_hash (the simpler form). The wrapper still verifies the hash is actually paid against LNBits before unlocking the call. Callers who need cryptographic preimage proof should use @powforge/mcp-l402-gate — the full macaroon-based server-level gate.
Position in the PowForge L402 family
| Package | Surface | Use when |
|---|---|---|
| @powforge/mcp-l402-gate | HTTP server gate (macaroon + WWW-Authenticate: L402) | You want transport-level paywalling for the whole MCP server |
| @powforge/mcp-tool-l402 | per-tool MCP callback wrapper (this package) | You want some tools paid and some free on the same server |
| @powforge/paymcp-l402-provider | BasePaymentProvider for paymcp | You're already using paymcp and want to plug in L402 |
| @powforge/langchain-l402-middleware | LangChain.js DynamicTool wrapper | You're building a LangChain agent |
| @powforge/semantic-kernel-l402 | Semantic Kernel KernelFunction wrapper | You're building a Semantic Kernel agent |
All four use the same LNBits backend (POST /api/v1/payments, GET /api/v1/payments/{hash}), so a single LNBits wallet can power your whole agent surface.
Tests
npm test43 tests covering config validation, the no-proof path, the paid path, the unpaid/down LNBits paths, handler-result normalization (string / object / CallToolResult pass-through), proof-key configurability, state-sharing, and the factory.
License
MIT
