Pushlanedocs
Documentation menu

AI-agent install (MCP)

Let a coding agent wire up Pushlane over the MCP server.

#What the MCP server is

The Pushlane MCP server runs on the ingest worker at POST /mcp. It is a Streamable-HTTP MCP server (stateless, JSON-RPC 2.0) that your coding agent — Claude Code, Cursor, or any MCP-compatible client — connects to with a single URL and your publishable write-key.

Note
There is no npm package to install. No npx, no node, no version to manage. The agent connects directly to the worker URL with an Authorization header.

The tenant is derived server-side from the key, so a key can only ever read its own tenant's data. Every request is stateless — the key is sent on every call.

The server advertises MCP protocol version 2025-06-18 and reflects the client's version back when the client pins one during the initialize handshake.

#Connect in Claude Code

Add the MCP server to your project or global Claude Code settings. The URL and key are available in the dashboard under Settings › API.

json
// .claude/settings.json (or ~/.claude/settings.json for global)
{
  "mcpServers": {
    "pushlane": {
      "type": "http",
      "url": "https://<YOUR_WORKER>/mcp",
      "headers": {
        "Authorization": "Bearer lpk_live_…"
      }
    }
  }
}

After saving, reload Claude Code. You can ask it to call verify_integration to confirm the connection is live.

#Connect in Cursor

json
// .cursor/mcp.json
{
  "mcpServers": {
    "pushlane": {
      "type": "http",
      "url": "https://<YOUR_WORKER>/mcp",
      "headers": {
        "Authorization": "Bearer lpk_live_…"
      }
    }
  }
}

Restart Cursor after editing .cursor/mcp.json. The Pushlane tools appear in the tool list automatically.

#Available tools

The read-only tools take zero arguments; the platform, instrumentation, and replication tools take a small argument object. The tenant is always derived from your key — an argument can never name another tenant.

ToolArgumentsWhat it returns
get_setup_infoThe provisioned wiring: ingest URL, tenant id, the RevenueCat id contract, and the read endpoint URLs. Call this first to ground Pushlane.configure(…) in generated code.
detect_platform{ signals[] }Classifies the app's platform (ios / android / react-native / expo / flutter / web) from file/dependency signals you pass (Package.swift, package.json deps, app.json, pubspec.yaml, build.gradle…). Returns { platform, confidence, rationale }.
get_install_instructions{ platform }The per-platform install: iOS = the Swift SDK (SwiftPM); Expo / React Native / Flutter / Android = a zero-dependency drop-in file it returns. NEVER an unpublished package (no expo-pushlane / @pushlane/react-native).
get_setup_playbookThe ONE ordered, idempotent plan to set Pushlane up end-to-end: install the drop-in (wrapped in LOOP-MCP markers so a re-run removes it cleanly), instrument events everywhere, wire payments via RevenueCat (not client code), and replicate existing notifications. Call this first.
get_recommended_eventsPushlane's recommended high-signal event taxonomy with typed properties. Events marked auto:true are emitted by the SDK / drop-in start() — the agent must not re-instrument them.
plan_instrumentation{ analyticsProvider?, existingEvents? }Advisory (read-only text): which existing analytics events to mirror into Pushlane.track vs instrument fresh, which reserved names to rename (message_sent → …), and which auto-emitted names to skip. Reconciles against your catalogue.
get_catalogueThe tenant's live type catalogue: declared events, their owned properties, and user attributes. Used to ground property types and see what is already instrumented.
submit_notification_brief{ appName, platform, briefs[], replaceExisting? }Hands the app's EXISTING push/notification logic to Pushlane to replicate as real flows. Persists the brief and kicks replication in the background. Returns { jobId, detected, status:'queued' } — HONESTLY queued, never 'live' synchronously. Pass replaceExisting:true on a re-run so Pushlane archives the flows it replicated before (a clean re-install, never duplicates).
get_replication_status{ jobId }Polls a replication job: { status, detected, results, live, safeToRemove }. Remove the app's LOCAL notification code only for flows that are live AND fullyReplicated (safeToRemove) — a partial flow keeps its local code.
verify_integrationLive proof the SDK is sending events: distinct/total event names, last event time, and connected device counts. sdkDetected:true once any event or device registration exists.

#Multi-language install (detect → install)

The agent does not need to guess your platform. It calls detect_platform with the signals it sees in your repo, then get_install_instructions(platform) to get the exact, platform-correct install.

