Skip to content

非公式本サイトは非公式の日本語ドキュメントであり、Cloudflare 公式サイトではありません。最新情報はdevelopers.cloudflare.comをご確認ください。

Gateway ログで MCP トラフィックを検出する

最終更新 Markdown で表示Agent セットアップ

組織は、Model Context Protocol(MCP)トラフィックの可視性が不足していることがあり、従業員が IT の監督外でリモート MCP サーバーに接続できてしまいます。こうした接続は、機密の社内データや認証情報の持ち出し、ツールインジェクション攻撃、ソフトウェアサプライチェーンのリスクにつながります。

IT 管理者として、管理下のユースケースは支えつつ、未承認のデータ持ち出しを防ぐためにシャドウ MCP トラフィックを特定したいとします。このチュートリアルでは、Cloudflare GraphQL Analytics API を使い、Gateway の HTTP ログから MCP トラフィックのパターンをスキャンし、MCP の JSON-RPC メソッドを検出する DLP プロファイルを作成し、承認済みの MCP サーバーポータル向けトラフィックと「シャドウ」のリモート MCP サーバー向けトラフィックを分類します。

前提条件

  • Zero Trust 組織 がある Cloudflare アカウント
  • HTTP フィルタリングが有効で、ユーザートラフィックをプロキシしている Gateway
  • 次の権限を持つ API トークン:
    • アカウントレベルの Zero Trust: Read
    • アカウントレベルの DLP: Write
    • アカウントレベルの Gateway: Write
  • Cloudflare アカウント ID(Cloudflare ダッシュボードAccount Home で確認できます)
  • GraphQL Analytics API のクエリに慣れていること
  • TypeScript と REST API の実務知識

1. Gateway HTTP データセットを確認する

GraphQL Analytics API の gatewayHttpRequestsAdaptiveGroups データセットは、集計済みの Gateway HTTP ログデータを提供します。このデータセットで MCP 関連のトラフィックパターンをクエリします。

  • ディメンション: httpHost, httpRequestURI, action, users, dlpProfiles
  • 期間: 最大 30 日分の履歴データ
  • グループ化: ディメンションの値ごとに結果を集計します
  • フィルタ: ORANDlike 演算子に対応します

2. MCP 検出クエリを組み立てる

