들어가며
34차시는 붙인 서버를 관리하는 법이었다 — 서버가 어디에 사는지(스코프), 같은 이름이 겹치면 뭐가 이기는지(우선순위), 클론한 저장소의 .mcp.json을 처음 쓸 때 왜 승인을 묻는지(승인 게이트), 그리고 OAuth 자격증명의 수명주기까지. 그 차시에서 .mcp.json은 팀과 공유되는 project 스코프의 저장소로만 등장했다. 파일이 어디 있고 무슨 역할인지는 정했지만, 그 안을 어떻게 직접 쓰는지는 다루지 않았다.
그리고 33·34차시 내내 같은 숙제를 두 번 미뤘다.
GitHub PAT나 DB 비밀번호처럼 평문 자격증명이 든 서버를
project스코프에 그대로 넣으면 안 된다 —.mcp.json은 git에 올라가므로 시크릿이 저장소에 노출된다. 팀 공유가 필요하면 자격증명은 환경 변수로 분리하고.mcp.json엔 변수 이름만 둔다(35차시에서 깊게 다룬다).
35차시는 이 둘을 한꺼번에 답한다. 그리고 답의 중심은 하나다.
.mcp.json은 그저 JSON 파일이다. 그런데 git에 커밋하는 파일은 시크릿을 담을 수 없다 — 그래서 이 차시의 심장은 ${VAR} 환경 변수 확장이다. 커밋되는 파일엔 변수 이름만 남기고, 진짜 시크릿은 각 개발자의 환경에 둔다.
순서는 이렇다. 먼저 .mcp.json이 무슨 모양인지(그냥 JSON), 전송 방식별로 어떤 필드를 쓰는지 본다. 그다음 이 차시의 핵심인 환경 변수 확장 — 두 문법, 확장되는 다섯 자리, 그리고 비어 있을 때의 함정. 마지막으로 손으로 안 쓰고 넣는 claude mcp add-json과, 이미 Claude Desktop에 깔아 둔 서버를 가져오는 claude mcp add-from-claude-desktop까지.
.mcp.json은 손으로 쓰는 게 필수가 아니다
먼저 분명히 해 둘 것 — .mcp.json을 손으로 쓸 일은 생각보다 적다. 34차시에서 봤듯, project 스코프로 서버를 추가하면 Claude Code가 이 파일을 자동으로 만들거나 갱신한다.
# 이 한 줄이 프로젝트 루트에 .mcp.json을 생성·갱신한다
claude mcp add --transport http paypal --scope project https://mcp.paypal.com/mcp
그럼 왜 스키마를 알아야 하나? 세 가지 이유다.
- PR을 읽기 위해. 팀원이 올린
.mcp.json변경이 무슨 서버를 어떤 권한으로 붙이는지 리뷰할 수 있어야 한다. 외부 콘텐츠를 가져오는 서버는 prompt injection 위험이 있으니(34차시 승인 게이트의 이유) 무엇이 들어오는지 읽을 줄 알아야 한다. - 손으로 고치기 위해. URL 하나, 헤더 하나를 바꾸려고 매번 remove + re-add 하는 것보다 파일을 직접 여는 게 빠를 때가 많다.
- 그리고 이 차시의 본론 — 환경 변수 확장.
${VAR}같은 표현은 CLI 플래그로 깔끔히 넣기 어렵다. 시크릿을 분리하려면 결국 파일을 손으로 다듬게 된다.
그러니 “자동 생성에 기대되, 읽고 고칠 줄은 안다”가 이 차시의 목표다.
.mcp.json의 뼈대 — mcpServers 객체 하나
파일의 구조는 단순하다. 최상위에 mcpServers라는 객체 하나가 있고, 그 안에서 키가 서버 이름, 값이 서버 엔트리다. 34차시에서 본 표준 형식이 이렇다.
{
"mcpServers": {
"shared-server": {
"command": "/path/to/server",
"args": [],
"env": {}
}
}
}
shared-server가 서버 이름(claude mcp get shared-server로 조회할 때 쓰는 그 이름)이고, 그 아래 객체가 “이 서버를 어떻게 띄우고 연결하는가”를 적은 엔트리다. 서버를 더 붙이려면 mcpServers 안에 키를 더 추가하면 된다.
이 파일은 프로젝트 루트에 두고 git에 커밋한다 — 그래야 클론한 팀원 모두가 같은 서버를 갖는다(project 스코프의 정의). 그리고 커밋되는 순간 34차시의 승인 게이트가 작동한다 — 팀원이 처음 세션을 열면
⏸ Pending approval이 뜨고, 승인해야 연결된다..mcp.json을 고쳤는데 변경이 안 먹으면 예전에 거부했을 수 있으니claude mcp reset-project-choices로 초기화한다. 같은mcpServers모양은 local·user 스코프가 사는~/.claude.json에서도 쓰인다(거기서는 한 단계 더 중첩된다 — 34차시 참고). 즉 여기서 배우는 엔트리 스키마는 어느 스코프에서나 동일하다.
서버 엔트리 스키마 — 전송 방식별 필드
엔트리에 들어가는 필드는 전송 방식에 따라 갈린다. 32·33차시에서 CLI로 붙일 때 쓴 플래그가 JSON 필드로 1:1 대응된다고 보면 된다.
stdio 서버 (로컬 프로세스)
로컬에서 프로세스로 띄우는 서버다. 세 필드를 쓴다.
{
"mcpServers": {
"airtable": {
"command": "npx",
"args": ["-y", "airtable-mcp-server"],
"env": {
"AIRTABLE_API_KEY": "your-key-here"
}
}
}
}
command— 실행할 명령(실행 파일 경로 또는npx같은 런처).args— 명령에 넘길 인자들의 배열. CLI에서--뒤에 적던 것이 여기로 온다.env— 서버 프로세스에 주입할 환경 변수(키→값 객체). CLI의--env KEY=value가 여기로 온다.
33차시의 claude mcp add --env AIRTABLE_API_KEY=YOUR_KEY --transport stdio airtable -- npx -y airtable-mcp-server가 위 JSON과 정확히 같은 결과다. stdio는 type을 생략해도 되지만, 명시하려면 "type": "stdio"다.
http 서버 (원격, 권장)
원격 서버는 type·url·headers를 쓴다.
{
"mcpServers": {
"github": {
"type": "http",
"url": "https://api.githubcopilot.com/mcp/",
"headers": {
"Authorization": "Bearer your-github-pat"
}
}
}
}
type—"http". 33차시의--transport http에 대응.url— 서버 엔드포인트.headers— 정적 인증 헤더(키→값 객체). CLI의--header "Authorization: Bearer ..."가 여기로 온다.
streamable-http라는 별칭 —.mcp.json,~/.claude.json,claude mcp add-json으로 설정할 때type필드는"streamable-http"를"http"의 별칭으로 받는다. MCP 표준 명세가 이 전송을streamable-http라 부르기 때문에, 서버 문서에서 복사한 설정을 수정 없이 그대로 붙여 넣어도 동작하게 하려는 배려다. 둘은 같은 것이니 어느 쪽으로 써도 된다.
한 파일에 여러 서버
실제로는 전송 방식이 섞인 서버 여러 개를 한 mcpServers에 담는다. 공식 문서의 예시가 stdio와 http를 함께 보여 준다.
{
"mcpServers": {
"github": {
"type": "http",
"url": "https://api.githubcopilot.com/mcp/"
},
"database": {
"command": "/path/to/db-server",
"args": ["--config", "./config.json"],
"env": {
"DB_URL": "${DB_URL}"
}
}
}
}
database 엔트리의 "DB_URL": "${DB_URL}" — 바로 이게 다음 절의 주제다.
선택 필드 몇 가지 — 엔트리에는 위 핵심 필드 외에
timeout(이 서버만의 도구 실행 제한, 밀리초. 예:"timeout": 600000은 10분이며MCP_TOOL_TIMEOUT환경 변수를 이 서버에 한해 덮어쓴다. 1000 미만 값은 무시된다)도 둘 수 있다. OAuth 관련oauth(clientId·callbackPort·scopes 등)는 34차시에서, Tool Search 관련alwaysLoad는 36차시에서 다룬다. 또workspace는 내부 예약 이름이라 서버 이름으로 쓰면 경고와 함께 건너뛴다.
핵심 — ${VAR} 환경 변수 확장
이제 이 차시의 심장이다. 문제를 다시 세운다.
.mcp.json은 git에 커밋된다. 그런데 위 github 예시의 "Authorization": "Bearer your-github-pat"처럼 토큰을 평문으로 박으면, 그 토큰이 저장소 히스토리에 영구히 남는다. 33·34차시가 “project 스코프엔 평문 시크릿 금지”라고 못 박은 이유다. 그렇다고 시크릿이 든 서버를 팀과 공유 못 하는 것도 아니다 — 변수로 분리하면 된다.
Claude Code는 .mcp.json 안에서 환경 변수 확장을 지원한다. 문법은 둘이다.
${VAR}— 환경 변수VAR의 값으로 치환된다.${VAR:-default}—VAR이 설정돼 있으면 그 값, 없으면default를 쓴다.
치환은 다섯 자리에서 일어난다. 그 외 위치에서는 확장되지 않는다.
| 위치 | 용도 |
|---|---|
command | 서버 실행 파일 경로 |
args | 명령행 인자 |
env | 서버에 전달할 환경 변수 |
url | HTTP 서버 타입에서 |
headers | HTTP 서버 인증에서 |
공식 문서의 정석 예시가 두 문법을 한 번에 보여 준다.
{
"mcpServers": {
"api-server": {
"type": "http",
"url": "${API_BASE_URL:-https://api.example.com}/mcp",
"headers": {
"Authorization": "Bearer ${API_KEY}"
}
}
}
}
읽어 보면.
url은${API_BASE_URL:-https://api.example.com}/mcp—API_BASE_URL이 설정돼 있으면 그걸 베이스로(예: 스테이징 환경), 없으면 기본 프로덕션 URL을 쓴다. 머신마다 다른 값을 기본값과 함께 다룬다.headers의Bearer ${API_KEY}— 토큰 자체는 파일에 없다. 변수 이름API_KEY만 커밋되고, 실제 키는 각 개발자의 환경에서 온다.
이게 곧 시크릿 분리의 해법이다
패턴을 정리하면 이렇다.
.mcp.json엔${API_KEY}처럼 변수 이름만 적는다. → git에 커밋해도 안전하다.- 진짜 시크릿은 Claude Code가 실행되는 환경에 둔다. 셸에서
export API_KEY=...하거나, 셸 프로필에 넣거나,direnv같은 도구를 쓰거나, 커밋하지 않는.claude/settings.local.json의env필드에 둔다. - Claude Code가
.mcp.json을 읽을 때${API_KEY}를 그 환경 값으로 치환해 서버에 연결한다.
이렇게 하면 *하나의 .mcp.json*을 팀 전체가 공유하면서도, 각자는 자기 토큰으로 연결한다. 34차시에서 “시크릿 든 서버는 일단 local/user에 두는 게 안전한 기본값”이라 했던 임시 처방을, 이제 project 스코프로 올리는 정식 방법으로 대체한다.
팁 — 팀의 온보딩을 돕고 싶으면 저장소에
.env.example(또는 README)로 어떤 변수를 채워야 하는지 목록을 함께 커밋한다. 값은 비운 채 이름만. 신규 팀원은 그걸 보고 자기 환경에 시크릿을 채우면 된다.
가장 흔한 함정 — 변수가 비어 있으면 설정 전체가 죽는다
여기서 반드시 알아야 할 규칙.
필수 환경 변수가 설정돼 있지 않고 기본값도 없으면, Claude Code는 설정 파싱에 실패한다.
즉 ${API_KEY}라고 써 놓고 API_KEY를 환경에 안 넣으면, 그 서버 하나만 조용히 빠지는 게 아니라 .mcp.json 파싱 자체가 실패한다. 그래서 두 가지 원칙이 따라온다.
- 반드시 있어야 하는 값(시크릿 등)은
${VAR}로 쓰고, 그 변수가 환경에 확실히 존재하도록 보장한다(셸 프로필·settings.local.json·온보딩 문서). - 없어도 합리적 기본이 있는 값(베이스 URL, 캐시 경로 등)은
${VAR:-default}로 써서 파싱 실패를 막는다.
미묘한 함정 — 확장은 Claude Code의 환경에서 일어난다
한 가지 헷갈리기 쉬운 지점을 콕 짚는다. ${VAR}는 Claude Code 자신이 실행되는 환경을 기준으로, 설정을 읽는 시점에 치환된다. 서버가 띄워진 뒤 그 서버 프로세스의 환경이 아니다.
이 구분이 실제로 문제가 되는 대표 사례가 CLAUDE_PROJECT_DIR다. Claude Code는 stdio 서버를 띄울 때 그 서버의 환경에 CLAUDE_PROJECT_DIR(프로젝트 루트)을 넣어 준다 — 그래서 서버 코드 안에서는 process.env.CLAUDE_PROJECT_DIR(Node)나 os.environ["CLAUDE_PROJECT_DIR"](Python)로 읽을 수 있다.
그런데 이 변수는 서버의 환경에만 있지 Claude Code 자신의 환경엔 없다. 그래서 project·user 스코프의 .mcp.json에서 command나 args에 ${CLAUDE_PROJECT_DIR}를 그냥 쓰면 — 치환 시점에 그 변수가 “없는” 것으로 취급돼 위의 파싱 실패 함정에 걸린다. 해법은 기본값을 주는 것이다.
{
"mcpServers": {
"local-tool": {
"command": "${CLAUDE_PROJECT_DIR:-.}/scripts/server.sh",
"args": []
}
}
}
${CLAUDE_PROJECT_DIR:-.}처럼 기본값(.)을 줘야 파싱이 통과한다.
단, 플러그인이 제공하는 MCP 설정은 예외다 — 거기서는 Claude Code가
${CLAUDE_PLUGIN_ROOT}와${CLAUDE_PROJECT_DIR}를 직접 치환해 주므로 기본값이 필요 없다. 이 차시에서 다루는 손으로 쓰는 project·user.mcp.json에서만 기본값 규칙이 적용된다.
요점은 하나다 — ${VAR}가 보는 환경은 당신이 claude를 띄운 셸의 환경이지, 붙는 서버의 환경이 아니다.
손으로 안 쓰고 넣기 — claude mcp add-json
서버 문서가 완성된 JSON 조각을 그대로 주는 경우가 많다. 이걸 .mcp.json에 손으로 끼워 넣는 대신, 통째로 던질 수 있다.
# 기본 구문
claude mcp add-json <name> '<json>'
# HTTP 서버
claude mcp add-json weather-api \
'{"type":"http","url":"https://api.weather.com/mcp","headers":{"Authorization":"Bearer token"}}'
# stdio 서버
claude mcp add-json local-weather \
'{"type":"stdio","command":"/path/to/weather-cli","args":["--api-key","abc123"],"env":{"CACHE_DIR":"/tmp"}}'
붙였으면 확인한다.
claude mcp get weather-api
몇 가지 짚을 점.
- 넘기는 JSON은 엔트리 하나의 내용이다 —
mcpServers로 감싸지 않는다. 이름은 첫 인자(weather-api)로 따로 준다. - 셸에서 JSON이 제대로 이스케이프돼야 한다. 작은따옴표로 전체를 감싸 셸이 내부 큰따옴표를 건드리지 않게 하는 게 보통이다.
add-json도 스코프를 따른다 — 기본은 local이고,--scope project면.mcp.json에 기록되고,--scope user면~/.claude.json최상위에 들어간다.- JSON은 MCP 서버 설정 스키마를 따라야 한다 — 즉 위에서 배운 그 필드들이다.
add-json은 “이미 JSON이 있는데 손으로 병합하기 싫다”는 경우의 길이고, 손으로 여는 건 “읽고 미세 조정한다”는 경우의 길이다. 둘은 같은 파일을 다루는 두 방식일 뿐이다.
이미 Claude Desktop에 있다면 — claude mcp add-from-claude-desktop
Claude Desktop을 먼저 써 봤다면, 거기에 설정해 둔 MCP 서버를 Claude Code로 그대로 가져올 수 있다.
claude mcp add-from-claude-desktop
실행하면 대화형 선택 창이 떠서, 가져올 서버를 고를 수 있다. 가져온 뒤 확인한다.
claude mcp list
여기서 왜 이게 매끄럽게 되는지가 이 차시의 마무리다. Claude Desktop의 설정 파일 claude_desktop_config.json은 .mcp.json과 똑같은 mcpServers 형식을 쓴다. 예를 들어 Claude Desktop에 파일시스템 서버를 붙였다면 그 파일은 이렇게 생겼다.
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/Users/username/Desktop",
"/Users/username/Downloads"
]
}
}
}
이 차시 앞부분에서 배운 stdio 엔트리 그대로다 — command·args. 그래서 add-from-claude-desktop은 사실상 한 mcpServers 블록에서 다른 mcpServers 블록으로 엔트리를 복사하는 일이다. 참고로 이 파일의 표준 위치는 macOS가 ~/Library/Application Support/Claude/claude_desktop_config.json, Windows가 %APPDATA%\Claude\claude_desktop_config.json이다(Claude Desktop의 Settings → Developer → Edit Config로도 연다).
가져오기에서 알아 둘 점.
- macOS와 WSL(Windows Subsystem for Linux)에서만 동작한다. 네이티브 Windows에서는 이 명령이 동작하지 않는다. 해당 플랫폼의 표준 위치에서 Claude Desktop 설정 파일을 읽는다.
- 가져온 서버는 Claude Desktop에서와 같은 이름을 유지한다.
- 같은 이름이 이미 있으면 숫자 접미사가 붙는다(예:
server_1). - 기본은 local 스코프로 들어온다.
--scope user를 주면 user 설정으로 가져온다.
가져온 서버를 팀과 공유하고 싶으면 한 단계가 더 필요하다 — 가져오기는 local(또는 user)로 들어오므로, 그대로는
.mcp.json에 없다. 34차시에서 배운 대로 스코프는 추가 시점에 고정되니, 공유하려면--scope project로 다시 붙이거나(remove 후 re-add) 엔트리를 손으로.mcp.json에 옮긴다. 그리고 그때 평문 시크릿이 섞여 있지 않은지 — 이 차시의${VAR}분리를 적용했는지 — 다시 확인한다.
흔한 함정
.mcp.json은mcpServers로 감싼다 —add-json은 안 감싼다. 파일을 손으로 쓸 땐 최상위mcpServers객체가 필요하다. 반면claude mcp add-json <name> '<json>'에 넘기는 건 엔트리 하나의 JSON이고 이름은 따로 준다.- 시크릿을
.mcp.json에 평문으로 박지 않는다. 이 파일은 git에 올라간다. 토큰·비밀번호는${VAR}로 빼고 진짜 값은 환경에 둔다. - 확장은 다섯 자리에서만 된다 —
command·args·env·url·headers. 다른 위치의${...}는 치환되지 않는다. - 필수 변수가 비어 있고 기본값도 없으면 설정 전체가 파싱 실패한다. 한 서버만 빠지는 게 아니다. 없어도 되는 값엔
${VAR:-default}로 안전망을 둔다. ${VAR}는 Claude Code의 환경을 본다, 서버의 환경이 아니다. 서버 환경에만 있는CLAUDE_PROJECT_DIR등을command·args에서 참조하려면${CLAUDE_PROJECT_DIR:-.}처럼 기본값이 필요하다(플러그인 설정은 예외).streamable-http는http의 별칭이다. 서버 문서에서 복사한"type":"streamable-http"를 그대로 둬도 된다 — 굳이http로 안 바꿔도 동작한다.add-json의 JSON은 셸 이스케이프가 관건이다. 작은따옴표로 전체를 감싸고, 안 되면claude mcp get으로 실제 저장된 모양을 확인한다.add-from-claude-desktop은 macOS·WSL 전용이다. 네이티브 Windows에서는 안 된다. 같은 이름 충돌은_1접미사로 처리된다.- 가져오기·
add-json의 기본 스코프는 local이다. 팀 공유(.mcp.json)로 올리려면--scope project를 명시하거나 엔트리를 옮긴다. 옮길 때 다시${VAR}분리를 확인한다. .mcp.json을 고쳤는데 안 먹으면 — 예전에 승인 게이트에서 거부했을 수 있다.claude mcp reset-project-choices로 초기화하고 다시 승인한다(34차시).
정리
핵심 요점
.mcp.json은 그냥 JSON이다 — 최상위mcpServers객체 아래 “이름 → 엔트리”.claude mcp add --scope project가 자동 생성·갱신하지만, 스키마를 알면 PR을 읽고 손으로 고칠 수 있다.- 엔트리 필드는 전송 방식별로 갈린다 — stdio는
command·args·env, http는type·url·headers. CLI 플래그(--transport·--env·--header)와 1:1로 대응한다.streamable-http는http의 별칭. - 차시의 심장은 환경 변수 확장 —
${VAR}와${VAR:-default}, 확장되는 다섯 자리(command·args·env·url·headers). 커밋된 파일엔 변수 이름만, 진짜 시크릿은 각자의 환경에 — 이것이 33·34차시가 미룬 “평문 시크릿 분리”의 정식 해법이다. - 두 함정 — (1) 필수 변수가 비어 있고 기본값도 없으면 설정 전체가 파싱 실패한다. (2) 확장은 Claude Code 자신의 환경에서 일어나므로, 서버 환경에만 있는
CLAUDE_PROJECT_DIR등은${CLAUDE_PROJECT_DIR:-.}처럼 기본값이 필요하다(플러그인 설정은 예외). - 손으로 안 쓰고 넣기 —
claude mcp add-json <name> '<json>'. 엔트리 하나의 JSON을 통째로 던진다(셸 이스케이프 주의).--scope를 따른다. - 이미 Claude Desktop에 있다면 —
claude mcp add-from-claude-desktop(macOS·WSL 전용)로 대화형 선택해 가져온다.claude_desktop_config.json은.mcp.json과 똑같은mcpServers형식이라 배운 그대로 읽힌다. 같은 이름은_1접미사.
다음 단계
다음 36차시는 MCP 리소스와 Tool Search다. 지금까지가 서버를 붙이고·관리하고·설정하는 법이었다면, 36차시는 붙인 뒤 잘 쓰는 법이다 — @server:protocol://resource/path 형식의 @ 멘션으로 서버의 리소스를 파일처럼 참조하는 법, 그리고 서버가 많아질 때 컨텍스트를 지키는 핵심인 Tool Search(이 차시에서 미룬 alwaysLoad 포함)다. 마지막으로 조직 단위로 서버를 통제하는 관리형(managed) MCP 설정까지 본다. 이로써 섹션 7(MCP 서버 연동)이 마무리된다.
참고 자료
- Claude Code MCP 공식 문서 (전체 레퍼런스)
- .mcp.json 환경 변수 확장 (
${VAR}·${VAR:-default}·확장 위치) - project 스코프와
.mcp.json표준 형식 - JSON으로 서버 추가 (
claude mcp add-json) - Claude Desktop에서 서버 가져오기 (
claude mcp add-from-claude-desktop) - .mcp.json 직접 편집 가이드 (MCP 퀵스타트)
- Claude Code 설정 파일 위치 (MCP local 스코프 vs settings.local.json)
- Claude Code 환경 변수 레퍼런스 (
MCP_TIMEOUT등) - Claude Desktop 로컬 MCP 서버 설정 (
claude_desktop_config.json위치·형식) - Model Context Protocol 공식 사이트