TypeScriptで自作するMCPサーバー入門:AIアシスタントを社内システムと安全に連携させる方法

Development tutorial - IT technology blog
Development tutorial - IT technology blog

課題:強力なLLMが直面する社内データの「壁」

日々のコーディング作業でClaudeやChatGPTを活用するのは非常に便利です。しかし、実際のチーム開発や業務フローに導入しようとすると、大規模言語モデル(LLM)は完全に外部から隔離されているため、すぐに限界に突き当たります。AIは社内のPostgreSQLにアクセスしてエラーの発生した注文データを調査することもできなければ、ステージング環境にある500MBのログファイルを直接読み取ることも、Jiraのチケットを自動起票するAPIを叩くこともできません。

これまでAIにこうした処理を実行させるには、プラットフォームごとに独自のFunction Calling用コードを記述する必要がありました。OpenAI向けに実装した連携ロジックはClaudeでは再利用できず、CursorやWindsurfに移行すれば、また別のグルーコードを一から書き直さなければなりませんでした。プロジェクトが拡大するほど、保守コストは跳ね上がります。こうした分断を解消すべく、Anthropicが策定・公開したオープン標準が「Model Context Protocol(MCP)」です。

Model Context Protocol(MCP)とは?

分かりやすく例えるなら、MCPはAIアプリケーションにとっての「USB-Cポート」のような標準規格です。かつて機器ごとにバラバラだった充電コネクタが統一されたように、AIの外部連携インターフェースを単一の標準にまとめます。

このプロトコルは、IDEの世界におけるLanguage Server Protocol(LSP)と非常によく似た仕組みで動作します。各エディタが言語ごとに独自のパーサーを実装する代わりに、LSPが構文解析の仕様を共通化しました。同様にMCPを使えば、あらゆるAIクライアントが同一の標準インターフェースを介して外部データソースに接続できるようになります。

クライアント・サーバー型のアーキテクチャ

MCPシステムは、馴染みのあるクライアント・サーバーモデルで動作します。

  • MCP Host / Client: 日常的に利用するAIアプリケーション(Claude Desktop、Cursor、Zed、あるいは独自開発のAIエージェントなど)。会話のコンテキストを管理し、いつ外部ツールを呼び出すかを判断します。
  • MCP Server: 開発者自身やコミュニティによって開発される独立したプロセス。ゲートウェイとして機能し、データや安全にカプセル化された処理関数を外部に公開します。
  • 通信チャネル(Transports): クライアントとサーバーはJSON-RPC 2.0プロトコルを用いてハンドシェイクを行います。代表的な方式として、標準入出力ストリームを介してやり取りするローカルプロセス向けのstdioと、ネットワーク経由のリモートサーバーに適したSSE / HTTP Streamの2種類があります。

MCPサーバーが提供する3つのコアリソース

  1. Tools(ツール): 副作用を伴う実行可能な関数(REST APIの呼び出し、SQLクエリの実行、ビルドトリガーなど)。AIが引数を指定してツールの実行を自律的に提案しますが、最終的な承認権限はユーザー側にあります。
  2. Resources(リソース): ファイルやURI形式の静的または動的データ(設定ファイル、システムログ、データベーススキーマなど)。クライアント側が能動的に読み込み、プロンプトのコンテキストウィンドウに注入します。
  3. Prompts(プロンプト): パラメータ付きの事前定義済みプロンプトテンプレート。プルリクエストのレビュー用テンプレートやエラーコード調査用テンプレートなど、チーム全体でプロンプトエンジニアリングのベストプラクティスを標準化するのに役立ちます。

実践:TypeScriptで初めてのMCPサーバーを構築する

ここではNode.jsとTypeScriptを使い、ローカルで動作するMCPサーバーを構築してみましょう。このサーバーでは、ホストマシンのハードウェアリソースを監視するツール(`get_system_metrics`)と、商品の割引価格を計算するツール(`calculate_discount`)の2つの実用的なツールを実装します。

ステップ1: TypeScriptプロジェクトの初期化

ターミナルを開き、新しい作業ディレクトリを作成します。

mkdir my-mcp-server
cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx

モジュール設定用に tsconfig.json を作成します。

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"]
}

package.json を開き、ES Module仕様への変更と便利なスクリプトの追加を行います。

{
  "name": "my-mcp-server",
  "version": "1.0.0",
  "type": "module",
  "bin": {
    "my-mcp-server": "./dist/index.js"
  },
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "dev": "tsx src/index.ts"
  }
}

ステップ2: MCPサーバーのソースコード実装

