Frequently Asked Questions

WebMCP Technical Integration & Best Practices

What is WebMCP and how does it enable AI agents to interact with websites?

WebMCP is a standard proposed by Google that allows AI agents to interact with websites through structured tools, rather than by reverse-engineering user interfaces. It provides both declarative and imperative APIs, enabling websites to expose their capabilities directly to AI agents for faster, more reliable, and precise interactions. [Source]

Why does my AI agent get 'Message sent' instead of the real answer when using WebMCP?

This happens when the WebMCP tool's execute() function returns immediately after sending a message, rather than waiting for the actual response. The correct approach is to return a Promise that resolves only when the real answer is received, ensuring the AI agent gets the intended response. [Source]

What is the correct way to handle asynchronous responses in a WebMCP tool?

The correct way is to have the execute() function return a Promise that resolves with the actual answer, not just an acknowledgment. This ensures the AI agent waits for the full response, even if it takes several seconds. [Source]

How do I implement a WebMCP tool that works with a chat widget inside an iframe?

To handle cross-origin iframes, use window.postMessage to send the question from the parent page to the iframe. The parent page's execute() function should return a Promise that resolves when the iframe posts the answer back, using a unique callId for correlation. [Source]

What are the three rules for building reliable WebMCP tools?

The three rules are: 1) Always return a Promise from execute(); 2) Never reject the Promise—resolve with an error message instead; 3) Always set a timeout to avoid hanging indefinitely if a response never arrives. [Source]

How does the WebMCP Gateway ensure AI agents receive actual answers from websites?

The WebMCP Gateway uses a headless browser to call the tool's execute() function and waits for the Promise to resolve, ensuring the AI agent receives the real answer, not just a confirmation. [Source]

Can you provide a production-ready example of a WebMCP integration?

Yes, a production-ready script is provided in the blog post. It registers an ask_question tool, handles async responses, and works with both Chrome's native WebMCP and the WebMCP Gateway. See the full code example in the original article. [Source]

Why is a unique callId important in WebMCP async patterns?

A unique callId ensures that concurrent requests are correctly matched to their responses, preventing one agent from receiving another's answer. This is critical for reliability in multi-agent or multi-request scenarios. [Source]

How does WebMCP handle widgets that need to be opened before sending messages?

For widgets that start hidden, the execute() function should check if the widget is open, open it if necessary, and only send the message once the widget is ready. This ensures the question is delivered and the response is received reliably. [Source]

What are common mistakes when implementing WebMCP tools?

Common mistakes include returning immediately from execute() without waiting for the answer, not handling errors gracefully, and failing to set a timeout, which can cause the AI agent to hang indefinitely. [Source]

How does the WebMCP Gateway parse responses from website tools?

The Gateway waits for the Promise from execute() to resolve, then parses the result. If the result is a string, it uses that as the answer; if it's in MCP format, it extracts the text; otherwise, it converts the object to a JSON string. [Source]

What is the long-term vision for the WebMCP Gateway?

The long-term vision is for every website to function as an API, allowing AI agents to gather and compare information across sites efficiently, such as comparing vendor pricing or policies in real time. [Source]

How can I ensure my WebMCP tool doesn't hang indefinitely?

Always set a timeout in your execute() function so that if a response is not received within a reasonable period (e.g., 60 seconds), the Promise resolves with an error message, preventing indefinite hangs. [Source]

What are the benefits of using Promises in WebMCP tool development?

Promises allow the execute() function to wait for asynchronous processes (like LLM responses or iframe communication) to complete, ensuring the AI agent receives the actual answer rather than an immediate acknowledgment. [Source]

How does WebMCP improve the reliability of AI-driven website interactions?

By providing structured APIs and enforcing async patterns, WebMCP ensures that AI agents can reliably interact with websites, receive accurate answers, and avoid common pitfalls like race conditions or missed responses. [Source]

What use cases does WebMCP enable for AI agents?

WebMCP enables AI agents to perform tasks like comparing vendor pricing, retrieving company policies, and querying product details directly from websites, streamlining research and decision-making for users. [Source]

