WebMCP lets a web page expose tools that AI agents can discover and execute inside the browser. That sounds simple until you want to test those too...
For further actions, you may consider blocking this person and/or reporting abuse
Interesting post! ๐ WebMCP + Playwright + AI seems like a really powerful combination. I'm also experimenting with AI and Playwright for E2E testing right now, and I'm surprised by how much Playwright can do. It makes many tasks much easier than I expected!
Thanks! ๐
Nice, AI + Playwright for E2E testing sounds really interesting. Iโm curious how you are using it, because the first thing that comes to my mind is high token consumption. ๐
I'm mainly using AI to create tests rather than run them, so the token usage isn't exceptionally high. ๐ If I gather enough useful content and experience, I'd like to write a DEV post about it.
Nice! That sounds like a great topic. Looking forward to the post! ๐
Thank you! ๐บ
playwright for MCP tool testing is a clever pivot - usually reach for it for browser automation not agent harness work. curious if gemini calls stay within context limits or if you needed chunking on the tool responses.
Thanks! For the Gemini free tier, it was surprisingly okay.
I think I only hit the limit with Gemini 3.5 Flash. The other models were fine without chunking, at least for this experiment, because the tool responses were still quite small.
makes sense - flash is the one that runs into rate limits fastest on free tier even with small payloads. the response size being small probably helped though - once tool calls start returning large blobs it gets painful fast.
Really interesting approach, Daniel. I hadn't really thought about using Playwright as the bridge here instead of building another extension, but it makes a lot of sense after reading this.
Also appreciate how you broke it down step by step. Made it much easier to follow.
Thanks for the kind words, Hemapriya! Iโm glad you found it interesting and easy to follow. That was exactly what I hoped for with the breakdown.
Quick question on the safety check before executing a tool. Right now you allow only getGameState and reject anything else, which is a clean guard while there's just one tool. Once this grows into the full loop where Gemini can call whatever the page exposes, how are you thinking about that check? Do you keep an allowlist you maintain by hand, or trust the page to only ever expose tools that are safe to run? I ask because the second the model picks the name, the page becomes the real trust boundary, and I'm curious where you'd draw the line.
Great question, and yes, I would definitely not blindly trust the model here.
For this proof of concept, I allow only
getGameStatebecause I wanted to prove the bridge first. In the full loop, I still keep an allowlist on the agent side, something likeconst PLANNING_TOOL_NAMES = ["getGameState","checkPlan", "submitPlan"];. The page can expose multiple WebMCP tools, but the external agent should decide which of them it is willing to execute.So for me, the page is one trust boundary, but the agent still needs its own execution policy. You have to trust the page/tool provider at least a little, because the tools are implemented there, but model-selected tool calls should still be treated as untrusted input, especially for state-changing tools like
submitPlan.Hey Daniel, I've been building almost the same thing on our end โ Playwright + DeepSeek V4 instead of Gemini, Python instead of TS โ but the idea is the same: skip the middleware, plug a capable LLM straight into the browser via Playwright. Your executeWebMcpTool helper is cleaner than what I cobbled together though. Definitely stealing that pattern next time ๐
Also, smart call going with Gemini 2.5 Flash Lite for this โ low-latency function calling fits tool orchestration really well. Did the free tier give you any rate limit headaches?
Nice! Iโd love to see your results.๐ Are you planning to write a post about it?
Feel free to use any of the code! At first, I also wanted to create a generic mapper that would map WebMCP tool definitions to Gemini API tools, but that felt like overkill for this proof of concept and also for the game agent. ๐
There are actually different rate limits for different models. I hit the limit with Gemini 3 Flash multiple times, especially when the AI started playing level two. But Gemini 2.5 Flash and Gemini 2.5 Flash Lite were fine. And when I reached the limit, I just switched the model, so overall it was okay.
Not planning to write about it just yet โ the 36 Stratagems series is eating all my brain cells ๐ Might open source the framework down the road though, but I wanna put it through its paces first. Real-world usage always surfaces the kind of bugs you'd never catch in a demo. Need a few more rounds of that before I'd feel good about putting it out there.
I saw that you started the series, and Iโm definitely planning to read it!
Oh, so you have a framework around WebMCP? Cool! ๐ฅ Thatโs a much higher level than proof of concept. ๐
And yeah, a demo and a real app are like a seed and a plant. Real-world usage always shows what actually grows.
"Framework" is generous ๐ It's more of a glue script that grew legs. But it's been catching real bugs so far, and that's what matters.
Looking forward to hearing what you think of the series when you get to it!
That actually sounds good, even for a script. If itโs already catching real bugs, then itโs doing its job.
Definitely! I like your writing style, so Iโm looking forward to reading it too.
Using Playwright as the execution layer instead of another Chrome extension is the part I keep coming back to, because it means one agent can drive tools across pages that never shipped an inspector. The failure I would watch for is the model calling a WebMCP tool whose page state changed after discovery, so the tool signature and the live DOM disagree. Did you hit any drift between the discovered tool list and what actually executed once the page had been interacted with?
That is a good point. In my case, I did not really hit this problem because the WebMCP tool list was stable. Tools like
getGameState,checkPlan, andsubmitPlankept the same signatures during the whole session.What changed was only the game state returned by
getGameState, not the tool schema itself. So I did not see any drift between discovered tools and executed tools in this example.I think this highly depends on the design and architecture of the page. If the page can expose different tools depending on UI state, navigation or lifecycle state, the agent should probably rediscover tools or validate them before execution. The page should also be designed with that in mind and keep tool contracts stable where possible.
Using Playwright to get a real browser context instead of building a second Chrome extension is a clean call, since WebMCP tools only make sense when the page itself is live. The part I always end up wrestling with is the discovery step: when modelContext exposes a dozen tools, do you hand Gemini all of them every turn, or filter the schema down first so the model does not misfire on the wrong tool call?
Thanks for the comment!
Yes, I think it makes sense to pre-filter a large number of tools before sending them to the model. It helps with token consumption, and it can also make it easier for the model to choose the right tool.
In my full game-agent implementation, I only have three tools, but I still do some simple filtering based on the current state. At the start of a level, I send only the
getGameStatetool, because that is always the first step. After that, in the planning loop, I send only the tools that make sense for the current phase, withoutgetGameState.So yes, I would say the agent should not blindly send every available tool every time. It should send only the tools that make sense for the current state.
Nice minimal build. The thing worth flagging once an agent can drive a browser: the interesting risk stops being can it do the task and becomes what stops it from doing the wrong one. A Playwright agent with a goal will find a way to that goal, including paths you didn't intend. Minimal is the right way to learn it; the moment it touches anything real, the bounds on what it's allowed to click become the actual design work.
Thanks for the comment!
Yes I agree. Once an agent is interacting with a real prod app, the key concern isnโt just whether it can complete a task, but also what itโs permitted to do along the way.
For this article, I intentionally kept things minimal to focus on the bridge itself. In a real-world scenario, though, the agent would need well-defined boundaries, such as restricted tools and actions, validation of page state and likely user confirmation before performing any sensitive or state-changing operations.
Thank you post good article,
This article is very interesting because as I am a software developer, I have a little experience with AI.
I only use the Cursor but I do not know this program how to run it correctly.
I hope that y you will post next article and I want to communicate with you
thank.
Thank you for the kind words!
Cursor is a nice way to start using AI for coding.
Iโm glad you found it interesting, and Iโll try to share more articles like this. Happy to connect here on DEV!
The bridge works, but the schema round-trip is where this pattern quietly breaks. You're reading
tool.inputSchemafromgetTools(), but then in the agent you hardcode a GeminifunctionDeclarationsentry by hand. The moment you make this generic โ mapping the page'sinputSchemastraight into Gemini'sparametersโ you'll hit the fact that Gemini's function-calling schema is a constrained subset of JSON Schema (OpenAPI-flavored). WebMCP tools can expose things Gemini won't accept:oneOf/anyOf,$ref, tuple-styleitemsarrays, unboundedadditionalProperties. A page author writing a rich Zod-to-JSON-Schema tool has no idea their schema won't survive the trip, and the failure shows up as an opaque 400 fromgenerateContent, not a clear "unsupported keyword" error.Worth adding a normalization/validation layer between
getTools()and the model rather than passing the schema through raw โ even just stripping unsupported keywords and logging what you dropped. That's the difference between "plays two levels" and "works on any WebMCP page you throw at it," which is the actual promise of going generic here.Thanks for actually checking the code and for the thoughtful comment!
You are right. In this proof of concept, I hardcoded the Gemini function declarations instead of building a generic mapper between
getTools()from WebMCP and thetoolsobject expected by the Gemini API.I also mentioned this limitation in the article. The reason was mostly scope. A proper mapper/normalization layer would probably be larger than the proof of concept itself, especially if it needs to handle unsupported schema features, strip or transform keywords, and log what was changed.
For a real generic WebMCP agent, I agree that passing the schema through raw would not be enough. There should be a validation/normalization layer between WebMCP tool discovery and the model-specific function-calling schema.
For this article, I wanted to prove the bridge first: discover tools, execute them through Playwright, and connect that loop to Gemini. For my game agent implementation, I intentionally skipped the generic mapper because the agent is designed specifically for the game, so it was much easier to hardcode the tools while everything is still experimental.
But yes, making it work reliably across arbitrary WebMCP pages would require exactly the kind of schema handling you described.
This is a great practical guide MCP feels real when you can actually build something with it. Playwright + Gemini is a clean combo: one for doing, one for deciding.
Curious about state handling does the agent remember previous steps?
Thanks for sharing!
Thanks, Harsh, glad you liked it!
Not in this minimal proof of concept. But the agent I implemented for the game does keep previous steps in a simple conversation/history array. Basically, I concatenate the previous contents with the new request contents:
Then I send that full context back to Gemini, so the model can โrememberโ previous steps through the provided conversation history. It is not ideal because of token consumption, so there is still a lot of room for improvement, but it works well for this game + WebMCP experiment.
This is a really cool experiment! As someone who's currently building with AI as a "vibe coder", it's fascinating to see how Playwright can bridge WebMCP with stronger models.
I like how you used Playwright to bypass the extension limits - it's a smart and practical approach. The idea of letting Gemini decide which tool to call and then executing it through WebMCP feels like a foundation for something much bigger.
A question: Have you tried running this in a headless mode (without the browser UI) to make it more like a background service? Or is the visual feedback still necessary for debugging?
Also, nice touch with the puzzle game scenario - it makes the concept more tangible. Thanks for sharing this! ๐
Thanks, Iโm glad you found it fascinating and useful! ๐
WebMCP is still experimental and it does not support headless mode yet. The visual feedback was actually very useful for debugging, though.
Here are the docs for more info, if you are interested:
developer.chrome.com/docs/ai/webmc...
And honestly, for vibe coding, this kind of setup can be really fun to experiment with. Playwright gives you a lot of control, so it is a nice way to test what AI agents can actually do in the browser.
The minimal version is valuable because it shows the control loop clearly. The next hardening step is usually permissions, page-state verification, and making sure the agent knows when not to click.
Thanks for the comment!
You are right and I think this is exactly where the design and architecture matter.
Some of it should be handled on the page side, by exposing safe and predictable tools. But the agent side also needs its own checks, permissions, and page-state validation before executing anything important.
Yes, that two-sided contract is the right direction.
The page can expose safer actions, but the agent still needs to prove it is on the page/state it thinks it is on before acting. Otherwise a clean tool surface can still execute the right command in the wrong context.
the schema normalization point from Wren is the one that actually bites. we hit it building an MCP wrapper around our Next.js API routes. Zod schemas exported fine, but the oneOf branches Zod generates for optional unions came through as unsupported keywords and the model silently fell back to unstructured output.
fix was a thin normalization layer between getTools() and the model call: unsupported keywords get dropped with a warning, and we log the original alongside the normalized version so page authors can see what was lost.
curious how you handle tool discovery across a navigation โ does the agent call getTools() fresh on each page transition, or hold the initial list for the session?
That is a great real-world example, thanks for sharing it.
In my case, the agent was built only for one game page, so I discovered the tools once and kept the initial list for the session. There was no navigation where the available tools could change.
For a generic WebMCP agent, I agree this should be handled differently.
And yes, the normalization layer makes sense too. The more generic the agent becomes, the more important this validation step is.
Still a solid way to push stronger models through a browser context without fighting the Inspector limits.
Thanks! Yes, that was exactly the idea, use Playwright as the bridge and avoid being limited only by Googleโs Model Context Tool Inspector.
Playwright + Gemini + WebMCP... at this point the browser is one coffee away from replacing me. ๐ Great article!!!โค๏ธ๐ฅ
Thanks! โค๏ธ Yeah, we are definitely living in the browser era.
But I would not worry yet. Skynet still fails when Chrome flags are disabled. ๐คฃ