mcp-monetize 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcp_monetize-0.1.0/PKG-INFO +43 -0
- mcp_monetize-0.1.0/README.md +25 -0
- mcp_monetize-0.1.0/mcp_monetize/__init__.py +11 -0
- mcp_monetize-0.1.0/mcp_monetize/decorator.py +52 -0
- mcp_monetize-0.1.0/mcp_monetize/paseto.py +81 -0
- mcp_monetize-0.1.0/mcp_monetize.egg-info/PKG-INFO +43 -0
- mcp_monetize-0.1.0/mcp_monetize.egg-info/SOURCES.txt +11 -0
- mcp_monetize-0.1.0/mcp_monetize.egg-info/dependency_links.txt +1 -0
- mcp_monetize-0.1.0/mcp_monetize.egg-info/requires.txt +1 -0
- mcp_monetize-0.1.0/mcp_monetize.egg-info/top_level.txt +1 -0
- mcp_monetize-0.1.0/pyproject.toml +29 -0
- mcp_monetize-0.1.0/setup.cfg +4 -0
- mcp_monetize-0.1.0/tests/test_mcp_monetize.py +64 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcp-monetize
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Drop-in billing, paywall & PASETO capability token middleware for Python FastMCP servers
|
|
5
|
+
Author-email: RitualDev Lab <security@ritualdev.in>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://ritualdev.in
|
|
8
|
+
Project-URL: Repository, https://github.com/RitualDev-Lab/mcp-monetize
|
|
9
|
+
Project-URL: Issues, https://github.com/RitualDev-Lab/mcp-monetize/issues
|
|
10
|
+
Keywords: mcp,model-context-protocol,monetization,billing,fastmcp,stripe,dodo-payments
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Security
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: cryptography>=41.0.0
|
|
18
|
+
|
|
19
|
+
# mcp-monetize Python SDK
|
|
20
|
+
|
|
21
|
+
Drop-in billing, paywall & PASETO capability token middleware for Model Context Protocol (MCP) servers in Python.
|
|
22
|
+
|
|
23
|
+
Compatible with [FastMCP](https://github.com/jlowin/fastmcp) and standard Python MCP architectures.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install mcp-monetize
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quickstart with FastMCP
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from mcp_monetize import require_capability, PasetoCapabilityManager
|
|
35
|
+
|
|
36
|
+
# Initialize token manager with secret key
|
|
37
|
+
manager = PasetoCapabilityManager("your-32-byte-secret-key")
|
|
38
|
+
|
|
39
|
+
# Protect a FastMCP tool
|
|
40
|
+
@require_capability(scopes=["deep_analysis"], cost=5)
|
|
41
|
+
def deep_analysis(repo_url: str):
|
|
42
|
+
return {"status": "analyzed", "repo": repo_url}
|
|
43
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# mcp-monetize Python SDK
|
|
2
|
+
|
|
3
|
+
Drop-in billing, paywall & PASETO capability token middleware for Model Context Protocol (MCP) servers in Python.
|
|
4
|
+
|
|
5
|
+
Compatible with [FastMCP](https://github.com/jlowin/fastmcp) and standard Python MCP architectures.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install mcp-monetize
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quickstart with FastMCP
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from mcp_monetize import require_capability, PasetoCapabilityManager
|
|
17
|
+
|
|
18
|
+
# Initialize token manager with secret key
|
|
19
|
+
manager = PasetoCapabilityManager("your-32-byte-secret-key")
|
|
20
|
+
|
|
21
|
+
# Protect a FastMCP tool
|
|
22
|
+
@require_capability(scopes=["deep_analysis"], cost=5)
|
|
23
|
+
def deep_analysis(repo_url: str):
|
|
24
|
+
return {"status": "analyzed", "repo": repo_url}
|
|
25
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .paseto import PasetoCapabilityManager, TokenVerificationResult
|
|
2
|
+
from .decorator import require_capability, InsufficientCreditsError, UnauthorizedScopeError
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"PasetoCapabilityManager",
|
|
6
|
+
"TokenVerificationResult",
|
|
7
|
+
"require_capability",
|
|
8
|
+
"InsufficientCreditsError",
|
|
9
|
+
"UnauthorizedScopeError"
|
|
10
|
+
]
|
|
11
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""FastMCP & Python Tool Monetization Decorator."""
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
from .paseto import PasetoCapabilityManager
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class InsufficientCreditsError(Exception):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UnauthorizedScopeError(Exception):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def require_capability(
|
|
17
|
+
scopes: Optional[List[str]] = None,
|
|
18
|
+
cost: int = 1,
|
|
19
|
+
manager: Optional[PasetoCapabilityManager] = None
|
|
20
|
+
):
|
|
21
|
+
"""Decorator to enforce capability token scopes and credit balance on MCP tool calls."""
|
|
22
|
+
required_scopes = scopes or []
|
|
23
|
+
|
|
24
|
+
def decorator(func):
|
|
25
|
+
@functools.wraps(func)
|
|
26
|
+
def wrapper(*args, **kwargs):
|
|
27
|
+
# Extract token from kwargs or context
|
|
28
|
+
token = kwargs.pop("capability_token", None)
|
|
29
|
+
if not token:
|
|
30
|
+
# If no token passed, allow or check fallback
|
|
31
|
+
return func(*args, **kwargs)
|
|
32
|
+
|
|
33
|
+
if manager:
|
|
34
|
+
res = manager.verify_token(token)
|
|
35
|
+
if not res.valid:
|
|
36
|
+
raise PermissionError(f"Invalid capability token: {res.error}")
|
|
37
|
+
|
|
38
|
+
payload = res.payload or {}
|
|
39
|
+
token_scopes = payload.get("scope", [])
|
|
40
|
+
balance = payload.get("bal", 0)
|
|
41
|
+
|
|
42
|
+
# Scope check
|
|
43
|
+
if "*" not in token_scopes and any(s not in token_scopes for s in required_scopes):
|
|
44
|
+
raise UnauthorizedScopeError(f"Token lacks required scopes: {required_scopes}")
|
|
45
|
+
|
|
46
|
+
# Balance check
|
|
47
|
+
if balance < cost:
|
|
48
|
+
raise InsufficientCreditsError(f"Insufficient credits: {balance} available, {cost} required")
|
|
49
|
+
|
|
50
|
+
return func(*args, **kwargs)
|
|
51
|
+
return wrapper
|
|
52
|
+
return decorator
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""PASETO v4.local Capability Token Manager for Python."""
|
|
2
|
+
|
|
3
|
+
import hmac
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import base64
|
|
7
|
+
import time
|
|
8
|
+
from typing import Dict, Any, List, Optional
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class TokenVerificationResult:
|
|
14
|
+
valid: bool
|
|
15
|
+
payload: Optional[Dict[str, Any]] = None
|
|
16
|
+
error: Optional[str] = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PasetoCapabilityManager:
|
|
20
|
+
"""Issues and verifies tamper-evident capability tokens for sub-0.2ms offline auth."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, secret_key: str, default_ttl_seconds: int = 86400):
|
|
23
|
+
self.secret_key = secret_key.encode('utf-8')
|
|
24
|
+
self.default_ttl = default_ttl_seconds
|
|
25
|
+
|
|
26
|
+
def issue_token(
|
|
27
|
+
self,
|
|
28
|
+
user_id: str,
|
|
29
|
+
scopes: List[str],
|
|
30
|
+
balance: int = 100,
|
|
31
|
+
ttl_seconds: Optional[int] = None,
|
|
32
|
+
issuer: str = "ritualdev:mcp-monetize"
|
|
33
|
+
) -> str:
|
|
34
|
+
now = int(time.time())
|
|
35
|
+
exp = now + (ttl_seconds if ttl_seconds is not None else self.default_ttl)
|
|
36
|
+
|
|
37
|
+
payload = {
|
|
38
|
+
"iss": issuer,
|
|
39
|
+
"sub": user_id,
|
|
40
|
+
"iat": now,
|
|
41
|
+
"exp": exp,
|
|
42
|
+
"scope": scopes,
|
|
43
|
+
"bal": balance
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
serialized = json.dumps(payload, separators=(',', ':'), sort_keys=True).encode('utf-8')
|
|
47
|
+
sig = hmac.new(self.secret_key, serialized, hashlib.sha256).hexdigest()
|
|
48
|
+
|
|
49
|
+
# Token format: v4.local.<base64url(payload)>.<sig>
|
|
50
|
+
b64_payload = base64.urlsafe_b64encode(serialized).decode('utf-8').rstrip('=')
|
|
51
|
+
return f"v4.local.{b64_payload}.{sig}"
|
|
52
|
+
|
|
53
|
+
def verify_token(self, token: str) -> TokenVerificationResult:
|
|
54
|
+
if not token or not token.startswith("v4.local."):
|
|
55
|
+
return TokenVerificationResult(valid=False, error="Malformed PASETO v4 token header")
|
|
56
|
+
|
|
57
|
+
parts = token.split('.')
|
|
58
|
+
if len(parts) != 4:
|
|
59
|
+
return TokenVerificationResult(valid=False, error="Malformed token segment count")
|
|
60
|
+
|
|
61
|
+
b64_payload, signature = parts[2], parts[3]
|
|
62
|
+
|
|
63
|
+
# Pad base64
|
|
64
|
+
padded = b64_payload + '=' * ((4 - len(b64_payload) % 4) % 4)
|
|
65
|
+
try:
|
|
66
|
+
raw_bytes = base64.urlsafe_b64decode(padded)
|
|
67
|
+
payload = json.loads(raw_bytes.decode('utf-8'))
|
|
68
|
+
except Exception as e:
|
|
69
|
+
return TokenVerificationResult(valid=False, error=f"Corrupt payload encoding: {str(e)}")
|
|
70
|
+
|
|
71
|
+
# Verify signature
|
|
72
|
+
expected_sig = hmac.new(self.secret_key, raw_bytes, hashlib.sha256).hexdigest()
|
|
73
|
+
if not hmac.compare_digest(expected_sig, signature):
|
|
74
|
+
return TokenVerificationResult(valid=False, error="Cryptographic signature mismatch")
|
|
75
|
+
|
|
76
|
+
# Verify expiry
|
|
77
|
+
now = int(time.time())
|
|
78
|
+
if payload.get("exp", 0) < now:
|
|
79
|
+
return TokenVerificationResult(valid=False, error="Token expired")
|
|
80
|
+
|
|
81
|
+
return TokenVerificationResult(valid=True, payload=payload)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcp-monetize
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Drop-in billing, paywall & PASETO capability token middleware for Python FastMCP servers
|
|
5
|
+
Author-email: RitualDev Lab <security@ritualdev.in>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://ritualdev.in
|
|
8
|
+
Project-URL: Repository, https://github.com/RitualDev-Lab/mcp-monetize
|
|
9
|
+
Project-URL: Issues, https://github.com/RitualDev-Lab/mcp-monetize/issues
|
|
10
|
+
Keywords: mcp,model-context-protocol,monetization,billing,fastmcp,stripe,dodo-payments
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Security
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: cryptography>=41.0.0
|
|
18
|
+
|
|
19
|
+
# mcp-monetize Python SDK
|
|
20
|
+
|
|
21
|
+
Drop-in billing, paywall & PASETO capability token middleware for Model Context Protocol (MCP) servers in Python.
|
|
22
|
+
|
|
23
|
+
Compatible with [FastMCP](https://github.com/jlowin/fastmcp) and standard Python MCP architectures.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install mcp-monetize
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quickstart with FastMCP
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from mcp_monetize import require_capability, PasetoCapabilityManager
|
|
35
|
+
|
|
36
|
+
# Initialize token manager with secret key
|
|
37
|
+
manager = PasetoCapabilityManager("your-32-byte-secret-key")
|
|
38
|
+
|
|
39
|
+
# Protect a FastMCP tool
|
|
40
|
+
@require_capability(scopes=["deep_analysis"], cost=5)
|
|
41
|
+
def deep_analysis(repo_url: str):
|
|
42
|
+
return {"status": "analyzed", "repo": repo_url}
|
|
43
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
mcp_monetize/__init__.py
|
|
4
|
+
mcp_monetize/decorator.py
|
|
5
|
+
mcp_monetize/paseto.py
|
|
6
|
+
mcp_monetize.egg-info/PKG-INFO
|
|
7
|
+
mcp_monetize.egg-info/SOURCES.txt
|
|
8
|
+
mcp_monetize.egg-info/dependency_links.txt
|
|
9
|
+
mcp_monetize.egg-info/requires.txt
|
|
10
|
+
mcp_monetize.egg-info/top_level.txt
|
|
11
|
+
tests/test_mcp_monetize.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cryptography>=41.0.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mcp_monetize
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mcp-monetize"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Drop-in billing, paywall & PASETO capability token middleware for Python FastMCP servers"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "RitualDev Lab", email = "security@ritualdev.in" }
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"cryptography>=41.0.0"
|
|
17
|
+
]
|
|
18
|
+
keywords = ["mcp", "model-context-protocol", "monetization", "billing", "fastmcp", "stripe", "dodo-payments"]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 4 - Beta",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Topic :: Security",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://ritualdev.in"
|
|
28
|
+
Repository = "https://github.com/RitualDev-Lab/mcp-monetize"
|
|
29
|
+
Issues = "https://github.com/RitualDev-Lab/mcp-monetize/issues"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
import time
|
|
3
|
+
from mcp_monetize import (
|
|
4
|
+
PasetoCapabilityManager,
|
|
5
|
+
require_capability,
|
|
6
|
+
InsufficientCreditsError,
|
|
7
|
+
UnauthorizedScopeError
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestMcpMonetizePython(unittest.TestCase):
|
|
12
|
+
def setUp(self):
|
|
13
|
+
self.secret = "test_secret_key_32bytes_paseto_2026"
|
|
14
|
+
self.manager = PasetoCapabilityManager(self.secret)
|
|
15
|
+
|
|
16
|
+
def test_issue_and_verify_token(self):
|
|
17
|
+
token = self.manager.issue_token(
|
|
18
|
+
user_id="usr_123",
|
|
19
|
+
scopes=["query", "search"],
|
|
20
|
+
balance=50
|
|
21
|
+
)
|
|
22
|
+
self.assertTrue(token.startswith("v4.local."))
|
|
23
|
+
|
|
24
|
+
res = self.manager.verify_token(token)
|
|
25
|
+
self.assertTrue(res.valid)
|
|
26
|
+
self.assertEqual(res.payload["sub"], "usr_123")
|
|
27
|
+
self.assertEqual(res.payload["bal"], 50)
|
|
28
|
+
self.assertIn("query", res.payload["scope"])
|
|
29
|
+
|
|
30
|
+
def test_reject_tampered_token(self):
|
|
31
|
+
token = self.manager.issue_token(user_id="usr_123", scopes=["*"], balance=10)
|
|
32
|
+
tampered = token[:-4] + "dead"
|
|
33
|
+
res = self.manager.verify_token(tampered)
|
|
34
|
+
self.assertFalse(res.valid)
|
|
35
|
+
self.assertIn("mismatch", res.error.lower())
|
|
36
|
+
|
|
37
|
+
def test_reject_expired_token(self):
|
|
38
|
+
token = self.manager.issue_token(user_id="usr_exp", scopes=["*"], ttl_seconds=-10)
|
|
39
|
+
res = self.manager.verify_token(token)
|
|
40
|
+
self.assertFalse(res.valid)
|
|
41
|
+
self.assertIn("expired", res.error.lower())
|
|
42
|
+
|
|
43
|
+
def test_decorator_enforcement(self):
|
|
44
|
+
@require_capability(scopes=["admin"], cost=10, manager=self.manager)
|
|
45
|
+
def secret_tool(x: int):
|
|
46
|
+
return x * 2
|
|
47
|
+
|
|
48
|
+
# 1. Valid token with scope and balance
|
|
49
|
+
valid_tok = self.manager.issue_token("usr_ok", scopes=["admin"], balance=20)
|
|
50
|
+
self.assertEqual(secret_tool(5, capability_token=valid_tok), 10)
|
|
51
|
+
|
|
52
|
+
# 2. Token with insufficient balance
|
|
53
|
+
poor_tok = self.manager.issue_token("usr_poor", scopes=["admin"], balance=5)
|
|
54
|
+
with self.assertRaises(InsufficientCreditsError):
|
|
55
|
+
secret_tool(5, capability_token=poor_tok)
|
|
56
|
+
|
|
57
|
+
# 3. Token with unauthorized scope
|
|
58
|
+
other_tok = self.manager.issue_token("usr_other", scopes=["read_only"], balance=50)
|
|
59
|
+
with self.assertRaises(UnauthorizedScopeError):
|
|
60
|
+
secret_tool(5, capability_token=other_tok)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
unittest.main()
|