👏

既存の mcp を adk 経由で叩いてみる。 playwright を使う。

2025/04/12に公開

mcp の client に付いて詳しくなりたいと思いつつ adk についてもやりたいのでチョット調べてみます。

今回は playwright の mcp に繋いでみようと思います。
https://mcp.so/server/playwright-mcp/microsoft?tab=content

playwright は別サーバで立てるような想定で考えておきます。そのためドキュメントにある通り以下のように記載します

$ npx @playwright/mcp@latest --port 8931

Listening on http://localhost:8931
Put this in your client config:
{
  "mcpServers": {
    "playwright": {
      "url": "http://localhost:8931/sse"
    }
  }
}

とりあえず mcp として接続できることを確認したいので adk で agent として建てます。

from typing import Any, Dict, Optional
from google.adk.agents import LlmAgent
from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset, SseServerParams, StdioServerParameters
from google.adk.tools.tool_context import ToolContext
from google.adk.tools.base_tool import BaseTool

async def get_tools_async():
  tools, exit_stack = await MCPToolset.from_server(
    connection_params=SseServerParams(
      url="http://localhost:8931/sse",
    )
  )
  return tools, exit_stack

# Create parent agent and assign children via sub_agents
async def get_agent_async():
  """Creates an ADK Agent equipped with tools from the MCP Server."""
  print(f"start tools from MCP server.")
  tools, exit_stack = await get_tools_async()
  print(f"Fetched {len(tools)} tools from MCP server.")

  instruction = """
あなたは、ウェブスクレイピングを行うエージェントです。
また、取得した内容を要約します。
人間の代わりにウェブを検索し、情報を収集するのを手伝ってください。
利用可能なツールを使用して、実用的で動作するソリューションを優先してください。
"""

  root_agent = LlmAgent(
    model='gemini-2.0-flash', # Adjust model name if needed based on availability
    name='playwrite',
    instruction=instruction,
    tools=tools, # Provide the MCP tools to the ADK agent
    before_tool_callback=before_request
  )
  return root_agent, exit_stack

def before_request(tool: BaseTool, args: Dict[str, Any], tool_context: ToolContext) -> Optional[Dict]:
    print("Before request callback")
    print(f"tool name: {tool.name}")
    print(f"arg: {args}")
    # もし送る内容を変更したい場合はここで変更ができる。 after もあるので、何かしらの reponse の修正をしたいときとかは良いかもです
    # https://google.github.io/adk-docs/callbacks/types-of-callbacks/#before-tool-callback
    return None

呼び出し側は以下のようにサンプルで作成しています。

import asyncio
import asyncclick as click
from uuid import uuid4
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types
from agents.playwright_agent.agent import get_agent_async
from google.adk.agents import RunConfig

@click.command()
@click.option("--session", default=0)
@click.option('--app-name', default='root_agent', help='Name of the application.')
async def cli(session, app_name):

    if session == 0:
        session_id = uuid4().hex
    else:
        session_id = session

    continue_loop = True

    user_id = 'user_fs'
    session_service = InMemorySessionService()
    session = session_service.create_session(
        state={}, app_name=app_name, user_id=user_id, session_id=session_id
    )

    playwright_agent, exit_stack = await get_agent_async()
    runner = Runner(
      app_name=app_name,
      agent=playwright_agent,
      session_service=session_service,
    )

    while continue_loop:
        continue_loop = await completeTask(runner, user_id, session_id)

    await exit_stack.aclose()
    print("Cleanup complete.")


async def completeTask(runner: Runner, user_id, session_id):
    prompt = click.prompt(
        "\nWhat do you want to send to the agent? (:q or quit to exit)"
    )
    if prompt == ":q" or prompt == "quit":
        return False

    content = types.Content(role='user', parts=[types.Part(text=prompt)])
    events = runner.run_async(
        session_id=session_id,
        user_id=user_id,
        new_message=content,
    )

    async for event in events:
        if (event.content and event.content.parts and event.content.parts[0].text):
            response = '\n'.join([p.text for p in event.content.parts if p.text])
            click.echo(response)

    return True

if __name__ == "__main__":
    asyncio.run(cli())

これを行うことで mcp 経由で playwright が立ち上がります。session_id を引き継いでいるので前回の内容を下に動きます。
実際に叩いてみます。
topページ表示してもらい、 contact ページに言って情報埋めてもらいます

ログ

$ python client.py
start tools from MCP server.
Fetched 20 tools from MCP server.

What do you want to send to the agent? (:q or quit to exit): http://3-shake.com を検索をして日本語にしてください

