はじめに
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つの主要コンポーネントで構成されています:
- ホスト:ユーザーが利用するLLMアプリケーション(Claude DesktopやClineなど)
- MCPクライアント:ホストアプリケーション内でサーバーとの接続を管理するコンポーネント
- MCPサーバー:クライアントにコンテキスト、ツール、プロンプトを提供するサービス
Claude Desktopで既存のMCPサーバーを利用する
Claude Desktopのインストール
まずはClaude Desktopを公式サイトからダウンロードしてインストールします。既にインストール済みの場合は、最新バージョンにアップデートしておきましょう。
MCPサーバーの追加方法
Claude Desktopで既存のMCPサーバーを利用するには、設定ファイルを編集する必要があります:
- Claude Desktopを起動し、メニューバーから「Claude」→「Settings...」を選択
- 左側のメニューから「Developer」を選択
- 「Edit Config」ボタンをクリック
-
claude_desktop_config.json
ファイルをテキストエディタで開く
例として、GitHubのMCPサーバーを追加してみましょう:
{
"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サーバーです。
プロジェクトのセットアップ
まず新しいプロジェクトを作成し、必要なパッケージをインストールします:
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
を次のように編集します:
{
"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
を作成します:
{
"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サーバーを初期化します:
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
を作成します:
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.",
},
],
});
});
});
テストを実行するには以下のコマンドを使用します:
npm run test
サーバーのビルドと起動
サーバーをビルドして実行可能ファイルを生成します:
npm run build
生成されたファイルを実行して、サーバーが起動することを確認します:
node ./build/index.js
「MCP Math Tools Server running on stdio」というメッセージが表示されれば成功です。
Claude DesktopでMCPサーバーを利用する
自作したMath Toolsサーバーをクライアントから呼び出してみましょう。Claude Desktopの設定ファイルclaude_desktop_config.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サーバーでは適切なエラーハンドリングが重要です。例えば、不正な入力に対してわかりやすいエラーメッセージを返すようにしましょう:
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はテキストだけでなく、リッチなコンテンツを返すこともできます:
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の主なポイントは以下の通りです:
- MCPはアプリケーションがLLMにコンテキストを提供する方法を標準化するプロトコル
- MCPを使うことで、LLMは外部ツールやサービスからコンテキストを取得し、様々なアクションを実行できる
- MCPはホスト、クライアント、サーバーの3つのコンポーネントで構成される
- MCPサーバーはリソース、ツール、プロンプトを提供し、クライアントはこれらを利用してタスクを実行する
今回実装した数値計算ツールは基本的な機能しか持っていませんが、この基盤を拡張することで、より複雑で実用的なMCPサーバーを構築できるでしょう。MCPの標準化されたインターフェースを活用することで、LLMアプリケーションの可能性は無限に広がります。
TypeScriptによるMCPサーバー実装は、型安全性と開発効率の高さから、特に複雑なツールを開発する際に強みを発揮します。今後のAI開発において、MCPは重要な役割を担うことでしょう。