feat: add send_poll tool and /api/send/poll endpoint#83
Conversation
There was a problem hiding this comment.
Pull request overview
Adds WhatsApp poll creation support end-to-end by introducing a new MCP tool (send_poll) backed by a new bridge REST endpoint (POST /api/send/poll) that uses whatsmeow’s poll creation API.
Changes:
- Added
send_pollfunction to the MCP server client layer with input validation and bridge POST call. - Added
@mcp.tool()wrapper to exposesend_pollvia the MCP server. - Added
/api/send/pollhandler in the Go bridge to validate and send poll creation messages, plus README/tool documentation and tests.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| whatsapp-mcp-server/whatsapp.py | Adds send_poll() client function with validation and bridge call. |
| whatsapp-mcp-server/main.py | Exposes send_poll as an MCP tool. |
| whatsapp-mcp-server/tests/test_send_poll.py | Adds unit tests for poll validation and request shape. |
| whatsapp-bridge/main.go | Adds /api/send/poll endpoint, request type, recipient JID resolution helper, and poll send helper. |
| README.md | Documents the new send_poll tool and parameters. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Thanks for this — code quality is solid (the Before merging, I want to think out loud about scope. This adds poll creation but the PR description is clear that vote collection is out of scope. That makes the capability asymmetric: an agent can ask a poll question via Claude, but it can never read what people voted. For a single-account bridge, that's a half-feature — and once it's in, the natural follow-up is "now wire up vote results", which I'd want to be sure has a maintainer behind it. Also worth being honest about: every new MCP tool is a permanent context tax for every agent that connects, regardless of whether they use it. So the bar for adding tools is "users are clearly asking for this" rather than "it's possible". I haven't seen demand for polls in issues yet. Two questions to help me decide:
If a vote-collection follow-up isn't realistic for you, I'd rather not ship the send-only side. Either way, the |
- Validate selectable_option_count >= 1 in both Python client and Go bridge (was allowing 0, which is invalid for WhatsApp polls). - Make SendPollRequest.SelectableOptionCount a *int so omitted requests default to 1 instead of decoding to 0. - De-duplicate recipient JID resolution: sendWhatsAppMessage now calls the shared resolveRecipientJID helper used by the poll handler. - Add test for selectable_option_count=0 rejection. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Exposes WhatsApp polls (single- and multi-select) via a new MCP tool and bridge endpoint, using whatsmeow's BuildPollCreation. Vote collection is intentionally out of scope for this PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Validate selectable_option_count >= 1 in both Python client and Go bridge (was allowing 0, which is invalid for WhatsApp polls). - Make SendPollRequest.SelectableOptionCount a *int so omitted requests default to 1 instead of decoding to 0. - De-duplicate recipient JID resolution: sendWhatsAppMessage now calls the shared resolveRecipientJID helper used by the poll handler. - Add test for selectable_option_count=0 rejection. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Align bridge validation wording with the Python layer ("At least two poll options are required").
- Note in whatsapp.py that the validation rules are intentionally duplicated in main.go and must stay in sync.
- Soften test_too_few_options assertion to tolerate wording changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reverts the over-restrictive validation introduced in 9b8e7f0: whatsmeow's BuildPollCreation treats selectableOptionCount=0 as "multi-select with no limit" (msgsecret.go:300-308) and silently coerces out-of-range values to 0. Forcing a minimum of 1 and defaulting omitted requests to 1 silently removed access to the unlimited-select mode and rewrote caller intent. - Allow 0 (unlimited) again in both Python and Go validation; reject only negative or > len(options). - Drop the *int pointer indirection on SendPollRequest — int's zero-value already maps cleanly to whatsmeow's "unlimited" sentinel. - Document the three-way semantics on the request type and update error messages. - Replace the zero-rejection test with one asserting 0 is accepted and forwarded as-is, plus a negative-value rejection test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Thanks for the thoughtful pushback — both points landed. 1. Vote-collection follow-up: yes, I'm willing to do it. It does look meaningfully harder (consuming 2. Use case: honestly nothing exotic — I just needed to send a poll from an agent on WhatsApp. No special workflow behind it. But your "permanent context tax per tool" point is the right frame, and it pushed me to think about this more broadly. I agree the bar shouldn't be "it's possible". Where I'd push back a little: if the answer to "is this in scope?" is consistently no for individual features, the bridge ends up with structural gaps that make agents unreliable for anything off the happy path. Shipping half of polls is the symptom of that, not the disease. The cleaner resolution might be to step outside the MCP frame entirely. The value of MCP is that it plugs into web chat UIs that can't shell out — but for WhatsApp specifically, the realistic consumers are Claude Code / codex-style agents, which are perfectly comfortable invoking a CLI. A CLI can expose the full whatsmeow surface without paying per-feature context tax, because the agent only loads the subcommand it needs. There are already a few candidates in this space — That's a separate conversation from this PR though — happy to start with finishing polls (send + vote collection) here, and we can take the broader scope question to an issue if it's useful. |
…ults Closes the asymmetric send-only gap raised on PR verygoodplugins#83: the bridge now captures inbound poll creation messages (previously dropped because extractTextContent has no PollCreationMessage branch), decrypts inbound PollUpdateMessage events via whatsmeow.Client.DecryptPollVote and persists votes to a new poll_votes table, and exposes a /api/vote/poll endpoint so agents can cast votes too. Outbound polls now persist their metadata and return message_id. MCP surface: - vote_poll: cast or clear a vote on a poll already in the local store - list_polls: enumerate captured polls - get_poll_results: aggregate vote counts per option Decryption needs the per-poll secret that whatsmeow stores when the creation message is processed live, so polls created before the bridge started will surface in list_polls (via history sync) but their votes are not recoverable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Validation failures (unknown option, exceeding selectable_count, empty IDs) now return HTTP 400, and a missing poll returns 404. Only genuine bridge-side failures (DB lookup, BuildPollVote, SendMessage) return 500. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… JID Outbound polls are stored under the recipient's @lid form because resolveRecipientJID converts PN -> LID at send time, but inbound PollUpdateMessage events reference the chat in whatever form the vote sender's client used (often the @s.whatsapp.net form). resolveLIDChat only goes LID -> PN, so the lookup missed in the asymmetric direction and every vote on an outbound poll was silently dropped with a "unknown poll" warning. Try the literal value, then map LID<->PN both ways via the whatsmeow LID store before giving up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
I will switch to wacli so I don't need it anymore but still would be nice to have this feature |
|
I pulled out the generally useful non-poll piece into a separate small PR: #90 ( That keeps this PR from being the only home for the recipient-resolution cleanup while leaving the poll feature itself scoped separately. I still think poll support needs an issue/scope decision and a CI-green implementation before we merge it here, especially given the added MCP surface and vote/persistence behavior. |
Summary
send_pollMCP tool andPOST /api/send/pollbridge endpoint, allowing AI clients to send WhatsApp polls (single- or multi-select).Client.BuildPollCreation— no new dependencies.PollUpdateMessage) is intentionally out of scope for this PR; only poll creation is wired up.Details
Bridge (
whatsapp-bridge/main.go):SendPollRequesttype and/api/send/pollhandler (POST only, validates recipient/name/options/selectable count).sendWhatsAppPollhelper.resolveRecipientJIDhelper extracted so the poll path reuses the same PN→LID resolution assendWhatsAppMessage(existing function unchanged).MCP server (
whatsapp-mcp-server/):send_poll(recipient, name, options, selectable_option_count=1)inwhatsapp.py(validates input, posts to bridge).@mcp.tool()wrapper inmain.py.tests/test_send_poll.pycovering input validation and the bridge call shape.Docs:
Test plan
go vet ./...andgo build ./...inwhatsapp-bridgeuv run ruff check . && uv run ruff format --check .uv run pytest -v(all 19 tests pass — 7 new)🤖 Generated with Claude Code