실제 터미널 캡처: MCP filesystem 서버에 허용 폴더(allowed)만 열어 두고 read_text_file을 호출하자 allowed/note.txt만 읽히고 outside/secret.txt, /etc/passwd, ../ 경로는 Access denied가 나온 화면과 글 제목

MCP filesystem Access denied: 허용 폴더 밖 읽기는 막힙니다

MCP filesystem 서버에 폴더 하나를 열어 주면, 정말 그 폴더 안만 읽을까요. 운영 도구에 붙이려면 이 질문에 답이 있어야 해서, 아래에서는 Debian에서 돌린 재현·해결·점검 스크립트의 출력으로 답을 가립니다. 허용 폴더 밖 파일 읽기가 실제로 거부되는지, 허용 범위를 바꾸면 어떤 파일이 열리는지입니다. 작업 자료는 읽게 하되 같은 호스트의 다른 파일로 접근이 번지지 않아야 하기 때문입니다. @modelcontextprotocol/server-filesystem 1.0.0에 허용 폴더 하나를 주고 read_text_file을 불러, 정상 읽기와 Access denied - path outside allowed directories를 같은 조건에서 나란히 놓았습니다.

순서는 거부를 먼저 재현하는 것부터입니다. 그다음 허용 폴더 안 경로로 읽는 방법과, 필요한 폴더를 서버 인자에 더해 읽는 방법을 성공 출력으로 확인합니다. 끝으로 심볼릭 링크, Windows 형태 경로, 클라이언트의 Roots 교체, 인자 없이 띄운 경우를 같은 패키지로 비교합니다.

MCP SDK 2.3.0 FastMCP ModuleNotFoundError와 MCPServer 전환 글은 FastMCP 임포트가 첫 줄에서 멈추는 원인과 MCPServer로 옮기는 방법을 설명합니다.

본문의 코드는 재현·해결·점검 스크립트에서 잘라 온 것입니다. 클라이언트 app/call.mjs는 서버를 세 가지 모드로 띄웁니다. Roots를 광고하지 않는 cli 모드, 루트 하나를 돌려주는 roots 모드, 디렉터리를 아예 넘기지 않는 none 모드입니다. 숫자와 오류 문장은 각 출력 블록과 같고, 설치에 걸린 시간이나 레지스트리 time.modified처럼 다시 돌리면 바뀌는 값은 해당 블록을 보시면 됩니다.

허용 폴더 밖을 읽으면 read_text_file이 돌려주는 Access denied

표의 실행 환경과 패키지 버전은 실행 로그에 찍힌 값 그대로입니다. 서버가 스스로 밝힌 이름은 secure-filesystem-server이고, 버전 문자열은 레지스트리의 1.0.0과 같습니다. 같은 증상을 다른 곳에서 비교하려면 이 정도는 맞춰 두어야 합니다. 패키지 이름만 적어 두면 부족하고, 실제 설치된 SDK와 서버 응답의 버전까지 함께 남겨 두는 편이 좋습니다.

항목출력 값
실행일2026-10-14
OSDebian GNU/Linux 13 (trixie)
커널Linux 6.12.94+ x86_64
Node.jsv20.19.2
npm9.2.0
filesystem 패키지1.0.0
서버 이름secure-filesystem-server
서버 버전1.0.0
SDK1.32.1
패키지가 선언한 SDK 범위^1.31.0
도구 수14

레지스트리 조회는 npm-view 문서의 명령이고, 패키지 문서는 registry.npmjs.org의 server-filesystem입니다. 두 주소는 점검 스크립트의 curl -sI에서 HTTP/2 200이었습니다. name은 @modelcontextprotocol/server-filesystem, version은 1.0.0, repository.url은 git+https://github.com/modelcontextprotocol/servers.git, license는 SEE LICENSE IN LICENSE입니다. 이 license 값은 라이선스 본문이 아니라서 조건을 풀어 쓰지 않습니다.

설치 디렉터리 app의 package.json은 filesystem 패키지 1.0.0과 SDK 1.32.1을 고정합니다. 설치된 SDK 1.32.1은 패키지가 선언한 범위 ^1.31.0 안에 있고, 이번 호출은 그 조합으로 끝났습니다. SDK 저장소 필드는 git+https://github.com/modelcontextprotocol/typescript-sdk.git이고, 같은 저장소의 https 주소도 HTTP/2 200이었습니다.

run date +%F
grep '^PRETTY_NAME=' /etc/os-release
run uname -srm
run node --version
run npm --version
run npm view @modelcontextprotocol/server-filesystem name version repository.url license time.modified
npm install --omit=dev --ignore-scripts --no-fund --no-audit

