guidesUpdated February 26, 2026

License keys for MCP servers: how they work

How to add license key verification to your paid MCP server using the mcp-marketplace-license SDK. Install, integrate, and test in minutes.

Key takeaways

  • Install mcp-marketplace-license and add a few lines to gate your entire server or individual tools
  • The SDK reads MCP_LICENSE_KEY from environment and verifies against the API
  • On a remote server, pass the key from each request's Authorization header instead (SDK 1.2.0 or later)
  • Mock the SDK in automated tests to avoid network calls

How licensing works on MCP Marketplace

When someone buys a paid MCP server on the marketplace, they receive a license key. That key unlocks the server's premium tools. The mcp-marketplace-license SDK handles all the verification: you add a few lines of code and the rest is automatic.

Here is the lifecycle:

  1. Purchase. Buyer pays on MCP Marketplace.
  2. Key issued. Marketplace generates a unique license key starting with mcp_live_.
  3. Key stored. For a local server, the buyer sets MCP_LICENSE_KEY as an environment variable in their MCP config. For a remote server, the buyer's client sends the key as an Authorization: Bearer header.
  4. Verification. Your server uses the SDK to verify the key: on startup or per tool call for a local server, on every request for a remote one.

Installing the SDK

pip install mcp-marketplace-license

Works with any Python 3.10+ project.

Basic usage

Gate the entire server

The simplest approach: if the key is invalid, the server does not start:

import sys

from mcp.server.mcpserver import MCPServer
from mcp_marketplace_license import verify_license

mcp = MCPServer("my-server")

@mcp.tool()
def my_tool(query: str) -> str:
    return do_thing(query)

if __name__ == "__main__":
    result = verify_license("my-server")
    if not result.get("valid"):
        print(f"License check failed: {result.get('reason')}", file=sys.stderr)
        sys.exit(1)
    mcp.run()

If the key is missing or invalid, the server exits before it starts, and result["reason"] says why (for example missing_key or expired).

The SDK also exports with_license(mcp, slug="my-server"), which does the same check in one line: call it before mcp.run(). From SDK version 1.2.0 it works with MCPServer (called FastMCP in version 1 of the Python SDK) and with the low-level Server class. In 1.1.0 it only worked with the low-level Server: on MCPServer the wrapped server never started.

Gate individual tools (freemium)

For servers with both free and pro tools:

from mcp_marketplace_license import verify_license
import json

def require_license(tool_name: str) -> str | None:
    """Returns None if licensed, or an error message if not."""
    result = verify_license(slug="my-server")
    if result.get("valid"):
        return None
    return json.dumps({
        "error": "premium_required",
        "message": f"'{tool_name}' requires a license key. "
                   "Set MCP_LICENSE_KEY in your environment.",
    })

@mcp.tool()
def free_tool(query: str) -> str:
    # No license check: available to everyone
    return do_free_thing(query)

@mcp.tool()
def pro_tool(query: str) -> str:
    err = require_license("pro_tool")
    if err:
        return err
    return do_pro_thing(query)

Free tools work immediately. Pro tools return a helpful error if no valid key is present.

The verify_license function

from mcp_marketplace_license import verify_license

result = verify_license(slug="my-server")

Parameters

ParameterTypeDefaultDescription
slugstrrequiredYour server's slug on MCP Marketplace
env_varstr"MCP_LICENSE_KEY"Name of the environment variable the key is read from
verify_urlstr | NoneNoneVerification endpoint. None uses the MCP Marketplace endpoint
cache_ttlint86400How long a valid result is cached, in seconds
keystr | Nonenot passedKeyword-only, 1.2.0+. The key to verify, for remote servers. When passed, the environment is not read
negative_cache_ttlfloat60Keyword-only, 1.2.0+. With key: how long a rejected key is remembered, in seconds
max_cache_entriesint10000Keyword-only, 1.2.0+. With key: the most valid keys, and separately the most rejected keys, kept in memory

Leave key out on a local server: the key is read from the environment. Pass key on a remote server: that key is verified and the environment is never read. An empty key (None or "") is rejected with the reason missing_key.

Return value

The function returns a dictionary. The two fields you need:

  • result["valid"]: True if the key is active, False otherwise
  • result["reason"]: why it failed (e.g., "expired", "missing_key")

Check result["valid"] to decide whether to allow access.

Remote servers

A remote (hosted) server has no buyer key in its environment. Each buyer's client sends their key on every request as an Authorization: Bearer mcp_live_... header, and your server checks the key that arrived. This needs SDK version 1.2.0 or later.

import asyncio
import json

from mcp.server.mcpserver import MCPServer
from mcp_marketplace_license import license_key_from_header, verify_license

mcp = MCPServer("my-server")

# ... register your tools ...

mcp_app = mcp.streamable_http_app(stateless_http=True)


