0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MCPサーバーを自作してみる。

Posted at

はじめに

生成AI系の案件に携わる中で耳にするようになった MCP についてまとめてみます。
今回は実際にMCPサーバーの自作まで行ってみようと思います。

※誤りありましたら、ご指摘いただけますと幸いです。

MCPとは

MCPは Model Context Protocol の略称で、AIモデルと外部システムを接続するための共通プロトコルのことです。
対話型生成AIとして有名なClaudeを開発した、Anthropic社によって、2024年11月に提唱されました。

従来であれば、AIに外部の情報や機能を使わせる場合、個別のAPI設計や独自実装が必要でした。
MCPはこのやり取りを標準化することで、AIが外部ツール・データにアクセスする仕組みの統一を図っている、ということです。

もっとかみ砕いていうと、よく例えとして、MCPはUSB-Cポートのようなものと例えられます。
従来携帯やゲーム機の充電等、媒体によって規格が異なることで、充電ができなかったりということがありましたが、
USB-Cポートが統一規格となれば、上記の問題は解消されます。

それと同様に、MCPは異なるAIモデルやツール間のやりとりを標準化することによって、
簡単に相互に連携できるようになった、という事なんですね。

上の説明を行った下記の画像がMCPの説明では頻繁に使われますので、添付しておきます。
結構しっくりくるかなと思います。

MCPとAPIの違い

通常のAPIは、特定のアプリケーション専用のエンドポイントを提供するため、サービスごとに設計・実装が必要となります。
例えば、Slack API、GitHub API、Google Calendar API等は、それぞれ独立した仕様・エンドポイントを持ちます。

しかしAI、特にLLMを使ったシステムでは、柔軟に複数のアプリケーションにアクセスしたり、文脈に応じて異なる情報ソースからデータを取得したりする必要があります。

この場合、上記で説明した従来型のAPIをそのまま使おうとすると、以下の課題が生じます。

・ N×M問題
→LLMの数(N) × 接続先アプリケーション数(M)の組み合わせごとに、個別のAPIクライアント実装や認証処理を作る必要がある。

・ メンテナンスコストの増加
→「各アプリケーションの仕様変更や認証更新に合わせて、全ての接続コードを更新する必要がある。

NCPはいわば、これらを解決するための共通規格(プロトコル)と言えます。

・ 一つのMCPサーバーを作れば、複数のLLMホストから利用可能
・ 一度実装すれば、ツール・リソース・プロンプト(後述)が統一的に扱える
・ AI向けに設計されているため、JSON Schemaなどで入出力を定義しやすく、LLMが正しくリクエストを作りやすい

つまりまとめると、MCPはAPIの一段手前にある、AIが理解しやすい統一インターフェースであるというのが私の私見です。(あくまで私が解釈しやすいようにまとめた私見なので悪しからず。。。)

MCPのアーキテクチャ

MCPについて説明した先述の画像にあるように、MCPのアーキテクチャは、MCPホストMCPクライアントMCPサーバーの3つに大別されます。

MCPホスト

役割としては、**AI機能を持つ「アプリケーション本体」**になります。
具体例としては、ChatGPT、Claude、Cursor、Cline などで、我々が普段触っている「ChatGPT本体」や「Claudeのアプリ」そのものです。
この中に「MCPクライアント」が組み込まれていて、外部サービスに接続するための土台となります。

MCPクライアント

役割としては、**ホストの中で、MCPルールに従ってサーバーと通信する「橋渡し役」**になります。
イメージとしては、ChatGPTの場合で言うと、中にある 「外部サービス接続用モジュール」 のことで、例えば、MCPクライアントは「Gmailのサーバーにメールを取得するリクエストを送る」「Googleカレンダーから予定を取ってくる」などのやり取りを行います。

MCPサーバー

役割としては、**サービス提供者が用意する、AIからのアクセスの「受け皿」**になります。
具体例としては、GmailならGoogleが用意するMCPサーバー、GitHubならGitHubが用意するMCPサーバーというように、サービス提供者側が用意するものです。(今回はこれを自作してみます)
各サービスのAPIエンドポイントを、MCP仕様に沿った形でまとめた「窓口」であり、クライアントからのリクエストを受け取り、実際のサービスにアクセスして結果を返す役割を担います。

