はじめに
MCPの利用はしていますが、MCP Serverはまだ自作できていません。
今回は勉強のため、公式のMCPサーバー構築チュートリアルを実際にやってみたいと思います。
基本写経しているだけですが、同じ環境で躓いた、という人は参考にしてみてください。
前提条件
- OS: macOS
- 言語:TypeScript
何を作るか?
- get_alerts: MCPサーバーに保存されているアラートを取得するエンドポイント
- get_forecast: MCPサーバーに保存されている予測を取得するエンドポイント
前提知識
MCPサーバーのロギング
- STDIO-based servers
- stdoutは使ってはいけない
- print(), console.log(), fmt.Println()など
- stdoutは使ってはいけない
- HTTP-based servers
- stdoutが使える
ベストプラクティス
- stderrやログファイルへの出力はLoggingライブラリを使う
- JSではconsole.log()がstdoutに出力されるため注意
環境準備
nodejsがインストールされていることを確認
% node --version
npm --version
v25.2.1
11.6.2
# 執筆時点の最新版を入れていますが、16以上であれば問題ないようです
プロジェクトのセットアップ
# Create a new directory for our project
mkdir weather
cd weather
# Initialize a new npm project
npm init -y
# Install dependencies
npm install @modelcontextprotocol/sdk zod@3
npm install -D @types/node typescript
# Create our files
mkdir src
touch src/index.ts
package.jsonの編集
typeとscriptsを追加します。
(dependencies, devDependenciesは消さないように注意)
{
"type": "module",
"bin": {
"weather": "./build/index.js"
},
"scripts": {
"build": "tsc && chmod 755 build/index.js"
},
"files": ["build"]
}
tsconfig.jsonの作成
プロジェクトルートに tsconfig.json を作成します。
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
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 NWS_API_BASE = "https://api.weather.gov";
const USER_AGENT = "weather-app/1.0";
// Create server instance
const server = new McpServer({
name: "weather",
version: "1.0.0",
});
Helper関数の追加
以下のコードを ./src/index.ts に追加します。
// 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[];
};
}
ツール実行ハンドラの追加
続いて、以下のコードも追加します。
// Register weather tools
server.registerTool(
"get_alerts",
{
description: "Get weather alerts for a state",
inputSchema: {
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.registerTool(
"get_forecast",
{
description: "Get weather forecast for a location",
inputSchema: {
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,
},
],
};
},
);
main関数の追加
最後にサーバーを起動する main関数を追加します。
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);
});
起動してみる
ここまでで、MCPサーバーの実装自体は完了です。
ビルド&起動を試してみましょう。
npm run build
node build/index.js
# 以下が表示されればOK
# Weather MCP Server running on stdio
セットアップ
Claude for Desktop で実際に利用してみます。
本当はClaude Codeで利用したかったのですが、まだProを契約していないため利用できませんでした...
MCP Serverの設定追加
~/Library/Application\ Support/Claude/claude_desktop_config.json を作成します
{
"mcpServers": {
"weather": {
"command": "node",
"args": ["{Absolute path to parent folder}/weather/build/index.js"]
}
}
}
Claudeの再起動
Claude for Desktop を再起動すると「weather」の項目が追加されています。
Get forecast のお試し
シカゴの天気を聞いてみると、MCP Serverを使った結果が返ってきました。
APIが米国の天気しか対応していないため、日本の天気を聞くとWeb検索になります
https://api.weather.gov
Get alerts のお試し
同じく警報情報を聞くと、MCP Serverを使って結果が返ってきます。
終わりに
チュートリアルをなぞっただけではありますが、MCP Serverを実際に作って動かしてみました。
Tool の description をきちんと書いておけば、上手いこと使ってくれますね。
次はオリジナルのMCP Serverを作ってみたいと思います。