허용 폴더는 fixture/allowed 하나입니다. 비교 파일은 fixture/outside/secret.txt와, 이름이 허용 폴더로 시작하는 형제 폴더 fixture/allowed-evil/note.txt입니다. 재현 스크립트는 허용 폴더만 서버 인자로 넘기고, 아래 네 경로를 read_text_file로 읽습니다. note.txt의 본문 허용된 폴더의 메모와 secret.txt의 본문 허용 폴더 밖의 문장은 읽기가 성공한 출력에 있습니다.

cat > calls-a.json << EOF
[
  {"name":"read_text_file","arguments":{"path":"${OUTSIDE}/secret.txt"}},
  {"name":"read_text_file","arguments":{"path":"/etc/passwd"}},
  {"name":"read_text_file","arguments":{"path":"${ALLOWED}/../../fixture/outside/secret.txt"}},
  {"name":"read_text_file","arguments":{"path":"${EVIL}/note.txt"}}
]
EOF
node call.mjs "$SERVER" cli "$ALLOWED" calls-a.json

cli 모드는 클라이언트 능력에 Roots를 넣지 않으며, stderr는 서버 인자에서 받은 fixture/allowed 하나를 허용 목록으로 표시합니다. 도구 목록은 tool_count=14이고 read_text_file, list_allowed_directories와 함께 write_file, edit_file, move_file도 나옵니다. 아래 출력은 이 연결 조건에서 허용 폴더 밖 경로를 요청한 결과를 모은 것입니다. 도구가 목록에 나타나는 것과 요청한 파일에 접근할 수 있는 것은 별개이므로, 각 호출의 isError와 오류에 적힌 경로를 함께 읽습니다.