How does WebMCP handle errors and exceptions in tool execution?

Errors should be caught and the Promise resolved with a user-friendly error message, rather than rejecting the Promise, to ensure the AI agent can handle the situation gracefully. [Source]

What is the role of Playwright in the WebMCP Gateway?

Playwright is used by the WebMCP Gateway to run a headless browser session, inject scripts, and await the resolution of Promises from WebMCP tools, ensuring accurate and complete responses are captured. [Source]

How can I test if my website is ready for AI agent interactions via WebMCP?

You can use the WebMCP Gateway or Chrome's native WebMCP support to register tools and test async interactions, ensuring your execute() functions follow best practices for Promises, error handling, and timeouts. [Source]

Salespeak Product Features & Capabilities

What is Salespeak.ai and what does it do?

Salespeak.ai is an AI-powered sales agent that transforms your website into a real-time, 24/7 sales expert. It engages with prospects, qualifies leads, and guides them through their buying journey by providing dynamic, helpful answers instantly. [Source]

What are the key features of Salespeak.ai?

Key features include 24/7 customer engagement, expert-level conversations, seamless CRM integration, actionable insights from buyer interactions, and a user-friendly, quick setup process. [Source]

How does Salespeak.ai integrate with CRM systems?

Salespeak.ai integrates seamlessly with popular CRM platforms such as Salesforce, Pardot, and HubSpot, enabling real-time CRM sync and streamlined sales operations. [Source]

Does Salespeak.ai support custom integrations or APIs?

Yes, Salespeak.ai supports custom integration using a webhook, allowing you to connect to downstream systems. For more details, consult Salespeak's official resources or support team. [Source]

How quickly can Salespeak.ai be implemented?

Salespeak.ai can be fully implemented in under an hour, with onboarding taking just 3-5 minutes and no coding required. Customers have reported seeing live results the same day. [Source]

What security and compliance certifications does Salespeak.ai have?

Salespeak.ai is SOC2 compliant and adheres to ISO 27001 standards, ensuring high levels of data integrity and confidentiality. For more details, visit the Salespeak Trust Center. [Source]

What measurable results have customers achieved with Salespeak.ai?

Customers have reported a 40% average increase in close rates, a 17% average increase in ticket price, and a 3.2x increase in qualified demos in 30 days. Notable examples include Cardinal HVAC and Pella Windows improving their sales metrics significantly. [Source]

How easy is it to use Salespeak.ai for non-technical users?

Salespeak.ai is designed for ease of use, with onboarding taking just 3-5 minutes and no coding required. Customers like RepSpark and Tim McLain have reported setting up and seeing results within 30 minutes, without needing demos or onboarding calls. [Source]

What types of companies and roles benefit most from Salespeak.ai?

Salespeak.ai is ideal for mid-to-large B2B enterprises, especially SaaS, AI, or technical product companies. Key roles include CMOs, Demand Generation Leaders, and RevOps Leaders seeking to improve conversion rates and pipeline quality. [Source]

What pain points does Salespeak.ai address for businesses?

Salespeak.ai addresses pain points such as lack of 24/7 customer interaction, inefficient lead qualification, misalignment with buyer needs, complex implementation, and concerns about ROI and pricing. [Source]

How does Salespeak.ai differentiate itself from traditional chatbots?

Unlike basic chatbots, Salespeak.ai provides expert-level, personalized conversations, real-time adaptive Q&A, deep product training, and seamless CRM integration, focusing on the buyer's journey rather than just sales team processes. [Source]

What is the pricing model for Salespeak.ai?

Salespeak.ai offers a month-to-month pricing model based on the number of conversations per month, with no long-term contracts. A free trial with 25 conversations is available. [Source]

What support options are available for Salespeak.ai customers?

Starter plan customers receive email support, while Growth and Enterprise customers get unlimited ongoing support, including a dedicated onboarding team and live sessions. Training videos and documentation are also provided. [Source]

Can you share customer success stories using Salespeak.ai?

Yes, case studies such as RepSpark and Faros AI demonstrate measurable growth and improved sales outcomes using Salespeak.ai. See the Salespeak Success Stories for details. [Source]

