LVNDRby Toolfora

HelpMCP

MCP

The endpoint

ItemValue
URLPOST https://lvndr.toolfora.com/mcp
AuthOAuth 2.1 (the client signs in through your Shopify admin), or Authorization: Bearer lv_… with a key from Settings > Agents & API
TransportStreamable HTTP, JSON responses, stateless. No sessions, no SSE stream. Each request stands alone.
Protocol versions2026-07-28 (no handshake; send the version in _meta), and 2025-11-25, 2025-06-18, 2025-03-26 (initialize first)
Methodsinitialize, ping, server/discover, tools/list, tools/call, resources/list, resources/templates/list, resources/read, prompts/list, prompts/get; notifications are accepted with 202
PlanIncluded on every LVNDR plan and free on development stores, as for the REST API.

GET and DELETE answer 405: there is no stream to open and no session to close. JSON-RPC batches are refused; send one message per POST. Browser origins must be https or localhost.

A public description of the server, with tool names, is at /.well-known/mcp.json. The agent kit's server.json lists the server in the MCP registry as io.github.kasazen/lvndr-agent.

Connect with OAuth (Claude, Cursor)

A client that supports MCP authorization needs only the URL. No key to copy.

  • Claude (claude.ai and Claude Desktop): add a custom connector with the URL https://lvndr.toolfora.com/mcp.
  • Claude Code: claude mcp add -t http lvndr https://lvndr.toolfora.com/mcp, then /mcp and authenticate.
  • Cursor: add the server with only a url in mcp.json; Cursor offers to log in.

What happens next:

  • The first request without a token gets 401 with a WWW-Authenticate header pointing at /.well-known/oauth-protected-resource. The client finds the authorization server there.
  • Your browser opens LVNDR's connect page. Enter your store's myshopify.com address.
  • LVNDR sends you into your Shopify admin, where the consent screen opens inside LVNDR. It names the client, the store and each scope it asks for. Shopify proves who you are; LVNDR needs no separate login.
  • Approve, and the browser goes back to the client with a one-time code (valid 10 minutes). The client exchanges it for a token.

The access token is an LVNDR key of its own, listed in Settings > Agents & API as “Connected via OAuth” with the client's name. It lasts one hour. The client refreshes it with a refresh token that lasts 30 days and is replaced on every use; presenting an old refresh token again revokes the whole connection. Revoke the key in Settings to cut the client off.

A client that names no scope gets read undo; it can ask for rollback and export as well, and the consent screen lists each one. A refresh can never widen it. PKCE (S256) is required. Clients identify themselves with a Client ID Metadata Document URL or by registering at /oauth/register. Metadata: /.well-known/oauth-authorization-server.

The store must have LVNDR installed with an active plan, or the consent screen says so and cannot be approved. Uninstalling LVNDR revokes every OAuth connection.

Connect with a key

Make a key in Settings > Agents & API. Pick the client and the snippet appears with the key filled in. The same snippets, with a placeholder, are on the developer page. Replace lv_YOUR_KEY, or export LVNDR_API_KEY where the snippet reads it.

Claude Code

claude mcp add -t http -H "Authorization: Bearer lv_YOUR_KEY" lvndr https://lvndr.toolfora.com/mcp

Or commit a project .mcp.json; Claude Code fills ${LVNDR_API_KEY} from the shell:

{
  "mcpServers": {
    "lvndr": {
      "type": "http",
      "url": "https://lvndr.toolfora.com/mcp",
      "headers": {
        "Authorization": "Bearer ${LVNDR_API_KEY}"
      }
    }
  }
}

The LVNDR plugin for Claude Code bundles this server and the LVNDR skill and reads LVNDR_API_KEY the same way.

Cursor

.cursor/mcp.json (project) or ~/.cursor/mcp.json (global). The “Add to Cursor” link in Settings and on the developer page installs the same server reading LVNDR_API_KEY from the environment.

{
  "mcpServers": {
    "lvndr": {
      "url": "https://lvndr.toolfora.com/mcp",
      "headers": {
        "Authorization": "Bearer lv_YOUR_KEY"
      }
    }
  }
}

VS Code

.vscode/mcp.json. VS Code asks for the key the first time the server starts.

{
  "inputs": [
    {
      "type": "promptString",
      "id": "lvndr-key",
      "description": "LVNDR API key",
      "password": true
    }
  ],
  "servers": {
    "lvndr": {
      "type": "http",
      "url": "https://lvndr.toolfora.com/mcp",
      "headers": {
        "Authorization": "Bearer ${input:lvndr-key}"
      }
    }
  }
}

Codex

~/.codex/config.toml, with LVNDR_API_KEY exported before Codex starts:

[mcp_servers.lvndr]
url = "https://lvndr.toolfora.com/mcp"
bearer_token_env_var = "LVNDR_API_KEY"

Claude Desktop

claude_desktop_config.json (Settings > Developer > Edit Config), through mcp-remote. Restart Claude Desktop afterwards.

{
  "mcpServers": {
    "lvndr": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://lvndr.toolfora.com/mcp",
        "--header",
        "Authorization: Bearer lv_YOUR_KEY"
      ]
    }
  }
}

