Metadata-Version: 2.4
Name: mcp-simplr
Version: 0.4.3
Summary: Simplr client library for MCP services
Author-email: contact@mcp-simplr.au
License: MIT
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: httpx>=0.27
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Requires-Dist: pytest-mock>=3.12; extra == "dev"
Requires-Dist: respx>=0.21; extra == "dev"

# mcp-simplr

Python client library for [MCP Simplr](https://mcp-simplr.au) — payments, emailing, and auth for MCP services.

## Installation

```bash
pip install mcp-simplr
```

## Payments

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.

### Token billing — charge per output token

Best for AI tools where cost scales with usage.

```python
from mcp_simplr import MCPPayments, PaymentConfig, PaymentModel

payments = MCPPayments(PaymentConfig(
    api_key="simplr_owner_...",
    payment_model=PaymentModel.TOKEN,
    price_per_token=0.000_5,   # AUD per output token
    currency="aud",
    environment="production",
))

# Charge explicitly
await payments.charge(customer_token, tokens=500)

# Or use the decorator — counts tokens and charges automatically
@payments.track_usage(customer_token="<customer-token>")
async def my_tool(query: str) -> str:
    return await run_model(query)

# Dynamic customer token from request payload
@payments.track_usage(customer_token_key="customer_token")
async def my_tool(query: str, customer_token: str) -> str:
    return await run_model(query)
```

### Fixed billing — charge a set amount per call

Best for tools with a predictable cost per request.

```python
from mcp_simplr import MCPPayments, PaymentConfig, PaymentModel

payments = MCPPayments(PaymentConfig(
    api_key="simplr_owner_...",
    payment_model=PaymentModel.FIXED,
    currency="aud",
    environment="production",
))

await payments.charge(
    customer_token,
    amount_cents=500,           # AUD $5.00
    description="Property report",
)
```

### Recurring billing — subscription plans

Best for services with ongoing access (monthly/yearly).

```python
from mcp_simplr import MCPPayments, PaymentConfig, PaymentModel, PlanConfig, BillingInterval

payments = MCPPayments(PaymentConfig(
    api_key="simplr_owner_...",
    payment_model=PaymentModel.RECURRING,
    currency="aud",
    environment="production",
    plans=[
        PlanConfig(id="basic", name="Basic", amount=999, interval=BillingInterval.MONTH),
        PlanConfig(id="pro",   name="Pro",   amount=2999, interval=BillingInterval.MONTH),
    ],
))

# Subscribe a customer to a plan
await payments.charge(customer_token, plan_id="pro")

# Cancel a subscription
payments.cancel_subscription(customer_token, plan_id="pro")

# List available plans
plans = payments.list_plans()
```

### Charge history

```python
history = await payments.get_charges(customer_token)
history = await payments.get_charges(customer_token, from_date="2026-06-01", to_date="2026-06-30")
```

### Testing

Use `MCPPayments.TEST_CUSTOMER_TOKEN` in sandbox mode to test the full charge flow without a real customer:

```python
await payments.charge(MCPPayments.TEST_CUSTOMER_TOKEN, tokens=500)
```

---

## Emailing

Send emails to your users from your branded `slug@mcp-simplr.au` address. Subscribe to the email service on the platform website first — your project API key encodes the service slug automatically.

Customer replies are forwarded to the personal inbox you registered during subscription.

```python
from mcp_simplr import MCPEmail, EmailServiceConfig

email = MCPEmail(EmailServiceConfig(
    api_key="simplr_owner_...",   # project key — encodes your service slug
    environment="production",
))

await email.send(
    to="customer@example.com",
    subject="Your property report is ready",
    body="Hi Sarah, your report for 123 Main St is attached.",
)
```

---

## Auth

Verify that a caller holds a valid customer token before your MCP tool runs. Subscribe to the auth service on the platform website first.

`verify()` accepts both a raw `simplr_` token and a JWT session from the platform website, so customers can auth either way without you changing any code.

```python
from mcp_simplr import MCPAuth, AuthConfig

auth = MCPAuth(AuthConfig(
    api_key="simplr_owner_...",   # project key — encodes your service slug
    environment="production",
))

# Both work identically — raw token or platform JWT
customer = await auth.verify(customer_token)
# {
#   "customer_id":  "uuid",          # ← stable UUID — safe to use as a DB key
#   "email":        "...",
#   "name":         "...",
#   "simplr_token": "simplr_...",   # ← use this to charge the customer
# }
```

### Auth-only (decorator)

```python
@auth.require(customer_token_key="customer_token")
async def search_properties(query: str, customer_token: str) -> str:
    return await do_search(query)
```

### Auth + charge in the same tool

Pass `result_key` to inject the full verify result — including `simplr_token` — into your handler:

```python
from mcp_simplr import MCPAuth, MCPPayments, AuthConfig, PaymentConfig, PaymentModel, Currency, Environment

auth     = MCPAuth(AuthConfig(api_key="simplr_owner_..."))
payments = MCPPayments(PaymentConfig(
    api_key="simplr_owner_...",
    payment_model=PaymentModel.TOKEN,
    currency=Currency.AUD,
    environment=Environment.PRODUCTION,
    price_per_token=0.0005,
))

@auth.require(customer_token_key="customer_token", result_key="auth")
async def search_properties(query: str, customer_token: str, auth: dict = None) -> str:
    result = await run_model(query)
    await payments.charge(auth["simplr_token"], tokens=result.usage.output_tokens)
    return result
```

### Customer auth flows

**Direct token (MCP PAT):** Customer gets their `simplr_xxx` token on registration and passes it to your tool — works out of the box. The token never expires.

**JWT session (your website):** Use `issue_jwt=True` on first verify — Simplr issues a signed JWT your site uses as the customer's session token. On every subsequent request call `verify(jwt)` to validate and recover `simplr_token` for charging. Your backend stays completely stateless — no Simplr credentials to store.

```python
# ── On login (customer enters simplr_xxx once) ──────────────────────────────
result = await auth.verify(simplr_token, issue_jwt=True)
# Send result["access_token"] (JWT, 24 h) to the browser — nothing else to store.
# simplr_token lives encrypted in Simplr's DB and is recovered on every verify() call.

# ── On every protected request (browser sends JWT) ──────────────────────────
customer = await auth.verify(jwt_from_browser)
await payments.charge(customer["simplr_token"], amount_cents=500, description="Report")
# simplr_token is returned on demand — never needs to be persisted on your side.
```

The JWT is scoped to your project key — it cannot be replayed against another owner's service.

> **Background / scheduled charging:** If you need to charge a customer without them being online (e.g. a cron job), there is no JWT to pass to `verify()`. In that case, storing `simplr_token` encrypted in your own DB is intentional and appropriate.

> **Note:** JWT auth requires an encrypted copy of the raw token stored at registration. Customers who registered before this feature was introduced must rotate their token once (via the platform website) before JWT auth will work for them.

---

## Support

Contact us at [contact@mcp-simplr.au](mailto:contact@mcp-simplr.au)
