Skip to content

feat(agent-core): unify the v1 MCP management plane - #2858

Open
7Sageer wants to merge 20 commits into
mainfrom
feat/v1-mcp-mgmt-unify
Open

feat(agent-core): unify the v1 MCP management plane#2858
7Sageer wants to merge 20 commits into
mainfrom
feat/v1-mcp-mgmt-unify

Conversation

@7Sageer

@7Sageer 7Sageer commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Related Issue

No GitHub issue — the requirement comes from an internal design doc ("Agent Core V1 MCP 管理面统一", Feishu doc J3jQd3BAaoQNkaxA4PScl8Frn0e). The problem is summarized below.

Problem

v1 has two MCP server sources that only merge at session start: global servers (three-layer mcp.json files + SDK caller injection) and plugin servers (manifest mcpServers + install state). The management plane only sees the user-level file, so:

  • The 11 global management RPCs (list / CRUD / connection test / OAuth) don't cover plugin servers at all — no connection test, no proactive login/reset, no runtime status view, no effective-config query.
  • Config writes only touch the file: live sessions never learn about adds/updates/removes, and a plugin disabled at runtime can still be reconnected inside an open session (reconnects reuse the boot-time snapshot).
  • OAuth sign-in / credential reset never reaches live sessions; a server stays needs-auth until a manual reconnect.
  • Tokens carry no absolute expiry, so "authorized" is a file-existence guess — a dead refresh token still reports oauth-authorized, and a mid-session 401 is reported as a generic connection failure.

What changed

Unified config registry (packages/agent-core/src/mcp/registry.ts): every server — global (layered files), plugin (manifests), caller (SDK injection) — is exposed with source / origin / mutable and its final effective config; the plugin rename / env-injection / cwd-constraint transforms stay inside PluginManager.mcpServerEntries(). All management lookups go through the registry: queries cover plugin servers, mutations on read-only entries are rejected with an actionable error, and getGlobalMcpServer exposes the effective config. Global CRUD and plugin install/enable/disable/remove/reload now push into every live session (upsert / remove / reconnect-on-change).

Interface extensions (name + full config): testGlobalMcpServer accepts an inline unsaved config; new addSessionMcpServer (name + full config + persist flag); reconnectMcpServer accepts a replacement config, and a name-only reconnect re-resolves the current config from the registry.

OAuth conduction & status semantics: one process-wide McpOAuthService shared by core and all sessions; token writes are stamped with obtained_at so expiry is computable; refresh runs ahead of expiry and single-flight per credential; credential events (saved / invalidated / refresh-failed) are pushed into affected sessions automatically. Mid-session 401s classify as needs-auth; listGlobalMcpServerAuthStatuses supports verify (online probe) and the new oauth-expired state.

SDK: the surface is exposed on both clients (getMcpServer, testMcpServerConfig, session addMcpServer, reconnectMcpServer(name, config), cwd / verify options, source-tagged list entries). v2-client limitations (no source/config tags on session entries, workspace-shared session adds) are documented in its header comment and pinned in the parity test's KNOWN_DIFFS.

Relationship to #2856: this PR's second commit absorbs that PR's v1 side — inspectAppMcpServers, the locator-addressed OAuth RPCs, and reconnectAndJoin are ported and implemented over the unified registry (its McpOAuthCoordinator and PluginManager.mcpServers() are superseded by the service's built-in credential events and mcpServerEntries(); the inspection also reports dead grants as oauth-expired rather than oauth-required). What remains unique to #2856 after this: the v2-engine credential propagation (IMcpAuthCoordinator driving workspace MCP reconnects), which is orthogonal and can land separately.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my feature works. (test/mcp/registry.test.ts, test/mcp/oauth-service.test.ts, test/rpc/mcp-rpc.test.ts, connection-manager / session-config / node-sdk / parity suites; full repo suite green: 18327 passed.)
  • Ran gen-changesets skill, or this PR needs no changeset. (.changeset/v1-mcp-management-plane.md)
  • Ran gen-docs skill, or this PR needs no doc update. (docs/{en,zh}/customization/mcp.md: plugin server changes now take effect in open sessions immediately.)

@changeset-bot

changeset-bot Bot commented Aug 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5435dce

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@moonshot-ai/kimi-code Patch
@moonshot-ai/kimi-code-sdk Minor
kimi-code Patch
@moonshot-ai/acp-adapter Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@7Hanrui

7Hanrui commented Aug 12, 2026

Copy link
Copy Markdown

@codex review

@pkg-pr-new

pkg-pr-new Bot commented Aug 12, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@5435dce
npx https://pkg.pr.new/@moonshot-ai/kimi-code@5435dce

commit: 5435dce

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d908167f47

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +891 to +892
if (entry !== undefined && entry.source !== undefined && entry.source !== 'global') return;
await session.mcp.connect(name, config, 'global');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve project-layer MCP entries during global sync

When a live session's effective server came from a project .mcp.json or .kimi-code/mcp.json, resolveSessionMcpConfig still tags it only as source: 'global', so this source-only check treats it like a mutable user-level entry and reconnects it with the user-level config on add/update. In a repo that shadows a user/global MCP name, editing the global entry will therefore replace the repo-local server in the running session even though the layered registry marks project-layer entries read-only; carry the origin/mutability into the session or re-resolve per session before pushing.

AGENTS.md reference: packages/agent-core/AGENTS.md:L9-L9

