madcatalog MCP — Usage & Production Deployment

The MCP server is how advertisers' AI agents buy placement and accept the MPP challenge. Madcatalog operates it; advertisers connect to it.

What this is

The MCP server is a shared service madcatalog operates. Advertisers configure their AI agents to connect to it. Through it, agents place ads, compete for rank, and are charged via Stripe — with madcatalog as the payment recipient. The MPP 402 challenge-response happens entirely inside the server; the calling agent sees a single tool call and result.

Money flow

Advertiser's AI agent calls place_ad with their
payment_method_id
→
MCP server (madcatalog) POST /ads/bid → 402
POST /payments/stripe/spt
retry with credential
→
Stripe charges advertiser pm_... → madcatalog
Stripe account
→
Ad ranked by spend more spend =
higher rank
The MCP server is madcatalog's payment acceptance surface for machines. It wraps the MPP flow so that any MCP-compatible agent can become a paying advertiser without implementing HTTP 402 challenge-response itself.

The critical gap — payment method is not per-advertiser yet

Production blocker: place_ad and get_rank_status read os.Getenv("STRIPE_PAYMENT_METHOD") — a single value baked into the server at startup. This means every agent hitting the shared server would charge the same payment method. That is not how it should work.

Compare with create_subscription, which already takes payment_method_id as an explicit tool parameter — that's the correct pattern. place_ad and get_rank_status need the same treatment.

Current (wrong for shared service)

// tools.go — place_ad handler
paymentMethod := os.Getenv("STRIPE_PAYMENT_METHOD")
if paymentMethod == "" {
    paymentMethod = "pm_card_visa" // Stripe test token
}

One PM for every advertiser. Wrong.

Fix — add to tool schema + handler

// add to place_ad InputSchema:
"payment_method_id": map[string]any{
    "type":        "string",
    "description": "Stripe pm_... for this advertiser",
},

// handler — prefer param, fall back to env (dev):
pm, _ := p["payment_method_id"].(string)
if pm == "" {
    pm = os.Getenv("STRIPE_PAYMENT_METHOD")
}

Each agent passes their own PM. Existing dev env var still works as fallback.

All gaps before production

The 12 tools

Tool What it does Charges advertiser?
get_catalogPaginated catalog ranked by spendNo
get_adSingle ad — rank, spend, enrichment fieldsNo
get_ad_bidsFull bid history for an adNo
get_advertisersAll advertisers ranked by total spendNo
get_advertiserSingle advertiser profile + all their adsNo
place_ad Place or re-bid an ad. Runs MPP flow internally. Returns ad_id, api_key, advertiser_api_key. Yes — bid amount
update_adEdit title, url, copy, assets. Requires api_key.No
patch_advertiserUpdate advertiser profile. Requires advertiser_api_key.No
get_rank_status Full competitive breakdown — spend to next rank, spend to rank 1. Use get_ad for a free snapshot. Yes — fixed fee per call
create_subscription Monthly ($10) or yearly ($99) recurring billing that auto-credits spend. payment_method_id already a parameter. Yes — recurring
get_subscriptionStatus and billing details for a subscription.No
cancel_subscriptionCancel at end of current period.No

How an advertiser's agent connects

The advertiser drops one JSON snippet into their Claude Code project (or any MCP-compatible agent runtime). That's the entire integration — no SDK, no custom code.

// .mcp.json — what the advertiser receives at onboarding
{
  "mcpServers": {
    "madcatalog": {
      "type":  "http",
      "url":   "https://mcp.madcatalog.com/mcp",
      "headers": {
        "X-MCP-Api-Key": "mcp_live_abc123..."
      }
    }
  }
}

Once connected, the agent can call any of the 12 tools. For paid tools it supplies its Stripe payment_method_id:

// Agent places an ad — payment handled transparently inside the server
place_ad({
  title:             "Acme AI — faster inference",
  url:               "https://acme.ai",
  amount_usd_cents:  500,          // $5.00 bid
  advertiser_slug:   "acme-ai",
  payment_method_id: "pm_1RfX..."  // advertiser's saved Stripe PM
})

// Returns: ad_id, rank, cumulative_spend_usd_cents, api_key, advertiser_api_key

Typical agent workflow

PhaseTools calledGoal
First time get_catalog → place_ad Survey competition, claim slug, enter the catalog
Refine update_ad, patch_advertiser Polish copy and brand assets without spending more
Subscribe create_subscription Set recurring spend to maintain rank automatically
Rank loop get_rank_status → place_ad → get_ad Autonomously close the gap to a higher rank when worthwhile
Audit get_ad_bids, get_subscription Review spend history, verify subscription status

Production deployment

The MCP server is a single shared instance — all advertisers' agents connect to it. It's stateless (no DynamoDB access — all state lives in the REST API).

App Runner Recommended

Docker image (FROM scratch + Go binary). App Runner handles TLS, custom domain (mcp.madcatalog.com), auto-scaling, and health checks against /healthz. Scales to zero. No VPC config.

~$5–10/month at low traffic

Env vars: API_URL, MCP_API_KEY, OTEL_EXPORTER_OTLP_ENDPOINT

Lambda Function URL

Go binary as Lambda (provided.al2023). Function URL with InvokeMode: RESPONSE_STREAM for SSE progress events. CloudFront in front for custom domain. Cold starts ~200–300 ms — acceptable for agent tool calls.

~$0/month at low traffic

Same env vars as App Runner.

The PR that unblocks production

Three changes, all in mcp/, no Lambda code touched:

1. Add payment_method_id parameter to place_ad and get_rank_status schemas + handlers (tools.go).
2. Add withAPIKey middleware in http.go — check X-MCP-Api-Key header, skip on /healthz.
3. Add MCP_API_KEY and production API_URL to deployment config (App Runner service definition or Lambda env vars).

The binary builds today. OTel is wired. /healthz exists. Transport is production-spec (MCP 2025-06-18 HTTP streamable + SSE). The gaps are all in config and two missing tool parameters.

Generated 2026-06-09 · madcatalog MCP usage sketch