iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

ローカルMCPサーバを立ててClaudDesktopと繋いでみた

◼️ はじめに

こんにちは!株式会社iimonでフロントエンジニアをしているあめくです!

前回は我らがEMの松田さんに「MCPサーバ」について熱く語っていただきました。

その熱き思いに心を打たれたこともあり、また私自身もMCPサーバとはなんぞやと言うことで実際にサーバを立てて動かしてみようと思いテックブログにさせていただきました。

MCPサーバの詳細については前回の記事をご覧ください!

駆け出しエンジニアはAI禁止!?使う前に身に着けてほしい2つの力〜RustもMCPも知らない中年エンジニアの奮闘から〜 - iimon TECH BLOG

今回はローカルにMCPサーバを立てて、不動産ライブラリという国土交通省が提供してるAPIを叩いてデータを取得するということを試したいと思います!
また作成したローカルMCPサーバをClaudDesktopと連携させ、Claudeくんと会話しつつMCPサーバを叩きたいと思います!

不動産ライブラリ

◼️ プロジェクト作成

まずはローカル環境でMCPサーバを立てるために、プロジェクトを作成していきます。

Cloudflareを一緒に試したいと思いwranglerを用いてプロジェクト作成しました。
*Cloudflareのアカウント作成などは省略します

$ npm install -g wrangler
$ wrangler login

$ wrangler init test-mcp-server
$ cd test-mcp-server

wrangler init test-mcp-server を実行することでCloudflareにデプロイできる環境まで作成してくれます。

初期状態では下記のコードだと思いますので、このコードを変更してMCPサーバとして動かせるようにします。

  • src/index.ts
export default {
    async fetch(request, env, ctx): Promise<Response> {
        return new Response('Hello World!');
    },
} satisfies ExportedHandler<Env>;

MCPサーバを立てるにあたり、今回必要なライブラリをインストールします。

$ npm install @modelcontextprotocol/sdk zod agents

◼️ コード修正

src/index.tsを下記のように書き換えます

  • src/index.ts
import { McpAgent } from 'agents/mcp';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';

const PREFECTURE_CODES_TO_JAPANESE_NAMES: { [key: string]: string } = {
    '01': '北海道',
    '02': '青森県',
    '03': '岩手県',
    '04': '宮城県',
    '05': '秋田県',
    '06': '山形県',
    '07': '福島県',
    '08': '茨城県',
    '09': '栃木県',
    '10': '群馬県',
    '11': '埼玉県',
    '12': '千葉県',
    '13': '東京都',
    '14': '神奈川県',
    '15': '新潟県',
    '16': '富山県',
    '17': '石川県',
    '18': '福井県',
    '19': '山梨県',
    '20': '長野県',
    '21': '岐阜県',
    '22': '静岡県',
    '23': '愛知県',
    '24': '三重県',
    '25': '滋賀県',
    '26': '京都府',
    '27': '大阪府',
    '28': '兵庫県',
    '29': '奈良県',
    '30': '和歌山県',
    '31': '鳥取県',
    '32': '島根県',
    '33': '岡山県',
    '34': '広島県',
    '35': '山口県',
    '36': '徳島県',
    '37': '香川県',
    '38': '愛媛県',
    '39': '高知県',
    '40': '福岡県',
    '41': '佐賀県',
    '42': '長崎県',
    '43': '熊本県',
    '44': '大分県',
    '45': '宮崎県',
    '46': '鹿児島県',
    '47': '沖縄県',
};

// 都道府県名から都道府県コードを取得する関数
function getPrefectureCode(prefectureName: string): string | null {
    // 完全一致を優先
    for (const [code, name] of Object.entries(PREFECTURE_CODES_TO_JAPANESE_NAMES)) {
        if (name === prefectureName) {
            return code;
        }
    }

    // 部分一致(「沖縄」→「沖縄県」など)
    for (const [code, name] of Object.entries(PREFECTURE_CODES_TO_JAPANESE_NAMES)) {
        // 入力された名前が都道府県名に含まれる場合
        if (name.includes(prefectureName)) {
            return code;
        }
        // 都道府県の接尾辞を除いた名前が入力された場合(「東京」→「東京都」)
        const baseName = name.replace(/[都道府県]$/, '');
        if (baseName === prefectureName) {
            return code;
        }
    }

    return null;
}

// 候補を取得する関数
function getPrefectureSuggestions(prefectureName: string): string[] {
    return Object.values(PREFECTURE_CODES_TO_JAPANESE_NAMES)
        .filter((name) => name.includes(prefectureName) || prefectureName.includes(name.replace(/[都道府県]$/, '').slice(0, 2)))
        .slice(0, 5);
}

export class MCP extends McpAgent {
    server = new McpServer({
        name: 'Omikuji Fortune Teller',
        version: '1.0.0',
    });

