組織は、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 の実務知識
GraphQL Analytics API の gatewayHttpRequestsAdaptiveGroups データセットは、集計済みの Gateway HTTP ログデータを提供します。このデータセットで MCP 関連のトラフィックパターンをクエリします。
- ディメンション:
httpHost,httpRequestURI,action,users,dlpProfiles - 期間: 最大 30 日分の履歴データ
- グループ化: ディメンションの値ごとに結果を集計します
- フィルタ:
OR、AND、like演算子に対応します
MCP トラフィックは、次の 3 つのシグナルで識別できます。
- ドメインパターン:
mcpを含むホスト名(例:mcp.datadog.com) - URL パス:
/mcp、/mcp/sse、/sseなどの標準 MCP エンドポイント - 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 のタイムスタンプに置き換えます。
応答の各グループは、特定の httpHost と action の組み合わせに対する集計トラフィックを表します。結果を解析し、ブロックされていない 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 サーバーに接続しているかを示します
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 の制限に注意してください。
- 正規表現構文 — Rust の正規表現(JavaScript や PCRE とは少し異なります)
- スキャン深度 — リクエスト本文の先頭 1,024 バイトだけ
- POST のみ — DLP がスキャンするのは
POSTリクエストだけです - パフォーマンス — 壊滅的なバックトラッキングを避けるため、正規表現は効率的である必要があります
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 に一致します
すべての検出パターンを含むカスタム 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 トークンに置き換えます。
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 プロファイルが一致したときに発動します。
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.com、api.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 の実際のドメインに置き換えます。