DeepSeek Harness · 모델과 외부 연동

MCP와 Extensions: 외부 도구 연동의 두 길

생태계 표준 브리지와 네이티브 확장이 어떻게 역할을 나누는지. 핵심 소스:packages/mcp/mcp-client/packages/extensions/

강의 목표읽고 나면 세 가지를 말로 설명할 수 있어요. DSH가 외부 능력을 붙이는 길은 두 개예요 — MCP 브리지는 프로토콜 생태계의 기성 도구 서버를 붙이고, Extensions는 모델이 harness 안에서 플러그인을 바로 쓰고 돌리게 해요. MCP 브리지가 왜 일부러 tools만 브리지하는지, 도구 이름이 hash로 충돌을 어떻게 막는지, 서버가 끊기면 모델이 쥐고 있던 도구에 무슨 일이 생기는지. 그리고 두 길의 신뢰 모델이 어디서 다른지 — 한쪽은 위험을 프로세스 밖에 두고, 한쪽은 승인과 샌드박스로 지켜요.
인터랙티브 데모 · 연동 방식 대조대

먼저 해보고 설명해요. 같은 외부 능력 “날씨 조회”를 왼쪽은 MCP 브리지, 오른쪽은 네이티브 Extension으로 동시에 붙여 봐요. 세 가지를 보세요: 도구 이름이 어떻게 생기는지, 서버가 끊길 때 모델 시점이 어떻게 바뀌는지, 두 길의 능력면이 얼마나 다른지. 「재생」으로 자동 재생하거나 「단스텝」으로 프레임마다 보세요.

길 A · MCP 브리지(외부 프로세스)세대 G1
바깥 세계
weather server(아직 시작 안 함)
원본 도구명 get_forecast(와이어에만 나타남)
harness 안의 ctx.tools 레지스트리
공개명 mcp__weather__get_forecast
도구 이벤트 서비스 UI
길 B · 네이티브 Extension(프로세스 내)
모델의 동작
cordis_define을 호출해 플러그인 소스를 제출
사용자 승인 대기: 이 플러그인을 실행할까요?
실행 중인 두 반쪽
Host 반쪽: node:vm 샌드박스에서 로직 실행
Browser 반쪽: 페이지에 날씨 패널 렌더
도구 이벤트 서비스 UI
「재생」을 눌러, 같은 능력이 두 길로 harness에 붙는 모습을 보세요.
로직 분해 · 길 A: MCP 브리지, 남의 서버를 붙이기

먼저 용어부터요. MCP(Model Context Protocol)는 열린 프로토콜이에요. 누구나 도구 서버를 쓰면, MCP를 지원하는 클라이언트는 연결해서 그 도구를 쓸 수 있어요. DSH의 dsh-mcp-client 플러그인이 바로 그 클라이언트로, 플러그인 인스턴스 하나가 서버 하나에 붙고, stdio 서브프로세스와 streamable-http 전송을 모두 지원해요. 연결되면 하는 일은 단순해요. listTools()로 도구 목록을 받아 공개명으로 ctx.tools에 등록하고, 모델은 그걸 네이티브 도구처럼 써요.

이름 짓기가 첫 설계 포인트예요. MCP 도구마다 이름이 둘이에요. 원본명은 와이어에만 나타나고(tools/call이 씀), 모델이 보는 공개명은 mcp__서버명__원본명이에요. 형식이 Claude Code·Codex와 같고, mcp-client README가 그걸 스스로 짚어요. 이름은 DeepSeek 함수명 규칙을 지켜야 해요. 최대 64자, 영숫자·밑줄·하이픈만. 치환이나 잘림으로 이름이 바뀌면 끝에 12자리 16진 SHA-256 hash를 붙여, 서로 다른 도구 정체성이 한 이름으로 접히지 않게 해요. 전체는 (serverName, rawName)의 순수 함수예요. 연결 순서, 재동기화, 다른 서버도 도구 이름을 못 바꿔요.

packages/mcp/mcp-client/src/tools.ts96–102행
export function publicToolName(serverName: string, rawName: string): string {
  const joined = `mcp__${serverName}__${rawName}`
  const normalized = joined.replace(INVALID_NAME_CHARS, '_')
  if (normalized === joined && normalized.length <= MAX_PUBLIC_NAME_LENGTH) return normalized
  const hash = createHash('sha256').update(`${serverName}\0${rawName}`).digest('hex').slice(0, HASH_LENGTH)
  return `${normalized.slice(0, MAX_PUBLIC_NAME_LENGTH - HASH_LENGTH - 1)}_${hash}`
}
소스 스냅샷 안내: 로컬 저장소 deepseek-harness-master 기준, 확인 파일 packages/mcp/mcp-client/src/tools.ts, 확인일 2026-08-13. 코드 블록은 소스 원문을 유지합니다.

