fix(mcp-client): await Cordis startup discovery

This commit is contained in:
Tianyi Cui
2026-08-08 21:13:20 +08:00
parent 67b9e9b1d6
commit 913ecf5f1d
18 changed files with 108 additions and 39 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/self-modification/repository-plugin/README.md
README.md: 1d858c5adcc208768dcbe99f129b47abb1722431
README.zh.md: c2d0e6d72baeb0efb25abd8d50fee2922d2e810b
README.md: e10907408a98cd470ae935b327ae44322f2e54a7
README.zh.md: b8ba7dccd929574cc5aa1b7a3d68bf4f1e2f80e4

View File

@@ -69,7 +69,7 @@ Loading this package registers one effect-scoped Loader builtin. Each generated
The `.mcp.json` root is `{ "mcpServers": { ... } }`. A stdio entry accepts only `type: "stdio"` (optional), `command`, `args`, and `env`; an HTTP entry accepts only `type: "http"`, `url`, and `headers`. String values support exact `${NAME}` process-environment expansion at Plugin load, and a missing name fails that load. HTTP URLs become the existing MCP client's `streamable-http` transport; stdio entries use the prepared package directory as `cwd`.
Unknown fields reject, including OAuth and `auth` objects. There is no `CLAUDE_PLUGIN_ROOT` expansion or compatibility layer. After translation, the existing `dsh-mcp-client` exclusively owns transport creation, connection diagnostics, tool synchronization, calls, and disconnect lifecycle. Plugin activation waits for the initial connection and tool discovery, so the first model request observes a successful initial tool generation; a network or child-process connection failure is logged and still activates with no tools.
Unknown fields reject, including OAuth and `auth` objects. There is no `CLAUDE_PLUGIN_ROOT` expansion or compatibility layer. After translation, the existing `dsh-mcp-client` exclusively owns transport creation, connection diagnostics, tool synchronization, calls, and disconnect lifecycle. Repository-declared servers enable its strict startup mode: Plugin activation waits for the initial connection and tool discovery, so the first model request observes a successful initial tool generation, while a network, child-process, or discovery failure rejects the candidate repository generation instead of silently activating without its declared tools.
## Export shape

View File

@@ -69,7 +69,7 @@ Git 传输使用宿主的常规 Git 认证。公共仓库无需凭据;私有
`.mcp.json` 根对象是 `{ "mcpServers": { ... } }`。stdio 条目只接受可选的 `type: "stdio"``command``args``env`HTTP 条目只接受 `type: "http"``url``headers`。字符串值在插件加载时支持严格的 `${NAME}` 进程环境变量展开缺失变量会使该次加载失败。HTTP URL 映射到现有 MCP client 的 `streamable-http` transportstdio 条目以已准备的包目录作为 `cwd`
未知字段会被拒绝,包括 OAuth 字段与 `auth` 对象。不提供 `CLAUDE_PLUGIN_ROOT` 展开或兼容层。完成格式转换后,现有 `dsh-mcp-client` 独占 transport 创建、连接诊断、工具同步、调用和断开生命周期。插件激活会等待初始连接与工具发现,因此首个模型请求会看到成功的初始工具 generation网络子进程连接失败会记录日志,且插件仍会激活但不注册工具
未知字段会被拒绝,包括 OAuth 字段与 `auth` 对象。不提供 `CLAUDE_PLUGIN_ROOT` 展开或兼容层。完成格式转换后,现有 `dsh-mcp-client` 独占 transport 创建、连接诊断、工具同步、调用和断开生命周期。Repository 声明的 server 会启用其严格启动模式:插件激活会等待初始连接与工具发现,因此首个模型请求会看到成功的初始工具 generation网络子进程或发现失败则会拒绝候选 repository generation而不是在缺少已声明工具的情况下静默激活
## 导出形状

View File

@@ -49,12 +49,14 @@ export type ResolvedMcpServer =
args: string[]
env: Record<string, string>
cwd: string
failOnStartupError: true
}
| {
transport: 'streamable-http'
serverName: string
url: string
headers: Record<string, string>
failOnStartupError: true
}
function assertTemplate(value: string, location: string): void {
@@ -135,6 +137,7 @@ export function resolveMcpServers(document: McpDocument, environment: NodeJS.Pro
args: (definition.args ?? []).map((value, index) => expand(value, environment, `mcpServers.${serverName}.args[${index}]`)),
env: expandMap(definition.env, environment, `mcpServers.${serverName}.env`),
cwd,
failOnStartupError: true,
}
}
const url = expand(definition.url, environment, `mcpServers.${serverName}.url`)
@@ -147,6 +150,7 @@ export function resolveMcpServers(document: McpDocument, environment: NodeJS.Pro
serverName,
url,
headers: expandMap(definition.headers, environment, `mcpServers.${serverName}.headers`),
failOnStartupError: true,
}
})
}

View File

@@ -22,6 +22,7 @@ describe('repository plugin common .mcp.json support', () => {
serverName: 'expo',
url: 'https://mcp.expo.dev/mcp',
headers: {},
failOnStartupError: true,
}])
})
@@ -43,6 +44,7 @@ describe('repository plugin common .mcp.json support', () => {
args: ['--endpoint', 'http://localhost:8000'],
env: { DJ_API_URL: 'http://localhost:8000' },
cwd: '/plugin',
failOnStartupError: true,
}])
})
@@ -74,12 +76,14 @@ describe('repository plugin common .mcp.json support', () => {
args: [],
env: {},
cwd: '/plugin',
failOnStartupError: true,
},
{
transport: 'streamable-http',
serverName: 'remote',
url: 'http://localhost:3000/mcp',
headers: { Authorization: 'Bearer test-token' },
failOnStartupError: true,
},
])
})

View File

@@ -261,7 +261,7 @@ describe('prepared repository plugin Loader composition', () => {
await ctx.fiber.dispose()
})
it('delegates an MCP-only plugin to the existing client without turning connect failure into Loader failure', async () => {
it('fails an MCP repository plugin load when its declared server cannot connect', async () => {
const root = await temporaryDirectory('mcp-loader')
await writeFile(join(root, '.mcp.json'), JSON.stringify({
mcpServers: { offline: { command: join(root, 'missing-mcp-command') } },
@@ -275,12 +275,10 @@ describe('prepared repository plugin Loader composition', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(RepositoryPlugin)
const id = await ctx.loader.create({
await expect(ctx.loader.create({
name: pathToFileURL(join(directory, RepositoryPlugin.PREPARED_ENTRY_FILENAME)).href,
})
await ctx.loader.await()
})).rejects.toThrow('initial connection or tool discovery failed')
expect(ctx.tools.schemas().some(tool => tool.name.startsWith('mcp__offline__'))).toBe(false)
await ctx.loader.remove(id)
await ctx.fiber.dispose()
})