All guides

How to Monetize an MCP Server with License Keys

Sell access to MCP tools with product-scoped license checks, private client configuration and clear offline, authentication and seat boundaries.

To monetize an MCP server, choose which useful operations are paid, associate the customer with an entitlement and check that entitlement before the protected work runs. A local license key suits a distributed tool; a hosted service also needs its own user authentication and authorisation.

This guide covers local Node.js and Python servers and the boundaries of a hosted design. PermitCore supplies license checks and entitlement data. It does not provide a built-in MCP adapter, MCP hosting or an OAuth authorization server. The code below is a tested licensing-policy helper, not a complete MCP server.

Choose where the paid value runs

  • Local server with online checks. Ship the program and check a customer key when a paid operation begins. Plan for service failures and keep free tools usable.
  • Local server with signed offline proof. Verify a previously issued proof against trusted keys and its actual constraints. Offline revocation visibility is bounded by reconnect and token expiry.
  • Hosted server. Authenticate the caller, map their account to its entitlement and authorise each protected operation. You retain control of remote computation, storage and API costs.

A customer controls a local installation and can modify its code. Licensing supports your commercial policy; it is not a guarantee that a local gate cannot be patched. Put especially valuable shared data or costly computation behind an appropriately authenticated service when your product needs that boundary.

Keep secrets in trusted client configuration

MCP_LICENSE_KEY is this guide's application convention, not an MCP-standard field. Supply it to your own local process through private configuration or a trusted launcher. Do not ask the model to paste it into a prompt or pass it as a tool argument: those paths can expose it in conversation history and traces. Your PermitCore administrative API key never belongs in the customer program.

The following Cursor configuration uses documented environment interpolation. Set the secret in the environment available to Cursor, replace the product GUID and point to the actual server entry point you built. Do not install a package named after this placeholder. On Windows, use a real absolute Windows path and escape backslashes in JSON.

{
  "mcpServers": {
    "my-local-server": {
      "type": "stdio",
      "command": "node",
      "args": ["/absolute/path/to/your/server.mjs"],
      "env": {
        "MCP_LICENSE_KEY": "${env:MCP_LICENSE_KEY}",
        "MCP_PRODUCT_ID": "YOUR_PRODUCT_GUID"
      }
    }
  }
}

Cursor supports project and user-level mcp.json files. Keep real keys outside a committed project configuration; a private user-level configuration or secret-aware launcher can keep the value out of source control. Environment variables are accessible to the process and suitably privileged local users. They are not encrypted secret storage.

For Claude Desktop, use Settings → Developer → Edit Config and the official local-server guide. It documents the command/arguments and environment map. Do not assume Cursor's interpolation syntax works in another client. Check paths, launch permissions and the environment available to that GUI application; restart the client after changing its configuration.

Check the paid operation before any side effect

Keep installation, discovery and genuinely free tools available without a paid key. A tool name or description saying “Pro” is a user-interface hint. Enforce the entitlement inside the actual execution handler before exporting files, calling a billable service or modifying data. Paid entitlement does not grant filesystem or network permission; apply your resource-access boundaries to free and paid tools. Hiding a tool in the client is not an access-control boundary.

For the examples, create a product in PermitCore and use its GUID as MCP_PRODUCT_ID. Add the pro_export feature to licenses that should unlock your export tool. An active key for another product must not grant access. Feature helpers inspect feature strings; call them only after a valid, product-matching result.

Install SDK 2.1.1 using the Node or Python guide. Save the corresponding helper in your own server project. Call it from the protected handler and stop before performing the operation when allowed is false. A free handler does not call it.

Node.js policy helper

import { PermitCoreClient } from '@permitcore/permitcore';

export async function checkProLicense() {
  const key = process.env.MCP_LICENSE_KEY?.trim();
  const productId = process.env.MCP_PRODUCT_ID?.trim();
  if (!key || !productId) {
    return { allowed: false, message: 'Configure MCP_LICENSE_KEY and MCP_PRODUCT_ID privately.' };
  }
  try {
    const client = new PermitCoreClient({
      baseUrl: process.env.MCP_API_URL || 'https://api.permitcore.dev',
      enableOfflineCache: false, timeoutMs: 5000, maxRetries: 0,
    });
    const result = await client.validate(key, '1.0.0', productId);
    if (result.isValid !== true || result.productId?.toLowerCase() !== productId.toLowerCase()) {
      return { allowed: false, message: 'Pro access could not be confirmed. Check the key, product and service.' };
    }
    if (!PermitCoreClient.hasFeature(result, 'pro_export')) {
      return { allowed: false, message: 'This license does not include Pro export.' };
    }
    return { allowed: true, message: null };
  } catch {
    return { allowed: false, message: 'Pro access could not be confirmed. Check the configuration and service.' };
  }
}

Python policy helper

import os
from permitcore import PermitCoreClient

