MCP server
Gives Claude (and any MCP client) the same capabilities as the CLI — reads, onboarding, and human-relayed queue decisions. Runs over stdio; authenticates with an account API key.
Install
Claude Code:
claude mcp add funnelkeeper -e FUNNELKEEPER_API_KEY=fk_live_… -- npx -y funnelkeeper funnelkeeper-mcp
Project .mcp.json:
{
"mcpServers": {
"funnelkeeper": {
"command": "npx",
"args": ["-y", "funnelkeeper", "funnelkeeper-mcp"],
"env": { "FUNNELKEEPER_API_KEY": "fk_live_…" }
}
}
}
Optional: FUNNELKEEPER_API_URL to point at a non-production API.
Tools
| Tool | Kind | What it does |
|---|---|---|
get_portfolio | read | One row per product: spend 7d/30d, visitors, leads, revenue, customers, CAC, LTV:CAC, gate status, pending card count. |
get_product | read | A single product’s portfolio row. |
get_funnel | read | Funnel stage × channel volumes for a product. |
get_payback | read | Cohort payback curves ({cohorts, cac}): cumulative revenue per customer by days since acquisition, per channel, plus per-channel CAC. |
get_queue | read | Pending Keeper cards (proposals, insights, alerts). Cards are resolved only by a human decision. |
get_health | read | Pipeline health: job runs, per-source data freshness, queue backlog, unattributed revenue share. |
get_connections | read | Integration inventory: each source’s status, config, last sync, last error. |
create_product | write | Create a product in the authenticated account. |
connect_source_start | write | Begin connecting ga4, gtm, or google_ads. Returns an authorization URL — give it to the human to open in their browser (the model cannot browse), then poll connect_source_status with the returned state. |
connect_source_status | read | Poll an in-progress Google connection. pending → keep waiting; complete → results plus entity options (properties/containers) for connect_source_select; error → what went wrong. |
connect_source_select | write | Finish a Google connection by choosing which GA4 property / GTM container / Ads customer to use, from the options returned by connect_source_status. |
set_semrush_key | write | Connect SEMrush by storing the customer’s own API key (encrypted at rest). No test call is made — SEMrush calls burn the customer’s units. |
test_connection | write | Verify a connected source can deliver data (cheap, side-effect-free probe). |
get_spend | read | Daily spend by channel and campaign for a product, with the daily cap and whether it is currently binding. |
log_distribution | write | Record a distribution event you performed for the user — a post shipped, a listing submitted, an email sent — so its traffic joins back via utm_campaign. |
propose_budget_change | write | budget.propose: suggest a spend change with rationale and rollback condition. Returns pending_human — a HUMAN approves it in the app; you cannot execute it. Policy caps are enforced server-side: over-cap proposals are rejected with policy_code. |
get_attribution | read | Revenue by channel under first-touch AND last-touch models, with spend, customers, CAC and ROAS per channel, plus attribution coverage. Unattributed revenue is its own row — never smeared. |
get_timeseries | read | Daily spend, revenue, visits, leads and new customers for a product — zero-filled, oldest first. |
get_health_report | read | Scored audit of the product (0–100): site & tracking, funnel, advertising, SEO & social. Every failing check carries the action that fixes it. |
get_history | read | The audit timeline: every change and decision — Keeper cards raised, human approvals (resolved_by), policy blocks, config edits — newest first. |
get_funnel_definition | read | The product’s funnel definition (ordered, labelled steps) plus per-stage tracking status: recent volume and which sources feed each canonical stage. |
set_funnel_definition | write | Save the product’s funnel steps. Stages must come from the canonical taxonomy (impression, visit, engaged, lead, qualified, signup, activated, converted, payment, churned); labels are free text. Audited. |
list_dashboards | read | The product’s saved dashboards (name + widget spec). The built-in Overview is not stored. |
create_dashboard | write | Build a dashboard for the user. Pass a full spec ({widgets:[…]}, schema DashboardSpec in /openapi.json — widget types: metric, timeseries, funnel, payback, queue, breakdown, note; each widget may carry layout {x,y,w,h} on a 12-column × 32px-row grid, or omit it to auto-pack in order) to compose it exactly, or just a prompt to let the server’s deterministic composer draft it. Users can then rearrange everything by drag and drop. |
update_dashboard | write | Rename a dashboard or replace its widget spec (validated server-side). Audited. |
approve_card | write, human-gated | HUMAN-GATED: approve a pending Keeper card. Approving a proposal can change ad spend. Only call this to relay an explicit decision the human just made — never decide for them. The server’s policy engine still enforces caps and walls. |
reject_card | write, human-gated | HUMAN-GATED: dismiss a pending Keeper card, relaying the human’s explicit decision. |
snooze_card | write, human-gated | HUMAN-GATED: snooze a pending Keeper card for the human. |
Input schemas
get_portfolio
{
"type": "object",
"properties": {},
"additionalProperties": false
}
get_product
{
"type": "object",
"properties": {
"slug": {
"type": "string",
"description": "Product slug, e.g. 'demo-product'."
}
},
"required": [
"slug"
],
"additionalProperties": false
}
get_funnel
{
"type": "object",
"properties": {
"slug": {
"type": "string",
"description": "Product slug, e.g. 'demo-product'."
},
"days": {
"type": "integer",
"exclusiveMinimum": true,
"minimum": 0,
"maximum": 365,
"default": 30
}
},
"required": [
"slug"
],
"additionalProperties": false
}
get_payback
{
"type": "object",
"properties": {
"slug": {
"type": "string",
"description": "Product slug, e.g. 'demo-product'."
}
},
"required": [
"slug"
],
"additionalProperties": false
}
get_queue
{
"type": "object",
"properties": {},
"additionalProperties": false
}
get_health
{
"type": "object",
"properties": {},
"additionalProperties": false
}
get_connections
{
"type": "object",
"properties": {},
"additionalProperties": false
}
create_product
{
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"maxLength": 120
},
"slug": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9-]*[a-z0-9]$"
},
"currency": {
"type": "string",
"minLength": 3,
"maxLength": 3,
"default": "USD"
},
"domain": {
"type": "string"
}
},
"required": [
"name",
"slug"
],
"additionalProperties": false
}
connect_source_start
{
"type": "object",
"properties": {
"product_slug": {
"type": "string",
"description": "Product slug, e.g. 'demo-product'."
},
"kind": {
"type": "string",
"enum": [
"ga4",
"gtm",
"google_ads"
]
}
},
"required": [
"product_slug",
"kind"
],
"additionalProperties": false
}
connect_source_status
{
"type": "object",
"properties": {
"state": {
"type": "string"
}
},
"required": [
"state"
],
"additionalProperties": false
}
connect_source_select
{
"type": "object",
"properties": {
"state": {
"type": "string"
},
"ga4_property_id": {
"type": "string"
},
"gtm_container_path": {
"type": "string"
},
"ads_customer_id": {
"type": "string"
}
},
"required": [
"state"
],
"additionalProperties": false
}
set_semrush_key
{
"type": "object",
"properties": {
"product_slug": {
"type": "string",
"description": "Product slug, e.g. 'demo-product'."
},
"api_key": {
"type": "string"
},
"domain": {
"type": "string"
},
"database": {
"type": "string",
"minLength": 2,
"maxLength": 2,
"default": "us"
}
},
"required": [
"product_slug",
"api_key",
"domain"
],
"additionalProperties": false
}
test_connection
{
"type": "object",
"properties": {
"product_slug": {
"type": "string",
"description": "Product slug, e.g. 'demo-product'."
},
"kind": {
"type": "string",
"enum": [
"ga4",
"gtm",
"google_ads",
"semrush",
"mysql"
]
}
},
"required": [
"product_slug",
"kind"
],
"additionalProperties": false
}
get_spend
{
"type": "object",
"properties": {
"product_slug": {
"type": "string",
"description": "Product slug, e.g. 'demo-product'."
},
"days": {
"type": "integer",
"exclusiveMinimum": true,
"minimum": 0,
"maximum": 365,
"default": 30
}
},
"required": [
"product_slug"
],
"additionalProperties": false
}
log_distribution
{
"type": "object",
"properties": {
"product_slug": {
"type": "string",
"description": "Product slug, e.g. 'demo-product'."
},
"channel": {
"type": "string",
"description": "Channel id, e.g. social_reddit, community, email."
},
"kind": {
"type": "string",
"description": "What happened: forum_post, dm_wave, launch, social_post, email_blast."
},
"url": {
"type": "string",
"format": "uri"
},
"utm_campaign": {
"type": "string"
},
"note": {
"type": "string"
}
},
"required": [
"product_slug",
"channel",
"kind"
],
"additionalProperties": false
}
propose_budget_change
{
"type": "object",
"properties": {
"product_slug": {
"type": "string",
"description": "Product slug, e.g. 'demo-product'."
},
"network": {
"type": "string",
"enum": [
"meta",
"google"
]
},
"campaign_id": {
"type": "string"
},
"from_cents": {
"type": "integer",
"minimum": 0
},
"to_cents": {
"type": "integer",
"exclusiveMinimum": true,
"minimum": 0,
"description": "Proposed daily cap, integer cents."
},
"rationale": {
"type": "string",
"minLength": 10,
"maxLength": 2000,
"description": "Why — shown to the human on the card."
},
"rollback_if": {
"type": "object",
"additionalProperties": {}
}
},
"required": [
"product_slug",
"network",
"to_cents",
"rationale"
],
"additionalProperties": false
}
get_attribution
{
"type": "object",
"properties": {
"slug": {
"type": "string",
"description": "Product slug, e.g. 'demo-product'."
},
"days": {
"type": "integer",
"exclusiveMinimum": true,
"minimum": 0,
"maximum": 365,
"default": 30
}
},
"required": [
"slug"
],
"additionalProperties": false
}
get_timeseries
{
"type": "object",
"properties": {
"slug": {
"type": "string",
"description": "Product slug, e.g. 'demo-product'."
},
"days": {
"type": "integer",
"exclusiveMinimum": true,
"minimum": 0,
"maximum": 365,
"default": 90
}
},
"required": [
"slug"
],
"additionalProperties": false
}
get_health_report
{
"type": "object",
"properties": {
"slug": {
"type": "string",
"description": "Product slug, e.g. 'demo-product'."
},
"days": {
"type": "integer",
"exclusiveMinimum": true,
"minimum": 0,
"maximum": 365,
"default": 30
}
},
"required": [
"slug"
],
"additionalProperties": false
}
get_history
{
"type": "object",
"properties": {
"product_slug": {
"type": "string",
"description": "Limit to one product."
},
"days": {
"type": "integer",
"exclusiveMinimum": true,
"minimum": 0,
"maximum": 365,
"default": 90
},
"limit": {
"type": "integer",
"exclusiveMinimum": true,
"minimum": 0,
"maximum": 500,
"default": 200
}
},
"additionalProperties": false
}
get_funnel_definition
{
"type": "object",
"properties": {
"slug": {
"type": "string",
"description": "Product slug, e.g. 'demo-product'."
}
},
"required": [
"slug"
],
"additionalProperties": false
}
set_funnel_definition
{
"type": "object",
"properties": {
"slug": {
"type": "string",
"description": "Product slug, e.g. 'demo-product'."
},
"steps": {
"type": "array",
"items": {
"type": "object",
"properties": {
"stage": {
"type": "string"
},
"label": {
"type": "string",
"minLength": 1,
"maxLength": 40
}
},
"required": [
"stage",
"label"
],
"additionalProperties": false
},
"minItems": 2,
"maxItems": 10
}
},
"required": [
"slug",
"steps"
],
"additionalProperties": false
}
list_dashboards
{
"type": "object",
"properties": {
"slug": {
"type": "string",
"description": "Product slug, e.g. 'demo-product'."
}
},
"required": [
"slug"
],
"additionalProperties": false
}
create_dashboard
{
"type": "object",
"properties": {
"slug": {
"type": "string",
"description": "Product slug, e.g. 'demo-product'."
},
"name": {
"type": "string",
"minLength": 1,
"maxLength": 80
},
"description": {
"type": "string",
"maxLength": 300
},
"spec": {
"type": "object",
"additionalProperties": {},
"description": "A DashboardSpec object. Validated server-side."
},
"prompt": {
"type": "string",
"maxLength": 500,
"description": "Plain-words description, used only when spec is omitted."
}
},
"required": [
"slug",
"name"
],
"additionalProperties": false
}
update_dashboard
{
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"name": {
"type": "string",
"minLength": 1,
"maxLength": 80
},
"spec": {
"type": "object",
"additionalProperties": {}
}
},
"required": [
"id"
],
"additionalProperties": false
}
approve_card
{
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"actor": {
"type": "string",
"minLength": 1,
"description": "The human operator's name or email. Ask the user for it; never supply a model name or invent one. Written to the audit log as resolved_by."
}
},
"required": [
"id",
"actor"
],
"additionalProperties": false
}
reject_card
{
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"actor": {
"type": "string",
"minLength": 1,
"description": "The human operator's name or email. Ask the user for it; never supply a model name or invent one. Written to the audit log as resolved_by."
}
},
"required": [
"id",
"actor"
],
"additionalProperties": false
}
snooze_card
{
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"actor": {
"type": "string",
"minLength": 1,
"description": "The human operator's name or email. Ask the user for it; never supply a model name or invent one. Written to the audit log as resolved_by."
},
"hours": {
"type": "integer",
"exclusiveMinimum": true,
"minimum": 0,
"maximum": 720,
"default": 24
}
},
"required": [
"id",
"actor"
],
"additionalProperties": false
}
The approval rule
approve_card, reject_card, and snooze_card require actor — the human’s name or email. They exist to relay a decision the human just made, never to make one. The server records that the write came via MCP and which key made it, and its policy engine enforces spend caps and network walls regardless of the client.