🐥

Windows 環境で Model Context Protocol (MCP) サーバーを立ち上げるチュートリアルをやった

に公開

最近、Windsurfを使用したAIコーディングを触り始めていく中でMCPを使うようになり、キーワードとしてもよく登場するようになったのでMCPを自分自身で立ち上げるチュートリアルを実際にやってみました。

やることはMCP公式のQuickStartをやるだけですが、Windows環境では一部変更しないといけない部分があったので、自分と同じくらいの超初心者向けにチュートリアルを完走する備忘録として記事を残します。

Claude Desktopは利用しておらず、Windsurfを利用しているのでMCPを実行してみる環境はWindsurfとなります。(どちらで起動するからどうという違いはないはずです)

https://modelcontextprotocol.io/quickstart/server

https://windsurf.com/editor


  • チュートリアルはNode向けを実施
  • Windows 11
  • WindsurfからMCPを呼び出す

チュートリアル開始

前提環境の確認

node --version
npm --version

nodeとnpmのバージョンを確認します。
Node.jsバージョン16以上が必要となります。

勉強用リポジトリの作成

echo "# study-mcp-server" >> README.md
git init
git add README.md
git commit -m "first commit"
git branch -M main
git remote add origin https://github.com/big-mon/study-mcp-server.git
git push -u origin main

チュートリアルの記録を残しておこうと、まずはリポジトリをGitHub上に作成してローカルのフォルダとリモートリポジトリを紐づけておきます。

ローカルのプロジェクトフォルダは/study-mcp-serverです。

プロジェクトフォルダの作成

# Initialize a new npm project
npm init -y

# Install dependencies
npm install @modelcontextprotocol/sdk zod
npm install -D @types/node typescript

# Create our files
md src
new-item src\index.ts

チュートリアルではweatherというフォルダの作成と移動を行っていますが、専用のプロジェクトフォルダで既に作業しているので飛ばします。

package.jsonの更新

package.json
{
+  "type": "module",
+  "bin": {
+    "weather": "./build/index.js"
+  },
  "scripts": {
-    "build": "tsc && chmod 755 build/index.js"
+    "build": "tsc"
  },
+  "files": [
+    "build"
+  ],
}

type, bin, scripts.build, filesをそれぞれpackage.jsonに追加します。

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"]
}

チュートリアルの通り、新規作成します。

src/index.tsの更新

長いので折り畳み
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",
  capabilities: {
    resources: {},
    tools: {},
  },
});

// 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.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);
});

チュートリアルの通り、更新します。

プロジェクトをビルド

npm run build

.tsファイルがビルドされ、/build/index.jsが作成されていることを確認します。

WindsurfでMCPの設定

WindsurfのCascadeにてハンマーマークを押下するとMCP関連のメニューが表示されるので、Configureを押下してmcp_config.jsonを開き、今回作成したMCPを追加します。

mcp_config.json
{
    "mcpServers": {
+        "weather": {
+            "command": "node",
+            "args": [
+                "C:\\PATH\\TO\\PARENT\\FOLDER\\study-mcp-server\\build\\index.js"
+            ]
+        }
    }
}

ビルドして生成された/build/index.jsへの絶対パスを指定します。

mcp_config.jsonの更新が終わったらメニューからRefreshを選択すると再読み込みが行われ、ここまでの手順が問題なければweatherが緑色に点灯して、Windsurf上で正しく認識されます。

天気を訪ねてみる

「サクラメントの天気はどうですか?」とCascadeに尋ねてみます。

現在のサクラメントの天気と今後の予報をお伝えします:

現在の天気:

気温: 48°F (約9°C)
風速: 2 mph SE (東南の風)
天候: ほぼ晴れ
明日(日曜日)の予報:

日中: 晴れ、最高気温76°F (約24°C)
夜間: 曇り後小雨の可能性、最低気温55°F (約13°C)
今週の傾向:

火曜日から金曜日にかけて気温は70-80°F (21-27°C)で安定
全体的に晴れまたは曇りが多く、降水確率は低め
風は2-9 mphと穏やか
暖かい日中と涼しい夜が続く予報です。朝晩は上着があると良いでしょう。

他に何かお聞きになりたいことがあればお知らせください。

という具合にちゃんとMCPを使用すると天気の情報を取得できることをAIエージェントが理解し、現在の天気を取得してくれるようになりました。server.toolとして定義されたget-forecastを使用して天気を取得しているようです。

同じくget-alertsを使用して発令中の気象警報も取得してくれるはずですが、割愛します。


超ざっくりな今回の動作の仕組み

index.ts
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-forecastが呼び出されたわけですが、実装部分を見てみます。

server.toolにて関数名のような扱いでget-forecastが定義されており、次に説明文、そして引数が定義されています。第4引数はget-forecastが実行する実際の処理が記述されています。


パッと見の印象ではMCPは従来のAPIを単にラップしただけの存在にも思えますが、適切な前準備さえ整えておけばAPIと違ってリクエストパラメーターやレスポンスボディに対する処理を用意せずとも、どちらもAIエージェントがよしなに解釈して設定してくれるので楽ですね。

自分自身が良く使用するサービスや操作をserver.toolとしてまとめておくことで、自分自身のテンプレートがAIエージェントにより自動化していくような光景の一端を垣間見ました。

同時に、実装部分は案外普通のTypeScript処理が記載されているだけなので、悪意ある処理を仕込むことも簡単そうだなぁとも思ったので、信頼できるMCP以外を検査せずに導入することは危ういなとも感じました。

まだまだGitHubのMCPを使用してIssueやプルリクの作成をするくらいしか活用できていないので、キャッチアップを続けたいと思います。

以上でMCPの体験記を終えます。


作成したMCPを保存しているリポジトリ

https://github.com/big-mon/study-mcp-server

Discussion