MCP トラフィックは、次の 3 つのシグナルで識別できます。

  1. ドメインパターン: mcp を含むホスト名(例: mcp.datadog.com
  2. URL パス: /mcp/mcp/sse/sse などの標準 MCP エンドポイント
  3. DLP 一致: リクエスト本文内の JSON-RPC メソッド(後の手順で扱います)

次の GraphQL クエリは、最初の 2 つのシグナルについて Gateway ログをスキャンします。

const query = `
  query MCPTrafficScan($accountTag: string, $since: string, $until: string) {
    viewer {
      accounts(filter: { accountTag: $accountTag }) {
        gatewayHttpRequestsAdaptiveGroups(
          filter: {
            datetime_geq: $since
            datetime_leq: $until
            OR: [
              { httpHost_like: "%mcp%" }
              { httpRequestURI_like: "%/mcp%" }
              { httpRequestURI_like: "%/sse%" }
            ]
          }
          limit: 10000
        ) {
          dimensions {
            httpHost
            action
            users
          }
          count
        }
      }
    }
  }
`;

const variables = {
	accountTag: "<YOUR_ACCOUNT_ID>",
	since: "<START_DATE>", // ISO-8601 format, for example 2025-03-08T00:00:00Z
	until: "<END_DATE>", // Up to 30 days after start date
};

const response = await fetch("https://api.cloudflare.com/client/v4/graphql", {
	method: "POST",
	headers: {
		Authorization: `Bearer ${apiToken}`,
		"Content-Type": "application/json",
	},
	body: JSON.stringify({ query, variables }),
});

const data = await response.json();
const groups =
	data.data?.viewer?.accounts?.[0]?.gatewayHttpRequestsAdaptiveGroups || [];
const query = `
  query MCPTrafficScan($accountTag: string, $since: string, $until: string) {
    viewer {
      accounts(filter: { accountTag: $accountTag }) {
        gatewayHttpRequestsAdaptiveGroups(
          filter: {
            datetime_geq: $since
            datetime_leq: $until
            OR: [
              { httpHost_like: "%mcp%" }
              { httpRequestURI_like: "%/mcp%" }
              { httpRequestURI_like: "%/sse%" }
            ]
          }
          limit: 10000
        ) {
          dimensions {
            httpHost
            action
            users
          }
          count
        }
      }
    }
  }
`;

const variables = {
	accountTag: "<YOUR_ACCOUNT_ID>",
	since: "<START_DATE>", // ISO-8601 format, for example 2025-03-08T00:00:00Z
	until: "<END_DATE>", // Up to 30 days after start date
};

const response = await fetch("https://api.cloudflare.com/client/v4/graphql", {
	method: "POST",
	headers: {
		Authorization: `Bearer ${apiToken}`,
		"Content-Type": "application/json",
	},
	body: JSON.stringify({ query, variables }),
});

const data = await response.json();
const groups =
	data.data?.viewer?.accounts?.[0]?.gatewayHttpRequestsAdaptiveGroups || [];

<YOUR_ACCOUNT_ID> を Cloudflare アカウント ID に置き換えます。<START_DATE><END_DATE> は、対象期間(最大 30 日)をカバーする ISO-8601 のタイムスタンプに置き換えます。

3. クエリ結果を処理する

応答の各グループは、特定の httpHostaction の組み合わせに対する集計トラフィックを表します。結果を解析し、ブロックされていない MCP 接続を特定します。

const hits = groups.map((group) => ({
	domain: group.dimensions.httpHost,
	requestCount: group.count,
	users: group.dimensions.users || [],
	actions: {
		allowed: group.dimensions.action === "allow" ? group.count : 0,
		blocked: group.dimensions.action === "block" ? group.count : 0,
	},
}));

const totalMCPRequests = hits.reduce((sum, h) => sum + h.requestCount, 0);
const unblockedHits = hits.filter((h) => h.actions.allowed > 0);

console.log(`Found ${totalMCPRequests} MCP requests`);
console.log(`${unblockedHits.length} destinations are unblocked`);
interface MCPTrafficHit {
	domain: string;
	requestCount: number;
	users: string[];
	actions: {
		allowed: number;
		blocked: number;
	};
}

const hits: MCPTrafficHit[] = groups.map((group: any) => ({
	domain: group.dimensions.httpHost,
	requestCount: group.count,
	users: group.dimensions.users || [],
	actions: {
		allowed: group.dimensions.action === "allow" ? group.count : 0,
		blocked: group.dimensions.action === "block" ? group.count : 0,
	},
}));

const totalMCPRequests = hits.reduce((sum, h) => sum + h.requestCount, 0);
const unblockedHits = hits.filter((h) => h.actions.allowed > 0);

console.log(`Found ${totalMCPRequests} MCP requests`);
console.log(`${unblockedHits.length} destinations are unblocked`);

データから得られる主なポイント:

  • ブロックされていないトラフィックaction = allow)— 調査またはブロックが必要な、稼働中の MCP 接続です
  • ブロックされたトラフィックaction = block)— 既存のポリシーが機能しています
  • ユーザーの帰属 — どの従業員が MCP サーバーに接続しているかを示します

4. MCP JSON-RPC 検出用の DLP プロファイルを作成する

Gateway の HTTP ポリシーはドメインと URL パスに一致できますが、リクエスト本文は検査できません。DLP プロファイルは POST 本文の内容をパターンでスキャンします。MCP は HTTP 上の JSON-RPC を使い、検出可能な特徴がいくつかあるため、シャドウ MCP の検出に役立ちます。

すべての MCP リクエストには "method" フィールドがあります。

{
	"jsonrpc": "2.0",
	"id": 1,
	"method": "tools/call",
	"params": { "name": "read_file", "arguments": { "path": "/etc/passwd" } }
}

攻撃者は、ドメインベースやパスベースのルールに引っかからない非標準ドメイン(例: internal-tools.company.com/api/assistant)で MCP サーバーを動かせます。POST 本文を DLP でスキャンし、"method": "tools/call" やその他の MCP 固有パターンを検出すると、MCP トラフィックをより確実に保護できます。

DLP の制約を確認する

検出パターンを作る前に、次の DLP の制限に注意してください。

  • 正規表現構文 — Rust の正規表現(JavaScript や PCRE とは少し異なります)
  • スキャン深度 — リクエスト本文の先頭 1,024 バイトだけ
  • POST のみ — DLP がスキャンするのは POST リクエストだけです
  • パフォーマンス — 壊滅的なバックトラッキングを避けるため、正規表現は効率的である必要があります

MCP 検出パターンを作る

MCP の指標は、JSON-RPC の method フィールドにあります。次の正規表現パターンは、中核となる MCP プロトコルメソッドをカバーします。

