Nevermined Payments Library provides robust tools to add a paywall to your Model Context Protocol (MCP) server. This allows you to monetize your AI tools, resources, and prompts by restricting access to subscribers with valid payment plans. The integration is designed to be seamless, whether you are using a high-level framework like the official MCP SDKs or a custom low-level JSON-RPC router.
  • Paywall Protection: Wrap your handlers with withPaywall to automatically verify Authorization tokens and check for valid subscriptions.
  • Credit Burning: Automatically burn credits after a successful API call, with support for both fixed and dynamic costs.
  • Declarative Registration: Use the attach method to register and protect your tools in a single, clean step.
  • Framework Agnostic: Works with both high-level servers (like the official TypeScript SDK’s McpServer or Python’s FastMCP) and custom low-level ASGI/Express routers.

What is MCP?

As Large Language Models (LLMs) and AI agents become more sophisticated, their greatest limitation is their isolation. By default, they lack access to real-time information, private data sources, or the ability to perform actions in the outside world. The Model Context Protocol (MCP) was designed to solve this problem by creating a standardized communication layer for AI. Think of MCP as a universal language that allows any AI agent to ask a server, “What can you do?” and “How can I use your capabilities?”. It turns a closed-off model into an agent that can interact with the world through a secure and discoverable interface. An MCP server essentially publishes a “menu” of its services, which can include:
  • Tools: These are concrete actions the agent can request, like sending an email, querying a database, or fetching a weather forecast. The agent provides specific arguments (e.g., city="Paris") and the server executes the action.
  • Resources: These are stable pointers to data, identified by a URI. While a tool call might give a human-readable summary, a resource link (weather://today/Paris) provides the raw, structured data (like a JSON object) that an agent can parse and use for further tasks.
  • Prompts: These are pre-defined templates that help guide an agent’s behavior, ensuring it requests information in the correct format or follows a specific interaction pattern.

Why integrate MCP with Nevermined Payments Library?

While MCP provides a powerful standard for what an AI agent can do, it doesn’t specify who is allowed to do it or how those services are paid for. This is where Nevermined Payments Library comes in. By integrating Nevermined, you can transform your open MCP server into a secure, monetizable platform. The core idea is to place a “paywall” in front of your MCP handlers. This paywall acts as a gatekeeper, intercepting every incoming request to a tool, resource, or prompt. Before executing your logic, it checks the user’s Authorization header to verify they have a valid subscription and sufficient credits through the Nevermined protocol. If they don’t, the request is blocked. If they do, the request proceeds, and after your handler successfully completes, the paywall automatically deducts the configured number of credits. This integration allows you to build a sustainable business model around your AI services. You can offer different subscription tiers (plans), charge dynamically based on usage, and maintain a complete audit trail of every transaction, all without cluttering your core application logic with complex payment code.

Step-by-step Tutorial

In this tutorial, we will embark on a practical journey to build a secure, monetizable MCP server. Our starting point will be a standard, unprotected server—a common scenario for developers who have already created useful AI tools and now wish to commercialize them. From there, we will layer on the security and monetization capabilities of Nevermined Payments Library step by step.

0) Requirements & Installation

  • Node.js >= 18
  • MCP SDK (@modelcontextprotocol/sdk)
  • @nevermined-io/payments (Nevermined SDK)
  • Express.js
yarn add express @modelcontextprotocol/sdk @nevermined-io/payments zod
yarn add -D typescript ts-node @types/express

1) Create a Minimal MCP Server

First, let’s create a basic MCP server without any paywall to ensure the core setup is working.
This server uses the official MCP SDK and exposes a single tool, weather.today.
// server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server";
import { z } from "zod";

export function createMcpServer() {
    const server = new McpServer({
        name: "weather-mcp-ts",
        version: "0.1.0",
    });

    server.registerTool(
        "weather.today",
        {
            title: "Today's Weather",
            inputSchema: z.object({ city: z.string() }),
        },
        async (args) => ({
            content: [{ type: "text", text: `Weather for ${args.city}: Sunny, 25C.` }],
        })
    );
    return server;
}

2) Initialize Nevermined Payments

Next, initialize the Nevermined Payments SDK. This requires your builder/agent owner API key and the environment (sandbox or live).
// payments-setup.ts
import { Payments, EnvironmentName } from "@nevermined-io/payments";

const payments = Payments.getInstance({
  nvmApiKey: process.env.NVM_API_KEY!,
  environment: (process.env.NVM_ENV || "sandbox") as EnvironmentName,
});

