One script tag that lets an AI agent get a human sales rep on the phone with the person it is helping — in about a minute, with their explicit say-so.
<script src="https://callingly.com/js/webmcp.js"
data-callingly-key="YOUR_WEBHOOK_KEY" async></script>That is the whole integration. The snippet registers two WebMCP tools on the page:
| Tool | What it does |
|---|---|
callingly-check-sales-availability |
Read-only. Is the team inside its hours, is a rep actually free right now, when is the next opening, and are call requests being accepted at all. |
callingly-request-sales-call |
Asks Callingly to ring the sales team and bridge the visitor on the number the agent passes. |
| URL | What it is |
|---|---|
| https://joesplumbing.callingly.com/ | The fictional plumber's site, with the snippet on it and nothing else to give it away. Ask your agent whether anyone is free, or to have someone call you. |
| https://callingly.com/webmcp | The agent console — the same snippet, plus a live view of which tools registered and every call an agent makes against them, with arguments and return values. Start here if you want to watch the tool calls. |
Both need a WebMCP-capable browser: ChatGPT's in-app browser with Settings →
Browser → Permissions → "Enable site tools" on, or Chrome 149+ with
chrome://flags/#enable-webmcp-testing enabled. The console says which of
those is missing if the tools do not appear.
Every B2B site has the same dead end: a visitor with a real question, and a contact form that answers in "1–2 business days". The visitor wants to talk to someone. The site can only offer them a form or a scheduling link three days out.
An agent browsing that page hits the same wall. It can read your pricing and summarise your docs, but the moment its user says "I just need to talk to a human about this", the best it can do is fill in the form and hope.
With these tools on the page, the agent can find out whether anyone is actually free — not whether the office is nominally open, but whether a rep is on, off another call and past their cooldown — and then get that rep dialling. The visitor's phone rings while they still have the page open and the question in their head.
What is newly possible: the agent negotiates the timing of a human conversation. "They're free now, want me to get someone on the line?" / "Nobody until 9am Tuesday — shall I have them call you then?" That is a conversation about human availability that a form cannot have and a chatbot has no authority to make good on.
git clone https://github.com/callingly/webmcp
cd webmcp
npm start # no dependencies; serves on http://localhost:8787Then open it in a WebMCP-capable browser:
- ChatGPT's in-app browser — WebMCP works out of the box, or
- Chrome 149+ with
chrome://flags/#enable-webmcp-testingenabled.
Three pages are served:
The before/after pair is the whole argument in one diff. Nothing else on the
page changes — the contact form still says 1–2 business days, because the human
path is not what this replaces. What changes is that an agent can now find out
whether anyone is free and get the visitor a phone call in the next minute.
Ask your agent things like:
"Is the sales team available to talk right now?" "When is the earliest I could speak to someone?" "Have someone call me on +1 415 555 0134 about pricing."
npm start runs a mock backend, so no real call is placed and the
console prints what would have been sent. To drive a real Callingly account,
pass the API origin and a webhook key:
http://localhost:8787/?api=https://callingly.com&key=YOUR_WEBHOOK_KEY
Set the mock's team name to match the page you are on:
DEMO_TEAM="Copperleaf Solar" npm start # the name the agent says back
DEMO_CLOSED=1 npm start # exercise the after-hours pathThe snippet is dependency-free ES5 in a single IIFE, ~500 lines. It:
- reads its config from the script tag (
data-callingly-key,data-api,data-team,data-prefix) orwindow.callinglySettings; - registers both tools via
document.modelContext.registerTool(tool, { signal }), falling back tonavigator.modelContextfor older origin-trial builds; - retries registration when
document.modelContextis not there yet — agent browsers inject it at different points in page load — and recovers a tool whose registration rejects after the retry window has closed; - unregisters through an
AbortController, since WebMCP has nounregisterTool; - returns MCP
contentresults, usingisErrorfor refusals so the agent gets a sentence it can act on rather than an exception; - exposes everything on
window.callinglyso ordinary page code, or an extension that does not speak WebMCP yet, can call the same functions.
The server side is two public endpoints:
GET /embed/v1/{key}/availability
POST /embed/v1/{key}/calls
keyed by a Callingly webhook integration's slug. Accepted calls are handed to
the same queued pipeline every webhook lead goes through — routing rules,
de-duplication, then dialling — so the endpoint answers 202 rather than
pretending to know which rep will pick up. See
server/README.md for the real implementation and the
reasoning behind the key model, the rate limits and the field-mapping guard.
webmcp.js The snippet, exactly as production serves it
demo/index.html Agent console — tools and live tool calls
demo/joes-plumbing/ A fictional customer site, before and after the one line
site/ Just the "after" page, built for hosting — see DEPLOY.md
build-site.mjs Generates site/ from demo/joes-plumbing/after/
demo/server.js Dependency-free mock API + static server
server/ The real Laravel implementation, for reading
test/ Browser checks for both demo pages
The server side has a Pest feature suite (server/EmbedWebMcpTest.php):
availability open and closed, unknown keys, the exact payload handed to the
queue, undialable numbers, the origin allow-list, the field-mapping guard and
CORS preflight.
The client side has browser checks that drive a stubbed document.modelContext
the way an agent browser would:
npm install # playwright, for the browser checks only
npx playwright install # the browser binary itself
npm start & # in another shell
npm testThey assert that both tools register, that availability is marked
readOnlyHint, that a call reaches the endpoint with the arguments the agent
passed, that a call with no number is refused with an actionable message, and
that the tools recover when the model context arrives late or is replaced.
MIT — see LICENSE.
