Skip to content

Claude Code .mcp.json 직접 작성과 ${VAR} 환경 변수 확장 — 서버 엔트리 스키마·시크릿 분리·add-json·Claude Desktop 가져오기

Published: at 09:06 PM

들어가며

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

그럼 왜 스키마를 알아야 하나? 세 가지 이유다.

그러니 “자동 생성에 기대되, 읽고 고칠 줄은 안다”가 이 차시의 목표다.

.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"
      }
    }
  }
}

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"
      }
    }
  }
}

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.jsongit에 커밋된다. 그런데 위 github 예시의 "Authorization": "Bearer your-github-pat"처럼 토큰을 평문으로 박으면, 그 토큰이 저장소 히스토리에 영구히 남는다. 33·34차시가 “project 스코프엔 평문 시크릿 금지”라고 못 박은 이유다. 그렇다고 시크릿이 든 서버를 팀과 공유 못 하는 것도 아니다 — 변수로 분리하면 된다.

Claude Code는 .mcp.json 안에서 환경 변수 확장을 지원한다. 문법은 둘이다.

치환은 다섯 자리에서 일어난다. 그 외 위치에서는 확장되지 않는다.

위치용도
command서버 실행 파일 경로
args명령행 인자
env서버에 전달할 환경 변수
urlHTTP 서버 타입에서
headersHTTP 서버 인증에서

공식 문서의 정석 예시가 두 문법을 한 번에 보여 준다.

{
  "mcpServers": {
    "api-server": {
      "type": "http",
      "url": "${API_BASE_URL:-https://api.example.com}/mcp",
      "headers": {
        "Authorization": "Bearer ${API_KEY}"
      }
    }
  }
}

읽어 보면.

이게 곧 시크릿 분리의 해법이다

패턴을 정리하면 이렇다.

  1. .mcp.json${API_KEY}처럼 변수 이름만 적는다. → git에 커밋해도 안전하다.
  2. 진짜 시크릿은 Claude Code가 실행되는 환경에 둔다. 셸에서 export API_KEY=... 하거나, 셸 프로필에 넣거나, direnv 같은 도구를 쓰거나, 커밋하지 않는 .claude/settings.local.jsonenv 필드에 둔다.
  3. Claude Code가 .mcp.json을 읽을 때 ${API_KEY}를 그 환경 값으로 치환해 서버에 연결한다.

이렇게 하면 *하나의 .mcp.json*을 팀 전체가 공유하면서도, 각자는 자기 토큰으로 연결한다. 34차시에서 “시크릿 든 서버는 일단 local/user에 두는 게 안전한 기본값”이라 했던 임시 처방을, 이제 project 스코프로 올리는 정식 방법으로 대체한다.

팁 — 팀의 온보딩을 돕고 싶으면 저장소에 .env.example(또는 README)로 어떤 변수를 채워야 하는지 목록을 함께 커밋한다. 값은 비운 채 이름만. 신규 팀원은 그걸 보고 자기 환경에 시크릿을 채우면 된다.

가장 흔한 함정 — 변수가 비어 있으면 설정 전체가 죽는다

여기서 반드시 알아야 할 규칙.

필수 환경 변수가 설정돼 있지 않고 기본값도 없으면, Claude Code는 설정 파싱에 실패한다.

${API_KEY}라고 써 놓고 API_KEY를 환경에 안 넣으면, 그 서버 하나만 조용히 빠지는 게 아니라 .mcp.json 파싱 자체가 실패한다. 그래서 두 가지 원칙이 따라온다.

미묘한 함정 — 확장은 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에서 commandargs${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

몇 가지 짚을 점.

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로도 연다).

가져오기에서 알아 둘 점.

가져온 서버를 팀과 공유하고 싶으면 한 단계가 더 필요하다 — 가져오기는 local(또는 user)로 들어오므로, 그대로는 .mcp.json에 없다. 34차시에서 배운 대로 스코프는 추가 시점에 고정되니, 공유하려면 --scope project로 다시 붙이거나(remove 후 re-add) 엔트리를 손으로 .mcp.json에 옮긴다. 그리고 그때 평문 시크릿이 섞여 있지 않은지 — 이 차시의 ${VAR} 분리를 적용했는지 — 다시 확인한다.

