オリジナルのちょっと便利な MCP サーバー を作ってみた

はじめに

こんにちは、サイオステクノロジーの小沼 俊治です。
以前の記事ではベンダーが公開されている MCP サーバーを組み込んで利用してみましたが、

もう少し仕組みを理解してみたくなったので、独自で考えた機能を搭載した
「オリジナルのちょっと便利な MCP サーバー」 を自作してみることにしました。
実際に MCP サーバーの実装に取り組んで、色々試していく過程でやっと手順が理解でき、その経験から得られたナレッジをまとめましたので、これから MCP サーバーを作りたい皆さんの力になれたら嬉しいです。

何ができるのかは、私たちが普段話す言葉で Claude から天気予報を教えてもらう驚きのデモンストレーションをご覧ください。

なお、MCP については既にネット上でたくさんのコンテンツが親切に説明されているので、本資料の範囲外とさせて頂きます。個人的には、KDDIアジャイル開発センター 御田さんが書かれた以下資料がお薦めです。


構成概要

筆者が動かした際の主な構成要素は以下の通りです。

  • Windows 10 Professional
  • Claude desktop for Windows version 0.9.3
  • Windows PowerShell 5.1.19041.5737
  • Node.js v22.15.0
  • Visual Studio Code version 1.99.3

ハンズオンを構成する環境は以下の通りです。

  • MCP ホストは、Claude desktop for Windows のネイティブアプリ版を利用します。
  • MCP サーバーの構成における主な考慮事項は以下の通りです。
    • MCP サーバーを稼働させる環境は Node.js 環境を選んでいます。
    • ソースコードは TypeScript 言語で実装します。
    • MCP サーバーを実行するために、TypeScript で実装したソースコードをトランスパイルして JavaScript のソースコードを生成します。
    • 実装したツールは、MCP Inspector を起動してブラウザから動作確認します。
    • 気象庁の API を利用した天気予報を取得する MCP サーバーのツールを実装します。

MCPサーバーとして実装する4つのツール仕様は以下の通りです。

  1. 「get_officecode_from_prefecture」は、都道府県名からエリアコード API を用いて、天気予報 API で必要な気象台コードを取得します。
  2. 「get_officecode_from_regionname」は、地域名からエリアコード API を用いて、天気予報 API で必要な気象台コードを取得します。
  3. 「get_officecode_from_cityname」は、地区町村名からエリアコード API を用いて、天気予報 API で必要な気象台コードを取得します。
  4. 「get_weather_forecast_from_areacode」は、気象台コードから天気予報 API を用いて天気予報を取得します。

ハンズオンの手順で作成する MCP サーバーの設定ファイルやソースコードは、以下の GitHub リポジトリで公開しています。手順と合わせてご確認ください。


基礎環境の構築

Claude desktop for Windows インストール

MCP ホストに Web 版ではなくアプリ版の Claude desktop for Windows を利用するため、以下公式手順を参考に Windows PC へインストールします。

Node.js インストール

JavaScript の実行環境が必要なため、以下手順を参考に Windows PC へ Node.js をインストールします。

Visual Studio Code インストール

Windows 環境でソースコードや設定ファイルの編集をするためのエディターとして Visual Studio Code (VS Code) のインストールをお薦めします。インストール手順については、ネット上でたくさんのコンテンツが親切に説明されているので、本資料の範囲外とさせて頂きます。

なお、VS Code 以外のお気に入りのエディターを利用される場合は、文中の VS Code をご利用のエディターに置き換えて読み進めてください。


MCP サーバーのスケルトンを作成

プロジェクトの新規作成

Windows のスタートメニュー「W」セクションから「Windows PowerShell」を選択して起動します。

Window PowerShell ではコマンドラインベースで操作を行います。

「ドキュメント」フォルダ配下に、MCP サーバー開発用のプロジェクトフォルダ「devlopment\hands-on-mcp-sios-apisl」を作成して遷移します。

