10分でカスタムMCPサーバーを構築してデプロイする方法

💡ノーコードで自分のエージェントAIワークフローを作成したいですか? Anakin AIを使用すれば、コーディングの知識がなくても簡単にAIワークフローを作成できます。GPT-4、Claude 3.5 Sonnet、Uncensored Dolphin-Mixtral、Stable Diffusion、DALLE、Web ScrapingなどのLLM APIを1つのワークフローに接続できます! 複雑なコーディングは忘れて、Anakin AIで日常の作業を自動化しましょう! 期間限定で、Google Gemini 1.5とStable Diffusionも無料で使用できます! 無料で始める はじめに モデルコンテキストプロトコル(MCP)は、AIエコシステムの重要な進展を表しており、大規模言語モデルとのコミュニケーションに標準化された方法を提供します。各AIプラットフォームがメッセージの独自のフォーマットを実装するのではなく、MCPはさまざまなモデルやプラットフォーム間でプロンプト、応答、関数呼び出しの一貫したインターフェイスを提供することを目指しています。 プロトコル自

Anakin AIを無料で利用開始

10分でカスタムMCPサーバーを構築してデプロイする方法

Start for free
目次
💡
ノーコードで自分のエージェントAIワークフローを作成したいですか?

Anakin AIを使用すれば、コーディングの知識がなくても簡単にAIワークフローを作成できます。GPT-4、Claude 3.5 Sonnet、Uncensored Dolphin-Mixtral、Stable Diffusion、DALLE、Web ScrapingなどのLLM APIを1つのワークフローに接続できます!

複雑なコーディングは忘れて、Anakin AIで日常の作業を自動化しましょう!

期間限定で、Google Gemini 1.5とStable Diffusionも無料で使用できます!
Anakin AIを使ってAIエージェントワークフローを簡単に構築!
Anakin AIを使ってAIエージェントワークフローを簡単に構築!

はじめに

モデルコンテキストプロトコル(MCP)は、AIエコシステムの重要な進展を表しており、大規模言語モデルとのコミュニケーションに標準化された方法を提供します。各AIプラットフォームがメッセージの独自のフォーマットを実装するのではなく、MCPはさまざまなモデルやプラットフォーム間でプロンプト、応答、関数呼び出しの一貫したインターフェイスを提供することを目指しています。

プロトコル自体は進化していますが、基本的なMCP互換サーバーを構築することは簡単です。このガイドでは、現代のAIシステムにおけるメッセージ処理の基本原則に従ったシンプルで機能的なサーバーを作成する手順を説明します。私たちの実装は、後により高度な機能を拡張できる基盤を作成することに焦点を当てます。

10分で十分ですか?生産準備が整ったシステムには、もちろん足りません。しかし、主要な概念を示すワーキングプロトタイプには?絶対に大丈夫です。それでは始めましょう!

前提条件

始める前に、以下が必要です:

  • システムにNode.js(v16+)がインストールされている
  • JavaScript/TypeScriptの基本的な知識
  • Express.jsまたは同様のウェブフレームワークに慣れている
  • コードエディタ(VS Code推奨)
  • ターミナル/コマンドラインへのアクセス
  • npmまたはyarnパッケージマネージャー

ステップ1:プロジェクトのセットアップ(2分)

まず、新しいディレクトリを作成し、プロジェクトを初期化します:

mkdir mcp-server
cd mcp-server
npm init -y

次に、必要な依存関係をインストールします:

npm install express cors typescript ts-node @types/node @types/express @types/cors
npm install --save-dev nodemon

TypeScriptの設定ファイルを作成します:

npx tsc --init

生成されたtsconfig.jsonを編集して、以下の必須設定を含めます:

{
  "compilerOptions": {
    "target": "es2018",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true
  },
  "include": ["src/**/*"]
}

package.jsonのスクリプトセクションを更新します:

"scripts": {
  "start": "node dist/index.js",
  "dev": "nodemon --exec ts-node src/index.ts",
  "build": "tsc"
}

ステップ2:コアサーバーの作成(3分)

ソースディレクトリとメインサーバーファイルを作成します:

mkdir -p src/handlers
touch src/index.ts
touch src/handlers/messageHandler.ts
touch src/types.ts

まず、src/types.tsに私たちのタイプを定義しましょう:

// 基本メッセージ構造
export interface Content {
  type: string;
  text?: string;
}

