7,656 of 12,915 MCP servers answered tools/list; 1,239 only with a placeholder key or start argument they asked for; 5,259 never answered.
MCP 레지스트리는 수만 개의 서버 목록을 보유하고 있습니다. 이들의 안전성에 대해 쓰여진 대부분의 내용은 해당 서버들을 읽어보는 것, 즉 도구 설명(tool descriptions), README 파일, 코드를 통해 얻습니다. 우리는 나머지 절반에 대한 정보를 원했습니다. 클라이언트가 실제로 설치하고 실행할 때 어떤 일이 벌어지는지? 응답하는가? 무엇을 요청하는가? 시작하는 동안 무엇을 건드리는가?
그래서 HISTOR라는 MCP 서버의 공개 로그는 자신이 발견한 npm 및 PyPI 서버를 설치하고, 각각을 샌드박스(sandbox)에서 실행하며 무슨 일이 일어났는지 기록합니다. 이 레지스트리들은 47,586개의 npm 및 PyPI 패키지를 나열하고 있으며, 크롤링당 최대 8,000개를 실행합니다. 본 게시물은 2026년 10월 13일 기준이며, 그동안 12,915개 서버를 처리한 결과입니다.
우리가 진행한 방법
- 최신 버전으로 새로 설치.
--ignore-scripts 옵션을 사용한 npm (설치 스크립트가 자체적으로 실행되며 추적됨); PyPI 휠(wheels)만 사용하며 소스에서 빌드되는 것은 없음.
- gVisor (
runsc) 환경, 특권이 없는 사용자(unprivileged user), 읽기 전용 루트 디렉토리(read-only root), 512MB 메모리 및 CPU 1개 할당. 설치에 360초가 걸리고, 시작하고 응답하는 데는 150초가 걸림.
- 외부 연결 차단. 모든 DNS 이름은 우리의 트랩(traps)으로 해석되며, 이곳에서 도달한 내용을 기록할 뿐 아무것도 전달하지 않습니다.
- 미끼 파일 배치.
~/.ssh, ~/.aws, ~/.npmrc, ~/.netrc와 같은 파일들, 홈 디렉토리(HOME)에 몇 가지 추가적인 미끼 파일, 그리고 작업 디렉터리(working directory)의 미끼 .env 파일을 준비했습니다.
- 관찰자로서의 MCP:
initialize, tools/list를 실행한 후, 최대 15개의 도구를 각각 한 번씩 캐나리아 인자(canary arguments)와 함께 호출합니다. gVisor는 샌드박스 외부에서 자체적인 시스템 호출 추적(syscall trace)을 기록하므로, 패키지가 무엇을 열거나, 시작하거나, 다이얼(dialled)했는지 숨길 수 없습니다.
- 요청하는 내용 (플레이스홀더로 처리).
MYTOOL_API_KEY is not set으로 종료되는 서버나, 사용법 텍스트에 serve를 명시하는 경우, 해당 서버는 MYTOOL_API_KEY=histor-envkey-<run> 또는 serve와 함께 다시 시작됩니다. 이름은 오직 패키지 자체의 오류 텍스트나 레지스트리 항목에서만 가져옵니다. 값들은 우리가 제공한 것이며 어느 곳에서도 작동하지 않습니다.
응답한 59%
총 12,915개 중 7,656개가 initialize 및 tools/list에 응답했습니다: npm에서 64%, PyPI에서 52%였습니다. 이들 패키지는 총 182,157개의 도구 정의를 나열했으며, 서버당 중앙값은 10개(하나의 서버는 3,631개를 나열함)였습니다.
이 중 1,239개는 요청한 것만으로 시작했습니다: 플레이스홀더 키가 있는 경우 1,039개, 시작 인수가 있는 경우 177개, 둘 다 있는 경우 23개입니다. 클라이언트 설정(client config)을 사용했다면 이들을 제공했을 것이므로, 우리는 이들이 작동한다고 간주합니다. 하지만 1,039개를 다시 살펴보세요. 어느 곳에서도 작동하지 않는 키만으로도 모든 서버가 자신들의 도구를 알려주는 데 충분했습니다. 그들은 아무런 실제 키 없이도 그렇게 할 수 있었고, 단지 키 없이는 시작하기를 거부했을 뿐입니다.
응답하지 않은 5,259개에 대한 분석
Why 5,259 never answered, by what each package printed as it exited: usage text 924, crash 874, key or setting 793, not a stdio server 666, will not install 636, hangs or wrong protocol 490, MCP SDK 1.x on 2.x 292, needs something the machine lacks 236, cannot run as published 184, serves HTTP by default 164.
저희는 각 패키지가 종료될 때 출력한 내용을, 가장 최근 실행된 것부터 역순으로 읽었습니다.
- 거의 절반인 2,476개는 배포된 상태로 작동하지 않습니다. 874개는 자체 오류와 함께 충돌(crash)했습니다. 636개는 설치되지 않을 것입니다: 특정 종속성에 대한 wheel이 없거나, 충돌하는 핀(conflicting pins)이 있거나, 플랫폼이 잘못되었기 때문입니다. 490개는 멈추거나 침묵하거나 MCP가 아닌 내용을 출력합니다. 292개는 MCP SDK 1.x용으로 작성되었습니다(다음 섹션 참조). 184개는 자체 파일을 하나 누락했거나, Node에서
node_modules로부터 로드하지 못하는 TypeScript를 배포했거나, 선언하지 않은 종속성을 가져왔습니다.
- 793개는 키나 설정을 요구하며 플레이스홀더가 있어도 멈춥니다. 일반적인 이유는 다음과 같습니다: 시작 시 키의 형식을 확인하거나(
must start with sk_), 무언가를 응답하기 전에 자체 API를 호출하는데, 우리의 트랩(trap)은 그들의 API가 아니기 때문입니다.
- 924개는 사용법 텍스트(usage text)를 출력합니다. 일부는 다음에 우리가 전달할 시작 인수(
serve, mcp, --stdio)를 원합니다. 많은 것이 MCP라는 단어를 언급하는 명령줄 도구입니다. 아예 명령을 선언하지 않은 666개나, 다른 무언가를 수행한 후 종료 코드 0으로 끝나는 경우를 제외하고, 목록에 있는 패키지 중 최대 8개 중 1개만이 자신이 나열된 방식대로 설치되었을 때 stdio MCP 서버가 아닙니다.
- 164개는 stdin에서 응답하는 대신 HTTP 서버를 시작합니다.
다음 실행에서는 --transport=stdio를 사용합니다.
- 236개는 기기가 갖추지 못한 것이 필요함: 다른 프로그램(git, Java, Bun, 데스크톱 앱), 시작 시 인터넷 연결, 또는 다른 운영체제.
MCP SDK 2.0이 292개의 PyPI 서버를 망가뜨림
MCP SDK 2.0: 207 of 269 PyPI servers start again with mcp<2. Placeholders: 1,039 started with a key, 177 with an argument, 23 with both.
mcp 2.0.0 (2026년 7월 28일)은 FastMCP의 이름을 변경하고 저수준(low-level) 서버의 데코레이터(decorator)를 수정했습니다. 상한선(upper bound) 없이 mcp에 의존하는 PyPI 서버는 오늘날 2.x 버전을 설치하며, 임포트 시 다음과 같은 오류로 인해 작동을 멈춥니다: 'FastMCP' 이름을 가져올 수 없음, 또는 'Server' 객체에 'list_tools' 속성이 없습니다. 저희가 mcp<2로 고정하여 다시 시작한 서버 중 269개와, 그중 207개(77%)가 응답했습니다. 오늘날 이 서버들을 설치하는 사람은 서버를 얻는 것이 아니라 충돌을 경험하게 됩니다.
샌드박스에서 수행된 작업
What they did: 2,239 ran npm install scripts; 1,484 reached for the network at startup; 580 while installing; 295 opened a decoy credential file; 73 runs sent the placeholder key over the network.
11,891개의 실행이 gVisor 트레이스(trace)를 가지고 수행되었습니다.
-
2,239개의 npm 패키지가 설치 스크립트를 실행했습니다. 이는 우리가 실행한 npm 패키지의 약 30%에 해당합니다. 이 중 580개는 설치 과정에서 네트워크 연결을 시도했으며, 주로 사전 빌드된 바이너리(prebuild-install, node-gyp의 헤더 파일, esbuild)를 가져오기 위함이었습니다. 여기서 실패가 발생했습니다. 여러분의 머신에서는 자격 증명이 도달 가능한 상태로 실행됩니다.
-
1,484개가 어떤 도구가 호출되기 전, 시작 시 네트워크에 접속을 시도했습니다. 이들이 가장 많이 요청한 이름은 pypi.org (613), github.com (478), nodejs.org (245), registry.npmjs.org (243)였습니다. 대부분 버전 확인 및 다운로드 목적이었습니다.
-
295개가 가짜 자격 증명 파일을 열었습니다 (.env 파일은 제외). 누구나 당황하기 전에: Python의 requests는 모든 HTTP 요청 전에 ~/.netrc를 읽고 (141), npm과 npx는 ~/.npmrc를 읽으며 (103), boto3의 자격 증명 체인은 ~/.aws를 읽습니다 (39). 나머지 17개 파일들 (~/.kube, ~/.docker, ~/.ssh, 셸 히스토리, gh의 설정)은 Kubernetes, Docker, SSH 또는
-
880개의 서버가 침묵했습니다. 이들은 도움말을 stdout에 출력했으며, 저희는 stderr만 보존했었습니다. 이제는 둘 다와 종료 코드까지 보존합니다. 그중 179개는 정말로 침묵한 경우입니다.
-
약 400개의 '설치 실패' 사례가 발생했습니다. 이는 Python 3.13 또는 3.14를 요구하는데 저희 이미지는 3.12였기 때문입니다. 이때 pip은 from versions: none을 출력했는데, 이는 마치 패키지가 누락된 것처럼 보였습니다. 이제 샌드박스는 Requires-Python에서 이미지를 가져옵니다.
-
1,917개의 Python 서버가 '시작 시도(reached out at startup)'로 표시되었습니다. asyncio의 이벤트 루프는 자신의 socketpair에 바이트를 작성합니다. 저희 트레이스 리더는 목적지 없이 전송된 것을 보고 네트워크 관련으로 분류했습니다. gVisor는 파일 디스크립터인 socketpair가 반환하는 값을 출력하지 않기 때문에, 이제 규칙은 다음과 같습니다: socket()이 만들지 않았고 connect()도 도달하지 않은 소켓에서, socketpair 이후 목적지를 명시하지 않은 전송이 발생하면 해당 프로세스 내에 머무릅니다. 이 모든 서버는 시작 실패 사례를 포함하여 재추적되었으며, 페이지가 이전 읽기 상태를 유지했으나 다시 읽히지 않는 경우도 있었습니다.
-
126개의 '지속성(persistence)' 쓰기가 상대 경로였습니다. 기존 리더는 node-gyp의 buildcheck.gypi와 같은 경로를 해결할 수 없었습니다. gVisor는 각 openat 옆에 디렉토리를 출력하며, 저희는 이를 읽고 있지 않았습니다.
-
레드팀(MOMUS)이 발견한 놓친 사례가 하나 있었습니다. Debian 이미지를 사용하게 된 후, 72가지 동작 케이스 중 하나가 부하 상태에서 관찰되지 않았습니다: glibc의 리졸버가 지연된 DNS 응답을 포기했고, 패키지는 연결하지 못했으며, 실행은 여전히 '완료'라고 표시되었습니다. 이제 리졸버는 더 오래 기다리며, 저희 로거가 본 적 없는 DNS 쿼리가 실행을 미완성(incomplete) 상태로 만들고, 결코 깨끗하게 완료되지 않게 합니다. 관찰자(observer)에 대한 모든 변경 사항은 이 72가지 케이스를 다시 통과해야 하며, 그렇지 않으면 HISTOR의 출시 게이트는 빨간색으로 유지됩니다.
-
실제 패키지가 더 많은 수신자에게 미끼 데이터를 메일링하는 것이 확인되었습니다. 이전에는 그렇지 않았습니다. 트랩은 실행을 컨테이너 주소로만 알고 있으며, Docker는 해제된 주소를 다음 컨테이너에 할당합니다. 저희의 창(window)이 docker run 이전에 열렸기 때문에, 잠시 전에 해당 주소를 보유했던 레드팀 테스트 환경의 마지막 요청을 수신했습니다.
수신자는 장치(fixture)의 실행 태그를 가지고 있었고, 이것이 우리가 그것을 포착한 방식입니다. 이제 증거는 컨테이너 자체의 시작부터 제거될 때까지, 그리고 연결이 열린 시점까지만 일치합니다. 모든 기록은 다른 실행의 카나리아(canary)가 담겨 있어 다시 추적되고 있습니다.
MCP 서버를 게시할 경우
- 키 없이 도구를 나열하세요. 도구가 호출될 때 요청하십시오. 시작 시 형식 검사를 하지 마십시오.
- 기본값은 stdio로 하거나, 오류 텍스트에서 이를 활성화하는 플래그 이름을 명시하십시오.
mcp<2를 고정하거나 2.x로 이동하세요.
- 하나의 명령을 배포하세요. 패키지에 CLI가 포함되어 있다면, 서버 이름에
mcp를 넣으십시오.
- JSON-RPC용 stdout을 유지하십시오. 로그, 배너 및 도움말은 stderr로 전송됩니다.
Requires-Python을 정직하게 설정하고 wheels를 게시하세요.
- 설치 또는 시작 시 바이너리를 다운로드하지 마십시오. 플랫폼별 선택적 종속성으로 배포하십시오.
실행 내용을 확인하세요
모든 패키지의 기록은 histor.modelmarket.dev에서 공개적으로 확인할 수 있습니다: 사용된 도구, 요청한 내용, 건드린 것, 어떤 이미지에서 실행했는지 등이 포함됩니다. WARDEN은 동일한 로그를 읽습니다. npx -y @aimarket/[email protected] scan --histor는 HISTOR가 Claude, Cursor 또는 VS Code 설정의 모든 서버에 대해 다른 사람들이 받는 도구 세트를 얻는지 확인하고, 해당 패키지가 샌드박스에서 무엇을 했는지 보여줍니다.
저희는 HISTOR와 WARDEN을 구축합니다. 수치는 2026년 10월 13일의 스냅샷이며; 크롤링은 계속되므로 사이트는 이동했을 것입니다. 여기에 이름이 붙여진 패키지는 없습니다. 방법론과 관찰자 코드는 alexar76/histor (docs/architecture.md, sandbox/histor_observe.py)에 있습니다.