Skip to content

feat: add send_poll tool and /api/send/poll endpoint#83

Open
Ortes wants to merge 8 commits into
verygoodplugins:mainfrom
Ortes:feat/send-poll
Open

feat: add send_poll tool and /api/send/poll endpoint#83
Ortes wants to merge 8 commits into
verygoodplugins:mainfrom
Ortes:feat/send-poll

Conversation

@Ortes
Copy link
Copy Markdown

@Ortes Ortes commented May 6, 2026

Summary

  • New send_poll MCP tool and POST /api/send/poll bridge endpoint, allowing AI clients to send WhatsApp polls (single- or multi-select).
  • Backed by whatsmeow's Client.BuildPollCreation — no new dependencies.
  • Vote collection (decrypting incoming PollUpdateMessage) is intentionally out of scope for this PR; only poll creation is wired up.

Details

Bridge (whatsapp-bridge/main.go):

  • SendPollRequest type and /api/send/poll handler (POST only, validates recipient/name/options/selectable count).
  • New sendWhatsAppPoll helper.
  • New resolveRecipientJID helper extracted so the poll path reuses the same PN→LID resolution as sendWhatsAppMessage (existing function unchanged).

MCP server (whatsapp-mcp-server/):

  • send_poll(recipient, name, options, selectable_option_count=1) in whatsapp.py (validates input, posts to bridge).
  • Matching @mcp.tool() wrapper in main.py.
  • tests/test_send_poll.py covering input validation and the bridge call shape.

Docs:

  • README updated with the new tool entry.

Test plan

  • go vet ./... and go build ./... in whatsapp-bridge
  • uv run ruff check . && uv run ruff format --check .
  • uv run pytest -v (all 19 tests pass — 7 new)
  • Tested locally end-to-end: poll sent successfully via the running bridge + MCP server

🤖 Generated with Claude Code

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_poll function to the MCP server client layer with input validation and bridge POST call.
  • Added @mcp.tool() wrapper to expose send_poll via the MCP server.
  • Added /api/send/poll handler 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.

Comment thread whatsapp-mcp-server/whatsapp.py Outdated
Comment thread whatsapp-bridge/main.go
Comment thread whatsapp-bridge/main.go Outdated
@jack-arturo
Copy link
Copy Markdown
Member

Thanks for this — code quality is solid (the resolveRecipientJID extraction is a nice cleanup, validation is thorough on both sides, and the tests look good).

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:

  1. Are you willing to follow up with poll-vote collection (consuming PollUpdateMessage / EncPollVote events, persisting them, exposing a get_poll_results or similar)? If yes, I'd be happy to merge this as the first half and track the follow-up in an issue.
  2. What's the use case driving this for you? A concrete "I tried to do X with Claude and couldn't" would help me weigh it against the ROADMAP — polls aren't in the listed message-type coverage today.

If a vote-collection follow-up isn't realistic for you, I'd rather not ship the send-only side. Either way, the resolveRecipientJID extraction is genuinely useful — happy to take that as a standalone refactor PR if you'd like to split it out.

Ortes added a commit to Ortes/whatsapp-mcp that referenced this pull request May 9, 2026
- 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>
Ortes and others added 4 commits May 9, 2026 10:55
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>
@Ortes
Copy link
Copy Markdown
Author

Ortes commented May 9, 2026

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 PollUpdateMessage / EncPollVote, persisting votes, exposing a get_poll_results tool), but I'd rather finish the loop than ship a half-feature. If you don't mind, I'd prefer to roll it into this same PR rather than land send-only now and chase the read side in a follow-up — that way you only have to review the asymmetry once, fully resolved.

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 — wacli (whatsmeow-based, has a SQLite mirror + search/send), whatscli, Vicente Reig's whatsapp-cli explicitly pitched at codex/claude. I haven't audited them seriously yet, but if one of them is solid, it might be worth pointing users there for the long-tail features and keeping this MCP scoped to whatever genuinely benefits from being an MCP tool.

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.

@Ortes Ortes force-pushed the feat/send-poll branch from 4878129 to 81a6aa0 Compare May 9, 2026 15:22
Ortes and others added 3 commits May 9, 2026 11:55
…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>
@Ortes
Copy link
Copy Markdown
Author

Ortes commented May 11, 2026

I will switch to wacli so I don't need it anymore but still would be nice to have this feature

@jack-arturo
Copy link
Copy Markdown
Member

I pulled out the generally useful non-poll piece into a separate small PR: #90 (resolveRecipientJID extraction for outbound sends).

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants