MCP serverを作っていると、「このserverにはどのディレクトリを見せればよいのか」で迷います。旧来のMCPでは、clientがroots/listでworkspaceのrootをserverへ渡せました。便利に見えますが、2026-07-28以降の設計では、そのまま新規採用しない方が安全です。
理由は単純です。rootsは「どこを見てよいかのヒント」であって、アクセス制御そのものではないからです。さらに2026-07-28仕様ではrootsがdeprecatedになり、server-initiated requestはMRTRへ寄せる流れになりました。この記事では、MCP rootsを新しく増やさず、既存のroots/list依存をtool parameters、resource URIs、server configurationへ移す設計だけを扱います。
2026-07-28全体の移行順は MCP 2026-07-28 移行ガイド、MRTRのinput_required / inputResponsesは MCP MRTR 設計ガイド、未信頼入力とwrite toolの分離は MCP prompt injection 対策ガイド、顧客データや社内docsの混在判断は AIエージェント データ境界設計ガイド に分けます。
先に結論: rootsは権限ではなく、参照範囲のヒント
私なら、2026-07-28以降のMCP serverでは次の方針にします。
| 状況 | 判断 |
|---|---|
| 新規serverで作業対象が必要 | roots/listではなく、tool parameterかserver configurationで明示する |
| 既存clientがrootsを返す | 互換として読むが、権限判定には使わない |
| ユーザーが毎回ファイルを選ぶ | MRTRで追加入力を求め、requestStateへbindする |
| 固定の社内repositoryだけ読む | server configurationとdeployment権限で固定する |
| 生成物から別pathへ書く | roots外拒否ではなく、allowlistと承認で止める |
rootsを「この範囲なら安全」と見なすと事故ります。server側でpath traversalを検証し、client側で公開してよいrootを絞り、実際の読み書き権限はOS、token、tool policy、承認UIで別に持つ必要があります。
なぜrootsを新規採用しないのか
MCPの公式仕様では、Roots featureは2026-07-28でdeprecatedです。少なくとも12か月は仕様に残りますが、新規実装では採用せず、既存実装は移行する前提です。移行先として示されているのは、directories / filesをtool parameters、resource URIs、server configurationで渡す形です。
もう1つ重要なのは、rootsの性質です。公式docsでも、rootsはserverへ関連ディレクトリを知らせるための情報であり、アクセス制御機構ではないと説明されています。つまり、serverがroots外を読まない保証はprotocolだけでは作れません。
ここを誤解すると、次のような設計になります。
bad_assumption:
roots:
- file:///repo/frontend
server_behavior:
- roots配下なら自動で読んでよい
- roots配下ならwriteも許可してよい
- rootsがあるのでpath validationは省略する
これは危険です。rootsが返ったことと、そのpathへ安全にアクセスしてよいことは別です。特にremote MCPやmulti-tenant MCPでは、clientが返したURI、server上のfilesystem、実際のcredential scopeが一致するとは限りません。
既存roots依存を棚卸しする
まず、codeとtraffic logからroots/list依存を一覧化します。
rg "roots/list|clientCapabilities.*roots|notifications/roots|rootUri|workspaceRoot" .
棚卸しでは、単に呼び出し箇所を数えるだけでは足りません。次の情報を1行ずつ残します。
mcp_roots_inventory:
server: docs-search-mcp
observed_protocols:
- "2025-11-25"
- "2026-07-28"
roots_usage:
- method: roots/list
purpose: "repository内の検索対象を推定する"
fallback: "tool argumentのrepoPath"
reads_files: true
writes_files: false
trusts_roots_for_authz: false
risk:
path_traversal: medium
tenant_mixup: low
prompt_injection_bridge: medium
migration_owner: platform
特に見るべきなのは、trusts_roots_for_authzです。ここがtrueなら、単なるdeprecated対応ではなく権限設計の修正です。
移行先1: tool parametersに作業対象を渡す
ユーザーやagentが「このrepositoryだけ検索して」と明示するなら、tool parameterに寄せるのが一番扱いやすいです。
{
"name": "search_repo",
"arguments": {
"repoId": "frontend",
"path": "src/components",
"query": "payment validation"
}
}
この形にすると、tool call単位で作業対象が見えます。modelもreviewerも、どのpathを対象にしたかを後から追えます。
ただし、pathをそのままfilesystem pathとして信じてはいけません。server側では必ず正規化します。
import fs from "node:fs/promises";
import path from "node:path";
const allowedRepos = {
frontend: "/srv/workspaces/frontend",
backend: "/srv/workspaces/backend",
} as const;
async function resolveWorkspacePath(
repoId: keyof typeof allowedRepos,
inputPath: string,
) {
const base = await fs.realpath(allowedRepos[repoId]);
const candidate = path.resolve(base, inputPath);
const canonical = await fs.realpath(candidate);
if (!canonical.startsWith(base + path.sep) && canonical !== base) {
throw new Error("path outside allowed repository");
}
return canonical;
}
この例で重要なのは、文字列上のprefix一致だけで閉じないことです。許可済みworkspace内に外部pathへのsymlinkがあると、path.resolve()だけではlink-to-outside/fileを許可してしまいます。実際のread/write前にcanonical pathを検証するか、symlinkを辿らないdirectory traversalへ寄せます。
もう1つ重要なのは、clientからfile:// URIを受け取らないことです。clientが自由にfilesystem URIを渡せる形にすると、server上のpathとclient上のpathの対応が曖昧になります。remote serverなら、repoIdやworkspaceIdのようなserver側で解決できるIDへ寄せます。
移行先2: resource URIで参照対象を表す
ファイルそのものではなく、docs、issue、design artifact、log bundleのような参照対象を渡すなら、resource URIに寄せます。
{
"name": "summarize_resource",
"arguments": {
"resourceUri": "mcp-resource://docs/frontend/payment-flow"
}
}
resource URIにすると、serverは実体の保存場所を隠せます。filesystem pathをmodelへ見せず、authorization、cache、redaction、auditをserver側で管理できます。
私なら、次のように扱います。
| 対象 | rootsではなく使うもの |
|---|---|
| repository内の相対path | repoId + relative path |
| 共有docs | resourceUri |
| 顧客別データ | tenantIdにbindされたresourceUri |
| build log | 短命なartifactUri |
| 手元のローカルファイル | client側で明示uploadまたはtool parameter化 |
file:// URIは便利ですが、remote serverでは特に危険です。clientのfile:///Users/...とserverの/srv/...は同じ意味ではありません。
移行先3: server configurationで固定する
社内のread-only search serverやdocs serverなら、毎回rootsを聞く必要はありません。server configurationで対象を固定します。
mcp_server:
name: internal-docs-search
mode: read_only
workspaces:
- id: frontend
base_path: /srv/indexes/frontend
allowed_operations:
- search
- read_snippet
- id: handbook
base_path: /srv/indexes/handbook
allowed_operations:
- search
この形は地味ですが、組織運用では強いです。deployment reviewで「このserverは何を読めるか」を確認でき、runtimeでclientが広いrootを返してもscopeが広がりません。
設定固定にすると、ユーザーごとのworkspace選択ができないという弱点はあります。そこで、workspaceIdだけをtool parameterで受け、実pathはserver configurationで解決する形にします。
{
"name": "search_workspace",
"arguments": {
"workspaceId": "frontend",
"query": "checkout retry policy"
}
}
こうすると、modelがpathを作らず、serverが許可済みworkspaceだけを解決します。
MRTRでroots/listを互換する場合
2026-07-28では、旧来のserver-initiated requestはMRTRのinput_requiredへ寄せます。rootsも例外ではありません。互換が必要なら、serverは処理中にroots/listを直接投げるのではなく、追加入力として要求します。
{
"resultType": "input_required",
"inputRequests": {
"workspace_roots": {
"method": "roots/list"
}
},
"requestState": "signed-short-lived-state"
}
clientは元のrequestをretryし、inputResponsesへrootsを付けて返します。
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "analyze_workspace",
"arguments": {
"scope": "current-repo"
},
"inputResponses": {
"workspace_roots": {
"roots": [
{
"uri": "file:///home/user/repos/frontend",
"name": "Frontend"
}
]
}
},
"requestState": "signed-short-lived-state"
}
}
ただし、この互換経路は新規設計の中心に置きません。使うなら、次の条件を満たす場合だけです。
- clientがroots capabilityを明示している
requestStateが短命で、元tool、arguments、user、tenantへbindされている- rootsをpath hintとしてだけ扱い、authorizationには使わない
- rootsが返らなくても縮退できる
- roots結果を監査ログへ保存しすぎない
特に5は見落とされます。file:///Users/...のようなローカルpathには、ユーザー名、組織名、顧客名が入ることがあります。監査に必要な粒度へ丸めます。
path validationを二重に置く
roots移行で一番大事なのは、代替入力の形ではなくpath validationです。私ならclient側とserver側で責務を分けます。
| 場所 | やること |
|---|---|
| client | ユーザーに見せてよいrootだけを選ばせる |
| gateway | tenant、server、tool、methodの組み合わせを拒否できるようにする |
| server | pathを正規化し、許可済みbaseから出たら拒否する |
| tool | read/writeごとに承認とaudit eventを分ける |
| reviewer | 実際に読んだpathと出力に混ざった内容を確認する |
rootsを使っていた頃の実装には、「rootがあるから安全」という暗黙の前提が残りがちです。移行時には、あえて次のテストを入れます。
const cases = [
"../secrets.env",
"src/../../.ssh/id_rsa",
"/etc/passwd",
"subdir/%2e%2e/%2e%2e/secrets",
"allowed/../allowed/file.md",
];
通すべきpathと拒否すべきpathを明示しないと、実装者ごとに判断が割れます。
write toolではrootsに頼らない
read-only searchなら、rootsの代替は比較的簡単です。危ないのはwrite toolです。
dangerous_tool:
name: update_file
arguments:
path: "src/config.ts"
patch: "..."
このtoolで「roots内だからwriteしてよい」と判断してはいけません。writeは別の承認境界です。
私なら、write toolでは次のようにします。
write_policy:
allowed_workspace_ids:
- frontend
allowed_paths:
- "src/**"
- "tests/**"
denied_paths:
- ".env*"
- "**/secrets/**"
- "**/*.pem"
requires_human_approval:
- "package.json"
- "infra/**"
output:
include_diff: true
include_audit_event_id: true
rootsはこのpolicyの入力の1つにもなりません。許可済みworkspace、path pattern、risk tier、承認、diff reviewを別に持ちます。
監査ログにはraw rootをそのまま残さない
rootsやpath parameterは、監査には必要です。ただし、raw pathを全部残すと別の問題が出ます。
{
"event": "mcp_workspace_scope_resolved",
"server": "docs-search-mcp",
"tool": "search_workspace",
"workspaceId": "frontend",
"pathScope": "src/components",
"source": "tool_parameter",
"rootCompatibilityUsed": false,
"requestStateId": "mrs_20260825_001",
"decision": "allowed"
}
監査ログでは、次を分けます。
| 残す | 残し方 |
|---|---|
| workspace識別子 | workspaceIdやrepoId |
| path範囲 | 相対pathまたは分類済みscope |
| raw filesystem path | 原則保存しない。incident hold時だけ制限付きで保存 |
| roots互換の利用有無 | booleanで残す |
| 拒否理由 | outside_allowed_baseなど定型値 |
「何を読んだか」は追える必要があります。ただし、ユーザーのローカルpathや社内構成を余計にばらまく必要はありません。
移行チェックリスト
既存serverを直すなら、この順で進めます。
roots/list、notifications/roots/list_changed、clientCapabilities.rootsの利用箇所を検索する- rootsをauthorizationとして使っている箇所を最優先で修正する
- 新規toolでは
repoId、workspaceId、relativepath、resourceUriへ寄せる - remote serverでは
file://URIをserver側の実pathとして解釈しない - path normalizationとdeny patternのテストを追加する
- write toolはrootsから切り離し、別policyと承認にする
- MRTR互換はcapability確認、短命
requestState、縮退動作を必須にする - 監査ログはraw pathを残しすぎず、scopeとdecisionを残す
よくある誤解
rootsがdeprecatedなら、ファイル操作MCPは作れないのか
作れます。変わるのは、workspace範囲の渡し方です。tool parameter、resource URI、server configurationで対象を明示し、実際の権限は別に検証します。
rootsを返したclientが悪いのか
違います。clientは関連rootを示しているだけです。serverがそれを権限として扱ったり、path validationを省略したりする設計が危険です。
local MCPなら気にしなくてよいのか
localでも必要です。ローカルpathには秘密情報や別repositoryが同居しやすく、prompt injectionを受けたagentがread toolからwrite toolへつなぐ事故も起きます。
MRTRでroots/listを使えるなら移行しなくてよいのか
互換のために使えることと、新規設計の中心にすることは別です。2026-07-28以降は、必要な対象をtool callに明示する設計へ寄せる方がレビューしやすいです。
まとめ
MCP rootsの移行は、deprecated APIを別APIへ置き換えるだけの作業ではありません。workspace、filesystem、resource、tenant、承認、監査をどこで結びつけるかを見直す作業です。
私なら、新規serverではroots/listを使いません。read-onlyならworkspaceIdとresource URI、write toolならpath policyと人間承認、既存client互換だけMRTRで受ける。この形にすると、MCP serverをstatelessに寄せても、作業対象と権限境界をレビュー可能なまま残せます。