What is the vision and mission of Salespeak.ai?

Salespeak.ai's vision is to delight, excite, and empower buyers by radically rewriting the sales narrative, prioritizing buyer experiences over quotas. Its mission is to transform B2B sales by acting as an AI brain and buddy for custom engagement and delight. [Source]

Where can I find more technical resources and blog posts about Salespeak and WebMCP?

You can read more technical articles and insights on the Salespeak Blog, including deep dives into WebMCP, agent analytics, and AI-driven sales strategies. [Source]

WebMCP Deep Dive: How to Actually Wait for Answers (Not Just Send Messages)

A red, orange and blue "S" - Salespeak Images

WebMCP Deep Dive: How to Actually Wait for Answers (Not Just Send Messages)

Omer Gotlieb Cofounder and CEO - Salespeak Images
Salespeak AI
9 min read
March 20, 2026

WebMCP Deep Dive: How to Actually Wait for Answers (Not Just Send Messages)

The async response pattern that separates working WebMCP tools from broken ones — with copy-paste code for every scenario.

Here's the mistake everyone makes when they first wire up a WebMCP tool. They register a tool that sends a message to their chatbot. The chatbot eventually responds. But the tool already returned "Message sent!" three seconds ago, and the AI assistant has no idea what the actual answer was.

// THE WRONG WAY — AI gets "Message sent!" instead of the answer
navigator.modelContext.registerTool({
  name: "ask_question",
  execute: ({ question }) => {
    chatWidget.sendMessage(question);
    return { content: [{ type: "text", text: "Message sent!" }] };
    // The real answer arrives 5 seconds later. Nobody's listening.
  }
});

The AI assistant receives "Message sent!" and moves on. Your chatbot talks to the void. The entire point of WebMCP — letting AI agents interact with your website's intelligence — is lost.

The fix isn't complicated, but you need to understand one thing first.

The Core Insight: Promises Are the Mechanism

When Chrome (or a headless browser like the WebMCP Gateway) calls your tool's execute() function, it runs:

const result = await tool.execute(args);

That await means the caller will wait as long as your Promise takes to resolve. Five seconds? Fine. Thirty seconds while an LLM generates a thoughtful response? Also fine. The caller doesn't time out after 200ms and move on — it waits for the real answer.

Your execute() function just needs to return a Promise that resolves with the actual response. Not "got it, working on it." The response itself.

Here are three patterns, from simplest to most real-world.

Pattern 1: Direct API Call

If your website has a backend that answers questions over HTTP, this is all you need:

navigator.modelContext.registerTool({
  name: "ask_question",
  description: "Ask about our products and services",
  inputSchema: {
    type: "object",
    properties: {
      question: { type: "string", description: "The question to ask" }
    },
    required: ["question"]
  },
  execute: async ({ question }) => {
    // This fetch might take 5-30 seconds for an LLM-backed endpoint.
    // The async/await keeps the Promise pending until it resolves.
    const response = await fetch('/api/chat', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ question })
    });
    const data = await response.json();

    return {
      content: [{ type: 'text', text: data.answer }]
    };
  }
});

That's it. The async keyword makes execute return a Promise. The await fetch(...) keeps that Promise pending until the API responds. The caller — whether it's Chrome natively or a headless browser — waits the whole time and gets the real answer.

What actually happens:

T+0s:    AI calls execute({ question: "What's your pricing?" })
T+0s:    fetch('/api/chat') fires
T+0.2s:  Request reaches your server
T+3s:    LLM generates the response
T+3.1s:  fetch resolves → data.answer = "Our Pro plan starts at $99/mo..."
T+3.1s:  Promise resolves → AI receives the actual answer

Three seconds of waiting. That's the whole trick.

Pattern 2: iframe postMessage Bridge

This is where most people get stuck, because this is how most real websites actually work.

Your chat widget doesn't live on the parent page. It lives in an iframe — loaded from a different domain, running its own JavaScript, talking to its own LLM backend. The parent page can't call the widget's API directly. Cross-origin rules prevent it.

