Windows 環境で Model Context Protocol (MCP) サーバーを立ち上げるチュートリアルをやった
最近、Windsurfを使用したAIコーディングを触り始めていく中でMCPを使うようになり、キーワードとしてもよく登場するようになったのでMCPを自分自身で立ち上げるチュートリアルを実際にやってみました。
やることはMCP公式のQuickStartをやるだけですが、Windows環境では一部変更しないといけない部分があったので、自分と同じくらいの超初心者向けにチュートリアルを完走する備忘録として記事を残します。
Claude Desktopは利用しておらず、Windsurfを利用しているのでMCPを実行してみる環境はWindsurfとなります。(どちらで起動するからどうという違いはないはずです)
- チュートリアルは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の更新
{
+ "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の作成
長いので折り畳み
{
"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の更新
長いので折り畳み
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を追加します。
{
"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
を使用して発令中の気象警報も取得してくれるはずですが、割愛します。
超ざっくりな今回の動作の仕組み
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を保存しているリポジトリ
Discussion