Class: ActionAgent::PlaywrightMcpClient

Inherits:
Object
  • Object
show all
Defined in:
app/services/action_agent/playwright_mcp_client.rb

Overview

Minimal MCP client (streamable HTTP transport) for a Playwright MCP server — typically npx @playwright/mcp --port 8931 running beside the app in development, or a sandbox-provisioned browser container in production. Speaks just enough JSON-RPC for tools/call: initialize once per process, then call tools under the session id the server hands back.

Defined Under Namespace

Classes: Error

Constant Summary collapse

DEFAULT_URL =
ENV.fetch("PLAYWRIGHT_MCP_URL", "http://host.orb.internal:8931/mcp")
OPEN_TIMEOUT_SECONDS =
5
READ_TIMEOUT_SECONDS =
60

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(url: DEFAULT_URL) ⇒ PlaywrightMcpClient

Returns a new instance of PlaywrightMcpClient.



27
28
29
30
# File 'app/services/action_agent/playwright_mcp_client.rb', line 27

def initialize(url: DEFAULT_URL)
  @uri = URI(url)
  @mutex = Mutex.new
end

Class Method Details

.instanceObject



19
20
21
# File 'app/services/action_agent/playwright_mcp_client.rb', line 19

def self.instance
  @instance ||= new
end

.reset!Object



23
24
25
# File 'app/services/action_agent/playwright_mcp_client.rb', line 23

def self.reset!
  @instance = nil
end

Instance Method Details

#call_tool(name, arguments = {}) ⇒ Object

Returns { text:, is_error: } — the tool result's text content.



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'app/services/action_agent/playwright_mcp_client.rb', line 33

def call_tool(name, arguments = {})
  Rails.logger.debug("[PlaywrightMcpClient] call #{name} args=#{arguments.inspect[0, 200]}")
  ensure_session!
  response = post(
    { jsonrpc: "2.0", id: next_id, method: "tools/call",
      params: { name: name, arguments: arguments } },
    session: @session_id
  )
  result = response["result"]
  unless result
    Rails.logger.warn("[PlaywrightMcpClient] #{name} unexpected response: #{response.inspect[0, 500]}")
    raise Error, (response.dig("error", "message") || "empty MCP response")
  end

  text = Array(result["content"]).filter_map { |block| block["text"] }.join("\n")
  { text: text, is_error: result["isError"] ? true : false }
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Net::OpenTimeout, SocketError => e
  self.class.reset!
  raise Error, "Playwright MCP server unreachable at #{@uri} (#{e.class}): start it with `npx @playwright/mcp --port 8931`"
end