Useful? React with 👍 / 👎.

Comment on lines +361 to +362
entry.config = resolved.config;
entry.source = resolved.source;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep caller MCP overrides on name-only reconnect

When this entry is source: 'caller' but the registry also has a global or plugin server with the same name, the resolver result reaches this assignment and replaces the SDK-injected config/source. Caller MCP injection is layered on top of disk config for the session, so a name-only reconnect of a caller override silently switches the live session back to the registry entry; skip registry resolution for caller entries or make the resolver include the caller layer.

AGENTS.md reference: packages/agent-core/AGENTS.md:L9-L9

Useful? React with 👍 / 👎.

@7Hanrui

7Hanrui commented Aug 12, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 36d11d3363

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

async listGlobalMcpServers(
input?: ListGlobalMcpServersPayload,
): Promise<readonly McpManagedServerInfo[]> {
return (await this.mcpRegistry.list({ cwd: input?.cwd })).map(toManagedServerInfo);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Await plugin load before MCP registry lookups

When the MCP management surface is called immediately after KimiCore construction, this.pluginsReady may still be pending, so PluginManager is still empty and this registry snapshot omits plugin-contributed MCP servers. The same unready registry path is used by the add/update/remove guards below, so a startup-time addMcpServer can create a user-level entry that should have been rejected as a read-only plugin server; await the plugin load (and surface its load error) before any registry-backed management lookup.

AGENTS.md reference: packages/agent-core/AGENTS.md:L9-L9

Useful? React with 👍 / 👎.

@7Hanrui

7Hanrui commented Aug 12, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a9c3e6728a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +2605 to +2607
const expiresAt =
typeof tokens.obtained_at === 'number' && typeof tokens.expires_in === 'number'
? tokens.obtained_at + tokens.expires_in * 1000

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stamp v2 OAuth tokens before relying on obtained_at

When a v2 client completes the new MCP auth flow, agent-core-v2's provider saves the SDK token payload as-is, without an obtained_at field, so this expiry calculation is never available for grants created through the v2 path. A refresh-token-less token with expires_in will keep being reported as oauth-authorized offline instead of oauth-expired; stamp tokens in the v2 save path or derive expiry consistently before using this check.

Useful? React with 👍 / 👎.

Comment thread apps/vscode/src/handlers/mcp.handler.ts Outdated
Comment on lines +127 to +128
const { source: _source, origin: _origin, mutable: _mutable, plugin: _plugin, ...config } =
server;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve read-only MCP metadata for the webview

When listMcpServers() now includes plugin and project-layer entries, stripping source/origin/mutable here makes the existing MCPServersModal treat every returned item as editable and deletable (it renders Update/Delete for each MCPServerConfig). Users who open VS Code with plugin MCP servers will see mutating controls that can only fail through global CRUD by bare name; pass the metadata through and disable or hide mutating actions for mutable: false entries.

Useful? React with 👍 / 👎.

@7Hanrui

7Hanrui commented Aug 13, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 58d3b7d82f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

let source: McpServerSource = 'caller';
if (persist === true) {
await this.awaitMcpRegistryReady();
const registryEntry = await this.mcpRegistry.get(server.name).catch(() => undefined);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject persisted session adds over project MCP entries

When a session is running in a repo that defines a project-only MCP server, persist: true misses that read-only project layer because this registry lookup omits the session cwd. The call then writes a same-named user-level entry and immediately connects it into the live session, replacing the project-owned server even though management mutations are only supposed to accept mutable user-level entries; resolve with session.metadata.workDir and reject non-mutable entries before writing.

AGENTS.md reference: packages/agent-core/AGENTS.md:L9-L9

Useful? React with 👍 / 👎.

: 0,
error: entry.error,
source: entry.source,
config: entry.config,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep MCP secrets out of session status results

When any SDK client calls session.listMcpServers(), this now returns the effective config verbatim. For stdio/http entries that config can include env API keys or static headers such as Authorization, so a status-only session API now discloses credentials for project/plugin/caller servers; keep the config on an internal accessor or redact secret-bearing fields before returning McpServerInfo.

Useful? React with 👍 / 👎.

- McpServerRegistry: one config view over global (layered mcp.json),
  plugin (manifests, read-only, final effective config), and caller
  (SDK-injected) servers; name collisions keep both entries.
- Write plane: add/update/removeGlobalMcpServer mutate the user-level
  file and push into live sessions; getGlobalMcpServer returns the
  effective config; mutations of read-only entries are rejected.
- testGlobalMcpServer accepts an inline config; addSessionMcpServer
  connects a server in one live session with an optional persist flag;
  reconnect accepts a replacement config and re-resolves via the registry.
- One process-wide McpOAuthService shared with every session: obtained_at
  stamps, offline token state, single-flight and proactive refresh, and
  credential events. Sessions self-subscribe in the constructor, so even
  initializing sessions see every event; token writes serialize through
  the process-local OAuthTokenTransaction per credential identity.
- inspectAppMcpServers + locator-addressed begin/complete/cancel/reset
  cover plugin servers; inspection output redacts env/headers to sorted
  key lists; locator OAuth ops reject ambiguous shared runtime names.
- The legacy auth-status surface reads the registry (offline by default,
  verify=true probes) and never mutates credentials.
- VS Code panel receives source/origin/mutable and hides mutating
  actions on read-only entries.
- v2 client facade in node-sdk mirrors the surface over agent-core-v2
  (plugin inventory stays v1-only for now).
@7Sageer
7Sageer force-pushed the feat/v1-mcp-mgmt-unify branch from 58d3b7d to 2ab668b Compare August 13, 2026 04:34
@7Hanrui

7Hanrui commented Aug 13, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2ab668b0b3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

[...this.sessions.values()].map(async (session) => {
for (const entry of session.mcp.list()) {
if (entry.source !== 'plugin' || target.has(entry.name)) continue;
await session.mcp.remove(entry.name);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reconnect the fallback MCP server after plugin disable

When a plugin MCP server shadows a user/global server in a live session, disabling or removing that plugin makes the effective registry entry fall back to the global config. This loop only removes the plugin-sourced connection and never re-resolves the name, so after setPluginEnabled or setPluginMcpServerEnabled the global server remains disconnected until restart or a manual reconnect even though it is still configured.

AGENTS.md reference: packages/agent-core/AGENTS.md:L9-L9

Useful? React with 👍 / 👎.

// A project-layer shadow still defines the server for this session;
// only the user-level definition went away.
const resolved = await this.resolveMcpRegistryEntry(name, session.metadata.workDir);
if (resolved !== undefined && !resolved.mutable) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove stale global MCP entries when plugins are disabled

When a live user/global MCP entry shares its runtime name with a disabled plugin MCP server, removing the user-level entry leaves only the plugin's read-only enabled: false descriptor in the registry. This guard treats any non-mutable result as a project-layer shadow and returns, so the old user connection keeps running in live sessions even though the mutable config was deleted; restrict the skip to project/global shadows, and remove or switch entries for plugin results.

AGENTS.md reference: packages/agent-core/AGENTS.md:L9-L9

Useful? React with 👍 / 👎.

Recompute each live session's MCP target from the registry's runtime
resolution (enabled plugin > project layer > user file; caller injection
shadows everything) behind every config mutation, instead of per-path
patching: shadowed file layers recover when a plugin winner is disabled or
removed, removing a user-level entry resurrects its project-layer shadow,
disabled plugin descriptors no longer block removals, persisted session
adds validate against the session's project layer and broadcast to other
live sessions, and per-session sync failures are logged with context.

Session status entries and read-only management entries now report
redacted config views (envKeys/headerKeys instead of literal env/headers
values); core-internal reconciliation compares full configs via the
connection manager's raw-entry accessor.

OAuth: interactive flows are serialized per credential (concurrent
begins join the in-flight flow instead of clobbering its PKCE/state), a
malformed credential meta sidecar no longer aborts core start, grants
inside the refresh-ahead window refresh immediately while far-future
grants re-arm through a max-length timer, and the service shuts its
timers and flows down with KimiCore/SDKRpcClient close.
@7Hanrui

7Hanrui commented Aug 14, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1801605861

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// rejected refresh token falls through to the interactive branch and
// comes back as REDIRECT, which this non-interactive path treats as
// failure.
const result = await auth(provider as OAuthClientProvider, { serverUrl });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Run proactive refresh through token transaction

When a proactive timer fires while an MCP transport is also doing a 401-driven refresh for the same credential, this auth() call uses the SDK default fetch instead of provider.createOAuthFetch(). That leaves the token-endpoint request outside OAuthTokenTransaction, so a slower response using an old rotating refresh token can later save over the newer durable grant and leave sessions with invalid MCP OAuth credentials; pass the wrapped fetch here like the interactive auth paths do.

AGENTS.md reference: packages/agent-core/AGENTS.md:L12-L12

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复(888c489d):refreshNow 的 /token 请求改走 provider.createOAuthFetch(),与传输侧 401 刷新共用同一条按 credential 串行化的事务通道。并补了契约测试防止回退。

return this.globalMcpConfig.add(server);
): Promise<readonly McpManagedServerInfo[]> {
await this.awaitMcpRegistryReady();
const existing = await this.mcpRegistry.get(server.name).catch(() => undefined);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve adds against the workspace MCP layer

When a client is operating from a workspace that has a project .mcp.json/.kimi-code/mcp.json server with this name, this guard performs the registry lookup without the caller cwd, even though list/get can include those project layers. It therefore misses the read-only project entry and writes a user-level shadow into mcp.json, contradicting the non-mutable project entry the same management view exposes; carry cwd through mutation payloads or resolve against the cwd-scoped registry before writing.

AGENTS.md reference: packages/agent-core/AGENTS.md:L9-L9

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

确认属实,但这条不在本次修复范围:根因是管理面 add 缺 workspace 上下文(cwd 语义本身是契约决策),与待办的「locator/serverId 贯通到 VS Code UI」是同一根线,计划随那一项统一处理——届时 add/update/remove 的守卫都按调用方工作区解析项目层,而不是现在补一个孤立的 cwd 参数。当前行为兜底:项目层优先级高于用户层,误写入的 user-level 条目在该工作区不会改变生效配置。

…ken transaction

 refreshNow ran its /token request with the SDK default fetch, outside the
 credential-serializing OAuthTokenTransaction that every other token write
 uses; a slower response carrying an older rotating refresh token could
 overwrite a newer grant written by a concurrent transport-side refresh.
@7Hanrui

7Hanrui commented Aug 14, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 888c489d01

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

verify: boolean,
): Promise<GlobalMcpServerAuthState> {
const server = entry.config;
if (server.transport === 'stdio') return 'not-applicable';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat disabled MCP entries as not applicable

When a saved server has enabled: false, this auth-state path no longer short-circuits before the OAuth checks. For a disabled remote server marked auth: 'oauth', listGlobalMcpServerAuthStatuses() now reports oauth-required; for an unmarked disabled remote server it can even run the probe below, despite the server being intentionally disabled. The previous helper and the inspection path classify disabled entries as not-applicable, so add the same server.enabled === false guard here before transport/OAuth handling.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复(7aadb042):mcpServerAuthState 开头补回 enabled === false → not-applicable 的短路(与 main 的旧分类器对齐),verify 在线验证路径同样不再探测 disabled 条目。附回归测试。

Comment thread apps/vscode/src/handlers/mcp.handler.ts Outdated
[Methods.GetMCPServers]: async (_, ctx): Promise<MCPServerConfig[]> => {
return toWebviewServers(await ctx.harness.listMcpServers());
return toWebviewServers(
await ctx.harness.listMcpServers({ cwd: ctx.workDir ?? undefined }),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Carry MCP cwd or locator into VS Code auth actions

Including cwd here makes project-layer .mcp.json / .kimi-code/mcp.json servers appear in the VS Code modal, but the Auth and Reset handlers below still call the name-only authenticateMcpServer / resetMcpServerAuth APIs with no cwd or locator. For a listed project-layer OAuth server, clicking the key/reset button either fails as not found or targets a same-named user/global entry instead; keep the listed entry's workspace identity through those actions rather than authenticating by bare name.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

确认属实。项目层条目从这一轮开始随 cwd 传入变为可见(list 侧修复),但 Auth/Reset 依赖的 workspace/locator 上下文契约还不存在——这与已标注的 F11(locator/serverId 贯通到 VS Code 管理 UI)是同一块工作,届时 auth/reset 会走 locator 地址化 API 一并解决。如果需要临时止血,可以在 modal 里对 source: 'global' 且 mutable: false 的条目禁用 Auth/Reset 按钮,但默认留给 F11 统一交付。

…cation

The unified mcpServerAuthState dropped the previous enabled short-circuit,
so a disabled oauth-flagged server reported oauth-required — or was even
probed over the network — instead of not-applicable.
@7Hanrui

7Hanrui commented Aug 14, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7aadb04231

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

cwd: string | undefined,
verify: boolean,
): Promise<GlobalMcpServerAuthState> {
if (server.transport === 'stdio') return 'not-applicable';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Short-circuit disabled v2 MCP auth statuses

When a disabled remote MCP server is listed through the v2 harness, this classifier still falls into the OAuth/probe path; the v1 classifier now has the enabled === false guard, but this v2 parity copy does not. A disabled { transport: 'http', auth: 'oauth', enabled: false } entry will therefore report oauth-required (or perform an online probe with verify) even though disabled servers should be not-applicable, causing the v2 management UI/status surface to prompt for auth on servers the user intentionally turned off.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复(3af3bae56):v2 分类器补齐同款 enabled === false 短路,并在 v1↔v2 parity 套件里新增 disabled-oauth 用例,双引擎断言一致(not-applicable)。

…-status classifier

The v2 parity copy of v1's mcpServerAuthState missed the same enabled
guard v1 just regained; a disabled oauth-flagged entry would report
oauth-required (or be probed). The parity suite now pins the disabled
case on both engines.
@7Hanrui

7Hanrui commented Aug 14, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3af3bae56f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/vscode/src/handlers/mcp.handler.ts Outdated
[Methods.GetMCPServers]: async (_, ctx): Promise<MCPServerConfig[]> => {
return toWebviewServers(await ctx.harness.listMcpServers());
return toWebviewServers(
await ctx.harness.listMcpServers({ cwd: ctx.workDir ?? undefined }),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh MCP lists with the workspace cwd after mutations

When VS Code is opened in a repo with project-layer .mcp.json entries, this initial list includes those cwd-scoped servers, but Add/Update/Remove still broadcast the mutation RPC's returned list, which is produced without the workspace cwd. After any global MCP mutation, the modal state drops all project-layer entries until it is reopened or explicitly reloaded; re-fetch with the same ctx.workDir after the mutation before broadcasting.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复(25e2bbb3):add/update/remove 后不再用 mutation RPC 返回的无 cwd 列表广播,改为按工作区 cwd 重新拉取——项目层条目不再闪没。集成测试补了项目层条目贯穿 add/remove 刷新后仍可见的用例。

…fter mutations

The add/update/remove RPCs return a cwd-less management list, so the
webview broadcast dropped project-layer entries until the next full load;
re-list with the workspace cwd after every mutation instead.
@7Hanrui

7Hanrui commented Aug 14, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 25e2bbb301

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +176 to +177
const stored: StoredMcpOAuthTokens = { ...tokens, obtained_at: Date.now() };
await this.tokenTransaction.save(stored);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep OAuth saves matched to token transactions

When the MCP SDK uses provider.createOAuthFetch() for an authorization-code or refresh-token grant, OAuthTokenTransaction records the exact token object returned from the token endpoint so the following SDK saveTokens() can be consumed instead of re-writing after intervening clears or newer winners. Adding obtained_at before calling tokenTransaction.save() changes that object, so consumeSave() misses; if a user resets auth, or another refresh wins, after the /token response but before the SDK invokes saveTokens, this late callback can persist the stale grant again and defeat the per-credential serialization. Stamp inside the transaction write path or otherwise keep the object passed to save() matchable.

AGENTS.md reference: packages/agent-core/AGENTS.md:L12-L12

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复(687f25d4):saveTokens 不再先克隆盖戳再进 transaction——原始对象直接交给 OAuthTokenTransaction.save,让 fetch 时记录的 effect 能被 consume 命中(匹配即跳过重写),durable 的 obtained_at 改由 write 回调统一落。补了回归测试:grant fetch 与 SDK saveTokens 之间发生 reset 时,不会再把已清除的凭证写回。另确认了 v2 provider 不走 OAuthTokenTransaction,无同款问题。

…saction

saveTokens stamped obtained_at onto a fresh object before calling
tokenTransaction.save, so it never matched the exact payload the
transaction recorded for a grant fetch; the consume path was dead and
every save re-wrote. Between the fetch and the SDK callback an intervening
clear could then be overwritten — the resurrected grant came back after a
reset. The write callback stamps the durable record instead.
@7Hanrui

7Hanrui commented Aug 14, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fbc7b13b7e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

}: SessionScopedPayload<AddSessionMcpServerPayload>): Promise<McpServerInfo> {
const session = this.requireSession(sessionId);
const existing = session.mcp.get(server.name);
if (existing?.source === 'plugin') {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow session-local MCP overrides over plugins

When addSessionMcpServer is called without persist for a name that is currently supplied by a plugin, this guard rejects before the method can connect the new entry as source: 'caller'. Startup-time caller MCP injection (CreateSessionOptions.mcpServers) can shadow plugin/global/project config for that session, so the live add API now behaves inconsistently and prevents SDK clients from applying the same session-local override unless they restart the session; only the persisted path should reject read-only plugin/project owners.

AGENTS.md reference: packages/agent-core/AGENTS.md:L10-L10

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

确认过代码后认为这条的前提不成立,不做改动:启动时 caller 注入并不能 shadow plugin——mergePluginMcpConfig 的合并顺序是把 plugin 条目最后 spread,撞名时是 plugin 覆盖 caller;McpServerRegistry.resolveRuntimeTarget 的语义同样写明 enabled plugin 胜文件层。caller 注入只能 shadow user/project 文件层,任何入口都抢不过 plugin。因此 live addSessionMcpServer 拒绝 plugin 同名条目与启动行为是一致的:plugin 拥有的运行时名字在所有入口都不可被覆盖。

`MCP runtime name "${name}" is shared by multiple enabled servers`,
);
}
const entry = matches[0]!;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Probe the enabled MCP target after disabled collisions

When a workspace has both a disabled global/project entry and an enabled plugin entry with the same runtime name, the ambiguity check sees only one enabled match but this line still returns matches[0], and registry order puts global entries before plugins. In the VS Code Test path (bridge.testMCP(server.name) with cwd), clicking Test on the plugin row therefore probes the disabled file entry and reports disabled instead of the live plugin server; pick the sole enabled match or use the runtime resolver after the count check. Fresh evidence after the previous ambiguity fix is that the code now counts enabled matches but still selects the unfiltered first match.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复(227e9c7c):碰撞校验保留按 enabled 计数,选取从 matches[0] 改为取唯一 enabled 匹配——registry 顺序文件层在前、plugin 在后,而运行时语义是 enabled plugin 胜文件层,现在探测目标与 live session 实际运行的一致;全部 disabled 时回落到首个条目,按 disabled 报告。附回归测试(disabled 文件层 + enabled plugin 同名时探测 plugin 条目)。

…llision

The name-only connection test counted enabled matches for its ambiguity
guard but still probed the first registry match, and the file layers list
before plugins. With a disabled file entry shadowing an enabled plugin of
the same runtime name, Test probed the disabled entry instead of the one a
live session would run. Select the sole enabled match, falling back to the
first entry only when every match is disabled so it reports as disabled.
@7Hanrui

7Hanrui commented Aug 14, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 227e9c7cdf

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

}: SessionScopedPayload<AddSessionMcpServerPayload>): Promise<McpServerInfo> {
const session = this.requireSession(sessionId);
const existing = session.mcp.get(server.name);
if (existing?.source === 'plugin') {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow live caller MCP overrides over plugins

When an SDK client adds a session-local MCP server (persist omitted/false) with the same runtime name as an already-connected plugin server, this guard rejects before the entry can be connected as source: 'caller'. Fresh evidence since the earlier thread is that mergePluginMcpConfig now skips plugin entries when sources[entry.name] === 'caller', so startup caller injection can shadow plugins; the live add path should match that behavior and only reject plugin-owned names for persisted/user-level writes.

AGENTS.md reference: packages/agent-core/AGENTS.md:L10-L10

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

你说得对,之前的 dismiss 基于合入 main 前的旧代码:main 重构 mergePluginMcpConfig 后 caller 注入会跳过同名 plugin 条目(sources[name] === "caller" → continue),启动语义已经是 caller 胜 plugin,live add 的拒绝确实不一致。已修复(9d436a11f):plugin-source 拒绝收窄到 persist === true——非 persist 的 session-local add 按 caller 连接,与启动注入及 reconcile 跳过 caller 的语义一致;persist 仍经由 registry 守卫拒绝。附测试:plugin 条目在场时 non-persist add 变为 caller、persist add 依旧拒绝且不落盘。

…w-followups

# Conflicts:
#	packages/node-sdk/src/sdk-rpc-client-v2.ts
Caller injection shadows every registry source at session start, plugins
included, and reconciliation leaves caller entries untouched; the live
non-persist add path rejected plugin-owned names anyway, so SDK clients
could not apply the same per-session override without a restart. Gate the
plugin-source rejection on persist: session-local adds connect as caller,
while persisted adds stay rejected as user-level writes behind a read-only
owner.
@7Hanrui

7Hanrui commented Aug 14, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9d436a11f2

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

await this.globalMcpConfig.add(server);
source = 'global';
}
await session.mcp.connect(server.name, parsed.data, source);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize session MCP names before connecting

When an SDK caller passes a session MCP name with leading/trailing whitespace (or a blank name when persist is false), this uses the raw server.name even though the persisted store normalizes names with trim(). With persist: true, the file entry is written as the trimmed key (for example foo) but the requesting session connects and the cross-session reconciliation is driven under the raw name (" foo "), so other live sessions do not pick up the persisted server and later reconnect/list operations disagree about the server identity; normalize/reject the name once before both the store write and the live-session connect/reconcile.

AGENTS.md reference: packages/agent-core/AGENTS.md:L9-L9

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复(6c399855):addSessionMcpServer 入口统一走 store 同款的 normalizeServerName(trim + 空名拒绝),store 写入、live connect、跨 session reconcile 都用归一后的名字,身份不再分裂;空白名在两条路径上都会在连接前被拒。附回归测试。

The persisted store trims server names, but addSessionMcpServer used the
raw name for the live connect and cross-session reconciliation: a padded
name persisted under the trimmed key while the requesting session ran and
reconciled the raw one, and a blank name connected with no identity at
all. Normalize once up front (rejecting blank) so the store write, the
session entry, and reconciliation agree on the same server.
@7Hanrui

7Hanrui commented Aug 14, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6c399855fd

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +1151 to +1154
const entry = await this.mcpRegistry.get(name);
const descriptor = this.appMcpServerDescriptor(entry);
const catalog = await this.appMcpServerDescriptors();
this.requireUnambiguousRuntimeName(catalog, descriptor);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Select the enabled MCP target for legacy auth

When a disabled user/project entry named plugin-demo:api coexists with the enabled plugin server of the same runtime name, this legacy resolver still starts from mcpRegistry.get, which returns the disabled file-layer entry because registry order puts globals first. requireUnambiguousRuntimeName then sees the enabled plugin as a conflict and rejects beginGlobalMcpServerAuth/resetGlobalMcpServerAuth; VS Code still calls these name-only actions for the plugin row, so OAuth controls fail even though there is only one enabled runtime target. Select the sole enabled match before applying the ambiguity guard, as the test path now does.

AGENTS.md reference: packages/agent-core/AGENTS.md:L9-L10

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复(937c2920):legacy resolver 先取唯一 enabled 匹配再交由歧义守卫(与 test 路径同款逻辑)——disabled 文件层 + enabled plugin 同名时,auth/reset 作用于 enabled 的 plugin 条目,不再误报冲突;双 enabled 碰撞依旧拒绝。附回归测试。

Comment on lines +2738 to +2742
const parsed = parseInlineMcpServer(input.server);
if (input.persist === true) {
await this.globalMcpConfig.add(input.server);
}
await manager.connect(parsed.name, mcpConfigWithoutName(parsed));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize v2 session MCP names before connecting

When session.addMcpServer({ name: ' foo ', ... }, { persist: true }) runs through the v2 client, globalMcpConfig.add(input.server) writes the trimmed foo key, but this path then connects and returns the raw parsed.name (' foo '). That splits the live workspace manager from the persisted config, so later reconnectMcpServer('foo') cannot find the live entry and v1/v2 parity disagrees; normalize once and use that name for both the store write and manager connect. Fresh evidence is that the new v2 addSessionMcpServer path still does not use the store-normalized name.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复(937c2920):v2 addSessionMcpServer 入口统一归一化名字(复用 v2/global-mcp 的 normalizeServerName),store 写入与 manager connect 用同一身份,与 v1 对齐;空白名两条路径都会在连接前拒绝。parity 套件补了 padded/blank 用例,并验证过回退旧实现会失败。

return server.auth === 'oauth' ? 'oauth-required' : 'not-applicable';
};
const probe = (): Promise<GlobalMcpServerAuthState> =>
this.withGlobalMcpServerProbe(server, cwd, (manager) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Use the fresh v2 OAuth service for verified probes

When v2 listMcpServerAuthStatuses({ verify: true }) runs after the cached globalMcpOAuth provider was constructed before a token file was added or updated by another process, this classifier reads a fresh token snapshot into tokens but the probe goes through withGlobalMcpServerProbe, which builds the manager with the cached service. The manager can therefore probe without the new token and report oauth-expired/oauth-required for a valid grant; pass the same fresh oauth into the probe path, like inspectAppMcpServers already does.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复(937c2920):withGlobalMcpServerProbe 增加可选 oauth 参数,listGlobalMcpServerAuthStatuses 的两个探针调用点把本次调用新建的 fresh service 传进去——首次探测后落盘的 grant 在 verify:true 下能正确判为 oauth-authorized。parity 回归覆盖,且验证过旧实现下该用例在 v2 报 oauth-expired。

…shness gaps

The legacy name-only auth resolver started from the first registry match,
so a disabled file-layer shadow plus an enabled plugin of the same runtime
name was misread as an ambiguity conflict; select the sole enabled match
before judging ambiguity, exactly like the test probe path. On the v2
client, addSessionMcpServer connected the raw name while the store wrote
the trimmed key — normalize once for both, and route the verify-triggered
auth probes through the per-call OAuth service instead of the cached one
whose providers snapshot tokens at construction, so a grant saved after
the first probe is honored.
@7Hanrui

7Hanrui commented Aug 14, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 937c292070

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

this.throwReadOnlyMcpServer(existing);
}
await this.globalMcpConfig.add(server);
await this.reconcileMcpServerInSessions([server.name], 'global-add');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize global MCP names before syncing live sessions

When an SDK caller passes a padded name to a global MCP mutation, the store normalizes it but this path still does the registry guard and live-session reconcile with the raw server.name; for example adding " foo " persists foo in mcp.json but reconciles " foo ", so open sessions never connect the new foo entry, and padded plugin runtime names can miss the read-only guard before the normalized write. Normalize once before lookup, persistence, and reconciliation for add/update/remove.

AGENTS.md reference: packages/agent-core/AGENTS.md:L9-L9

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复(9bee6519):add/update/remove 三个全局变更入口统一先 normalizeServerName——只读守卫、落盘 key、跨 session reconcile 同一身份,padded 名字下 live session 能真正连上/断开,padded plugin 名也挡得住。附回归测试(验证过回退旧实现即失败)。

`MCP server "${name}" is contributed by a plugin; update the plugin manifest instead`,
);
}
entry.config = config;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not mutate reconnect entries before disabled checks

When reconnectMcpServer(name, config) is called for an already connected server with a replacement config containing enabled: false, this assignment updates the entry's config, then the disabled check throws before closeClient() runs. The old client and tools remain live while listMcpServers() now reports the disabled replacement config, so either reject before mutating or drive the entry through the normal disabled connect/remove path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复(9bee6519):disabled 替换配置在赋值前就拒绝(同样的 mcp.server_disabled 报错),entry 保持原 config 与 live client,不再出现「报 disabled 但还在跑」的半切换状态。回归测试用 getRawEntry 深比较确认无泄漏(已验证 toMatchObject 子集断言抓不住这类残留,特意换成全等 + 回退验证)。

…ed reconnect swaps

The global add/update/remove mutations guarded and reconciled with the raw
server name while the store persisted the trimmed key, so a padded name
left live sessions unreconciled and could slip past the plugin read-only
guard; normalize once before lookup, persistence, and reconciliation. And
a config-carrying reconnect assigned the replacement before the disabled
check fired, leaving a connected entry that reported the disabled config;
reject disabled replacements before mutating, keeping the same error.
@7Hanrui

7Hanrui commented Aug 14, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9bee65197d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

throw new Error(`MCP server "${serverName}" has no refreshable OAuth grant`);
}
const provider = this.getProvider(serverName, serverUrl);
provider.resetFlow();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not reset active OAuth flows during refresh

When a proactive refresh timer fires (or another caller invokes refresh()) while beginAuthorization() is already waiting for the browser callback for the same credential, this uses the same cached provider and immediately clears its redirect URL, PKCE verifier, and state. The active flow is only serialized against other beginAuthorization() calls, so the later complete() can skip/mismatch state and then fail the code exchange with a missing verifier even though the user authorized successfully; make refresh wait for/skip the active authorization for that store key instead of resetting the shared provider.

AGENTS.md reference: packages/agent-core/AGENTS.md:L12-L12

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复(95474949):refresh 在同 store key 存在活跃交互流程时直接跳过——不再 resetFlow 清掉共享 provider 的 redirect URL / PKCE / state;流程完成本身会产生新 grant,失败的兜底仍是 401 传输路径。回归测试用「拒绝 refresh 的假授权服务器 + 进行中流程」复现竞态:修复前 refresh 会拒绝并毁掉流程,修复后跳过且 complete 成功。

…s the credential

refreshNow reset the shared provider's flow state before and after the
token request; when a proactive timer (or a manual refresh) fired while
beginAuthorization was waiting on the browser callback for the same store
key, that wiped the redirect URL, PKCE verifier, and state the in-flight
flow needed — complete() then failed the exchange even though the user
authorized. Refresh now skips when an interactive flow is active for the
credential: the flow delivers fresh tokens on completion, and the 401
transport path is the backstop if it fails.
@7Hanrui

7Hanrui commented Aug 14, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 95474949b9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +875 to +876
const existing = await this.mcpRegistry.get(name).catch(() => undefined);
if (existing !== undefined && !(existing.source === 'global' && existing.mutable)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Allow adds over disabled plugin MCP descriptors

When the only registry match is a plugin MCP entry with enabled: false (for example, after a user disables demo/api), this guard still treats that disabled descriptor as the read-only owner and rejects adding a user-level plugin-demo:api. Disabled plugin entries are absent from the runtime target, so users cannot install a global fallback unless it happened to exist before the plugin was disabled; reject only enabled read-only owners, or resolve the runtime target before blocking the add.

AGENTS.md reference: packages/agent-core/AGENTS.md:L10-L10

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复(ca6d0b1b):守卫放宽到「disabled plugin 描述符不挡变更」——与运行时语义一致(resolveRuntimeTarget 视 disabled plugin 为缺席),禁用插件后即可安装 user-level 兜底并即时生效;disabled 的项目层条目仍会遮蔽 user 文件、保持拒绝。四个调用点(add/update/remove/persist-add)共享同一 helper,行为一致。附回归测试(回退旧实现即失败)。

A disabled plugin entry is absent from the runtime target, but the
read-only guard still treated it as the owner, so a user-level fallback
could only exist if it predated the plugin disable. Relax the shared
guard: disabled plugin descriptors never block mutations (disabled
project entries still shadow the user file and keep their rejection).
@7Hanrui

7Hanrui commented Aug 14, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ca6d0b1bc8

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

'reconnectMcpServer with an explicit config is not supported for v2 sessions with ephemeral MCP servers',
);
}
await manager.connect(input.name, parseReconnectMcpServerConfig(input.name, input.config));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject disabled v2 reconnect configs

When a v2 SDK caller supplies an explicit replacement config with enabled: false, this path uses connect(), whose upsert semantics close the current client and leave a disabled entry instead of rejecting. The v1 reconnect(name, config) path now throws before mutating in this scenario, so v2 callers can silently lose a live MCP connection and get parity-breaking success; validate the replacement and reject disabled configs before calling connect().

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复(3c61691d):v2 reconnect 在 connect 之前拒绝 enabled:false 的替换配置,报错与 v1 完全一致(mcp.server_disabled);parity 套件新增断言双引擎同错且 live 连接保持 toolCount 不变(已回退验证旧实现失败)。

Comment on lines +2755 to +2758
if (input.persist === true) {
await this.globalMcpConfig.add(target);
}
await manager.connect(target.name, mcpConfigWithoutName(target));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve persisted v2 adds against workspace MCP config

When a v2 session is in a workspace that already defines this MCP name in a project .mcp.json/.kimi-code/mcp.json, persist: true writes a user-level entry and then directly upserts the workspace-shared manager with that user config, bypassing the effective workspace config where the project layer should win. Fresh evidence after the v1 project-shadow finding is that this new v2 implementation has the session context but no project-layer guard, so all live sessions in that workspace can be switched to the wrong server until a reload/reconcile; resolve against the workspace MCP config and reject or reconcile rather than directly connecting the persisted fallback.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复(3c61691d):persist 分支新增项目层守卫——按会话 workspace 双层加载(includeProject 有无对比),项目层拥有同名条目时拒绝,不落盘也不 upsert 工作区共享 manager,与 v1 的只读规则对齐。parity 回归用例覆盖(回退旧实现即失败)。

A v2 reconnect with an explicit enabled:false replacement config used
connect()'s upsert semantics — closing the live client and reporting
success where v1's manager reconnect rejects before applying anything;
reject disabled replacements up front with the same error. And a persisted
v2 session add never consulted the workspace config, so a same-named
project-layer entry was silently shadowed: the user-level write never
takes effect while the direct workspace-manager upsert displaces the
project config for every live session. Resolve the workspace layers and
reject like v1's read-only rule.
@7Hanrui

7Hanrui commented Aug 14, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3c61691d63

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +86 to +87
servers[name] = config;
origins[name] = path;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use null-prototype maps for MCP layers

When an mcp.json contains a server literally named __proto__, assigning into these normal {} accumulators invokes the special prototype setter instead of creating an enumerable own key. That drops the server (and its origin) from loadMcpServersDetailed(), so session startup and the unified registry no longer see a valid configured MCP server; build these maps with Object.create(null) or define entries via Object.fromEntries/Object.defineProperty so special names remain own properties.

AGENTS.md reference: packages/agent-core/AGENTS.md:L9-L9

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复(5435dce5)。实际丢 key 的环节比累加器更早:z.record() 的 parse 输出就是靠属性赋值重建的,__proto__ 在 schema 校验阶段就没了——所以改成逐 entry 校验(JSON own-keys + 每条单独过 McpServerConfigSchema),合并累加器也换成 null-prototype。v2 加载器查过:{...user, ...project} spread 语义安全,无需改动。回归测试用原始 JSON 字符串写文件(对象字面量会自吞 proto),回退验证通过。

A z.record() parse rebuilds its output via property assignment, so a
server literally named __proto__ hit the prototype setter and vanished
before validation; the layer merge then repeated the same trap with plain
object accumulators. Parse the server map entry-by-entry over the JSON own
keys and accumulate into null-prototype maps, so session startup and the
unified registry keep the declared server and its origin.
@7Hanrui

7Hanrui commented Aug 14, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5435dce556

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines 2367 to +2368
const oauth = await this.globalMcpOAuthService();
const flow = await oauth.beginAuthorization(server.name, config.url);
const flow = await oauth.beginAuthorization(server.runtimeName, config.url);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Use a fresh OAuth provider when beginning v2 MCP auth

When another Kimi process saves or resets this MCP credential after this v2 client has already materialized globalMcpOAuth, starting auth here reuses the cached service and the provider's in-memory token snapshot. In that case auth() can return already-authorized from tokens that were removed on disk, or start a browser flow even though a fresh grant was just saved, while the status/probe paths below already build a fresh OAuth service to avoid this cache. Reload or recreate the provider for the begin-auth credential read before calling beginAuthorization.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants