Files
Playa/docs/mcp.md

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:

  1. the host's Mcp section (appsettings.json, or Mcp__Servers__<name>__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.

"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). 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 <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'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, 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 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: <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:

  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.