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

Next steps