mcp-simplr 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_simplr-0.1.0/PKG-INFO +176 -0
- mcp_simplr-0.1.0/README.md +161 -0
- mcp_simplr-0.1.0/mcp_simplr/__init__.py +50 -0
- mcp_simplr-0.1.0/mcp_simplr/api/__init__.py +0 -0
- mcp_simplr-0.1.0/mcp_simplr/api/client.py +55 -0
- mcp_simplr-0.1.0/mcp_simplr/auth/__init__.py +4 -0
- mcp_simplr-0.1.0/mcp_simplr/auth/client.py +84 -0
- mcp_simplr-0.1.0/mcp_simplr/billing/__init__.py +0 -0
- mcp_simplr-0.1.0/mcp_simplr/billing/fixed.py +32 -0
- mcp_simplr-0.1.0/mcp_simplr/billing/recurring.py +70 -0
- mcp_simplr-0.1.0/mcp_simplr/billing/token.py +34 -0
- mcp_simplr-0.1.0/mcp_simplr/config.py +107 -0
- mcp_simplr-0.1.0/mcp_simplr/email/__init__.py +4 -0
- mcp_simplr-0.1.0/mcp_simplr/email/client.py +57 -0
- mcp_simplr-0.1.0/mcp_simplr/exceptions.py +38 -0
- mcp_simplr-0.1.0/mcp_simplr/middleware/__init__.py +0 -0
- mcp_simplr-0.1.0/mcp_simplr/middleware/tracker.py +97 -0
- mcp_simplr-0.1.0/mcp_simplr/payments/__init__.py +3 -0
- mcp_simplr-0.1.0/mcp_simplr/payments/client.py +169 -0
- mcp_simplr-0.1.0/mcp_simplr.egg-info/PKG-INFO +176 -0
- mcp_simplr-0.1.0/mcp_simplr.egg-info/SOURCES.txt +24 -0
- mcp_simplr-0.1.0/mcp_simplr.egg-info/dependency_links.txt +1 -0
- mcp_simplr-0.1.0/mcp_simplr.egg-info/requires.txt +7 -0
- mcp_simplr-0.1.0/mcp_simplr.egg-info/top_level.txt +1 -0
- mcp_simplr-0.1.0/pyproject.toml +29 -0
- mcp_simplr-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcp-simplr
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Simplr client library for MCP services
|
|
5
|
+
Author-email: contact@mcp-simplr.au
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: httpx>=0.27
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
12
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
13
|
+
Requires-Dist: pytest-mock>=3.12; extra == "dev"
|
|
14
|
+
Requires-Dist: respx>=0.21; extra == "dev"
|
|
15
|
+
|
|
16
|
+
# mcp-simplr
|
|
17
|
+
|
|
18
|
+
Python client library for [MCP Simplr](https://mcp-simplr.au) — payments, emailing, and auth for MCP services.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install mcp-simplr
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Payments
|
|
27
|
+
|
|
28
|
+
Customers register once on the platform and receive a `customer_token`. MCP owners accept this token from their users and pass it to `charge()` — no customer registration code needed on your end.
|
|
29
|
+
|
|
30
|
+
### Token billing — charge per output token
|
|
31
|
+
|
|
32
|
+
Best for AI tools where cost scales with usage.
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from mcp_simplr import MCPPayments, PaymentConfig, PaymentModel
|
|
36
|
+
|
|
37
|
+
payments = MCPPayments(PaymentConfig(
|
|
38
|
+
api_key="simplr_owner_...",
|
|
39
|
+
payment_model=PaymentModel.TOKEN,
|
|
40
|
+
price_per_token=0.000_5, # AUD per output token
|
|
41
|
+
currency="aud",
|
|
42
|
+
environment="production",
|
|
43
|
+
))
|
|
44
|
+
|
|
45
|
+
# Charge explicitly
|
|
46
|
+
await payments.charge(customer_token, tokens=500)
|
|
47
|
+
|
|
48
|
+
# Or use the decorator — counts tokens and charges automatically
|
|
49
|
+
@payments.track_usage(customer_id="<customer-token>")
|
|
50
|
+
async def my_tool(query: str) -> str:
|
|
51
|
+
return await run_model(query)
|
|
52
|
+
|
|
53
|
+
# Dynamic customer token from request payload
|
|
54
|
+
@payments.track_usage(customer_id_key="customer_token")
|
|
55
|
+
async def my_tool(query: str, customer_token: str) -> str:
|
|
56
|
+
return await run_model(query)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Fixed billing — charge a set amount per call
|
|
60
|
+
|
|
61
|
+
Best for tools with a predictable cost per request.
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from mcp_simplr import MCPPayments, PaymentConfig, PaymentModel
|
|
65
|
+
|
|
66
|
+
payments = MCPPayments(PaymentConfig(
|
|
67
|
+
api_key="simplr_owner_...",
|
|
68
|
+
payment_model=PaymentModel.FIXED,
|
|
69
|
+
currency="aud",
|
|
70
|
+
environment="production",
|
|
71
|
+
))
|
|
72
|
+
|
|
73
|
+
await payments.charge(
|
|
74
|
+
customer_token,
|
|
75
|
+
amount_cents=500, # AUD $5.00
|
|
76
|
+
description="Property report",
|
|
77
|
+
)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Recurring billing — subscription plans
|
|
81
|
+
|
|
82
|
+
Best for services with ongoing access (monthly/yearly).
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from mcp_simplr import MCPPayments, PaymentConfig, PaymentModel, PlanConfig, BillingInterval
|
|
86
|
+
|
|
87
|
+
payments = MCPPayments(PaymentConfig(
|
|
88
|
+
api_key="simplr_owner_...",
|
|
89
|
+
payment_model=PaymentModel.RECURRING,
|
|
90
|
+
currency="aud",
|
|
91
|
+
environment="production",
|
|
92
|
+
plans=[
|
|
93
|
+
PlanConfig(id="basic", name="Basic", amount=999, interval=BillingInterval.MONTH),
|
|
94
|
+
PlanConfig(id="pro", name="Pro", amount=2999, interval=BillingInterval.MONTH),
|
|
95
|
+
],
|
|
96
|
+
))
|
|
97
|
+
|
|
98
|
+
# Subscribe a customer to a plan
|
|
99
|
+
await payments.charge(customer_token, plan_id="pro")
|
|
100
|
+
|
|
101
|
+
# Cancel a subscription
|
|
102
|
+
payments.cancel_subscription(customer_token, plan_id="pro")
|
|
103
|
+
|
|
104
|
+
# List available plans
|
|
105
|
+
plans = payments.list_plans()
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Charge history
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
history = await payments.get_charges(customer_token)
|
|
112
|
+
history = await payments.get_charges(customer_token, from_date="2026-06-01", to_date="2026-06-30")
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Testing
|
|
116
|
+
|
|
117
|
+
Use `MCPPayments.TEST_CUSTOMER_TOKEN` in sandbox mode to test the full charge flow without a real customer:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
await payments.charge(MCPPayments.TEST_CUSTOMER_TOKEN, tokens=500)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Emailing
|
|
126
|
+
|
|
127
|
+
Send emails to your users from your branded `slug@mcp-simplr.au` address. Subscribe to the email service on the platform website first, then use this client to send.
|
|
128
|
+
|
|
129
|
+
This is a **no-reply, unidirectional** service — emails are sent from your address but replies from users are not forwarded back to you. It is intended for outbound notifications, reports, and updates only.
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from mcp_simplr import MCPEmail, EmailServiceConfig
|
|
133
|
+
|
|
134
|
+
email = MCPEmail(EmailServiceConfig(
|
|
135
|
+
api_key="simplr_owner_...",
|
|
136
|
+
service_slug="property-ai", # → property-ai@mcp-simplr.au
|
|
137
|
+
environment="production",
|
|
138
|
+
))
|
|
139
|
+
|
|
140
|
+
await email.send(
|
|
141
|
+
to="customer@example.com",
|
|
142
|
+
subject="Your property report is ready",
|
|
143
|
+
body="Hi Sarah, your report for 123 Main St is attached.",
|
|
144
|
+
)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Auth
|
|
150
|
+
|
|
151
|
+
Verify that a customer token is valid before your MCP tool runs.
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from mcp_simplr import MCPAuth, AuthConfig
|
|
155
|
+
|
|
156
|
+
auth = MCPAuth(AuthConfig(
|
|
157
|
+
api_key="simplr_owner_...",
|
|
158
|
+
service_slug="property-ai",
|
|
159
|
+
environment="production",
|
|
160
|
+
))
|
|
161
|
+
|
|
162
|
+
# Decorator — verifies token before the function runs
|
|
163
|
+
@auth.require(customer_token_key="customer_token")
|
|
164
|
+
async def search_properties(query: str, customer_token: str) -> str:
|
|
165
|
+
return await do_search(query)
|
|
166
|
+
|
|
167
|
+
# Or verify explicitly — raises AuthenticationError on failure
|
|
168
|
+
customer = await auth.verify(customer_token)
|
|
169
|
+
# {"customer_id": "...", "email": "...", "name": "..."}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Support
|
|
175
|
+
|
|
176
|
+
Contact us at [contact@mcp-simplr.au](mailto:contact@mcp-simplr.au)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# mcp-simplr
|
|
2
|
+
|
|
3
|
+
Python client library for [MCP Simplr](https://mcp-simplr.au) — payments, emailing, and auth for MCP services.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install mcp-simplr
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Payments
|
|
12
|
+
|
|
13
|
+
Customers register once on the platform and receive a `customer_token`. MCP owners accept this token from their users and pass it to `charge()` — no customer registration code needed on your end.
|
|
14
|
+
|
|
15
|
+
### Token billing — charge per output token
|
|
16
|
+
|
|
17
|
+
Best for AI tools where cost scales with usage.
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from mcp_simplr import MCPPayments, PaymentConfig, PaymentModel
|
|
21
|
+
|
|
22
|
+
payments = MCPPayments(PaymentConfig(
|
|
23
|
+
api_key="simplr_owner_...",
|
|
24
|
+
payment_model=PaymentModel.TOKEN,
|
|
25
|
+
price_per_token=0.000_5, # AUD per output token
|
|
26
|
+
currency="aud",
|
|
27
|
+
environment="production",
|
|
28
|
+
))
|
|
29
|
+
|
|
30
|
+
# Charge explicitly
|
|
31
|
+
await payments.charge(customer_token, tokens=500)
|
|
32
|
+
|
|
33
|
+
# Or use the decorator — counts tokens and charges automatically
|
|
34
|
+
@payments.track_usage(customer_id="<customer-token>")
|
|
35
|
+
async def my_tool(query: str) -> str:
|
|
36
|
+
return await run_model(query)
|
|
37
|
+
|
|
38
|
+
# Dynamic customer token from request payload
|
|
39
|
+
@payments.track_usage(customer_id_key="customer_token")
|
|
40
|
+
async def my_tool(query: str, customer_token: str) -> str:
|
|
41
|
+
return await run_model(query)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Fixed billing — charge a set amount per call
|
|
45
|
+
|
|
46
|
+
Best for tools with a predictable cost per request.
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from mcp_simplr import MCPPayments, PaymentConfig, PaymentModel
|
|
50
|
+
|
|
51
|
+
payments = MCPPayments(PaymentConfig(
|
|
52
|
+
api_key="simplr_owner_...",
|
|
53
|
+
payment_model=PaymentModel.FIXED,
|
|
54
|
+
currency="aud",
|
|
55
|
+
environment="production",
|
|
56
|
+
))
|
|
57
|
+
|
|
58
|
+
await payments.charge(
|
|
59
|
+
customer_token,
|
|
60
|
+
amount_cents=500, # AUD $5.00
|
|
61
|
+
description="Property report",
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Recurring billing — subscription plans
|
|
66
|
+
|
|
67
|
+
Best for services with ongoing access (monthly/yearly).
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from mcp_simplr import MCPPayments, PaymentConfig, PaymentModel, PlanConfig, BillingInterval
|
|
71
|
+
|
|
72
|
+
payments = MCPPayments(PaymentConfig(
|
|
73
|
+
api_key="simplr_owner_...",
|
|
74
|
+
payment_model=PaymentModel.RECURRING,
|
|
75
|
+
currency="aud",
|
|
76
|
+
environment="production",
|
|
77
|
+
plans=[
|
|
78
|
+
PlanConfig(id="basic", name="Basic", amount=999, interval=BillingInterval.MONTH),
|
|
79
|
+
PlanConfig(id="pro", name="Pro", amount=2999, interval=BillingInterval.MONTH),
|
|
80
|
+
],
|
|
81
|
+
))
|
|
82
|
+
|
|
83
|
+
# Subscribe a customer to a plan
|
|
84
|
+
await payments.charge(customer_token, plan_id="pro")
|
|
85
|
+
|
|
86
|
+
# Cancel a subscription
|
|
87
|
+
payments.cancel_subscription(customer_token, plan_id="pro")
|
|
88
|
+
|
|
89
|
+
# List available plans
|
|
90
|
+
plans = payments.list_plans()
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Charge history
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
history = await payments.get_charges(customer_token)
|
|
97
|
+
history = await payments.get_charges(customer_token, from_date="2026-06-01", to_date="2026-06-30")
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Testing
|
|
101
|
+
|
|
102
|
+
Use `MCPPayments.TEST_CUSTOMER_TOKEN` in sandbox mode to test the full charge flow without a real customer:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
await payments.charge(MCPPayments.TEST_CUSTOMER_TOKEN, tokens=500)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Emailing
|
|
111
|
+
|
|
112
|
+
Send emails to your users from your branded `slug@mcp-simplr.au` address. Subscribe to the email service on the platform website first, then use this client to send.
|
|
113
|
+
|
|
114
|
+
This is a **no-reply, unidirectional** service — emails are sent from your address but replies from users are not forwarded back to you. It is intended for outbound notifications, reports, and updates only.
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from mcp_simplr import MCPEmail, EmailServiceConfig
|
|
118
|
+
|
|
119
|
+
email = MCPEmail(EmailServiceConfig(
|
|
120
|
+
api_key="simplr_owner_...",
|
|
121
|
+
service_slug="property-ai", # → property-ai@mcp-simplr.au
|
|
122
|
+
environment="production",
|
|
123
|
+
))
|
|
124
|
+
|
|
125
|
+
await email.send(
|
|
126
|
+
to="customer@example.com",
|
|
127
|
+
subject="Your property report is ready",
|
|
128
|
+
body="Hi Sarah, your report for 123 Main St is attached.",
|
|
129
|
+
)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Auth
|
|
135
|
+
|
|
136
|
+
Verify that a customer token is valid before your MCP tool runs.
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from mcp_simplr import MCPAuth, AuthConfig
|
|
140
|
+
|
|
141
|
+
auth = MCPAuth(AuthConfig(
|
|
142
|
+
api_key="simplr_owner_...",
|
|
143
|
+
service_slug="property-ai",
|
|
144
|
+
environment="production",
|
|
145
|
+
))
|
|
146
|
+
|
|
147
|
+
# Decorator — verifies token before the function runs
|
|
148
|
+
@auth.require(customer_token_key="customer_token")
|
|
149
|
+
async def search_properties(query: str, customer_token: str) -> str:
|
|
150
|
+
return await do_search(query)
|
|
151
|
+
|
|
152
|
+
# Or verify explicitly — raises AuthenticationError on failure
|
|
153
|
+
customer = await auth.verify(customer_token)
|
|
154
|
+
# {"customer_id": "...", "email": "...", "name": "..."}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Support
|
|
160
|
+
|
|
161
|
+
Contact us at [contact@mcp-simplr.au](mailto:contact@mcp-simplr.au)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from mcp_simplr.payments.client import MCPPayments
|
|
2
|
+
from mcp_simplr.email.client import MCPEmail
|
|
3
|
+
from mcp_simplr.auth.client import MCPAuth
|
|
4
|
+
from mcp_simplr.config import (
|
|
5
|
+
PaymentConfig,
|
|
6
|
+
PlanConfig,
|
|
7
|
+
EmailServiceConfig,
|
|
8
|
+
AuthConfig,
|
|
9
|
+
PaymentModel,
|
|
10
|
+
Environment,
|
|
11
|
+
Currency,
|
|
12
|
+
BillingInterval,
|
|
13
|
+
)
|
|
14
|
+
from mcp_simplr.exceptions import (
|
|
15
|
+
MCPPaymentError,
|
|
16
|
+
ConfigurationError,
|
|
17
|
+
CustomerNotFoundError,
|
|
18
|
+
PaymentFailedError,
|
|
19
|
+
InsufficientPaymentMethodError,
|
|
20
|
+
PlanNotFoundError,
|
|
21
|
+
MCPAuthError,
|
|
22
|
+
AuthenticationError,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
# Payments
|
|
27
|
+
"MCPPayments",
|
|
28
|
+
"PaymentConfig",
|
|
29
|
+
"PlanConfig",
|
|
30
|
+
# Enums
|
|
31
|
+
"PaymentModel",
|
|
32
|
+
"Environment",
|
|
33
|
+
"Currency",
|
|
34
|
+
"BillingInterval",
|
|
35
|
+
# Email service
|
|
36
|
+
"MCPEmail",
|
|
37
|
+
"EmailServiceConfig",
|
|
38
|
+
# Auth + rate limit
|
|
39
|
+
"MCPAuth",
|
|
40
|
+
"AuthConfig",
|
|
41
|
+
# Exceptions
|
|
42
|
+
"MCPPaymentError",
|
|
43
|
+
"ConfigurationError",
|
|
44
|
+
"CustomerNotFoundError",
|
|
45
|
+
"PaymentFailedError",
|
|
46
|
+
"InsufficientPaymentMethodError",
|
|
47
|
+
"PlanNotFoundError",
|
|
48
|
+
"MCPAuthError",
|
|
49
|
+
"AuthenticationError",
|
|
50
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
|
|
3
|
+
from mcp_simplr.exceptions import MCPPaymentError, PaymentFailedError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class APIClient:
|
|
7
|
+
"""
|
|
8
|
+
HTTP client that talks to the mcp-payments backend.
|
|
9
|
+
Uses sync httpx for setup calls (register_customer, subscribe)
|
|
10
|
+
and async httpx for per-request charges so the event loop is never blocked.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, api_key: str, base_url: str, environment: str = "sandbox"):
|
|
14
|
+
self._headers = {
|
|
15
|
+
"Authorization": f"Bearer {api_key}",
|
|
16
|
+
"X-Stripe-Environment": environment,
|
|
17
|
+
}
|
|
18
|
+
self._base_url = base_url.rstrip("/")
|
|
19
|
+
|
|
20
|
+
# ------------------------------------------------------------------
|
|
21
|
+
# Sync — used at server startup (register_customer, subscribe, cancel)
|
|
22
|
+
# ------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
def post(self, path: str, body: dict) -> dict:
|
|
25
|
+
with httpx.Client(headers=self._headers, timeout=30) as client:
|
|
26
|
+
return self._handle(client.post(f"{self._base_url}{path}", json=body))
|
|
27
|
+
|
|
28
|
+
def delete(self, path: str) -> dict:
|
|
29
|
+
with httpx.Client(headers=self._headers, timeout=30) as client:
|
|
30
|
+
return self._handle(client.delete(f"{self._base_url}{path}"))
|
|
31
|
+
|
|
32
|
+
# ------------------------------------------------------------------
|
|
33
|
+
# Async — used on every MCP request (charge)
|
|
34
|
+
# ------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
async def apost(self, path: str, body: dict) -> dict:
|
|
37
|
+
async with httpx.AsyncClient(headers=self._headers, timeout=30) as client:
|
|
38
|
+
return self._handle(await client.post(f"{self._base_url}{path}", json=body))
|
|
39
|
+
|
|
40
|
+
async def aget(self, path: str, params: dict | None = None) -> dict:
|
|
41
|
+
async with httpx.AsyncClient(headers=self._headers, timeout=30) as client:
|
|
42
|
+
return self._handle(await client.get(f"{self._base_url}{path}", params=params))
|
|
43
|
+
|
|
44
|
+
# ------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def _handle(response: httpx.Response) -> dict:
|
|
48
|
+
try:
|
|
49
|
+
response.raise_for_status()
|
|
50
|
+
return response.json()
|
|
51
|
+
except httpx.HTTPStatusError as e:
|
|
52
|
+
detail = e.response.json().get("detail", str(e)) if e.response.content else str(e)
|
|
53
|
+
raise PaymentFailedError(detail)
|
|
54
|
+
except httpx.RequestError as e:
|
|
55
|
+
raise MCPPaymentError(f"Could not reach mcp-payments backend: {e}")
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from mcp_simplr.config import AuthConfig
|
|
6
|
+
from mcp_simplr.exceptions import AuthenticationError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MCPAuth:
|
|
10
|
+
"""
|
|
11
|
+
Authentication client for MCP owners.
|
|
12
|
+
|
|
13
|
+
Verifies that a customer_token is valid before your MCP tool runs.
|
|
14
|
+
|
|
15
|
+
Subscription is managed via the platform website — not through this library.
|
|
16
|
+
Once subscribed, use verify() or the @require decorator in every tool.
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
from mcp_simplr import MCPAuth, AuthConfig
|
|
20
|
+
|
|
21
|
+
auth = MCPAuth(AuthConfig(
|
|
22
|
+
api_key="simplr_owner_xxx",
|
|
23
|
+
service_slug="property-ai",
|
|
24
|
+
))
|
|
25
|
+
|
|
26
|
+
# Decorator — auto-verifies before the function runs
|
|
27
|
+
@auth.require(customer_token_key="customer_token")
|
|
28
|
+
async def my_tool(query: str, customer_token: str) -> str:
|
|
29
|
+
return await do_work(query)
|
|
30
|
+
|
|
31
|
+
# Or explicit — raises AuthenticationError on failure
|
|
32
|
+
customer = await auth.verify(customer_token)
|
|
33
|
+
# customer = {"customer_id": "...", "email": "...", "name": "..."}
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, config: AuthConfig) -> None:
|
|
37
|
+
self._config = config
|
|
38
|
+
self._headers = {
|
|
39
|
+
"Authorization": f"Bearer {config.api_key}",
|
|
40
|
+
"Content-Type": "application/json",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async def verify(self, customer_token: str) -> dict:
|
|
44
|
+
"""
|
|
45
|
+
Verify a customer token.
|
|
46
|
+
|
|
47
|
+
Returns {"customer_id": str, "email": str, "name": str | None} on success.
|
|
48
|
+
Raises AuthenticationError if the token is invalid.
|
|
49
|
+
"""
|
|
50
|
+
async with httpx.AsyncClient(timeout=10) as client:
|
|
51
|
+
res = await client.post(
|
|
52
|
+
f"{self._config.api_base}/v1/auth-service/verify",
|
|
53
|
+
headers=self._headers,
|
|
54
|
+
json={
|
|
55
|
+
"service_slug": self._config.service_slug,
|
|
56
|
+
"customer_token": customer_token,
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if res.status_code == 401:
|
|
61
|
+
raise AuthenticationError(res.json().get("detail", "Invalid customer token"))
|
|
62
|
+
res.raise_for_status()
|
|
63
|
+
return res.json()
|
|
64
|
+
|
|
65
|
+
def require(self, customer_token_key: str = "customer_token"):
|
|
66
|
+
"""
|
|
67
|
+
Decorator that verifies the customer token before the function runs.
|
|
68
|
+
|
|
69
|
+
The decorated function must accept the token as a keyword argument
|
|
70
|
+
matching customer_token_key (default: "customer_token").
|
|
71
|
+
"""
|
|
72
|
+
def decorator(fn):
|
|
73
|
+
@functools.wraps(fn)
|
|
74
|
+
async def wrapper(*args, **kwargs):
|
|
75
|
+
token = kwargs.get(customer_token_key)
|
|
76
|
+
if not token:
|
|
77
|
+
raise AuthenticationError(
|
|
78
|
+
f"Missing '{customer_token_key}' argument — "
|
|
79
|
+
"pass the customer's simplr_ token to this tool."
|
|
80
|
+
)
|
|
81
|
+
await self.verify(token)
|
|
82
|
+
return await fn(*args, **kwargs)
|
|
83
|
+
return wrapper
|
|
84
|
+
return decorator
|
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
_STRIPE_MIN_CENTS = 50
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class FixedBilling:
|
|
5
|
+
"""
|
|
6
|
+
One-time fixed-price billing. The MCP owner calculates the cost and calls
|
|
7
|
+
charge() explicitly. customer_id is the platform UUID from /v1/customers/register.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, config, api_client):
|
|
11
|
+
self.config = config
|
|
12
|
+
self._api = api_client
|
|
13
|
+
|
|
14
|
+
async def charge(self, customer_id: str, amount_cents: int, description: str = "") -> dict | None:
|
|
15
|
+
"""
|
|
16
|
+
Charge the customer a fixed amount calculated by the MCP owner.
|
|
17
|
+
Returns the charge result dict, or None if below Stripe's minimum.
|
|
18
|
+
"""
|
|
19
|
+
if amount_cents < _STRIPE_MIN_CENTS:
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
return await self._api.apost(
|
|
23
|
+
"/v1/charge",
|
|
24
|
+
{
|
|
25
|
+
"customer_token": customer_id,
|
|
26
|
+
"amount_cents": amount_cents,
|
|
27
|
+
"currency": self.config.currency,
|
|
28
|
+
"description": description,
|
|
29
|
+
"service_name": self.config.service_name,
|
|
30
|
+
"service_description": self.config.service_description,
|
|
31
|
+
},
|
|
32
|
+
)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from mcp_simplr.exceptions import PlanNotFoundError
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class RecurringBilling:
|
|
5
|
+
"""
|
|
6
|
+
Recurring subscription billing. The MCP owner defines plans and Stripe handles
|
|
7
|
+
automatic charges on each billing cycle. customer_id is the platform UUID
|
|
8
|
+
from /v1/customers/register.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, config, api_client):
|
|
12
|
+
self.config = config
|
|
13
|
+
self._api = api_client
|
|
14
|
+
# customer_id -> {plan_id, stripe_sub_id} — tracked in memory for cancel()
|
|
15
|
+
self._subscriptions: dict[str, dict] = {}
|
|
16
|
+
|
|
17
|
+
async def subscribe(self, customer_id: str, plan_id: str) -> str:
|
|
18
|
+
"""
|
|
19
|
+
Subscribe a customer to a recurring plan via the backend.
|
|
20
|
+
Returns the Stripe Subscription ID.
|
|
21
|
+
"""
|
|
22
|
+
plan = self._resolve_plan(plan_id)
|
|
23
|
+
result = await self._api.apost(
|
|
24
|
+
"/v1/subscriptions",
|
|
25
|
+
{
|
|
26
|
+
"customer_token": customer_id,
|
|
27
|
+
"plan": {
|
|
28
|
+
"id": plan.id,
|
|
29
|
+
"name": plan.name,
|
|
30
|
+
"amount": plan.amount,
|
|
31
|
+
"currency": self.config.currency,
|
|
32
|
+
"interval": plan.interval,
|
|
33
|
+
"description": plan.description,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
)
|
|
37
|
+
self._subscriptions[customer_id] = {
|
|
38
|
+
"plan_id": plan_id,
|
|
39
|
+
"stripe_sub_id": result["stripe_subscription_id"],
|
|
40
|
+
}
|
|
41
|
+
return result["stripe_subscription_id"]
|
|
42
|
+
|
|
43
|
+
def cancel(self, customer_id: str, plan_id: str) -> None:
|
|
44
|
+
sub = self._subscriptions.get(customer_id)
|
|
45
|
+
if not sub or sub["plan_id"] != plan_id:
|
|
46
|
+
raise KeyError(
|
|
47
|
+
f"No active subscription found for customer '{customer_id}' on plan '{plan_id}'. "
|
|
48
|
+
"subscribe() must be called in the same process before cancel()."
|
|
49
|
+
)
|
|
50
|
+
self._api.delete(f"/v1/subscriptions/{sub['stripe_sub_id']}")
|
|
51
|
+
del self._subscriptions[customer_id]
|
|
52
|
+
|
|
53
|
+
def list_plans(self) -> list[dict]:
|
|
54
|
+
return [
|
|
55
|
+
{
|
|
56
|
+
"id": p.id,
|
|
57
|
+
"name": p.name,
|
|
58
|
+
"amount": p.amount,
|
|
59
|
+
"currency": self.config.currency.upper(),
|
|
60
|
+
"interval": p.interval,
|
|
61
|
+
"description": p.description,
|
|
62
|
+
}
|
|
63
|
+
for p in self.config.plans
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
def _resolve_plan(self, plan_id: str):
|
|
67
|
+
for plan in self.config.plans:
|
|
68
|
+
if plan.id == plan_id:
|
|
69
|
+
return plan
|
|
70
|
+
raise PlanNotFoundError(f"Plan '{plan_id}' not found in config")
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
_STRIPE_MIN_CENTS = 50
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TokenBilling:
|
|
5
|
+
"""
|
|
6
|
+
Per-request billing charged by output tokens. Every tracked MCP call immediately
|
|
7
|
+
fires a charge through the platform backend using the customer's platform UUID.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, config, api_client):
|
|
11
|
+
self.config = config
|
|
12
|
+
self._api = api_client
|
|
13
|
+
|
|
14
|
+
async def charge_for_request(self, customer_id: str, tokens: int) -> dict | None:
|
|
15
|
+
"""
|
|
16
|
+
Calculate cost and immediately charge the customer via the backend.
|
|
17
|
+
customer_id is the platform UUID from /v1/customers/register.
|
|
18
|
+
Returns the charge result dict, or None if below Stripe's minimum.
|
|
19
|
+
"""
|
|
20
|
+
amount_cents = int(tokens * self.config.price_per_token * 100)
|
|
21
|
+
if amount_cents < _STRIPE_MIN_CENTS:
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
return await self._api.apost(
|
|
25
|
+
"/v1/charge",
|
|
26
|
+
{
|
|
27
|
+
"customer_token": customer_id,
|
|
28
|
+
"amount_cents": amount_cents,
|
|
29
|
+
"currency": self.config.currency,
|
|
30
|
+
"description": f"MCP request — {tokens:,} output tokens",
|
|
31
|
+
"service_name": self.config.service_name,
|
|
32
|
+
"service_description": self.config.service_description,
|
|
33
|
+
},
|
|
34
|
+
)
|