【完全ガイド】TypeScriptでMCPサーバー構築入門：Claude AIとの連携とカスタムツール実装方法


## はじめに

MCPとはModel Context Protocolの略であり、LLM（大規模言語モデル）にコンテキストを提供する方法を標準化するプロトコルです。MCPを活用することで、LLMは単なる質問応答だけでなく、外部ツールからの情報取得、コード実行、データ保存など、より実用的なタスクを実行できるようになります。

本記事では、TypeScriptを使ってMCPサーバーを実装し、Claude Desktopなどのホストアプリケーションから利用する方法を解説します。さらに、実用的なサーバーの構築方法から実際の運用までをカバーします。

## MCP（Model Context Protocol）の基本概念

### MCPとは何か

MCPは、アプリケーションがLLMにコンテキスト情報を提供するための標準プロトコルです。例えば、「今日の天気は？」という質問に対して、LLMは過去のデータだけでは回答できませんが、MCPを通じて天気APIを呼び出し、最新の情報を取得することができます。

Function Callingとの違いは何でしょうか？Function Callingも外部APIを呼び出す手段ですが、実装がLLMごとに異なります。一方、MCPは標準化されたインターフェースを提供し、複数のツールを組み合わせた複雑なワークフローの構築が容易になります。

### MCPのアーキテクチャ

MCPは以下の3つの主要コンポーネントで構成されています：

1. ホスト：ユーザーが利用するLLMアプリケーション（Claude DesktopやClineなど）
2. MCPクライアント：ホストアプリケーション内でサーバーとの接続を管理するコンポーネント
3. MCPサーバー：クライアントにコンテキスト、ツール、プロンプトを提供するサービス

## Claude Desktopで既存のMCPサーバーを利用する

### Claude Desktopのインストール

