Codex · 확장점: MCP 연결 + Plugins (설치 배포 단위)

한 줄 요약

Codex 확장은 두 갈래다 — MCP = “지금 이 대화에 도구를 꽂는 케이블”, Plugin = “스킬·앱·MCP·훅을 매니페스트 하나로 묶어 배포하는 봉지”. AI에게 손발을 달고 팀에 배포하는 표준 방법이라, 이 두 단어 차이만 잡아도 확장 설계의 8할이 풀린다.


그림

flowchart TD
  C[config.toml 의 mcp_servers] --> MGR[McpManager 카탈로그 합치기]
  P[플러그인 안 .mcp.json] --> MGR
  E[확장 기여자 extension] --> MGR
  MGR --> CAT[MCP 서버 카탈로그]
  CAT --> CLI["RmcpClient 서버 켜기<br/>stdio · HTTP+OAuth · 같은프로세스"]
  CLI --> TOOLS["도구목록 받기 tools/list"]
  TOOLS --> EXP["노출 결정<br/>바로보임 vs 검색뒤로미룸 + 가시성"]
  EXP --> SPEC[모델에게 mcp__서버__도구 형태로 보임]
  SPEC --> MODEL[모델]
  MODEL -->|도구 호출| CALL[승인 받고 서버로 중계]
  CALL --> CLI
  PLG[플러그인이 멘션됨] --> INJ[플러그인 안내문 주입] --> MODEL

그림 읽는 법

왼쪽 세 갈래(config·플러그인·확장)가 하나의 카탈로그로 합쳐지고, 그 카탈로그가 서버를 켜서(RmcpClient) 도구 목록을 받아온 뒤, 도구들이 모델에게 보이는 카드(mcp__서버__도구)로 바뀐다. 모델이 도구를 부르면 다시 케이블을 타고 서버로 중계된다.


쉽게 풀기

MCP = 도구를 꽂는 케이블

USB 케이블을 떠올리면 된다. Codex 본체는 그대로 두고, 외부 프로그램(MCP 서버)을 꽂으면 그 프로그램이 “내가 할 줄 아는 일 목록”을 건넨다. Codex는 이걸 도구 카드로 바꿔 대화에 올려두고, 모델이 “이 도구 써줘”라 하면 케이블을 타고 서버에 일을 시켜 결과만 받아온다.

꽂는 방식은 둘뿐이다.

  • stdio — Codex가 그 프로그램을 자식 프로세스로 켜서 표준 입출력으로 대화 (내 컴퓨터 프로그램)
  • streamable-HTTP — 원격 서버에 인터넷 접속, 인증(베어러 토큰/OAuth)이 붙음 (남이 운영하는 서버)
flowchart LR
  M["모델: 도구 써줘"] --> CX[Codex]
  CX -->|"stdio: 자식프로세스"| L[내 컴퓨터 MCP 프로그램]
  CX -->|"HTTP+인증"| R[원격 MCP 서버]
  L --> RES[결과]
  R --> RES
  RES --> M

Plugin = 봉지

플러그인은 케이블 하나가 아니라 여러 물건을 담은 장바구니 봉지다. 안에 스킬(skills) + 앱(apps) + MCP 서버 정의(mcp) + 훅(hooks)이 들어가고, 이 모두를 **매니페스트 하나(plugin.json)**가 묶는다. 마켓플레이스나 깃에서 봉지째 설치·캐시·활성화하며, 안의 MCP 서버는 켜지는 순간 위 MCP 케이블 경로를 그대로 탄다.

flowchart TD
  PKG[plugin.json 매니페스트] --> S[skills]
  PKG --> A[apps]
  PKG --> MC[mcp .mcp.json]
  PKG --> H[hooks]
  MC -.켜지면.-> CABLE[MCP 케이블 경로 그대로]

시험에 나오면

MCP = 런타임에 도구를 꽂는 연결 · Plugin = 훅·스킬·MCP·앱을 한 매니페스트로 배포하는 봉지. 플러그인은 MCP를 “담을 수 있는” 더 큰 단위다.

사용자 config는 TOML, 플러그인 파일은 JSON

과제 브리핑엔 “PluginManifest … TOML”이라 적혔으나 실제 소스는 JSON이다. 매니페스트는 .codex-plugin/plugin.json(또는 .claude-plugin/plugin.json)에서 serde_json::from_str로 파싱된다 (core-plugins/src/manifest.rs:141).

  • MCP 서버 정의 = .mcp.json · 앱 = .app.json · 훅 = hooks/hooks.json
  • config 레벨 MCP = config.toml[mcp_servers.<name>]
  • 사용자 config는 TOML, 플러그인 매니페스트/자원파일은 JSON.

핵심 정리

MCP 서버 설정 필드

전송은 stdiostreamable_http 중 자동 판정 — command가 있으면 stdio, url이 있으면 HTTP다 (config/src/mcp_types.rs:424, serde(untagged)).

