kimi-code/docs/zh/customization/mcp.md
7Sageer 59dde734f3
feat(agent-core): unify the v1 MCP management plane (#2858)
* feat(agent-core): unify the v1 MCP management plane

- 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).

* fix(agent-core): close the v1 MCP live-session reconciliation gaps

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.

* fix(agent-core): route the proactive MCP OAuth refresh through the token 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.

* fix(agent-core): keep disabled MCP servers out of auth-state classification

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.

* fix(kimi-code-sdk): short-circuit disabled MCP servers in the v2 auth-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.

* fix(kimi-code): refresh the VS Code MCP list with the workspace cwd after 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.

* fix(agent-core): keep SDK token saves matched to the OAuth token transaction

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.

* fix(agent-core): reject ambiguous legacy name-based MCP auth lookups

The legacy begin/reset auth RPCs took the registry's first name match,
silently starting OAuth for one entry of a runtime-name collision while
the locator path refused the same ambiguity; align them on the shared
uniqueness rule and point callers at the locator-addressed variants.

* fix(agent-core): propagate registry errors during live-session MCP sync

resolveMcpRuntimeTarget collapsed every registry failure into "no target":
a project config file that turned malformed mid-session made sync treat a
still-configured server as gone (tearing down the live connection) and
made config-aware reconnects report "no longer configured" instead of the
actionable config error. Absence still resolves to undefined; malformed
config now propagates — per-session sync logs and keeps the entry, and
reconnect surfaces config.invalid.

* fix(agent-core): close the remaining registry-error and ambiguity gaps

The management guard lookup mapped every registry failure to "absent",
so a malformed project config let a persisted session add write a
user-level entry over an unknown state; only not-found is a miss now. And
the name-only connection test now shares the auth paths' uniqueness rule
instead of probing the first match of a runtime-name collision.

* fix(agent-core): probe the enabled MCP entry under a disabled-name collision

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.

* fix(agent-core): let session-local MCP adds shadow plugin entries

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.

* fix(agent-core): normalize session MCP names before connecting

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.

* fix(agent-core,node-sdk): close the collision-selection and probe-freshness 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.

* fix(agent-core): normalize global MCP mutation names and guard disabled 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.

* fix(agent-core): skip proactive refresh while an interactive flow owns 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.

* fix(agent-core): allow global MCP adds over disabled plugin descriptors

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).

* fix(node-sdk): close the v2 session-MCP parity gaps

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.

* fix(agent-core): keep __proto__-named MCP servers through config parsing

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.

* fix(node-sdk): begin v2 MCP auth against a fresh OAuth service

The v2 begin path ran through the cached globalMcpOAuth, whose providers
snapshot tokens at construction: a grant another process saved (or reset)
after that cache materialized was invisible, so begin could open a browser
flow over a valid grant, or report already-authorized off a removed one.
Build the service per call — the read path and the verify probes already
do — and route the status list through the same helper. The test fixture
grows a real token endpoint honoring one rotating refresh token; the
regression fails against the cached-service implementation on v2.

* fix(agent-core): broadcast SDK-driven MCP token invalidations to live sessions

* test(agent-core-v2): give the no-op reconnect test runtime plumbing

The branch added the case against a bare McpConnectionManager, but #2961
made stdio connects resolve the runtime through runtimeResolver, matching
every other case in the file.
2026-08-17 13:19:51 +08:00

5.9 KiB
Raw Permalink Blame History

Model Context Protocol

Model Context ProtocolMCP 是一个开放协议,让模型可以安全地调用外部进程或服务暴露的工具——例如读取 GitHub issues、查询数据库、操作本地文件系统。Kimi Code CLI 作为 MCP client 接入这些外部工具,并把它们与内置工具(ReadBashGrep 等)一起暴露给 Agent 使用,行为上没有差异。

接入方式

Kimi Code CLI 支持三种 MCP server 接入方式:

  • stdioCLI 以子进程方式启动本地 MCP server通过标准输入输出通信。适合本地命令行工具。
  • HTTPCLI 连接一个已在运行的 HTTP 端点。适合远程服务或需要持久运行的进程。
  • SSECLI 连接旧式 HTTP+SSE 端点Server-Sent Events一种流式 HTTP 机制)。新 MCP server 优先使用 HTTP只有服务仍仅暴露旧式 SSE 传输时,才设置 transport: "sse"

配置