By hand

curl -X POST https://lvndr.toolfora.com/mcp \
  -H "Authorization: Bearer $LVNDR_API_KEY" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

curl -X POST https://lvndr.toolfora.com/mcp \
  -H "Authorization: Bearer $LVNDR_API_KEY" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"lvndr_search_changes","arguments":{"limit":5}}}'

Tool results carry the JSON both as text and as structuredContent, and every tool declares an output schema.

Read tools

All need the read scope and count against the read limit. Annotations: read-only, idempotent.

ToolArgumentsUse it for
lvndr_statusnonePlan, tracked counts, last activity check. Call first to confirm which store the key belongs to.
lvndr_search_changesq, type, action, actor, since, until (ISO date-time), hideUndos, runId, resourceId (gid or Product-123), limit (1 to 100), cursor“What changed”, finding the change id to undo, one record's changes, or with runId what your run did
lvndr_get_changeidOne change with its full before/after diff
lvndr_record_historyresourceId (gid or Product-123), cursorHow one record got to where it is: latest state, versions, changes
lvndr_get_batchanchorChangeIdEverything the same actor did around a change, no gap over ten minutes
lvndr_list_restore_pointsnoneMoments the store can be rolled back to
lvndr_preview_undoid, includeConflictsWhat undoing one change would write, field by field. Returns a confirmation_token for lvndr_undo.
lvndr_preview_undo_runid, reason, runIdWhat undoing a whole run would do (the newest 50 planned). Returns a confirmation_token for lvndr_undo_run.
lvndr_preview_undo_batchanchorChangeId, reason, runIdWhat undoing one actor's batch would do (the newest 50 planned). Returns a confirmation_token for lvndr_undo_batch.
lvndr_preview_rollbackpointIdWhat rolling back to a point would undo (the first 50). Returns a confirmation_token for lvndr_rollback.
lvndr_list_alertsstatus (open, dismissed, resolved, all), limitBursts of changes by one actor. resolved: closed because the batch, run or rollback was undone.
lvndr_get_runid, limit (1 to 200), cursorReviewing what a run changed. attributedThrough and pendingDeliveries say whether LVNDR has caught up with your last write.
lvndr_list_runslimit, openOnlyFinding a run id, or runs left open
lvndr_get_jobidProgress of a run undo, batch undo or rollback: status (queued, running, done, partial, failed), counts and one result per change. Poll every few seconds until it finishes.

Write tools

All count against the write limit.

ToolScopeAnnotationsArgumentsUse it for
lvndr_mark_restore_pointreadwrites to LVNDR only, not destructive, idempotentlabel, reason, runIdMarking a moment before a bulk job. The same label from the same key within a minute returns the same point.
lvndr_start_runreadwrites to LVNDR only, not destructivetask (required), agent, clientId, markRestorePoint (default true)Before you change the store with your Shopify tools. Marks a restore point and tags changes until lvndr_end_run; capture says which (below).
lvndr_end_runreadwrites to LVNDR only, not destructive, idempotentidClosing your run when the work is done
lvndr_undoundowrites to the store, destructive, not idempotentid, confirmation_token (both required), includeConflicts (must match the preview), reason, runIdPutting one change back
lvndr_undo_runundowrites to the store, destructive, not idempotentid, confirmation_token (both required), reason, runIdUndoing everything a run changed, newest first. Returns a job.
lvndr_undo_batchundowrites to the store, destructive, not idempotentanchorChangeId, confirmation_token (both required), reason, runIdUndoing one actor's burst, newest first. Returns a job.
lvndr_rollbackrollbackwrites to the store, destructive, not idempotentpointId, confirmation_token (both required), reason, runIdUndoing everything after a restore point, newest first. Returns a job.
lvndr_dismiss_alertundowrites to LVNDR only, not destructive, idempotentidMarking an alert as seen once it is explained or undone (needs Undo: a read-only key cannot silence its own alert)

Every tool also sets openWorldHint: false. A reason (up to 500 characters) is stored with the undo and shown to the merchant. A runId groups the write with your current run.

Runs: capture and freshness

lvndr_start_run answers capture and a one-line captureNote. With a key that has the undo scope the run is all-unattributed: while open (up to 24 hours) it takes every change Shopify leaves unattributed or only inferred, staff edits included. An OAuth token without undo gets client (only changes linked to its own OAuth client), a Settings key without undo gets none (nothing is tagged; review with lvndr_search_changes by since). A clientId argument is used only when it is the key's own OAuth client id.

Changes are tagged within about 4 minutes; collection rules and SEO, menus, themes and metafield definitions come from a 10-minute check and can take up to 10. Image alt text waits for the daily sweep. After your last write, call lvndr_get_run about every 20 seconds until attributedThrough is after that write and pendingDeliveries is 0. Only then review the run.

A run that collects nothing (capture: none) also answers a top-level warning; show it to the merchant. Many changes carry actor.tier: unknown: Shopify names no actor for field edits, so treat unknown as “no evidence”, not “nobody”. LVNDR's own undos are labelled lvndr undo as soon as they arrive and never join a run or batch.

The confirmation token

