🤸‍♀️

SlackからLambda上にあるMCPクライアントとMCPサーバー動かす

に公開

はじめに

本記事では、Slackアプリ経由で受け取ったメッセージをトリガーに、Lambda 上で動作する Model Context Protocol (MCP) クライアントと MCP サーバーを使って質問応答処理を実行する構成を紹介します。

注意点

  • Slackアプリについてまだあまりよくわかってないのでベストプラクティスではない
  • MCPクライアントとサーバーを 別々のLambda関数に分離していますが、特別な理由はないため、今後は統合予定です

MCPクライアントの作成

基本的には、公式のQuickStartをベースにMCPクライアントを作成する。

MCPサーバーに接続する処理

MCP_SERVER_URLには、MCPサーバーを配置したLambdaのURLを貼る。

client.ts
const serverUrl = process.env.MCP_SERVER_URL as string;
const slackWebhookUrl = process.env.SLACK_WEB_HOOK_URL as string;
const CLAUDE_MODEL = "claude-3-5-haiku-20241022";

async function connectToServer(): Promise<void> {
  if (client) {
    return;
  }

  console.log(`サーバーに接続中: ${serverUrl}`);

  try {
    // Create a new client
    client = new Client({
      name: 'claude-tools-client',
      version: '1.0.0'
    });
    client.onerror = (error) => {
      console.error('\x1b[31mClient error:', error, '\x1b[0m');
    }

    transport = new StreamableHTTPClientTransport(
      new URL(serverUrl),
      {
        sessionId: sessionId
      }
    );

    await client.connect(transport);
    sessionId = transport.sessionId;
  } catch (error) {
    console.error('接続エラー:', error);
    throw new Error('サーバーに接続できませんでした');
  }
}

クエリ処理

クエリの処理はほぼQuickStartだが、responseをslackへ送信する処理に修正

client.ts
/**
 * クエリを処理する関数
 */
async function processQuery(query: string): Promise<void> {
  if (!client) {
    throw new Error('サーバーに接続されていません');
  }
  
  try {
    const anthropic = new Anthropic({ apiKey: ANTHROPIC_API_KEY });
    
    const toolsRequest: ListToolsRequest = {
      method: 'tools/list',
      params: {}
    };
    
    const toolsResult = await client.request(toolsRequest, ListToolsResultSchema);
    
    const formattedTools = toolsResult.tools.map(tool => ({
      name: tool.name,
      description: tool.description || "",
      input_schema: tool.inputSchema 
    }));
    
    console.log(`質問を処理します: "${query}"`);
    
    // 会話履歴を初期化
    let conversationHistory: MessageParams[] = [
      {
        role: "user",
        content: `質問: ${query}`
      }
    ];
    // シンプルな要約プロンプト
    conversationHistory.push({
      role: "user",
      content: `これまでに収集した情報を基に、質問「${query}」に対する最終的な回答をまとめてください。`
    });
    
    // 要約応答を生成
    const response = await anthropic.messages.create({
      model: CLAUDE_MODEL,
      max_tokens: 2000,
      messages: conversationHistory,
    });
    
    // 要約を表示
    console.log("\n===== 最終回答 =====");
    for (const content of response.content) {
      if (content.type === "text") {
        console.log(content.text);
        await slackNotify(content.text);
      }
    }
}

Lambda実行処理+Slack通知

Lambdaで実行するためにhandlerとSlackへのレスポンスのためにslackNotifyを追加。

client.ts
async function slackNotify(message: string): Promise<void> {
  const bodyText = { text: message };
  const body = JSON.stringify(bodyText);

  const requestOptions = {
    ...parseUrl(slackWebhookUrl),
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Content-Length': Buffer.byteLength(body),
    },
  };

  await new Promise((resolve, reject) => {
    const req = https.request(requestOptions, (res) => {
      res.statusCode === 200 ? resolve : reject(new Error(`Slack responded with ${res.statusCode}`));
    });
    req.on('error', reject);
    req.write(body);
    req.end();
  });
}

export const handler = async (event: any): Promise<void> => {
  try {
    console.log('event:', event);
    const jsonBodyObject = JSON.parse(event.body);
    const query = jsonBodyObject.event?.text || '';

    await connectToServer();
    await processQuery(query);
} catch (error) {
    console.error('Lambda実行エラー:', error);
    throw error;
  } finally {
    await cleanup();
  }

  console.log('event:', event);
};

MCPサーバーの実装はこちらの記事ににまとめています。
Lambdaに配置して、関数URLを発行しMCP_SERVER_URLに追記。

Slackのアプリの開発は割愛。
WebhookURLを発行しSLACK_WEB_HOOK_URLに追記。

おわりに

本構成では Slack → Lambda (MCPクライアント) → Lambda (MCPサーバー) → Slack の流れでやり取りを完結できます。

Discussion