MCP server 配置写在 mcp.json 中,分两层:

  • 用户级~/.kimi-code/mcp.json(或 $KIMI_CODE_HOME/mcp.json),跨项目共享
  • 项目级:工作目录下的 .kimi-code/mcp.json,只对当前仓库生效

同名条目以项目级为准,覆盖用户级。

在 TUI 中运行 /mcp-config 可以交互式地新增、编辑或删除 server无需手动编辑 JSON 文件。运行 /mcp 可查看当前所有 server 的连接状态。

从配置中删除某个 server 不会打断进行中的会话:该 server 在 /mcp 中仍显示为 removed,其工具在这些会话中保持可见,但调用会失败并返回移除提示;新会话则完全不会注册这些工具。反过来,会话进行中新增的 server——无论是编辑 mcp.json 还是安装 plugin——都不会注册到已打开的会话中只会加入之后创建的会话。

当 Kimi Code 在不受信任的文件夹中发现项目级 MCP server 时,工作区信任提示会显示每个 server 的传输方式和启动目标。提示默认选中 Don't trust;请先移动到 Trust this folder,核对列出的命令与参数或远程 URL 后,再确认信任。信任文件夹后,该工作区的项目级 MCP server 才会启用。

mcp.json 的结构:

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
    },
    "linear": {
      "url": "https://mcp.linear.app/mcp"
    },
    "legacy-events": {
      "transport": "sse",
      "url": "https://mcp.example.com/sse"
    }
  }
}

command 字段的条目为 stdio serverurl 字段且未写 transport 的条目为 HTTP server。旧式 SSE server 需要显式把 transport 设为 "sse"

可选字段:

字段 类型 适用方式 说明
env Record<string, string> stdio 注入子进程的环境变量
cwd string stdio 子进程工作目录
headers Record<string, string> HTTP、SSE 附加到每次请求的静态请求头
bearerTokenEnvVar string HTTP、SSE 存放 bearer token 的环境变量名
enabled boolean 全部 设为 false 可禁用该 server
startupTimeoutMs number 全部 连接超时,取值范围为 12147483647 毫秒,默认 30000
toolTimeoutMs number 全部 单次工具调用超时,取值范围为 12147483647 毫秒
enabledTools string[] 全部 工具白名单
disabledTools string[] 全部 工具黑名单

连接超时和单次工具调用超时的默认值都不必逐个 server 设置:config.toml[mcp] startup_timeout_ms / [mcp] tool_timeout_ms 或环境变量 KIMI_MCP_STARTUP_TIMEOUT_MS / KIMI_MCP_TOOL_TIMEOUT_MS 可以调整全局默认值,优先级为 server 字段 > 环境变量 > config.toml > 内置默认。详见 配置文件

HTTP 与 SSE server 支持通过 headersbearerTokenEnvVar 提供静态凭证。需要 OAuth 时,运行 /mcp-config login <server-name> 完成浏览器授权。

Plugins 也可以在 manifest 中声明 MCP servers。Plugin 声明的 servers 默认启用,可以在 /plugins 中禁用或重新启用:禁用或移除后,已打开会话中的工具调用会失败并返回移除提示;新增或启用 server 会立即连接到已打开的会话。详见 Plugins

::: warning 注意 项目级 .kimi-code/mcp.json 中的 stdio 条目会在会话启动时执行本地命令,只在你信任的仓库里启用。 :::

工具命名与权限

MCP 工具按 mcp__<server>__<tool> 格式命名,例如 mcp__github__create_issue。权限规则中支持 *** 通配,例如 mcp__github__* 命中该 server 下所有工具。MCP 工具参数不参与权限匹配。

未命中权限规则的调用会触发审批请求;在审批弹窗中选择"Approve for this session"后,本次会话内的后续同类调用自动放行。

也可以在 config.toml[[permission.rules]] 中预置永久规则:

[[permission.rules]]
decision = "allow"
pattern = "mcp__github__*"

[[permission.rules]]
decision = "deny"
pattern = "mcp__filesystem__write_file"

权限规则的完整语法见配置文件

安全性

接入外部 MCP server 时需注意:

  • 只接入可信来源的 server
  • 在审批请求中核查工具名与参数是否合理
  • 对高风险工具(写文件、执行命令等)维持手动审批,避免用 mcp__* 通配放行全部工具

::: warning 注意 在 YOLO 模式下MCP 工具调用会被自动批准。仅在完全信任所接入的 MCP server 时使用此模式。 :::

下一步

  • Plugins — 在 plugin manifest 中声明 MCP server一键打包和分发
  • 配置文件 — 权限规则的完整字段参考