言語を切り替える
テーマを切り替える

MCP Server 開発入門:ゼロから作る初めての MCP サービス

はじめに: Cursor でコードを書いているとき、「AI にプロジェクト依存の最新バージョンをそのまま調べてほしい」と思ったことはありませんか?Claude でデータを分析するとき、データベースの中身まで読み取ってほしい、という場面もあるでしょう。それぞれ個別に実装するなら、AI ツールごとに専用のアダプターコードが必要になります。MCP Server を用意すれば、一度書くだけで MCP 対応クライアントすべてから使えます。本記事では、TypeScript でゼロから完全な MCP Server を手書きしていきます。

30 分
習得時間
ゼロから動作まで
3 種類
コア機能
Tools/Resources/Prompts
1000+
MCP サーバー
GitHub オープンソースコミュニティ
Source: MCP 公式データ(2025 年)

MCP とは?3 分で理解するコアコンセプト

USB 端子の物語

数年前のデジタル機器を使っていた人なら、あのややこしい時期を覚えているでしょう。マウスは丸い端子、キーボードは四角い端子、プリンターはパラレルポート。機器ごとに対応する差し込み口を探す必要がありました。その後 USB が登場し、1 つの端子ですべて解決しました。

MCP(Model Context Protocol)は、AI ツールの世界における「USB 標準」になりつつあります。

MCP がない時代は、AI にデータソースへアクセスさせるたびに、ツールごとのアダプター層を別々に書く必要がありました。Claude 用のプラグイン、Cursor 用の拡張、Windsurf 用の連携……。複雑さは N × M(データソース N 個 × AI ツール M 個)です。

MCP があれば、MCP Server を 1 つ書くだけで、MCP に対応したすべてのクライアントが直接呼び出せます。複雑さは N+M まで下がります。

3 層アーキテクチャはとてもシンプルです。

+-------------+     +-------------+     +-------------+
|    Host     | ->  |   Client    | ->  |   Server    |
|  (Claude)   |     | (MCPクライアント)|  | (あなたのサービス)|
+-------------+     +-------------+     +-------------+
  • Host:AI アプリ本体。たとえば Claude Desktop、Cursor。
  • Client:MCP クライアント。Host との通信を担う。
  • Server:あなたが書くサービス。具体的な機能を提供する。

MCP Server が提供する 3 種類の能力

MCP Server は、3 種類の異なる機能を提供できます。

能力用途
Tools(ツール)操作を実行する天気を照会、メッセージを送信、データベースを読み取る
Resources(リソース)データを提供するファイル内容、API レスポンス、設定情報
Prompts(プロンプト)事前定義テンプレートコードレビュー用テンプレート、日報生成用テンプレート

Tools は「関数」だと考えてください。AI が呼び出して何らかのアクションを実行できます。Resources は「データソース」で、AI がそこから内容を読み取れます。Prompts は「テンプレート」で、AI がタスクをより早く理解する助けになります。

既存記事との違い:他の MCP チュートリアルを見たことがあれば、Python と FastMCP で実装したバージョンを目にしたかもしれません。本記事は TypeScript ネイティブ SDK を使い、フロントエンドやフルスタックの開発者に向いています。どちらの実装も機能は同等なので、慣れている言語を選べば問題ありません。

"https://modelcontextprotocol.io"


開発環境の準備

前提条件

この記事では、次のことを前提とします。

  • Node.js 18+ または Bun 1.0+ がインストール済み
  • TypeScript を書いたことがあり、interfaceasync/await が何かを知っている
  • Claude Desktop または MCP 対応クライアント(Cursor、Windsurf など)がある

Bun を使ったことがなければ、ぜひ一度試してみてください。npm よりずっと高速で、TypeScript サポートも内蔵しているため、ts-node の追加設定は不要です。

プロジェクトの初期化

# プロジェクトディレクトリを作成
mkdir mcp-weather-server && cd mcp-weather-server

# 初期化(Bun または npm を使用)
bun init -y
# または npm init -y

# MCP TypeScript SDK をインストール
bun add @modelcontextprotocol/sdk zod
# または npm install @modelcontextprotocol/sdk zod

ここでは 2 つの依存を使います。

  • @modelcontextprotocol/sdk:MCP 公式の TypeScript SDK
  • zod:TypeScript のランタイム型検証。ツール引数のスキーマ定義に使う

