Repository navigation
feat(agent-core-v2): add the unified MCP management plane #3002
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 11 commits
Commits
Show all changes
41 commits
Select commit
Hold shift + click to select a range
2304554
feat(agent-core-v2): add the unified MCP management plane
7Sageer 29ca69a
merge: main into feat/v2-mcp-management-plane
7Sageer 07b03ea
fix(agent-core-v2): settle early and cancelled MCP OAuth callbacks
7Sageer 21b3011
refactor(node-sdk): write session MCP persists through the engine con…
7Sageer 4715a8b
refactor(agent-core-v2): strip comments from the MCP management plane…
7Sageer 4805c7b
fix(agent-core-v2): harden MCP management readiness
7Sageer a912121
test(node-sdk): cover offline MCP auth statuses
7Sageer 39ea35b
fix(agent-core-v2): isolate stdio MCP probes
7Sageer 430879b
fix(klient): normalize MCP OAuth errors
7Sageer fce8716
fix(mcp): honor workspace CRUD context and refresh timing
7Sageer 03e1e11
fix(mcp): drain OAuth refreshes during shutdown
7Sageer ce7695c
fix(mcp): guard CRUD across registry collisions
7Sageer 4024a5e
fix(mcp): canonicalize trust and refresh scheduling
7Sageer 99d4b5d
fix(mcp): close callback listener on setup failure
7Sageer e49897d
fix(mcp): preserve trust and oauth behavior
7Sageer 38c229d
fix(oauth): retain refresh tokens after SDK saves
7Sageer 3951e2b
fix(oauth): stop proactive sweep during shutdown
7Sageer a55288b
fix: await MCP workspace reconciliation
7Sageer 35aeaad
fix: serialize MCP OAuth and trust cleanup
7Sageer 69e1d79
fix: reject persisted MCP plugin collisions
7Sageer 6ed3fa0
fix: reconcile MCP workspaces concurrently
7Sageer c4a9386
fix(mcp): check project-layer trust at the queried cwd
7Sageer 47e3f66
fix(mcp): expire abandoned OAuth flows after an idle timeout
7Sageer cd8a10d
fix(mcp): keep mutable user entries writable past read-only collisions
7Sageer 3cf1061
fix(mcp): abort the auth::complete long poll on client disconnect
7Sageer 2e9ed91
fix(mcp): map OAuth flow failures to wire code 40929
7Sageer 5d2f6b4
docs(mcp): note probe credential effects and plane semantics
7Sageer cb0ada9
chore: add the SDK changeset for MCP management cwd params
7Sageer 68d4e7a
feat(mcp): expose the management plane without the experimental flag
7Sageer 234c689
fix(mcp): preserve auth management semantics
7Sageer f8da2c9
fix(agent-core-v2): bound MCP OAuth auth-server requests and the shut…
7Sageer aae4ce7
fix(node-sdk): restate engine MCP management errors as KimiError
7Sageer 24bc9b1
fix(agent-core-v2): preserve shared OAuth flow lifetime
7Sageer f027539
fix(agent-core-v2): close MCP OAuth cancellation and shutdown gaps
7Sageer eeaf1e5
fix(agent-core-v2): harden MCP probe runtime resolution and path hand…
7Sageer 1b15bc3
fix(agent-core-v2): await workspace MCP reconciliation on plugin muta…
7Sageer a5b2737
fix(mcp): close the SDK, klient, and server edge gaps
7Sageer eabb4df
fix(agent-core-v2): keep file-over-plugin MCP precedence and harden t…
7Sageer eb3bce0
chore: condense the sdk MCP changeset to one sentence
7Sageer 6ab6452
test(node-sdk): pin verify:false auth-status parity and fix the sdk c…
7Sageer 9b442d6
Merge remote-tracking branch 'origin/main' into codex/pr-3002-f2-oaut…
7Sageer File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@moonshot-ai/kimi-code": patch | ||
| --- | ||
|
|
||
| Respect workspace trust and configuration readiness when managing MCP servers. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
196 changes: 196 additions & 0 deletions
196
packages/agent-core-v2/src/app/mcpConfig/configLoader.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,196 @@ | ||
| import { dirname, isAbsolute, join, normalize, resolve } from 'pathe'; | ||
|
|
||
| import { resolveKimiHome } from '#/app/bootstrap/bootstrap'; | ||
| import { findGitWorkTree } from '#/app/git/workTree'; | ||
| import { ErrorCodes, Error2 } from '#/errors'; | ||
| import { McpServerConfigSchema, type McpServerConfig } from '#/mcpCore/config-schema'; | ||
| import type { IHostFileSystem } from '#/os/interface/hostFileSystem'; | ||
| import { OsFsErrors, HostFsError } from '#/os/interface/hostFsErrors'; | ||
|
|
||
| export interface McpJsonPaths { | ||
| readonly user: string; | ||
| readonly projectRoot: string; | ||
| readonly project: string; | ||
| } | ||
|
|
||
| export interface ResolveMcpJsonPathsInput { | ||
| readonly fs: IHostFileSystem; | ||
| readonly cwd: string; | ||
| readonly homeDir?: string; | ||
| } | ||
|
|
||
| export async function resolveMcpJsonPaths(input: ResolveMcpJsonPathsInput): Promise<McpJsonPaths> { | ||
| const start = normalize(input.cwd); | ||
| const projectRoot = (await findGitWorkTree(input.fs, start))?.root ?? start; | ||
|
|
||
| return { | ||
| user: join(resolveKimiHome(input.homeDir), 'mcp.json'), | ||
| projectRoot: join(projectRoot, '.mcp.json'), | ||
| project: join(input.cwd, '.kimi-code', 'mcp.json'), | ||
| }; | ||
| } | ||
|
|
||
| export interface LoadMcpServersInput { | ||
| readonly fs: IHostFileSystem; | ||
| readonly cwd: string; | ||
| readonly homeDir?: string; | ||
| readonly includeProject?: boolean; | ||
| } | ||
|
|
||
| export interface LoadMcpServersDetailedResult { | ||
| /** Later layers override earlier ones with the same key. */ | ||
| readonly servers: Record<string, McpServerConfig>; | ||
| /** The file each effective entry was last defined in. */ | ||
| readonly origins: Record<string, string>; | ||
| } | ||
|
|
||
| export async function loadMcpServers( | ||
| input: LoadMcpServersInput, | ||
| ): Promise<Record<string, McpServerConfig>> { | ||
| return (await loadMcpServersDetailed(input)).servers; | ||
| } | ||
|
|
||
| /** | ||
| * {@link loadMcpServers} plus the defining-file origin of every effective | ||
| * entry, for management surfaces that show where a server came from. | ||
| */ | ||
| export async function loadMcpServersDetailed( | ||
| input: LoadMcpServersInput, | ||
| ): Promise<LoadMcpServersDetailedResult> { | ||
| const paths = await resolveMcpJsonPaths(input); | ||
| if (input.includeProject === false) { | ||
| const user = await readMcpJson(input.fs, paths.user); | ||
| return { servers: user, origins: mapValuesToPath(user, paths.user) }; | ||
| } | ||
| const layers: readonly [path: string, servers: Record<string, McpServerConfig>][] = | ||
| await Promise.all([ | ||
| readMcpJson(input.fs, paths.user), | ||
| readMcpJson(input.fs, paths.projectRoot, { stdioCwdBase: dirname(paths.projectRoot) }), | ||
| readMcpJson(input.fs, paths.project), | ||
| ]).then(([user, projectRoot, project]) => [ | ||
| [paths.user, user], | ||
| [paths.projectRoot, projectRoot], | ||
| [paths.project, project], | ||
| ]); | ||
| const servers: Record<string, McpServerConfig> = Object.create(null); | ||
| const origins: Record<string, string> = Object.create(null); | ||
| for (const [path, layer] of layers) { | ||
| for (const [name, config] of Object.entries(layer)) { | ||
| servers[name] = config; | ||
| origins[name] = path; | ||
| } | ||
| } | ||
| return { servers, origins }; | ||
| } | ||
|
|
||
| interface ReadMcpJsonOptions { | ||
| readonly stdioCwdBase?: string; | ||
| } | ||
|
|
||
| async function readMcpJson( | ||
| fs: IHostFileSystem, | ||
| filePath: string, | ||
| options: ReadMcpJsonOptions = {}, | ||
| ): Promise<Record<string, McpServerConfig>> { | ||
| let text: string; | ||
| try { | ||
| text = await fs.readText(filePath); | ||
| } catch (error: unknown) { | ||
| if (isFileNotFound(error)) return {}; | ||
| throw new Error2( | ||
| ErrorCodes.CONFIG_INVALID, | ||
| `Failed to read ${filePath}: ${describeError(error)}`, | ||
| { | ||
| cause: error, | ||
| }, | ||
| ); | ||
| } | ||
|
|
||
| if (text.trim().length === 0) return {}; | ||
|
|
||
| let data: unknown; | ||
| try { | ||
| data = JSON.parse(text); | ||
| } catch (error: unknown) { | ||
| throw new Error2( | ||
| ErrorCodes.CONFIG_INVALID, | ||
| `Invalid JSON in ${filePath}: ${describeError(error)}`, | ||
| { | ||
| cause: error, | ||
| }, | ||
| ); | ||
| } | ||
|
|
||
| try { | ||
| return normalizeMcpServers(parseMcpJsonServers(data), options); | ||
| } catch (error: unknown) { | ||
| throw new Error2( | ||
| ErrorCodes.CONFIG_INVALID, | ||
| `Invalid MCP server config in ${filePath}: ${describeError(error)}`, | ||
| { | ||
| cause: error, | ||
| }, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| function parseMcpJsonServers(data: unknown): Record<string, McpServerConfig> { | ||
| if (!isRecord(data)) { | ||
| throw new Error('expected a JSON object'); | ||
| } | ||
| const raw = data['mcpServers'] ?? {}; | ||
| if (!isRecord(raw)) { | ||
| throw new Error('"mcpServers" must be an object'); | ||
| } | ||
| return Object.fromEntries( | ||
| Object.entries(raw).map(([name, value]) => [name, McpServerConfigSchema.parse(value)]), | ||
| ); | ||
| } | ||
|
|
||
| function normalizeMcpServers( | ||
| servers: Record<string, McpServerConfig>, | ||
| options: ReadMcpJsonOptions, | ||
| ): Record<string, McpServerConfig> { | ||
| const stdioCwdBase = options.stdioCwdBase; | ||
| if (stdioCwdBase === undefined) return servers; | ||
|
|
||
| return Object.fromEntries( | ||
| Object.entries(servers).map(([name, config]) => [ | ||
| name, | ||
| normalizeStdioCwd(config, stdioCwdBase), | ||
| ]), | ||
| ); | ||
| } | ||
|
|
||
| function normalizeStdioCwd(config: McpServerConfig, cwdBase: string): McpServerConfig { | ||
| if (config.transport !== 'stdio') return config; | ||
| const cwd = config.cwd === undefined ? cwdBase : resolvePath(cwdBase, config.cwd); | ||
| return { ...config, cwd }; | ||
| } | ||
|
|
||
| function mapValuesToPath( | ||
| servers: Record<string, McpServerConfig>, | ||
| path: string, | ||
| ): Record<string, string> { | ||
| const origins: Record<string, string> = Object.create(null); | ||
| for (const name of Object.keys(servers)) { | ||
| origins[name] = path; | ||
| } | ||
| return origins; | ||
| } | ||
|
|
||
| function resolvePath(base: string, value: string): string { | ||
| return isAbsolute(value) ? normalize(value) : resolve(base, value); | ||
| } | ||
|
|
||
| function isRecord(value: unknown): value is Record<string, unknown> { | ||
| return typeof value === 'object' && value !== null && !Array.isArray(value); | ||
| } | ||
|
|
||
| function isFileNotFound(error: unknown): boolean { | ||
| return error instanceof HostFsError && error.code === OsFsErrors.codes.OS_FS_NOT_FOUND; | ||
| } | ||
|
|
||
| function describeError(error: unknown): string { | ||
| return error instanceof Error ? error.message : String(error); | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When an MCP configuration is malformed as
{ "mcpServers": null }, the nullish fallback converts it to an empty object instead of rejecting it. The replaced Zod loader rejected this value, whereas the new loader silently publishes an empty server set, so a bad edit can disconnect every server from that layer without surfacingCONFIG_INVALID; default only when the property is absent and continue rejectingnull.Useful? React with 👍 / 👎.