Zenn
Open6

MCPサーバを書いてみる

蓮蒲ルナ蓮蒲ルナ

MCP.tool

Tools - Model Context Protocol
modelcontextprotocol/typescript-sdk: The official Typescript SDK for Model Context Protocol servers and clients
ほとんどサンプルを流用して(ちょっと手抜きな)実装。コード全文。
cityコードはXML形式で提供されているが、手軽さを取ってハードコーディングで埋め込んだ。
全国の地点定義表

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

const NWS_API_BASE = "https://weather.tsukumijima.net";
const USER_AGENT = "weather_jp-app/1.0";

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

server.tool(
  "get-forcasts",
  "Get weather forecasts for a location",
  {
    city: z.string().length(6).describe("6-digits city code (e.g. 400040)"),
  },
  async ({ city }) => {
    const forecastAPIUrl = `${NWS_API_BASE}/api/forecast/city/${city}`;
    const forecastData = await makeNWSRequest<WeatherForecast>(forecastAPIUrl);

    if (!forecastData) {
      return {
        content: [
          {
            type: "text",
            text: "Failed to retrieve forecast data",
          },
        ],
      };
    }

    const features = forecastData.forecasts || [];
    if (features.length === 0) {
      return {
        content: [
          {
            type: "text",
            text: `No active forecast for ${city}`,
          },
        ],
      };
    }

    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(forecastData),
        },
      ],
    };
  }
);

