genai-toolbox を実装して mcp server として公開し adk から使ってみる
mcp server を作ってみるということで、genai-toolbox という物があるのでそれを元にやっていきます
こちらは、各 DB への接続情報と、どういう SQL を実行するかを yaml、または、http の baseurl と request parameter などで記載することで tool を作成することができます。
接続先は図にもある形になると思います。
そのため、複数の接続先をまとめつつ、tool の記載に対しても実装コスト低くまとめることができるので、取っ掛かりにはちょうどいいのかなと思っています。
また、toolbox を mcpサーバ として公開することもできます
使ってみる
とりま利用するには toolbox 自体が必要です。docker でも公開されています。
そのため、今回は database も postgresql をつかうのもあるため、docker compose でやってみます。
docker compose のサンプルはそのままあるので、それを使ってみます。構成はこんな感じ。
.
├── compose.yaml
└── config
├── postgres
│ ├── schema.sql
│ └── seed.sql
└── toolbox
└── tools.yaml
services:
toolbox:
image: us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:latest
hostname: toolbox
ports:
- "5000:5000"
volumes:
- ./config/toolbox:/config
command: [ "toolbox", "--tools_file", "/config/tools.yaml", "--address", "0.0.0.0"]
depends_on:
postgres:
condition: service_healthy
networks:
- tool-network
postgres:
image: postgres
hostname: postgres
environment:
POSTGRES_USER: toolbox_user
POSTGRES_PASSWORD: my-password
POSTGRES_DB: toolbox_db
ports:
- "5432:5432"
volumes:
- ./db:/var/lib/postgresql/data
- ./config/postgres/schema.sql:/docker-entrypoint-initdb.d/2-schema.sql
- ./config/postgres/seed.sql:/docker-entrypoint-initdb.d/3-seed.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U toolbox_user -d toolbox_db"]
interval: 10s
timeout: 5s
retries: 5
networks:
- tool-network
networks:
tool-network:
tools.yaml の中身は以下のように記載ができます。
sources:
my-pg-source:
kind: postgres
host: postgres
port: 5432
database: toolbox_db
user: toolbox_user
password: my-password
toolsets:
postgres-toolset:
- show-tables
- show-columns-in-table
- select-hotel-lower-rows
tools:
show-tables:
kind: postgres-sql
source: my-pg-source
description: show all tables in the database.
statement: SELECT table_name FROM information_schema.tables WHERE table_schema = 'public';
show-columns-in-table:
kind: postgres-sql
source: my-pg-source
description: Search for hotels based on location.
parameters:
- name: table
type: string
description: table name to show columns.
statement: SELECT column_name FROM information_schema.columns WHERE table_name = $1;
select-hotel-lower-rows:
kind: postgres-sql
source: my-pg-source
description: >-
Search for hotels by name, reputation (rate) and number of results (limit).
If you only have the name when searching, use the default value.
For example, in the following case, store it as:
{
"name": "A",
"rate": none,
"limit": none
}
and the default values will be used, for example:
{
"name": "A",
"rate": 1,
"limit": 5
}
parameters:
- name: name
type: string
description: required. The name of the hotel to search for.
- name: rate
type: integer
description: optional. if not provided then this value is default 1. This number ranges from 1 to 5. It is the rating value by which the rows are filtered.
- name: limit
type: integer
description: optional. if not provided then this value is default 5. The maximum number of rows to return.
statement: >-
SELECT * FROM hotels WHERE name ILIKE '%' || $1 || '%' and rate >= $2 order by rate desc limit $3;
今回は select だけをやってみます。
optional な値の場合がある場合、agent にちゃんと説明をしてあげないといけないです。
ちなみに、以下のように sql 部分は prepared statement であり、そのままの埋め込みではないため、table 名を動的にしよう, where 部分をうめようとしても怒られます。できたほうが実装は楽ではあるのですが、ハンドリングしにくいと思いますし、セキュリティ的にもそのほうが良いでしょうね。
The specified SQL statement is executed as a prepared statement, and specified parameters will inserted according to their position: e.g. 1 will be the first parameter specified, $@ will be the second parameter, and so on.
ちなみに、以下のようなレコードを入れています
INSERT INTO hotels(id, name, location, price_tier, checkin_date, checkout_date, booked, rate)
VALUES
(1, 'Hilton Basel', 'Basel', 'Luxury', '2024-04-22', '2024-04-20', B'0', 5),
(2, 'Marriott Zurich', 'Zurich', 'Upscale', '2024-04-14', '2024-04-21', B'0', 4),
(3, 'Hyatt Regency Basel', 'Basel', 'Upper Upscale', '2024-04-02', '2024-04-20', B'0', 4),
(4, 'Radisson Blu Lucerne', 'Lucerne', 'Midscale', '2024-04-24', '2024-04-05', B'0', 4),
(5, 'Best Western Bern', 'Bern', 'Upper Midscale', '2024-04-23', '2024-04-01', B'0', 3),
(6, 'InterContinental Geneva', 'Geneva', 'Luxury', '2024-04-23', '2024-04-28', B'0', 3),
(7, 'Sheraton Zurich', 'Zurich', 'Upper Upscale', '2024-04-27', '2024-04-02', B'0', 4),
(8, 'Holiday Inn Basel', 'Basel', 'Upper Midscale', '2024-04-24', '2024-04-09', B'0', 2),
(9, 'Courtyard Zurich', 'Zurich', 'Upscale', '2024-04-03', '2024-04-13', B'0', 5),
(10, 'Comfort Inn Bern', 'Bern', 'Midscale', '2024-04-04', '2024-04-16', B'0', 1),
(11, 'Radisson Blu Geneva', 'Geneva', 'Luxury', '2024-04-01', '2024-04-15', B'0', 5),
(12, 'Ibis Basel', 'Basel', 'Economy', '2024-04-05', '2024-04-10', B'0', 2),
(13, 'NH Zurich Airport', 'Zurich', 'Midscale', '2024-04-06', '2024-04-12', B'0', 3),
(14, 'Motel One Bern', 'Bern', 'Economy', '2024-04-07', '2024-04-11', B'0', 1),
(15, 'Park Hyatt Geneva', 'Geneva', 'Luxury', '2024-04-08', '2024-04-14', B'0', 5);
これらを使い toolbox というより今回は mcp server 起動します
docker compose up -d
これにより https://googleapis.github.io/genai-toolbox/how-to/connect_via_mcp/ に記載している通りの情報でアクセスできます。
{
"mcpServers": {
"toolbox": {
"type": "sse",
"url": "http://127.0.0.1:5000/mcp/sse",
}
}
}
mcp client は adk を通して使います。ここは正直前回の playwright と mcp server への接続情報が違う、agent の名前と instruction が違うだけで殆ど変わりません。
もしお試ししたい方は、前回のをコピーして使ってみてください。
async def get_tools_async():
# Replace with the actual URL where your Toolbox service is running
tools, exit_stack = await MCPToolset.from_server(
connection_params=SseServerParams(
url="http://localhost:5000/mcp/sse",
)
)
return tools, exit_stack
instruction = """
あなたはpostgresqlを操作できるエージェントです。
ユーザの指示に従い、postgresqlを操作してください。
"""
実際に叩くと以下のようになります、
$ python client.py
start tools from MCP server.
Fetched 3 tools from MCP server.
What do you want to send to the agent? (:q or quit to exit): Hilton という名前のホテルを検索してください
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: select-hotel-lower-rows
arg: {'limit': 5, 'name': 'Hilton', 'rate': 1}
ヒルトン・バーゼルのホテルが見つかりました。評価は5です。他に何かお手伝いできることはありますか?
What do you want to send to the agent? (:q or quit to exit): バーゼル って名前のホテルは他にもありますか?
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: select-hotel-lower-rows
arg: {'name': 'Basel', 'limit': 5, 'rate': 1}
バーゼルという名前のホテルは、ヒルトン・バーゼル、ハイアット・リージェンシー・バーゼル、ホリデー・イン・バーゼル、イビス・バーゼルの4軒あります。
Hilton Basel、Hyatt Regency Basel、Holiday Inn Basel、Ibis Basel っていうホテルがテーブルにはあるので正しそうです。
ちなみに最初の方に定義している、show-tables, show-columns-in-table を使い、テーブル名が取得できます。
そのため、これを使っていい感じで db の job 情報と組み合わせることもできるかもしれません。
What do you want to send to the agent? (:q or quit to exit): name のカラムを持っているテーブルを教えて下さい
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.
どのテーブルに name カラムがあるか知るためには、まずデータベース内のすべてのテーブルをリストする必要があります。その後、各テーブルの列を調べて、name カラムの有無を確認します。
まず、利用可能なテーブルをリストします。
Before request callback
tool name: show-tables
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.
テーブルは一つだけ hotels というテーブルがあることがわかりました。
次に、hotels テーブルに name カラムがあるかどうかを調べます。
Before request callback
tool name: show-columns-in-table
arg: {'table': 'hotels'}
hotels テーブルには name カラムがあります。
感想
簡単に mcp server を実装する事もできるので良いかなと思いつつ、とはいえカラム名とかテーブル名とかも埋められるモードがほしい気もしつつどこまで許可するかが難しいなと思います。簡単にデータを吹き飛ばされても困りますし。
yaml の記載ぐらいは AI に協力してもらいつつ記載するのが良いと思うので、このままでもいいかなぁ。
また、API 実装と同じく tool として開けすぎると簡単にセキュリティホールになるという認識です。
そのため、利用に関しては mcp server にきちんと認証を実装するなど( https://googleapis.github.io/genai-toolbox/resources/authservices/google/ )
対策をしましょう。
ちなみに Cloud Run で deploy する方法も以下に記載があるので参考にしてください
Discussion