지식위키

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

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

[!note] 그림 읽는 법 왼쪽 세 갈래(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 케이블 경로 그대로]

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

[!warning] 사용자 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

[!note]- 펼쳐보기: 타임아웃·승인 등 추가 필드

  • startup_timeout_sec/startup_timeout_ms — 핸드셰이크 타임아웃
  • tool_timeout_sec — 개별 도구 호출 타임아웃
  • enabled — 켜고 끄기 (기본 true)
  • default_tools_approval_mode — 서버 전체 기본 승인 모드 (AppToolApproval)
  • tools — 도구별 approval_mode 등 개별 지정 (map<string, McpServerToolConfig>)
  • env_vars/http_headers/env_http_headers/scopes/oauth_resource — 환경변수·헤더·OAuth 세부

플러그인 매니페스트 필드 (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/모델용 메타

[!note]- 펼쳐보기: interface 안에 들어갈 수 있는 것 displayName, developerName, shortDescription, longDescription, category, capabilities[], websiteUrl, defaultPrompt(최대 3개·각 128자), brandColor, composerIcon, logo, screenshots[] (plugin/src/manifest.rs:35, core-plugins/src/manifest.rs:44).

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

  • 각 도구는 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" }

[!note]- 펼쳐보기: uvx 패키지형 MCP 켜기 예시

# 파일경로: ~/.codex/config.toml
[mcp_servers.weather]
command = "uvx"
args = ["weather-mcp"]
startup_timeout_sec = 15
default_tools_approval_mode = "prompt"   # auto | prompt | approve ...
disabled_tools = ["debug_dump"]

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

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

[!note]- 펼쳐보기: plugin.json 전문 + 변형(인라인 hooks, .mcp.json, 활성화)

// 파일경로: <plugin_root>/.codex-plugin/plugin.json
{
  "name": "demo-plugin",
  "version": "1.2.3",
  "description": "Inbox helpers",
  "keywords": ["email", "developer tools"],
  "mcpServers": "./.mcp.json",
  "skills": "./skills",
  "apps": "./.app.json",
  "hooks": "./hooks/hooks.json",
  "interface": {
    "displayName": "Demo Plugin",
    "developerName": "Acme",
    "defaultPrompt": ["Summarize my inbox", "Draft the reply"]
  }
}

hooks는 인라인으로도 가능 (core-plugins/src/manifest.rs:100, RawPluginManifestHooks = Path|Paths|Inline|InlineList):

{ "name": "demo-plugin", "hooks": [ { "hooks": [ /* HooksFile 객체 */ ] } ] }

MCP 정의 파일은 config의 mcp_servers와 같은 스키마:

// 파일경로: my-plugin/.mcp.json
{ "mcpServers": { "weather": { "command": "uvx", "args": ["weather-mcp"] } } }

플러그인 활성화 (loader가 [plugins.*]를 읽음 — core-plugins/src/loader.rs:496):

# 파일경로: ~/.codex/config.toml
[plugins."marketplace/my-plugin"]
enabled = true

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

[!note]- 펼쳐보기: 단계별 상세 + 근거 라인

  • 카탈로그: McpManager(core/src/mcp.rs:23)가 config·plugin·extension 세 출처를 합치고, apps_enabledcodex_apps 호환 서버를 자동 등록 (mcp.rs:70).
  • 연결: RmcpClient(rmcp-client/src/rmcp_client.rs:304)가 stdio / streamable-HTTP(+OAuth) / in-process로 서버를 띄우고 initialize 핸드셰이크. 세션 만료(404)면 자동 재초기화 (:1069).
  • 노출: build_mcp_tool_exposure(mcp_tool_exposure.rs:20)가 direct/deferred를 정함. 도구 100개(DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD) 이상이면 자동 deferred (:35).
  • 실행: 모델이 mcp__서버__도구를 부르면 handle_mcp_tool_call(mcp_tool_call.rs:108)이 인자 파싱 → 메타 조회 → 승인 결정 → call_tool 중계 → 결과 정제 후 emit.
  • 플러그인 주입: 멘션 시 build_plugin_injections(plugins/injection.rs:14)가 안내문(developer hint)을 만들어 모델에 넣고, 실제 문구는 render_explicit_plugin_instructions(render.rs:12)가 생성:
Capabilities from the `<displayName>` plugin:
- Skills from this plugin are prefixed with `<displayName>:`.
- MCP servers from this plugin available in this session: `<server>`.
- Apps from this plugin available in this session: `<app>`.
Use these plugin-associated capabilities to help solve the task.

요약 & 셀프체크

세 줄 요약:

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

스스로 답해보기:

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

[!note]- 펼쳐보기: 만들 때 체크리스트(전체)

  • MCP 전송: stdio(command 필수) vs streamable_http(url 필수) — 배타 (untagged enum)
  • HTTP 인증: bearer_token_env_var(평문 금지) 또는 OAuth 중 택1
  • 도구 이름은 mcp__<server>__<tool> (서버명 충돌 시 해시 접미)
  • 매니페스트는 JSON, 경로 필드는 전부 "./..." 상대경로 (.. 금지)
  • 매니페스트 위치: .codex-plugin/plugin.json 또는 .claude-plugin/plugin.json (utils/plugins/plugin_namespace.rs:11)
  • 자원 기본 경로: skills=./skills, mcp=./.mcp.json, apps=./.app.json, hooks=./hooks/hooks.json
  • 플러그인 간 MCP 서버명이 겹치면 나중 것이 스킵됨 (loader.rs:153 경고)
  • codex_apps 서버명은 예약어 — 직접 쓰지 말 것
  • 도구 100개 넘으면 자동 deferred — 직접 노출 전제로 설계 금지

연결

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


근거 파일

[!note]- 펼쳐보기: 실제로 열어 확인한 파일 전체

  • /home/seunghyeong/harness-work/codex/codex-rs/core/src/mcp.rsMcpManager, config/plugin/extension 카탈로그 병합, codex_apps 호환 등록
  • /home/seunghyeong/harness-work/codex/codex-rs/core/src/mcp_tool_exposure.rs — direct vs deferred, 임계값 100, codex_apps 분리
  • /home/seunghyeong/harness-work/codex/codex-rs/core/src/mcp_tool_call.rshandle_mcp_tool_call 실행/승인/중계
  • /home/seunghyeong/harness-work/codex/codex-rs/rmcp-client/src/rmcp_client.rsTransportRecipe(stdio/streamable_http+OAuth/in_process), initialize, 세션복구
  • /home/seunghyeong/harness-work/codex/codex-rs/plugin/src/manifest.rsPluginManifest/...Paths/...Hooks/...Interface
  • /home/seunghyeong/harness-work/codex/codex-rs/core-plugins/src/manifest.rs — 매니페스트 JSON 파싱(load_plugin_manifest/parse_plugin_manifest), ./ 경로 검증
  • /home/seunghyeong/harness-work/codex/codex-rs/core-plugins/src/loader.rs — 로드/캐시/마켓플레이스/깃, 기본 경로
  • /home/seunghyeong/harness-work/codex/codex-rs/core/src/plugins/mod.rs · injection.rs · render.rsplugin_instructions 주입/렌더
  • /home/seunghyeong/harness-work/codex/codex-rs/codex-mcp/src/tools.rs · mcp/mod.rs · connection_manager.rsToolInfo, 네임스페이스, tool_is_model_visible, CODEX_APPS_MCP_SERVER_NAME
  • /home/seunghyeong/harness-work/codex/codex-rs/config/src/mcp_types.rsRawMcpServerConfig/McpServerConfig/McpServerTransportConfig(Stdio|StreamableHttp)
  • /mnt/d/6study/10_프레임워크분석/_원문아카이브/codex/48_running-codex-as-an-mcp-server.md — 공식문서(codex mcp-server, codex/codex-reply 도구)

[!tip]- 펼쳐보기: Codex 교차검증 (원문 분석 보존)

  • 전송은 command/url 유무로 자동 판정되는 serde(untagged) enum, stdio·streamable_http 배타 (config/src/mcp_types.rs:424).
  • 노출 이름 mcp__<server_name>__<tool>, 접두는 qualified_mcp_tool_name_prefix가 생성 (codex-mcp/src/mcp/mod.rs:65). codex_apps(CODEX_APPS_MCP_SERVER_NAME)는 일반 노출 필터에서 제외, connector 게이팅 별도 (mcp_tool_exposure.rs:54/:64).
  • 카탈로그는 McpManager가 세 출처를 합치고, runtime_config_with_context가 충돌 해소, apps_enabledcodex_apps 자동 등록 (core/src/mcp.rs:23, :70, :91).
  • 임계값 DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD = 100, 또는 ToolSearchAlwaysDeferMcpTools 켜지면 deferred, tool_is_model_visible로 숨김 거름 (mcp_tool_exposure.rs:35, connection_manager.rs:88).
  • 실행: arguments 파싱 → 메타 조회 → 승인(approval_mode/hook/guardian/elicitation) → _meta에 thread_id·sandbox_state 첨부 후 call_tool 중계 → 결과 정제(이미지 미지원 모델이면 제거) → emit (mcp_tool_call.rs:108, :119, :140, :547, :864, :1144).
  • 집필 주의: 브리핑의 “매니페스트 TOML”은 오류, 실제는 JSON 파싱 (core-plugins/src/manifest.rs:141). 사용자 config만 TOML.