TypeScript 設定のポイント

bun init を使えば、tsconfig.json はすでに設定済みです。手動で設定する場合は、次のオプションに注意してください。

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "esModuleInterop": true,
    "strict": true
  }
}

moduleResolution: "bundler" は ESM モジュールにとって重要です。これがないと「xxx is not defined」というエラーに遭遇することがあります。


実践:天気照会 MCP Server を手書きする

このチュートリアルでは、次のことができる完全な MCP Server を作ります。

  1. AI からの呼び出しリクエストを受け取る
  2. OpenWeatherMap API を照会してリアルタイムの天気を取得する
  3. 整形した結果を返す

プロジェクト構成の設計

mcp-weather-server/
+-- src/
|   +-- index.ts      # エントリーファイル
|   +-- weather.ts    # 天気ツールの実装
|   +-- resources.ts  # リソース定義
+-- package.json
+-- tsconfig.json

実際のコードはすべて index.ts にまとめることもできます(この記事ではそうします)。ただし、モジュールに分割するほうが保守しやすくなります。

ステップ 1:MCP Server の骨格を作る

最もシンプルなところから始めましょう。起動できる MCP Server を作ります。

// src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

// サーバーインスタンスを作成
const server = new McpServer({
  name: "weather-service",
  version: "1.0.0",
});

// ツール(Tools)を登録
server.tool(
  "get_weather",
  "指定した都市の現在の天気情報を取得する",
  {
    city: z.string().describe("都市名。例:北京、上海"),
  },
  async ({ city }) => {
    // ツールの実装は次の節で展開する
    return { content: [{ type: "text", text: `${city} の天気を照会中...` }] };
  }
);

// サーバーを起動
const transport = new StdioServerTransport();
await server.connect(transport);

McpServer は SDK が提供するコアクラスで、nameversion を渡す必要があります。tool() メソッドはツールを 1 つ登録するためのもので、第 1 引数がツール名、第 2 引数が説明、第 3 引数が引数スキーマ、最後が実行関数です。

ステップ 2:天気照会ツールを実装する(コアコード)

それでは、ツールを実際に動かしましょう。OpenWeatherMap の無料 API を使います。

// src/weather.ts
import { z } from "zod";

// OpenWeatherMap API のレスポンス型を定義
interface WeatherResponse {
  name: string;
  main: { temp: number; feels_like: number; humidity: number };
  weather: [{ description: string }];
  wind: { speed: number };
}