The bridge: window.postMessage. Parent sends the question to the iframe, iframe processes it, iframe posts the answer back. The key is making execute() return a Promise that stays pending until that answer arrives.

The Parent Page (Your Website)

let callCounter = 0;
const TIMEOUT_MS = 60_000;

navigator.modelContext.registerTool({
  name: "ask_question",
  description: "Ask our AI assistant and get the full answer",
  inputSchema: {
    type: "object",
    properties: {
      question: { type: "string", description: "Your question" }
    },
    required: ["question"]
  },
  execute: ({ question }) => {
    if (!question || typeof question !== 'string') {
      return { content: [{ type: 'text', text: 'Error: question is required' }] };
    }

    // Unique ID for this call — critical for concurrency
    const callId = `webmcp_${++callCounter}_${Date.now()}`;

    // Return a Promise that stays PENDING until the iframe responds
    return new Promise((resolve) => {
      let timeoutId;

      const cleanup = () => {
        clearTimeout(timeoutId);
        window.removeEventListener('message', onMessage);
      };

      // Listen for the iframe's response
      const onMessage = (event) => {
        // Match: has a botResponse AND the callId matches ours
        if (
          event.data &&
          typeof event.data === 'object' &&
          'botResponse' in event.data &&
          event.data._callId === callId
        ) {
          cleanup();
          resolve({
            content: [{ type: 'text', text: event.data.botResponse }]
          });
        }
      };

      window.addEventListener('message', onMessage);

      // Safety net — don't hang forever
      timeoutId = setTimeout(() => {
        cleanup();
        resolve({
          content: [{
            type: 'text',
            text: `The assistant did not respond within ${TIMEOUT_MS / 1000} seconds.`
          }]
        });
      }, TIMEOUT_MS);

      // Send the question to the iframe
      const iframe = document.querySelector('#chat-widget-iframe');
      iframe.contentWindow.postMessage(
        { question: question, _callId: callId },
        'https://your-widget-domain.com'
      );
    });
  }
});

The iframe (Chat Widget Side)

Inside the iframe, receive the question, do the LLM work, post the answer back:

window.addEventListener('message', async (event) => {
  // Only handle messages with a question and callId
  if (!event.data?.question || !event.data?._callId) return;

  const { question, _callId } = event.data;

  try {
    // Your LLM call — this is the slow part
    const answer = await generateAnswer(question);

    // Post the answer back to the parent, echo the callId
    window.parent.postMessage({
      botResponse: answer,
      _callId: _callId
    }, event.origin);

  } catch (error) {
    window.parent.postMessage({
      botResponse: `Sorry, something went wrong: ${error.message}`,
      _callId: _callId
    }, event.origin);
  }
});

The Full Timeline

T+0s:    AI calls execute({ question: "What's your pricing?" })
T+0ms:   callId = "webmcp_1_1710000001" generated
T+0ms:   Promise created — now PENDING
T+0ms:   message listener registered on parent window
T+0ms:   60s timeout started
T+1ms:   postMessage({ question, _callId }) → iframe
         ┌───────────────────────────────────────┐
T+1ms:   │ iframe receives the message           │
T+10ms:  │ iframe calls LLM backend              │
T+4s:    │ LLM generates the response            │
T+4.1s:  │ iframe postMessage({ botResponse,     │
         │   _callId }) → parent                 │
         └───────────────────────────────────────┘
T+4.1s:  onMessage fires, callId matches ours
T+4.1s:  cleanup() — remove listener, clear timeout
T+4.1s:  resolve({ content: [{ text: "Our pricing..." }] })
T+4.1s:  Promise RESOLVES → AI receives the actual answer

The AI waited 4.1 seconds. It got the real LLM response, not "message sent." That's the difference between a useful integration and a broken one.

Why `_callId` Matters (Call Correlation)

Without correlation IDs, concurrent requests break in ways that are hard to debug:

// Without callId — BROKEN
Agent A asks: "What's your pricing?"      → widget → "Starting at $99/mo"
Agent B asks: "How do returns work?"      → widget → "30-day return policy"

// Agent A's listener picks up Agent B's answer
// because both listeners match on 'botResponse' existing

