feat(agent-core): unify the v1 MCP management plane - #2858
Conversation
🦋 Changeset detectedLatest commit: 5435dce The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
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 |
|
@codex review |
commit: |
There was a problem hiding this comment.
💡 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".
| if (entry !== undefined && entry.source !== undefined && entry.source !== 'global') return; | ||
| await session.mcp.connect(name, config, 'global'); |
There was a problem hiding this comment.
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 👍 / 👎.
| entry.config = resolved.config; | ||
| entry.source = resolved.source; |
There was a problem hiding this comment.
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 👍 / 👎.
|
@codex review |
There was a problem hiding this comment.
💡 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); |
There was a problem hiding this comment.
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 👍 / 👎.
|
@codex review |
There was a problem hiding this comment.
💡 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".
| const expiresAt = | ||
| typeof tokens.obtained_at === 'number' && typeof tokens.expires_in === 'number' | ||
| ? tokens.obtained_at + tokens.expires_in * 1000 |
There was a problem hiding this comment.
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 👍 / 👎.
| const { source: _source, origin: _origin, mutable: _mutable, plugin: _plugin, ...config } = | ||
| server; |
There was a problem hiding this comment.
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 👍 / 👎.
|
@codex review |
There was a problem hiding this comment.
💡 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); |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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).
58d3b7d to
2ab668b
Compare
|
@codex review |
There was a problem hiding this comment.
💡 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); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
|
@codex review |
There was a problem hiding this comment.
💡 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 }); |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
已修复(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); |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
确认属实,但这条不在本次修复范围:根因是管理面 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.
|
@codex review |
There was a problem hiding this comment.
💡 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'; |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
已修复(7aadb042):mcpServerAuthState 开头补回 enabled === false → not-applicable 的短路(与 main 的旧分类器对齐),verify 在线验证路径同样不再探测 disabled 条目。附回归测试。
| [Methods.GetMCPServers]: async (_, ctx): Promise<MCPServerConfig[]> => { | ||
| return toWebviewServers(await ctx.harness.listMcpServers()); | ||
| return toWebviewServers( | ||
| await ctx.harness.listMcpServers({ cwd: ctx.workDir ?? undefined }), |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
确认属实。项目层条目从这一轮开始随 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.
|
@codex review |
There was a problem hiding this comment.
💡 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'; |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
已修复(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.
|
@codex review |
There was a problem hiding this comment.
💡 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".
| [Methods.GetMCPServers]: async (_, ctx): Promise<MCPServerConfig[]> => { | ||
| return toWebviewServers(await ctx.harness.listMcpServers()); | ||
| return toWebviewServers( | ||
| await ctx.harness.listMcpServers({ cwd: ctx.workDir ?? undefined }), |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
已修复(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.
|
@codex review |
There was a problem hiding this comment.
💡 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".
| const stored: StoredMcpOAuthTokens = { ...tokens, obtained_at: Date.now() }; | ||
| await this.tokenTransaction.save(stored); |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
已修复(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.
|
@codex review |
There was a problem hiding this comment.
💡 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') { |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
确认过代码后认为这条的前提不成立,不做改动:启动时 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]!; |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
已修复(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.
|
@codex review |
There was a problem hiding this comment.
💡 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') { |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
你说得对,之前的 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.
|
@codex review |
There was a problem hiding this comment.
💡 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); |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
已修复(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.
|
@codex review |
There was a problem hiding this comment.
💡 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".
| const entry = await this.mcpRegistry.get(name); | ||
| const descriptor = this.appMcpServerDescriptor(entry); | ||
| const catalog = await this.appMcpServerDescriptors(); | ||
| this.requireUnambiguousRuntimeName(catalog, descriptor); |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
已修复(937c2920):legacy resolver 先取唯一 enabled 匹配再交由歧义守卫(与 test 路径同款逻辑)——disabled 文件层 + enabled plugin 同名时,auth/reset 作用于 enabled 的 plugin 条目,不再误报冲突;双 enabled 碰撞依旧拒绝。附回归测试。
| const parsed = parseInlineMcpServer(input.server); | ||
| if (input.persist === true) { | ||
| await this.globalMcpConfig.add(input.server); | ||
| } | ||
| await manager.connect(parsed.name, mcpConfigWithoutName(parsed)); |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
已修复(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) => { |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
已修复(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.
|
@codex review |
There was a problem hiding this comment.
💡 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'); |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
已修复(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; |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
已修复(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.
|
@codex review |
There was a problem hiding this comment.
💡 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(); |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
已修复(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.
|
@codex review |
There was a problem hiding this comment.
💡 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".
| const existing = await this.mcpRegistry.get(name).catch(() => undefined); | ||
| if (existing !== undefined && !(existing.source === 'global' && existing.mutable)) { |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
已修复(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).
|
@codex review |
There was a problem hiding this comment.
💡 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)); |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
已修复(3c61691d):v2 reconnect 在 connect 之前拒绝 enabled:false 的替换配置,报错与 v1 完全一致(mcp.server_disabled);parity 套件新增断言双引擎同错且 live 连接保持 toolCount 不变(已回退验证旧实现失败)。
| if (input.persist === true) { | ||
| await this.globalMcpConfig.add(target); | ||
| } | ||
| await manager.connect(target.name, mcpConfigWithoutName(target)); |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
已修复(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.
|
@codex review |
There was a problem hiding this comment.
💡 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".
| servers[name] = config; | ||
| origins[name] = path; |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
已修复(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.
|
@codex review |
There was a problem hiding this comment.
💡 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".
| const oauth = await this.globalMcpOAuthService(); | ||
| const flow = await oauth.beginAuthorization(server.name, config.url); | ||
| const flow = await oauth.beginAuthorization(server.runtimeName, config.url); |
There was a problem hiding this comment.
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 👍 / 👎.
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.jsonfiles + SDK caller injection) and plugin servers (manifestmcpServers+ install state). The management plane only sees the user-level file, so:needs-authuntil a manual reconnect.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 withsource/origin/mutableand its final effective config; the plugin rename / env-injection / cwd-constraint transforms stay insidePluginManager.mcpServerEntries(). All management lookups go through the registry: queries cover plugin servers, mutations on read-only entries are rejected with an actionable error, andgetGlobalMcpServerexposes 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):
testGlobalMcpServeraccepts an inline unsaved config; newaddSessionMcpServer(name + full config +persistflag);reconnectMcpServeraccepts a replacement config, and a name-only reconnect re-resolves the current config from the registry.OAuth conduction & status semantics: one process-wide
McpOAuthServiceshared by core and all sessions; token writes are stamped withobtained_atso 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 asneeds-auth;listGlobalMcpServerAuthStatusessupportsverify(online probe) and the newoauth-expiredstate.SDK: the surface is exposed on both clients (
getMcpServer,testMcpServerConfig, sessionaddMcpServer,reconnectMcpServer(name, config),cwd/verifyoptions, source-tagged list entries). v2-client limitations (nosource/configtags on session entries, workspace-shared session adds) are documented in its header comment and pinned in the parity test'sKNOWN_DIFFS.Relationship to #2856: this PR's second commit absorbs that PR's v1 side —
inspectAppMcpServers, the locator-addressed OAuth RPCs, andreconnectAndJoinare ported and implemented over the unified registry (itsMcpOAuthCoordinatorandPluginManager.mcpServers()are superseded by the service's built-in credential events andmcpServerEntries(); the inspection also reports dead grants asoauth-expiredrather thanoauth-required). What remains unique to #2856 after this: the v2-engine credential propagation (IMcpAuthCoordinatordriving workspace MCP reconnects), which is orthogonal and can land separately.Checklist
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.)gen-changesetsskill, or this PR needs no changeset. (.changeset/v1-mcp-management-plane.md)gen-docsskill, or this PR needs no doc update. (docs/{en,zh}/customization/mcp.md: plugin server changes now take effect in open sessions immediately.)