=== deny paths outside the allowed directory ===
+ date +%F
2026-10-14
+ grep ^PRETTY_NAME= /etc/os-release
PRETTY_NAME="Debian GNU/Linux 13 (trixie)"
+ uname -srm
Linux 6.12.94+ x86_64
+ node --version
v20.19.2
+ npm --version
9.2.0
+ npm view @modelcontextprotocol/server-filesystem name version repository.url license time.modified
name = '@modelcontextprotocol/server-filesystem'
version = '1.0.0'
repository.url = 'git+https://github.com/modelcontextprotocol/servers.git'
license = 'SEE LICENSE IN LICENSE'
time.modified = '2026-10-11T02:58:37.880Z'
+ mkdir -p app fixture/allowed/nested fixture/outside fixture/allowed-evil
+ cat > app/package.json << 'EOF'
+ cat > app/call.mjs << 'ENDCALLJS'
+ cd app
+ npm install --omit=dev --ignore-scripts --no-fund --no-audit
(#########⠂⠂⠂⠂⠂⠂⠂⠂⠂) ⠇ idealTree: timing idealTree Completed in 59ms(#########⠂⠂⠂⠂⠂⠂⠂⠂⠂) ⠸ idealTree: timing idealTree Completed in 59ms
up to date in 161ms
+ printf note outside and prefix files
+ cat > calls-a.json << EOF
+ cat calls-a.json
[
  {"name":"read_text_file","arguments":{"path":"/home/user/mcp/run/fixture/outside/secret.txt"}},
  {"name":"read_text_file","arguments":{"path":"/etc/passwd"}},
  {"name":"read_text_file","arguments":{"path":"/home/user/mcp/run/fixture/allowed/../../fixture/outside/secret.txt"}},
  {"name":"read_text_file","arguments":{"path":"/home/user/mcp/run/fixture/allowed-evil/note.txt"}}
]
+ node call.mjs node_modules/@modelcontextprotocol/server-filesystem/dist/index.js cli /home/user/mcp/run/fixture/allowed calls-a.json
mode=cli
sdk_version=1.32.1
sdk_repo=git+https://github.com/modelcontextprotocol/typescript-sdk.git
fs_version=1.0.0
fs_sdk_range=^1.31.0
fs_repo=https://github.com/modelcontextprotocol/servers.git
server_name=secure-filesystem-server
server_version=1.0.0
tool_count=14
tool_names=read_file,read_text_file,read_media_file,read_multiple_files,write_file,edit_file,create_directory,list_directory,list_directory_with_sizes,directory_tree,move_file,search_files,get_file_info,list_allowed_directories
CALL read_text_file {"path":"/home/user/mcp/run/fixture/outside/secret.txt"}
isError=true
Access denied - path outside allowed directories: /home/user/mcp/run/fixture/outside/secret.txt not in /home/user/mcp/run/fixture/allowed
ENDCALL
CALL read_text_file {"path":"/etc/passwd"}
isError=true
Access denied - path outside allowed directories: /etc/passwd not in /home/user/mcp/run/fixture/allowed
ENDCALL
CALL read_text_file {"path":"/home/user/mcp/run/fixture/allowed/../../fixture/outside/secret.txt"}
isError=true
Access denied - path outside allowed directories: /home/user/mcp/run/fixture/outside/secret.txt not in /home/user/mcp/run/fixture/allowed
ENDCALL
CALL read_text_file {"path":"/home/user/mcp/run/fixture/allowed-evil/note.txt"}
isError=true
Access denied - path outside allowed directories: /home/user/mcp/run/fixture/allowed-evil/note.txt not in /home/user/mcp/run/fixture/allowed
ENDCALL
client_close=ok
STDERR_BEGIN
Secure MCP Filesystem Server running on stdio
Client does not support MCP Roots, using allowed directories set from server args: [ '/home/user/mcp/run/fixture/allowed' ]
STDERR_END
MCP filesystem 서버 실행 화면: 허용 폴더 밖 read_text_file 호출마다 Access denied가 반환됩니다
2026-10-14 15:25:34 KST, Debian 13 — 허용 폴더 밖 read_text_file 호출마다 Access denied가 반환됩니다.

화면에 보이는 허용 폴더 경로는 실제 작업 폴더 이름 그대로이고, 본문 블록에서는 같은 위치를 /home/user/mcp로 줄여 썼습니다. 호출 순서와 거부 메시지는 양쪽이 같습니다.

네 호출 모두 isError=true이고 파일 내용은 없습니다. outside/secret.txt와 /etc/passwd의 문장은 Access denied - path outside allowed directories이고, 콜론 뒤의 경로 다음에 not in과 허용 폴더가 붙습니다. ..가 들어 있는 요청은 CALL 줄에 그대로 남아 있지만, 오류 문장의 경로는 ..가 빠진 fixture/outside/secret.txt입니다. 서버가 요청 문자열을 정규화한 절대 경로로 허용 여부를 판단했다는 뜻이 이 차이입니다.

allowed-evil/note.txt도 같은 형식의 거부입니다. 디렉터리 이름이 허용 폴더 이름으로 시작해도 오류는 그 경로를 허용 폴더 밖이라고 적습니다. 네 건이 거부된 뒤에도 출력은 client_close=ok로 끝납니다. 도구 결과의 isError=true와 클라이언트 종료를 구분해서 보면 됩니다.

허용 범위 안에서 읽거나, 범위를 넓히거나

파일을 허용 폴더 안에 두고 읽기

거부의 직접 원인은 요청 경로가 서버 인자로 넘긴 허용 폴더 밖이라는 점입니다. 파일을 그 폴더 안에 두고 같은 read_text_file을 부르면 내용이 돌아옵니다. 해결 스크립트의 첫 세션은 허용 폴더를 fixture/allowed 하나로 유지한 채 아래 호출을 보냅니다.

cat > calls-b-narrow.json << EOF
[
  {"name":"read_text_file","arguments":{"path":"${ALLOWED}/note.txt"}},
  {"name":"read_text_file","arguments":{"path":"${ALLOWED}/nested/../note.txt"}},
  {"name":"read_text_file","arguments":{"path":"${ALLOWED}/note.txt","head":1}},
  {"name":"list_allowed_directories","arguments":{}}
]
EOF
node call.mjs "$SERVER" cli "$ALLOWED" calls-b-narrow.json

note.txt와 nested/../note.txt는 둘 다 isError=false이고 본문은 허용된 폴더의 메모입니다. 경로에 ..가 섞여 있어도, 정규화한 결과가 허용 폴더 안에 머물면 같은 파일을 읽습니다. head를 1로 준 호출도 성공하는데, 이번 파일은 한 줄뿐이라 옵션 없이 부른 호출과 같은 문장이 돌아옵니다. list_allowed_directories에는 Allowed directories: 다음에 허용 폴더 하나만 나옵니다. 읽기가 성공한 범위와 서버가 밝힌 허용 범위가 정확히 겹친다는 것을 이 한 줄로 맞춰 볼 수 있습니다.

이 구성은 서버 인자를 바꾸지 않고 허용 폴더 안의 파일을 읽으며, 이번 성공 본문은 허용된 폴더의 메모입니다. 정규화 뒤에도 허용 폴더 안에 남는 .. 경로 역시 같은 파일로 읽힙니다. 반면 허용 폴더 밖 파일은 이 인자 구성에서 본문이 되지 않고, 그 거부는 앞 절의 출력과 같습니다.

필요한 폴더를 서버 인자에 더하기

읽어야 할 파일이 이미 다른 폴더에 있으면, 그 폴더를 서버 인자에 더하는 방법을 선택할 수 있습니다. 해결 스크립트의 둘째 세션은 fixture/allowed와 fixture/outside를 클라이언트에 쉼표로 이어서 넘깁니다. 이어서 앞에서 거부되었던 secret.txt를 다시 읽고, list_allowed_directories로 적용된 목록을 받습니다. 이 쉼표 표기는 아래 call.mjs 호출의 입력 형식이므로, 다른 실행 설정에 옮길 때는 서버에 전달되는 디렉터리 인자를 따로 점검해야 합니다.

cat > calls-b-wide.json << EOF
[
  {"name":"read_text_file","arguments":{"path":"${OUTSIDE}/secret.txt"}},
  {"name":"list_allowed_directories","arguments":{}}
]
EOF
node call.mjs "$SERVER" cli "${ALLOWED},${OUTSIDE}" calls-b-wide.json

출력은 두 세션을 이어 붙인 것입니다. 허용 폴더를 그대로 둔 첫 세션에서는 note.txt가 읽히고 허용 목록에 폴더 하나가 남습니다. outside를 더한 둘째 세션에서는 secret.txt의 내용이 돌아오고, 허용 목록과 stderr에 두 경로가 찍힙니다. 같은 파일 요청인데 결과를 바꾼 것은 허용 범위 설정뿐입니다. 거부를 풀겠다고 읽기 도구를 바꿀 필요는 없다는 이야기입니다.

=== read inside the allowed directory, then add a second directory ===
+ mkdir -p app fixture/allowed/nested fixture/outside fixture/allowed-evil
+ cat > app/package.json << 'EOF'
+ cat > app/call.mjs << 'ENDCALLJS'
+ cd app
+ npm install --omit=dev --ignore-scripts --no-fund --no-audit
(#########⠂⠂⠂⠂⠂⠂⠂⠂⠂) ⠏ idealTree: timing idealTree Completed in 73ms(#########⠂⠂⠂⠂⠂⠂⠂⠂⠂) ⠼ idealTree: timing idealTree Completed in 73ms
up to date in 206ms
+ printf note and outside files
+ cat > calls-b-narrow.json << EOF
+ cat calls-b-narrow.json
[
  {"name":"read_text_file","arguments":{"path":"/home/user/mcp/run/fixture/allowed/note.txt"}},
  {"name":"read_text_file","arguments":{"path":"/home/user/mcp/run/fixture/allowed/nested/../note.txt"}},
  {"name":"read_text_file","arguments":{"path":"/home/user/mcp/run/fixture/allowed/note.txt","head":1}},
  {"name":"list_allowed_directories","arguments":{}}
]
=== b1 narrow root ===
+ node call.mjs node_modules/@modelcontextprotocol/server-filesystem/dist/index.js cli /home/user/mcp/run/fixture/allowed calls-b-narrow.json
mode=cli
sdk_version=1.32.1
sdk_repo=git+https://github.com/modelcontextprotocol/typescript-sdk.git
fs_version=1.0.0
fs_sdk_range=^1.31.0
fs_repo=https://github.com/modelcontextprotocol/servers.git
server_name=secure-filesystem-server
server_version=1.0.0
tool_count=14
tool_names=read_file,read_text_file,read_media_file,read_multiple_files,write_file,edit_file,create_directory,list_directory,list_directory_with_sizes,directory_tree,move_file,search_files,get_file_info,list_allowed_directories
CALL read_text_file {"path":"/home/user/mcp/run/fixture/allowed/note.txt"}
isError=false
허용된 폴더의 메모

ENDCALL
CALL read_text_file {"path":"/home/user/mcp/run/fixture/allowed/nested/../note.txt"}
isError=false
허용된 폴더의 메모

ENDCALL
CALL read_text_file {"path":"/home/user/mcp/run/fixture/allowed/note.txt","head":1}
isError=false
허용된 폴더의 메모
ENDCALL
CALL list_allowed_directories {}
isError=false
Allowed directories:
/home/user/mcp/run/fixture/allowed
ENDCALL
client_close=ok
STDERR_BEGIN
Secure MCP Filesystem Server running on stdio
Client does not support MCP Roots, using allowed directories set from server args: [ '/home/user/mcp/run/fixture/allowed' ]
STDERR_END
+ cat > calls-b-wide.json << EOF
+ cat calls-b-wide.json
[
  {"name":"read_text_file","arguments":{"path":"/home/user/mcp/run/fixture/outside/secret.txt"}},
  {"name":"list_allowed_directories","arguments":{}}
]
=== b2 second allowed directory ===
+ node call.mjs node_modules/@modelcontextprotocol/server-filesystem/dist/index.js cli /home/user/mcp/run/fixture/allowed,/home/user/mcp/run/fixture/outside calls-b-wide.json
mode=cli
sdk_version=1.32.1
sdk_repo=git+https://github.com/modelcontextprotocol/typescript-sdk.git
fs_version=1.0.0
fs_sdk_range=^1.31.0
fs_repo=https://github.com/modelcontextprotocol/servers.git
server_name=secure-filesystem-server
server_version=1.0.0
tool_count=14
tool_names=read_file,read_text_file,read_media_file,read_multiple_files,write_file,edit_file,create_directory,list_directory,list_directory_with_sizes,directory_tree,move_file,search_files,get_file_info,list_allowed_directories
CALL read_text_file {"path":"/home/user/mcp/run/fixture/outside/secret.txt"}
isError=false
허용 폴더 밖의 문장

ENDCALL
CALL list_allowed_directories {}
isError=false
Allowed directories:
/home/user/mcp/run/fixture/allowed
/home/user/mcp/run/fixture/outside
ENDCALL
client_close=ok
STDERR_BEGIN
Secure MCP Filesystem Server running on stdio
Client does not support MCP Roots, using allowed directories set from server args: [
  '/home/user/mcp/run/fixture/allowed',
  '/home/user/mcp/run/fixture/outside'
]
STDERR_END
MCP filesystem 서버 실행 화면: outside를 허용하자 secret.txt 내용과 isError=false를 읽습니다
2026-10-14 15:25:40 KST, Debian 13 — outside를 허용하자 secret.txt 내용과 isError=false를 읽습니다.

둘째 세션의 secret.txt는 isError=false이고 본문은 허용 폴더 밖의 문장입니다. list_allowed_directories에는 허용 폴더와 outside 폴더가 두 줄로 나옵니다. stderr도 Client does not support MCP Roots라고 알린 뒤, 서버 인자에서 받은 두 경로를 허용 목록으로 적습니다. 첫 세션에서 거부의 원인이었던 경로는, 그 부모 폴더를 인자에 넣은 뒤에는 읽기 성공이 됩니다.

필요한 디렉터리를 두 번째 인자로 넣으면 그 안의 파일을 읽고, 이번 성공 본문은 허용 폴더 밖의 문장입니다. 실제로 열린 폴더 두 곳은 list_allowed_directories로 서버에게 다시 물어볼 수 있습니다. 대신 추가한 디렉터리 전체가 허용 목록에 올라갑니다. 도구 이름 14개 안에 write_file, edit_file, move_file이 있으므로 범위를 넓히기 전에 그 목록을 함께 봅니다. 클라이언트가 Roots를 보내면 이 명령줄 목록이 그대로 유지되지 않으며, 그 경우는 다음 절의 roots 모드와 같습니다.

링크 대상 검사, Windows 형태 경로, Roots 교체로 갈리는 거부

경로가 허용 폴더 안에 있는 것처럼 보여도 읽기가 거부되는 경우가 있습니다. 점검 스크립트는 허용 폴더 안의 escape.txt를 outside의 secret.txt로 잇는 심볼릭 링크를 만듭니다. readlink로 대상을 확인한 뒤 같은 허용 폴더 설정에서 read_text_file을 호출합니다. 이어지는 Windows 형태 경로 호출과 비교하면 링크 대상 검사에서 막힌 경우와 호스트에 맞지 않는 경로 형식으로 막힌 경우를 나눠 볼 수 있습니다.

ln -sfn "${OUTSIDE}/secret.txt" "${ALLOWED}/escape.txt"
readlink "${ALLOWED}/escape.txt"
node call.mjs "$SERVER" cli "$ALLOWED" calls-c-link.json
node call.mjs "$SERVER" cli "$ALLOWED" calls-c-win.json

readlink 결과는 outside의 secret.txt입니다. escape.txt 호출은 isError=true이고, 문장은 Access denied - symlink target outside allowed directories입니다. 콜론 뒤에 적힌 경로는 링크 파일 escape.txt가 아니라 대상인 secret.txt입니다. 앞 절의 일반 경로 거부와 문장 앞부분이 다르므로, 링크를 따라간 결과인지 요청 경로 자체가 밖인지 구분할 수 있습니다.

같은 허용 폴더에 Windows 형태 경로 C:\Users\note.txt를 넘기면 isError=true이고 문장은 Access denied - Windows-style path received on a POSIX host입니다. 이번 호스트는 표의 Debian이고, 이 문장은 그 POSIX 환경에서 나온 거부입니다. 드라이브 글자로 시작하는 경로를 이 서버에 그대로 넣으면 허용 폴더 안 파일로 열리지 않습니다.

연결 전에 한 번 더 볼 항목은 클라이언트가 Roots를 보내는지입니다. 점검 스크립트의 roots 모드는 서버 인자로 허용 폴더를 넘기되, 클라이언트 능력에 Roots를 넣습니다. roots/list 응답으로는 outside 폴더의 file URI 하나만 돌려줍니다. 아래에는 디렉터리 인자와 Roots가 모두 없는 경우도 함께 넣어, 허용 목록 교체와 초기 연결 실패를 비교합니다.

node call.mjs "$SERVER" roots "$ALLOWED" calls-c-roots.json "$OUTSIDE"
node call.mjs "$SERVER" none - calls-c-none.json
curl -sI --max-time 20 "$u" | head -n 1

stderr는 Updated allowed directories from MCP roots: 1 valid directories입니다. list_allowed_directories에는 outside 폴더만 남고, 명령줄로 넘겼던 허용 폴더는 목록에 없습니다. 그래서 note.txt는 isError=true가 되고, 오류의 not in 뒤에는 outside 폴더가 옵니다. 같은 세션의 secret.txt는 isError=false이고 본문은 허용 폴더 밖의 문장입니다. Roots가 명령줄 허용 폴더를 부분 추가가 아니라 통째로 바꿉니다.

디렉터리 인자 없이 none 모드로 띄우고 Roots도 광고하지 않으면, 서버 이름 secure-filesystem-server와 버전 1.0.0이 찍힌 다음 client_error=MCP error -32000: Connection closed가 납니다. 이 구간에는 tool_count 줄이 없습니다. stderr에는 사용법 안내와 함께 Server cannot operate: No allowed directories available.로 시작하는 문장이 있고, 명령줄 인자와 MCP roots 가운데 하나는 있어야 한다는 안내가 이어집니다.

출처로 적은 주소 다섯 곳은 같은 스크립트의 curl -sI에서 상태 줄이 HTTP/2 200이었습니다. 패키지 레지스트리, servers 저장소, filesystem README, npm-view 문서, typescript-sdk 저장소입니다. filesystem 패키지의 fs_repo와 SDK의 sdk_repo는 출력에 있고, 위 https 주소는 그 저장소 페이지입니다.

=== symlink, windows path, roots replacement, empty args, urls ===
+ mkdir -p app fixture/allowed/nested fixture/outside fixture/allowed-evil
+ cat > app/package.json << 'EOF'
+ cat > app/call.mjs << 'ENDCALLJS'
+ cd app
+ npm install --omit=dev --ignore-scripts --no-fund --no-audit
(#########⠂⠂⠂⠂⠂⠂⠂⠂⠂) ⠇ idealTree: timing idealTree Completed in 61ms(#########⠂⠂⠂⠂⠂⠂⠂⠂⠂) ⠸ idealTree: timing idealTree Completed in 61ms
up to date in 178ms
+ printf note outside and prefix files
+ ln -sfn /home/user/mcp/run/fixture/outside/secret.txt /home/user/mcp/run/fixture/allowed/escape.txt
+ readlink /home/user/mcp/run/fixture/allowed/escape.txt
/home/user/mcp/run/fixture/outside/secret.txt
+ cat > calls-c-link.json << EOF
+ cat calls-c-link.json
[
  {"name":"read_text_file","arguments":{"path":"/home/user/mcp/run/fixture/allowed/escape.txt"}}
]
=== c1 symlink ===
+ node call.mjs node_modules/@modelcontextprotocol/server-filesystem/dist/index.js cli /home/user/mcp/run/fixture/allowed calls-c-link.json
mode=cli
sdk_version=1.32.1
sdk_repo=git+https://github.com/modelcontextprotocol/typescript-sdk.git
fs_version=1.0.0
fs_sdk_range=^1.31.0
fs_repo=https://github.com/modelcontextprotocol/servers.git
server_name=secure-filesystem-server
server_version=1.0.0
tool_count=14
tool_names=read_file,read_text_file,read_media_file,read_multiple_files,write_file,edit_file,create_directory,list_directory,list_directory_with_sizes,directory_tree,move_file,search_files,get_file_info,list_allowed_directories
CALL read_text_file {"path":"/home/user/mcp/run/fixture/allowed/escape.txt"}
isError=true
Access denied - symlink target outside allowed directories: /home/user/mcp/run/fixture/outside/secret.txt not in /home/user/mcp/run/fixture/allowed
ENDCALL
client_close=ok
STDERR_BEGIN
Secure MCP Filesystem Server running on stdio
Client does not support MCP Roots, using allowed directories set from server args: [ '/home/user/mcp/run/fixture/allowed' ]
STDERR_END
+ cat > calls-c-win.json << 'EOF'
+ cat calls-c-win.json
[
  {"name":"read_text_file","arguments":{"path":"C:\\Users\\note.txt"}}
]
=== c2 windows-style path ===
+ node call.mjs node_modules/@modelcontextprotocol/server-filesystem/dist/index.js cli /home/user/mcp/run/fixture/allowed calls-c-win.json
mode=cli
sdk_version=1.32.1
sdk_repo=git+https://github.com/modelcontextprotocol/typescript-sdk.git
fs_version=1.0.0
fs_sdk_range=^1.31.0
fs_repo=https://github.com/modelcontextprotocol/servers.git
server_name=secure-filesystem-server
server_version=1.0.0
tool_count=14
tool_names=read_file,read_text_file,read_media_file,read_multiple_files,write_file,edit_file,create_directory,list_directory,list_directory_with_sizes,directory_tree,move_file,search_files,get_file_info,list_allowed_directories
CALL read_text_file {"path":"C:\\Users\\note.txt"}
isError=true
Access denied - Windows-style path received on a POSIX host: C:\Users\note.txt
ENDCALL
client_close=ok
STDERR_BEGIN
Secure MCP Filesystem Server running on stdio
Client does not support MCP Roots, using allowed directories set from server args: [ '/home/user/mcp/run/fixture/allowed' ]
STDERR_END
+ cat > calls-c-roots.json << EOF
+ cat calls-c-roots.json
[
  {"name":"read_text_file","arguments":{"path":"/home/user/mcp/run/fixture/allowed/note.txt"}},
  {"name":"read_text_file","arguments":{"path":"/home/user/mcp/run/fixture/outside/secret.txt"}},
  {"name":"list_allowed_directories","arguments":{}}
]
=== c3 client roots replace cli directories ===
+ node call.mjs node_modules/@modelcontextprotocol/server-filesystem/dist/index.js roots /home/user/mcp/run/fixture/allowed calls-c-roots.json /home/user/mcp/run/fixture/outside
mode=roots
sdk_version=1.32.1
sdk_repo=git+https://github.com/modelcontextprotocol/typescript-sdk.git
fs_version=1.0.0
fs_sdk_range=^1.31.0
fs_repo=https://github.com/modelcontextprotocol/servers.git
server_name=secure-filesystem-server
server_version=1.0.0
tool_count=14
tool_names=read_file,read_text_file,read_media_file,read_multiple_files,write_file,edit_file,create_directory,list_directory,list_directory_with_sizes,directory_tree,move_file,search_files,get_file_info,list_allowed_directories
CALL read_text_file {"path":"/home/user/mcp/run/fixture/allowed/note.txt"}
isError=true
Access denied - path outside allowed directories: /home/user/mcp/run/fixture/allowed/note.txt not in /home/user/mcp/run/fixture/outside
ENDCALL
CALL read_text_file {"path":"/home/user/mcp/run/fixture/outside/secret.txt"}
isError=false
허용 폴더 밖의 문장

ENDCALL
CALL list_allowed_directories {}
isError=false
Allowed directories:
/home/user/mcp/run/fixture/outside
ENDCALL
client_close=ok
STDERR_BEGIN
Secure MCP Filesystem Server running on stdio
Updated allowed directories from MCP roots: 1 valid directories
STDERR_END
+ cat > calls-c-none.json << 'EOF'
+ cat calls-c-none.json
[]
=== c4 no allowed directory ===
+ node call.mjs node_modules/@modelcontextprotocol/server-filesystem/dist/index.js none - calls-c-none.json
mode=none
sdk_version=1.32.1
sdk_repo=git+https://github.com/modelcontextprotocol/typescript-sdk.git
fs_version=1.0.0
fs_sdk_range=^1.31.0
fs_repo=https://github.com/modelcontextprotocol/servers.git
server_name=secure-filesystem-server
server_version=1.0.0
client_error=MCP error -32000: Connection closed
STDERR_BEGIN
Usage: mcp-server-filesystem [allowed-directory] [additional-directories...]
Note: Allowed directories can be provided via:
  1. Command-line arguments (shown above)
  2. MCP roots protocol (if client supports it)
At least one directory must be provided by EITHER method for the server to operate.
Secure MCP Filesystem Server running on stdio
Started without allowed directories - waiting for client to provide roots via MCP protocol
Error: Server cannot operate: No allowed directories available. Server was started without command-line directories and client does not support MCP roots protocol. Please either: 1) Start server with directory arguments, or 2) Use a client that supports MCP roots protocol and provides valid root directories.
STDERR_END
=== c5 official url status ===
+ curl -sI --max-time 20 https://registry.npmjs.org/@modelcontextprotocol/server-filesystem
HTTP/2 200 
+ curl -sI --max-time 20 https://github.com/modelcontextprotocol/servers
HTTP/2 200 
+ curl -sI --max-time 20 https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/README.md
HTTP/2 200 
+ curl -sI --max-time 20 https://docs.npmjs.com/cli/v11/commands/npm-view/
HTTP/2 200 
+ curl -sI --max-time 20 https://github.com/modelcontextprotocol/typescript-sdk
HTTP/2 200 
MCP filesystem 서버 실행 화면: 허용 폴더·Roots가 없는 서버가 Connection closed로 연결을 끊습니다
2026-10-14 15:27:52 KST, Debian 13 — 허용 폴더·Roots가 없는 서버가 Connection closed로 연결을 끊습니다.

캡처에는 링크 대상 검사, Windows 형태 경로 거부, Roots에 의한 허용 목록 교체가 차례로 담겨 있습니다. 눈여겨볼 것은 실패의 종류입니다. 링크와 경로 형식 오류는 isError=true인 도구 결과로 돌아올 뿐 세션은 client_close=ok로 정상 종료합니다. 반면 인자와 Roots가 모두 없는 구간에서는 tool_count가 나오기도 전에 MCP error -32000: Connection closed가 찍힙니다. 운영 로그에서도 이 차이로 파일 요청 거부와 서버를 아예 쓸 수 없는 연결 문제를 갈라 보아야 합니다.

클라이언트 Roots가 바꾸는 접근 범위와 헬스체크

이번 출력에서 filesystem 패키지와 서버 응답은 모두 1.0.0이고, 설치된 SDK는 1.32.1, 패키지가 선언한 SDK 범위는 ^1.31.0입니다. 운영 기록에는 이 값을 각각 남겨야 서버 패키지와 SDK를 혼동하지 않습니다. 버전 갱신 때에는 현재 도구 목록과 호출 결과를 기준으로 동작을 다시 대조하는 편이 좋습니다. 특히 같은 서버 인자를 쓰더라도 클라이언트가 Roots를 제공하면 허용 목록이 바뀌므로, 클라이언트 설정 변경도 파일 접근 정책 변경으로 다룹니다.

헬스체크를 서버 이름과 버전 응답만으로 끝내면 안 됩니다. 인자 없이 띄웠을 때도 그 값은 찍힌 뒤에 연결이 닫혔기 때문입니다. 초기 연결 이후 list_allowed_directories가 기대한 목록을 돌려주는지까지 봐야 합니다. 배포 설정의 디렉터리 인자나 클라이언트의 Roots 경로를 바꿀 때는 점검에 쓰는 허용 파일 경로도 같이 고칩니다. Kubernetes에서 이 연결을 감싸는 애플리케이션을 운영한다면 readiness 판단에 이 확인을 넣되, 허용 폴더 밖 요청이 거부됐다는 이유로 서버를 재시작하지 않도록 요청 오류와 준비 상태를 나눠 둡니다.

좁은 전용 폴더 하나만 여는 구성부터

표에는 실행 결과가 남은 선택만 넣었고, 결과 열은 해당 출력을 줄여 적은 것입니다. 일반 경로 거부와 링크 대상 거부는 오류 문장부터 다르고, 인자가 없으면 도구 결과 대신 연결 종료가 나옵니다. 장애가 나면 이 세 갈래 중 어디에 해당하는지 먼저 가른 뒤 허용 목록과 요청 경로를 대조하십시오. 그러면 어느 설정을 고쳐야 하는지가 빨리 보입니다.

선택isError결과
허용 폴더 밖 secret.txttruepath outside allowed directories
/etc/passwdtruepath outside allowed directories
.. 로 outside에 닿는 경로true오류 경로는 .. 가 빠진 outside/secret.txt
형제 폴더 allowed-eviltruepath outside allowed directories
허용 폴더 안 note.txtfalse허용된 폴더의 메모
nested/../note.txtfalse허용된 폴더의 메모
note.txt에 head 1false허용된 폴더의 메모
인자에 outside를 추가한 secret.txtfalse허용 폴더 밖의 문장
허용 폴더 안 심볼릭 링크truesymlink target outside allowed directories
Windows 형태 경로trueWindows-style path received on a POSIX host
Roots가 outside만 돌려준 note.txttruenot in 뒤에 outside 폴더
Roots가 outside만 돌려준 secret.txtfalse허용 폴더 밖의 문장
인자 없고 Roots도 없음연결이 닫힘MCP error -32000: Connection closed

위 표의 호출 결과를 놓고 보면 먼저 권하는 구성은, 작업에 필요한 자료를 좁은 전용 폴더에 모으고 그 폴더만 허용하는 것입니다. note.txt의 정상 읽기와 폴더 밖 경로의 거부를 한 번에 확인할 수 있어서, 의도한 접근 범위를 남에게 설명하기도 쉽습니다. 기존 파일 위치를 바꿀 수 없다면 필요한 디렉터리만 더하고, list_allowed_directories의 응답을 배포 설정과 맞춰 봅니다. 도구 목록에는 쓰기와 이동 도구도 들어 있으니, 읽기 오류 하나 없애려고 상위 폴더를 넓게 여는 방식은 피하는 편이 좋습니다.

실시간 서비스에 연결한다면 자원 사양을 늘리는 처방보다 허용 목록과 연결 준비 상태를 먼저 점검하겠습니다. 이 글의 거부 사례는 파일 내용이 돌아오기 전에 경로나 설정 조건에서 막혔으므로, 처리 자원을 추가하는 것만으로 해결할 문제로 보기는 어렵습니다. 클라이언트가 Roots를 관리하는 구성에서는 그 응답을 접근 정책의 기준으로 삼고, 고정된 디렉터리 인자를 쓰는 구성에서는 Roots 지원 여부까지 배포 기록에 남기기를 권합니다. 심볼릭 링크 오류는 대상 경로를, Windows 형태 경로 오류는 호스트에 맞는 경로 전달 방식을 먼저 점검합니다. 운영 연결을 승인할 기준은 프로세스가 떠 있다는 사실보다 허용한 파일은 읽히고 범위 밖 파일은 거부되며 적용된 허용 목록을 다시 확인할 수 있다는 점입니다.

Similar Posts

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다