Heads up
There is no npm / pub.dev / Maven package for the non-iOS SDKs yet. get_install_instructions returns a zero-dependency drop-in client the agent pastes into one file (using the push-token library your app already has) — it never tells the agent to npm install expo-pushlane or @pushlane/react-native (both are 404s). iOS is the exception: it returns the real Swift SDK (SwiftPM), a git URL, not a package registry.
json
// get_install_instructions { "platform": "expo" } response (illustrative)
{
  "platform": "expo",
  "dropinFilename": "lib/pushlane.ts",
  "dropinSource": "// Pushlane — zero-dependency drop-in client for Expo…",
  "doNotUse": [
    { "pkg": "expo-pushlane", "reason": "not published on npm (404)" }
  ],
  "steps": [
    "Paste dropinSource into lib/pushlane.ts (no package to install).",
    "Pushlane.configure({ tenantId, publishableKey }); Pushlane.identify(userId).",
    "Pushlane.start(); await Pushlane.registerForPush();"
  ],
  "eventWiring": "Mirror analytics.track into Pushlane.track; skip app_open/session_started."
}

// For "ios" the tool returns the Swift SDK (SwiftPM) path, dropinFilename: null.

Pushlane.track works over HTTP on every platform. Pushlane's sender is APNs, so end-to-end push delivery is proven on iOS; Android / Flutter register an FCM token and their events flow immediately.

#plan_instrumentation — mirror your analytics

Before writing any Pushlane.track calls, the agent calls plan_instrumentation (optionally passing your analytics provider and the event names it found). The tool returns advisory text that keeps instrumentation honest and additive:

  • Mirror, don't replace — add a Pushlane.track alongside each existing analytics.track / logEvent call; keep your current analytics.
  • Rename reserved names — a name Pushlane writes server-side (e.g. message_sent, the delivery-telemetry name) can never trigger a flow, so a chat app's own message_sent must become chat_message_sent.
  • Skip auto-emitted namesapp_open and session_started are emitted by the SDK / drop-in start(); mirroring an “App Opened” from your analytics would double-count.
  • Reconcile against the catalogue — reuse existing event names and property types (via get_catalogue) instead of inventing new ones.

#Replicate your existing notifications

If your app already sends push notifications from its own code (a hand-rolled scheduler, a competitor SDK, cron jobs), the agent can hand that logic to Pushlane to rebuild as real flows — so you can delete the local code. The flow is deliberately honest and asynchronous:

  1. The agent captures each existing notification as a structured brief and calls submit_notification_brief.
  2. Pushlane validates and persists the brief and returns { jobId, detected, status: 'queued' } — never “live” on the spot (the worker cannot run the flow generator; replication happens in the Pushlane web app).
  3. Pushlane replicates the briefs into real flows (verbatim copy, so your exact wording is preserved) and notifies the founder in the dashboard.
  4. The agent polls get_replication_status(jobId) and removes the app's local notification code only for flows it reports as live. A brief whose trigger event has never been recorded comes back skipped, not live.
json
// get_replication_status { "jobId": "…" } response
{
  "status": "done",           // queued | processing | done | failed
  "detected": 3,              // valid briefs submitted
  "live": 2,                  // flows that actually activated — delete ONLY these locally
  "results": [
    { "sourceName": "Trial ending reminder", "flowId": "flw_…", "status": "live" },
    { "sourceName": "Win-back after cancel", "flowId": "flw_…", "status": "live" },
    { "sourceName": "Daily streak nudge", "flowId": null, "status": "skipped",
      "reason": "trigger event 'streak_tick' has never been recorded" }
  ]
}
Heads up
Never delete working notification code on the promise of replication. Delete only what get_replication_status confirms is live and fullyReplicated (see safeToRemove) — the count is real, derived from flows that actually activated.

#Full setup & clean re-run (get_setup_playbook)

get_setup_playbook returns the one ordered plan the agent follows to wire Pushlane end-to-end — and to do it idempotently. Every bit of Pushlane code it adds is wrapped in // LOOP-MCP:BEGIN … // LOOP-MCP:END markers and kept in one managed client file, so re-running the MCP removes its prior work and re-applies from scratch — no stale SDK, no half-migrations. On a re-run the agent also calls submit_notification_brief with replaceExisting:true, which archives the flows Pushlane replicated before so you never get duplicates.

Events everywhere. The playbook instruments broadly: growth events (signup_completed, onboarding_completed, paywall_viewed) plus the app's own recurring actions — the reason people open it (a daily check-in, a mood log, a streak…), renamed to fit your domain.