Warning: there are non-text parts in the response: ['function_call', 'function_call'],returning concatenated text result from text parts,check out the non text parts for full response from model.
Before request callback
tool name: browser_navigate
arg: {'url': 'http://3-shake.com'}
Before request callback
tool name: browser_snapshot
arg: {}
3-shake.comのウェブサイトを日本語で要約します。

3-shakeは、インフラをシンプルにしてイノベーションが起こりやすい世界を作ることを目指している会社です。2025年に10周年を迎えました。

主なサービスは以下の通りです。

  • Sreake: AWS/GCP/Kubernetesに強みを持ったプロフェッショナルチームによるSREコンサルティング
  • Securify: アプリケーション層のセキュリティプラットフォーム
  • Reckoner: データ連携プラットフォーム
  • Relance: フリーランスエンジニア向け人材紹介サービス

最新ニュースとして、データ連携ツール「Reckoner」がTeamSpiritと連携開始、代表取締役社長の吉田拓真が信州大学特任准教授に就任、統合セキュリティプラットフォーム「Securify」がCSPM機能をリリース、2025 Google Cloud Infrastructure Modernization Partner of the Year - Japan を受賞などが掲載されています。

採用情報も掲載されており、フロントエンドエンジニア、サーバーエンドエンジニア、VP of Engineering、SRE、セキュリティエンジニアなど様々な職種を募集しています。

What do you want to send to the agent? (:q or quit to exit):
What do you want to send to the agent? (:q or quit to exit): contact ページへのリンクを押して 内容を埋めてください。絶対に submit button は押さないでください。
Warning: there are non-text parts in the response: ['function_call'],returning concatenated text result from text parts,check out the non text parts for full response from model.
contact ページへのリンクが見つかりませんでした。もう一度探します。

Before request callback
tool name: browser_snapshot
arg: {}
Warning: there are non-text parts in the response: ['function_call'],returning concatenated text result from text parts,check out the non text parts for full response from model.
Contact ページへのリンクを見つけました。"Contact"という名前のリンクのURLは https://3-shake.com/contact/ です。

Before request callback
tool name: browser_navigate
arg: {'url': 'https://3-shake.com/contact/'}
Warning: there are non-text parts in the response: ['function_call', 'function_call', 'function_call', 'function_call', 'function_call', 'function_call', 'function_call', 'function_call', 'function_call'],returning concatenated text result from text parts,check out the non text parts for full response from model.
お問い合わせフォームに記入します。

  • お問い合わせの目的: 弊社サービスのお問い合わせ
  • 姓: 山田
  • 名: 太郎
  • 会社名: AI株式会社
  • 部署: 開発部
  • 役職: エンジニア
  • 業務用メールアドレス: yamada.taro@ai.co.jp
  • 電話番号: 03-1234-5678
  • お問い合わせ詳細: サービスについて詳しく教えてください。

Before request callback
tool name: browser_select_option
arg: {'values': ['弊社サービスのお問い合わせ'], 'element': 'お問い合わせの目的必須', 'ref': 's1e59'}
Before request callback
tool name: browser_type
arg: {'ref': 's1e70', 'text': '山田', 'element': '姓必須'}
Before request callback
tool name: browser_type
arg: {'ref': 's1e75', 'text': '太郎', 'element': '名必須'}
Before request callback
tool name: browser_type
arg: {'element': '会社名必須', 'text': 'AI株式会社', 'ref': 's1e80'}
Before request callback
tool name: browser_type
arg: {'text': '開発部', 'element': '部署必須', 'ref': 's1e85'}
Before request callback
tool name: browser_type
arg: {'text': 'エンジニア', 'ref': 's1e90', 'element': '役職必須'}
Before request callback
tool name: browser_type
arg: {'text': 'yamada.taro@ai.co.jp', 'element': '業務用メールアドレス必須', 'ref': 's1e95'}
Before request callback
tool name: browser_type
arg: {'ref': 's1e100', 'element': '電話番号必須', 'text': '03-1234-5678'}
Before request callback
tool name: browser_type
arg: {'element': 'お問い合わせ詳細', 'ref': 's1e105', 'text': 'サービスについて詳しく教えてください。'}
Warning: there are non-text parts in the response: ['function_call'],returning concatenated text result from text parts,check out the non text parts for full response from model.
申し訳ありません。どうやら要素の参照が古くなってしまったようです。もう一度スナップショットを取得して、もう一度試してみます。