const DLP_REGEX_PATTERNS = [
	{
		name: "MCP Initialize Method",
		regex: '"method"\\s{0,5}:\\s{0,5}"initialize"',
	},
	{
		name: "MCP Tools Call",
		regex: '"method"\\s{0,5}:\\s{0,5}"tools/call"',
	},
	{
		name: "MCP Tools List",
		regex: '"method"\\s{0,5}:\\s{0,5}"tools/list"',
	},
	{
		name: "MCP Resources Read",
		regex: '"method"\\s{0,5}:\\s{0,5}"resources/read"',
	},
	{
		name: "MCP Resources List",
		regex: '"method"\\s{0,5}:\\s{0,5}"resources/list"',
	},
	{
		name: "MCP Prompts List",
		regex: '"method"\\s{0,5}:\\s{0,5}"prompts/(list|get)"',
	},
	{
		name: "MCP Sampling Create Message",
		regex: '"method"\\s{0,5}:\\s{0,5}"sampling/createMessage"',
	},
	{
		name: "MCP Protocol Version",
		regex: '"protocolVersion"\\s{0,5}:\\s{0,5}"202[4-9]',
	},
	{
		name: "MCP Notifications Initialized",
		regex: '"method"\\s{0,5}:\\s{0,5}"notifications/initialized"',
	},
	{
		name: "MCP Roots List",
		regex: '"method"\\s{0,5}:\\s{0,5}"roots/list"',
	},
];
const DLP_REGEX_PATTERNS = [
	{
		name: "MCP Initialize Method",
		regex: '"method"\\s{0,5}:\\s{0,5}"initialize"',
	},
	{
		name: "MCP Tools Call",
		regex: '"method"\\s{0,5}:\\s{0,5}"tools/call"',
	},
	{
		name: "MCP Tools List",
		regex: '"method"\\s{0,5}:\\s{0,5}"tools/list"',
	},
	{
		name: "MCP Resources Read",
		regex: '"method"\\s{0,5}:\\s{0,5}"resources/read"',
	},
	{
		name: "MCP Resources List",
		regex: '"method"\\s{0,5}:\\s{0,5}"resources/list"',
	},
	{
		name: "MCP Prompts List",
		regex: '"method"\\s{0,5}:\\s{0,5}"prompts/(list|get)"',
	},
	{
		name: "MCP Sampling Create Message",
		regex: '"method"\\s{0,5}:\\s{0,5}"sampling/createMessage"',
	},
	{
		name: "MCP Protocol Version",
		regex: '"protocolVersion"\\s{0,5}:\\s{0,5}"202[4-9]',
	},
	{
		name: "MCP Notifications Initialized",
		regex: '"method"\\s{0,5}:\\s{0,5}"notifications/initialized"',
	},
	{
		name: "MCP Roots List",
		regex: '"method"\\s{0,5}:\\s{0,5}"roots/list"',
	},
];

パターンの説明:

  • \\s{0,5} — 空白を 0〜5 文字許し、minify された JSON と整形された JSON の両方に対応します
  • "method" — JSON では必須のため、二重引用符はリテラルです
  • "tools/call" — 正確な MCP メソッド名に一致します
  • 202[4-9] — MCP プロトコルバージョン 2024 から 2029 に一致します

API で DLP プロファイルを作成する

すべての検出パターンを含むカスタム DLP プロファイルを作成するには、POST リクエストを送ります。

const dlpProfile = {
	name: "MCP-Shield: MCP JSON-RPC Detection",
	description: "Detects MCP protocol JSON-RPC methods in HTTP request bodies.",
	type: "custom",
	entries: DLP_REGEX_PATTERNS.map((p) => ({
		name: p.name,
		enabled: true,
		pattern: {
			regex: p.regex,
			validation: "luhn",
		},
	})),
};

const response = await fetch(
	`https://api.cloudflare.com/client/v4/accounts/${accountId}/gateway/rules`,
	{
		method: "POST",
		headers: {
			Authorization: `Bearer ${apiToken}`,
			"Content-Type": "application/json",
		},
		body: JSON.stringify(dlpRule),
	},
);

const data = await response.json();
if (data.success) {
	console.log(`Created DLP profile: ${data.result.id}`);
}
const dlpProfile = {
	name: "MCP-Shield: MCP JSON-RPC Detection",
	description: "Detects MCP protocol JSON-RPC methods in HTTP request bodies.",
	type: "custom",
	entries: DLP_REGEX_PATTERNS.map((p) => ({
		name: p.name,
		enabled: true,
		pattern: {
			regex: p.regex,
			validation: "luhn",
		},
	})),
};