server.tool(
  "get-cities",
  "Get city codes for a location",
  {},
  async () => {
    const cities = [
      [ "011000", "道北/稚内" ],
      [ "012010", "道北/旭川" ],
      [ "012020", "道北/留萌" ],
      [ "013010", "道東/網走" ],
      [ "013020", "道東/北見" ],
      [ "013030", "道東/紋別" ],
      [ "014010", "道東/根室" ],
      [ "014020", "道東/釧路" ],
      [ "014030", "道東/帯広" ],
      [ "015010", "道南/室蘭" ],
      [ "015020", "道南/浦河" ],
      [ "016010", "道央/札幌" ],
      [ "016020", "道央/岩見沢" ],
      [ "016030", "道央/倶知安" ],
      [ "017010", "道南/函館" ],
      [ "017020", "道南/江差" ],
      [ "020010", "青森県/青森" ],
      [ "020020", "青森県/むつ" ],
      [ "020030", "青森県/八戸" ],
      [ "030010", "岩手県/盛岡" ],
      [ "030020", "岩手県/宮古" ],
      [ "030030", "岩手県/大船渡" ],
      [ "040010", "宮城県/仙台" ],
      [ "040020", "宮城県/白石" ],
      [ "050010", "秋田県/秋田" ],
      [ "050020", "秋田県/横手" ],
      [ "060010", "山形県/山形" ],
      [ "060020", "山形県/米沢" ],
      [ "060030", "山形県/酒田" ],
      [ "060040", "山形県/新庄" ],
      [ "070010", "福島県/福島" ],
      [ "070020", "福島県/小名浜" ],
      [ "070030", "福島県/若松" ],
      [ "080010", "茨城県/水戸" ],
      [ "080020", "茨城県/土浦" ],
      [ "090010", "栃木県/宇都宮" ],
      [ "090020", "栃木県/大田原" ],
      [ "100010", "群馬県/前橋" ],
      [ "100020", "群馬県/みなかみ" ],
      [ "110010", "埼玉県/さいたま" ],
      [ "110020", "埼玉県/熊谷" ],
      [ "110030", "埼玉県/秩父" ],
      [ "120010", "千葉県/千葉" ],
      [ "120020", "千葉県/銚子" ],
      [ "120030", "千葉県/館山" ],
      [ "130010", "東京都/東京" ],
      [ "130020", "東京都/大島" ],
      [ "130030", "東京都/八丈島" ],
      [ "130040", "東京都/父島" ],
      [ "140010", "神奈川県/横浜" ],
      [ "140020", "神奈川県/小田原" ],
      [ "150010", "新潟県/新潟" ],
      [ "150020", "新潟県/長岡" ],
      [ "150030", "新潟県/高田" ],
      [ "150040", "新潟県/相川" ],
      [ "160010", "富山県/富山" ],
      [ "160020", "富山県/伏木" ],
      [ "170010", "石川県/金沢" ],
      [ "170020", "石川県/輪島" ],
      [ "180010", "福井県/福井" ],
      [ "180020", "福井県/敦賀" ],
      [ "190010", "山梨県/甲府" ],
      [ "190020", "山梨県/河口湖" ],
      [ "200010", "長野県/長野" ],
      [ "200020", "長野県/松本" ],
      [ "200030", "長野県/飯田" ],
      [ "210010", "岐阜県/岐阜" ],
      [ "210020", "岐阜県/高山" ],
      [ "220010", "静岡県/静岡" ],
      [ "220020", "静岡県/網代" ],
      [ "220030", "静岡県/三島" ],
      [ "220040", "静岡県/浜松" ],
      [ "230010", "愛知県/名古屋" ],
      [ "230020", "愛知県/豊橋" ],
      [ "240010", "三重県/津" ],
      [ "240020", "三重県/尾鷲" ],
      [ "250010", "滋賀県/大津" ],
      [ "250020", "滋賀県/彦根" ],
      [ "260010", "京都府/京都" ],
      [ "260020", "京都府/舞鶴" ],
      [ "270000", "大阪府/大阪" ],
      [ "280010", "兵庫県/神戸" ],
      [ "280020", "兵庫県/豊岡" ],
      [ "290010", "奈良県/奈良" ],
      [ "290020", "奈良県/風屋" ],
      [ "300010", "和歌山県/和歌山" ],
      [ "300020", "和歌山県/潮岬" ],
      [ "310010", "鳥取県/鳥取" ],
      [ "310020", "鳥取県/米子" ],
      [ "320010", "島根県/松江" ],
      [ "320020", "島根県/浜田" ],
      [ "320030", "島根県/西郷" ],
      [ "330010", "岡山県/岡山" ],
      [ "330020", "岡山県/津山" ],
      [ "340010", "広島県/広島" ],
      [ "340020", "広島県/庄原" ],
      [ "350010", "山口県/下関" ],
      [ "350020", "山口県/山口" ],
      [ "350030", "山口県/柳井" ],
      [ "350040", "山口県/萩" ],
      [ "360010", "徳島県/徳島" ],
      [ "360020", "徳島県/日和佐" ],
      [ "370000", "香川県/高松" ],
      [ "380010", "愛媛県/松山" ],
      [ "380020", "愛媛県/新居浜" ],
      [ "380030", "愛媛県/宇和島" ],
      [ "390010", "高知県/高知" ],
      [ "390020", "高知県/室戸岬" ],
      [ "390030", "高知県/清水" ],
      [ "400010", "福岡県/福岡" ],
      [ "400020", "福岡県/八幡" ],
      [ "400030", "福岡県/飯塚" ],
      [ "400040", "福岡県/久留米" ],
      [ "410010", "佐賀県/佐賀" ],
      [ "410020", "佐賀県/伊万里" ],
      [ "420010", "長崎県/長崎" ],
      [ "420020", "長崎県/佐世保" ],
      [ "420030", "長崎県/厳原" ],
      [ "420040", "長崎県/福江" ],
      [ "430010", "熊本県/熊本" ],
      [ "430020", "熊本県/阿蘇乙姫" ],
      [ "430030", "熊本県/牛深" ],
      [ "430040", "熊本県/人吉" ],
      [ "440010", "大分県/大分" ],
      [ "440020", "大分県/中津" ],
      [ "440030", "大分県/日田" ],
      [ "440040", "大分県/佐伯" ],
      [ "450010", "宮崎県/宮崎" ],
      [ "450020", "宮崎県/延岡" ],
      [ "450030", "宮崎県/都城" ],
      [ "450040", "宮崎県/高千穂" ],
      [ "460010", "鹿児島県/鹿児島" ],
      [ "460020", "鹿児島県/鹿屋" ],
      [ "460030", "鹿児島県/種子島" ],
      [ "460040", "鹿児島県/名瀬" ],
      [ "471010", "沖縄県/那覇" ],
      [ "471020", "沖縄県/名護" ],
      [ "471030", "沖縄県/久米島" ],
      [ "472000", "沖縄県/南大東" ],
      [ "473000", "沖縄県/宮古島" ],
      [ "474010", "沖縄県/石垣島" ],
      [ "474020", "沖縄県/与那国島" ]
    ];
    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(cities),
        },
      ],
    };
  }
);

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

main().catch((error) => {
  console.error("Fatal error in main():", error);
  process.exit(1);
});

// Helper function for making NWS API requests
async function makeNWSRequest<T>(url: string): Promise<T | null> {
  const headers = {
    "User-Agent": USER_AGENT,
    Accept: "application/json",
  };

  try {
    const response = await fetch(url, { headers });
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return (await response.json()) as T;
  } catch (error) {
    console.error("Error making NWS request:", error);
    return null;
  }
}

interface Image {
  title: string;
  url: string;
  width: number;
  height: number;
  link?: string;
}

interface Temperature {
  min: {
    celsius: string | null;
    fahrenheit: string | null;
  };
  max: {
    celsius: string | null;
    fahrenheit: string | null;
  };
}

