0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MCP(Model Context Protocol)で外部APIを安全に呼び出すための認証・権限設計と実装パターン

0
Posted at

読者が抱える課題

LLM(大規模言語モデル)やAIエージェントが外部APIを呼び出して処理を実行する仕組みとして、Anthropicが提唱する MCP(Model Context Protocol) の活用が進んでいます。しかし、AIエージェントにAPIの操作権限を付与する際、以下のようなセキュリティ上の課題が発生します。

  • APIキーやアクセス delegated token をMCPサーバー側またはクライアント側でどのように保持・管理すべきか分からない
  • AIエージェントのプロンプトインジェクション等による過剰な操作(削除APIの意図しない実行など)を回避したい
  • ユーザーごとのアクセス権限を保持したまま安全にツールを実行させたい

本記事では、MCPアーキテクチャにおける認証・認可のベストプラクティス、権限設計モデル、およびNode.jsによる具体的な実装例を解説します。


この記事で分かること

  • MCPにおける認証(Authentication)と認可(Authorization)の基本分離構造
  • APIキーのプロキシ管理パターンとユーザー委任(OAuth 2.0)パターン
  • Minimum Privilege(最小権限の原則)を適用したMCPツール設計
  • 導入時に確認すべきセキュリティ・チェックリスト

対象読者・前提条件

  • 対象読者: LLMを活用した社内ツールやAIエージェントの構築を担当するWebエンジニア、インフラ・セキュリティ担当者
  • 前提条件: REST API、OAuth 2.0、TypeScript/Node.jsの基本知識を有していること

※MCPの仕様は急速に更新される可能性があるため、実装時には公式のMCP仕様ドキュメント(modelcontextprotocol.io)も合わせてご確認ください。


MCP認証・権限設計の2大パターン

MCPにおいて、認証情報(API Key / Token)をどこで保持し、誰の権限として外部APIを叩くかは非常に重要です。主に以下の2つのパターンが存在します。

比較表

項目 パターンA: サーバー統合型(システム共通権限) パターンB: ユーザー委任型(OAuth/Passthrough)
概要 MCPサーバーが共通APIキーを保持してリクエストを代行 ユーザー自身のトークンをMCPツール呼び出し時に伝播
利用ユースケース 社内DBの参照、システム共通の通知送信など GitHub、Slack、Google Driveなどの個人紐付け操作
セキュリティリスク 1つのプロンプト突破で全体権限が危険に晒される トークンの有効期限切れやハンドシェイクの管理が必要
実装難易度 低(環境変数にAPIキーをセットするのみ) 中〜高(認証フローとトークンストアが必要)

実務において、更新・削除権限を伴うリソース操作には パターンB(またはコンテキスト制限を強めたパターンA) を採用することが推奨されます。


具体的な実装例(TypeScript / MCP SDK)

以下は、MCPツール定義時に「読み取り専用」と「変更操作」で権限を分離し、実行時にコンテキストチェックを行う実装例です。

※以下のコードは概念および動作構造を示すための実装例です。実際の運用環境に導入する際は、利用フレームワークや環境に合わせて検証を行ってください。

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

// 外部API実行用クライアント(独自実装の想定例)
interface UserContext {
  userId: string;
  scopes: string[];
}

// ツールごとの必要スコープ定義
const REQUIRED_SCOPES: Record<string, string> = {
  "get_user_data": "read:user",
  "update_user_status": "write:user"
};

const server = new Server(
  {
    name: "secure-api-mcp-server",
    version: "1.0.0",
  },
  {
    capabilities: {
      tools: {},
    },
  }
);

// 1. 利用可能なツールの宣伝(読み取り権限と書き込み権限を明示)
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "get_user_data",
        description: "ユーザーの基本情報を取得します(読み取り専用)",
        inputSchema: {
          type: "object",
          properties: {
            userId: { type: "string" }
          },
          required: ["userId"]
        }
      },
      {
        name: "update_user_status",
        description: "ユーザーのステータスを変更します(書き込み権限要)",
        inputSchema: {
          type: "object",
          properties: {
            userId: { type: "string" },
            status: { type: "string" }
          },
          required: ["userId", "status"]
        }
      }
    ]
  };
});

// 2. ツール実行時の認可判定(擬似コード)
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;
  
  // 環境変数やヘッダー等からセッション情報を抽出する想定(独自実装部)
  const userContext: UserContext = getContextFromEnvironment();

  // スコープのチェック
  const requiredScope = REQUIRED_SCOPES[name];
  if (requiredScope && !userContext.scopes.includes(requiredScope)) {
    throw new Error(`Permission Denied: Missing required scope [${requiredScope}]`);
  }

  switch (name) {
    case "get_user_data":
      // 安全な読み取り処理の実行
      return { content: [{ type: "text", text: `User Data for ${args?.userId}` }] };
      
    case "update_user_status":
      // 書き込み処理の実行
      return { content: [{ type: "text", text: `Updated ${args?.userId} status to ${args?.status}` }] };
      
    default:
      throw new Error(`Unknown tool: ${name}`);
  }
});

function getContextFromEnvironment(): UserContext {
  // 認証コンテキストを取得する処理のダミー実装
  return {
    userId: process.env.USER_ID || "guest",
    scopes: (process.env.USER_SCOPES || "read:user").split(",")
  };
}

導入時の注意点・失敗しやすいポイント

1. プロンプトインジェクションによる間接的権限昇格

AIエージェントが外部から取得したテキスト(メール本文やWebページ)に悪意ある指示が含まれていた場合、エージェントが「削除API」などの高リスクツールを誤って呼び出す危険があります。

対策: 高リスクな操作(Destructive Operations)を行うツールに対しては、MCPクライアント側(LLMの呼び出し元)で Human-in-the-loop(人間の承認ステップ) を挟む設計にしてください。

2. 環境変数への認証情報ベタ書き

MCPサーバーの構成ファイル(mcpServers 設定ファイル等)に平文で秘密鍵やAPI Tokenを直接記述してGit等にコミットしてしまうケースが散見されます。

対策: 認証情報はシークレット管理ツール(AWS Secrets Manager, HashiCorp Vault等)から動的に取得するか、実行時環境変数経由でのみ注入してください。


実務用 セキュリティ設計チェックリスト

設計・レビュー時に以下の項目を満たしているか確認してください。

  • 最小権限: AIエージェントに渡すAPIトークンのスコープは必要最小限(Read-Onlyが標準)になっているか
  • 承認フロー: 破壊的変更(DELETE, UPDATE, 送金等)を行うツールには人間の確認(Human-in-the-loop)が必須になっているか
  • ログと監査: MCPツールが実行された際、「誰が(User)」「どのLLMの指示で」「何のパラメータを渡して」実行したかがログに残るか
  • タイムアウト設定: 長時間動作するエージェントに対して、トークンの有効期限(TTL)を適切に短く設定しているか
  • 入力バリデーション: MCPサーバー側で、LLMから渡された引数の型・形式チェック(ZodやJSON Schemaなど)を厳格に行っているか

まとめ

MCPを実務で安全に運用するためには、「LLMの判断を全面的に信頼しない」というゼロトラスト前提の設計が必要です。MCPサーバーを単なるAPIの土台とするのではなく、権限の分離・検証・ログ監査の層として位置付けることで、安全なAIエージェント活用が可能になります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?