Before request callback
tool name: browser_snapshot
arg: {}
Warning: there are non-text parts in the response: ['function_call'],returning concatenated text result from text parts,check out the non text parts for full response from model.
お問い合わせフォームに記入します。

  • お問い合わせの目的: 弊社サービスのお問い合わせ
  • 姓: 山田
  • 名: 太郎
  • 会社名: AI株式会社
  • 部署: 開発部
  • 役職: エンジニア
  • 業務用メールアドレス: yamada.taro@ai.co.jp
  • 電話番号: 03-1234-5678
  • お問い合わせ詳細: サービスについて詳しく教えてください。

Before request callback
tool name: browser_select_option
arg: {'ref': 's11e59', 'values': ['弊社サービスのお問い合わせ'], 'element': 'お問い合わせの目的必須'}
Warning: there are non-text parts in the response: ['function_call'],returning concatenated text result from text parts,check out the non text parts for full response from model.
Before request callback
tool name: browser_type
arg: {'element': '姓必須', 'ref': 's11e80', 'text': '山田'}
Warning: there are non-text parts in the response: ['function_call'],returning concatenated text result from text parts,check out the non text parts for full response from model.
また参照エラーが発生しました。ページが動的に変化しているようです。参照エラーを避けるために、すべての入力フィールドに一度に値を入力するのではなく、1つずつ実行し、その都度スナップショットを更新します。

まず、「お問い合わせの目的」を選択します。

Before request callback
tool name: browser_select_option
arg: {'values': ['弊社サービスのお問い合わせ'], 'ref': 's11e59', 'element': 'お問い合わせの目的必須'}
Warning: there are non-text parts in the response: ['function_call'],returning concatenated text result from text parts,check out the non text parts for full response from model.
これはうまくいきませんでした。別のスナップショットを取得して再試行します。

Before request callback
tool name: browser_snapshot
arg: {}
Warning: there are non-text parts in the response: ['function_call'],returning concatenated text result from text parts,check out the non text parts for full response from model.
Before request callback
tool name: browser_select_option
arg: {'element': 'お問い合わせの目的必須', 'ref': 's15e59', 'values': ['弊社サービスのお問い合わせ']}
Warning: there are non-text parts in the response: ['function_call'],returning concatenated text result from text parts,check out the non text parts for full response from model.
Before request callback
tool name: browser_type
arg: {'ref': 's16e80', 'text': '山田', 'element': '姓必須'}
Warning: there are non-text parts in the response: ['function_call'],returning concatenated text result from text parts,check out the non text parts for full response from model.
Before request callback
tool name: browser_type
arg: {'element': '名必須', 'ref': 's17e85', 'text': '太郎'}
Warning: there are non-text parts in the response: ['function_call'],returning concatenated text result from text parts,check out the non text parts for full response from model.
Before request callback
tool name: browser_type
arg: {'element': '会社名必須', 'text': 'AI株式会社', 'ref': 's18e90'}
Warning: there are non-text parts in the response: ['function_call'],returning concatenated text result from text parts,check out the non text parts for full response from model.
Before request callback
tool name: browser_type
arg: {'text': '開発部', 'element': '部署必須', 'ref': 's19e95'}
Warning: there are non-text parts in the response: ['function_call'],returning concatenated text result from text parts,check out the non text parts for full response from model.
Before request callback
tool name: browser_type
arg: {'text': 'エンジニア', 'element': '役職必須', 'ref': 's20e100'}
Warning: there are non-text parts in the response: ['function_call'],returning concatenated text result from text parts,check out the non text parts for full response from model.
Before request callback
tool name: browser_type
arg: {'ref': 's21e105', 'text': 'yamada.taro@ai.co.jp', 'element': '業務用メールアドレス必須'}
Warning: there are non-text parts in the response: ['function_call'],returning concatenated text result from text parts,check out the non text parts for full response from model.
Before request callback
tool name: browser_type
arg: {'element': '電話番号必須', 'ref': 's22e110', 'text': '03-1234-5678'}
Warning: there are non-text parts in the response: ['function_call'],returning concatenated text result from text parts,check out the non text parts for full response from model.
Before request callback
tool name: browser_type
arg: {'element': 'お問い合わせ詳細', 'text': 'サービスについて詳しく教えてください。', 'ref': 's23e115'}
お問い合わせフォームへの入力を完了しました。「送信する」ボタンはクリックしません。

What do you want to send to the agent? (:q or quit to exit):

動画で見せられればよかったんですが、仕方ないですね。

まとめと感想

playwright mcp がでたときにためそうとか思っていたのですが client 作るのがめんどいなと思ってたのを adk だと簡単に作れたので良かったです。(A2A をやるって行って結局やってないな)

最後に、一応書きますが

Discussion