@gopigeon/mcp
MCP server for Gopigeon — anonymous form endpoints + durable queues for AI coding agents. Thirteen stdio tools, zero signup.
Tools
Thirteen stdio tools across four surfaces.
Forms (5 tools)
create_endpoint— anonymous claim flow by default; owned mode whenGOPIGEON_API_KEYis set (no claim email, no 30-day TTL — the form belongs to the authenticated user immediately). Passkind: "telemetry"for a high-volume event endpoint (see Telemetry endpoints). Supportsdry_run: truefor forward-compat (see note below).update_endpoint— requiresGOPIGEON_API_KEY; patch shape — only the supplied fields are applied (name,redirect_url,allowed_origins,is_active). Omitted fields are preserved; re-applying the same patch is byte-identical (idempotent). Cannot changekind.get_submissions— requiresGOPIGEON_API_KEY; read submissions on endpoints you own. Passsince(ISO-8601 UTC) for incremental pulls (see Incremental reads).list_endpoints— requiresGOPIGEON_API_KEY; list endpoints you own.readOnlyHint: true, idempotentHint: true.add_destination— requiresGOPIGEON_API_KEY; attach an email / slack / discord / webhook destination to an owned endpoint. CC/BCC unsupported — use N email destinations instead.
Destinations (3 tools)
list_destinations— requiresGOPIGEON_API_KEY; list destinations on an owned endpoint. Secrets (signing secrets, full webhook URLs) are redacted in the response.readOnlyHint: true, idempotentHint: true.update_destination— requiresGOPIGEON_API_KEY; patch a destination'stype/config/is_activein place (no delete-and-recreate). Use{is_active: false}to pause fan-out without losing config. Omitted fields are preserved.idempotentHint: true.remove_destination— requiresGOPIGEON_API_KEY; idempotent — removing an already-removed destination returns success (no error).
Queues (5 tools, post-v1.0 surface)
create_queue— no auth; returnsqueue_api_keyONCEpublish_to_queue— requiresGOPIGEON_QUEUE_API_KEY(supportsdry_run: truefor routing preview)pull_queue— requiresGOPIGEON_QUEUE_API_KEYack_queue— requiresGOPIGEON_QUEUE_API_KEYsubscribe_form_to_queue— requiresGOPIGEON_API_KEY(forms-side, not queue-side)
Utility (1 tool, works with both surfaces)
inspect_endpoint— no auth; no-side-effect probe; prefix-dispatches onf_*(form) /q_*(queue) IDs; v1.3 verifiability primitive paired withdry_run
Quota
Forms
Free tier: 50 submissions/month per form endpoint.
On exceed, the form submission endpoint returns HTTP 402 with auto_enqueued: true — the submission is durably persisted server-side and will replay automatically when the form owner upgrades to Pro. PRICE-04 shape preserved at the lower threshold (form-side cap was 500 submissions/month in v1.3; v1.4 drops to 50 to mirror the queue free-tier strictness).
Telemetry
Free tier: 5,000 events/month. Pro: 100,000 events/month.
Metered on a counter of its own — telemetry events never consume submissions_per_month, and form submissions never consume the telemetry allowance.
On exceed, POST /f/:id returns HTTP 402 with kind: "telemetry", queued: false, will_replay_on_upgrade: false. Unlike form endpoints there is no auto-enqueue: a telemetry endpoint has no destination to replay to, so holding the event would preserve nothing.
Queues
Free tier: 100 messages/month per user across all queues.
On exceed, publish_to_queue returns HTTP 402 with auto_enqueued: true — the envelope is durably persisted server-side and will replay automatically when you (or the queue owner) upgrade to Pro. No envelope is silent-dropped.
For owned (Bearer-claimed) queues, the counter is per-user across every queue they own. For anonymous (unclaimed) queues, a separate per-source-IP counter applies; the envelope still persists, but replay requires the queue to be claimed AND the owner to upgrade.
Read the full Free vs Pro section at /docs/queues#free-vs-pro.
Telemetry endpoints (v0.5.0+)
For high-volume product events — clicks, page views, funnel steps — rather than leads. Create with kind: "telemetry" (owned mode only; the kind is fixed at creation):
// create_endpoint
{ "name": "Site events", "kind": "telemetry" }
Submit to /f/:id exactly as with a form endpoint. What differs:
| Form endpoint | Telemetry endpoint | |
|---|---|---|
| Counter | submissions_per_month | separate telemetry allowance |
| Free / Pro | 50 / 10,000 per month | 5,000 / 100,000 per month |
| Retention | 30 / 90 days | 7 / 30 days, actively swept |
| Destinations | required to be useful | refused (400) |
| Spam scoring | _gotcha honeypot evaluated | not scored; always is_spam: 0 |
| Over quota | 402 + auto-enqueue + replay on upgrade | 402, queued: false |
The separation is the point: a click stream can never consume the quota that gates your lead delivery. add_destination against a telemetry endpoint returns 400 — the guard applies after creation as well as at creation.
Idempotency
POST /f/:id accepts an optional Idempotency-Key header (max 255 printable ASCII characters, no spaces). A repeat carrying a key already seen on that endpoint stores nothing and returns the original submission_id with idempotent_replay: true, consuming no quota.
This matters for fire-and-forget browser sends with keepalive: true, where the browser may retry on its own and inflate counts invisibly. Generate a fresh UUID per logical event; reuse it verbatim on retries of that event. Omit the header to opt out. A malformed key returns 400 rather than being ignored.
Incremental reads (v0.5.0+)
get_submissions accepts since — an ISO-8601 UTC timestamp — returning only rows changed after that instant, oldest-first, plus next_since in the response. Store that watermark and pass it back on the next call.
// first call
{ "form_id": "f_abc123def456xyz0", "since": "2026-08-01T00:00:00Z" }
// response carries next_since — hand it back next time
The cursor runs over updated_at, not created_at, so it catches edits to older rows as well as new submissions. Without it every pull re-walks the full history and gets slower forever — the cost that makes telemetry volume painful to ingest.
Rows carry data (raw JSON string, kept for backwards compatibility) and data_parsed (the same payload already decoded — prefer it), plus created_at_iso / updated_at_iso alongside the database-native timestamps.
Verifiability (v0.3.0+)
Before consuming the one-shot claim event, agents can verify endpoint wiring without side effects. Two mechanisms — one is a flag on the existing tools, the other is a new probe tool.
Dry-run mode — dry_run flag on existing tools
Pass dry_run: true to create_endpoint or publish_to_queue. The tool appends ?dry_run=1 to the outgoing request; the backend runs the full guard cascade (CORS, rate-limit, honeypot, quota read for forms; Bearer auth, body-size guard, read-only quota preview for queues) and returns a routing preview only — sends nothing, no claim email, no funnel event, no quota counter, no AMQP publish.
// MCP tool call (Claude Code):
{
"tool": "publish_to_queue",
"args": {
"queue_id": "q_abc123",
"payload": { "hello": "world" },
"dry_run": true
}
}
// → structuredContent: { dry_run: true, would_route_to: [...], would_have_counted: {...} }
The dry_run field is z.boolean().optional().default(false) — agents using older @gopigeon/mcp@^0.2.0 that omit the field continue to work; the URL has no ?dry_run=1 suffix (a regular real-submission call).
Dry-run is a routing preview — sends nothing. Not a sandbox tier.
Claim-status probe — inspect_endpoint tool
inspect_endpoint is a no-side-effect probe that prefix-dispatches on the endpoint ID:
f_*IDs route toGET /f/{id}/statusq_*IDs route toGET /q/{id}/status- Other prefixes return a structured error WITHOUT making an HTTP call
Returns {exists, claimed, active} on 200, or {exists: false} on 404 (treated as a valid "not found" answer, not an error). The tool advertises readOnlyHint: true, idempotentHint: true so agent runtimes can safely cache or reorder calls. Shares the 60/min/IP rate-limit bucket with ?dry_run=1 requests.
// MCP tool call:
{
"tool": "inspect_endpoint",
"args": { "endpoint_id": "f_abc123def456xyz0" }
}
// → structuredContent: { exists: true, claimed: false, active: true }
Use this BEFORE consuming the claim event with a real submission — it tells you whether the form exists and whether the one-shot claim slot is still available.
Response shapes
| Tool | Real call returns | Dry-run / probe returns |
|---|---|---|
create_endpoint | {form_id, endpoint_url, claim_note} | {dry_run: true, would_route_to, ...} |
publish_to_queue | {seq, id} | {dry_run: true, would_have_counted: {quota_used, quota_remaining}} |
inspect_endpoint | (no real-call variant — always probe) | {exists, claimed, active} or {exists: false} |
Install as a Claude Code plugin
The quickest way to get both the connector and the gopigeon skill — which
teaches agents how to use it — is the plugin:
/plugin marketplace add arvaer/gopigeon-mcp
/plugin install gopigeon@gopigeon-mcp
That wires up the MCP server and installs the skill in one step. Set
GOPIGEON_API_KEY / GOPIGEON_QUEUE_API_KEY in your environment first if you
need the authenticated tools (see Environment).
Prefer to wire the MCP server up by hand? The manual installs are below.
Install (Claude Code)
# Anonymous mode — create_endpoint + create_queue only
claude mcp add --transport stdio gopigeon -- npx -y @gopigeon/mcp@^0.4.0
# With form API key for get_submissions + list_endpoints
claude mcp add --transport stdio --env GOPIGEON_API_KEY=YOUR_KEY gopigeon -- npx -y @gopigeon/mcp@^0.4.0
# With queue API key for publish/pull/ack
claude mcp add --transport stdio --env GOPIGEON_QUEUE_API_KEY=YOUR_QK gopigeon -- npx -y @gopigeon/mcp@^0.4.0
Install (Cursor / generic JSON config)
{
"mcpServers": {
"gopigeon": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@gopigeon/mcp@^0.4.0"],
"env": {
"GOPIGEON_API_KEY": "${GOPIGEON_API_KEY}",
"GOPIGEON_QUEUE_API_KEY": "${GOPIGEON_QUEUE_API_KEY}"
}
}
}
}
Both env vars are optional — omit either to disable that surface. The server boots without keys and exposes only the no-auth tools (create_endpoint, create_queue).
Environment
GOPIGEON_API_BASE— API base URL (v0.3.0+, preferred). Defaults tohttps://api.gopigeon.dev(the queue API host). Override for self-hosted or local (http://localhost:8090). Do NOT usehttps://gopigeon.dev(marketing host, CSRF-gated).GOPIGEON_BASE_URL— Legacy alias forGOPIGEON_API_BASE(backward-compat, one release). UseGOPIGEON_API_BASEfor new configurations.GOPIGEON_API_KEY— Forms-side. Required forget_submissions,list_endpoints, andsubscribe_form_to_queue. Generate viaPOST /api/keysfrom the dashboard.GOPIGEON_QUEUE_API_KEY— Queue-side. Required forpublish_to_queue,pull_queue,ack_queue. Returned ONCE bycreate_queue; never re-issued.
The two API keys live on different trust boundaries — a forms key cannot publish to queues, and a queue key cannot read form submissions. subscribe_form_to_queue is the one cross-surface tool, and it authenticates on the forms side (it mutates forms.queue_id).
Get an API Key
# Log in first, then:
curl -X POST https://gopigeon.dev/api/keys \
-H "Authorization: Bearer <your_session_token>" \
-H "Content-Type: application/json" \
-d '{"name":"claude-code"}'
# → { "api_key": "<64 hex chars>", ... }
# Store immediately — it will not be shown again.
Get a Queue API Key
Either via this MCP server's create_queue tool (preferred for agents) or directly via the REST API:
curl -X POST https://api.gopigeon.dev/q/create \
-H "Content-Type: application/json" \
-d '{"consumers":["me"]}'
# → { "queue_id": "q_...", "queue_api_key": "qk_<64 hex>", ... }
# Store the queue_api_key immediately — it is NOT shown again.
See /docs/queues for the full publish/pull/ack worked example.
Develop
git clone https://github.com/arvaer/gopigeon-mcp.git
cd gopigeon-mcp
npm install
npm run build # compiles src/ → dist/
npm test # vitest suite
npm run dev # tsx src/index.ts — runs against live server
This repository is also a Claude Code plugin marketplace — the plugin lives in plugins/gopigeon/ and the catalog is .claude-plugin/marketplace.json. Pull requests welcome.
License
MIT