중간값 MCP 서버는 94개의 패키지를 설치하며, 88%가 stdio 프로세스에 HTTP 프레임워크를 끌어들입니다
요약
MCP(Model Context Protocol) 서버의 의존성을 분석한 결과, 단순한 stdio 방식임에도 불구하고 평균 94개의 패키지가 설치되는 과도한 의존성 문제를 발견했습니다. 특히 88%의 서버가 불필요하게 HTTP 프레임워크를 포함하고 있음을 확인했습니다.
핵심 포인트
- MCP 서버 설치 시 중간값 94개의 패키지가 설치됨
- stdio 전송 방식 서버의 88%가 불필요한 HTTP 프레임워크를 포함
- 단순한 도구임에도 과도한 의존성 트리가 형성되는 경향 확인
- 공식 MCP 레지스트리 내 6,139개 npm 패키지 대상 전수 조사 결과
이번 주에 저는 MCP 서버를 하나 구축했습니다. 이를 게시하기 전에, 제가 다른 누군가가 해주길 바랐던 일을 직접 해보았습니다. 바로 이 서버를 설치했을 때 실제로 내 컴퓨터에 무엇이 설치되는지 확인하는 것이었습니다.
설치 결과 95개의 패키지가 다운로드되었습니다. 제 서버는 단 두 개의 파일과 145줄의 코드로 이루어져 있습니다. 하나의 읽기 전용 도구(tool)를 노출하며, 자체적인 stdin 및 stdout을 통해 JSON-RPC로 통신하고, 소켓(socket)을 절대 열지 않습니다.
그런데 이 95개의 패키지 안에는 express, hono, 그리고 @hono/node-server가 포함되어 있었습니다.
이 사실은 저를 충분히 괴롭혔고, 이것이 저만의 문제인지 확인하고 싶게 만들었습니다. 확인 결과, 저만의 문제가 아니었습니다. 저는 공식 MCP 레지스트리(registry)에 있는 모든 npm 패키지 기반 stdio 서버를 측정했습니다. 총 6,139개 중 6,030개가 해결 가능한(resolvable) 상태였으며, 설치 패키지의 중간값은 94개였습니다. v1 SDK로 구축된 88%의 서버에 대한 중간값은 95개였으며, 이는 제가 의도하지 않았음에도 제 서버가 위치한 지점과 정확히 일치했습니다.
흥미로운 점은 숫자가 아니었습니다. 거의 아무도 해당 프레임워크를 선택하지 않았으며, 그들이 작성한 문서 어디에서도 이를 확인할 수 없다는 점이었습니다.
측정 내용
제가 데이터를 가져왔을 때 공식 MCP 레지스트리에는 18,990개의 서버가 있었으며, 그중 18,788개가 활성 상태로 표시되어 있었습니다. npm 패키지를 배포하면서 stdio 전송 방식(transport)을 선언한 항목으로 필터링하면 6,139개의 고유한 npm 패키지가 나옵니다. 이것이 분석 대상인 모집단입니다.
각 패키지에 대해, 프로덕션 설치 시 무엇이 해결되는지 npm에 요청했습니다:
npm install <package>@<version> \
--omit=dev --package-lock-only --no-audit --ignore-scripts
--package-lock-only 옵션은 단 하나의 tarball도 다운로드하지 않고 전체 트리를 해결하므로, 6,139개의 패키지를 분석하는 것이 가능했습니다.
스크립트와 측정된 데이터는 research/mcp-dependency-census 리포지토리에 있습니다. 이 포스트는 자체 도구(instrument)에 대한 주장을 담고 있으므로, 해당 도구도 함께 제공됩니다. article_numbers.py를 실행하면 아래에 표시된 모집단, 분포, 프레임워크 및 SDK 수치가 각각의 라벨과 함께 다시 출력됩니다. 네트워크가 필요한 수치들 — 샘플링된 의존성 선언(dependency declarations), SDK 버전 테이블, 그리고 크기 — 는 리포지토리에 포함되어 있지 않습니다.
6,030개는 해결되었고, 109개는 해결되지 않았습니다. 그 이유를 언급할 가치가 있습니다: 58개는 npm에 게시되지 않은 버전을 선언했고, 29개는 npm에 아예 존재하지 않는 패키지 이름을 명시했으며, 19개는 npm이 사양(spec)으로 파싱할 수 없는 식별자를 가지고 있었고, 나머지 3개는 기타 이유로 실패했습니다 — 하나는 이 플랫폼을 거부했고, 하나는 시간 초과(timeout)가 발생했으며, 하나는 0이 아닌 종료 코드(non-zero exit)로 종료되었습니다. 이 109개는 아래의 모든 수치에서 제외되었습니다.
일관되게 사용할 용어에 대한 참고 사항: 각 행은 서버가 아니라 npm **패키지 (package)**입니다. 166개의 패키지가 레지스트리에서 두 개의 서로 다른 서버 이름으로 나타나며, 저는 이를 한 번만 카운트했습니다.
첫 번째 확인이 무가치했기에, 자를 두 번 확인하기
믿을 수 없는 결과가 나왔습니다. 서버 패키지는 하나의 슬롯과 SDK가 가져오는 모든 것을 차지하므로, SDK에 의존하는 서버는 SDK 단독보다 더 커야 합니다. 하지만 제 데이터는 중간값 서버가 더 작다고 말했습니다.
저에게는 두 가지 카운팅 규칙이 있었는데 이를 인지하지 못했습니다. **설치 위치 (Install locations)**는 모든 node_modules/… 항목을 카운트합니다 — 예를 들어 content-type은 하위의 충돌로 인해 SDK 트리 내의 두 가지 버전에서 세 번 나타납니다. **고유 패키지 이름 (Distinct package names)**은 이 세 개를 하나로 합칩니다. 제 검증(validation)은 설치 위치를 측정하여 실제 npm install 결과와 일치했습니다. 하지만 제 측정(measurement)은 고유한 이름을 카운트했습니다. 저는 하나의 자로 검증하고, 다른 자로 측정했던 것입니다.
그래서 문제를 수정하고 다시 실행한 뒤, 6개의 패키지를 실제 설치와 대조하여 검증했습니다. 6개 중 6개가 일치했으며, 저는 하마터면 그 문장을 제 엄밀함(rigour)의 증거로 그대로 발행할 뻔했습니다.
그것은 아무런 증거도 되지 못했습니다. 그 6개의 패키지 모두는 플랫폼 특화 선택적 의존성 (platform-specific optional dependencies)이 전혀 없었으며, 이는 이 도구가 가진 유일한 실패 모드입니다. --package-lock-only는 모든 optionalDependency의 플랫폼 변형에 대해 락파일 (lockfile) 항목을 작성하지만, 실제 설치 (real install)는 사용자의 OS 및 CPU와 일치하는 것들만 압축을 해제합니다. kubernetes-mcp-server@0.0.65의 경우, 락파일에는 7개가 나열되어 있지만 실제 설치 시에는 2개만 생성됩니다. 제 검증 세트가 시도했더라도 이를 감지할 수 없었을 것입니다.
두 가지 도구를 더 거친 후 — npm install --dry-run --json은 권위 있어 보였으나 실제 설치와 비교했을 때 appium-mcp를 43%나 적게 계산했습니다 — 저는 os/cpu를 기준으로 선택적 항목 (optional entries)을 제한하기로 결정했고, 처음부터 제대로 했어야 하는 방식으로 검증했습니다: 실제 모집단에서 무작위로 선택된 150개 패키지에 대한 실제 설치 (real installs).
| 패키지 수 | 정확히 일치 | |
|---|---|---|
| 선택적 항목 없음 | 128 | 128 (100%) |
| ... |
1.5%에서 2.0% 사이의 초과 계산이 3건 있었습니다. 적게 계산된 경우는 단 한 건도 없었습니다. 표본 중앙값(Sample median): 측정값 94, 실제값 94. 따라서 아래 수치들은 무작위 표본의 98%에 대해 정확했으며, 결코 낮게 측정되지 않았고, 최악의 경우 플랫폼 특화 바이너리를 포함하는 트리에서 2% 높게 나타났습니다. 중앙값은 영향을 받지 않았는데, 중앙값에 위치한 트리들은 선택적 항목을 전혀 포함하고 있지 않기 때문입니다.
제가 이를 상세히 설명하는 이유는, 이 포스트의 첫 번째 버전이 여전히 가지고 있던 버그를 구조적으로 찾아낼 수 없는 방식으로 검증하면서, 자신의 도구를 확인했다고 스스로를 축하하는 섹션이 포함되어 있었기 때문입니다.
분포 (The distribution)
| 설치된 패키지 수 | 패키지 수 | 비중 |
|---|---|---|
| 1 | 443 | 7.3% |
| ... |
중앙값은 94입니다. 제1사분위수(First quartile) 또한 94입니다 — 2,356개의 패키지가 정확히 94개로 결정되며, 모집단의 52.6%가 중앙값으로부터 5개 패키지 이내에 위치합니다. 가장 많은 패키지가 결정되는 경우는 2,052개입니다.
6,030개 중 5,280개 — 87.6% — 가 HTTP 서버 프레임워크를 설치합니다:
| 프레임워크 | 패키지 수 |
|---|---|
express | 5,276 |
| ... |
이들 각각은 레지스트리 (registry)에 stdio 전송 (stdio transport)을 선언하고 있습니다.
94라는 벽이 생기는 이유
이 패키지들의 87.8%는 프로덕션 트리 (production tree)에 v1 SDK인 @modelcontextprotocol/sdk를 포함하고 있습니다. 이 패키지 하나만으로도 93개의 설치 위치 (install locations)가 결정됩니다. express, hono, @hono/node-server, cors, jose, ajv, 그리고 express-rate-limit는 모두 해당 SDK의 dependencies (의존성)에 포함되어 있으며, 이는 선택적 (optional)이거나 피어 의존성 (peer dependency)이 아닙니다. 사용자가 어떤 전송 방식 (transport)을 사용하든 이들은 모두 설치됩니다.
현재 버전의 SDK v1을 해결 (resolving)하는 4,992개의 패키지 중, 45.9%는 정확히 94개에서 멈춥니다. 즉, 자기 자신과 SDK의 93개 패키지 외에는 npm이 풀어낼 (unpack) 다른 것이 아무것도 없는 상태입니다.
이것이 무엇을 의미하고 무엇을 의미하지 않는지에 대해 신중하게 말하고자 합니다. 왜냐하면 이 포스트의 초안에서 저는 이를 잘못 파악했기 때문입니다. 이것은 해당 개발자들이 단 하나의 의존성만을 선언했다는 것을 의미하지 않습니다. 저는 400개를 샘플링하여 공개된 dependencies를 읽어보았습니다. 35.8%는 정확히 하나를 선언했고, 대부분은 두 개를, 일부는 네 개를 선언했습니다. 어떤 서버는 zod, cross-spawn, zod-to-json-schema를 목록에 올리더라도 여전히 94개에 머물 수 있는데, 이는 세 가지 모두 이미 SDK의 트리 (tree) 내부에 있기 때문입니다. "아무것도 추가하지 않는다"는 것은 중복 제거 (deduplication)에 관한 사실이지, 저자 (authorship)에 관한 사실이 아닙니다. 제 초안에서는 전체적인 도덕적 논거를 담은 문장에서 이 두 개념을 은연중에 하나로 묶어버렸습니다.
남은 사실은 더 좁지만 여전히 말할 가치가 있습니다. HTTP 프레임워크를 포함하는 5,280개의 패키지 중, 99.1%가 v1 SDK를 사용하고 있습니다. express를 직접 입력한 사람은 거의 없었습니다.
아무도 선택하지 않은 숫자
이 부분은 제가 첫 번째 검토 때 완전히 놓쳤던 부분이며, 이 포스트에서 가장 중요한 내용입니다.
94는 이 개발자 중 누구도 고정 (pinned)한 숫자가 아닙니다. 그것은 오늘날 npm이 그들의 범위 (ranges)를 부동 (floats)시킨 결과입니다. 샘플링된 400개의 패키지 모두, 400개 모두 SDK에 대해 캐럿 범위 (caret range)를 선언했습니다. 단 하나도 버전을 고정하지 않았습니다. 그리고 SDK의 무게는 엄청나게 커졌습니다:
| SDK 버전 | 게시일 | 설치 위치 | express | hono |
|---|---|---|---|---|
| 1.0.0 | 2024-11-25 | 14 | no | no |
| ... | ||||
2024년 말 ^1.0.0에 대해 게시된 서버는 출시 당일에 약 14개의 패키지를 설치했습니다. 변경되지 않은 동일한 package.json이 오늘날에는 94개를 설치합니다. express는 2025년 4월경 SDK의 의존성 (dependencies)에 추가되었고, hono는 2025년 12월에 추가되었습니다. 둘 다 오래된 것이 아니며, 현재 이를 포함하고 있는 5,203개의 서버에 어떠한 요구 사항도 부과하지 않았습니다. |
따라서 "그 한 줄이 과거에 치렀던 비용"에 대한 제 초안의 문장은 화살표 방향이 반대로 되어 있었습니다. 이 저자들이 작성한 내용 중 무거워진 것은 아무것도 없습니다. 캐럿 범위 (caret range)는 캐럿 범위가 하는 일을 수행했고, 상류 (upstream)에서 HTTP 스택을 추가했으며, 5,295개의 의존성 트리 (dependency trees)는 그중 단 하나의 커밋도 없이 성장했습니다. 94개라는 벽은 누군가의 코드 특성이 아니라, 어느 날 오후의 npm을 찍은 사진일 뿐입니다.
이는 또한 이 포스트의 유효 기간이 정해져 있으며, 다음 SDK 출시 이후에 이를 다시 실행한다고 해서 이 수치들이 재현되지는 않을 것임을 의미합니다. 저는 하루 단위의 규모에서 드리프트 (drift)를 입증할 수는 없었습니다. 몇 시간 후 413개의 무작위 행을 다시 해결 (re-resolve)해 보았으나 모두 일치했습니다. 하지만 이는 400개 중 400개의 캐럿 범위로부터 기계적으로 도출되는 결과입니다.
3일 전에 무엇이 변했는가
2026-07-27에 TypeScript SDK v2가 출시되면서 모놀리스 (monolith) 구조가 분리되었습니다: @modelcontextprotocol/server, 그 아래의 @modelcontextprotocol/core, 그리고 HTTP 프레임워크 어댑터 (/express, /fastify, /hono)가 HTTP를 서비스할 경우에만 설치하는 별도의 패키지로 나뉘었습니다.
저는 마이그레이션(migration)을 진행했습니다. 제 서버는 95개의 패키지에서 5개로 줄었습니다. 이제 npm install frisk-mcp를 실행하면 패키지 자체와 @modelcontextprotocol/server, @modelcontextprotocol/core, zod, 그리고 제가 만든 스크리닝 라이브러리만 가져옵니다. 웹 프레임워크는 없습니다. 두 숫자 모두 위에서 언급한 방식과 동일하게 계산됩니다: 패키지 자체와 npm이 이를 위해 압축을 푸는 모든 항목을 포함합니다. 코드 변경은 단 두 줄의 임포트 (import) 문이었으며, 나머지는 코드모드 (codemod)가 제 dependencies에 수행한 작업을 수정하는 것이었습니다. 코드모드는 MCP 클라이언트 (client) 패키지를 서버가 필요하지 않은 런타임 의존성 (runtime deps)에 넣어두었습니다.
측정 시점(6,030개 중 27개)에는 출시 후 3일 만에 v2를 포함하는 패키지가 있었습니다. 그리고 그 0.4%라는 수치는 들리는 것보다 부드럽습니다. 즉, 27개 중 9개는 여전히 v1과 전체 express-plus-hono 스택을 가지고 있으며, 이는 101개에서 361개 패키지를 해결합니다. v2 그룹의 3분의 1은 아직 아무것도 제거하지 않았습니다.
제가 주장하는 바가 아닌 것들
종속성(dependency)이 취약점(vulnerability)을 의미하지는 않습니다. 94개의 패키지가 94개의 문제를 의미하지 않으며, express가 안전하지 않다는 것도 아닙니다. 여기서 언급된 어떤 서버도 손상되었거나 잘못 작성되었다고 말하는 내용은 없습니다. 저는 위험도가 아닌 패키지 개수를 측정했습니다.
stdio와 HTTP에 관하여: 레지스트리(registry)는 제가 센 모든 항목에 대해 stdio를 선언하며, 이 프레임워크는 v1 SDK의 하드 종속성(hard dependency)이므로 어쨌든 트리에 포함됩니다. 하지만 이 패키지들 중 일부는 HTTP도 제공하며,
버전에 대하여: 저는 각 레지스트리 항목이 선언하는 버전을 측정했습니다. npx -y <package>를 사용하면 대신 npm에서 가장 최신인 것을 설치하게 되는데, 이는 제가 집계한 것과 다른 트리(tree)로 해석될 수 있습니다.
이것이 왜 중요한가
저는 에이전트(agents)를 위한 결제 심사(payment screening) 작업을 하고 있습니다. 제가 비용을 받고 고민해야 하는 문제는 자율 에이전트(autonomous agent)가 돈을 건네주려는 대상이 무엇인가 하는 점입니다. MCP 서버는 머신 상의 그 어떤 것보다 그 결정에 더 가깝게 위치합니다. 즉, 에이전트와 동일한 권한으로, 동일한 자격 증명(credentials)을 사용하여, 동일한 프로세스 트리(process tree) 내에서 실행됩니다.
그렇기에 제가 직접 만든 서버 — 즉, 무언가를 의심하는 것이 임무인 서버 — 가 실행 경로(code path)가 전혀 없음에도 웹 프레임워크(web framework)를 포함하고 있었으며, 제가 직접 확인해 본 덕분에 그 사실을 알 수 있었다는 점을 분명히 밝힐 가치가 있습니다. 그리고 제가 처음 확인을 시도했을 때 두 번이나 틀렸다는 사실도 말입니다. 한 번은 어떤 측정 도구(ruler)를 사용했는지에 대해서였고, 다른 한 번은 그 측정 도구가 무엇을 볼 수 없는지에 대해서였습니다.
만약 MCP 서버를 유지 관리하고 있다면, 확인 작업은 약 10초 정도 소요됩니다. --dry-run 옵션을 사용하지 말고, 빈 디렉토리에서 실제 설치를 수행하십시오. --dry-run은 제가 잘못되었다고 파악한 두 가지 도구 중 하나입니다.
mkdir /tmp/dep-check && cd /tmp/dep-check && npm init -y
npm install <your-package> --omit=dev
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기