まずは[Claude Desktop](https://claude.ai/download)を公式サイトからダウンロードしてインストールします。既にインストール済みの場合は、最新バージョンにアップデートしておきましょう。

### MCPサーバーの追加方法

Claude Desktopで既存のMCPサーバーを利用するには、設定ファイルを編集する必要があります：

1. Claude Desktopを起動し、メニューバーから「Claude」→「Settings...」を選択
2. 左側のメニューから「Developer」を選択
3. 「Edit Config」ボタンをクリック
4. `claude_desktop_config.json`ファイルをテキストエディタで開く

例として、GitHubのMCPサーバーを追加してみましょう：

```json
{
  "mcpServers": {
    "github": {
      "command": "docker",
      "args": [
        "run",
        "-i",
        "--rm",
        "-e",
        "GITHUB_PERSONAL_ACCESS_TOKEN",
        "mcp/github"
      ],
      "env": {
        "GITHUB_PERSONAL_ACCESS_TOKEN": "YOUR_TOKEN_HERE"
      }
    }
  }
}
```

設定を保存してClaude Desktopを再起動すると、設定画面のDeveloperメニューに「github」が表示されるようになります。

## TypeScriptで独自のMCPサーバーを実装する

それでは、TypeScriptで独自のMCPサーバーを実装してみましょう。今回は「数値計算ツール」を作成します。これは数学的な計算を実行するシンプルなMCPサーバーです。

### プロジェクトのセットアップ

まず新しいプロジェクトを作成し、必要なパッケージをインストールします：

```bash
mkdir mcp-math-tools
cd mcp-math-tools
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D @types/node typescript vitest
```

`package.json`を次のように編集します：

```json
{
  "name": "mcp-math-tools",
  "version": "1.0.0",
  "main": "src/server.ts",
  "type": "module",
  "bin": {
    "mathTools": "./build/index.js"
  },
  "scripts": {
    "build": "tsc && chmod 755 build/index.js",
    "test": "vitest"
  },
  "files": [
    "build"
  ]
}
```

`tsconfig.json`を作成します：

```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`ファイルを作成し、MCPサーバーを初期化します：

```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

// サーバーインスタンスの作成
export const server = new McpServer({
  name: "MathTools",
  version: "0.1.0",
});

// 因数分解ツールの実装
server.tool(
  "factorize",
  "Factorize a number into its prime factors",
  { number: z.number().int().positive().describe("An integer to factorize") },
  async ({ number }) => {
    const factors = findPrimeFactors(number);
    
    return {
      content: [
        {
          type: "text",
          text: `Prime factors of ${number}: ${factors.join(' × ')}`,
        },
      ],
    };
  }
);

// 最大公約数を計算するツール
server.tool(
  "gcd",
  "Calculate the greatest common divisor of two numbers",
  {
    a: z.number().int().describe("First integer"),
    b: z.number().int().describe("Second integer")
  },
  async ({ a, b }) => {
    const result = calculateGCD(a, b);
    
    return {
      content: [
        {
          type: "text",
          text: `The greatest common divisor of ${a} and ${b} is ${result}`,
        },
      ],
    };
  }
);

// 素数判定ツール
server.tool(
  "isPrime",
  "Check if a number is prime",
  { number: z.number().int().positive().describe("Number to check") },
  async ({ number }) => {
    const prime = isPrime(number);
    
    return {
      content: [
        {
          type: "text",
          text: prime ? 
            `${number} is a prime number.` : 
            `${number} is not a prime number.`,
        },
      ],
    };
  }
);

// 素数判定関数
function isPrime(num: number): boolean {
  if (num <= 1) return false;
  if (num <= 3) return true;
  
  if (num % 2 === 0 || num % 3 === 0) return false;
  
  for (let i = 5; i * i <= num; i += 6) {
    if (num % i === 0 || num % (i + 2) === 0) return false;
  }
  
  return true;
}

// 因数分解関数
function findPrimeFactors(n: number): number[] {
  const factors: number[] = [];
  let divisor = 2;
  
  while (n > 1) {
    while (n % divisor === 0) {
      factors.push(divisor);
      n /= divisor;
    }
    divisor++;
    
    if (divisor * divisor > n && n > 1) {
      factors.push(n);
      break;
    }
  }
  
  return factors;
}

// 最大公約数計算関数
function calculateGCD(a: number, b: number): number {
  while (b !== 0) {
    const temp = b;
    b = a % b;
    a = temp;
  }
  return Math.abs(a);
}

// サーバー起動
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("MCP Math Tools Server running on stdio");
}

main().catch((error) => {
  console.error("Fatal error in main():", error);
  process.exit(1);
});
```

### ツールのテスト実装

ツールをテストするためのファイル`src/index.test.ts`を作成します：

```typescript
import { describe, it, expect } from "vitest";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { server } from "./index.js";

describe("MathTools Tests", () => {
  it("factorize returns correct prime factors", async () => {
    const client = new Client({
      name: "test client",
      version: "0.1.0",
    });

    const [clientTransport, serverTransport] = 
      InMemoryTransport.createLinkedPair();
    
    await Promise.all([
      client.connect(clientTransport),
      server.connect(serverTransport),
    ]);

    const result = await client.callTool({
      name: "factorize",
      arguments: {
        number: 60,
      },
    });

    expect(result).toEqual({
      content: [
        {
          type: "text",
          text: "Prime factors of 60: 2 × 2 × 3 × 5",
        },
      ],
    });
  });

  it("gcd calculates correct greatest common divisor", async () => {
    const client = new Client({
      name: "test client",
      version: "0.1.0",
    });

    const [clientTransport, serverTransport] = 
      InMemoryTransport.createLinkedPair();
    
    await Promise.all([
      client.connect(clientTransport),
      server.connect(serverTransport),
    ]);

    const result = await client.callTool({
      name: "gcd",
      arguments: {
        a: 48,
        b: 18,
      },
    });

    expect(result).toEqual({
      content: [
        {
          type: "text",
          text: "The greatest common divisor of 48 and 18 is 6",
        },
      ],
    });
  });

  it("isPrime correctly identifies prime numbers", async () => {
    const client = new Client({
      name: "test client",
      version: "0.1.0",
    });

    const [clientTransport, serverTransport] = 
      InMemoryTransport.createLinkedPair();
    
    await Promise.all([
      client.connect(clientTransport),
      server.connect(serverTransport),
    ]);

    const result = await client.callTool({
      name: "isPrime",
      arguments: {
        number: 17,
      },
    });

    expect(result).toEqual({
      content: [
        {
          type: "text",
          text: "17 is a prime number.",
        },
      ],
    });
  });
});
```

テストを実行するには以下のコマンドを使用します：

```bash
npm run test
```

### サーバーのビルドと起動

サーバーをビルドして実行可能ファイルを生成します：

```bash
npm run build
```

生成されたファイルを実行して、サーバーが起動することを確認します：

```bash
node ./build/index.js
```

「MCP Math Tools Server running on stdio」というメッセージが表示されれば成功です。

## Claude DesktopでMCPサーバーを利用する

自作したMath Toolsサーバーをクライアントから呼び出してみましょう。Claude Desktopの設定ファイル`claude_desktop_config.json`に以下のJSONを追加します：

```json
{
  "mcpServers": {
    "mathTools": {
      "command": "node",
      "args": [
        "/absolute/path/to/your/mcp-math-tools/build/index.js"
      ]
    }
  }
}
```

Claude Desktopを再起動すると、ツールアイコンをクリックして「factorize」「gcd」「isPrime」のツールが追加されていることが確認できます。

例えば、「120という数字の素因数分解をしてください」と尋ねてみましょう。すると「factorize」ツールの実行が求められ、ツールの使用を許可すると「Prime factors of 120: 2 × 2 × 2 × 3 × 5」という結果が返ってきます。

## 実践的なMCPサーバーの構築ポイント

### エラーハンドリング

実際のMCPサーバーでは適切なエラーハンドリングが重要です。例えば、不正な入力に対してわかりやすいエラーメッセージを返すようにしましょう：

```typescript
server.tool(
  "fibonacci",
  "Generate Fibonacci sequence up to n terms",
  { terms: z.number().int().positive().max(100).describe("Number of terms to generate") },
  async ({ terms }) => {
    try {
      if (terms > 100) {
        return {
          content: [
            {
              type: "text",
              text: "Error: Can only generate up to 100 terms for performance reasons.",
            },
          ],
        };
      }
      
      const sequence = generateFibonacci(terms);
      
      return {
        content: [
          {
            type: "text",
            text: `Fibonacci sequence (${terms} terms): ${sequence.join(', ')}`,
          },
        ],
      };
    } catch (error) {
      return {
        content: [
          {
            type: "text",
            text: `Error calculating Fibonacci sequence: ${error.message}`,
          },
        ],
      };
    }
  }
);

function generateFibonacci(n: number): number[] {
  const sequence: number[] = [0, 1];
  for (let i = 2; i < n; i++) {
    sequence.push(sequence[i-1] + sequence[i-2]);
  }
  return sequence.slice(0, n);
}
```

### リッチコンテンツの返却

MCPはテキストだけでなく、リッチなコンテンツを返すこともできます：

```typescript
server.tool(
  "calculateStats",
  "Calculate basic statistics for a set of numbers",
  { 
    numbers: z.array(z.number()).min(1).describe("Array of numbers to analyze") 
  },
  async ({ numbers }) => {
    const sum = numbers.reduce((a, b) => a + b, 0);
    const average = sum / numbers.length;
    const sortedNumbers = [...numbers].sort((a, b) => a - b);
    const median = getMedian(sortedNumbers);
    const min = sortedNumbers[0];
    const max = sortedNumbers[sortedNumbers.length - 1];
    
    return {
      content: [
        {
          type: "text",
          text: "Statistical analysis results:",
        },
        {
          type: "json",
          json: {
            count: numbers.length,
            sum,
            average,
            median,
            min,
            max,
            sortedData: sortedNumbers
          },
        }
      ],
    };
  }
);

function getMedian(sortedArr: number[]): number {
  const mid = Math.floor(sortedArr.length / 2);
  return sortedArr.length % 2 === 0
    ? (sortedArr[mid - 1] + sortedArr[mid]) / 2
    : sortedArr[mid];
}
```

## まとめ

本記事では、TypeScriptを使ってMCP（Model Context Protocol）サーバーを実装する方法を解説しました。MCPを活用することで、LLMはより豊かな機能を持つアプリケーションとなります。

MCPの主なポイントは以下の通りです：

1. MCPはアプリケーションがLLMにコンテキストを提供する方法を標準化するプロトコル
2. MCPを使うことで、LLMは外部ツールやサービスからコンテキストを取得し、様々なアクションを実行できる
3. MCPはホスト、クライアント、サーバーの3つのコンポーネントで構成される
4. MCPサーバーはリソース、ツール、プロンプトを提供し、クライアントはこれらを利用してタスクを実行する

今回実装した数値計算ツールは基本的な機能しか持っていませんが、この基盤を拡張することで、より複雑で実用的なMCPサーバーを構築できるでしょう。MCPの標準化されたインターフェースを活用することで、LLMアプリケーションの可能性は無限に広がります。

TypeScriptによるMCPサーバー実装は、型安全性と開発効率の高さから、特に複雑なツールを開発する際に強みを発揮します。今後のAI開発において、MCPは重要な役割を担うことでしょう。