Skip to main content
Glama
amansk
by amansk

Meetgrid

Dead-simple scheduling polls — a no-login Doodle alternative.

Create a poll, share a link, collect Yes/No availability, pick a time, and close. Built on Cloudflare Workers + D1 with an MCP server for AI agents.

What & why

Meetgrid helps groups find a meeting time without accounts, calendar integrations, or marketing fluff. Organizers get a public poll URL plus a secret capability token. Respondents enter a name and mark slots Yes or No. Results rank slots by yes-count; organizers mark the final choice and optionally close the poll.

Related MCP server: @calmesh/mcp-server

Features

  • Yes / No only — no maybe votes

  • Manual time slots (Doodle-style) — add date, start time, and duration per option; optional range generator

  • No accounts — unguessable poll IDs (or optional custom link slug), organizer secrets, and per-respondent edit tokens

  • Timezone-aware — required on create; defaults to America/Los_Angeles in the UI

  • Three thin pages — Create, Respond, Results (mobile-first, form aesthetic)

  • MCP tools for agents — same HTTP API the UI uses

Quick start

Prerequisites

Local development

npm install
npm run db:migrate:local
npm run dev

Open http://localhost:8787.

Deploy to Cloudflare

  1. Create a D1 database:

npx wrangler d1 create meetgrid-db
  1. Copy the returned database_id into wrangler.toml (replace local-dev-placeholder).

  2. Apply migrations to production:

npm run db:migrate:remote
  1. Deploy:

npm run deploy

When a poll is created with an email, Meetgrid sends that address the admin link through Resend. Set a Resend API key with sending access and a sender address on a domain you've verified in Resend:

npx wrangler secret put RESEND_API_KEY
npx wrangler secret put RESEND_FROM_EMAIL   # or add it under [vars] in wrangler.toml
# optional: RESEND_FROM_NAME, e.g. "Meetgrid"

For local dev, put the same keys in .dev.vars. With either one unset, no email is sent and poll creation works exactly as before; the response's email_status reads not_configured. A Resend error reads failed and is logged, and the poll is still created. The address is not stored.

Leave click tracking off for the sending domain in Resend (it is off by default). The admin link carries the organizer secret, and click tracking would route it through a redirect.

Project layout

├── src/
│   ├── index.ts           # Worker entry (Hono app + /mcp Streamable HTTP)
│   ├── mcp/               # MCP handler + tool registration
│   ├── routes/
│   │   ├── api.ts         # REST API
│   │   └── pages.ts       # HTML pages
│   ├── db/queries.ts      # D1 queries
│   └── lib/               # crypto, slots, timezone, rate-limit
├── migrations/
│   └── 0001_initial.sql
├── mcp/                   # MCP server (stdio)
└── wrangler.toml

API

Base URL: https://your-worker.workers.dev (or http://localhost:8787 locally)

Method

Path

Description

POST

/api/polls

Create poll

GET

/api/polls/:id

Public poll view (no organizer secret)

POST

/api/polls/:id/respond

Submit/update response

GET

/api/polls/:id/my-response?edit_token=

Load own votes by edit token

POST

/api/polls/:id/decision

Organizer: mark chosen slot

POST

/api/polls/:id/close

Organizer: close poll

POST

/api/polls/:id/slots

Organizer: add/remove slots

POST

/api/slots/generate

Preview range-generated slots for create UI

*

/mcp

MCP Streamable HTTP endpoint

Write endpoints are lightly rate-limited (30 requests / 60s per IP by default).

curl examples

Create a poll (explicit slots — preferred)

curl -s -X POST http://localhost:8787/api/polls \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Team sync",
    "timezone": "America/Los_Angeles",
    "slots": [
      {"date": "2026-09-15", "start_time": "10:00", "duration_minutes": 30},
      {"date": "2026-09-16", "start_time": "14:00", "duration_minutes": 45}
    ]
  }'

Create via range generator (optional fallback when slots omitted)

curl -s -X POST http://localhost:8787/api/polls \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Team sync",
    "timezone": "America/Los_Angeles",
    "duration_minutes": 30,
    "start_date": "2026-09-15",
    "end_date": "2026-09-19",
    "daily_start": "09:00",
    "daily_end": "17:00",
    "weekdays": [1, 2, 3, 4, 5]
  }'