src/index.ts を作成します。以下のコードは、標準の stdio トランスポート経由で通信するサーバーを初期化する実装です。

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import os from "os";

// MCPサーバーの初期化
const server = new Server(
  {
    name: "internal-system-tools",
    version: "1.0.0",
  },
  {
    capabilities: {
      tools: {},
    },
  }
);

// 1. AIクライアントがスキーマを認識できるようTool一覧を登録
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "get_system_metrics",
        description: "サーバーの現在のハードウェア仕様(空きメモリ、総メモリ容量、稼働時間)を取得",
        inputSchema: {
          type: "object",
          properties: {},
        },
      },
      {
        name: "calculate_discount",
        description: "割引率(%)に基づいて注文の割引後価格を計算",
        inputSchema: {
          type: "object",
          properties: {
            price: { type: "number", description: "商品の元の価格(VNDまたはUSD)" },
            discountPercent: { type: "number", description: "割引率(0から100)" },
          },
          required: ["price", "discountPercent"],
        },
      },
    ],
  };
});

// 2. クライアントからのTool呼び出しリクエストを処理するルーティング
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  if (name === "get_system_metrics") {
    const totalMem = (os.totalmem() / (1024 ** 3)).toFixed(2);
    const freeMem = (os.freemem() / (1024 ** 3)).toFixed(2);
    const uptimeHours = (os.uptime() / 3600).toFixed(1);

    return {
      content: [
        {
          type: "text",
          text: JSON.stringify({
            platform: os.platform(),
            arch: os.arch(),
            totalMemoryGB: totalMem,
            freeMemoryGB: freeMem,
            uptimeHours: uptimeHours,
          }, null, 2),
        },
      ],
    };
  }

  if (name === "calculate_discount") {
    const { price, discountPercent } = args as { price: number; discountPercent: number };
    
    if (discountPercent < 0 || discountPercent > 100) {
      throw new Error("割引率は0から100の間で指定してください。");
    }

    const finalPrice = price - (price * discountPercent) / 100;
    
    return {
      content: [
        {
          type: "text",
          text: `元の価格: ${price.toLocaleString()} | 割引: ${discountPercent}% | 割引後価格: ${finalPrice.toLocaleString()}`,
        },
      ],
    };
  }

  throw new Error(`ツールが存在しません: ${name}`);
});

// 3. Stdio接続の起動
async function run() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("[INFO] MCPサーバーがstdioトランスポートで正常に起動しました");
}

run().catch((error) => {
  console.error("[FATAL] サーバーの起動に失敗しました:", error);
  process.exit(1);
});

デバッグ時の極めて重要な注意点: stdio トランスポートを使用する場合、すべてのJSON-RPCパケットは stdout(標準出力)を経由して送受信されます。ログ出力のために安易に console.log() を使用すると、その文字列がJSONパケットのストリームに直接混入してしまい、クライアント側でのパースエラーを引き起こします。デバッグログの出力には、必ず console.error() を使用してください。

複雑なJSON-RPCペイロードを扱う際は、IDEで新しいタブを開かずにスキーマ構造の確認やデータバリデーションを行える toolcraft.appのJSON Formatter などのオンラインフォーマッタを活用すると便利です。

ステップ3: プロジェクトのビルド

ビルドコマンドを実行し、TypeScriptのソースコードをJavaScriptにコンパイルして dist ディレクトリに出力します。

npm run build

ステップ4: Claude Desktopでの設定

お使いの環境に応じてClaude Desktopの設定ファイルを開きます。

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

mcpServers の項目に作成したサーバーの情報を追加します(プロジェクトディレクトリの絶対パスは適宜置き換えてください)。

{
  "mcpServers": {
    "my-system-tools": {
      "command": "node",
      "args": ["/Users/username/my-mcp-server/dist/index.js"]
    }
  }
}

Claude Desktopを再起動します。チャット入力欄の右下にツールを表す金づちアイコンが表示されていれば準備完了です。あとはチャット欄に「マシンの空きメモリが何GBあるか確認して」と入力するだけで、Claudeが自動的に get_system_metrics を呼び出し、ローカルサーバーから返されたJSONデータを解析して数秒で回答してくれます。

まとめ

MCPモデルは、データセキュリティという極めて重要な課題を解決します。データベース全体をOpenAIやAnthropicのクラウドにアップロードするのではなく、ローカルネットワーク内で必要なエンドポイントのみを厳選して公開できます。TypeScriptで一度サーバーを構築しておけば、コードを書き直すことなくClaude DesktopやCursorをはじめとする多様なMCP対応クライアントへ柔軟に接続可能です。

Share: