🚗

初めてMCPサーバー作ってみた

に公開

はじめに

私は基本的にAI関連の開発をしたことはなく、Github copilotやClaude codeなどを使ってVibe Codingしつつ、最近になってAWSとかGithubのMCPサーバーを利用し初め、便利だなーと思った程度の知識で開発しました。

作成したもの

Cariot APIを経由してCariotのデータにアクセスできるようにするCariot MCP Serverを作成しました。
今回やりたかったことは、CariotのAPIを通して、自然言語でデータを良い感じに取り出し、クライアント側でグラフや地図などを使用してそれを良い感じに見れること。

2025/08/29現在では以下の機能をサポートしております。

  • ドライバーの基本情報
  • ドライバーの日報
  • 車両の基本情報
  • ドライバー/車両のリアルタイム情報

Node.js v22 + Typescript

https://github.com/CariotInc/cariot-mcp-server
https://www.npmjs.com/package/@cariot-labs/cariot-mcp-server

全体の流れ

かなりざっくり説明させていただくと、

MCPを構成する要素は大きく分けて以下3つ

  • Resource
  • Prompt
  • tool

今回はtoolを使用して、その中で外部API(今回はCariot API)を呼び出す。
レスポンス結果をそのまま標準出力ででAIにコンテキストとして渡す。

以上です。思ったより簡単に実装することができます。

詳細は公式ドキュメントを確認してください。
https://modelcontextprotocol.io/quickstart/server
実装するにあたり、上記とTypescriptのSDKリポジトリは非常に参考にさせていただきました。

export const dailyReportsTool = {
  name: 'get_daily_reports',
  config: {
    titleEn: 'Get Daily Reports',
    titleJa: '運転報告一覧取得',
    descriptionEn:
      'Retrieves a list of daily reports for all drivers within a specified date range.',
    descriptionJa: '指定された日付範囲内の全ドライバーの日報をリストで取得します。',
    inputSchema: {
      driverName: z
        .string()
        .optional()
        .describe(
          formatEnJaWithSlash(
            'Driver name (partial match, put a space between family and given name)',
            'ドライバー名(部分一致。姓と名の間に必ずスペースを入れてください)',
          ),
        ),
      dateFrom: z
        .string()
        .optional()
        .describe(formatEnJaWithSlash('Start date (yyyy-MM-dd)', '開始日 (yyyy-MM-dd)')),
      dateTo: z
        .string()
        .optional()
        .describe(formatEnJaWithSlash('End date (yyyy-MM-dd)', '終了日 (yyyy-MM-dd)')),
      limit: z
        .number()
        .min(1)
        .max(100)
        .optional()
        .describe(
          formatEnJaWithSlash(
            'Number of results to retrieve (default: 20, max: 100)',
            '取得件数 (デフォルト:20, 最大:100)',
          ),
        ),
    },
  },
  handler,
};

こんな感じで、SDKを利用すると、一番下のhandler部分で、外部APIの認証、コール部分を実装し、決まったフォーマットでreturnするだけでMCPを作成することができます。

ポイント

inputSchemaでAPIの引数を定義

個人的に面白いなと思ったのですが、inputSchemaで変数schemaを定義すると、AIがプロンプトからそれっぽいものを抽出して、ここに当てこんでくれます。

例えば

「昨日のデモ太郎の運転報告を取得して」

といったプロンプトをClaude desktopなどで入力すると、

driverName="デモ 太郎"
dateFrom="2025-08-28"
dateTo="2025-08-28"

みたいな感じで、当てこんでくれます。
あとはhandlerの中でそれを参照して外部APIのクエリーパラメータなどに設定すれば指定したデータを取得できるというわけです。

titleとdescriptionに日英を記述する

プロンプトに入力された単語を使って、適切なtoolを呼び出させる必要があったのですが、色々試した結果、toolとdescriptionに記述するのが一番AIに伝わったので、/区切りで日英両方記述しました。

運転報告やdaily reportという単語があれば、AIは良い感じに判定してこのget_daily_reportsを呼び出してくれます。

inputSchemaでもきちんとdescribeを記述してコンテキストを渡す

例えばdriverNameなどは、今の様に「姓と名の間に必ずスペースを入れてください」と記述しない場合、連結した名前で検索してしまい、検索結果0になってしまうことがありました。
describeに記述すると、その辺りもちゃんと読み取ってくれます。

実装して困ったこと

コンテキスト制限

AIは一度に扱えるコンテキスト(トークン数)に限りがあります。
大量の情報を返すAPIだと、そのままではAI側に負担が大きい。

特にClaude desktopでは、1MBを超えるコンテキストになると以下の様にエラーになってしまう。

なので、不要なコンテキストはなるべく除外したい。
ということで今回はAPIレスポンスの中で、特に大きい部分をMCPサーバー側のhandlerで省くという形を取りました。

ちなみに、これからのAI連携時代に、最適化されたデータ取得手段としてGraphQLが再評価されそうだなと感じました。
GraphQLを用いて、外部APIが実装されていれば、呼び出し時にピンポイントで欲しい情報だけ取得できるので。

N+1問題

ちょっとN+1の誤用な気もしますが、何が言いたいかと言うと、例えば以下のようなケース

危険運転の多かったランキングを作成するには、get_daily_reportsを呼び出し、そのレスポンスを利用して、get_daily_reportを呼び出す必要があります。

その前にまず、上の一連の動作を特にプロンプトに含めずにいい感じにやってくれるAIに感動しました。

ただ一方で、get_daily_reportsで取得したレポート全てに対して、get_daily_reportの実行はしてくれませんでした。いくつかピックアップするだけ。
賢すぎて、パフォーマンスまで考慮してくれているのかもしれません。

とはいえ、全てのレポートに対して実行し、ランキングを作成して欲しかったので困りました。

結論、まだ解決していないのですが、
ざっくり以下の対応をする必要があったのかなと思っています。

  • 1のlist部分に危険運転情報を乗っけるようにAPIを改修して、Nしなくて済むようにする
  • 危険運転用のtoolsetを作成し、その中のhandler内でN+1するように実装する

こちらのvercelの記事によると、API単位ではなく、ワークフロー単位でtoolsetを構築することを推奨しているので、2番目の危険運転用のtoolsetを作成する方がベターだったかなと今は思っております。
記事にもありますが、LLMにワークフローを考えさせるのは、確かに不確実なことが多かったので、やって欲しい一連の動作がある程度決まっている場合は、事前にこちら側で組み込むべきだったと反省しております。

まとめ

今後は既存システムとAIとの連携は避けて通れないだろうなと思っているので、MCPを利用する側としても、作る側としても、手を動かしてざっくりと中身の理解を深められたのは非常に良い経験でした。
また、AI利用を前提にAPI設計を行っていくことも非常に重要だと痛感しました。

株式会社キャリオット

Discussion