Repository navigation
Releases: DojoCodingLabs/whatsapp-adapter
Release list
sdk-v0.10.0
Version note. Earlier drafts of this section were labelled
sdk-v1.1.0on the assumption that1.0.0had shipped. It has
not — the stability tag is gated on a live smoke test against a
real WABA (ROADMAP.md§ Q4 2026). This
release therefore stays pre-1.0 as0.10.0; per the policy
above, a pre-1.0 minor may carry breaking changes, and this one
does (listed first).
Two batches in one release: the Phase B additions that have been
on main since May (OptInRegistry, retry telemetry,
markAsRead, media, agent-bridge) and the September 2026 deep
audit against Meta's current Cloud API documentation
(docs/_internal/2026-09-07-sdk-deep-audit.md, 25 findings, all
addressed). Ten OpenSpec changes archived under
openspec/changes/archive/2026-09-09-*.
859 SDK tests (was 603 in 0.9.0).
⚠ Breaking
- Meta error
131053(media upload error) now maps to
CapabilityError, notRateLimitError, and is never retried.
It is a permanent content failure. Consumers matching
instanceof RateLimitErrorto requeue media sends must switch
toCapabilityError. - Every error leaving a
WhatsAppClientmethod is a
WhatsAppErrorsubclass. RawTransientHttpError,
TypeError(fetch failure) andAbortErrorno longer escape
after retry exhaustion — they surface as the new
TransientError,NetworkErrorandRequestAbortedError
(original attached ascause). Anycatchthat special-cased
those raw types should now useinstanceofon the typed
classes. sendReaction(and reaction payloads viasendReply) are
window-gated. Meta documents only approved templates as
window-exempt. A reaction outside the 24 h window now throws
WindowClosedErrorbefore HTTP instead of a wasted Graph
round-trip ending in131047.buildInteractiveListenforces ≤ 10 rows in total across
sections (Meta's actual cap), not 10 per section.- Free-form builders enforce Meta's character limits and
throwWhatsAppError("UNKNOWN")before HTTP for over-long
fields (text body 4096, caption 1024, interactive body 1024 /
footer 60 / text header 60, reply-button title 20 / id 256,
list button 20, section title 24, row title 24 / description 72
/ id 200,cta_urldisplay text 20). Counted in Unicode code
points. Previously these round-tripped to Meta and came back as
100/131009. Limits exported asMESSAGE_LENGTH_LIMITS. - Input-validation errors are
WhatsAppError("UNKNOWN"), never
bareTypeError/Error, acrossuploadMedia,
downloadMedia,markAsRead,getTemplate,sendReply
(real and mock). - Template pre-flight is stricter. A
HEADERof format
IMAGE/VIDEO/DOCUMENT/LOCATIONnow requires exactly
one parameter of the matching type (previously validated clean
with zero params and failed at Meta with132012); named
(parameter_format: NAMED) templates are validated by
set-equality against{{name}}placeholders; mixed
positional + named templates are rejected. - Pinned Graph API version
v25.0→v26.0. Override per
instance withgraphApiVersionif you need to stay onv25.0
(supported by Meta until 2028-07-29).v26.0carries no
WhatsApp Cloud API breaking changes.
Security
whatsapp.pathOTel span attribute no longer carries the
bearer token.healthCheck()hits
/debug_token?input_token=<token>; the query string was
recorded verbatim on the span.whatsapp.pathis now the path
only, for every request. If you export traces to a third-party
backend, rotate the token that was in scope.
Fixed
- Re-engagement gate is Meta code
131047, not131026.
131026("Message Undeliverable" — recipient not on WhatsApp /
outdated client / unaccepted ToS) maps to the new
UndeliverableError; a template send does not recover from
it, so the MCP recovery hint no longer tells the model to retry
that path. - Meta error taxonomy realigned against the Cloud API
error-code reference (mapMetaError):4,80007(app- / WABA-level throttling) → retryable
RateLimitError(wereUNKNOWN, never retried).131049,131064,133016→RateLimitErrorbut not
retried (enforcement windows are hours / days or
per-recipient).131008,131009,131051,131052,131053→
CapabilityError.131050(recipient stopped marketing) →OptOutErrorwith
metaCode.368,130497,131031(integrity) → new
AccountRestrictedError.0→AuthenticationError;3,10,131005→
PermissionError.132xxx→TemplateErrornow carriesmetaCode.- Exhausted 429 / throttling →
RateLimitError(metaCode
kept); exhausted 408 / 5xx →TransientError; fetch failure →
NetworkError; non-JSON 2xx →WhatsAppError("UNKNOWN")
without retry (double-send risk). whatsapp.error.meta_codespan attribute is emitted for every
typed error that carries ametaCode.
- Retry loop honours the caller's
AbortSignal. A pre-aborted
or mid-flight-abortedRequestOptions.signalwas classified as a
retryable failure — three retries and ~8 s of jittered backoff
before the cancellation surfaced. Aborted signal → immediate
RequestAbortedError, pending backoff cut short,signal.reason
preserved ascause. Non-callerAbortErrors (fetch-internal
timeouts) remain retryable. WindowTracker.notifyInbound(from, atMs)honours the
customer's timestamp. Meta retries webhooks for up to 7 days;
a late or replayed delivery re-opened a 24 h window from receipt
time, so the next free-form send failed at Meta with131047.
The tracker now stores the inbound timestamp, sets the storage
TTL to the remaining window, ignores deliveries older than the
window, never lets an older replay shorten a live window, and
clamps future-dated timestamps. Legacytrueentries still read
as open for rolling upgrades. Passe.timestampfrom your
messagehandler;createAgentBridgedoes so automatically.- Template button index accepts numeric whole numbers ≥ 0
(carousel card buttons use numeric indices); malformed and
fractional indices are rejected pre-flight. - Stale claims corrected in JSDoc and docs:
windowTrackeroption
(reactions are gated),StatusEvent.pricingCategory(it is the
billing category, not the pricing model),messages/types.ts
header (Graph API v26), dedupe-TTL note inwebhooks.md
(24 h, not 1 h), several broken relative links in
docs/compliance.md.
Added — MediaExpiredError + media downloads through the transport
OpenSpec change 2026-09-09-media-transport-parity.
fetchExternal(client, url, options)— authenticated GET of
Meta's media CDN sharing the Graph transport pipeline: a
whatsapp.media.fetchOTel span (host only — never the signed
query string), retry on 408 / 429 / 5xx honouringRetry-After,
fetchImpl,signal,requestId, typed errors.MediaExpiredError(code: "MEDIA_EXPIRED",httpStatus,
optionalmediaId) for CDN 401 / 403 / 404 / 410 — the
~5-minute pre-signed URL has lapsed. Not retried; never carries
the URL. Recovery: calldownloadMedia(mediaId)again.DownloadedMedia.fetchBytes(options?)and
fetchMediaUrl(client, url, options?)acceptMediaFetchOptions
(retry policy, hooks, signal, requestId, fetchImpl);
downloadMedia(mediaId, options?)forwards policy / hooks /
fetchImpl tofetchBytes. A bareAbortSignalis still accepted
for0.9.xcompatibility.- Exported size-gate helpers
payloadByteLength,
assertSizeAllowed,validateUploadInput;
MockWhatsAppClient.uploadMediaruns the same pre-flight as the
real client. - Contract tests for
uploadMedia,downloadMedia(+ span),
markAsRead; a media parity suite (real ⇄ mock).
Added — Typed error classes
AccountRestrictedError(ACCOUNT_RESTRICTED) — integrity /
policy restrictions (368,130497,131031). Not retryable;
surface to a human.TransientError(TRANSIENT) — retries exhausted on 408 / 5xx.NetworkError(NETWORK) —fetchitself failed after retries.RequestAbortedError(ABORTED) — the caller's signal fired;
causeissignal.reason.UndeliverableError(UNDELIVERABLE) — Meta131026.MediaExpiredError(MEDIA_EXPIRED) — see above.TemplateErrorandOptOutErrorgain an optionalmetaCode.
Added — Webhook events
OpenSpec changes 2026-09-09-webhook-user-preferences-and-flows,
2026-09-09-status-event-pricing-fields.
UserPreferencesEvent(kind: "user_preferences") — Meta's
authoritative marketing stop / resume signal, one event per
entry, typed.on("user_preferences", h), deduped on
waId + category + value + timestamp. Wire it to
OptInRegistry.optOut/optInscoped toMARKETING; the
inbound STOP-keyword pattern is a fallback only.IncomingMessageKindgains"interactive_nfm_reply"(WhatsApp
Flows completion;response_jsonpreserved inbody) and
"request_welcome"(Click-to-WhatsApp conversation opened) —
they no longer collapse to"unsupported".StatusEvent.pricingType(regular|
free_customer_service|free_entry_point),
StatusEvent.pricingModel(PMP|CBP) and
StatusEvent.billable. Financially material from Oct 1,
2026, when Meta bills every free-form message and every
in-window UTILITY template per message; only the 72 h free
entry-point window stays free. Seedocs/compliance.md§ 2.
Added — Template pre-flight for named / media templates
OpenSpec change 2026-09-09-fix-template-preflight-validation.
TemplateDefinition.parameter_format(POSITIONAL|NAMED);
parameter_nameon text / currency / date_time parameters;
TemplateParameterLocation;TemplateMediaHeaderFormat,
TemplateParameterFormattypes.listTemplatePlaceholders, `hasNamedTem...
mcp-v0.5.0
Version note. Earlier drafts of this section were labelled
mcp-v1.1.0on the assumption that1.0.0had shipped. It has
not — the stability tag is gated on a live smoke test against a
real WABA (ROADMAP.md§ Q4 2026). This
release stays pre-1.0 as0.5.0and carries the breaking change
listed first.
Depends on @dojocoding/whatsapp-sdk@0.10.0. The server now
registers 19 tools (was 16), 2 resources, 1 prompt.
201 MCP tests (was 152 in 0.4.0).
⚠ Breaking
whatsapp_upload_media_from_urlrefuses non-public
sourceUrlvalues. The server fetches whatever URL the model
supplies, which made the tool a server-side-request-forgery
vector (z.string().url()accepted
http://169.254.169.254/…). It now rejects — with
isError: true,error.code: "source_url_rejected", and before
any network I/O — anything that is nothttps://, embeds
credentials, targetslocalhost/*.local, or targets a
literal loopback / unspecified / link-local / RFC 1918 /
100.64/10IPv4 address (incl. IPv4-mapped IPv6) or an IPv6
loopback / link-local / ULA. Redirects are not followed
(redirect: "error"). Movehttp://sources tohttps://.whatsapp_send_reactionis window-gated. Meta exempts only
approved templates; a reaction outside the 24 h window returns
WINDOW_CLOSEDwith the template recovery hint.- Inherits the SDK's
0.10.0breaking changes where they reach
the tool surface:131053is nowCAPABILITY(not
RATE_LIMIT); over-long text / captions / button titles are
rejected asUNKNOWNbefore HTTP with the field and limit
named; template pre-flight is stricter for media headers and
named parameters.
Added — Three tools: whatsapp_mark_as_read, whatsapp_upload_media_from_url, whatsapp_get_media_info
OpenSpec change 2026-09-09-mcp-media-ack-tools-surface (the
tools landed on main in May without a spec delta; this change
brings the spec, docs and tests up to date).
whatsapp_mark_as_read{ messageId, typing? }→
{ success, messageId, typing }. Acks an inbound wamid (blue
double-tick) and optionally shows a typing indicator while the
agent composes a reply (auto-dismisses on send or after ~25 s).
Window-independent;idempotentHint: true. Free under
per-message pricing — the right substitute for a "one moment…"
filler message.whatsapp_upload_media_from_url{ sourceUrl, mimeType, filename? }→{ mediaId, mimeType, bytes }. Server-side
fetch + upload so the model never handles bytes. Source-URL
guard as above; non-2xx / rejected fetch →
source_fetch_failed; oversize → SDKCAPABILITY.whatsapp_get_media_info{ mediaId }→
{ id, mimeType, sha256, fileSize }. Metadata only — never the
bearer-authenticated URL or the bytes.readOnlyHint,
idempotentHint.MARK_AS_READ_TOOL,UPLOAD_MEDIA_FROM_URL_TOOL,
GET_MEDIA_INFO_TOOLname constants exported.test/contract/media-ack-tools.test.ts(+32) covers all
three, including the SSRF guard table.
Added — Recovery hints for the SDK's new error classes
mapSdkError / recoveryHint now cover UNDELIVERABLE
(131026 — a template will not fix it; stop retrying),
ACCOUNT_RESTRICTED (surface to a human), TRANSIENT /
NETWORK (retry later), ABORTED, MEDIA_EXPIRED (call
whatsapp_get_media_info again), rate-limit codes 131049 /
131064 (per-recipient / spam-rate — do not retry soon), and
metaCode detail on TEMPLATE / OPT_OUT. See
docs/mcp/error-recovery.md.
Dependencies
@modelcontextprotocol/sdk^1.29.0→^1.30.0. Clears the
fourhightransitive advisoriespnpm audit --prodreported
(honoCORS reflection GHSA-88fw-hqm2-52qc,fast-uri×3,
ip-address). Remaining moderates sit inexpress>qsand
@hono/node-serverinside the MCP SDK's transports this package
does not use.
Fixed
- Re-engagement gate is
131047(was documented as131026). docs/mcp/tools.mddocuments all 19 tools; every "16 tools"
count corrected across docs and source comments.
Added — Streamable HTTP handler (createWhatsAppHttpHandler)
OpenSpec change 2026-05-12-mcp-streamable-http-transport.
New top-level export. Fetch-API native MCP Streamable HTTP
handler (spec revision 2025-06-18) runnable on Cloudflare
Workers, Vercel Functions (Node + Edge), AWS Lambda, Hono,
Next.js App Router, Bun, Deno, plain Node 18+. No Node-API-only
dependencies.
import { WhatsAppClient } from "@dojocoding/whatsapp-sdk";
import { createWhatsAppHttpHandler } from "@dojocoding/whatsapp-mcp";
const handler = createWhatsAppHttpHandler({
client: new WhatsAppClient({ ... }),
wabaPhoneNumberId: "...",
staticToken: process.env.MCP_GATEWAY_TOKEN!,
});
// Next.js App Router:
export { handler as GET, handler as POST, handler as DELETE };Returns (req: Request) => Promise<Response>. Safe to
construct at module scope; safe to reuse across concurrent
requests.
Same 16 tools / 2 resources / 1 prompt as the stdio bin and
the embedded toolset. Surface parity drift-detected at CI.
Added — Built-in bearer authentication
Two complementary modes on CreateWhatsAppHttpHandlerInput:
staticToken: string— shared-secret constant-time
compare againstAuthorization: Bearer <token>. Right for
closed-network deployments and defence-in-depth behind an
outer OAuth gateway.verifyToken: (token, req) => Promise<AuthInfo | null>
— consumer-supplied verifier callback. Returns the MCP-SDK
AuthInfoshape on success (passed verbatim to message
handlers);nullto reject with 401.
Both optional. Pass neither to delegate authentication to the
outer gateway. When both are set, verifyToken takes
precedence.
The MCP SDK's AuthInfo type is re-exported from the package
root so consumers writing a verifier don't need to import deep
paths.
Added — 401 response shape
All authentication failures (missing header, bad static token,
verifier returned null) return:
{ "jsonrpc": "2.0", "id": null, "error": { "code": -32001, "message": "Unauthorized" } }with Content-Type: application/json. The body does NOT vary
across failure paths — clients cannot infer the cause from
the body alone, and the rejected token is never echoed.
Added — Stateless / stateful modes
- Stateless (default): per-request server + transport.
Right for serverless deployments where state cannot survive
between invocations. ~1-2 ms per-request setup cost. - Stateful (
stateless: false): shared server + transport
at the factory level. The MCP SDK tracks sessions in-memory.
Right for long-lived Node / Bun / Deno servers; not safe
across multi-node deployments without an external session
store.
Tests (+17 new)
test/contract/http-handler-auth.test.ts— 10 tests:
pass-through with no auth set, static-token match,
case-insensitiveBearerscheme prefix per RFC 6750,
missing header → 401, wrong token → 401,verifyToken
null → 401,verifyTokenAuthInfo → passes through,
precedence (verifyToken wins), the 401 body does NOT
echo the rejected token.test/contract/http-handler-roundtrip.test.ts— 7 tests
driving an MCPClientagainst the handler via an
in-processfetchproxy. Initialize handshake;
tools/listreturns the 16 expected tools;
tools/call whatsapp_send_textround-trips with
structuredContent.messageId;resources/listreturns
the 2 expected URIs;resources/read whatsapp://templates
returns the cached body;prompts/listreturns
wa-template-send;prompts/getrenders the guided
messages.
152 in-process MCP tests (was 134 in 0.4.0).
Bundle
Library ESM 10.62 KB brotlied (was 9.93 KB; +700 B for the
HTTP handler + WebStandardStreamableHTTPServerTransport
wiring). CLI 9.26 KB brotlied (was 9.25 KB; unchanged).
Both well under their 200 KB / 300 KB budgets.
Docs
docs/mcp/http.md— handler reference (API, when-to-use
matrix vs stdio + embedded, auth modes, 401 shape,
runtime portability table).docs/cookbook/mcp/streamable-http-vercel.md— end-to-end
Vercel + JWT recipe with ajose-based verifier callback.docs/mcp/README.mdindex gains the new page.docs/mcp/transports.md§ "Streamable HTTP" replaces the
former "v2 (planned)" section.
The Streamable HTTP handler itself is non-breaking: the stdio
bin, the programmatic WhatsAppMcpServer class, and the embedded
toolset gain the new tools but keep their existing contracts.
sdk-v0.9.0
V1 runway — the final 0.x minor bundles three changes Site2Print
identified as Phase A blockers in the integration audit. All three
land together so consumers see one upgrade.
OpenSpec changes:
2026-05-12-web-adapter-waituntil2026-05-12-webhook-ctwa-clid2026-05-12-rename-idempotency-to-request-id
Added — waitUntil on the web adapter
CreateWhatsAppHandlerOptions.waitUntil?: (p: Promise<unknown>) => void
on the Fetch-API adapter at @dojocoding/whatsapp-sdk/web. Wires the
SDK's async dispatch promise into runtime lifecycle-extension APIs
(Vercel Functions @vercel/functions.waitUntil, Cloudflare Workers
ctx.waitUntil) so handlers actually run on serverless / edge
runtimes.
Without this, the fire-and-forget dispatch promise was silently
dropped after the response on Vercel + Workers — handlers never
finished, DB writes never landed, OTel spans never flushed. Now:
// Vercel Functions:
import { waitUntil } from "@vercel/functions";
const handler = createWhatsAppHandler(receiver, { waitUntil });
// Cloudflare Workers:
const handler = createWhatsAppHandler(receiver, {
waitUntil: ctx.waitUntil.bind(ctx),
});When omitted, behaviour is unchanged (fire-and-forget). The adapter
chains .catch(onUnhandledHandlerError) BEFORE passing the promise
to waitUntil, so the runtime never sees an unhandled rejection.
5 new contract tests in
packages/whatsapp-sdk/test/contract/adapters/web/wait-until.test.ts.
Added — MessageEvent.referral for CTWA attribution
When Meta attaches a referral object to the first inbound message
after a Click-to-WhatsApp ad click, the SDK's parser now surfaces it
on the parsed MessageEvent:
import type { MessageEvent, WhatsAppReferral } from "@dojocoding/whatsapp-sdk";
receiver.on("message", (e: MessageEvent) => {
if (e.referral?.ctwa_clid) {
// forward to Meta CAPI for ad attribution
}
});The new WhatsAppReferral type names the documented core fields
(ctwa_clid, source_url, source_type, source_id, headline,
body, media_type, media_url, thumbnail_url,
welcome_message). The runtime type is a permissive intersection
with Record<string, unknown> — unknown future fields Meta adds
are preserved without an SDK release.
Attribution semantics:
- Only the first message after a click carries
referral.
Subsequent messages don't; cachectwa_clidkeyed onfrom. - Empty
referral: {}is preserved (distinguishes "no referral"
from "referral present but Meta omitted details"). - The parser does NOT throw on a non-object / null
referral.
6 new tests + 2 fixtures
(message-with-ctwa-referral.json, message-with-empty-referral.json).
BREAKING (pre-1.0 minor) — idempotencyKey → requestId rename
The misleading "idempotency" naming on the request-correlation
surface is renamed:
RequestOptions.idempotencyKey→RequestOptions.requestId- Outbound header
X-Dojo-Idempotency-Key→X-Request-Id - OTel span attribute
whatsapp.idempotency_key→whatsapp.request.id
Behaviour is unchanged — the SDK still generates a UUID v4 per
logical call when requestId is omitted, still reuses it across
retry attempts, still attaches the header to every outbound
request. Only the naming changes.
Why: Meta does NOT consult any SDK-attached header for outbound
deduplication. The X-Dojo-Idempotency-Key naming created a
false-positive feeling for consumers who assumed retries were
deduplicated server-side. The v0.x → v1.0 window is the right
moment to clean this up. Real outbound dedup is post-1.0 (the
outbound-deduper capability on the v2 roadmap).
Migration: mechanical search-and-replace. See
MIGRATION.md § "SDK: 0.8.x → 1.0.0".
- await client.sendText({ to, body }, { idempotencyKey: "booking-123" });
+ await client.sendText({ to, body }, { requestId: "booking-123" });Downstream consumers reading req.idempotencyKey in custom retry
hooks, or asserting the legacy header / span attribute in tests,
break at compile or assertion time. Renames in one shot.
Docs
docs/sdk/web.md§ "Cloudflare Workers" and "Next.js App Router
(Vercel)" updated with thewaitUntilwiring. Long-lived vs
serverless / edge threading model called out explicitly.docs/sdk/webhooks.md§ "Click-to-WhatsApp (CTWA) referral" — new
subsection with the Meta CAPI handoff snippet + attribution caveats.docs/sdk/patterns.md§ 7 renamed and rewritten: "Request
correlation withrequestId" (was "Replay-safe sends with
idempotencyKey"). Honest framing — correlation, not dedup.docs/architecture.md§ "Idempotency hint" replaced with
"Request correlation" naming the new header + attribute and
pointing at the v2outbound-deduperfor real dedup.docs/compliance.md§ 3.4 updated with the rename and the v2
outbound-deduper roadmap note.MIGRATION.md§ "SDK: 0.8.x → 1.0.0" gains therequestIdrename
diff alongside the existingsetRedactSaltdeprecation entry.
Tests
- 5 new web-adapter
waitUntilcontract tests. - 6 new CTWA / referral parser tests.
- 1 new test asserting the legacy
X-Dojo-Idempotency-Keyheader
is NOT emitted (alongside the renamed-name tests).
603 SDK tests (was 591). Coverage 97.38 / 88.88 / 99.15 / 97.38 —
well above the 90/85/90/90 gate.
mcp-v0.4.0
OpenSpec change 2026-05-12-mcp-embedded-toolset. The Site2Print
v1 integration audit identified this as the single biggest unlock
for downstream gateway integrations — more important than the
planned Streamable HTTP transport, because it sidesteps the
transport question entirely.
Added — createWhatsAppToolset (embedded toolset)
New top-level factory exported from @dojocoding/whatsapp-mcp:
import { WhatsAppClient } from "@dojocoding/whatsapp-sdk";
import { createWhatsAppToolset } from "@dojocoding/whatsapp-mcp";
const toolset = createWhatsAppToolset({
client: new WhatsAppClient({ ... }),
wabaPhoneNumberId: "PNID",
});
// In your gateway's tools/list:
const tools = [...toolset.tools, ...otherUpstreamTools];
// In your gateway's tools/call:
const result = await toolset.dispatch(name, args);Returns a WhatsAppToolset with:
tools: ReadonlyArray<ToolDefinition>— 16 entries in stable order.resources: ReadonlyArray<ResourceDefinition>— 2 entries.prompts: ReadonlyArray<PromptDefinition>— 1 entry.dispatch(name, args, ctx?): Promise<CallToolResult>— schema-validates and routes.readResource(uri): Promise<ReadResourceResult>—whatsapp://templates+whatsapp://window/<phone>.renderPrompt(name, args?): Promise<GetPromptResult>—wa-template-send.
Same 16 tools, 2 resources, 1 prompt as WhatsAppMcpServer.
Same schemas. Same error mapping. Same recovery-hint text.
Surface parity is enforced by a new drift-detector contract
test on every PR; renaming a tool in one path without the other
fails CI immediately.
Added — MCP-spec-shaped types re-exported
The shared per-tool contract types now ship from
@dojocoding/whatsapp-mcp for downstream gateway
implementations:
ToolDefinition/ResourceDefinition/PromptDefinitionCallToolResult/ReadResourceResult/GetPromptResultResourceContent/ToolAnnotationsDispatchContext/ZodShape
These are MCP-spec-aligned types — pass-through compatible with
the MCP SDK's internal shapes.
Internal refactor (zero behavioural change)
Each tool / resource / prompt file under
packages/whatsapp-mcp/src/{tools,resources,prompts}/*.ts now
exports a <name>Definition constant and a handle<Name>
function alongside the existing register<Name> glue. Both
the stdio server (WhatsAppMcpServer.connect(transport)) and
the new toolset consume the same per-tool pair. The
register<Name> functions are now thin shims over a shared
registerToolOnServer(server, definition, handler) bridge.
The change is invisible at the public surface; existing
consumers (Claude Desktop spawning the stdio bin, tests using
InMemoryTransport) see identical behaviour.
Tests
test/contract/embedded-toolset-parity.test.ts— drift
detector. Asserts the toolset and the stdio server expose
identical tool names, resource URIs, prompt names, and
JSON-Schema serialisations of everyinputSchema. New
capability addition that doesn't mirror in both paths fails
immediately.test/contract/toolset-dispatch.test.ts— 17 tests covering
happy paths (text / template / reaction / list-templates),
error paths (unknown_tool,invalid_argsfor wrong type +
missing required,WINDOW_CLOSEDfrom a typed SDK error),
surface invariants (no credential fields in any
inputSchema, stable orderings), resource reads
(whatsapp://templates,whatsapp://window/<phone>,
unknown URI fallback), prompt renders.
134 in-process MCP tests (was 112). E2E suite (8 skipped)
unchanged.
Docs
docs/mcp/embedded.md— toolset reference page (API,
stability, when not to use, surface parity guarantee).docs/cookbook/mcp/embedded-toolset.md— end-to-end recipe
showing the toolset inside a Next.js App Router MCP gateway
with prefix-based routing alongside another upstream (the
Site2Print + Alegra shape).docs/mcp/README.mdindex updated to surface the new page.MIGRATION.md§ "MCP server: 0.3.x → 1.0.0" gains a
"What's new in 0.4.0" subsection.
Coverage
- Statements 98.76 (was 98.6)
- Branches 76.86 (was 72.0)
- Functions 98.41 (was 100; new
toolset.tsline 273
optional-abortSignal-spread branch is unreached by tests) - Lines 98.76 (was 98.6)
Comfortably above the 95/65/95/95 gate.
Bundle
Library ESM 9.93 KB brotlied (was 8.54 KB; +1.4 KB for toolset
- types + register bridge). CLI 9.25 KB brotlied (was 8.78 KB).
Both well under their 200 KB / 300 KB budgets.
v0.7.4
Tooling (no SDK behaviour change)
Phase 4 of the Track F + test-coverage audit hardening pass. The
published artefact is functionally identical to 0.7.3; only the
build / CI tooling and CHANGELOG differ.
-
Bundle-size budgets (F6).
size-limitis wired into CI with
per-entry-point budgets defined inpackage.json:Entry point Budget @dojocoding/whatsapp(root, ESM/CJS)100 KB @dojocoding/whatsapp/express6 KB @dojocoding/whatsapp/web3 KB @dojocoding/whatsapp/hono3 KB @dojocoding/whatsapp/storage/redis2 KB @dojocoding/whatsapp/storage/postgres4 KB Limits are roughly 1.5× the current measured sizes, so a single PR
cannot double a bundle by accident (e.g. an accidental
import "lodash"). Runpnpm sizelocally to see the same
budget report CI runs.
v0.7.3
Tests (no SDK behaviour change)
Phase 3 of the Track F + test-coverage audit hardening pass.
572 tests pass (was 524 — +48 across the seven audit items below).
The published artefact is functionally identical to 0.7.2; only the
test suite and CHANGELOG differ.
- Property-based assertions with
fast-checkfor three
high-leverage modules:webhooks/signature.ts— HMAC verifier invariants across
random body / secret / header inputs.webhooks/dedupe.ts— dedupe-key identity + Unicode handling.client/retry.ts—fullJitterDelaymath bounded by
[floorMs, min(maxDelayMs, expCap)]for any RNG output.
- Concurrent dedupe race test — 100 parallel
handlePayload
calls with the same wamid invoke the handler exactly once;
validates the single-flight semantics under Meta's aggressive
retry pattern. - Storage failure propagation — explicit assertions that
WebhookDeduper,WindowTracker, andWebhookReceiver
surface storage errors rather than swallowing them silently. sendReplytemplate-path coverage — the window-exempt path
for template + reaction payloads throughsendReply, plus the
window-gated path for free-form payloads, all asserted against
captured wire bodies.- Local pack-contents smoke —
test/contract/pack-contents.test.ts
mirrors the CI "Verify pack contents (dry-run)" assertion so
pnpm testcatches afilesallowlist regression before CI.
v0.7.2
Changed (CI / repo hygiene only — no SDK behaviour change)
- Dependabot now opens grouped weekly PRs for npm + GitHub Actions
updates.@types/*, lint/format tooling, vitest/msw, and the
@opentelemetry/*family ship as batched sweeps. Major-version
bumps to peer-dep ecosystems (express, hono, ioredis, pg) are
ignored — those need an explicit decision about widening the
supported range. - CodeQL static-analysis workflow added (
.github/workflows/codeql.yml)
with thesecurity-extendedquery suite. Runs on push, PR, and
a weekly Monday schedule. Findings appear in the Security tab. pnpm audit --prod --audit-level=moderateruns on every CI build
(continue-on-error initially, so advisories surface in the run
log without blocking PRs).- PR template + three issue templates (bug, feature, compliance
drift) shipped..github/CODEOWNERSpins compliance-relevant
files and the release pipeline.
No npm-published artefact changes from 0.7.1.
v0.7.1
Added
verifySignatureOrThrow(input)— throwing variant of
verifySignatureexported from the root entry. Resolves silently
on a valid signature; throwsWebhookSignatureErroron bad HMAC,
missing header, malformed hex, or wrong byte length. Use this when
wiring your own HTTP layer (not the SDK's Express / web / Hono
adapters) and you want a typed error rather than a boolean.
Changed
- CI: bumped GitHub Actions to Node 24-compatible versions —
actions/checkout@v5,actions/setup-node@v6,
pnpm/action-setup@v6,actions/upload-artifact@v7,
softprops/action-gh-release@v3. Removes the deprecation banner
on every CI run; ready for Meta's 2026-06-02 Node 20 default
removal.
Tests
- Added
test/contract/public-surface.test.ts— a drift detector
asserting every documented value/class/factory across the root
entry and all five subpaths (/express,/web,/hono,
/storage/redis,/storage/postgres) is reachable at runtime.
If a sub-module export is added without being plumbed through, or
a documented export is accidentally renamed/removed, this test
fails before consumers do. - Added negative-path coverage for
WebhookSignatureError(5 new
tests viaverifySignatureOrThrow) andMockModeError(4 new
contract tests pinning the public shape). - Re-shimmed
test/integration/express/middleware.test.tsto use a
Promise-resolved-by-handler pattern instead of 5 mssetTimeout
waits and wall-clock ack-timing windows — mirrors the
determinism fix already applied to the web adapter test in 0.2.0.
524 tests pass (was 447).
v0.7.0
Added
- Authentication template (OTP) builder.
buildAuthTemplate({ to, name, language, otp, otpButtonIndex? })and the matching
client.sendAuthTemplate(...)produce the documented copy-code /
one-tap / zero-tap wire payload with the OTP code duplicated into
both the body and URL-button parameters (the canonical footgun
this builder exists to remove). OTP length validated against
Meta's 15-char ceiling. - Voice-note builder.
buildVoice({ to, id|link })and
client.sendVoice(...)produce audio messages withvoice: true,
triggering transcription support, auto-download, and the "played"
delivery status. - Carousel-template builder.
buildCarouselTemplate({ to, name, language, bodyParameters?, cards })and
client.sendCarouselTemplate(...)produce media-card carousel
template sends with 1–10 cards. Each card'scard_indexis
computed from iteration order; consumers can't misorder it. - Limited-time-offer template support. Three new
TemplateParameterunion variants:
TemplateParameterLimitedTimeOffer,
TemplateParameterCouponCode, andTemplateParameterPayload.
TemplateComponent.typewidens to accept"carousel"and
"limited_time_offer". Use via the existingbuildTemplate(...)
/
client.sendTemplate(...). - New
CarouselCardComponentexported type.AudioMessage.audio
gains an optionalvoice?: booleanfield.
Every wire shape is grounded in a Meta doc URL referenced in
source-file JSDoc and pinned via byte-for-byte snapshot tests:
- https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/authentication-templates/copy-code-button-authentication-templates/
- https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/audio-messages
- https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/marketing-templates/media-card-carousel-templates/
- https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/marketing-templates/limited-time-offer-templates/
v0.6.0
Added
- Redis and Postgres
Storageadapters at subpath exports.@dojocoding/whatsapp/storage/redisexports
createRedisStorage(client, options?). Takes an
ioredis-compatible client; uses nativeSET PX/SET NX
for TTL and atomicity.ioredisis an optional peer
dependency on^5.0.0.@dojocoding/whatsapp/storage/postgresexports
createPostgresStorage(client, options?)and
POSTGRES_STORAGE_SCHEMA: string. Takes apg-compatible
client; runs four SQL statements (SELECT, twoINSERT ... ON CONFLICT,DELETE).pgis an optional peer dependency on
^8.0.0.- Both adapters use minimal structural interfaces (
RedisLike,
PgLike) so the SDK doesn't import either library at runtime;
consumers pass any compatible client (including test fakes). - See
docs/storage.md.
- Shared
Storagecontract test:
test/unit/storage/contract.tsexports a parametrised suite
that every implementation (InMemoryStorage,
createRedisStorage,createPostgresStorage) runs against —
drift between implementations is impossible-to-not-notice.