🚂

既存サービス使って MCP サーバーにしてみる

に公開

ふとしです。MCP ってビッグウェーブですよねえ。

この記事の流れ

そこで、現在所属している会社のサービスである HeartRails Express API を MCP サーバーとして利用可能にしつつ、AI にとって効果的なツールにするにはこうしたらいいのでは?とやっていった手順を紹介します。

  1. とりあえず MCP サーバーにする
  2. AI が使い方をわかるようにする
  3. AI が使いやすいようにする

MCP サーバーにする

まず、必要最低限の機能から実装を始めます。

ここでは HeartRails Express API を利用して、緯度経度を受け取って最寄り駅情報を取得する関数と、それを MCP サーバーから利用可能にするためのツールを定義します。

MCP サーバーの実装には TypeScript SDK を使いますが、SDK 自体の詳細な使い方は省略します。

最寄り駅取得関数 (getNearestStations)

HeartRails Express は他にも色々な情報を提供していますが、特にすぐ使えて楽しいサービスとして最寄駅情報取得 API を使います。

// 緯度経度で最寄駅情報を取得する
export async function getNearestStations({
  x,
  y,
}: {
  x: number;
  y: number;
}): Promise<unknown> {
  const url = `https://express.heartrails.com/api/json?method=getStations&x=${x}&y=${y}`;
  const response = await fetch(url);

  if (!response.ok) {
    throw new Error(
      `APIリクエストが失敗しました: ${response.statusText} (${url}, ${response.status})`
    );
  }

  return response.json();
}

この関数は、引数として経度 x と緯度 y を受け取り、HeartRails Express API にリクエストを送信して結果を返します。

データの解釈は AI におまかせということで、型は気にしません。

最寄り駅取得ツール

次に、この関数を MCP サーバーのツールとして登録します。

// 最寄駅情報取得ツール
server.tool(
  'getNearestStations',
  {
    x: z.number(),
    y: z.number(),
  },
  ({ x, y }) => getNearestStations({ x, y }).then(createSuccessResponse).catch(createErrorResponse)
);

// 成功レスポンスを生成する関数
function createSuccessResponse(data: unknown): CallToolResult {
  return {
    content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
  };
}

// エラーレスポンスを生成する関数
function createErrorResponse(error: unknown): CallToolResult {
  return {
    content: [{ type: 'text', text: `エラー: ${error}` }],
    isError: true,
  };
}

これで最小限の MCP サーバーができました。

しかし、この状態では AI(や他の開発者)がこのツールをどのように使えば良いのか分かりにくいという問題があります。

AI が使い方をわかるようにする

次に、AI がツールの目的や必要な引数を理解できるように、説明を追加します。

// 最寄駅情報取得ツール (説明追記版)
server.tool(
  'getNearestStations',
  'ご指定の場所(緯度、経度)の最寄駅の情報の一覧を取得するAPIです。',
  {
    x: z.number().describe('最寄駅の情報を取得したい場所の経度(世界測地系)'),
    y: z.number().describe('最寄駅の情報を取得したい場所の緯度(世界測地系)'),
  },
  ({ x, y }) => getNearestStations({ x, y }).then(createSuccessResponse).catch(createErrorResponse)
);

これらの説明を追加することで、AI は getNearestStations ツールが「緯度経度から最寄り駅を取得するもの」であり、「x に経度、y に緯度を指定する必要がある」ことを理解できるようになります。

AI が使いやすいようにする

緯度経度から最寄り駅を取得できるようになりましたが、ユーザーは常に緯度経度を知っているわけではありません。「東京タワーの最寄り駅は?」のように、住所や場所の名前で質問することの方が多いでしょう。

住所から緯度経度を取得

そこで、まず、住所文字列を受け取り、緯度経度を返すツールを追加します。ここでは Google Maps Geocoding API を利用することを想定します(関数の具体的な実装は省略します)。

// 住所から緯度経度を取得する関数 (geocodeAddress) の実装は省略

// 住所から緯度経度を取得するツール
server.tool(
  'geocodeAddress',
  '住所文字列から緯度経度を取得するAPIです。Google Maps Geocoding APIを使用します。',
  {
    address: z.string().describe('緯度経度を取得したい住所(例: 東京都千代田区千代田1-1)'),
  },
  ({ address }) =>
    geocodeAddress({ address }).then(createSuccessResponse).catch(createErrorResponse)
);

この geocodeAddress ツールがあれば、AI は「東京タワーの住所」→ geocodeAddress ツール →「緯度経度」→ getNearestStations ツール →「最寄り駅」という流れでユーザーの質問に答えられるようになります。

住所から最寄り駅を直接取得

しかし、上記の流れでは、AI は 2 回のツール呼び出し(geocodeAddressgetNearestStations)を行う必要があり、その分やり取りが増えてしまいます。AI とサーバー間のデータの往復は少ない方が、効率が良く、応答も速くなります。

そこで、住所を受け取って直接最寄り駅情報を返す、より便利なツールを追加しましょう。

// 住所から最寄り駅を取得するツール
server.tool(
  'getNearestStationsByAddress',
  '住所から最寄り駅の情報の一覧を取得するAPIです。Google Maps Geocoding APIとHeartRails Express APIを使用します。',
  {
    address: z.string().describe('最寄り駅を取得したい住所(例: 東京都千代田区千代田1-1)'),
  },
  ({ address }) =>
    // 内部で geocodeAddress と getNearestStations を順に呼び出す
    geocodeAddress({ address })
      .then((location) => getNearestStations({ x: location.lng, y: location.lat }))
      .then(createSuccessResponse)
      .catch(createErrorResponse)
);

使ってみる

cline に登録して実際に使ってもらってみたのがこちら。

新北島周辺の駅教えて!

新北島周辺の駅情報は以下の通りです:

住之江公園駅(大阪南港ポートタウン線・大阪四つ橋線)

距離:約700m
大阪南港ポートタウン線:前の駅は平林駅、終点駅
大阪四つ橋線:前の駅は北加賀屋駅、終点駅
平林駅(大阪南港ポートタウン線)

距離:約760m
前の駅:南港口駅
次の駅:住之江公園駅
南港口駅(大阪南港ポートタウン線)

距離:約1940m
前の駅:南港東駅
次の駅:平林駅
最寄り駅は住之江公園駅で、大阪南港ポートタウン線と大阪四つ橋線の2路線が利用可能です。

このように、基本的なツールを組み合わせたり、一連の処理をまとめた複合的なツールを提供したりすることで、AI はより効率的に、そしてユーザーの意図に沿った形でタスクを実行できるようになります。

まとめ

MCP サーバーを実装していった雰囲気を書きました。

単に機能を実装するだけでなく、AI がそのツールを意図通りに、かつ効率的に利用できるよう配慮する「AI フレンドリー」であることが重要であると思います。

Discussion