@reevit/mcp
v0.2.0
Published
Reevit MCP server — let AI agents create payment links, check payments, and issue refunds through your Reevit account
Readme
@reevit/mcp
MCP server for Reevit — lets AI agents create payment links, look up payments, issue refunds, and read analytics through your Reevit account, using a scoped API key you control.
📖 Full documentation: docs.reevit.io/docs/reevit/mcp
Tools
| Tool | What it does | Scope needed |
| --- | --- | --- |
| reevit_create_payment_link | Shareable pay link (fixed or open amount) | payment_links:write |
| reevit_list_payment_links | Links with filters and use counts, paged | payment_links:read |
| reevit_get_payment_link | One link by id (plink_…) | payment_links:read |
| reevit_get_payment | One payment by id, with status + route | payments:read |
| reevit_list_payments | Recent payments with filters, paged | payments:read |
| reevit_get_payment_failures | Why a payment failed: provider error, fault party, retriable | payments:read |
| reevit_get_payment_status_history | Ordered status transitions and what triggered each | payments:read + a plan with audit logs |
| reevit_create_refund | Full/partial refund — live mode requires human approval | payments:write |
| reevit_get_refund | One refund by id — did it actually settle? | payments:read |
| reevit_list_refunds | Refunds account-wide, or every refund on one payment | payments:read |
| reevit_get_payment_analytics | Volume, counts, success rate | payments:read |
Scopes are enforced by the Reevit backend on every call: a payments:read-only
key cannot refund, no matter what the model asks for.
reevit_get_payment_status_history carries a second gate on top of scopes — a
plan-feature check for audit logs. It answers 402, not 403, when the account
plan does not include it. The two are distinct on purpose: 403 is about the key
and 402 is about the account, so no amount of re-keying clears a 402.
Renamed in 0.2.0. Tool names are now prefixed
reevit_, andget_analytics_summarybecamereevit_get_payment_analytics. Tool names share one flat namespace across every server in a session, so the old names collided with any other server exposing aget_paymentorget_analytics_summary— and told the model nothing about which product they queried. Nothing needs changing in your config; if you have prompts or agent instructions that name tools explicitly, update them.
Results are typed in 0.2.0. Every tool now declares an
outputSchemaand returnsstructuredContentalongside the JSON text, so clients get typed data rather than a blob to re-parse. Two results changed shape to make room for it:reevit_get_paymentreturns{ payment: … }andreevit_create_payment_linkreturns{ payment_link: … }, matching the wrappingreevit_list_paymentsandreevit_create_refundalready used.The schemas are deliberately permissive — every field optional and nullable, unknown fields passed through. The SDK turns a schema mismatch into a failed call, so a schema stricter than the API would break a working read rather than report a data problem. Treat them as documentation of the common fields, not as a guarantee that a field is present.
Setup
Create an API key in the Reevit dashboard (Developers → API keys) with only the scopes you want the agent to have.
Claude Code
claude mcp add reevit -e REEVIT_API_KEY=pfk_test_... -- npx -y @reevit/mcpClaude Desktop
Add to claude_desktop_config.json (Settings → Developer → Edit Config):
{
"mcpServers": {
"reevit": {
"command": "npx",
"args": ["-y", "@reevit/mcp"],
"env": { "REEVIT_API_KEY": "pfk_test_..." }
}
}
}Cursor
Add to ~/.cursor/mcp.json (global) or .cursor/mcp.json in your project:
{
"mcpServers": {
"reevit": {
"command": "npx",
"args": ["-y", "@reevit/mcp"],
"env": { "REEVIT_API_KEY": "pfk_test_..." }
}
}
}VS Code (Copilot agent mode)
Add to .vscode/mcp.json in your workspace:
{
"servers": {
"reevit": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@reevit/mcp"],
"env": { "REEVIT_API_KEY": "pfk_test_..." }
}
}
}Windsurf
Add to ~/.codeium/windsurf/mcp_config.json:
{
"mcpServers": {
"reevit": {
"command": "npx",
"args": ["-y", "@reevit/mcp"],
"env": { "REEVIT_API_KEY": "pfk_test_..." }
}
}
}Cline
Open Cline → MCP Servers → Configure, and add the same mcpServers entry as
Cursor/Windsurf above to cline_mcp_settings.json.
Zed
Add to Zed settings.json:
{
"context_servers": {
"reevit": {
"command": { "path": "npx", "args": ["-y", "@reevit/mcp"] },
"settings": {}
}
}
}Set REEVIT_API_KEY in the environment Zed is launched from.
Codex CLI
Add to ~/.codex/config.toml:
[mcp_servers.reevit]
command = "npx"
args = ["-y", "@reevit/mcp"]
env = { REEVIT_API_KEY = "pfk_test_..." }Gemini CLI
Add to ~/.gemini/settings.json:
{
"mcpServers": {
"reevit": {
"command": "npx",
"args": ["-y", "@reevit/mcp"],
"env": { "REEVIT_API_KEY": "pfk_test_..." }
}
}
}Configuration
| Env var | Default | Notes |
| --- | --- | --- |
| REEVIT_API_KEY | — (required) | Scoped API key from the dashboard. Must start pfk_test_ or pfk_live_ |
| REEVIT_API_URL | https://api.reevit.io | Point at a self-hosted / local API |
| REEVIT_MCP_HTTP_TOKEN | — | Bearer token required by --http. See Streamable HTTP mode |
| REEVIT_MODE | derived from the key | Optional. If set it must agree with the key, or the server refuses to start |
Mode comes from the key
Test vs live is a property of the API key (pfk_test_… / pfk_live_…), not of
the environment. The backend derives it from the key prefix and ignores any
mode header on API-key requests, so REEVIT_MODE cannot put a live key into
test mode — it can only misdescribe one. The server therefore derives mode from
the key, and treats a contradicting REEVIT_MODE as a startup error.
A key with an unrecognized prefix is rejected at startup rather than assumed to be test — that also catches a truncated or mispasted key immediately instead of on the first API call.
Safety model
- Scopes, not trust: authorization lives in the backend key scopes.
- Live-money gate:
reevit_create_refundin live mode will not proceed without human approval of that specific refund. Live mode is determined by the key, so this cannot be bypassed by omitting or mis-settingREEVIT_MODE. There are two mechanisms, and which one applies depends on the client:- The client supports elicitation —
the server sends an
elicitation/createrequest naming the payment, the amount and the reason, and the client puts it to the user. This supersedesconfirmentirely: the refund proceeds only on an explicit approval, and settingconfirm: truedoes not skip the prompt. It fails closed — a decline, a dismissal, a submitted-but-unticked form, or an error reaching the client all refuse the refund without calling the API. - The client does not — the server falls back to requiring
confirm: trueon the call. This is weaker on purpose-of-necessity:confirmis a tool input, so the model writes it, and the gate is only as good as the agent's honesty. Prefer a client that can elicit for live keys.
- The client supports elicitation —
the server sends an
- Credentials never enter the transcript: a payment's
client_secret(and any*_secretfield, at any depth, including inside the opaquenext_actionprovider blob) is replaced with a[redacted …]marker before the result leaves the server. These are bearer tokens for a single payment — holding one is enough to drive that payment to completion without an API key — andreevit_get_paymentpreviously returned them verbatim, so an ordinary read-only lookup wrote a live credential into the model's context and into whatever transcript or log the client keeps. The marker replaces the value rather than dropping the key, so an agent is told the field is withheld instead of concluding it was empty. - Raw API keys are caught by shape, not just by name: any string anywhere in
a result matching a full
pfk_test_…/pfk_live_…key (id and secret half) is replaced before the result leaves the server. This backstops the field-name sweep above, which is blind to a credential that turns up somewhere nobody expected — and one did:api_key_idon payment links held whole raw keys until migration 000139 scrubbed them. A bare key id with no secret half is left alone, because that is a reference the field is meant to carry. Only the key itself is replaced, not the string around it, so an error message quoting one stays readable. - Provider response bodies are truncated, not trusted:
response_bodyon a failure event is whatever the PSP returned — usually a short JSON error, sometimes a gateway's HTML page. It is swept for keys and then cut at 2000 characters with an explicit marker, so one oversized body cannot crowd the other failures out of the result. - Idempotency: money-moving calls send an
Idempotency-Keyderived from the request itself — a SHA-256 of the method, path and canonicalised body. So a retry of the same logical operation carries the same key, whether the retry comes from this client's own backoff or from an agent re-issuing a tool call that timed out. The backend deduplicates it and no second refund is created. (Before 0.2.0 the key was a fresh UUID per invocation, which could not deduplicate anything; the backend's over-refund check was the only thing standing between a retried tool call and a double refund.) - Deduplication is visible, not silent: the flip side of stable keys is that
a deliberate repeat with identical parameters inside the backend's
idempotency window (24h by default) also collapses. When that happens the
result says
"replayed": trueand explains that no new refund was created, rather than returning the older refund as if it were new. Pass a distinctidempotency_keyto force a genuinely separate second refund. - Bounded work: each request gets a 30s deadline and the whole call, retries
included, gets 60s. Retries are capped at 3 attempts with jittered backoff,
honour
Retry-After, and only ever replay a GET or a request carrying an idempotency key. - Amounts are minor units everywhere (GHS 50.00 =
5000).
Streamable HTTP mode
For remote/agent-platform use, run the same server over streamable HTTP (stateless; each request gets a fresh server, no session leakage):
REEVIT_API_KEY=pfk_test_... \
REEVIT_MCP_HTTP_TOKEN="$(openssl rand -hex 32)" \
npx -y @reevit/mcp --http --port 8788
# endpoint: http://127.0.0.1:8788/mcp
# every request needs: Authorization: Bearer $REEVIT_MCP_HTTP_TOKENREEVIT_MCP_HTTP_TOKEN is required — the server refuses to start without
it. Anyone who can reach this endpoint can drive every tool with your key,
including reevit_create_refund, so it is not an optional hardening step.
Three guards apply in HTTP mode:
- Loopback bind. It listens on
127.0.0.1, not0.0.0.0, so it is not reachable from the rest of your network by default. Override withhostonly behind a boundary you control. - Bearer token. Compared in constant time; a missing or wrong token is a
401. - DNS-rebinding guard. Requests whose
Hostis not a loopback name get a403, so a website you visit cannot script your local server.
Bodies over 1 MB are rejected with 413.
For local-only experiments you can pass --allow-insecure-no-auth to skip the
token. It logs a warning and should never be used anywhere but your own
machine.
Development
npm install
npm test # vitest — gate + request-shaping tests
npm run build # tsc → dist/
REEVIT_API_KEY=pfk_test_... npm run dev