Note
Payments come from RevenueCat, not client code. Don't instrument trial/purchase/cancel in the app — connect RevenueCat once in Settings → Integrations → RevenueCat and those arrive automatically as revenuecat.* events (trial started, purchase, renewal, cancellation), resolved to the user by the same id you pass to Pushlane.identify and RevenueCat.logIn. They work as flow triggers.

#get_setup_info

Call this first. It gives the agent everything it needs to write the correct Pushlane.configure(…) call, including the ingest URL, tenant UUID, and the RevenueCat user-id contract string.

json
// get_setup_info response (structuredContent)
{
  "ingestUrl":  "https://<YOUR_WORKER>",
  "tenantId":   "YOUR_TENANT_UUID",
  "keyConfigured": true,
  "revenueCatContract": "Pass the SAME stable user id to BOTH Pushlane.identify(<id>) and …",
  "endpoints": {
    "catalogue":     "https://<YOUR_WORKER>/v1/catalogue",
    "installStatus": "https://<YOUR_WORKER>/v1/install-status"
  }
}

Returns the recommended event taxonomy. Use it to decide which events to instrument. The taxonomy includes event names, when to fire them, and their typed properties. Events with auto: true are emitted automatically by the SDK (or the drop-in's start()) — do not instrument them manually.

EventautoWhen
app_openYesCold launch — emitted by the SDK / drop-in start().
session_startedYesForeground return after ≥30 s background.
signup_completedNoAccount created. Optional property: method (string).
onboarding_completedNoFirst-run onboarding finished. Optional: duration_sec (number).
paywall_viewedNoA paywall is shown. Required: placement (string). Optional: paywall_id.
trial_startedNoFree trial began. Required: plan (string). Optional: product_id.
subscription_startedNoFirst paid period. Required: plan. Optional: product_id, price_usd (number).
subscription_renewedNoPaid period renewed. Required: plan.
subscription_cancelledNoChurn signal. Optional: plan, reason.
lesson_finishedNoYour app's core action — rename to match (workout_finished, recipe_saved, …).

#get_catalogue

Returns the live catalogue. The catalogue fills itself as events arrive — events and properties that have never been declared appear with declared: false. The agent should reconcile every event name and property type it generates against this catalogue to avoid type drift.

json
// get_catalogue response (structuredContent)
{
  "ok": true,
  "tenantId": "YOUR_TENANT_UUID",
  "events": [
    { "name": "paywall_viewed", "declared": true, "volume30d": 840, "lastSeenAt": 1751400000000 },
    { "name": "app_open",       "declared": false, "volume30d": 12000 }
  ],
  "properties": [
    { "name": "placement", "kind": "event_property", "type": "string", "eventName": "paywall_viewed" },
    { "name": "app_version","kind": "user_attribute",  "type": "string", "eventName": null }
  ]
}

#verify_integration

Call this after the agent has added Pushlane SDK instrumentation and the app has been run at least once. It queries ClickHouse and Supabase in parallel and reports each source's status independently.

json
// Your agent can run this immediately after connecting:
// tools/call  verify_integration  {}
// Expected shape:
{
  "ok": true,
  "sdkDetected": true,
  "totalEvents": 142,
  "distinctEventNames": 6,
  "eventNames": ["app_open", "session_started", "paywall_viewed", "..."],
  "devicesConnected": 3,
  "activeDevices": 3,
  "sources": {
    "clickhouse": { "ok": true, "error": null },
    "supabase":   { "ok": true, "error": null }
  }
}

Poll this tool until sdkDetected: true. The agent can automate this: generate the instrumentation code, ask the developer to build and run the app, then call verify_integration to confirm events are flowing.

#Protocol notes

For debugging or integration testing outside an MCP client, you can call the endpoint directly with raw JSON-RPC:

bash
// Raw JSON-RPC 2.0 call (for debugging or non-MCP clients)
curl https://<YOUR_WORKER>/mcp \
  -H "Authorization: Bearer lpk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"verify_integration","arguments":{}}}'

The server also handles batch JSON-RPC arrays. The initialize, ping, and tools/list methods are supported in addition to tools/call. Notification messages (methods starting with notifications/) are accepted and silently discarded — the server responds 202 with no body when a request consists entirely of notifications.

Heads up
The MCP endpoint (POST /mcp) always requires a valid write-key. There is no unauthenticated access. A missing or invalid key returns a JSON-RPC error with code -32001 and HTTP status 401.

Back to ← Overview