7.8 KiB
MCP servers and the Playwright browser
extensions/Volcanic.AgenticCore.Extensions.Mcp is an MCP client: it starts stdio MCP servers and exposes their
tools to the agent as ordinary tools. It uses only the public extension API and the official ModelContextProtocol.Core
SDK. (ClaudeCode.Bridge is the opposite, an MCP server that shows core tools to claude -p.)
Configuration
Servers come from two places. The first one wins a server name:
- the host's
Mcpsection (appsettings.json, orMcp__Servers__<name>__Commandand friends in the environment), sorted by name (configuration binding sorts keys, so the order inappsettings.jsondoesn't count), then the Playwright preset; "mcpServers"in~/.volcanic/agent/settings.json(VOLCANIC_AGENT_DIRmoves it), with the same entry shape in camelCase, in file order. The extension reads this file itself, so a host that setsAgenticCoreOptions.AgentDirto another directory must also setVOLCANIC_AGENT_DIRformcpServersto be found there.
There is no per-workspace source: a Telegram workspace is writable by the agent itself.
"Mcp": {
"Servers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github@2025.9.1"],
"env": { "GITHUB_TOKEN": "${env:GITHUB_TOKEN}" },
"toolPrefix": "gh_",
"enabled": true,
"parallel": false,
"startupTimeoutSeconds": 60,
"callTimeoutSeconds": 120,
"idleTimeoutSeconds": 1800,
"stderrLevel": "Warning",
"promptSection": null
}
},
"Playwright": { "Enabled": false, "Headless": true, "Browser": null, "ExtraArgs": [], "Package": null, "Command": null }
}
${workspace}incommand,args,cwdandenvvalues becomes the chat's workspace when the process starts. The process runs in${workspace}unlesscwdsays otherwise.${env:NAME}in anenvvalue copies a host variable (unset: empty). It's the only way a deny-listed variable such asGITHUB_TOKENreaches a server, and it reaches only that server. Anullvalue removes a variable (use the user settings file for that; configuration binding may turnnullinto an empty string).- Every server process gets the process policy's filtered environment (see extensions.md).
Host secrets (
TELEGRAM_BOT_TOKEN,OPENAI_API_KEY, ...) are never inherited, and known secret values in tool output become[REDACTED]. toolPrefixmay contain letters, digits,_and-, and may not start withmcp__. Timeouts must be positive;idleTimeoutSeconds: 0never stops an idle process. The host refuses to start on invalid configuration.
The Playwright preset
PLAYWRIGHT_ENABLED=true (or Mcp:Playwright:Enabled) adds a server named playwright running @playwright/mcp:
npx -y @playwright/mcp@0.0.82 [--headless] --browser chromium
--output-dir ${workspace}/browser --user-data-dir ${workspace}/.volcanic/browser-profile <extra args>
--user-data-dir is left out when the extra arguments contain --isolated.
| Variable | Setting | Default | Notes |
|---|---|---|---|
PLAYWRIGHT_ENABLED |
Mcp:Playwright:Enabled |
false |
|
PLAYWRIGHT_HEADLESS |
Mcp:Playwright:Headless |
true |
|
PLAYWRIGHT_BROWSER |
Mcp:Playwright:Browser |
chromium |
Without it @playwright/mcp would look for Google Chrome |
PLAYWRIGHT_EXTRA_ARGS |
Mcp:Playwright:ExtraArgs |
— | Shell-style split: --device "iPhone 15" is two arguments. --isolated keeps no profile on disk |
PLAYWRIGHT_COMMAND |
Mcp:Playwright:Command |
npx |
Run this binary (e.g. a global playwright-mcp) instead of npx -y <package> |
| — | Mcp:Playwright:Package |
@playwright/mcp@0.0.82 |
npm spec used with npx. The pinned version is PlaywrightPreset.DefaultPackage |
Configured values win over the variables; an empty variable counts as unset. The bot logs
MCP: playwright enabled (headless, chromium) or MCP: playwright disabled (set PLAYWRIGHT_ENABLED=true) at startup.
- Screenshots saved with
filename: "browser/<name>.png", PDFs and downloads land in the workspace'sbrowser/folder, wheresendcan deliver them. A screenshot withoutfilenamecomes back to the model as an image (the OpenAI chat-completions provider can't show tool-result images, so there it only gets[image]). - Each chat has its own browser profile under
.volcanic/browser-profile, so site logins survive idle restarts,/newand bot restarts. The agent'sbashcan read that profile, like everything else in its workspace. - The system prompt gets a short "Browser" section while the browser tools are active. It mentions
sendonly when asendtool is active.
Deployment requirements
The server needs Node and the Chromium build that the pinned @playwright/mcp expects:
npx playwright install --with-deps chromium, run from that package so the versions match
(PLAYWRIGHT_BROWSERS_PATH is honoured). The first npx run downloads the package, which is why the preset allows
120 s to start. The Docker image (docker.md, built with INSTALL_PLAYWRIGHT=true) preinstalls both and sets
PLAYWRIGHT_COMMAND=playwright-mcp and PLAYWRIGHT_EXTRA_ARGS=--no-sandbox. Chromium's sandbox needs user namespaces,
which Docker's default profile blocks, so there the container is the isolation boundary. A missing browser shows up as
an error from the first browser_* call.
Tool names
The agent sees toolPrefix + name, with characters outside [A-Za-z0-9_-] replaced by _, cut to 48 characters,
and _2, _3 added to duplicates. The server is always called with its own name. Through the Claude Code provider the
tools appear as mcp__volcanic__<name>.
- A tool named like a built-in (
read,bash, ...) is not registered, and a warning suggests atoolPrefix. - If two servers offer the same name, the first in the order above wins (host servers by name) and a warning names
both. Give one of them a
toolPrefixto keep both. - MCP tools run one at a time unless the server sets
"parallel": true: most servers (Playwright included) are stateful.
Lifecycle
- The tool list is discovered once per bot process per server configuration, in a temp directory, while the first
session activates. Concurrent chats share that one discovery. A server that fails to start is logged, shows up in
/mcpas failed, contributes no tools, and is retried after 60 s. Other servers and the chat carry on. - Each chat gets its own server process, started by the first call in the chat's workspace. It stops after
idleTimeoutSecondswithout calls, and on/new,/resumeor shutdown, so a new conversation gets a fresh browser. - A server that dies turns the call into an error ("… MCP server exited (code): . It restarts on the next call; browser state was lost."), and the next call starts a new one. Failed calls are never retried automatically.
- A call runs at most
callTimeoutSeconds./stopcancels it and the server is told (notifications/cancelled).
/mcp
| Command | |
|---|---|
/mcp |
Each server: not started, running, exited, failed: <reason> or disabled, and its tool count |
/mcp <name> |
Command line (without env values), env names, server version, tools, last error, stderr tail |
/mcp restart <name> |
Stop the chat's process; the next call starts a fresh one (clears a wedged browser) |
No model call is made. Known secret values in the output (a server's stderr, say) are shown as [REDACTED].
Manual check (not in CI)
With PLAYWRIGHT_ENABLED=true and Chromium installed:
- "Open https://example.com and tell me the H1" →
Example Domain. - "Screenshot example.com and send it to me" → a photo arrives in Telegram (needs the
sendtool). /mcpshowsplaywright: running.- Two chats browse at the same time without seeing each other's tabs.