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.
@@ -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
+ 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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()