두 번째 설계 포인트는 세대(generation)예요. 서버 도구 목록은 바뀔 수 있고, 바뀌면 다시 동기화해요. 동기화는 두 단계예요. 먼저 다음 세대의 도구 정의를 전부 받아 만들고, 한 단계라도 실패하면 레지스트리는 건드리지 않아 이전 세대가 그대로 살아 있어요. 다 받은 뒤에야 교환해요 — 옛 세대를 해제하고 새 세대를 등록해요.

교환 단계 작성법은 짚을 만해요. 등록 루프에서 도구를 하나 등록할 때마다 해제 함수를 표에 넣어요. 등록이 충돌을 던지면(외부 등록이 이 서버 네임스페이스를 점유했다는 뜻) catch 분기가 그 표에 있는 등록을 전부 해제해 도구를 하나도 남기지 않고, error 로그를 남겨요. 주석이 의도를 직설해요. 롤백은 모델이 완전한 한 세대만 보거나 아예 아무것도 안 보게 하려는 거지, 반쪽 세트는 절대 안 돼요.

출처: packages/mcp/mcp-client/src/tools.ts 159–172행의 등록·롤백 분기, 확인일 2026-08-13.

단선 후 재연결도 세대 위에 쌓여 있어요. stdio 서브프로세스가 죽으면 supervisor가 지수 백오프로 재시작해요. 첫 지연 기본 500ms, 매번 두 배, 상한 30초, 한 번의 중단에 최대 10회(README.zh.md 설정표). 중단 동안 마지막 정상 세대는 등록을 유지해요. 이때 호출은 실패하지만 도구 이름은 사라지지 않아요. 재연결되면 다시 발견하고, 복구된 세대가 옛 세대를 통째로 바꿔 도구가 중복되지도 새지도 않아요. serverName이 그대로면 새 세대 이름도 글자 그대로 같아 KV cache 접두사도 살아요. 예산도 있어요. 연결이 30초 넘게 살면 시도 예산을 리셋해서, 가끔 한 번 죽는 서버는 무한 복구되고, 반복 크래시 루프 서버는 예산을 다 써 해제되며 영원히 재시작하지 않아요.

마지막 설계 포인트는 놓치기 쉬워요. 이 브리지는 일부러 MCP의 tools 능력만 브리지해요. 프로토콜에는 resources(리소스)와 prompts(프롬프트 템플릿)도 있는데 DSH는 둘 다 안 붙였어요. README의 “알려진 제한과 보류 사항” 절이 솔직해요:

“MCP 도구 능력만 브리지해요. 리소스와 프롬프트는 harness 소비 인터페이스가 없어 보류합니다.” 출처: packages/mcp/mcp-client/README.zh.md 111행, 확인일 2026-08-13

논리는 어렵지 않아요. harness 안에 외부 resource나 외부 prompt를 소비할 쪽이 없으니 먼저 교각을 놓을 이유가 없어요. 도구는 소비 주체가 분명해요(agent loop의 도구 호출). 그래서 도구부터 브리지해요. 필요할 때 다리를 놓는 거죠. 이미지·오디오 같은 비텍스트 결과도 손실 투영해서 모델 컨텍스트에는 자리표시자만 두고, 바이너리 페이로드는 넣지 않아요.

로직 분해 · 길 B: Extensions, 모델이 스스로 플러그인을 키우게

둘째 길은 전혀 달라요. Cordis는 DSH의 플러그인 프레임워크고, harness 전체가 Cordis 플러그인 트리예요. Extensions 서브시스템은 모델이 세션에서 Cordis 플러그인을 바로 쓰고 돌리게 해요. 코드를 쓰기 전에 cordis_inspect로 현재 런타임에 어떤 서비스·인터페이스가 있는지 묻고, cordis_define으로 소스를 제출하고, cordis_run으로 시작하고, 필요 없으면 cordis_stop이나 cordis_undefine이에요. 이 다섯 도구는 packages/extensions/tool-cordis가 등록해요.

동적 플러그인은 두 반쪽이에요. Host 반쪽은 Node 쪽 node:vm 샌드박스에서 로직을 돌리고, Browser 반쪽은 페이지에 UI를 그려요. 수명은 ctx.dynamicCordisRunner(packages/extensions/cordis-host-runner/src/index.ts 124행부터)가 한꺼번에 관리해요. Browser 반쪽이 있으면 시작할 때 승인이 필요해요. cordis/request-run 이벤트가 요청을 페이지로 보내고, 사용자가 허용해야 이어가요. 이 플러그인의 이후 버전도 함께 허용할 수 있어요(runHostHalfapproveFutureVersions). Package 버전은 불변이고, 코드를 바꾸면 새 버전을 추가하는 거예요.

