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.
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.
// .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
// .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.
| Tool | Arguments | What it returns |
|---|---|---|
get_setup_info | — | The 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_playbook | — | The 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_events | — | Pushlane'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_catalogue | — | The 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_integration | — | Live 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.
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.// 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.trackalongside each existinganalytics.track/logEventcall; 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 ownmessage_sentmust becomechat_message_sent. - Skip auto-emitted names —
app_openandsession_startedare emitted by the SDK / drop-instart(); 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:
- The agent captures each existing notification as a structured brief and calls
submit_notification_brief. - 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). - Pushlane replicates the briefs into real flows (verbatim copy, so your exact wording is preserved) and notifies the founder in the dashboard.
- The agent polls
get_replication_status(jobId)and removes the app's local notification code only for flows it reports aslive. A brief whose trigger event has never been recorded comes backskipped, not live.
// 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" }
]
}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.
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.
// 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"
}
}#get_recommended_events
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.
| Event | auto | When |
|---|---|---|
app_open | Yes | Cold launch — emitted by the SDK / drop-in start(). |
session_started | Yes | Foreground return after ≥30 s background. |
signup_completed | No | Account created. Optional property: method (string). |
onboarding_completed | No | First-run onboarding finished. Optional: duration_sec (number). |
paywall_viewed | No | A paywall is shown. Required: placement (string). Optional: paywall_id. |
trial_started | No | Free trial began. Required: plan (string). Optional: product_id. |
subscription_started | No | First paid period. Required: plan. Optional: product_id, price_usd (number). |
subscription_renewed | No | Paid period renewed. Required: plan. |
subscription_cancelled | No | Churn signal. Optional: plan, reason. |
lesson_finished | No | Your 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.
// 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.
// 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:
// 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.
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