codexMcpServers.ts ×17

Frontier kind: Code frontier

unlabeled · c_52e379ef2f65

35 tests · 5004 LOC · 21 files · introduces 0 tests · 194 LOC · 1 file

Introduces — evidence that enters the hierarchy at this concept

Code
17 ranges194 lines · 1 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
499 ranges5004 lines · 21 files · Browse complete extent
All tests (intent)
35 testsBrowse complete intent

Neighbourhood graph

The orange circle is the focus. Violet and green circles are every ancestor and descendant, broader and narrower, at any distance; blue squares and pink diamonds are the introduced files and exact introduced tests of every visible concept, not only the focus's. Arrows point from broader to narrower concepts and bridge only concepts omitted from this view. Undirected links show source or test introduction. Concept and file size follows LOC; exact test nodes use test-count units.

Introduced files, introduced tests, and structurally relevant concept specialization

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the native relationship evidence on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the native relationship evidence on this page while the interactive map is unavailable.

Native relationship evidence

Every exact file and test below is linked only from the concept that introduces it.

Introduced tests

Every collected test enters the hierarchy at exactly one concept.

No tests are introduced at this concept. Its intent tests are introduced by other concepts.

Introduced code

Every collected source range enters the hierarchy at exactly one concept.

1 file ranked by introduced lines: 194 introduced LOC across 17 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/platform/agentHost/node/codex/codexMcpServers.ts 194 introduced LOC · 17 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- codexMcpServers.ts
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { McpServerType, type IMcpServerConfiguration } from '../../../mcp/common/mcpPlatformTypes.js';
7 > import { McpServerStatus, type McpServerState } from '../../common/state/protocol/channels-session/state.js';
8 > import type { ISdkMcpServer } from '../shared/mcpCustomizationController.js';
9 > import type { McpServerStartupState } from './protocol/generated/v2/McpServerStartupState.js';
10 > import type { McpServerStatus as CodexMcpServerStatus } from './protocol/generated/v2/McpServerStatus.js';
11 > import type { Resource } from './protocol/generated/Resource.js';
12 > import type { ResourceTemplate } from './protocol/generated/ResourceTemplate.js';
13 > import type { Tool } from './protocol/generated/Tool.js';
14 >
15 > /**
16 > * Cached inventory entry for a single MCP server reported by the codex
17 > * app-server. {@link state} drives the AHP customization surface while
18 > * {@link tools} / {@link resources} / {@link resourceTemplates} back the
19 > * read-only `tools/list`, `resources/list` and `resources/templates/list`
20 > * MCP methods so the host can answer them from cache without
21 > * round-tripping to codex.
22 > */
23 > export interface ICodexMcpServerEntry {
24 > readonly state: McpServerState;
25 > readonly tools: readonly Tool[];
26 > readonly resources: readonly Resource[];
27 > readonly resourceTemplates: readonly ResourceTemplate[];
28 > }
29 >
30 > /**
31 > * Translates a codex `mcpServer/startupStatus/updated` lifecycle state
32 > * into the AHP {@link McpServerState} union.
33 > *
34 > * V1 scope: codex's auth states are not surfaced as
35 > * {@link McpServerStatus.AuthRequired}; a connected server is reported as
36 > * {@link McpServerStatus.Ready} regardless of `authStatus`.
37 > */
38 > export function translateCodexMcpStartupState(status: McpServerStartupState, error: string | null | undefined): McpServerState {
39 switch (status) {
40 case 'ready':
53 }
54 }
56 > /**
57 > * Flattens the codex `McpServerStatus.tools` map (`{ [name]: Tool }`)
58 > * into a name-sorted array, dropping any holes the map type allows.
59 > */
60 > export function codexToolMapToArray(tools: CodexMcpServerStatus['tools']): Tool[] {
61 const out: Tool[] = [];
62 for (const key of Object.keys(tools)) {
69 return out;
70 }
72 > /**
73 > * Builds an {@link ICodexMcpServerEntry} from a codex `mcpServerStatus/list`
74 > * entry. Servers returned by `mcpServerStatus/list` are connected and
75 > * serving, so they map to {@link McpServerStatus.Ready}.
76 > */
77 > export function codexMcpStatusToEntry(status: CodexMcpServerStatus): ICodexMcpServerEntry {
78 return {
79 state: { kind: McpServerStatus.Ready },
83 };
84 }
86 > /**
87 > * Builds a name-keyed inventory snapshot from a codex `mcpServerStatus/list`
88 > * response page (or the concatenation of all paginated pages).
89 > */
90 > export function codexMcpListToInventory(data: readonly CodexMcpServerStatus[]): Map<string, ICodexMcpServerEntry> {
91 const inventory = new Map<string, ICodexMcpServerEntry>();
92 for (const status of data) {
95 return inventory;
96 }
98 > /**
99 > * Projects an inventory snapshot to the SDK-neutral
100 > * {@link ISdkMcpServer} list the {@link McpCustomizationController}
101 > * consumes (name + state only — tool/resource payloads stay in the
102 > * inventory and back {@link buildCodexMcpReadResult}).
103 > */
104 > export function inventoryToSdkServers(inventory: ReadonlyMap<string, ICodexMcpServerEntry>): ISdkMcpServer[] {
105 const out: ISdkMcpServer[] = [];
106 for (const [name, entry] of inventory) {
109 return out;
110 }
112 > /**
113 > * Answers the read-only MCP methods (`tools/list`, `resources/list`,
114 > * `resources/templates/list`) from a cached inventory entry without a
115 > * round-trip to codex. Returns `{ handled: false }` for any other method
116 > * so the caller can forward it as an RPC (`tools/call`, `resources/read`)
117 > * or reject it.
118 > */
119 > export function buildCodexMcpReadResult(method: string, entry: ICodexMcpServerEntry): { readonly handled: true; readonly result: unknown } | { readonly handled: false } {
120 switch (method) {
121 case 'tools/list':
129 }
130 }
132 > /**
133 > * Whether two inventory entries expose a different tool set (compared by
134 > * name). Drives the decision to fire `notifications/tools/list_changed`.
135 > */
136 > export function codexMcpToolsChanged(previous: ICodexMcpServerEntry | undefined, next: ICodexMcpServerEntry | undefined): boolean {
137 const a = (previous?.tools ?? []).map(t => t.name).sort();
138 const b = (next?.tools ?? []).map(t => t.name).sort();
142 return a.some((name, i) => name !== b[i]);
143 }
145 > // #region MCP server config → codex per-thread `config.mcp_servers`
146 > //
147 > // Codex's `thread/start.config` dict is applied as per-thread config overrides
148 > // that *merge* with (rather than replace) the user's global
149 > // `~/.codex/config.toml` (verified against the real app-server). We inject the
150 > // workbench's configured MCP servers (the root `mcpServers` config, keyed by
151 > // server name) via `config.mcp_servers` so codex launches them for that
152 > // thread — the same set Copilot passes to its SDK via
153 > // `toSdkMcpServersFromConfigMap`. Feeding them per-thread (rather than as
154 > // process-global `-c` spawn overrides) means each new session picks up the
155 > // current config without restarting the shared app-server.
156 > //
157 > // The codex MCP config schema (`codex-rs/config/src/mcp_types.rs`,
158 > // `RawMcpServerConfig`) infers the transport from the presence of `command`
159 > // (stdio) vs `url` (streamable http) and has no `type` field, so we drop the
160 > // workbench `type` discriminator and map `headers` → `http_headers`.
161 >
162 > /**
163 > * The codex JSON shape for one MCP server inside `thread/start.config.mcp_servers`.
164 > */
165 > export interface ICodexMcpServerConfigJson {
166 > command?: string;
167 > args?: readonly string[];
168 > env?: Record<string, string>;
169 > cwd?: string;
170 > url?: string;
171 > http_headers?: Record<string, string>;
172 > }
173 >
174 > /**
175 > * Narrows an untrusted root-config value to a supported
176 > * {@link IMcpServerConfiguration}: a `stdio` server with a string `command`,
177 > * or an `http` server with a string `url`. Mirrors Copilot's
178 > * `isSupportedMcpServerConfiguration` so a malformed entry can't surface as a
179 > * `command`/`url: undefined` server.
180 > */
181 > export function isSupportedMcpServerConfiguration(value: unknown): value is IMcpServerConfiguration {
182 if (!value || typeof value !== 'object') {
183 return false;
192 return false;
193 }
195 > /**
196 > * Coerces a record's values to strings (codex's `env`/`http_headers` are
197 > * `Map<string, string>`), dropping `null`/`undefined`. The root `mcpServers`
198 > * config is user-authored and only loosely schema-validated, so a stray
199 > * non-string (e.g. a numeric header value) is coerced here rather than passed
200 > * through — an un-coerced value can make codex reject the whole per-thread
201 > * config, disabling every server for the session.
202 > */
203 function toCodexStringRecord(record: Record<string, unknown> | undefined): Record<string, string> {
204 const result: Record<string, string> = {};
213 return result;
214 }
216 > /** Coerces command args to a string array (codex's `args` is `Vec<String>`), dropping `null`/`undefined`. */
217 function toCodexStringArray(values: readonly unknown[] | undefined): string[] {
218 if (!Array.isArray(values)) {
221 return values.filter(v => v !== null && v !== undefined).map(v => String(v));
222 }
224 > /**
225 > * Converts one supported MCP server configuration into codex's JSON shape.
226 > * Optional fields (`args`, `env`, `cwd`, `headers`) come from user-authored
227 > * config that the root schema does not deeply validate, so each is sanitized
228 > * (coerced to the string shapes codex requires, dropping holes) rather than
229 > * trusted, so a single malformed entry can't make codex reject the config.
230 > */
231 > export function toCodexMcpServerJson(config: IMcpServerConfiguration): ICodexMcpServerConfigJson {
232 if (config.type === McpServerType.LOCAL) {
233 const out: ICodexMcpServerConfigJson = { command: config.command };
252 return out;
253 }
255 > /**
256 > * Converts the workbench root `mcpServers` config (server name →
257 > * {@link IMcpServerConfiguration}) into the `mcp_servers` object codex accepts
258 > * in `thread/start.config`. Unsupported/malformed entries are skipped so a bad
259 > * entry can't surface as a `command`/`url: undefined` server. Returns an empty
260 > * object when nothing is configured.
261 > */
262 > export function codexMcpServersFromConfig(servers: Record<string, unknown> | undefined): Record<string, ICodexMcpServerConfigJson> {
263 const out: Record<string, ICodexMcpServerConfigJson> = {};
264 for (const [name, config] of Object.entries(servers ?? {})) {
269 return out;
270 }
272 > // #endregion
273 >
274 > // #region MCP server authentication (reuse the workbench OAuth path)
275 > //
276 > // codex won't expose an OAuth-gated http MCP server's tools until it is
277 > // authenticated (it reports a `failed` startup with a "not logged in" error).
278 > // Rather than drive codex's own `mcpServer/oauth/login` browser flow, we reuse
279 > // the *same* mechanism the Copilot agent uses: report the server as
280 > // `McpServerStatus.AuthRequired` so the workbench acquires an OAuth bearer
281 > // token (VS Code dynamic client registration), then inject that token into the
282 > // server's per-thread `http_headers.Authorization`. Verified against the real
283 > // codex binary: it forwards `http_headers` on every MCP HTTP request, so a
284 > // workbench-acquired bearer authenticates the connection.
285 >
286 > /**
287 > * Canonicalizes an MCP server URL for matching a workbench-acquired token
288 > * (keyed by the OAuth `resource`) against a configured server. Mirrors the
289 > * Copilot agent's normalization: strips the fragment and any trailing slashes
290 > * from the path. Returns `undefined` for a non-URL value (e.g. a stdio server).
291 > */
292 > export function normalizeCodexMcpResourceUrl(value: string): string | undefined {
293 if (!URL.canParse(value)) {
294 return undefined;
299 return url.href;
300 }
302 > /**
303 > * Whether a codex `mcpServer/startupStatus/updated` `failed` error indicates
304 > * the server needs authentication (rather than a generic crash), so it should
305 > * surface as {@link McpServerStatus.AuthRequired} (workbench "Authenticate"
306 > * affordance) instead of a fatal error. codex has no structured auth state on
307 > * this notification, so this matches its human-readable "not logged in" /
308 > * "run `codex mcp login`" phrasing and the standard OAuth challenge vocabulary.
309 > */
310 > export function codexStartupErrorNeedsAuth(error: string | null | undefined): boolean {
311 if (!error) {
312 return false;
314 return /not logged in|mcp login|log in to|unauthori[sz]ed|requires? (?:authentication|authorization|login)|\b401\b/i.test(error);
315 }
317 > /**
318 > * Returns a copy of `servers` with `Authorization: Bearer <token>` injected
319 > * into the `http_headers` of every http server whose (normalized) URL has a
320 > * token in `tokensByNormalizedUrl`. stdio servers and servers without a token
321 > * are passed through unchanged. Any existing authorization header is removed
322 > * first -- case-insensitively, since HTTP header names are case-insensitive and
323 > * a configured lowercase `authorization` would otherwise coexist with the
324 > * injected value and leave a stale credential in the payload.
325 > */
326 > export function injectCodexMcpAuthTokens(
327 servers: Record<string, ICodexMcpServerConfigJson>,
328 tokensByNormalizedUrl: ReadonlyMap<string, string>,
341 return out;
342 }
344 > /** Drops any header whose name is `authorization` (case-insensitive). */
345 function withoutAuthorizationHeaders(headers: Record<string, string> | undefined): Record<string, string> {
346 const out: Record<string, string> = {};