補足:MCPサーバーが提供する機能

MCPサーバーが提供する主な機能は、大きく**ツール(Tools)、リソース(Resources)、プロンプト(Prompts)**の3つに分かれます。

ツール(Tools)

AIから「操作」を呼び出すための仕組みのことで、LLMが呼び出して実行可能な関数アクションのようなものです。
入力(パラメータ)と出力(結果)の仕様がMCPサーバーで定義されており、AIが直接呼び出すことができます。

リソース(Resources)

サーバーが提供するファイルのようなデータセットナレッジを指します。
情報の源泉を提供することで、AIがリソースを探索し、文脈を補強することに役立ちます。

プロンプト(Prompts)

AIに渡す文章テンプレート指示文を、サーバー側で定義できる仕組みのことです。
一貫性のある指示や、事前に設計したプロンプト構造を保つために使用されます。

MCPサーバーを実装してみる

さて、それではMCPサーバーを実装してみます。
今回は、公式が公開している形に則って、NWSというAPIを使用し、アメリカの天気を取得できるMCPサーバーを作っていきましょう。
※今回エディタは VSCode ではなく Cursor を使用していきます。

1. ライブラリインストール

まずは下記で必要ライブラリのインストールを行いましょう。

mkdir mcp-server-quickstart
cd mcp-server-quickstart

npm init -y
npm install @modelcontextprotocol/sdk
npm install -D @types/node typescript

2. package.jsonとtsconfig.jsonの作成

package.jsonでプロジェクトの基本情報を明示、tsconfig.jsonでTypeScriptコンパイラの設定をしていきます。

// package.json
{
  "name": "mcp-server-quickstart",
  "version": "1.0.0",
  "main": "index.js",
  "type": "module",
  "bin": {
    "mcp-server-quickstart": "./build/index.js"
  },
  "scripts": {
    "build": "tsc && chmod 755 build/index.js"
  },
  "files": [
    "build"
  ],
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.18.1"
  },
  "devDependencies": {
    "@types/node": "^24.5.2",
    "typescript": "^5.9.2"
  }
}

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

3. サーバーを実装する

ここまでは前段階でしたが、ここからいよいよサーバーを実装していきます。
少し長くなるのでまずは全体像からお見せします。
/src/index.tsを作成し、中身を下記のように実装します。

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  ListToolsRequestSchema,
  CallToolRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";

// 1. 定数の定義
const NWS_API_BASE = "https://api.weather.gov";
const USER_AGENT = "weather-app/1.0";

// 2. サーバー初期化
const server = new Server(
    { 
        name: "weather", 
        version: "1.0.0" 
    }, 
    { 
        capabilities: {
             tools: {} 
        } 
    }
);

// 3. MCPサーバーのレスポンスを共通化したヘルパー関数
const sendText = (text: string) => ({
    content: [
        { type: "text", text }
    ],
});

// 4. API呼び出し関数
async function makeNWSRequest<T>(url: string): Promise<T> {
  const response = await fetch(url, {
    headers: { "User-Agent": USER_AGENT, Accept: "application/geo+json" },
  });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json() as Promise<T>;
}

// 5. 天気予報を取得する処理
async function getForecast(latitude: number, longitude: number) {
  const points = await makeNWSRequest<{ properties: { forecast?: string } }>(
    `${NWS_API_BASE}/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`
  );
  if (!points.properties?.forecast) throw new Error("No forecast URL available");

  const forecastData = await makeNWSRequest<{ properties: { periods: any[] } }>(
    points.properties.forecast
  );
  return forecastData.properties.periods.map(
    (p) =>
      `${p.name}:\nTemperature: ${p.temperature}°${p.temperatureUnit}\nWind: ${p.windSpeed} ${p.windDirection}\n${p.shortForecast}\n---`
  );
}

