MCPサーバを書いてみる
Model Context Protocol
参考情報。
Model Context Protocol
Introduction - Model Context Protocol
MCPサーバーの機能を全部(Prompt, Resource, Tool)試してみる #LLM - Qiita
TypeScript で MCP サーバーを実装し、Claude Desktop から利用する
サーバ実装例
For Server Developers - Model Context Protocol
そのままでは面白くないので、せっかくだから日本の天気予報APIを使ったものを作ってみる。
天気予報 API(livedoor 天気互換)
MCP.tool
Tools - Model Context Protocol
modelcontextprotocol/typescript-sdk: The official Typescript SDK for Model Context Protocol servers and clients
ほとんどサンプルを流用して(ちょっと手抜きな)実装。コード全文。
cityコードはXML形式で提供されているが、手軽さを取ってハードコーディングで埋め込んだ。
全国の地点定義表
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;
}
{
"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設定に追記。
{
"mcpServers": {
"weather_jp": {
"command": "npx",
"args": [
"-y",
"tsx",
"/Users/yukku/lab/mcp/weather_jp/src/index.ts"
]
}
}
}
アプリを立ち上げて、設定が正しければこのように出てくる。
「東京の天気予報」というプロンプトを流すと、LLMが追加されたMCPサーバを使うべきかを判断し、
tool
を使ってもいいか尋ねてくる。
許可すると、「東京」のコードを探し、コードを元に情報をAPIに問い合わせはじめる。
get-cities
とget-forecasts
にアクセスして、レスポンスを利用した回答が行われました。
デバッグ・動作確認
Inspector - Model Context Protocol
modelcontextprotocol/inspector: Visual testing tool for MCP servers
インスペクタを使うとサーバの呼び出しをテストできる。
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`
}
}]
})
);
role
でassistant
やrole
を使い分けて、Few-Shotプロンプティングできそう。
Claude Desktopで使う分には単なるテンプレのプリセットという感じ。
特定用途のMPCクライアントやLLMへ特定形式のプロンプティングが必要な場合には活躍しそう。
エリア情報のリソースも一緒に添付して「東京」をテンプレ引数として渡して、実行。