async def app(scope, receive, send):
    """Check the buyer's license key on every request, then hand over to MCP."""
    if scope["type"] == "http":
        # The buyer's client sends "Authorization: Bearer mcp_live_..." on every request.
        header = dict(scope["headers"]).get(b"authorization")
        key = license_key_from_header(header)
        # verify_license blocks while it asks the license server: keep it off the event loop.
        license = await asyncio.to_thread(verify_license, slug="my-server", key=key)
        if not license.get("valid"):
            body = json.dumps({"error": "license_required", "reason": license.get("reason")}).encode()
            await send({"type": "http.response.start", "status": 401, "headers": [(b"content-type", b"application/json")]})
            await send({"type": "http.response.body", "body": body})
            return
    await mcp_app(scope, receive, send)


# Run with:  uvicorn my_server:app --port 3000

license_key_from_header returns the key from a Bearer header, or None for a missing header or any other scheme. verify_license(slug=..., key=...) then checks that key:

  • The first request with a key asks the license server. After that the result is kept in your server's memory: a valid key for 24 hours, a rejected key for 60 seconds.
  • Results are stored per key. One buyer's valid result is never used for another buyer's key.
  • If the license server cannot be reached, buyers whose key was verified earlier keep working. A one-time purchase keeps working this way for as long as the outage lasts; a subscription key for at most 48 hours after its last successful check. A key that has never been verified is refused, with the reason network_error, until the license server answers.
  • A revoked or refunded key keeps working until its cached result expires, 24 hours by default. Lower cache_ttl to shorten that.
  • Rate-limit requests that carry no valid key on your own server. Every check you forward to the license server counts against your server's address, and too many wrong keys from one address are refused for a while.
  • Always pass key, even when the request has none. Without the argument, the SDK reads MCP_LICENSE_KEY from your own server's environment instead of checking the request.

The TypeScript package has the same pair: verifyLicense({ slug, key }) and licenseKeyFromHeader().

Environment variables

On a local server, the SDK reads one environment variable:

VariableDescription
MCP_LICENSE_KEYThe buyer's license key (starts with mcp_live_)

Users set this in their MCP config:

{
  "mcpServers": {
    "your-server": {
      "command": "your-server",
      "env": {
        "MCP_LICENSE_KEY": "mcp_live_..."
      }
    }
  }
}

Supporting legacy variable names

If your server used a different variable name before adopting the SDK, you can support both:

import os

# Support legacy MYSERVER_LICENSE_KEY alongside the standard MCP_LICENSE_KEY
if not os.environ.get("MCP_LICENSE_KEY") and os.environ.get("MYSERVER_LICENSE_KEY"):
    os.environ["MCP_LICENSE_KEY"] = os.environ["MYSERVER_LICENSE_KEY"]

Using with MCP Creator

If you scaffold your server with MCP Creator and paid=true, all of this is wired up automatically. Available for both TypeScript (mcp-creator-typescript) and Python (mcp-creator-python):

scaffold_server(
    package_name="my-paid-mcp",
    tools=["free_tool", "pro_tool"],
    paid=true,
    paid_tools=["pro_tool"]
)

The generated project includes:

  • License SDK in dependencies (@mcp_marketplace/license for TypeScript, mcp-marketplace-license for Python)
  • License verification wired into tool handlers
  • MCP_LICENSE_KEY in .env.example
  • License setup instructions in README.md

No manual SDK integration needed.

Testing license verification

During development, you need to test both licensed and unlicensed states.

Without a real key

Set MCP_LICENSE_KEY to any value and the SDK will verify it against the API, returning invalid. This lets you confirm your gating logic works correctly.

With a test key

Use your own purchased key to test the full happy path. Set MCP_LICENSE_KEY in your environment and run your server locally.

In automated tests

Mock the SDK to avoid network calls in tests:

from unittest.mock import patch

@patch("mcp_marketplace_license.verify_license")
def test_pro_tool_with_valid_key(mock_verify):
    mock_verify.return_value = {"valid": True, "reason": "active"}
    result = pro_tool("test query")
    assert "premium_required" not in result

@patch("mcp_marketplace_license.verify_license")
def test_pro_tool_without_key(mock_verify):
    mock_verify.return_value = {"valid": False, "reason": "missing_key"}
    result = pro_tool("test query")
    assert "premium_required" in result

Security notes

Keys are API tokens, not secrets. License keys are set in environment variables in MCP config files. They identify a buyer and grant access, similar to an API key.

Verification is server-side. The SDK calls the MCP Marketplace verification API. There is no client-side validation to bypass.

The local cache is not a lock. On a local server the SDK keeps a valid result in a file for 24 hours. That cache saves a network call for an honest local user. A paid tool installed on someone's own machine cannot be protected against that machine's owner.

Do not hardcode keys. Keys should come from the buyer: an environment variable on a local server, the Authorization header on a remote one. Never from source code. The SDK reads MCP_LICENSE_KEY from the environment unless you pass key.

Next steps

Browse MCP servers

Find the servers mentioned in this post and thousands more on MCP Marketplace. Security-checked, one-click install.

Browse servers

Keep reading