export interface Message {
  role: "user" | "assistant" | "system";
  content: Content[] | string;
}

// リクエストと応答の構造
export interface ModelRequest {
  messages: Message[];
  max_tokens?: number;
  temperature?: number;
  stream?: boolean;
}

export interface ModelResponse {
  message: Message;
}

// ツール呼び出しインターフェース
export interface Tool {
  name: string;
  description: string;
  input_schema: {
    type: string;
    properties: Record;
    required?: string[];
  };
}

export interface ToolCall {
  type: "tool_call";
  id: string;
  name: string;
  input: Record;
}

export interface ToolResult {
  type: "tool_result";
  tool_call_id: string;
  content: string;
}

次に、src/index.tsに基本的なサーバーを実装します:

import express from 'express';
import cors from 'cors';
import { handleMessageRequest } from './handlers/messageHandler';

const app = express();
const PORT = process.env.PORT || 3000;

// ミドルウェア
app.use(cors());
app.use(express.json({ limit: '10mb' }));

// メッセージ処理のためのメインエンドポイント
app.post('/v1/messages', handleMessageRequest);

// ヘルスチェックエンドポイント
app.get('/health', (req, res) => {
  res.status(200).json({ status: 'ok' });
});

// サーバーを起動
app.listen(PORT, () => {
  console.log(`サーバーはポート ${PORT} で動作中`);
  console.log(`ヘルスチェック: http://localhost:${PORT}/health`);
  console.log(`メッセージエンドポイント: http://localhost:${PORT}/v1/messages`);
});

次に、src/handlers/messageHandler.tsにメッセージハンドラを実装します:

import { Request, Response } from 'express';
import { ModelRequest, ModelResponse, Message, Content } from '../types';

export async function handleMessageRequest(req: Request, res: Response) {
  try {
    const request = req.body as ModelRequest;
    
    // 基本的なバリデーション
    if (!request.messages || !Array.isArray(request.messages) || request.messages.length === 0) {
      return res.status(400).json({ error: '無効なリクエスト形式。メッセージ配列が必要です。' });
    }
    
    // 受信したリクエストをログ出力(デバッグ用)
    console.log('受信したリクエスト:', request.messages.length, 'メッセージ');
    
    // メッセージを処理
    const response = processMessages(request.messages);
    
    // 応答を返す
    return res.status(200).json(response);
  } catch (error) {
    console.error('リクエスト処理中にエラー:', error);
    return res.status(500).json({ error: '内部サーバーエラー' });
  }
}

function processMessages(messages: Message[]): ModelResponse {
  // 最後のユーザーメッセージを抽出
  const lastUserMessage = findLastUserMessage(messages);
  
  if (!lastUserMessage) {
    return createErrorResponse("会話にユーザーメッセージが見つかりません");
  }
  
  const userQuery = extractTextContent(lastUserMessage);
  
  // 簡単な応答生成ロジック
  let responseText = "";
  
  if (userQuery.toLowerCase().includes('こんにちは') || userQuery.toLowerCase().includes('やあ')) {
    responseText = "こんにちは!今日はどのようにお手伝いできますか?";
  } else if (userQuery.toLowerCase().includes('天気')) {
    responseText = "リアルタイムの天気データにはアクセスできませんが、天気のパターンを理解する手助けができます。";
  } else if (userQuery.toLowerCase().includes('時間')) {
    responseText = `現在のサーバー時間は ${new Date().toLocaleTimeString()} です。`;
  } else {
    responseText = "メッセージを受け取りました。これはシンプルなモデルサーバーの応答です。";
  }
  
  // 応答を構築して返す
  return {
    message: {
      role: "assistant",
      content: [{ 
        type: "text", 
        text: responseText 
      }]
    }
  };
}

function findLastUserMessage(messages: Message[]): Message | undefined {
  // 'user'の役割を持つ最後のメッセージを探します
  for (let i = messages.length - 1; i >= 0; i--) {
    if (messages[i].role === 'user') {
      return messages[i];
    }
  }
  return undefined;
}

function extractTextContent(message: Message): string {
  if (typeof message.content === 'string') {
    return message.content;
  } else if (Array.isArray(message.content)) {
    return message.content
      .filter(item => item.type === 'text' && item.text)
      .map(item => item.text)
      .join(' ');
  }
  return '';
}