PS C:\Users\...> cd ~\Documents
PS C:\Users\...\Documents> mkdir -p development\hands-on-mcp-sios-apisl
PS C:\Users\...\Documents> cd development\hands-on-mcp-sios-apisl

プロジェクトの情報管理を担う「package.json」ファイルを生成します。

PS C:\Users\...\hands-on-mcp-sios-apisl> npm init -y

MCP サーバーの実装に必要な依存パッケージを「package.json」ファイルに定義します。

PS C:\Users\...\hands-on-mcp-sios-apisl> npm install @modelcontextprotocol/sdk zod
PS C:\Users\...\hands-on-mcp-sios-apisl> npm install -D typescript @types/node

TypeScript から JavaScript へのトランスパイルに必要な、コンパイルオプションを管理する「tsconfig.json」ファイルを生成します。

PS C:\Users\...\hands-on-mcp-sios-apisl> npx tsc --init

プロジェクトの初期化状態でファイルが揃いました。

PS C:\Users\...\hands-on-mcp-sios-apisl> ls

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----        2025/05/07     10:00                node_modules
-a----        2025/05/07     10:00          36094 package-lock.json
-a----        2025/05/07     10:00            399 package.json
-a----        2025/05/07     10:00          12926 tsconfig.json

ビルド環境の整備

TypeScript から JavaScript へソースコードをトランスパイルできるように設定ファイルを整えため VS Code を起動します。

PS C:\Users\...\hands-on-mcp-sios-apisl> code .

プロジェクトフォルダをルートに VS Code が起動しました。

