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 서버 설정 필드
전송은 stdio와 streamable_http 중 자동 판정 — command가 있으면 stdio, url이 있으면 HTTP다 (config/src/mcp_types.rs:424, serde(untagged)).
| 필드 | 언제 | 메모 |
|---|---|---|
command/args/env/cwd | stdio | command 있으면 stdio 판정 |
url | HTTP | url 있으면 HTTP 판정 |
bearer_token_env_var/oauth | HTTP 인증 | 평문 대신 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_enabled면codex_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.
요약 & 셀프체크
세 줄 요약:
- MCP는 런타임에 외부 도구를 꽂는 케이블, 전송은 stdio(내 프로그램) 또는 streamable-HTTP(원격+인증) 둘 중 하나.
- Plugin은 스킬·앱·MCP·훅을
plugin.json(JSON) 하나로 묶어 배포하는 봉지, 안의 MCP는 켜지면 MCP 경로를 그대로 탄다. - 도구는 모델에게
mcp__서버__도구로 보이며, 100개를 넘으면 자동으로 검색 뒤로 미뤄진다(deferred).
스스로 답해보기:
- 내 서버가 stdio인지 streamable_http인지는 어떤 필드 유무로 결정되나?
- config와 플러그인 매니페스트의 파일 형식은 각각 무엇이고, 왜 헷갈리나?
- 도구를 50개만 노출하게 설계했는데 합쳐서 120개가 됐다. 모델 입장에서 뭐가 달라지나?
[!note]- 펼쳐보기: 만들 때 체크리스트(전체)
- MCP 전송: stdio(command 필수) vs streamable_http(url 필수) — 배타 (
untaggedenum)- 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 — 직접 노출 전제로 설계 금지
연결
근거 파일
[!note]- 펼쳐보기: 실제로 열어 확인한 파일 전체
/home/seunghyeong/harness-work/codex/codex-rs/core/src/mcp.rs—McpManager, 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.rs—handle_mcp_tool_call실행/승인/중계/home/seunghyeong/harness-work/codex/codex-rs/rmcp-client/src/rmcp_client.rs—TransportRecipe(stdio/streamable_http+OAuth/in_process), initialize, 세션복구/home/seunghyeong/harness-work/codex/codex-rs/plugin/src/manifest.rs—PluginManifest/...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.rs—plugin_instructions주입/렌더/home/seunghyeong/harness-work/codex/codex-rs/codex-mcp/src/tools.rs·mcp/mod.rs·connection_manager.rs—ToolInfo, 네임스페이스,tool_is_model_visible,CODEX_APPS_MCP_SERVER_NAME/home/seunghyeong/harness-work/codex/codex-rs/config/src/mcp_types.rs—RawMcpServerConfig/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_enabled시codex_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.