// 天気照会ツールの実装
server.tool(
  "get_weather",
  "指定した都市の現在の天気情報を取得する",
  {
    city: z.string().describe("都市名。例:北京、上海"),
  },
  async ({ city }) => {
    const API_KEY = process.env.OPENWEATHER_API_KEY;
    const url = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${API_KEY}&units=metric&lang=ja`;

    try {
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error(`API リクエスト失敗:${response.status}`);
      }

      const data: WeatherResponse = await response.json();

      // 整形した結果を返す
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              city: data.name,
              temperature: `${data.main.temp}°C`,
              feels_like: `${data.main.feels_like}°C`,
              description: data.weather[0].description,
              humidity: `${data.main.humidity}%`,
              wind_speed: `${data.wind.speed} m/s`,
            }, null, 2),
          },
        ],
      };
    } catch (error) {
      return {
        content: [
          {
            type: "text",
            text: `照会失敗:${error instanceof Error ? error.message : '不明なエラー'}`,
          },
        ],
        isError: true,
      };
    }
  }
);

注意点です。

  1. API Key は環境変数から読み取る:鍵をコードに直接書き込んではいけません
  2. エラー処理isError: true を返して、呼び出しが失敗したことをクライアントに伝える
  3. 型定義WeatherResponse インターフェースにより、TypeScript がデータ構造をチェックしてくれる

OpenWeatherMap で無料アカウントを登録し、API Key を取得したら環境変数を設定します。

export OPENWEATHER_API_KEY=your_api_key_here

ステップ 3:Resources を追加する(任意だが推奨)

Resources は、Server が「読み取り専用」のデータを提供できるようにします。たとえば、サーバーの状態を AI が確認できるリソースを用意できます。

// src/resources.ts

// サーバーの状態情報を提供
server.resource(
  "server-status",
  "status://server",
  async (uri) => ({
    contents: [
      {
        uri: uri.href,
        text: JSON.stringify({
          name: "Weather Service",
          version: "1.0.0",
          status: "running",
          timestamp: new Date().toISOString(),
        }, null, 2),
      },
    ],
  })
);

// API ドキュメントを提供
server.resource(
  "api-docs",
  "docs://api",
  async (uri) => ({
    contents: [
      {
        uri: uri.href,
        text: `
# Weather MCP Server API

## Tools
- get_weather(city: string): 指定した都市の天気を取得

## Resources
- status://server - サーバーの状態
- docs://api - API ドキュメント
        `.trim(),
      },
    ],
  })
);

resource() の最初の 2 つの引数はリソース名と URI で、第 3 引数が読み取り関数です。URI には任意のスキームが使えます。status://docs:// のように、区別さえできれば問題ありません。

ステップ 4:Prompts を追加する(応用機能)

Prompts は事前定義された対話テンプレートです。たとえば「天気レポート」テンプレートを定義しておけば、AI が使うときに都市名を自動で埋め込んでくれます。

// 事前定義された天気レポートテンプレート
server.prompt(
  "weather_report",
  "整形された天気レポートを生成する",
  {
    city: z.string().describe("都市名"),
    include_tips: z.boolean().optional().describe("服装のアドバイスを含めるか"),
  },
  ({ city, include_tips }) => ({
    messages: [
      {
        role: "user",
        content: {
          type: "text",
          text: `${city} の天気レポートを生成してください。${include_tips ? "あわせて服装のアドバイスも提供してください。" : ""}`,
        },
      },
    ],
  })
);

prompt() の戻り値はメッセージの配列で、各メッセージには rolecontent があります。これにより、AI が使うときに事前設定したコンテキストをそのまま得られます。

ステップ 5:エントリーファイルを仕上げる

ここまでのコードをすべて 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 server = new McpServer({
  name: "weather-service",
  version: "1.0.0",
});

// すべてのツール・リソース・プロンプトを登録
// ...(上記のコード)

// エラー処理
process.stdin.on("error", (err) => {
  console.error("標準入力エラー:", err);
  process.exit(1);
});

process.stdout.on("error", (err) => {
  console.error("標準出力エラー:", err);
  process.exit(1);
});

// 安全な終了
process.on("SIGINT", async () => {
  await server.close();
  process.exit(0);
});

// サーバーを起動
const transport = new StdioServerTransport();
await server.connect(transport);

console.error("MCP Weather Server を起動しました。接続を待機中...");

StdioServerTransport は標準入出力で通信するため、stdin/stdout のエラー処理がとても重要です。SIGINT 処理を入れておけば、Ctrl+C でサービスを安全に停止できます。

bun run src/index.ts で起動し、「起動しました」という表示が出れば、すべて正常に動いている証拠です。


クライアントの設定:Claude にあなたの Server を使わせる

Server ができたので、次は Claude Desktop や Cursor から呼び出せるようにします。

Claude Desktop の設定

設定ファイルを探します。

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json

あなたの Server 設定を追加します。

{
  "mcpServers": {
    "weather": {
      "command": "bun",
      "args": ["run", "/absolute/path/to/mcp-weather-server/src/index.ts"],
      "env": {
        "OPENWEATHER_API_KEY": "あなたの API Key"
      }
    }
  }
}

注意args のパスは必ず絶対パスにしてください。相対パスだと Server の起動に失敗します。

Cursor / Windsurf の設定

Cursor と Windsurf の設定方法も同様です。IDE の設定で MCP 設定を見つけ、サーバーのエントリーを追加します(形式は上記と同じ)。

Cursor の設定ファイルは通常、次の場所にあります。

  • macOS: ~/Library/Application Support/Cursor/User/globalStorage/state.vscdb
  • または IDE 内:設定 -> AI -> MCP -> サーバーを追加

Server をテストする

  1. Claude Desktop / Cursor を再起動する
  2. 対話欄に「北京の天気を調べて」と入力する
  3. Claude が自動的にあなたの MCP Server を呼び出すはずです

次のような出力が見えれば成功です。

{
  "city": "北京",
  "temperature": "18°C",
  "feels_like": "16°C",
  "description": "曇り",
  "humidity": "65%",
  "wind_speed": "3.2 m/s"
}

よくある問題のトラブルシューティング

問題考えられる原因解決策
Server に接続できないパスの誤りargs の絶対パスを確認する
API Key が無効環境変数が渡っていないenv の設定が正しいか確認する
応答がないTypeScript のコンパイルエラーまず bun build または tsc でコンパイルする
権限エラー設定ファイルの権限設定ファイルが読み取り可能か確認する

"https://github.com/modelcontextprotocol/typescript-sdk"


拡張とデプロイのアドバイス

ツールをさらに追加する

天気照会はほんの始まりにすぎません。次のようなこともできます。

  • 過去の天気照会:履歴データ API を呼び出し、過去の特定日の天気を返す
  • 複数都市の比較:一度に複数の都市を照会し、比較表を返す
  • 悪天候アラートの購読:悪天候のアラートがあるかどうかを確認する

これらのツールの登録方法は get_weather とまったく同じで、実装ロジックが違うだけです。

デプロイ方法の比較

Server をチームと共有して使いたい場合、ローカルの stdio 通信では足りません。以下はいくつかのデプロイ方法です。

デプロイ方法適したシーン利点欠点
ローカル stdio個人利用、開発テストシンプル、安全共有できない
HTTP/SSEチーム共有、マルチユーザーリモートアクセス可能認証の対応が必要
Serverless本番環境自動スケーリングコールドスタートの遅延

本番環境での注意点

認証:HTTP デプロイ時は必ず認証を実装してください。MCP は OAuth 2.1 に対応していますが、シンプルな API Key でも構いません。

// リクエストヘッダーの API Key をチェック
const apiKey = request.headers.get("Authorization");
if (apiKey !== `Bearer ${process.env.API_KEY}`) {
  return new Response("Unauthorized", { status: 401 });
}

レート制限:悪意のある呼び出しで API のクォータを使い切られるのを防ぎます。express-rate-limit や Cloudflare Workers の内蔵レート制限が使えます。

ログpinowinston でツールの呼び出しを記録しておくと、問題の調査が楽になります。

import pino from "pino";
const logger = pino();

server.tool("get_weather", /* ... */, async ({ city }) => {
  logger.info({ city }, "天気を照会");
  // ...
});

モニタリング:ツール呼び出しの成功率や応答時間を追跡します。Prometheus + Grafana がよく使われる組み合わせです。


まとめ

この記事では、TypeScript を使ってゼロから MCP Server を書く方法を紹介しました。内容は次のとおりです。

  • MCP のコアコンセプトと 3 層アーキテクチャを理解する
  • MCP TypeScript SDK でサーバーを作成する
  • 天気照会ツール(Tools)を実装する
  • サーバー状態リソース(Resources)を追加する
  • 天気レポートテンプレート(Prompts)を定義する
  • Claude Desktop / Cursor から Server を呼び出すよう設定する

これであなたは、次のことができます。

  1. よく使う API の MCP ラッパーを構築する(GitHub、Slack、Notion など)
  2. 社内業務システムの MCP インターフェースを作る(CRM、データベース)
  3. MCP コミュニティの既存の成果を探索する

さらに学ぶためのリソース

MCP プロトコルの仕組みをさらに深く知りたい場合は、MCP プロトコルの仕組みを深掘りするの記事もあわせてご覧ください。

FAQ

MCP Server 開発にはどんな前提知識が必要ですか?
JavaScript/TypeScript の基礎が必要です。本記事は MCP TypeScript SDK を使うので、async/await と型定義に慣れていれば始められます。
MCP Server と FastMCP の違いは何ですか?
FastMCP は Python フレームワークで、Python 開発者に向いています。本記事は TypeScript ネイティブ SDK を使い、フロントエンド/フルスタック開発者に向いています。両者は機能が同等で、どちらを選ぶかは技術スタックの好み次第です。
MCP Server が正しく動作しているかをどうテストしますか?
Claude Desktop を設定したあと、対話欄に自然言語のリクエスト(例:「北京の天気を調べて」)を入力します。Claude が自動でツールを呼び出して結果を返せば、Server は正常に動作しています。
MCP Server はリモートサーバーにデプロイできますか?
できます。本記事で使う stdio 通信はローカル開発に向いています。本番環境では HTTP/SSE 通信を使い、OAuth 認証とレート制限による保護を実装する必要があります。

5分で読めます · 公開日: 2026年3月19日 · 更新日: 2026年6月8日

関連記事

コメント

GitHubアカウントでログインしてコメントできます