まず始めに「package.json」ファイルを VS Code で開き、ビルド用のコマンドを追記します。以下のファイルイメージで、各行の左端に記載されている「+」は、ファイルに追加する行を表現しているため、「+」自体は実際に追記しません。

	   :
	  "main": "index.js",
	  "scripts": {
+	    "build": "tsc",
	    "test": "echo \"Error: no test specified\" && exit 1"
	  },
	  "repository": {
	   :
  • 設定ファイルの編集における主な考慮事項は以下の通りです。
    • 「build」タスクでは、tsc コマンドが「tsconfig.json」ファイルに従って TypeScript から JavaScript へソースコードをトランスパイルします。
  • 編集後のできあがりは以下を参考にしてください。

次に「tsconfig.json」ファイルを VS Code で開き、以下の内容に書き換えます。

{
  "compilerOptions": {
    "target": "ES2023",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}
  • 設定ファイルの編集における主な考慮事項は以下の通りです。
    • 「target」「module」「moduleResolution」には現時点で選べる最新の値を指定します。
    • 「outDir」には、tsc コマンドでビルドしたソースコードを出力するフォルダを指定します。
    • 「rootDir」には、tsc コマンドでビルドするソースコードが存在するフォルダを指定します。
  • 編集後のできあがりは以下を参考にしてください。

初期状態の index.ts を作成

Windows PowerShell に戻り、MCP サーバーが起動できるようにソースコードの実装と管理をする「src」フォルダを作成します。

PS C:\Users\...\hands-on-mcp-sios-apisl> mkdir src

VS Code に戻り、MCP サーバーを起動するスケルトン状態の「src\index.ts」を、以下のソースコードをコピペして新規に作成します。

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({
  name: "mcp-sios-apisl-demo",
  version: "1.0.0",
});

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("MCP Server running on stdio");
}

main().catch((error) => {
  console.error("Fatal error in main():", error);
  process.exit(1);
});
  • 編集後のできあがりは以下を参考にしてください。

ビルドしてトランスパイル

src\index.ts」の作成が完了したら、スケルトンのできあがりを動作確認するために、ビルドして TypeScript から JavaScript のソースコードを生成します。

PS C:\Users\...\hands-on-mcp-sios-apisl> npm run build

> hands-on-mcp-sios-apisl@1.0.0 buildw
> tsc

ビルドが完了すると「build\index.js」ができあがり、プロジェクト全体としては以下の構成となります。(「node_modele」フォルダ配下の記載は省略します)

PS C:\Users\...\hands-on-mcp-sios-apisl> tree /f

C:.
│  package-lock.json
│  package.json
│  tsconfig.json
│
├─build
│      index.js
│
├─node_modules
│  :
│ (省略)
│
└─src
        index.ts

これまでの手順で作成したプロジェクトファイルを GitHub のリポジトリで管理する際は、以下にご留意ください。

  • package-lock.json」「package.json」「tsconfig.json」のファイルを構成管理します。
  • src」フォルダ、およびその配下のソースコードを構成管理します。
  • node_module」フォルダは構成管理の対象外とし、必要な依存パッケージは「npm install」コマンドで npm レジストリから随時ダウンロードします。
  • build」フォルダは構成管理の対象外とし、必要な際にビルドして JavaScript のソースコードを生成します。

MCP Inspector で動作確認

MCP サーバーのスケルトンができあがったことを確認するために、MCP Inspector を起動してログに表示される URL へブラウザでアクセスします。
(以下のコンソールイメージでは「http://127.0.0.1:6274」がアクセス先の URL です)

PS C:\Users\...\hands-on-mcp-sios-apisl> npx @modelcontextprotocol/inspector node build/index.js

Starting MCP inspector...
Proxy server listening on port 6277
MCP Inspector is up and running at http://127.0.0.1:6274

ブラウザで MCP Inspector へアクセスしたら画面左中央部の「Connect」ボタンをクリックして MCP サーバーに接続します。

現時点の MCP サーバーはツールが一つもないので、画面中央には「The connected server does not support any MCP capabilities」のメッセージが表示されるだけです。スケルトンは無事にできあがりました。


オリジナルの MCP サーバーを実装

スケルトンにツールを実装

MCP サーバーのスケルトンに4つのツールを構造化して実装するので、Windows PowerShell で「src」フォルダ配下にサブフォルダを作成します。

PS C:\Users\...\hands-on-mcp-sios-apisl> mkdir src\interfaces
PS C:\Users\...\hands-on-mcp-sios-apisl> mkdir src\services

以下の構成でフォルダとファイルが存在します。

PS C:\Users\...\hands-on-mcp-sios-apisl> tree /f src

C:\USERS\{ユーザー名}\DOCUMENTS\DEVELOPMENT\HANDS-ON-MCP-SIOS-APISL\SRC
│  index.ts
│
├─interfaces
└─services

VS Code をアクティブにして、新規に「src\interfaces\areaApi.ts」ファイルを作成してインターフェースを定義します。

export interface OfficeInfo {
  name: string;
  officeName: string;
  children: string[];
}

export interface OfficeCodes {
  [key: string]: OfficeInfo;
}

export interface AreaInfo {
  name: string;
  parent: string;
}

export interface AreaCodes {
  [key: string]: AreaInfo;
}

export interface AreaApiData {
  centers: OfficeCodes;
  offices: OfficeCodes;
  class10s: AreaCodes;
  class15s: AreaCodes;
  class20s: AreaCodes;
}

続いて新規に「src\services\areaWeatherForecast.ts」ファイルを作成し、ツールから呼び出される関数を実装します。

import { AreaApiData, OfficeCodes, AreaCodes } from "../interfaces/areaApi.js";

const URL_AREA = "https://www.jma.go.jp/bosai/common/const/area.json";
const URL_WEATHER_FORECAST = "https://www.jma.go.jp/bosai/forecast/data/forecast/{areaCode}.json";

export async function get_officecode_from_prefecture(prefecture: string): Promise<string> {
//  console.error(">>> areaName: ", areaName);
  const json: AreaApiData = await fetchAreaDataFromJmaAreaAPI();
  let officeCode = retrieveOfficeCodeFromPrefecture(json, prefecture);
  return officeCode;
}

export async function get_officecode_from_regionname(regionName: string): Promise<string> {
  const json: AreaApiData = await fetchAreaDataFromJmaAreaAPI();
  let officeName = retrieveOfficeNameFromRegionName(json, regionName);
  let officeCode = retrieveOfficeCodeFromOfficeName(json, officeName);
  return officeCode;
}

export async function get_officecode_from_cityname(cityName: string): Promise<string> {
  const json: AreaApiData = await fetchAreaDataFromJmaAreaAPI();
  let areaCode = retrieveAreaCodeFromCityName(json, cityName);
  let subOfficeCode = retrieveSubOfficeCodeFromAreaCode(json, areaCode);
  let officeCode = retrieveOfficeCodeFromSubOfficeCode(json, subOfficeCode);
  return officeCode;
}

export async function get_weather_forecast_from_officecode(areaCode: string): Promise<string> { 
  const resp = await fetch(URL_WEATHER_FORECAST.replace("{areaCode}", areaCode));
  const body = await resp.text();
  return body;
}

async function fetchAreaDataFromJmaAreaAPI(): Promise<AreaApiData> {
  const resp = await fetch(URL_AREA);
  const body = await resp.text();
  return JSON.parse(body);
}

function retrieveOfficeNameFromRegionName(json: AreaApiData, regionName: string): string {

  regionName = regionName.trim();
  if (regionName === "") {
    return "";
  }
  const officeCodes: OfficeCodes = json.centers;

  for (const code in officeCodes) {
    const target = officeCodes[code].name;
    if (target === regionName) {
      return officeCodes[code].officeName;
    }
  }
  for (const code in officeCodes) {
    const target = officeCodes[code].name;
    if (target.startsWith(regionName)) {
      return officeCodes[code].officeName;;
    }
  }

  return "";
}

function retrieveOfficeCodeFromPrefecture(json: AreaApiData, prefecture: string): string {

  prefecture = prefecture.trim();
  if (prefecture === "") {
    return "";
  }
  const officeCodes: OfficeCodes = json.offices;

  for (const code in officeCodes) {
    const target = officeCodes[code].name;
    if (target === prefecture) {
      return code;
    }
  }
  for (const code in officeCodes) {
    const target = officeCodes[code].name;
    if (target.startsWith(prefecture)) {
      return code;
    }
  }

  return "";
}

function retrieveOfficeCodeFromOfficeName(json: AreaApiData, officeName: string): string {

  officeName = officeName.trim();
  if (officeName === "") {
    return "";
  }
  const officeCodes: OfficeCodes = json.offices;

  for (const code in officeCodes) {
    const target = officeCodes[code].officeName;
    if (target === officeName) {
      return code;
    }
  }
  for (const code in officeCodes) {
    const target = officeCodes[code].officeName;
    if (target.startsWith(officeName)) {
      return code;
    }
  }

  return "";
}

function retrieveAreaCodeFromCityName(json: AreaApiData, cityName: string): string {

  cityName = cityName.trim();
  if (cityName === "") {
    return "";
  }
  const areaCodes: AreaCodes = json.class20s;

  for (const code in areaCodes) {
    const target = areaCodes[code].name;
    if (target === cityName) {
      return areaCodes[code].parent;
    }
  }
  for (const code in areaCodes) {
    const target = areaCodes[code].name;
    if (target.startsWith(cityName)) {
      return areaCodes[code].parent;
    }
  }

  return "";
}

function retrieveSubOfficeCodeFromAreaCode(json: AreaApiData, areaCode: string): string {

  areaCode = areaCode.trim();
  if (areaCode === "") {
    return "";
  }
  const areaCodes: AreaCodes = json.class15s;

  for (const code in areaCodes) {
    if (code === areaCode) {
      return areaCodes[code].parent;
    }
  }

  return "";
}

function retrieveOfficeCodeFromSubOfficeCode(json: AreaApiData, subOfficeCode: string): string {

  subOfficeCode = subOfficeCode.trim();
  if (subOfficeCode === "") {
    return "";
  }
  const officeCodes: OfficeCodes = json.offices;

  for (const code in officeCodes) {
    const target = officeCodes[code].children;
    if (target.includes(subOfficeCode)) {
      return code;
    }
  }

  return "";
}

既に存在している MCP サーバーのスケルトン「src\index.ts」へツールを実装するために2か所を改修します。

1か所目の改修は以下の通りです。行頭の「+」は追加する行を指示するイメージになります。

	 :
	import { z } from "zod";
+	import { get_officecode_from_prefecture, get_officecode_from_regionname, get_officecode_from_cityname, get_weather_forecast_from_officecode } from "./services/areaWeatherForecast.js";

	const server = new McpServer({
	 :
  • ソースコードの改修における主な考慮事項は以下の通りです。
    • import 文にて、ツールが呼び出す関数を読み込みます。

2か所目の改修は以下の通りです。

	 :
	const server = new McpServer({
	  name: "mcp-example",
	  version: "1.0.0",
	});

+	server.tool(
+	  "get_officecode_from_prefecture",
+	  "Get an office code from a prefecture before getting a weather forecast.",
+	  {prefecture: z.string().describe("Prefecture")},
+	  async ({prefecture}) => {
+	    const areaCode = await get_officecode_from_prefecture(prefecture);
+	    return {content: [{type: "text", text: areaCode}]};
+	  }
+	);
+
+	server.tool(
+	  "get_officecode_from_regionname",
+	  "Get an office code from a region name before getting a weather forecast.",
+	  {regionName: z.string().describe("Region name")},
+	  async ({regionName}) => {
+	    const areaCode = await get_officecode_from_regionname(regionName);
+	    return {content: [{type: "text", text: areaCode}]};
+	  }
+	);
+
+	server.tool(
+	  "get_officecode_from_cityname",
+	  "Get an office code from a city name before getting a weather forecast.",
+	  {cityName: z.string().describe("City name")},
+	  async ({cityName}) => {
+	    const areaCode = await get_officecode_from_cityname(cityName);
+	    return {content: [{type: "text", text: areaCode}]};
+	  }
+	);
+
+	server.tool(
+	  "get_weather_forecast_from_officecode",
+	  "Get a weather forecast from an office code.",
+	  {officeCode: z.string().describe("Office code")},
+	  async ({officeCode}) => {
+	    const body = await get_weather_forecast_from_officecode(officeCode);
+	    return {content: [{type: "text", text: body}]};
+	  }
+	);

	async function main() {
	 :
  • ソースコードの改修における主な考慮事項は以下の通りです。
    • 4つのツールの公開と、ビジネスロジックが実装された関数を呼び出します。
  • 編集後のできあがりは以下を参考にしてください。

ここまでの手順で以下のフォルダとファイル構成ができあがります。

PS C:\Users\...\hands-on-mcp-sios-apisl> tree /f src

C:\USERS\{ユーザー名}\DOCUMENTS\DEVELOPMENT\HANDS-ON-MCP-SIOS-APISL\SRC
│  index.ts
│
├─interfaces
│      area.ts
│
└─services
        areaWeatherForecast.ts

ビルドしてトランスパイル

ソースコードが揃ったので、実装したツールのできあがりを動作確認するために、ビルドして TypeScript から JavaScript のソースコードを生成します。

PS C:\Users\...\hands-on-mcp-sios-apisl> npm run build

> hands-on-mcp-sios-apisl@1.0.0 buildw
> tsc

MCP Inspector で動作確認

MCP サーバーに実装したツールを動作確認するため、MCP Inspector を起動し、ログに表示される URL へブラウザでアクセスします。ここでは、実装した4つのツールの中から、「get_officecode_from_prefecture」と「get_weather_forecast_from_officecode」の2つのテスト手順を説明します。

PS C:\Users\...\hands-on-mcp-sios-apisl> npx @modelcontextprotocol/inspector node build/index.js

Starting MCP inspector...
Proxy server listening on port 6277
MCP Inspector is up and running at http://127.0.0.1:6274

ブラウザで MCP Inspector へアクセスしたら画面左中央部の「Connect」ボタンをクリックして MCP サーバーに接続します。

MCP サーバーにツールが搭載されているので、画面中央部に Tools 関連の情報が表示されており、「List Tools」ボタンをクリックすると接続した MCP サーバーが持っているツール一覧が表示されます。

表示されているツール一覧から2つのツールをテストします。
まずは「get_officecode_from_prefecture」ツールを選んでテストを実施します。

テスト手順は以下の通りです。

  1. List Tools から「get_officecode_from_prefecture」ツールをクリックします。
  2. prefecture のテキストボックスに都道府県名を入力します。
  3. 「Run Tool」ボタンをクリックすると都道府県名に該当する気象台コードが表示され、正常にツールが動作することを確認できます。

次に「get_weather_forecast_from_officecode」ツールをテストします。

テスト手順は以下の通りです。

  1. List Tools から「get_weather_forecast_from_officecode」ツールをクリックします。
  2. officeCode のテキストボックスに先に実施した「get_officecode_from_prefecture」ツールのテストで得られた気象台コードを入力します。
  3. 「Run Tool」ボタンをクリックすると気象台コードに該当する天気予報が表示され、正常にツールが動作することを確認できます。

Claude(MCP ホスト)に組み込み

連携先 MCP サーバーの設定

オリジナルの MCP サーバーが Claude で利用できるように設定ファイルを編集するため、Windows のスタートメニュー「A」セクションから「Anthropic > Claude」を選択して起動します。

Claude が起動したら画面左上のハンバーガーメニューから「ファイル > 設定…」を選択します。

オリジナルの MCP サーバーが利用できるようにするために、「開発者」タブの「構成を編集」ボタンをクリックします。

エクスプローラーが立ち上がり、Claude の設定ファイルに該当する「claude_desktop_config.json」ファイルをダブルクリックしてエディターで開きます。

claude_desktop_config.json」ファイルにオリジナルの MCP サーバーを利用するための設定を書いて保存します。

{
	"mcpServers": {
		"mcp-sios-apisl-demo": {
			"command": "node",
			"args": [
				"C:\\Users\\{ユーザー名}\\Documents\\development\\hands-on-mcp-sios-apisl\\build\\index.js"
			]
		}
	}
}

claude_desktop_config.json」ファイルの編集における主な考慮事項は以下の通りです。

  • 「mcpServers > mcp-sios-apisl-demo > args」フィールドで主な考慮事項は以下の通りです。
    • node コマンドが MCP サーバーとして起動する「index.js」の Path を指定します。
    • Path にある「{ユーザー名}」は PC で利用しているユーザー名に置き換えます。
    • Path でフォルダの区切りを示す「\」は二重の「\\」で書く必要があります。

Claude の再起動で設定反映

変更した設定ファイルの適用に、ウィンドウの「×」ボタンで終了させるだけではなく、Claude のプロセスを完全に終了させる必要があるため、Windows タスクバーの画面右側のアイコン表示領域から Claude アイコン(トゲトゲのウニの様なデザイン)を探し、右クリックで表示されるメニューから「終了」を選択してプロセスを完全に終了させます。

Windows のスタートメニュー「A」セクションから「Anthropic > Claude」を選択して起動します。

Claude が起動したら画面中央のテキストボックス内の左下にある「検索とツール」アイコンをクリックします。表示されたメニューに、「mcp-sios-apisl-demo」の項目と右横に数字が表示されていたら、オリジナルの MCP サーバーは正常に設定できました。


オリジナルの MCP サーバーのデモ

私たちが普段話す言葉で Claude から天気予報を教えてもらう驚きのデモンストレーションをご覧ください。

  1. 高知県の天気予報を教えてください
  2. 湘南の天気予報を教えてください
  3. 次の火曜日はゴルフなんだけど鹿沼72カントリークラブの天気を教えて

まとめ

オリジナルの MCP サーバーの実装手順は、いかがでしたでしょうか?MCP は生成 AI をより実用的なものにする可能性を秘めた技術だと感じています。ぜひ、皆さんの自由な発想で様々なアイデアを試してみてください。

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

0人がこの投稿は役に立ったと言っています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です