function createErrorResponse(errorMessage: string): ModelResponse {
  return {
    message: {
      role: "assistant",
      content: [{ 
        type: "text", 
        text: `エラー: ${errorMessage}` 
      }]
    }
  };
}

ステップ3:ツール呼び出し機能の追加(3分)

ツールの定義と実装のための新しいファイルを作成します:

touch src/tools.ts

src/tools.tsにいくつかの基本的なツールを実装します:

import { Tool } from './types';

// ツール定義
export const availableTools: Tool[] = [
  {
    name: "get_current_time",
    description: "現在のサーバー時間を取得",
    input_schema: {
      type: "object",
      properties: {
        timezone: {
          type: "string",
          description: "オプションのタイムゾーン(サーバーのタイムゾーンがデフォルト)"
        }
      }
    }
  },
  {
    name: "calculate",
    description: "数学的計算を実行",
    input_schema: {
      type: "object",
      properties: {
        expression: {
          type: "string",
          description: "評価する数学的式"
        }
      },
      required: ["expression"]
    }
  }
];

// ツール実装
export function executeToolCall(name: string, params: Record): string {
  switch (name) {
    case "get_current_time":
      return getTime(params.timezone);
    case "calculate":
      return calculate(params.expression);
    default:
      throw new Error(`未知のツール: ${name}`);
  }
}

function getTime(timezone?: string): string {
  const options: Intl.DateTimeFormatOptions = { 
    hour: '2-digit', 
    minute: '2-digit', 
    second: '2-digit',
    timeZoneName: 'short' 
  };
  
  try {
    if (timezone) {
      options.timeZone = timezone;
    }
    return new Date().toLocaleTimeString('ja-JP', options);
  } catch (error) {
    return `${new Date().toLocaleTimeString()}(サーバー時間)`;
  }
}

function calculate(expression: string): string {
  try {
    // 注意: 実際のアプリケーションでは、安全な評価メソッドを使用すべきです
    // これはデモのための簡略化された例です
    const sanitizedExpression = expression.replace(/[^0-9+\-*/().\s]/g, '');
    const result = eval(sanitizedExpression);
    return `${expression} = ${result}`;
  } catch (error) {
    return `式 ${expression} を計算中にエラーが発生しました: ${error instanceof Error ? error.message : String(error)}`;
  }
}

次に、src/handlers/messageHandler.tsを修正して、ツール呼び出しをサポートするようにメッセージハンドラを更新します:

// 上部にこれらのインポートを追加
import { availableTools, executeToolCall } from '../tools';
import { ToolCall, ToolResult } from '../types';

// handleMessageRequest関数を更新
export async function handleMessageRequest(req: Request, res: Response) {
  try {
    const request = req.body as ModelRequest;
    
    // 基本的なバリデーション
    if (!request.messages || !Array.isArray(request.messages) || request.messages.length === 0) {
      return res.status(400).json({ error: '無効なリクエスト形式。メッセージ配列が必要です。' });
    }
    
    // ツール結果を含むリクエストか確認
    const lastMessage = request.messages[request.messages.length - 1];
    if (lastMessage.role === 'assistant' && Array.isArray(lastMessage.content)) {
      const toolCalls = lastMessage.content.filter(item => 
        item.type === 'tool_call') as ToolCall[];
      
      if (toolCalls.length > 0) {
        // ツール結果に関するフォローアップ
        return handleToolResultsResponse(request, res);
      }
    }
    
    // 通常のメッセージとして処理
    const response = processMessages(request.messages);
    
    // 応答を返す
    return res.status(200).json(response);
  } catch (error) {
    console.error('リクエスト処理中にエラー:', error);
    return res.status(500).json({ error: '内部サーバーエラー' });
  }
}

// ツール呼び出しを処理するためのこの関数を追加
function handleToolResultsResponse(request: ModelRequest, res: Response) {
  const messages = request.messages;
  const lastAssistantMessage = messages[messages.length - 1];
  
  if (lastAssistantMessage.role !== 'assistant' || !Array.isArray(lastAssistantMessage.content)) {
    return res.status(400).json({ error: '無効なツール結果形式' });
  }
  
  // ツール呼び出しと結果を取得
  const toolCalls = lastAssistantMessage.content.filter(
    item => item.type === 'tool_call'
  ) as ToolCall[];
  
  const toolResults = lastAssistantMessage.content.filter(
    item => item.type === 'tool_result'
  ) as ToolResult[];
  
  // 結果を処理
  let finalResponse = "以下の情報を処理しました:\n\n";
  
  toolResults.forEach(result => {
    const relatedCall = toolCalls.find(call => call.id === result.tool_call_id);
    if (relatedCall) {
      finalResponse += `- ${relatedCall.name}: ${result.content}\n`;
    }
  });
  
  return res.status(200).json({
    message: {
      role: "assistant",
      content: [{ type: "text", text: finalResponse }]
    }
  });
}

