블로그 • MCP
Claude Code 2.1.305로 올린 뒤, managed-mcp.json에서 변수를 env에만 적은 stdio 서버가 다시 로드된다. deniedMcpServers는 어떻게 쓰나?
Claude Code 2.1.305 업데이트 설명은 한 문장이지만, 업그레이드 뒤에 서버가 스스로 나타난다. allowedMcpServers는 managed-mcp.json에서 ${VAR}를 env에만 쓰는 stdio 서버를 더 이상 걸러 내지 않는다. 허용 목록에 없던 서버는 로드된다. 계속 멈추는 것은 deniedMcpServers이지, 허용 목록을 다시 베끼는 것이 아니다.
MCP 권한을 누가 맡는지는 MCP 보안 글. OAuth 자격 증명이 잘못된 인가 서버로 갈 때 로그가 멈추는 단계는 이전 OAuth 글. 이 글은 2.1.305의 이 한 가지만 쓴다. 허용 목록은 변수를 env에만 쓰는 stdio 서버를 더 이상 멈추지 않는다. 거부 목록 JSON은 명령과 완전히 같아야 한다.
- 대상은 managed-mcp.json의 stdio 서버이고, ${VAR}가 env에만 있는 경우다.
- 2.1.305부터 허용 목록은 그것을 걸러 내지 않는다. 허용 목록에 없으면 업그레이드 뒤 로드된다.
- 계속 멈추려면 deniedMcpServers의 serverCommand는 명령과 인자의 배열이고, 순서도 같아야 한다.
- 빈 거부 목록은 모양이 맞아도 아무것도 막지 않는다. serverName은 보안 제어가 아니다.
결론: env에만 ${VAR}를 쓰는 stdio 서버는 2.1.305 이후 허용 목록으로 멈추지 않는다. 거부 목록에 command와 args와 완전히 같은 serverCommand를 적는다.
2.1.305는 허용 목록의 어디를 바꿨나?
업데이트 설명은 이렇게 적는다. allowedMcpServers는 managed-mcp.json에서 ${VAR}를 env에만 쓰는 stdio 서버를 더 이상 걸러 내지 않는다. 허용 목록에 없던 한 대는 업그레이드 뒤 로드된다. 계속 멈추려면 deniedMcpServers를 쓴다. 원문은 Claude Code 업데이트 설명.
조직 MCP 문서는 더 앞의 규칙을 아직 적고 있다. managed-mcp.json의 서버는 허용 목록을 건너뛰지만, 정의에 ${VAR}가 있으면 여전히 허용 목록을 통과한다. 2.1.259 이전에는 허용 목록이 있으면 모든 관리 서버가 통과해야 했다. 2.1.305가 뺀 것은 좁은 한 조각이다. stdio이고, 변수가 env에만 있는 경우. 업데이트 설명은 명령, 인자, URL, headers의 ${VAR}도 검사에서 빠진다고 적지 않는다. 문서는 조직 MCP 문서.
변수가 env에만 있는지는 어떻게 보나?
문서의 stdio 예에는 type, command, args, env가 있다. 비밀은 env 값에 적지 않는다. 사용자별 자격 증명은 ${VAR}로 그 사용자의 환경에서 읽는다. 아래 파일은 변수를 env에만 둔다. 명령과 인자는 리터럴이다. 이것이 2.1.305가 말하는 경우다. 명령 경로는 자리표시이며, 설치할 패키지가 아니다.
{
"mcpServers": {
"notes": {
"type": "stdio",
"command": "/usr/local/bin/notes-server",
"args": ["--stdio"],
"env": {
"NOTES_TOKEN": "${NOTES_TOKEN}"
}
}
}
}
${VAR}가 command, args, URL, headers에 있다면 업데이트 설명은 그것들을 이번 면제에 넣지 않는다. 업데이트 설명이 스스로 바꾸기 전까지, 그 서버는 문서의 허용 목록 검사 대상으로 다룬다. 여기서의 거부 목록 예는 env인 경우만 다룬다.
deniedMcpServers는 어느 명령과 같아야 하나?
문서에서 serverCommand가 맞추는 것은 stdio 서버를 시작하는 명령과 인자이고, 일치는 완전해야 한다. 위의 command에 args를 더한 것이 그 배열이다. 인자를 하나 빼거나, 하나 더하거나, 순서를 바꾸면 맞지 않는다. serverName은 이름표일 뿐이다. 문서는 어느 목록의 serverName도 보안 제어가 아니라고 적는다. 사용자는 어떤 서버에도 같은 이름을 붙일 수 있다.
{
"deniedMcpServers": [
{
"serverCommand": ["/usr/local/bin/notes-server", "--stdio"]
}
]
}
거부 목록은 관리 서버에도 적용되고, 사용자 자신의 deniedMcpServers도 합쳐지므로, 사용자는 자신을 위해 관리 서버 하나를 막을 수 있다. 허용 목록이 빈 배열이면 문서는 허용 목록 검사를 건너뛰는 서버 외에는 로드되지 않는다고 한다. 2.1.305 이후 변수를 env에만 쓰는 이 stdio 서버는 건너뛰는 쪽이다. 빈 허용 목록으로는 막을 수 없다.
이 거부 목록의 모양은 어떤 Schema로 고정하나?
JSON Schema로 타입을 고정한다. deniedMcpServers는 배열이어야 한다. 각 serverCommand는 문자열 배열이어야 한다. 빈 배열의 모양은 맞다. 이 서버가 멈춘다는 것까지는 증명하지 않는다. 멈추는 것은 앱 규칙이다. 배열은 command와 args를 더한 것과 완전히 같아야 한다.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["deniedMcpServers"],
"properties": {
"deniedMcpServers": {
"type": "array",
"items": {
"type": "object",
"required": ["serverCommand"],
"properties": {
"serverCommand": {
"type": "array",
"items": { "type": "string" }
}
}
}
}
}
}
| 기록에 보이는 것 | Schema | 이 서버를 멈출 때 |
|---|---|---|
| serverCommand가 한 줄의 문자열 | 실패한다. 여기에는 문자열 배열이 필요하다. | 먼저 명령과 인자 두 문자열로 나눈다. |
| 인자가 숫자 | 실패한다. 배열의 각 항목은 문자열이다. | args와 같은 문자열로 바꾼다. |
| deniedMcpServers가 빈 배열이고 모양은 맞다 | 통과한다. 빈 배열도 배열이다. | command와 args에 맞는 serverCommand를 넣는다. 빈 목록은 아무것도 막지 않는다. |
로드 전 세 단계 중 어느 단계가 실제로 멈추나?
문서는 로드 전의 확인을 세 단계로 적고, 사용자가 /mcp에서 다시 연결하거나 끈 것을 다시 켤 때 다시 실행한다. 프로세스 안의 sdk 유형은 세 단계를 모두 건너뛴다. 거부 목록에 맞은 뒤에는 그것을 뒤집는 것이 없다.
| 단계 | 이 env만 쓰는 서버 |
|---|---|
| 각 층의 목록을 합친다 | 각 설정 층의 허용 목록과 거부 목록이 하나로 합쳐진다. allowManagedMcpServersOnly가 true이면 관리 출처의 허용 목록만 남는다. 거부 목록은 항상 모든 층에서 합쳐진다. |
| 먼저 거부 목록을 본다 | serverCommand가 완전히 같으면 멈춘다. 이 단계 뒤에 허용 목록은 그것을 되돌리지 못한다. |
| 그다음 허용 목록을 본다 | 2.1.305 이후 변수를 env에만 쓰는 이 stdio 서버는 이 단계를 건너뛴다. 허용 목록에 없어도 그 이유로는 더 이상 멈추지 않는다. |
JSONNote에서 이 거부 목록을 어떻게 확인하나?
- 1동작을 고른다
JSON Schema를 연다. 작동하다 를 Schema로 확인하세요 로 바꾼다. 첫 칸에 거부 목록, 둘째 칸에 위 Schema를 붙인다. serverCommand는 문자열 배열이어야 한다.
- 2나쁜 데이터 넷을 부딪힌다
serverCommand를 한 줄의 문자열로 하거나, deniedMcpServers를 지우거나, 인자를 숫자로 하거나, deniedMcpServers를 객체로 한다. 이 넷은 실패한다. 위의 거부 목록은 통과한다. 통과는 모양이 맞다는 뜻만 가진다.
- 3빈 목록과 그 명령을 비교한다
JSON 비교 Diff. 첫 칸에 위의 거부 목록을 붙인다. 둘째 칸은 deniedMcpServers를 빈 배열로 바꾼다. 둘 다 Schema는 통과한다. 이 서버를 막는 것은 그 serverCommand이지, 빈 배열이 아니다.
자주 묻는 질문
허용 목록에 같은 명령을 넣으면 거부 목록을 대신하나?
대신하지 않는다. 2.1.305 이후 이 서버는 허용 목록을 건너뛴다. 허용 목록에 그 명령이 있는지는 로드 여부를 더 이상 정하지 않는다. 멈추려면 맞는 거부 목록이 있어야 한다. 거부 목록에 맞은 뒤 허용 목록은 그것을 되돌리지 못한다.
serverName을 notes로 쓰면 충분한가?
충분하지 않다. 문서는 serverName이 사용자가 붙이는 이름표이고, 와일드카드도 펼쳐지지 않는다고 적는다. 사용자는 어떤 서버에도 같은 이름을 붙일 수 있다. 이 stdio 서버를 멈추려면 serverCommand를 쓰고, command와 args와 완전히 같게 한다.
명령에도 ${VAR}가 있으면 이번에도 로드되나?
업데이트 설명은 그 경우를 면제에 넣지 않는다. 지목한 것은 stdio이고, 변수가 env에만 있을 때다. 명령, 인자, URL, headers에도 ${VAR}가 있으면 조직 문서의 허용 목록 검사로 그대로 다룬다. 그 서버가 허용 목록과 맞지 않으면 2.1.305가 로드한다고 가정하지 않는다.
사용자 자신의 설정으로 조직이 배포한 한 대를 막을 수 있나?
할 수 있다. 문서는 deniedMcpServers가 managed-mcp.json의 서버에도 적용되고, 사용자 자신의 거부 목록이 합쳐진다고 적는다. 그래서 사용자는 자신을 위해 한 대를 막을 수 있다. 허용 목록은 이 단계를 대신하지 않는다.
env에 token 값을 적나?
적지 않는다. 문서는 그 머신의 누구나 managed-mcp.json을 읽을 수 있으므로 API key나 다른 자격 증명을 env에 적지 말라고 한다. 사용자별 자격 증명은 ${VAR}로 그 사용자의 환경에서 읽는다. 거부 목록에도 token을 넣지 않고, 명령과 인자만 남긴다.
정리
2.1.305가 면제하는 것은 하나다. managed-mcp.json의 stdio 서버이고 ${VAR}가 env에만 있는 경우. 허용 목록은 그것을 걸러 내지 않는다. 명령, 인자, URL, headers의 변수는 업데이트 설명이 함께 면제하지 않는다.
결론: 계속 멈추려면 deniedMcpServers에 command와 args와 완전히 같은 serverCommand를 적는다. 빈 허용 목록으로는 막지 못한다. serverName으로는 부족하다. env에 token 값은 적지 않는다.
다음: Schema로 거부 목록 배열을 고정한 뒤, Diff로 빈 배열과 그 serverCommand의 차이를 본다.