흔한 함정

  1. .mcp.jsonmcpServers로 감싼다 — add-json은 안 감싼다. 파일을 손으로 쓸 땐 최상위 mcpServers 객체가 필요하다. 반면 claude mcp add-json <name> '<json>'에 넘기는 건 엔트리 하나의 JSON이고 이름은 따로 준다.
  2. 시크릿을 .mcp.json에 평문으로 박지 않는다. 이 파일은 git에 올라간다. 토큰·비밀번호는 ${VAR}로 빼고 진짜 값은 환경에 둔다.
  3. 확장은 다섯 자리에서만 된다command·args·env·url·headers. 다른 위치의 ${...}는 치환되지 않는다.
  4. 필수 변수가 비어 있고 기본값도 없으면 설정 전체가 파싱 실패한다. 한 서버만 빠지는 게 아니다. 없어도 되는 값엔 ${VAR:-default}로 안전망을 둔다.
  5. ${VAR}Claude Code의 환경을 본다, 서버의 환경이 아니다. 서버 환경에만 있는 CLAUDE_PROJECT_DIR 등을 command·args에서 참조하려면 ${CLAUDE_PROJECT_DIR:-.}처럼 기본값이 필요하다(플러그인 설정은 예외).
  6. streamable-httphttp의 별칭이다. 서버 문서에서 복사한 "type":"streamable-http"를 그대로 둬도 된다 — 굳이 http로 안 바꿔도 동작한다.
  7. add-json의 JSON은 셸 이스케이프가 관건이다. 작은따옴표로 전체를 감싸고, 안 되면 claude mcp get으로 실제 저장된 모양을 확인한다.
  8. add-from-claude-desktop은 macOS·WSL 전용이다. 네이티브 Windows에서는 안 된다. 같은 이름 충돌은 _1 접미사로 처리된다.
  9. 가져오기·add-json의 기본 스코프는 local이다. 팀 공유(.mcp.json)로 올리려면 --scope project를 명시하거나 엔트리를 옮긴다. 옮길 때 다시 ${VAR} 분리를 확인한다.
  10. .mcp.json을 고쳤는데 안 먹으면 — 예전에 승인 게이트에서 거부했을 수 있다. claude mcp reset-project-choices로 초기화하고 다시 승인한다(34차시).

정리

핵심 요점

  1. .mcp.json은 그냥 JSON이다 — 최상위 mcpServers 객체 아래 “이름 → 엔트리”. claude mcp add --scope project가 자동 생성·갱신하지만, 스키마를 알면 PR을 읽고 손으로 고칠 수 있다.
  2. 엔트리 필드는 전송 방식별로 갈린다 — stdio는 command·args·env, http는 type·url·headers. CLI 플래그(--transport·--env·--header)와 1:1로 대응한다. streamable-httphttp의 별칭.
  3. 차시의 심장은 환경 변수 확장${VAR}${VAR:-default}, 확장되는 다섯 자리(command·args·env·url·headers). 커밋된 파일엔 변수 이름만, 진짜 시크릿은 각자의 환경에 — 이것이 33·34차시가 미룬 “평문 시크릿 분리”의 정식 해법이다.
  4. 두 함정 — (1) 필수 변수가 비어 있고 기본값도 없으면 설정 전체가 파싱 실패한다. (2) 확장은 Claude Code 자신의 환경에서 일어나므로, 서버 환경에만 있는 CLAUDE_PROJECT_DIR 등은 ${CLAUDE_PROJECT_DIR:-.}처럼 기본값이 필요하다(플러그인 설정은 예외).
  5. 손으로 안 쓰고 넣기claude mcp add-json <name> '<json>'. 엔트리 하나의 JSON을 통째로 던진다(셸 이스케이프 주의). --scope를 따른다.
  6. 이미 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 서버 연동)이 마무리된다.

참고 자료


Next Post
Claude Code MCP 서버 관리 — 스코프(local·project·user)·우선순위·project 승인 게이트·OAuth 인증 설정