Key takeaways
- Install
mcp-marketplace-licenseand add a few lines to gate your entire server or individual tools - The SDK reads
MCP_LICENSE_KEYfrom environment and verifies against the API - On a remote server, pass the key from each request's
Authorizationheader 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:
- Purchase. Buyer pays on MCP Marketplace.
- Key issued. Marketplace generates a unique license key starting with
mcp_live_. - Key stored. For a local server, the buyer sets
MCP_LICENSE_KEYas an environment variable in their MCP config. For a remote server, the buyer's client sends the key as anAuthorization: Bearerheader. - 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
| Parameter | Type | Default | Description |
|---|---|---|---|
slug | str | required | Your server's slug on MCP Marketplace |
env_var | str | "MCP_LICENSE_KEY" | Name of the environment variable the key is read from |
verify_url | str | None | None | Verification endpoint. None uses the MCP Marketplace endpoint |
cache_ttl | int | 86400 | How long a valid result is cached, in seconds |
key | str | None | not passed | Keyword-only, 1.2.0+. The key to verify, for remote servers. When passed, the environment is not read |
negative_cache_ttl | float | 60 | Keyword-only, 1.2.0+. With key: how long a rejected key is remembered, in seconds |
max_cache_entries | int | 10000 | Keyword-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"]:Trueif the key is active,Falseotherwiseresult["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_ttlto 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 readsMCP_LICENSE_KEYfrom 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:
| Variable | Description |
|---|---|
MCP_LICENSE_KEY | The 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/licensefor TypeScript,mcp-marketplace-licensefor Python) - License verification wired into tool handlers
MCP_LICENSE_KEYin.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
- How to monetize your MCP server: the full guide to pricing models
- Free vs Pro: structuring your freemium tiers
- Remote MCP servers: hosting and authentication for remote servers
- MCP Creator: scaffold paid servers with AI in TypeScript or Python
- How to build an MCP server: the full build guide before adding licensing