AI agents (MCP)
The platform runs a Model Context Protocol server —
workers/mcp — that exposes a fixed set of tenant-scoped tools to any MCP client (Claude Code,
another agent framework, or a hand-written client using @modelcontextprotocol/sdk). Every tool
call is a plain HTTP request to the same public /v1 API described in the
quickstart, made with your own organization API key. The MCP layer adds
zero additional privilege — an agent connected with a key that only has members:read can only
ever do what that key can already do through curl. What MCP adds is discoverability (a
self-describing tool catalog), a human-in-the-loop approval gate for the one tool that writes to
the ledger, and a full audit trail of every call.
Endpoint and transport
POST https://app.<your-domain>/mcp
Streamable HTTP only (the MCP spec’s current transport) — there is no legacy SSE endpoint. The
worker is stateless: no session is held between requests, MCP-Protocol-Version is honored, and
responses are single-JSON (no server-sent event stream to keep open). GET/OPTIONS on the same
path are also handled, per what the transport needs (CORS preflight, etc).
Authenticate every request with the same organization API key you’d use as x-api-key against
/v1 — here it’s a standard bearer token instead:
Authorization: Bearer lp_live_9f2c...
(or an lp_test_... key against a sandbox tenant). A missing or malformed key gets 401 with a
WWW-Authenticate: Bearer challenge; a well-formed but invalid/expired/revoked key gets 401 with
WWW-Authenticate: Bearer error="invalid_token". There is no OAuth flow in v1 — bearer
organization-API-key auth is spec-legal (MCP’s authorization framework is optional) and matches how
every other /v1 client authenticates.
Key setup
Create an organization-owned API key the same way the quickstart does
(POST /v1/api-keys from the dashboard session, or the dashboard’s own key-creation UI), and grant
only the permissions the tools you actually want to use require — the tools/list an agent sees is
filtered to what its key can call, so an under-scoped key just sees a smaller, honest catalog
rather than errors. To enable every tool in the table below:
{
"members": ["read"],
"transactions": ["read", "adjust"],
"reports": ["read"],
"campaigns": ["read", "manage"],
"redemptions": ["initiate"]
}
Omit transactions:adjust to keep issue_points off the key entirely (it still shows up as
unavailable rather than erroring). check_approval needs no permission beyond a valid key — it’s
scoped by construction to approvals whose originating call was made with the same key.
Connect with Claude Code
claude mcp add --transport http loyalty https://app.<your-domain>/mcp \
--header "Authorization: Bearer lp_test_..."
(loyalty is just the local name Claude Code will refer to this server by — pick anything.) Use an
lp_test_ key against a sandbox tenant while exploring; switch to lp_live_ once you’re ready to
point an agent at real data. Any other Streamable-HTTP MCP client connects the same way: POST to
the endpoint above with the same Authorization: Bearer header on every request.
Tools
Ten tools, each with a zod input schema whose field descriptions are the agent-facing contract
(visible in tools/list). Every read tool is bounded — lists are capped and identifiers are masked,
never raw contact details. “Default mode” is the out-of-the-box behavior; a merchant can change any
mutating tool’s mode per tenant in the dashboard (Settings → Agent Access) between auto,
require_approval, or disabled.
| Tool | What it does | Required permission | Default mode |
|---|---|---|---|
lookup_member |
Find members by phone, loyalty number, email, or QR/card token | members:read |
auto |
get_balance |
A member’s current point balance and tier standing | members:read |
auto |
list_transactions |
A member’s ledger history (earns, redemptions, adjustments, expiries), paginated | transactions:read |
auto |
query_analytics |
Overview KPIs, a metric time-series, or the enrollment funnel | reports:read |
auto |
list_segments |
The tenant’s audience segments (system presets + custom) | campaigns:read |
auto |
preview_segment |
Dry-run a segment definition: matching count + a sample of member ids, nothing saved | campaigns:read |
auto |
quote_redemption |
Price a reward redemption for a member — does not touch the ledger | redemptions:initiate |
auto |
create_campaign_draft |
Create a campaign as an inert status:draft row — cannot send until a human activates it in the dashboard |
campaigns:manage |
auto |
issue_points |
Writes to the ledger. Adjust a member’s balance by a signed integer amount | transactions:adjust |
require_approval |
check_approval |
Poll the status of an approval a pending issue_points call created |
(none — any valid key) | auto |
issue_points is the only tool that mutates the ledger, and it’s the only one gated by default.
create_campaign_draft also mutates (kind: mutate), but defaults to auto because its write is
inert — a draft campaign cannot send anything until a merchant reviews and activates it in the
dashboard, same as a draft created by hand.
Approval semantics
When issue_points runs in require_approval mode (the default), the framework does not
execute it. Instead it records the pending call and returns:
{
"status": "pending_approval",
"approval_id": "01J...",
"expires_at": "2026-07-21T12:00:00.000Z"
}
Approvals expire after 7 days if left undecided. A merchant reviews and decides it in the dashboard
under Settings → Agent Access: the approvals queue shows the tool, its arguments rendered
human-readably, the requesting key’s name, and its age. Approve or Deny, with an optional
note. Approving executes the underlying /v1/members/:id/adjust call as the approving merchant’s
own session — the merchant is the actor for the actual ledger write, and the approving user must
hold transactions:adjust themselves (an agent key can never launder a permission its approver
doesn’t have). Denying just marks the approval denied; nothing executes.
The agent polls for the outcome with check_approval:
{
"approval_id": "01J...",
"tool": "issue_points",
"state": "executed",
"decided_by": null,
"note": null,
"result": { "entry_id": "01J...", "replayed": false }
}
state is one of pending, approved, denied, executed, failed, or expired. result
(the underlying /v1/members/:id/adjust response) only appears once executed. decided_by and
note are always null on this key-scoped read — the approver’s identity is not exposed to the
agent key by design; the merchant sees who approved in the dashboard audit log. state and
result are the agent-legible outcome.
issue_points derives its Idempotency-Key from the originating call, so a redelivered tool
invocation that somehow ran twice collapses to a single ledger entry — two genuinely distinct calls
still produce two separate adjustments (and two separate approvals, if gated).
Audit log
Every tool call — read or write, executed or gated — writes an agent_audit row before anything
runs: which key called which tool with which arguments, and how it resolved (completed, failed,
pending_approval, or denied). Once a tool finishes, the row is updated with a bounded summary of
the result (never the full raw payload) or the error. This is the full audit trail behind the
platform’s MCP exit criterion — a merchant can see, filter, and page through it in the dashboard
under Settings → Agent Access.
Rate limits and errors
Calls are rate-limited per API key (a generous fixed window, 300 requests/60s by default) — an
exceeded limit returns 429 with Retry-After. A disabled tool (a merchant set it to disabled
for the tenant) never appears in tools/list for that tenant, and a direct call to it fails with a
tool_disabled error rather than executing. A tool’s own failure (e.g. the underlying /v1 call
rejects with a 422) is passed through to the agent as legitimate feedback — a structured
{code, message} error, never a raw stack trace or infra detail.
v1 limits
- Streamable HTTP only — no legacy SSE transport.
- No OAuth 2.1 flow — authentication is a bearer organization API key, the same key you’d use
anywhere else against
/v1. There is no per-agent, per-session token issuance in v1. - Stateless — no session state is held on the worker between calls; every request re-verifies the bearer key.
- The MCP worker never calls a model itself — it has no AI/LLM component of its own. The agent on the other end of the connection (Claude Code or otherwise) brings its own model; this server only exposes tools.
Next steps
- Quickstart — create an API key and see the same
/v1endpoints these tools call, directly withcurl. - API reference — the full schemas for
/v1/members/:id/adjust,/v1/agent-approvals,/v1/agent-audit, and/v1/mcp-settings.