Model Context Protocol(MCP)で天気情報ツールを自作する - クイックスタートチュートリアル実践
こんにちは、岡本秀高です。今回はModel Context Protocol(MCP)の公式クイックスタートチュートリアルを読みながら実装してみた経験について共有します。以前は公式が過去に提供していたセットアップコマンドを使った方法を紹介しました。今回はMCPの公式チュートリアルをほぼそのままなぞる形で実装していきます。最後に少しだけコードを読んでみた部分についても紹介しています。
MCPとは?
Model Context Protocol(MCP)は、AIモデルと外部ツールを連携させるためのプロトコルです。Claudeなどの大規模言語モデルに、APIやデータベースなど外部リソースにアクセスする能力を提供します。これにより、AIはリアルタイムの情報取得や特定の処理を行うことができるようになります。
今回作成するツール
今回は、米国気象局(National Weather Service)のAPIを利用して、以下の機能を持つ天気情報ツールを作成します:
- get-alerts: 米国の州ごとの気象警報情報を取得する
- get-forecast: 指定した緯度・経度の天気予報を取得する
必要なもの
- Node.js環境
- TypeScriptの基本的な知識
- MCP SDK(
@modelcontextprotocol/sdk
)
手順
1. プロジェクトのセットアップ
まずはプロジェクトディレクトリを作成し、必要なファイルを初期化します。
# プロジェクトの初期化
git init
npm init -y
2. package.jsonの設定
package.jsonファイルを編集して、TypeScriptプロジェクトとして必要な設定を行います。
{
"type": "module",
"bin": {
"weather": "./build/index.js"
},
"scripts": {
"build": "tsc && chmod 755 build/index.js"
},
"files": [
"build"
],
}
3. TypeScriptの設定
TypeScriptコンパイラの設定ファイル(tsconfig.json)を作成します。
npx tsc --init
生成されたtsconfig.jsonを以下のように編集します:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
4. 依存パッケージのインストール
必要なパッケージをインストールします。
npm install @modelcontextprotocol/sdk zod
5. ソースコードの作成
src/index.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://api.weather.gov";
const USER_AGENT = "weather-app/1.0";
// Helper function for making NWS API requests
async function makeNWSRequest<T>(url: string): Promise<T | null> {
const headers = {
"User-Agent": USER_AGENT,
Accept: "application/geo+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 AlertFeature {
properties: {
event?: string;
areaDesc?: string;
severity?: string;
status?: string;
headline?: string;
};
}
// Format alert data
function formatAlert(feature: AlertFeature): string {
const props = feature.properties;
return [
`Event: ${props.event || "Unknown"}`,
`Area: ${props.areaDesc || "Unknown"}`,
`Severity: ${props.severity || "Unknown"}`,
`Status: ${props.status || "Unknown"}`,
`Headline: ${props.headline || "No headline"}`,
"---",
].join("\n");
}
interface ForecastPeriod {
name?: string;
temperature?: number;
temperatureUnit?: string;
windSpeed?: string;
windDirection?: string;
shortForecast?: string;
}
interface AlertsResponse {
features: AlertFeature[];
}
interface PointsResponse {
properties: {
forecast?: string;
};
}
interface ForecastResponse {
properties: {
periods: ForecastPeriod[];
};
}
// Create server instance
const server = new McpServer({
name: "weather",
version: "1.0.0",
capabilities: {
resources: {},
tools: {},
},
});
// Register weather tools
server.tool(
"get-alerts",
"Get weather alerts for a state",
{
state: z.string().length(2).describe("Two-letter state code (e.g. CA, NY)"),
},
async ({ state }) => {
const stateCode = state.toUpperCase();
const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`;
const alertsData = await makeNWSRequest<AlertsResponse>(alertsUrl);
if (!alertsData) {
return {
content: [
{
type: "text",
text: "Failed to retrieve alerts data",
},
],
};
}
const features = alertsData.features || [];
if (features.length === 0) {
return {
content: [
{
type: "text",
text: `No active alerts for ${stateCode}`,
},
],
};
}
const formattedAlerts = features.map(formatAlert);
const alertsText = `Active alerts for ${stateCode}:\n\n${formattedAlerts.join("\n")}`;
return {
content: [
{
type: "text",
text: alertsText,
},
],
};
},
);
server.tool(
"get-forecast",
"Get weather forecast for a location",
{
latitude: z.number().min(-90).max(90).describe("Latitude of the location"),
longitude: z.number().min(-180).max(180).describe("Longitude of the location"),
},
async ({ latitude, longitude }) => {
// Get grid point data
const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`;
const pointsData = await makeNWSRequest<PointsResponse>(pointsUrl);
if (!pointsData) {
return {
content: [
{
type: "text",
text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).`,
},
],
};
}
const forecastUrl = pointsData.properties?.forecast;
if (!forecastUrl) {
return {
content: [
{
type: "text",
text: "Failed to get forecast URL from grid point data",
},
],
};
}
// Get forecast data
const forecastData = await makeNWSRequest<ForecastResponse>(forecastUrl);
if (!forecastData) {
return {
content: [
{
type: "text",
text: "Failed to retrieve forecast data",
},
],
};
}
const periods = forecastData.properties?.periods || [];
if (periods.length === 0) {
return {
content: [
{
type: "text",
text: "No forecast periods available",
},
],
};
}
// Format forecast periods
const formattedForecast = periods.map((period: ForecastPeriod) =>
[
`${period.name || "Unknown"}:`,
`Temperature: ${period.temperature || "Unknown"}°${period.temperatureUnit || "F"}`,
`Wind: ${period.windSpeed || "Unknown"} ${period.windDirection || ""}`,
`${period.shortForecast || "No forecast available"}`,
"---",
].join("\n"),
);
const forecastText = `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join("\n")}`;
return {
content: [
{
type: "text",
text: forecastText,
},
],
};
},
);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Weather MCP Server running on stdio");
}
main().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});
6. アプリケーションのビルド
ソースコードを作成したら、TypeScriptコードをJavaScriptにビルドします。
npm run build
7. MCPツールの登録
Claude Desktopの設定ファイルを編集して、MCPツールを登録します。macOSの場合、設定ファイルは通常以下の場所にあります:
~/Library/Application\ Support/Claude/claude_desktop_config.json
このファイルに以下の設定を追加します(絶対パスを正確に指定することが重要です):
"weather": {
"command": "node",
"args": [ "/ABSOLUTE/PATH/TO/YOUR/PROJECT/build/index.js" ]
}
例えば、プロジェクトが /Users/username/mcp-tutorial
にある場合は次のように書きます。
"weather": {
"command": "node",
"args": [ "/Users/username/mcp-tutorial/build/index.js" ]
}
動作確認
Claude Desktopを再起動すると、MCPツールが使用可能になります。以下のような質問をして動作を確認できます:
- 「What's the weather in Sacramento?」(Sacramentoの天気は?)
- 「Are there any weather alerts in CA?」(カリフォルニア州に気象警報はありますか?)
コードの解説
server.tool()メソッド
MCPサーバーでは、server.tool()
メソッドを使用してツールを登録します。パラメータは以下の通りです:
- ツール名(例:
get-forecast
) - ツールの説明
- パラメータの定義(Zodスキーマを使用)
- ツールの実装関数
server.tool(
"get-forecast",
"Get weather forecast for a location",
{
latitude: z.number().min(-90).max(90).describe("Latitude of the location"),
longitude: z.number().min(-180).max(180).describe("Longitude of the location"),
},
async ({ latitude, longitude }) => {
// 実装...
}
);
APIリクエストの処理
National Weather Service APIへのリクエストは、makeNWSRequest
関数を使って処理しています。これにより、エラーハンドリングや共通ヘッダーの設定を一貫して行うことができます。
レスポンスのフォーマット
MCPツールのレスポンスは、以下の形式で返します:
return {
content: [
{
type: "text",
text: forecastText,
},
],
};
複数のcontent
要素を含めることができるため、テキスト以外にも画像などを返すことも可能です。
まとめ
MCPを活用することで、AIに外部データへのアクセス能力を提供し、より実用的な対話を実現することができます。チュートリアルのコードをベースに、様々なAPIと連携したオリジナルのMCPツールを作成することも可能です。例えば、Backlogの課題検索やStripeを使ったデータの分析など、アイデア次第で拡張できそうです。
今回はサンプルコードをそのまま利用しましたが、今後はより独自の機能を追加したMCPツールの開発にも挑戦してみたいと思います。ご質問やフィードバックがあれば、お気軽にコメントください。
参考リンク
- Model Context Protocol 公式ドキュメント
- MCP クイックスタートチュートリアル(今回はこのチュートリアルをなぞりました)
- MCP TypeScript SDK
- National Weather Service API
Discussion