既存の mcp を adk 経由で叩いてみる。 playwright を使う。
mcp の client に付いて詳しくなりたいと思いつつ adk についてもやりたいのでチョット調べてみます。
今回は playwright の mcp に繋いでみようと思います。
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