interface ChanceOfRain {
  T00_06: string; // 0時-6時
  T06_12: string; // 6時-12時
  T12_18: string; // 12時-18時
  T18_24: string; // 18時-24時
}

interface ForecastDetail {
  weather: string | null;
  wind: string | null;
  wave: string | null;
}

interface Forecast {
  date: string;
  dateLabel: string;
  telop: string;
  detail: ForecastDetail;
  temperature: Temperature;
  chanceOfRain: ChanceOfRain;
  image: Image;
}

interface Description {
  publicTime: string;
  publicTimeFormatted: string;
  headlineText: string;
  bodyText: string;
  text: string;
}

interface Location {
  area: string;
  prefecture: string;
  district: string;
  city: string;
}

interface Provider {
  link: string;
  name: string;
  note: string;
}

interface Copyright {
  title: string;
  link: string;
  image: Image;
  provider: Provider[];
}

interface WeatherForecast {
  publicTime: string;
  publicTimeFormatted: string;
  publishingOffice: string;
  title: string;
  link: string;
  description: Description;
  forecasts: Forecast[];
  location: Location;
  copyright: Copyright;
}
package.json
{
  "name": "weather_jp",
  "type": "module",
  "scripts": {
    "start": "tsx src/index.ts"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.7.0",
    "zod": "^3.24.2"
  },
  "devDependencies": {
    "@types/node": "^22.13.10",
    "typescript": "^5.8.2"
  }
}

てきとーにフォルダ作って、ファイル置いて、npm installまでする。

蓮蒲ルナ蓮蒲ルナ

MCPクライアントとして、Claude Desktopを使う。
Claude DesktopのMCP設定に追記。

/path/to/your/claude_desktop_config.json
{
  "mcpServers": {
    "weather_jp": {
      "command": "npx",
      "args": [
        "-y",
        "tsx",
        "/Users/yukku/lab/mcp/weather_jp/src/index.ts"
      ]
    }
  }
}


アプリを立ち上げて、設定が正しければこのように出てくる。

「東京の天気予報」というプロンプトを流すと、LLMが追加されたMCPサーバを使うべきかを判断し、
toolを使ってもいいか尋ねてくる。

許可すると、「東京」のコードを探し、コードを元に情報をAPIに問い合わせはじめる。

get-citiesget-forecastsにアクセスして、レスポンスを利用した回答が行われました。

蓮蒲ルナ蓮蒲ルナ

MCP.resource

Resources - Model Context Protocol
modelcontextprotocol/typescript-sdk: The official Typescript SDK for Model Context Protocol servers and clients
リソースとしてエリア情報を扱ってみる。
以下を追記。また、get-citiesはコメントアウトして動作しないようにしておく。

server.resource(
  "PrimaryArea",
  "https://weather.tsukumijima.net/primary_area.xml",
  async (uri) => {
    const areaDataUrl = `${NWS_API_BASE}/primary_area.xml`;
    const areaResponse = await fetch(areaDataUrl, {
      headers: {
        'User-Agent': USER_AGENT
      }
    });
    if (!areaResponse.ok) {
      throw new Error(`Failed to fetch area data: ${areaResponse.statusText}`);
    }
    return {
      contents: [{
        uri: uri.href,
        text: await areaResponse.text(),
        mimeType: "text/xml"
      }]
    }
  }
)

Claude Desktopを再起動して、読み込ませる。リソースの選択UIが出現する。

これを選択して、プロンプトを入力。

今度はget-citiesが呼ばれなくても東京の(resource由来で)コードを引けた。

リソースを設定しなかった場合。

福岡県久留米のコード(400040)が使われたのは、おそらくコード内のコメントを読み取って使ったからみたい。

UIから手動で選択する方法について、Claude Desktopが汎用的なMCPクライアントとしての役割とすると、明示的にリソースを渡せるという意味合いでこうなっていると解釈した。

特定用途のMCPクライアントであれば、ユーザの操作や確認無しに添付する挙動をするものがあるのかも。

蓮蒲ルナ蓮蒲ルナ

MCP.prompt

Prompts - Model Context Protocol
modelcontextprotocol/typescript-sdk: The official Typescript SDK for Model Context Protocol servers and clients

server.prompt(
  "ForecastByArea",
  { area: z.string() },
  ({ area }) => ({
    messages: [{
      role: "user",
      content: {
        type: "text",
        text: `${area}の天気予報を伝えて:\n\n`
      }
    }]
  })
);

roleassistantroleを使い分けて、Few-Shotプロンプティングできそう。
Claude Desktopで使う分には単なるテンプレのプリセットという感じ。
特定用途のMPCクライアントやLLMへ特定形式のプロンプティングが必要な場合には活躍しそう。

エリア情報のリソースも一緒に添付して「東京」をテンプレ引数として渡して、実行。

ログインするとコメントできます