    async init() {
        this.server.tool(
            'getPrefectureCode',
            {
                prefectureName: z.string().describe('都道府県名(例:東京都、沖縄、北海道など)'),
            },
            async ({ prefectureName }) => {
                const code = getPrefectureCode(prefectureName);

                if (!code) {
                    const suggestions = getPrefectureSuggestions(prefectureName);
                    const suggestionText =
                        suggestions.length > 0 ? `\n\n候補: ${suggestions.join(', ')}` : '\n\n正確な都道府県名を入力してください。';

                    return {
                        content: [
                            {
                                type: 'text',
                                text: `都道府県「${prefectureName}」が見つかりませんでした${suggestionText}`,
                            },
                        ],
                    };
                }

                const fullName = PREFECTURE_CODES_TO_JAPANESE_NAMES[code];

                return {
                    content: [
                        {
                            type: 'text',
                            text: `都道府県コード取得結果\n\n都道府県名: ${fullName}\n都道府県コード: ${code}`,
                        },
                    ],
                };
            }
        );

        this.server.tool(
            'getMunicipalitiesList',
            {
                areaCode: z.string().length(2).describe(`都道府県コード(例:'13' (東京))`),
            },
            async ({ areaCode }) => {
                console.log(`Fetching real estate data for area code: ${areaCode}`);
                const apiKey = process.env.API_KEY; // 環境変数からAPIキーを取得
                if (!apiKey) {
                    return {
                        content: [{ type: 'text', text: 'Error: API Key is missing' }],
                    };
                }

                // APIエンドポイントとパラメータの構築
                const apiBaseUrl = 'https://www.reinfolib.mlit.go.jp/ex-api/external/XIT002';
                const url = new URL(apiBaseUrl);
                url.searchParams.append('area', areaCode);

                try {
                    const response = await fetch(url.toString(), {
                        headers: {
                            'Ocp-Apim-Subscription-Key': apiKey,
                            Accept: 'application/json',
                        },
                    });

                    if (!response.ok) {
                        const errorText = await response.text(); // エラーレスポンスはテキストである可能性も考慮
                        return {
                            content: [
                                {
                                    type: 'text',
                                    text: `API Error: ${response.status} - ${errorText}`,
                                },
                            ],
                        };
                    }

                    const parsedData = await response.json();

                    return {
                        content: [
                            {
                                type: 'text',
                                text: JSON.stringify(parsedData, null, 2), // 取得したデータをJSON形式で返す
                            },
                        ],
                    };
                } catch (error: unknown) {
                    if (error instanceof Error) {
                        return {
                            content: [{ type: 'text', text: `Fetch Error: ${error.message}` }],
                        };
                    }
                    return {
                        content: [
                            {
                                type: 'text',
                                text: 'An unknown error occurred during API fetch.',
                            },
                        ],
                    };
                }
            }
        );

        this.server.tool(
            'getPropertyPriceList',
            {
                areaCode: z.string().length(2).optional().describe(`都道府県コード(例:'13' (東京))`),
                municipalityCode: z.string().length(5).optional().describe(`市区町村コード(例:'13101' (千代田区))`),
                year: z.string().length(4).describe(`対象年(例:2025)`), // 例: '2023'
            },
            async ({ areaCode, municipalityCode, year }) => {
                const hasAreaCode = typeof areaCode === 'string' && areaCode.length > 0;
                const hasMunicipalityCode = typeof municipalityCode === 'string' && municipalityCode.length > 0;
                // どちらも存在しない場合
                if (!hasAreaCode && !hasMunicipalityCode) {
                    return {
                        content: [
                            {
                                type: 'text',
                                text: `areaCode または municipalityCode のいずれかを指定してください。`,
                            },
                        ],
                    };
                }

                const apiKey = process.env.API_KEY; // 環境変数からAPIキーを取得
                if (!apiKey) {
                    return {
                        content: [{ type: 'text', text: 'Error: API Key is missing' }],
                    };
                }

                // APIエンドポイントとパラメータの構築
                const apiBaseUrl = 'https://www.reinfolib.mlit.go.jp/ex-api/external/XIT001';
                const url = new URL(apiBaseUrl);
                if (hasAreaCode) {
                    url.searchParams.append('area', areaCode);
                }
                if (hasMunicipalityCode) {
                    url.searchParams.append('city', municipalityCode);
                }
                url.searchParams.append('year', year);

                try {
                    const response = await fetch(url.toString(), {
                        headers: {
                            'Ocp-Apim-Subscription-Key': apiKey,
                            Accept: 'application/json',
                        },
                    });

                    if (!response.ok) {
                        const errorText = await response.text(); // エラーレスポンスはテキストである可能性も考慮
                        return {
                            content: [
                                {
                                    type: 'text',
                                    text: `API Error: ${response.status} - ${errorText}`,
                                },
                            ],
                        };
                    }

                    const parsedData = await response.json();
                    return {
                        content: [
                            {
                                type: 'text',
                                text: JSON.stringify(parsedData, null, 2), // 取得したデータをJSON形式で返す
                            },
                        ],
                    };
                } catch (error: unknown) {
                    if (error instanceof Error) {
                        return {
                            content: [{ type: 'text', text: `Fetch Error: ${error.message}` }],
                        };
                    }
                    return {
                        content: [
                            {
                                type: 'text',
                                text: 'An unknown error occurred during API fetch.',
                            },
                        ],
                    };
                }
            }
        );
    }
}