// Configure MCP defaults once
payments.mcp.configure({
    agentId: process.env.NVM_AGENT_ID!,
    serverName: "weather-mcp-ts",
});

3) Protect Handlers with a Paywall

The withPaywall (TS) or with_paywall (PY) decorator is the core of the integration. It wraps your business logic, checks for authentication, and burns credits. First, define your handler. It should contain only your business logic, returning a standard MCP content object.
// handlers.ts
import { ToolHandler } from "@modelcontextprotocol/sdk/server";

export const weatherToolHandler: ToolHandler = async (args) => {
    const city = (args as any).city || "Madrid";
    return {
        content: [
            { type: "text", text: `Weather for ${city}: Sunny, 25C.` },
            {
                type: "resource_link",
                uri: `weather://today/${city}`,
                name: `weather today ${city}`,
                mimeType: "application/json",
            },
        ],
    };
};
Now, use the decorator to create a protected version of the handler and register it.
The extra object, containing request headers, is passed automatically by the MCP server to the handler. The paywall uses this to extract the Authorization token.
// server-factory.ts
import { McpServer } from "@modelcontextprotocol/sdk/server";
import { z } from "zod";
import { weatherToolHandler } from "./handlers";

// Assume 'payments' is initialized and configured
const protectedWeatherHandler = payments.mcp.withPaywall(
    weatherToolHandler,
    { 
        kind: "tool",
        name: "weather.today",
        credits: 1n // 1 credit per call (use BigInt)
    } 
);

const server = new McpServer(/* ... */);
server.registerTool(
    "weather.today",
    { inputSchema: z.object({ city: z.string() }) },
    protectedWeatherHandler // Use the wrapped handler
);

4) Dynamic Credit Calculation

For flexible pricing, provide a function to the credits option. It receives a context object (ctx) with request args, the handler’s result, and extra metadata.
const dynamicCreditsHandler = payments.mcp.withPaywall(
    weatherToolHandler,
    {
        kind: "tool",
        name: "weather.today",
        credits: (ctx) => {
            const city = (ctx.args as any).city || "";
            return city.length <= 5 ? 1n : 2n;
        },
    }
);

5) Alternative: Declarative Registration with attach

While withPaywall is useful, it can be repetitive. The attach method provides a more streamlined alternative by combining registration and protection into a single call. It takes your server instance and returns a registrar object with protected registerTool, registerResource, and registerPrompt methods.
// server-factory-with-attach.ts
const server = new McpServer(/* ... */);
const protectedRegistrar = payments.mcp.attach(server);

// Register and protect in one step
protectedRegistrar.registerTool(
    "weather.today",
    resourceTemplate,
    weatherResourceConfig,
    weatherResourceHandler,
    { credits: weatherToolCreditsCalculator }
);

6) Alternative: Custom Low-Level Server

If you prefer full control, you can implement a low-level JSON-RPC router. You are responsible for parsing the request, routing it, and manually passing an extra object (containing request headers) to the protected handler.
This example shows a minimal router using Express.js.
// low-level-server.ts
import express from "express";

const app = express();
app.use(express.json());

app.post("/mcp-low", async (req, res) => {
    const { method, params, id } = req.body;
    if (method === "tools/call" && params.name === "weather.today") {
        try {
            const result = await protectedHandler(params.arguments, {
                requestInfo: { headers: req.headers },
            });
            res.json({ jsonrpc: "2.0", id, result });
        } catch (e: any) {
            res.status(500).json({ error: { code: e.code, message: e.message } });
        }
    }
});

7) Client: Getting Access & Calling

On the client side, the process is to get a Nevermined accessToken and include it in the Authorization header of every MCP request.
// client-sdk.ts
import { Client as McpClient } from "@modelcontextprotocol/sdk/client";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp";

// 1. Get Access Token
const { accessToken } = await payments.agents.getAgentAccessToken(planId, agentId);

// 2. Create Transport with Authorization Header
const transport = new StreamableHTTPClientTransport(
    new URL("http://localhost:3000/mcp"),
    {
        requestInit: {
            headers: { Authorization: `Bearer ${accessToken}` },
        },
    }
);

// 3. Connect and Call Tool
const client = new McpClient({ name: "my-client" });
await client.connect(transport);
const result = await client.callTool({
    name: "weather.today",
    arguments: { city: "London" },
});

Error Handling

  • No token / Invalid token / Insufficient credits → JSON-RPC error -32003
  • Other server errors → JSON-RPC error -32002

Full Code Examples

You can find the complete working examples used in this tutorial in the following GitHub repositories: