Zenn
💽

genai-toolbox を実装して mcp server として公開し adk から使ってみる

2025/04/13に公開

mcp server を作ってみるということで、genai-toolbox という物があるのでそれを元にやっていきます
https://github.com/googleapis/genai-toolbox

こちらは、各 DB への接続情報と、どういう SQL を実行するかを yaml、または、http の baseurl と request parameter などで記載することで tool を作成することができます。
接続先は図にもある形になると思います。

https://github.com/googleapis/genai-toolbox/raw/main/docs/en/getting-started/introduction/architecture.png

そのため、複数の接続先をまとめつつ、tool の記載に対しても実装コスト低くまとめることができるので、取っ掛かりにはちょうどいいのかなと思っています。
また、toolbox を mcpサーバ として公開することもできます
https://googleapis.github.io/genai-toolbox/how-to/connect_via_mcp/

使ってみる

とりま利用するには toolbox 自体が必要です。docker でも公開されています。
そのため、今回は database も postgresql をつかうのもあるため、docker compose でやってみます。
docker compose のサンプルはそのままあるので、それを使ってみます。構成はこんな感じ。

.
├── compose.yaml
└── config
    ├── postgres
    │   ├── schema.sql
    │   └── seed.sql
    └── toolbox
        └── tools.yaml
compose.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 の中身は以下のように記載ができます。

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.

https://googleapis.github.io/genai-toolbox/resources/tools/postgres-sql/

ちなみに、以下のようなレコードを入れています

seed.sql
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 が違うだけで殆ど変わりません。
もしお試ししたい方は、前回のをコピーして使ってみてください。

postgres_agent.py
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 する方法も以下に記載があるので参考にしてください
https://googleapis.github.io/genai-toolbox/how-to/deploy_toolbox/

Discussion

ログインするとコメントできます