두 길을 나란히 보면 직교해요 — 각자가 한쪽을 맡아요. MCP 브리지는 프로세스 밖의 기성 능력을 마주하고, 신뢰 모델은 격리예요. 서버가 죽거나 쓰레기를 돌려주거나 끊겨도 세대와 오류 경로가 브리지 밖에 막아 두지만, 모델에게 줄 수 있는 건 도구뿐이에요. Extension은 모델이 현장에서 만든 코드를 자기 프로세스에서 돌리고, 능력면이 훨씬 커요. 도구·이벤트·서비스·UI를 다 더할 수 있지만, 매번 승인과 샌드박스 아래예요. 하나는 바깥 전기를 붙이고, 하나는 스스로 발전해요.

이름은 순수 함수

공개명은 (serverName, rawName)만으로 정해져요. 둘 다 search라는 도구를 쓰는 서버도 각자 네임스페이스에서 공존하고, 연결 순서나 재동기화는 도구 이름을 바꾸지 않아요.

세대는 전부 아니면 전무

가져오기가 실패하면 레지스트리는 안 건드리고, 등록 충돌이면 세대 전체를 롤백해요. 모델이 보는 건 언제나 완전한 도구 세트이지 반쪽은 아니에요. 단선 동안 옛 세대는 등록을 유지해 호출은 실패해도 이름은 남아요.

브리지는 tools만

resources와 prompts는 일부러 미뤄 두었어요. harness에 소비 인터페이스가 없기 때문이에요. 능력면 차이는 Extensions가 메워요. 도구·이벤트·서비스·UI 네 가지를 다 더할 수 있어요.

가로 비교 · 세 제품이 외부 능력을 붙이는 법

Claude Code: MCP 클라이언트의 풀스펙 구현

복원 소스의 MCP 구현은 DSH보다 훨씬 두꺼워요. 전송 여섯 가지(stdio, sse, sse-ide, http, ws, sdk — restored-src/src/services/mcp/types.ts 23–26행), 설정 출처 일곱 층(local, user, project, dynamic, enterprise, claudeai, managed), OAuth와 15분 캐시. 도구 이름 형식은 DSH와 같고 mcp__server__tool, 권한 규칙은 도구·서버 단위까지예요. DSH에 없는 방어도 있어요. 도구 설명을 2048자로 자르는데, OpenAPI 자동 생성 서버가 설명에 15–60KB 문서를 넣는 걸 봤기 때문이에요(services/mcp/client.ts 217–219행 주석). 자료: claude-code-sourcemap-main/study/chapters/08-mcp.md.

차이는 방향이에요. Claude Code는 MCP를 유일한 공식 확장점으로 깊게·넓게 만들고, DSH는 MCP 브리지를 얇게(tools만) 두고 무거운 능력은 네이티브 Extensions에 남겨요. 전자의 확장은 프로세스 밖이고, 후자는 프로세스 안 길도 하나 더 줘요.

Grok Build: 플러그인 마켓 루트

Grok Build 저장소에는 MCP 클라이언트(crates/codegen/xai-grok-mcp/)와 플러그인 마켓(crates/codegen/xai-grok-plugin-marketplace/)이 함께 있어요. MCP는 프로토콜 호환, 마켓은 배포와 신뢰 — 중앙 심사 생태계 루트예요. MCP 연결·발견·복구는 사이트 Grok 특집에서 이미 줄줄이 확인했어요. MCP 연결·발견·복구를 보세요. 마켓의 발견·신뢰는 Plugin Marketplace 발견과 신뢰예요. 여기서는 반복하지 않아요.

세 곳을 나란히 두면 스펙트럼이 보여요. Grok은 마켓으로 신뢰를 모으고, Claude Code는 일곱 층 설정과 권한 규칙으로 흩어 두고, DSH는 두 길을 나눠 각자 신뢰 모델을 줘요 — 브리지 밖은 격리, 안은 승인.

수업 실습
01

단선·재연결의 전체 타임라인을 추론해 보세요

weather 서버가 모델이 도구 목록을 막 받은 뒤 죽고, 8초 뒤 supervisor가 다시 올리는데 이번 목록에는 get_alerts가 하나 더 있어요. 시간 순으로 추론해 보세요. 죽은 순간 레지스트리에는 뭐가 있나요? 중단 동안 모델이 mcp__weather__get_forecast를 호출하면 뭘 받나요? 재연결 후 레지스트리에 어떤 연산이 일어나고, get_forecast의 공개명은 바뀌나요? 또: 서로 다른 서버 weatherweather2가 둘 다 get_forecast를 노출하면 충돌할까요, 왜요?(힌트: 세대 교체, 이름은 (serverName, rawName)의 순수 함수.)

Takeaway: MCP는 남의 능력을 브리지하고, Extensions는 자기 런타임을 확장해요. 두 길은 직교하고, 각자 신뢰 모델이 있어요. tools만 브리지하는 건 의도예요 — 소비 주체가 없으면 교각을 안 놓아요. 도구 이름은 (serverName, rawName)의 순수 함수이고, 세대 교체는 모델이 쥔 도구 세트가 완전하거나 비어 있게만 보장해 중간 상태는 없어요.