const response = await fetch(
	`https://api.cloudflare.com/client/v4/accounts/${accountId}/gateway/rules`,
	{
		method: "POST",
		headers: {
			Authorization: `Bearer ${apiToken}`,
			"Content-Type": "application/json",
		},
		body: JSON.stringify(dlpRule),
	},
);

const data = await response.json();
if (data.success) {
	console.log(`Created DLP profile: ${data.result.id}`);
}

${accountId} を Cloudflare アカウント ID に、${apiToken} を API トークンに置き換えます。

Gateway ルールで DLP プロファイルを参照する

DLP プロファイルができたら、そのプロファイルに一致するリクエストをブロックする Gateway HTTP ポリシーを作成します。

const dlpRule = {
	name: "MCP-Shield: Block MCP JSON-RPC via DLP",
	description: "Blocks requests with MCP JSON-RPC patterns detected by DLP",
	precedence: 85,
	enabled: true,
	action: "block",
	filters: ["http"],
	traffic:
		'any(http.request.body.scan.dlp.profiles[*] == "MCP-Shield: MCP JSON-RPC Detection")',
};
const dlpRule = {
	name: "MCP-Shield: Block MCP JSON-RPC via DLP",
	description: "Blocks requests with MCP JSON-RPC patterns detected by DLP",
	precedence: 85,
	enabled: true,
	action: "block",
	filters: ["http"],
	traffic:
		'any(http.request.body.scan.dlp.profiles[*] == "MCP-Shield: MCP JSON-RPC Detection")',
};

このルールは、リクエスト本文内のいずれかの正規表現パターンに DLP プロファイルが一致したときに発動します。

5. ポータルトラフィックとシャドウ MCP トラフィックを分類する

Cloudflare の MCP Server Portals は、組織内で承認された MCP アクセス向けの管理インフラを提供します。内容は次のとおりです。

  • 管理下のアクセス — IT チームが管理する、集中化された MCP インフラです
  • 監査証跡 — すべての MCP リクエストが、ユーザー帰属付きで Gateway 経由で記録されます
  • ポリシー適用 — 認証と DLP を含む Zero Trust ポリシーが自動で適用されます
  • 承認済みツール — セキュリティが審査した、厳選された MCP ツールとリソースです

Gateway ログを分析するときは、次の 2 種類の MCP トラフィックを区別すると役立ちます。

トラフィックの種類 特徴 リスクレベル 対応
MCP Portal トラフィック httpHost がポータルドメインに一致する(例: mcp.yourcompany.com または mcp-portal.pages.dev 承認済み 監視
シャドウ MCP トラフィック httpHost がどのポータルドメインにも一致しない(例: mcp.datadog.comapi.stripe.com/mcp 調査が必要 ブロック、リダイレクト、またはレビュー

クエリ結果を処理する の処理を拡張し、ホスト名を承認済みポータルドメインの一覧と比較してトラフィックを分類します。

const portalDomains = [
	"mcp.yourcompany.com",
	"mcp-portal.pages.dev",
	"approved-mcp.workers.dev",
];

const results = groups.map((group) => {
	const isPortalTraffic = portalDomains.some((domain) =>
		group.dimensions.httpHost.includes(domain),
	);

	return {
		domain: group.dimensions.httpHost,
		requestCount: group.count,
		users: group.dimensions.users || [],
		trafficType: isPortalTraffic ? "portal" : "shadow",
		riskLevel: isPortalTraffic ? "low" : "high",
	};
});

const portalTraffic = results.filter((r) => r.trafficType === "portal");
const shadowTraffic = results.filter((r) => r.trafficType === "shadow");

console.log("Portal traffic:", portalTraffic);
console.log("Shadow MCP traffic:", shadowTraffic);
const portalDomains = [
	"mcp.yourcompany.com",
	"mcp-portal.pages.dev",
	"approved-mcp.workers.dev",
];

const results = groups.map((group) => {
	const isPortalTraffic = portalDomains.some((domain) =>
		group.dimensions.httpHost.includes(domain),
	);

	return {
		domain: group.dimensions.httpHost,
		requestCount: group.count,
		users: group.dimensions.users || [],
		trafficType: isPortalTraffic ? "portal" : "shadow",
		riskLevel: isPortalTraffic ? "low" : "high",
	};
});

const portalTraffic = results.filter((r) => r.trafficType === "portal");
const shadowTraffic = results.filter((r) => r.trafficType === "shadow");

console.log("Portal traffic:", portalTraffic);
console.log("Shadow MCP traffic:", shadowTraffic);

portalDomains 配列を、承認済み MCP Server Portals の実際のドメインに置き換えます。

関連リソース

役に立ちましたか?