A tool that writes to the store needs a token from a preview, whatever kind of key calls it. This makes an agent show the plan before it writes, and stops it writing something nobody looked at. There is no other way to confirm a write.

PreviewThen
lvndr_preview_undolvndr_undo with the token
lvndr_preview_undo_runlvndr_undo_run with the token
lvndr_preview_undo_batchlvndr_undo_batch with the token
lvndr_preview_rollbacklvndr_rollback with the token
  • The token is valid for 10 minutes and works once.
  • It is bound to the key, the store and the target: one change, run, batch or restore point.
  • It is bound to what the preview saw: the field plan for one change, the set of changes for a run, batch or rollback. If the store moved or new changes arrived, the write is refused and the agent previews again.
  • A preview with nothing to write returns confirmation_token: null.
  • A missing, expired, reused or mismatched token comes back as a tool error that names the preview to call.
→ tools/call lvndr_preview_undo {"id": "cm3abc"}
← { "plan": { "counts": { "ready": 1, ... }, "items": [ ... ] },
    "confirmation_token": "v1.eyJr...", "expiresAt": "2026-11-13T10:15:00.000Z" }

   (show the merchant the plan; they say yes)

→ tools/call lvndr_undo {"id": "cm3abc", "confirmation_token": "v1.eyJr...", "reason": "wrong price"}
← { "plan": { ... }, "run": { "id": "...", "status": "done", "ops": [ ... ] } }

Background jobs

lvndr_undo_run, lvndr_undo_batch and lvndr_rollback do not write while the call is open. They queue a job and return it with status: "queued". Poll lvndr_get_job with its id every few seconds until the status is done, partial or failed, then tell the merchant the outcome.

→ tools/call lvndr_preview_undo_run {"id": "cm9run"}
← { "runId": "cm9run", "counts": { "planned": 40, ... }, "results": [ ... ], "total": 40, "more": 0,
    "confirmation_token": "v1.eyJr...", "expiresAt": "..." }

→ tools/call lvndr_undo_run {"id": "cm9run", "confirmation_token": "v1.eyJr...", "reason": "wrong collection"}
← { "runId": "cm9run", "job": { "id": "cm9job...", "status": "queued", "total": 40, ... }, "remaining": 0 }

→ tools/call lvndr_get_job {"id": "cm9job..."}
← { "job": { "status": "running", "done": 12, "failed": 0, "skipped": 1, "total": 40, ... } }
   ...
← { "job": { "status": "done", "done": 39, "failed": 0, "skipped": 1, "remaining": 0, "results": [ ... ] } }

A preview plans the newest 50 changes and reports total and more. One job covers up to 500 changes; when the finished job's remaining is above 0, preview and run again. If the set of changes moved after the token was issued, the job ends failed with an error that starts with plan_changed.

Resources and prompts

ResourceContent
lvndr://restore-pointsThe newest 50 restore points
lvndr://runs/{id}One run with its changes. resources/list shows the 10 newest runs.
lvndr://changes/{id}One change with its diff (template)
PromptArgumentWhat it asks the model to do
review_runrunIdRead the run, summarise by record, flag deletes, prices, inventory and publication, and preview an undo only with your yes
investigate_alertalertIdFind the burst, say who and what, and preview a batch undo only with your yes

Limits and errors

Tool calls share the key's REST limits, which follow the plan (reads and writes per minute: Starter 60 and 10, Growth 120 and 20, Scale 300 and 60, development stores 120 and 20 once billing is on). During early access, before managed billing, every store, development stores included, gets the top rate, 300 and 60. Which bucket a tool uses is in the tables above. See REST API.

HTTPJSON-RPC codeWhen
401-32001No token, or an invalid or expired one. WWW-Authenticate points at the OAuth metadata.
402-32002The store has no active LVNDR plan
403-32003The app is not installed, or an OAuth token lacks the tool's scope (WWW-Authenticate: insufficient_scope, so the client can ask for it)
429-32004Rate limited: an OAuth token on tools/call, any key on the other methods, or too many failed token lookups from one address. Retry-After says how long.
413-32600The body is over 64 KB (data.code payload_too_large)
400-32700The body is not JSON
400-32600Not a JSON-RPC request, a batch, or a refused Origin (403)
400-32022Unsupported protocol version; data lists the supported ones
400-32020MCP-Protocol-Version, Mcp-Method or Mcp-Name header does not match the body
200-32602Unknown tool or prompt, or arguments that are not an object (params that are not an object: 400)
200-32602 or -32002resources/read of an unknown URI (-32002 before 2026-07-28)
200 or 404-32601Unknown method (404 on 2026-07-28)

A tool that fails for a reason the model can fix (a bad argument, a stale token, an unknown id) returns a normal result with isError: true and a sentence saying what to do. For a key made in Settings, a missing scope and a rate limit come back the same way, as text the model can relay: “This key lacks the undo scope; create a key with Undo in LVNDR Settings > Agents & API” and “Rate limited; retry in N s”. Every other method (resources, prompts, initialize, ping) also counts against the read limit.

The agent name stored on writes is the client's name (from clientInfo), else the OAuth client's name, else the key's label.