# wayforpay-mcp

Model Context Protocol (MCP) server for **WayForPay** — a popular Ukrainian
payment gateway. Configure one or more merchant profiles once, then let any
MCP-capable agent query WayForPay data or trigger a limited set of payment
operations.

> Status: **production-shaped**. 15 tools available in read-only mode by
> default, 21 total when `WFP_ENABLE_WRITE_TOOLS=true`, plus 6 prompts.
> Output is TOON by default; read tools use `apiVersion: 2` by default so
> lean projections keep useful customer/product/PRRO fields without dumping
> the full payload.

## What it does

| Tool                            | Kind     | Wraps                              |
|---------------------------------|----------|------------------------------------|
| `wfp_list_profiles`             | read     | (config introspection)             |
| `wfp_get_merchant_info`         | read     | `mms/merchantInfo.php`             |
| `wfp_get_balance`               | read     | `mms/merchantBalance.php`          |
| `wfp_get_currency_rates`        | read     | `CURRENCY_RATES`                   |
| `wfp_get_transaction_list`      | read     | `TRANSACTION_LIST` raw rows (paginated, default limit 20) |
| `wfp_summarize_transactions`    | read     | `TRANSACTION_LIST` aggregates (counts, totals, by-status/day) — no rows |
| `wfp_search_transactions`       | read     | `TRANSACTION_LIST` filter (status / amount / paymentSystem / substrings) — requires at least one filter |
| `wfp_list_active_instruments`   | read     | Prefix-based analytics for social invoices / payment buttons |
| `wfp_instrument_transactions`   | read     | Prefix-based transaction slice with summary + paginated rows |
| `wfp_check_status`              | read     | `CHECK_STATUS`                     |
| `wfp_get_regular_status`        | read     | regularApi `STATUS`                |
| `wfp_verify_payment_signature`  | local    | HMAC-MD5 verifier                  |
| `wfp_build_service_ack`         | local    | webhook ack builder                |
| `wfp_create_invoice` 🔥         | write    | `CREATE_INVOICE`                   |
| `wfp_refund_payment` 🔥         | write    | `REFUND`                           |
| `wfp_settle_payment` 🔥         | write    | `SETTLE`                           |
| `wfp_suspend_recurring`         | write    | regularApi `SUSPEND`               |
| `wfp_resume_recurring`          | write    | regularApi `RESUME`                |
| `wfp_delete_recurring` 🔥       | write    | regularApi `REMOVE`                |
| `wfp_health`                    | meta     | health check via `getMerchantInfo` |
| `wfp_capabilities`              | meta     | tool discovery for foreign agents  |

🔥 = destructive: requires `confirm: true` in the tool input. Description
tells the model to confirm with you in chat first.

A full endpoint reference (including endpoints not yet wrapped) is in
[`docs/api-endpoints.md`](docs/api-endpoints.md).

## Output format

Tool responses follow MCP's dual-channel pattern:

- **`content[].text`** (what the model reads) — defaults to **TOON**
  ([Token-Oriented Object Notation](https://github.com/toon-format/toon)).
  Tabular arrays of uniform objects collapse to CSV-with-schema, ~40% fewer
  tokens than JSON.
- **`structuredContent`** (programmatic consumers) — always plain JSON-friendly
  objects.

Knobs on read tools:

| Param      | Type                  | Effect                                                                |
|------------|-----------------------|-----------------------------------------------------------------------|
| `format`   | `"toon"` \| `"json"`  | Switch the text channel back to JSON. Default `"toon"`.               |
| `verbose`  | `boolean`             | Skip the lean projection and return every field. Default `false`.     |
| `limit`    | `number` (1–500)      | Page size. Transaction list only.                                     |
| `offset`   | `number` (≥0)         | Page offset. Use `next_offset` from a prior response.                 |
| `merchant` | `string`              | Merchant alias from `wfp_list_profiles`. Defaults to `WFP_DEFAULT_MERCHANT`. |

Errors come back as JSON in `text` — they're small and diagnostics are
easier when format is universal.

## Read payloads

`wfp_get_transaction_list` and `wfp_check_status` use `apiVersion: 2` by
default. That means responses can include richer fields like `products`,
`clientName`, `clientEmail`, `clientPhone`, `clientComment`, `prroLink`,
`prroNumber`, `processingDate`, `settlementDate`, `settlementReference`,
`baseAmount`, and `baseCurrency`. Lean mode keeps the most useful of these;
`verbose: true` returns the full payload.

## Quickstart

### 1. Get WayForPay credentials

For each merchant in your account: `merchantAccount`, `secretKey` (used to
sign every request), optionally `merchantPassword` (only for `regularApi`).

### 2. Add to your MCP client

#### Claude Code

```bash
claude mcp add wayforpay \
  --env WFP_DEFAULT_MERCHANT=main \
  --env WFP_ENABLE_WRITE_TOOLS=false \
  --env WFP_MERCHANTS_JSON='[{"alias":"main","merchantAccount":"...","secretKey":"...","domainName":"shop.example.com","defaultCurrency":"UAH"}]' \
  -- bun run /path/to/wayforpay-mcp/src/index.ts
```

#### Claude Desktop

Edit `~/Library/Application Support/Claude/claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "wayforpay": {
      "command": "bun",
      "args": ["run", "/path/to/wayforpay-mcp/src/index.ts"],
      "env": {
        "WFP_DEFAULT_MERCHANT": "main",
        "WFP_ENABLE_WRITE_TOOLS": "false",
        "WFP_MERCHANTS_JSON": "[{\"alias\":\"main\",\"merchantAccount\":\"...\",\"secretKey\":\"...\"}]"
      }
    }
  }
}
```

### 3. Try it

```
Show me yesterday's WayForPay transactions for my main merchant.
```

Claude will call `wfp_get_transaction_list` with the right date range,
paginate if needed, and summarize via TOON.

## Local development

```bash
bun install
bun run dev                      # MCP server in stdio mode
bun run validate                 # typecheck + lint + tests
bun test                         # unit tests only
```

## Configuration (env vars)

| Variable                   | Required | Default | Notes                                                          |
|----------------------------|----------|---------|----------------------------------------------------------------|
| `WFP_MERCHANTS_JSON`       | yes*     | —       | Inline JSON array of merchant profiles. *Either this or `WFP_MERCHANTS_FILE`. |
| `WFP_MERCHANTS_FILE`       | yes*     | —       | Path to a JSON file with the same shape.                       |
| `WFP_DEFAULT_MERCHANT`     | no       | (auto) if 1 merchant | Alias to use when `merchant` parameter is omitted. |
| `WFP_ENABLE_WRITE_TOOLS`   | no       | `false` | Set `true` to expose invoice/refund/settle plus recurring suspend/resume/delete. |
| `WFP_REQUEST_TIMEOUT_MS`   | no       | `15000` | Per-request timeout.                                           |

Merchant profile shape:

```jsonc
{
  "alias": "main",                                  // identifier used in `merchant` param
  "merchantAccount": "merchant_x",                  // from WFP dashboard
  "secretKey": "secret_x",                          // signs every request
  "merchantPassword": "optional_regular_password",  // only for regularApi
  "domainName": "shop.example.com",                 // required for createInvoice
  "defaultCurrency": "UAH",
  "serviceUrl": "https://shop.example.com/api/wfp/webhook"
}
```

## Working with transaction lists safely

WayForPay's `TRANSACTION_LIST` returns the entire 31-day window in one response.
On a busy merchant that's hundreds of dense rows — enough to blow the LLM
context window if dumped raw. This MCP exposes three tools so the agent picks
the right shape for the question:

| Question                                              | Tool                          | Returns rows? |
|-------------------------------------------------------|-------------------------------|:-------------:|
| "How many / what's the total / how's it trending?"    | `wfp_summarize_transactions`  | ❌ aggregates only |
| "Find declined / over 5000 / from acme@..."           | `wfp_search_transactions`     | ✅ filtered + paginated |
| "Show me the rows" (after narrowing by date)          | `wfp_get_transaction_list`    | ✅ raw + paginated |

`wfp_search_transactions` refuses unfiltered calls (must pass at least one
of `status` / `amount_min` / `amount_max` / `payment_system` /
`orderReference_contains` / `email_contains` / `card_pan_contains`).
`wfp_get_transaction_list` defaults to 20 rows per page and refuses
`verbose: true` when `limit > 20`. The response carries a `hint` field
when `total > 100` so the model is reminded of the safer alternatives.

## How other coding agents discover this MCP

Foreign agents (Cursor / Cline / Continue) attach without context. The
expected first call is `wfp_capabilities`, which returns:

- Server `version` and `name`.
- Configured `merchants` list and `default_merchant`.
- `output_format`: `{default: "toon", alternative: "json"}`.
- `destructive_safety` note about `confirm: true` discipline.
- `tools[]`: every registered tool with `name`, `title`, one-line `summary`,
  and metadata flags (`destructive`, `has_pagination`, `has_format`,
  `has_verbose`, `input_keys`).

That single response replaces a README round-trip — agents can plan a
multi-step workflow (e.g. "find a transaction, then refund it") with no
trial-and-error tool guessing.

## Prompts

The server also registers 6 prompts for common flows:

- `financial-report`
- `daily-monitoring`
- `reconciliation`
- `subscription-overview`
- `find-transaction`
- `revenue-comparison`

Each prompt supports an optional `merchant` argument so multi-merchant setups
can stay explicit.

## Safety model

Every destructive tool requires `confirm: z.literal(true)`:

- `wfp_create_invoice` (generates a payment URL)
- `wfp_refund_payment` (real money out — irreversible)
- `wfp_settle_payment` (captures held funds)
- `wfp_delete_recurring` (irreversible recurring cancel)

The tool description for each one instructs the model to confirm the
action with you in chat before sending `confirm: true`. You can also force
read-only mode with `WFP_ENABLE_WRITE_TOOLS=false`, which removes all
mutation tools from the registered list, including recurring suspend/resume/delete.

Secrets are loaded from env or file, never from tool arguments. The server
never echoes `secretKey` or `merchantPassword` back to the client.

## Known constraints

- `TRANSACTION_LIST` is limited by WayForPay to a 31-day window — the tool
  enforces this.
- The `regularApi` is a different auth model and requires `merchantPassword`.
- WFP webhook ack signature wording is inconsistent across docs; this server
  uses the `HMAC_MD5(secretKey, "orderReference;status;time")` pattern
  documented on Purchase/Invoice pages.
- WFP API requires HMAC-MD5; cannot be upgraded to a stronger MAC.

## License

Private package; see workspace owner.