// 6. ツール一覧の提供
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "get_forecast",
      description: "Get weather forecast for a location",
      inputSchema: {
        type: "object",
        properties: {
          latitude: { type: "number", description: "Latitude" },
          longitude: { type: "number", description: "Longitude" },
        },
        required: ["latitude", "longitude"],
      },
    },
  ],
}));

// 7. ツール呼び出しの処理
server.setRequestHandler(CallToolRequestSchema, async ({ params }) => {
  if (params.name !== "get_forecast") return sendText("Unknown tool");
  const { latitude, longitude } = params.arguments as { latitude: number; longitude: number };

  try {
    const forecast = await getForecast(latitude, longitude);
    return sendText(`Forecast for ${latitude}, ${longitude}:\n\n${forecast.join("\n")}`);
  } catch (e: any) {
    return sendText(`Failed to get forecast: ${e.message}`);
  }
});

//8.サーバー接続開始
await server.connect(new StdioServerTransport());

さて、それでは分解して説明していきます。

1. 定数の定義

// 1. 定数の定義
const NWS_API_BASE = "https://api.weather.gov";
const USER_AGENT = "weather-app/1.0";

・ NWS_API_BASE
→ アメリカ国立気象局(NWS)の API ベースURLで、天気予報を取得するためのエンドポイントになります。

・ USER_AGENT
→ APIにアクセスする際の「クライアント名」で、HTTPヘッダー(User-Agent)に設定される識別情報となり、NWS APIでは必須項目になります。

2. サーバー初期化

// 2. サーバー初期化
const server = new Server(
    { 
        name: "weather", 
        version: "1.0.0" 
    }, 
    { 
        capabilities: {
             tools: {} 
        } 
    }
);

サーバーインスタンスを作成し、コンストラクタに引数を渡しています。

第一引数にはサーバーの基本情報を表すサーバー名とバージョンを、第二引数にはサーバーがサポートする機能一覧を渡しています。

ここでは tools のみを渡しているため、先程補足で触れた内容の通り、このMCPサーバーはツールを提供するものだと分かりますね。

3. 共通レスポンス用ヘルパー関数

// 3. MCPサーバーのレスポンスを共通化したヘルパー関数
const sendText = (text: string) => ({
    content: [
        { type: "text", text }
    ],
});

別に特別作成する必要があるというわけではないのですが、MCPではレスポンスは content: [{ type: "text", text: "..."}] の形式で返す必要があるので、そのためのショートカット関数になります。
毎回書くのが面倒なので共通化しました。

4. API呼び出し関数

// 4. API呼び出し関数
async function makeNWSRequest<T>(url: string): Promise<T> {
  const response = await fetch(url, {
    headers: { "User-Agent": USER_AGENT, Accept: "application/geo+json" },
  });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json() as Promise<T>;
}

ここでNWSへAPIリクエストしています。
先程定数で作成した USER_AGENT はここでヘッダーに付与して fetch を行います。

5. 天気予報を取得する処理

// 5. 天気予報を取得する処理
async function getForecast(latitude: number, longitude: number) {
  const points = await makeNWSRequest<{ properties: { forecast?: string } }>(
    `${NWS_API_BASE}/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`
  );
  if (!points.properties?.forecast) throw new Error("No forecast URL available");

  const forecastData = await makeNWSRequest<{ properties: { periods: any[] } }>(
    points.properties.forecast
  );
  return forecastData.properties.periods.map(
    (p) =>
      `${p.name}:\nTemperature: ${p.temperature}°${p.temperatureUnit}\nWind: ${p.windSpeed} ${p.windDirection}\n${p.shortForecast}\n---`
  );
}

ここでは実際に天気予報を取得する処理を書いています。
ここで気を付けたいのが、4で作成した関数である makeNWSRequest が2回呼び出されていることです。
これは間違いではなく、NWSのAPIの構造的な仕様に従っており、2段階で天気予報データを取得するように設計されています。

1回目は、指定した緯度経度から予報データ取得用URLを取得しており、(レスポンスイメージは下記)

