Charging for an MCP Server: Check the License Before the Paid Tool Runs

2026-11-03来源 dev.toEnglish

Originally published on the PermitCore blog.

If you ship an MCP server people actually use, sooner or later someone asks "is there a paid version?". The pattern is simple: decide which operations are paid, tie the customer to 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.

This guide covers local Node.js and Python servers, plus the boundaries of a hosted design. PermitCore supplies the license check and the entitlement data; you bring your MCP server library and transport. The code below is a licensing-policy helper that you call from your own tool handler, 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. Keep the free tools usable even if the license service is unreachable.
  • Local server with signed offline proof. Verify a previously issued proof against trusted keys and its actual constraints. Revocation visibility while offline is bounded by reconnect time and token expiry.
  • Hosted server. Authenticate the caller, map their account to its entitlement and authorise each protected operation. You also keep control of remote computation, storage and API costs.

A customer controls a local installation and can modify its code, so a local gate supports your commercial policy rather than guaranteeing anything. Put especially valuable shared data or costly computation behind an authenticated service.

Keep the key out of the conversation

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

Cursor supports environment interpolation in mcp.json, so the secret can stay in your environment:

{ "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" } } } } 

Replace the product GUID and point args at your real server entry point. Keep real keys out of a committed project config. For Claude Desktop, use Settings → Developer → Edit Config and the official local-server guide; don't assume Cursor's interpolation syntax works in another client.

Check the paid operation before any side effect

Keep installation, discovery and genuinely free tools available without a key. A tool description saying "Pro" is only a UI hint: enforce the entitlement inside the execution handler, before exporting files, calling a billable service or changing data. Hiding a tool in the client is not access control.

For the examples, create a product in PermitCore, use its GUID as MCP_PRODUCT_ID, and add a pro_export feature to the licenses that should unlock your export tool. A valid key for a different product must not grant access, so the helpers compare the product id as well as the feature.

Node.js

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

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.'} 

Call the helper from the protected handler and stop before doing the work when allowed is false. Free handlers never call it. These online-only helpers disable offline caching and retries and use a five-second timeout per attempt. The Python SDK call is synchronous, so an async server should run it on a worker thread. MCP_API_URL is trusted process configuration (self-hosting or a local test fixture); never derive it from a tool argument or model output.

Return a useful refusal

The MCP tools specification separates tool execution failures from malformed protocol requests. For a denied business operation, return a tool error (isError: true) with a short, safe explanation, using the result type your MCP library expects.

Say what to do next: configure the key outside the conversation, check that it belongs to this product, or get a license that includes the feature. A temporary verification failure shouldn't tell a paying customer their key is definitely invalid, and the message must never contain the key or account details.

One stdio detail: stdout carries protocol messages, so a licensing banner printed there can break the connection. Keep diagnostics on stderr and redact secrets there too.

Design offline use as its own policy

A local server may need to work on a train or an isolated workstation. Use the signed offline path from the offline licensing docs: a successful online check can provide a signed grace-cache token when configured. Verifying a saved unsigned JSON response, or caching a true forever, is not safe. Test expiry, tampering and key rotation separately.

Seats and usage

One key copied into several client configs doesn't define a team-seat policy. Pick node-locked or floating controls for what you sell, and don't activate on every tool call: activation is separate from validation. If you charge by usage, define the billable unit (messages, tokens, successful exports are all different) and use metering explicitly.

Hosted access needs authentication too

For an HTTP-based server, follow the MCP authorization specification. Authentication says who is calling; your commercial policy says what their account may use, so map the authenticated account to its entitlement instead of sharing one global key. Don't treat a model-supplied email or license string as proof of identity.

Sell it and test the whole path

A PermitCore Store can use your own Stripe or Paddle account and deliver a key after verified payment, with 0% PermitCore sales commission (provider fees apply). Configure the product, features and seat policy, then test a purchase together with the protected tool.

Before release, run through the real client and transport: free tools with no key; Pro with the right feature; a key for another product; an expired or revoked key; a service failure; and confirm no side effect happens after a denial and that license material never reaches protocol output, logs or the model.

Full original with all the links: permitcore.dev/blog/monetize-mcp-server. Docs: permitcore.dev/docs/getting-started.

If you're building MCP servers, how are you planning to charge for them: a license key in the client config, a hosted account, or something else? I'd love to hear what's working. 🙂

原文:https://www.a8q9.cn/en/article/charging-for-an-mcp-server-check-the-license-before-the-paid-tool-runs-363148.html

相关文章