Metadata-Version: 2.4
Name: mcp-simplr
Version: 0.11.2
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)
```

With token billing, the cost is only known *after* the model call finishes — so `charge()` necessarily runs after you've already paid for the LLM call yourself. Use `check_ready()` first to fail fast on a customer who can't be charged, before spending anything:

```python
if not await payments.check_ready(customer_token):
    return "Payment isn't set up for this customer right now."

result = await run_model(query)
await payments.charge(customer_token, tokens=result.usage.output_tokens)
```

### 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)
```

`TEST_CUSTOMER_TOKEN` only works when `environment=Environment.SANDBOX` — its payment method exists only in
Stripe's sandbox, so `check_ready()` returns `False` and `charge()` raises `PaymentFailedError` for it under
`environment=Environment.PRODUCTION`, even if the token is passed in some other way (e.g. via `oauth.verify()`).

---

## 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
))

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

Gate your MCP tools so only registered customers can call them. Customers authorise via OAuth — they log in with their email and approve your tool, no tokens for them to copy around. Subscribe to the Auth service on the platform website first.

### Setup

```python
from mcp_simplr import AuthConfig
from mcp_simplr.oauth import MCPOAuth

oauth = MCPOAuth(
    auth_config=AuthConfig(api_key="simplr_owner_..."),   # project key — encodes your service slug
    oauth_client_id="...",                 # from POST /oauth/register — omit for MCP-discovery only
    frontend_url="https://yourapp.com",    # required together with oauth_client_id
)
app.include_router(oauth.router)
# Serves /.well-known/oauth-authorization-server (always), plus /auth/login,
# /auth/callback, /auth/refresh when oauth_client_id + frontend_url are set.
```

One flow, no sandbox/production switch to configure — see Testing below.

### Auth-only (decorator)

```python
@oauth.require(bearer_token_key="bearer_token")
async def search_properties(query: str, bearer_token: str) -> str:
    return await do_search(query)
```

Raises `AuthenticationError` if the bearer token is missing, invalid, or expired.

### 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 AuthConfig, MCPPayments, PaymentConfig, PaymentModel, Currency
from mcp_simplr.oauth import MCPOAuth

oauth    = MCPOAuth(auth_config=AuthConfig(api_key="simplr_owner_..."))
payments = MCPPayments(PaymentConfig(
    api_key="simplr_owner_...",
    payment_model=PaymentModel.TOKEN,
    currency=Currency.AUD,
    environment="production",
    price_per_token=0.0005,
))

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

### Calling verify() directly

Prefer manual control over the decorator? `oauth.verify()` is the same call, just awaited directly:

```python
customer = await oauth.verify(bearer_token)
# {
#   "customer_id":  "uuid",          # ← stable UUID — safe to use as a DB key
#   "email":        "...",
#   "name":         "...",
#   "simplr_token": "simplr_...",   # ← use this to charge the customer
# }
```

### Testing

To test your own integration, log in on the consent screen as `test@simplr.dev` with *any* 6-digit code — that account skips the real OTP check too, so testing never depends on retrieving a code from logs or email. It always passes, regardless of your project's Auth service subscription status, and returns `customer["simplr_token"] == MCPPayments.TEST_CUSTOMER_TOKEN`, a working test card safe to call `charge()` against. Any other customer needs a real OTP and your project needs an active Auth service subscription.

```python
customer = await oauth.verify(test_bearer_token)
await payments.charge(customer["simplr_token"], tokens=500)  # never touches a real card
```

This only works to spend money in sandbox — if your `MCPPayments` config has `environment=Environment.PRODUCTION`,
`charge()` raises `PaymentFailedError` for `customer["simplr_token"]` here, since that test payment method only
exists in Stripe's sandbox. The auth bypass itself still lets `test@simplr.dev` through either way; it's `charge()`
and `check_ready()` that refuse to bill it in production.

---

## Support

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