{
  "properties": {
    "forecast": "https://api.weather.gov/gridpoints/XXX/YY,ZZ/forecast"
  }
}

2回目でようやく**実際の天気データ(予報内容)**が返ってきます。(レスポンスイメージは下記)

{
  "properties": {
    "periods": [
      {
        "name": "Tonight",
        "temperature": 68,
        "temperatureUnit": "F",
        "windSpeed": "5 mph",
        "windDirection": "NE",
        "shortForecast": "Partly Cloudy"
      },
      ...
    ]
  }
}

最後は return していますが、1つ1つの期間 (p.name) ごとに、温度・風速・風向・短い予報文を文字列化して返すようにデータ整形を行っています。

6. ツール一覧の提供

// 6. ツール一覧の提供
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "get_forecast",
      description: "Get weather forecast for a location",
      inputSchema: {
        type: "object",
        properties: {
          latitude: { type: "number", description: "Latitude" },
          longitude: { type: "number", description: "Longitude" },
        },
        required: ["latitude", "longitude"],
      },
    },
  ],
}));

ここでは、MCPクライアントから、**「使えるツールは何か?」**と聞かれたときの応答が記述されています。
提供するツール名は get_forecast とし、ツールの説明文(MCPクライアントがツールを理解する)や、
ツールの入力データの構造(スキーマ、今回は katitude(経度)とlongitude(緯度)で、requiredがあるため入力必須)を定義しています。

7. ツール呼び出しの処理

// 7. ツール呼び出しの処理
server.setRequestHandler(CallToolRequestSchema, async ({ params }) => {
  if (params.name !== "get_forecast") return sendText("Unknown tool");
  const { latitude, longitude } = params.arguments as { latitude: number; longitude: number };

  try {
    const forecast = await getForecast(latitude, longitude);
    return sendText(`Forecast for ${latitude}, ${longitude}:\n\n${forecast.join("\n")}`);
  } catch (e: any) {
    return sendText(`Failed to get forecast: ${e.message}`);
  }
});

ここでは、MCPクライアントが、**「get_forecastを実行して!」**と呼び出した時の処理が記述されています。
まず、params.name が "get_forecast" かどうかのチェックを行い、該当する場合は、緯度・経度の引数を受け取ります。
その後、5で定義した get_forecast() 関数を呼び出し、結果をsedText() で返しています。

8.サーバー接続開始

await server.connect(new StdioServerTransport());

最後、ここでサーバーを起動し、標準入出力でクライアントと接続を開始しています。
これでChatGPTやCursorなどのMCPホストと繋がり、ツールを呼び出せるようになる、というわけです。

4. mcp.jsonの設定

さて、MCPサーバーは作成できたので、続いてMCPサーバーの起動方法を指定する必要があります。
今回は、Cursorから呼び出すので、.cureor/mcp.json を作成し、MCPサーバーの起動に際する設定ファイルを定義していきましょう。

{
  "mcpServers": {
    "american-weather-forecast": {
      "command": "node",
      "args": [
        "C:/Users/tokut/src/mcp-server-quickstart/build/index.js"
      ]
    }
  }
}

この記述の大枠の意味としては、american-weather-forecastという名前のMCPサーバーを、Node.jsで実行するというものになります。
command で Node.js 実行環境を指定し、args では、command に渡す引数の配列を指定(ここでは Node.js に実行させたいスクリプトのパス)しています。

5. 実際に動かしてみる

さて、これでMCPサーバー起動の準備が整いました。
設定ができていると、Cursor Settings の MCP に今回作成したMCPサーバーの情報が登録されているはずです。

image.png

最初は Off になっているので、手動で On にしてあげます。
問題なく起動が完了すれば、緑色のインジケーターが表示されます。

image.png

こうなれば後は、チャットウインドウでアメリカの天気を問い合わせるだけです。
試しに、今日のニューヨークの天気を聞いてみましょう。

image.png

このように、結果が返ってきました。
無事MCPサーバーの実行に成功しました。

長くなりましたが、ご覧いただきありがとうございました。

参考文献

Build an MCP server
MCPサーバー自作入門

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?