필드언제메모
command/args/env/cwdstdiocommand 있으면 stdio 판정
urlHTTPurl 있으면 HTTP 판정
bearer_token_env_var/oauthHTTP 인증평문 대신 env 변수명/OAuth
enabled_tools/disabled_tools도구 허용/차단allowlist · denylist

플러그인 매니페스트 필드 (plugin.json, JSON·camelCase)

경로 필드(skills/mcpServers/apps/hooks)는 반드시 "./..." 상대경로, ..는 금지 (plugin/src/manifest.rs:8, :390).

필드필수기본/메모
name사실상 필수비면 디렉터리 이름으로 대체 (manifest.rs:142)
version/description/keywords아니오메타데이터
skills/mcpServers/apps/hooks아니오기본 ./skills·./.mcp.json·./.app.json·./hooks/hooks.json
interface아니오UI/모델용 메타

모델 눈에 도구는 이렇게 보인다

  • 각 도구는 mcp__<서버명>__<도구명> 네임스페이스로 보인다 (codex-mcp/src/tools.rs:31, mcp/mod.rs:65).
  • 서버명이 충돌하면 해시 접미가 붙는다.
  • codex_apps예약 서버명이라 직접 쓰면 안 된다 (mcp_tool_exposure.rs:54/:64).

실제 예시

A. config에 MCP 서버 직접 붙이기 (가장 빠른 길)

# 파일경로: $CODEX_HOME/config.toml  (사용자 config — TOML)
# (1) stdio: Codex가 자식 프로세스를 launch
[mcp_servers.docs]
command = "my-docs-mcp"
args = ["--root", "/srv/docs"]
startup_timeout_sec = 10
default_tools_approval_mode = "prompt"
 
# (2) streamable-HTTP + bearer 토큰 (OAuth는 자동 협상)
[mcp_servers.search]
url = "https://mcp.example.com/sse"
bearer_token_env_var = "SEARCH_MCP_TOKEN"
http_headers = { X-Team = "growth" }

B. 배포용 플러그인 패키지 구조

my-plugin/
├─ .codex-plugin/plugin.json   # 매니페스트 (JSON)
├─ .mcp.json                   # MCP 서버 정의 (config와 동일 스키마)
├─ .app.json                   # 앱(connector) 선언 (선택)
├─ skills/                     # 스킬 (선택)
└─ hooks/hooks.json            # 훅 (선택)

C. 도구 이름 네임스페이스 생성 코드

// 파일경로: codex-rs/codex-mcp/src/mcp/mod.rs
const MCP_TOOL_NAME_PREFIX: &str = "mcp";
const MCP_TOOL_NAME_DELIMITER: &str = "__";
 
pub fn qualified_mcp_tool_name_prefix(server_name: &str) -> String {
    sanitize_responses_api_tool_name(&format!(
        "{MCP_TOOL_NAME_PREFIX}{MCP_TOOL_NAME_DELIMITER}{server_name}{MCP_TOOL_NAME_DELIMITER}"
    ))
}

안에서 무슨 일이 벌어지나

도구가 카탈로그에 합쳐지고 → 서버가 켜지고 → 노출 방식이 정해지고 → 모델 호출이 중계되는 생명주기다.

sequenceDiagram
  participant Mgr as McpManager
  participant Cli as RmcpClient
  participant Exp as 노출판정
  participant Mod as 모델
  Mgr->>Mgr: config+plugin+extension 병합<br/>(apps_enabled면 codex_apps 등록)
  Mgr->>Cli: 서버 연결 (stdio/HTTP+OAuth/in-proc)
  Cli->>Cli: initialize 핸드셰이크<br/>(404면 자동 재초기화)
  Cli->>Exp: tools/list
  Exp->>Mod: direct vs deferred 결정<br/>(도구 100개↑면 자동 deferred)
  Mod->>Cli: mcp__서버__도구 호출
  Cli->>Cli: 인자파싱→승인→call_tool→결과정제
  Cli->>Mod: emit

요약 & 셀프체크

세 줄 요약:

  1. MCP는 런타임에 외부 도구를 꽂는 케이블, 전송은 stdio(내 프로그램) 또는 streamable-HTTP(원격+인증) 둘 중 하나.
  2. Plugin은 스킬·앱·MCP·훅을 plugin.json(JSON) 하나로 묶어 배포하는 봉지, 안의 MCP는 켜지면 MCP 경로를 그대로 탄다.
  3. 도구는 모델에게 mcp__서버__도구로 보이며, 100개를 넘으면 자동으로 검색 뒤로 미뤄진다(deferred).

스스로 답해보기:

  • 내 서버가 stdio인지 streamable_http인지는 어떤 필드 유무로 결정되나?
  • config와 플러그인 매니페스트의 파일 형식은 각각 무엇이고, 왜 헷갈리나?
  • 도구를 50개만 노출하게 설계했는데 합쳐서 120개가 됐다. 모델 입장에서 뭐가 달라지나?

연결

CX_개요 · _분석축_루브릭


근거 파일