With _callId:

// With callId — CORRECT
Agent A: callId="webmcp_1_..." → iframe echoes _callId → only A's listener matches
Agent B: callId="webmcp_2_..." → iframe echoes _callId → only B's listener matches

Simple pattern: send a unique ID with the request, echo it in the response, filter on it in the listener. Two agents can ask questions at the same time and each one gets its own answer.

Pattern 3: Widget That Opens on Demand

Some chat widgets start minimized or hidden. They need to be opened or initialized before they can receive messages. This is common with third-party widgets that lazy-load their iframe.

execute: ({ question }) => {
  const callId = `webmcp_${++callCounter}_${Date.now()}`;

  return new Promise((resolve) => {
    let timeoutId;

    const cleanup = () => {
      clearTimeout(timeoutId);
      window.removeEventListener('message', onMessage);
    };

    const onMessage = (event) => {
      if (event.data?.botResponse && event.data._callId === callId) {
        cleanup();
        resolve({ content: [{ type: 'text', text: event.data.botResponse }] });
      }
    };

    window.addEventListener('message', onMessage);

    timeoutId = setTimeout(() => {
      cleanup();
      resolve({
        content: [{ type: 'text', text: 'Request timed out after 60 seconds.' }]
      });
    }, 60_000);

    // The new part: open widget first if needed, then send
    const sendQuestion = () => {
      const iframe = document.querySelector('#chat-widget-iframe');
      iframe.contentWindow.postMessage(
        { question, _callId: callId },
        widgetOrigin
      );
    };

    if (!isWidgetOpen()) {
      openWidget(() => {
        // Callback fires once widget iframe is loaded and ready
        sendQuestion();
      });
    } else {
      sendQuestion();
    }
  });
}

The AI agent's question opens the chat, sends the message, waits for the LLM response — all invisible to the user. No UI flashing, no visible click sequence. The headless browser handles it behind the scenes.

The Three Rules of WebMCP `execute()`

After building the gateway and testing against a bunch of real-world sites, we've boiled it down to three rules that prevent basically every failure mode.

Rule 1: Always Return a Promise

The most common bug. Someone writes a .then() chain inside execute but forgets to return it:

// BROKEN — execute returns undefined
execute: ({ question }) => {
  fetch('/api/chat', { ... }).then(r => r.json()).then(data => {
    return { content: [{ type: 'text', text: data.answer }] };
    // This return is inside the .then() callback — it goes nowhere
  });
  // execute itself returns undefined
}

// FIXED — use async/await
execute: async ({ question }) => {
  const resp = await fetch('/api/chat', { ... });
  const data = await resp.json();
  return { content: [{ type: 'text', text: data.answer }] };
}

// ALSO WORKS — explicit Promise
execute: ({ question }) => {
  return new Promise((resolve) => {
    // ... resolve when the answer arrives
  });
}

If execute returns undefined, the AI gets nothing. Always return either an async function result or an explicit new Promise(...).

Rule 2: Never Reject — Resolve with an Error Message

// BAD — rejected Promise may crash the caller
execute: async ({ question }) => {
  const resp = await fetch('/api/chat', { ... }); // throws on network error
  // Unhandled rejection → caller gets a crash, not an answer
}

// GOOD — catch errors and resolve with a message
execute: async ({ question }) => {
  try {
    const resp = await fetch('/api/chat', { ... });
    const data = await resp.json();
    return { content: [{ type: 'text', text: data.answer }] };
  } catch (error) {
    return {
      content: [{ type: 'text', text: `Error: ${error.message}` }]
    };
  }
}

The AI can understand "Error: network timeout" and either retry or tell the human. A rejected Promise is just a crash log that nobody reads.

Rule 3: Always Set a Timeout

// DANGEROUS — hangs forever if iframe never responds
execute: ({ question }) => {
  return new Promise((resolve) => {
    window.addEventListener('message', (event) => {
      // What if the iframe crashed? This listener waits forever.
    });
    iframe.contentWindow.postMessage({ question }, origin);
  });
}