def check_pro_license():
    key = os.environ.get('MCP_LICENSE_KEY', '').strip()
    product_id = os.environ.get('MCP_PRODUCT_ID', '').strip()
    if not key or not product_id:
        return {'allowed': False, 'message': 'Configure MCP_LICENSE_KEY and MCP_PRODUCT_ID privately.'}
    try:
        client = PermitCoreClient(
            os.environ.get('MCP_API_URL') or 'https://api.permitcore.dev',
            enable_offline_cache=False, timeout=5, max_retries=0,
        )
        result = client.validate(key, version='1.0.0', expected_product_id=product_id)
        if result.is_valid is not True or (result.product_id or '').lower() != product_id.lower():
            return {'allowed': False, 'message': 'Pro access could not be confirmed. Check the key, product and service.'}
        if not result.has_feature('pro_export'):
            return {'allowed': False, 'message': 'This license does not include Pro export.'}
        return {'allowed': True, 'message': None}
    except Exception:
        return {'allowed': False, 'message': 'Pro access could not be confirmed. Check the configuration and service.'}

The Python SDK call is synchronous. An asynchronous server should run that blocking check through its framework's supported worker-thread mechanism, then inspect the returned decision. Neither helper starts an MCP transport or registers a tool.

These online-only helpers disable offline caching and extra retries and set a five-second per-attempt timeout. MCP_API_URL is trusted process configuration for self-hosting or a local test fixture; production traffic needs HTTPS. Never derive it from a tool argument or model output. The helpers do not print keys, raw provider responses or errors.

Return a useful refusal through your MCP library

The MCP tools specification distinguishes tool execution failures from malformed protocol requests. Adapt a denied business operation to an appropriate tool error, with isError: true and a short, safe explanation. Use the result type expected by your MCP library and negotiated protocol version; the policy helper's allowed object is not a wire response.

Explain the recovery action: configure the key outside the conversation, check its product, or obtain a license that includes the feature. A temporary verification failure should not tell a paying customer that their key is definitely invalid. Do not include the secret, account details or a payment URL constructed from untrusted input in the error.

For a stdio server, stdout carries protocol messages. A licensing banner printed there can break the connection. Diagnostic stderr may also be captured by the client, so redact secrets there too. Let the MCP framework encode protocol results instead of printing ad-hoc JSON or text from the license helper.

Design offline use as a separate policy

A server that is local to the customer may need to work on a train or an isolated workstation. Choose and test the appropriate offline licensing path. A successful online check can provide a signed grace-cache token when configured; verifying a saved unsigned JSON response or caching a true boolean forever is unsafe.

Offline access follows signature, tenant/product, device and time constraints. Previously issued proof cannot learn that a key was revoked while disconnected, and it cannot extend a floating lease. A first run without usable proof needs an honest recovery path. The sample helpers deliberately do not implement offline fallback; add it only through the documented SDK flow and test expiry, tampering and key rotation.

Choose seats and usage limits explicitly

One key copied into several local client configurations does not define a team-seat policy. Use node-locked or floating controls according to what you sell. Explicit device activation is separate from validation. Repeated count-based activation can consume additional seats and monthly pool entries; do not activate on every tool call.

A floating workload checks out a session, maintains its heartbeat while working and checks it in afterwards. Decide whether multiple local client processes should share a managed application session or require separate seats. A license check alone neither reserves a concurrent slot nor enforces your per-user authentication policy.

If you charge by usage, define the billable operation and integrate metering explicitly. MCP messages, model tokens, activation counts and successful exports are different units. PermitCore does not automatically bill every tool call because the program uses MCP.

Hosted access needs authentication as well as a license

The MCP authorization specification describes the HTTP-based authorization model. Authentication establishes who is calling; your commercial policy establishes what their account may use. Map that authenticated account to the correct entitlement instead of using one global customer key for all hosted users.

Do not treat a model-supplied email or license string as proof of identity. Validate tool inputs, authorise access to the actual resource and keep credentials intended for other services out of your MCP access-token path. A PermitCore license key is not an OAuth access token or an administrator login session.

Sell the entitlement and test the integration

A PermitCore Store can use the vendor's selected Stripe or Paddle account and deliver a key after verified payment. Configure the issued product, features and seat policy, then test a purchase and the protected operation together. PermitCore takes 0% sales commission; provider fees and the provider's own agreement apply. See the payment lifecycle guide for renewals, grace and refunds.

The exact Node and Python helpers are executed against a local HTTP fixture with the real SDK 2.1.1 code. That checks key/product/feature decisions and failure handling. It is not an end-to-end Claude or Cursor client test, a hosted paid purchase, device activation or floating-lease acceptance.

Before release, exercise the real client and transport: free tools with no key; Pro with the right feature; another product; an expired or revoked key; service failure; malformed responses; and no protected side effect after denial. Verify that license material never reaches protocol output, logs or the model. If adding offline, device, floating or metered behavior, test each separately. The CLI guide covers the surrounding package and headless-runtime decisions.

Put the guide into practice

Build your first licensed product without building a license server.

Create a free PermitCore account, issue a test license, and validate it from your application.