// ツール呼び出しの可能性を処理するようにprocessMessages関数を修正
function processMessages(messages: Message[]): ModelResponse {
  const lastUserMessage = findLastUserMessage(messages);
  
  if (!lastUserMessage) {
    return createErrorResponse("会話にユーザーメッセージが見つかりません");
  }
  
  const userQuery = extractTextContent(lastUserMessage);
  
  // ツール使用をトリガーするキーワードを探す
  if (userQuery.toLowerCase().includes('時間')) {
    return {
      message: {
        role: "assistant",
        content: [
          { type: "tool_call", id: "call_001", name: "get_current_time", input: {} },
          { 
            type: "text", 
            text: "現在の時間を確認します。" 
          }
        ]
      }
    };
  } else if (userQuery.toLowerCase().match(/計算する|算出する|何ですか \d+[\+\-\*\/]/)) {
    // 潜在的な計算を抽出
    const expression = userQuery.match(/(\d+[\+\-\*\/\(\)\.]*\d+)/)?.[0] || "1+1";
    
    return {
      message: {
        role: "assistant",
        content: [
          { 
            type: "tool_call", 
            id: "call_002", 
            name: "calculate", 
            input: { expression } 
          },
          { 
            type: "text", 
            text: `${expression}を計算します。` 
          }
        ]
      }
    };
  }
  
  // 他のクエリに対するデフォルトの応答
  return {
    message: {
      role: "assistant",
      content: [{ 
        type: "text", 
        text: `メッセージを受け取りました: "${userQuery}"。さらにどのようにお手伝いできますか?` 
      }]
    }
  };
}

ステップ4:テストとデプロイ(2分)

サーバーをチェックするためのシンプルなテストスクリプトを作成します。ルートディレクトリにtest.jsというファイルを作成します:

const fetch = require('node-fetch');

async function testServer() {
  const url = 'http://localhost:3000/v1/messages';
  
  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      messages: [
        {
          role: "user",
          content: [
            {
              type: "text",
              text: "今は何時ですか?"
            }
          ]
        }
      ]
    })
  });
  
  const data = await response.json();
  console.log(JSON.stringify(data, null, 2));
}

testServer().catch(console.error);

サーバーをテストするには:

# サーバーを起動
npm run dev

# 別のターミナルでテストを実行
node test.js

迅速なデプロイのために、シンプルなDockerfileを作成します:

FROM node:16-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .
RUN npm run build

EXPOSE 3000

CMD ["npm", "start"]

コンテナをビルドして実行します:

docker build -t mcp-server .
docker run -p 3000:3000 mcp-server

結論

わずか10分で、現代のAIメッセージプロトコルの主要概念を実装した基本的なサーバーを構築しました。私たちのサーバーは:

  1. 構造化されたメッセージリクエストを処理することができます
  2. 標準形式で応答することができます
  3. 基本的なツール呼び出し機能を扱うことができます
  4. ツール結果を処理して応答することができます

この実装は簡略化されていますが、さらなる開発のための確固たる基盤を提供します。このサーバーを本番環境で使用するために拡張するには、以下を検討してください:

  • 認証とレート制限の追加
  • 適切なエラーハンドリングとバリデーションの実装
  • 実際のAIモデルとの接続
  • より高度なツールの追加
  • ストリーミング応答の実装
  • 包括的なロギングとモニタリングの追加

私たちの実装が現代のAIメッセージプロトコルの一般的な原則に従っていることを忘れないでください。しかし、OpenAIのAPIやAnthropicのClaude APIなどの特定の実装には追加の要件や期待される形式のわずかな変動がある場合があります。統合している具体的なサービスの公式文書を常に確認してください。

AIメッセージプロトコルの分野は急速に進化しているので、最新の進展を常に把握し、基準が進化する際に実装を適応させる準備をしてください。このガイドで示されたコアコンセプトを理解することで、このエキサイティングな分野で新たな進展が出現したときに十分な準備ができます。