// SAFE — resolve with timeout after 60 seconds
execute: ({ question }) => {
  return new Promise((resolve) => {
    const timeout = setTimeout(() => {
      cleanup();
      resolve({
        content: [{ type: 'text', text: 'Request timed out after 60 seconds.' }]
      });
    }, 60_000);

    // ... listener and postMessage logic
  });
}

60 seconds is generous for most LLM-backed tools. If your backend is consistently faster, tighten it. But never skip the timeout entirely — a hanging Promise blocks the AI indefinitely, and there's no way for it to recover.

How the WebMCP Gateway Handles All of This

The WebMCP Gateway runs this whole flow from the outside using a headless browser:

1. Inject before page load — monkey-patches registerTool() to capture the execute callback

2. Navigate — Chromium loads the page, all JavaScript runs normally

3. Wait for tools — polls window.__webmcp_tools until tools appear

4. Call the toolawait tool.execute(args) and wait for the Promise

# What actually runs inside the headless browser:
call_result = await page.evaluate("""
    async ([toolName, args]) => {
        const tool = window.__webmcp_tools[toolName];
        const result = await tool.execute(args);  // WAITS for the real answer
        // ... parse the result ...
        return { success: true, answer: parsedAnswer };
    }
""", [tool_name, call_args])

Playwright's page.evaluate() with an async function properly awaits the inner Promise. Even if the tool's execute() takes 30 seconds (parent → iframe → LLM → postMessage → resolve), the gateway waits the full duration.

That's why the gateway can "ask any website" — it doesn't just fire off a message. It sits through the entire async dance and comes back with the actual answer.

Complete Copy-Paste Example

Here's a production-ready WebMCP integration you can drop into any page. It works with both Chrome's native WebMCP support and the WebMCP Gateway:

<script>
(function() {
  const check = setInterval(() => {
    if (!navigator.modelContext) return;
    clearInterval(check);

    let callCount = 0;

    navigator.modelContext.registerTool({
      name: "ask_question",
      description: "Ask a question about [Your Company] — products, pricing, support, anything.",
      inputSchema: {
        type: "object",
        properties: {
          question: {
            type: "string",
            description: "The question or request"
          }
        },
        required: ["question"]
      },
      execute: async ({ question }) => {
        callCount++;
        console.log(`[WebMCP] Call #${callCount}: "${question}"`);

        try {
          const response = await fetch('/api/assistant', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
              question,
              source: 'webmcp',
              call_id: callCount
            })
          });

          if (!response.ok) {
            return {
              content: [{
                type: 'text',
                text: `Our assistant is temporarily unavailable (HTTP ${response.status}).`
              }]
            };
          }

          const data = await response.json();

          return {
            content: [{
              type: 'text',
              text: data.answer || 'No answer available.'
            }]
          };

        } catch (error) {
          return {
            content: [{
              type: 'text',
              text: `Connection error: ${error.message}. Please try again.`
            }]
          };
        }
      }
    });

    console.log('[WebMCP] Tool registered: ask_question');
  }, 50);
})();
</script>

Add this to any page. Implement /api/assistant on your backend. Your website is now reachable by any AI assistant through WebMCP — whether Chrome calls it natively or the gateway calls it through a headless browser.

What This Actually Enables

Think about what becomes possible when AI agents can ask websites questions and get real answers:

"Compare pricing across three vendors." Claude calls three websites, each one answers from its own knowledge base, and Claude synthesizes a comparison table. No tabs. No copying and pasting.

"What does this company say about their refund policy?" Instead of reading through a help center and hoping you found the right page, the AI asks the site's own assistant and gets the authoritative answer.

"Find me a SaaS tool that integrates with Salesforce and costs under $200/month." The AI checks five vendor sites, asks each one about Salesforce integration and pricing, and gives you a shortlist.

The website decides what to expose. The AI decides what to ask. The Promise-based execute() pattern makes sure the AI actually gets the answer.

That's WebMCP.

Built by [Salespeak AI](https://salespeak.ai). [WebMCP Gateway](https://github.com/salespeak-ai/webmcp-gateway) is open source under the MIT license.

No items found.