# 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: 1. the host's `Mcp` section (`appsettings.json`, or `Mcp__Servers____Command` and friends in the environment), sorted by name (configuration binding sorts keys, so the order in `appsettings.json` doesn't count), then the Playwright preset; 2. `"mcpServers"` in `~/.volcanic/agent/settings.json` (`VOLCANIC_AGENT_DIR` moves it), with the same entry shape in camelCase, in file order. The extension reads this file itself, so a host that sets `AgenticCoreOptions.AgentDir` to another directory must also set `VOLCANIC_AGENT_DIR` for `mcpServers` to be found there. There is no per-workspace source: a Telegram workspace is writable by the agent itself. ```jsonc "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}` in `command`, `args`, `cwd` and `env` values becomes the chat's workspace when the process starts. The process runs in `${workspace}` unless `cwd` says otherwise. - `${env:NAME}` in an `env` value copies a host variable (unset: empty). It's the only way a deny-listed variable such as `GITHUB_TOKEN` reaches a server, and it reaches only that server. A `null` value removes a variable (use the user settings file for that; configuration binding may turn `null` into an empty string). - Every server process gets the process policy's filtered environment (see [extensions.md](extensions.md#spawning-processes)). Host secrets (`TELEGRAM_BOT_TOKEN`, `OPENAI_API_KEY`, ...) are never inherited, and known secret values in tool output become `[REDACTED]`. - `toolPrefix` may contain letters, digits, `_` and `-`, and may not start with `mcp__`. Timeouts must be positive; `idleTimeoutSeconds: 0` never 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 ``` `--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 ` | | — | `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/.png"`, PDFs and downloads land in the workspace's `browser/` folder, where `send` can deliver them. A screenshot without `filename` comes 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, `/new` and bot restarts. The agent's `bash` can 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 `send` only when a `send` tool 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](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__`. - A tool named like a built-in (`read`, `bash`, ...) is **not** registered, and a warning suggests a `toolPrefix`. - 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 `toolPrefix` to 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 `/mcp` as 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 `idleTimeoutSeconds` without calls, and on `/new`, `/resume` or 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`. `/stop` cancels it and the server is told (`notifications/cancelled`). ## `/mcp` | Command | | |---|---| | `/mcp` | Each server: `not started`, `running`, `exited`, `failed: ` or `disabled`, and its tool count | | `/mcp ` | Command line (without env values), env names, server version, tools, last error, stderr tail | | `/mcp restart ` | 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: 1. "Open https://example.com and tell me the H1" → `Example Domain`. 2. "Screenshot example.com and send it to me" → a photo arrives in Telegram (needs the `send` tool). 3. `/mcp` shows `playwright: running`. 4. Two chats browse at the same time without seeing each other's tabs.