Save poll_id, organizer_secret, and poll_url from the response. organizer_url is the admin link (results page with the secret). Add "email": "you@example.com" to have the admin link emailed to you; the response then includes email_status (sent, not_configured or failed). Optionally set a custom link with "slug": "team-sync" (or "poll_id" / "name") — lowercase letters, digits, and hyphens, 3–48 chars; returns 409 if taken.

curl -s -X POST http://localhost:8787/api/polls \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Team sync",
    "slug": "team-sync-sept",
    "timezone": "America/Los_Angeles",
    "slots": [{"date": "2026-09-15", "start_time": "10:00", "duration_minutes": 30}]
  }'

Get poll (public)

curl -s http://localhost:8787/api/polls/POLL_ID

Respond

curl -s -X POST http://localhost:8787/api/polls/POLL_ID/respond \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Alex",
    "votes": [
      {"slot_id": "SLOT_ID", "yes": true}
    ]
  }'

Update a response (pass edit_token from the prior response):

curl -s -X POST http://localhost:8787/api/polls/POLL_ID/respond \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Alex",
    "edit_token": "EDIT_TOKEN",
    "votes": [
      {"slot_id": "SLOT_ID", "yes": false}
    ]
  }'

Mark chosen slot

curl -s -X POST http://localhost:8787/api/polls/POLL_ID/decision \
  -H 'Content-Type: application/json' \
  -d '{
    "organizer_secret": "ORGANIZER_SECRET",
    "slot_id": "SLOT_ID"
  }'

Close poll

curl -s -X POST http://localhost:8787/api/polls/POLL_ID/close \
  -H 'Content-Type: application/json' \
  -d '{
    "organizer_secret": "ORGANIZER_SECRET"
  }'

MCP server

Meetgrid exposes MCP over Streamable HTTP at /mcp on the Worker itself (stateless WebStandardStreamableHTTPServerTransport, one server per request). Same tools as the REST API. A stdio transport is optional for local CLI use.

Production endpoint:

https://meetgrid.amandeep.app/mcp

Local dev (after npm run dev):

http://localhost:8787/mcp

Add to .cursor/mcp.json (or Cursor Settings → MCP):

{
  "mcpServers": {
    "meetgrid": {
      "url": "https://meetgrid.amandeep.app/mcp"
    }
  }
}

For local development, use "url": "http://127.0.0.1:8787/mcp".

Stdio (optional)

cd mcp
npm install
npm run build

Set MEETGRID_API_URL to your worker URL (defaults to http://127.0.0.1:8787).

{
  "mcpServers": {
    "meetgrid": {
      "command": "node",
      "args": ["/absolute/path/to/meetgrid/mcp/dist/index.js"],
      "env": {
        "MEETGRID_API_URL": "http://127.0.0.1:8787"
      }
    }
  }
}

MCP tools

Tool

Description

poll_create

Create poll with explicit slots or date-range grid; optional custom link via slug, poll_id, or name

poll_get

Get public poll view by ID

read_poll

Alias of poll_get — adds an options list (id, label, yes_count, no_count) for voting

poll_respond

Submit or update Yes/No votes

vote_poll

Alias of poll_respond — use name plus votes or options ({ slot_id, yes }); call read_poll first

poll_set_decision

Organizer marks chosen slot

poll_close

Organizer closes poll

Organizer secrets are returned only from poll_create — never from poll_get.

Auth (v0)

Meetgrid MCP matches the app’s no-login model: the /mcp endpoint is public and tools call the same REST API as the web UI. There is no OAuth or MCP-level authentication in v0.

  • Create: poll_create returns organizer_secret once in the response — save it; it is not retrievable later.

  • Organizer actions: pass organizer_secret to poll_set_decision and poll_close.

  • Respond: poll_respond / vote_poll returns an edit_token for the respondent to update their votes.

Do not treat MCP as a private admin API — anyone who can reach /mcp can create polls and respond. Protect the endpoint at the network layer if you need restriction (not implemented in v0).

Security model

  • poll_id — public, unguessable (12-char alphanumeric)

  • organizer_secret — capability token; shown once at creation; stored hashed in D1

  • edit_token — per-respondent; returned on respond; stored hashed; enables self-service edits via cookie or copyable link

  • Public GET responses never include organizer secrets

Pages

URL

Purpose

/

Create poll

/p/:id

Respond (Yes/No per slot)

/p/:id/results

Ranked results + heatmap; organizer actions with ?secret=

License

MIT

Related MCP Connectors

Related MCP Servers