export default {
    fetch(request: Request, env: any, ctx: ExecutionContext) {
        const url = new URL(request.url);
        if (url.pathname === '/mcp') {
            return MCP.serve('/mcp').fetch(request, env, ctx);
        }

        return new Response('Not found', { status: 404 });
    },
};
  • getPrefectureCode 都道府県名を引数で受け取り、都道府県コードを取得するToolです。
  • getMunicipalitiesList 都道府県コードを引数で受け取り、不動産ライブラリの 都道府県内市区町村一覧取得API を叩いて市区町村コードを取得するToolです。
  • getPropertyPriceList 都道府県コードまたは市区町村コードを引数で受け取り、不動産ライブラリの 不動産価格(取引価格・成約価格)情報取得API を叩いて不動産価格(取引価格・成約価格)情報を取得するToolです。

続いてwrangler.jsoncに下記の処理を追加します。

{
    "$schema": "node_modules/wrangler/config-schema.json",
    "name": "test-mcp-server",
    "main": "src/index.ts",
    "compatibility_date": "2025-06-14",
    "compatibility_flags": ["nodejs_compat"],
    "migrations": [
        {
            "new_sqlite_classes": ["MCP"],
            "tag": "v1"
        }
    ],
    "durable_objects": {
        "bindings": [
            {
                "class_name": "MCP",
                "name": "MCP_OBJECT"
            }
        ]
    },
    "observability": {
        "enabled": true
    }
}

次に .dev.varsに不動産ライブラリから取得したAPIKeyを設定します。

  • .dev.vars
API_KEY='取得したAPIKey'

上記の内容を記載すれば、動くはずです!!!!

◼️ デバッグ方法

MCPサーバですが、公式のMCP inspector やPostman、@muppet-kit/inspector などを用いることでデバックすることが可能です。

今回は公式のMCP inspectorを使ってみたいと思います!

下記コマンド打てば http://127.0.0.1:6274 でサービスが立ち上がりGUIで確認することが可能です。

$ npx @modelcontextprotocol/inspector@latest

http://localhost:6274/?MCP_PROXY_AUTH_TOKEN={token情報} をコピペしてブラウザのアドレスバーに貼り付けて確認できます!

※ Postmanや@muppet-kit/inspector については下記参考に!
参考: https://qiita.com/nagix/items/712672a7bc741eef03aa
参考: https://www.npmjs.com/package/@muppet-kit/inspector

List Toolsを押せば作成したTool一覧が表示されます。
今回の場合、 getPrefectureCode getMunicipalitiesList getPropertyPriceList のtoolを作成したのでこの一覧が表示されてます。

また、Toolを選択し引数を設定してRun Toolを押すことで指定したToolを実行することができます。

下記の例では沖縄県を指定して実行することで都道府県コードが取得できました。

一通り作成したToolは動きました!

getPrefectureCode の確認

getMunicipalitiesList の確認

getPropertyPriceList の確認

◼️ ClaudeDesktopとの接続

ClaudeDesktopはAnthropic社が提供しているサービスです。 このデスクトップアプリは設定をすることでMCPサーバと接続できるため試します!
※ Claudeのデスクトップアプリをインストールする必要があります 。

claude→設定→開発者→構成を編集 を選択するとFinderが開くため、 claude_desktop_config.json ファイルを編集します。
https://claude.ai/download

今回は下記の設定を記載しました。 設定を変更したらClaudeDesktopを再起動させます。

{
  "mcpServers": {
    "local": {
      "command": "npx", // npxのパス
      "args": ["mcp-remote@latest", "http://localhost:8787/mcp"]
    }
  }
}

では動かしてみましょう! いい感じに各toolを呼び出して動いてそうですね!

質問変えたりすることでグラフを表示したりすることもできました。

◼️ まとめ

今回はコード内に都道府県名と都道府県コードを定数としてデータを持たせてましたが、データベースにいろんなデータを格納して取得するようにすることは可能です。
(Cloudflareの場合、D1とかですぐ試せます)

今回は不動産Saasらしく不動産ライブラリのAPIを利用させてもらいましたが、データベースを活用してMCPサーバで接続するといろんなことができそうですね!
(可能性は無限大!!)

しかし便利そうになりそうだと思う反面懸念点もあります。
MCPサーバはAIが出てきて今盛り上がりの段階かと思います。いろんなところでお話を伺うとセキュリティ的にもまだまだ地盤が固まってなさそうで、扱う際には注意が必要そうです。
試す際はぜひ注意を払って試しましょう!

◼️ 最後に

現在弊社ではエンジニアを募集しています!

この記事を読んで少しでも興味を持ってくださった方は、ぜひカジュアル面談でお話ししましょう!

iimon採用サイト / Wantedly / Green

最後まで読んでいただきありがとうございました!