dsh-vscode-mode 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +129 -103
- package/assets/shell/dsh-open.cs +65 -149
- package/assets/shell/dsh-open.mjs +241 -0
- package/assets/shell/dsh-open.ps1 +28 -47
- package/assets/shell/dsh-open.sh +25 -66
- package/lib/client.js +12465 -11526
- package/lib/client.js.map +1 -1
- package/lib/index.js +3889 -2336
- package/lib/index.js.map +1 -1
- package/package.json +9 -8
- package/skills/dsh-vscodemode-mcp/SKILL.md +94 -132
- package/src/childEnv.ts +19 -0
- package/src/client/addToConversation.ts +4 -2
- package/src/client/ai/inlineProvider.ts +1 -8
- package/src/client/commandBridge.ts +6 -2
- package/src/client/externalOpen.ts +40 -19
- package/src/client/index.ts +10 -4
- package/src/client/keybindings.ts +18 -1
- package/src/client/monaco/loader.ts +3 -1
- package/src/client/openFlow.ts +36 -28
- package/src/client/openReceipt.ts +64 -0
- package/src/client/panelCommands.ts +101 -0
- package/src/client/pdf/pdfLoader.ts +4 -2
- package/src/client/receiptState.ts +120 -0
- package/src/client/rpc.ts +30 -4
- package/src/client/shortcutRecorder.ts +161 -0
- package/src/client/shortcutsOfficial.ts +13 -4
- package/src/client/sidebarBridge.ts +7 -0
- package/src/client/ui/DiffBadge.ts +2 -1
- package/src/client/ui/EditorView.ts +237 -55
- package/src/client/ui/McpSettings.ts +6 -3
- package/src/client/ui/OfficialSideTab.ts +5 -2
- package/src/client/ui/QuickOpen.ts +10 -9
- package/src/client/ui/ShortcutSettings.ts +34 -54
- package/src/client/ui/commandCatalog.ts +5 -4
- package/src/client/useOpenReceipt.ts +128 -0
- package/src/compat.ts +5 -3
- package/src/dap/manager.ts +46 -13
- package/src/devForm.ts +252 -92
- package/src/externalHandoff.ts +93 -52
- package/src/fileOpenSettings.ts +1 -24
- package/src/hostImport.ts +19 -0
- package/src/httpGuard.ts +101 -0
- package/src/index.ts +106 -97
- package/src/integrate.ts +56 -50
- package/src/lsp/transport.ts +84 -127
- package/src/mcp.ts +58 -9
- package/src/mcpIsolation.ts +211 -102
- package/src/mcpProject.ts +220 -133
- package/src/mcpRuntime.ts +343 -0
- package/src/openBridge.ts +69 -0
- package/src/openInbox.ts +207 -0
- package/src/paths.ts +8 -5
- package/src/processTree.ts +63 -0
- package/src/reveal.ts +5 -3
- package/src/routes.ts +156 -154
- package/src/rpc.ts +14 -13
- package/src/shared/appUrl.ts +19 -0
- package/src/shared/externalOpen.ts +71 -0
- package/src/shared/integration.ts +2 -0
- package/src/shared/keybindings.ts +8 -5
- package/src/shared/mcp.ts +4 -1
- package/src/shared/rpc.ts +7 -4
- package/src/shared/svn.ts +6 -8
- package/src/svnPlanTool.ts +51 -0
- package/src/unityBridge.ts +26 -3
- package/unity/com.dsh.editor/Editor/DshCodeEditor.cs +99 -203
- package/unity/com.dsh.editor/Editor/DshOpenWorker.cs +166 -0
- package/unity/com.dsh.editor/Editor/DshOpenWorker.cs.meta +2 -0
- package/unity/com.dsh.editor/README.md +16 -25
- package/unity/com.dsh.editor/package.json +2 -2
package/src/lsp/transport.ts
CHANGED
|
@@ -1,180 +1,137 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* 抽象:LSP 需要持续写 stdin + 流式读 stdout,而该抽象 stdin 不可写、输出为一次性收集。
|
|
5
|
-
* 作者 ddj 2026-08-27
|
|
6
|
-
*/
|
|
7
|
-
import { spawn, type ChildProcess } from 'node:child_process'
|
|
1
|
+
/** LSP 双向 stdio 传输;外部进程使用清理后的环境及独立进程组。 */
|
|
2
|
+
import { spawn } from 'node:child_process'
|
|
3
|
+
import type { ChildProcess, ChildProcessWithoutNullStreams } from 'node:child_process'
|
|
8
4
|
import { createFrameParser } from './jsonrpc.js'
|
|
5
|
+
import { childEnv } from '../childEnv.js'
|
|
6
|
+
import { killTree, stopTree } from '../processTree.js'
|
|
9
7
|
|
|
10
8
|
const STDERR_CAP = 256 << 10
|
|
11
|
-
|
|
12
9
|
export interface TransportSpec {
|
|
13
10
|
argv: string[]
|
|
14
11
|
cwd?: string
|
|
15
12
|
env?: Record<string, string>
|
|
16
13
|
}
|
|
17
|
-
|
|
18
14
|
export interface Transport {
|
|
19
|
-
/** 写一行帧到 stdin(自动加换行)。 */
|
|
20
15
|
write(chunk: Buffer): boolean
|
|
21
|
-
/** 推送 stdout 解析出的原始消息(内部由帧解析器调用)。 */
|
|
22
16
|
onMessage: ((message: unknown) => void) | null
|
|
23
|
-
/** stderr 收集缓冲(有界,供诊断)。 */
|
|
24
17
|
stderr(): string
|
|
25
|
-
/** 进程退出/崩溃回调(code/signal)。 */
|
|
26
18
|
onExit: ((code: number | null, signal: string | null) => void) | null
|
|
27
|
-
/** 优雅关闭后强杀(Windows 下连带进程树)。 */
|
|
28
19
|
dispose(): void
|
|
29
|
-
/** 是否仍存活。 */
|
|
30
20
|
readonly alive: boolean
|
|
31
21
|
readonly pid: number | undefined
|
|
32
22
|
}
|
|
33
|
-
|
|
34
23
|
const running = new Set<ChildProcess>()
|
|
35
24
|
|
|
36
|
-
/**
|
|
37
|
-
export function liveChildren(): ReadonlySet<ChildProcess> {
|
|
38
|
-
return running
|
|
39
|
-
}
|
|
25
|
+
/** 当前仍归本模块管理的进程。 */
|
|
26
|
+
export function liveChildren(): ReadonlySet<ChildProcess> { return running }
|
|
40
27
|
|
|
41
28
|
/**
|
|
42
|
-
*
|
|
43
|
-
* @
|
|
44
|
-
* @
|
|
45
|
-
* @param
|
|
46
|
-
* @param
|
|
47
|
-
* @
|
|
29
|
+
* 建立传输状态与回收路径;根进程退出也立即回收剩余进程组。
|
|
30
|
+
* @private
|
|
31
|
+
* @author ddj 2026年09月28号
|
|
32
|
+
* @param child 新建子进程
|
|
33
|
+
* @param onMessage 协议消息回调
|
|
34
|
+
* @param logger 诊断回调
|
|
35
|
+
* @returns 传输句柄与错误缓冲写入器
|
|
48
36
|
*/
|
|
49
|
-
|
|
50
|
-
const { argv, cwd, env } = spec
|
|
51
|
-
const child = spawn(argv[0], argv.slice(1), {
|
|
52
|
-
cwd,
|
|
53
|
-
env: { ...process.env, ...env },
|
|
54
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
55
|
-
windowsHide: true,
|
|
56
|
-
})
|
|
57
|
-
running.add(child)
|
|
58
|
-
let stderrBuf = Buffer.alloc(0)
|
|
37
|
+
function makeTransport(child: ChildProcessWithoutNullStreams, onMessage: (message: unknown) => void, logger?: (line: string) => void) {
|
|
59
38
|
let alive = true
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (stderrBuf.length < STDERR_CAP) {
|
|
64
|
-
stderrBuf = Buffer.concat([stderrBuf, chunk], Math.min(stderrBuf.length + chunk.length, STDERR_CAP))
|
|
65
|
-
}
|
|
66
|
-
logger?.(chunk.toString('utf8'))
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
let exited = false
|
|
70
|
-
const emitExit = (code: number | null, signal: string | null): void => {
|
|
71
|
-
if (exited) return
|
|
72
|
-
exited = true
|
|
73
|
-
alive = false
|
|
74
|
-
running.delete(child)
|
|
75
|
-
transport.onExit?.(code, signal)
|
|
76
|
-
}
|
|
77
|
-
child.on('error', (error) => {
|
|
78
|
-
logger?.('spawn error: ' + String(error))
|
|
79
|
-
emitExit(null, null)
|
|
80
|
-
})
|
|
81
|
-
child.on('exit', (code, signal) => emitExit(code, signal))
|
|
82
|
-
child.on('close', (code, signal) => emitExit(code, signal))
|
|
83
|
-
|
|
39
|
+
let disposed = false
|
|
40
|
+
let stderrBuf = Buffer.alloc(0)
|
|
41
|
+
let finish: (() => void) | undefined
|
|
84
42
|
const transport: Transport = {
|
|
43
|
+
/** @public @author ddj 2026年09月28号 @param chunk 协议帧 @returns 是否可写 */
|
|
85
44
|
write(chunk: Buffer): boolean {
|
|
86
45
|
if (!alive || !child.stdin.writable) return false
|
|
87
46
|
child.stdin.write(chunk)
|
|
88
47
|
return true
|
|
89
48
|
},
|
|
90
49
|
onMessage,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
},
|
|
50
|
+
/** @public @author ddj 2026年09月28号 @returns 有界错误缓冲 */
|
|
51
|
+
stderr(): string { return stderrBuf.toString('utf8') },
|
|
94
52
|
onExit: null,
|
|
53
|
+
/** @public @author ddj 2026年09月28号 @description 停止整个进程树,根退出不取消回收。 */
|
|
95
54
|
dispose(): void {
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
child
|
|
99
|
-
const timer = setTimeout(() => {
|
|
100
|
-
if (!alive) return
|
|
101
|
-
alive = false
|
|
102
|
-
running.delete(child)
|
|
103
|
-
try {
|
|
104
|
-
if (process.platform === 'win32') {
|
|
105
|
-
spawn('taskkill', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' })
|
|
106
|
-
} else {
|
|
107
|
-
child.kill('SIGKILL')
|
|
108
|
-
}
|
|
109
|
-
} catch (error) {
|
|
110
|
-
logger?.('force kill error: ' + String(error))
|
|
111
|
-
}
|
|
112
|
-
}, 1500)
|
|
113
|
-
timer.unref?.()
|
|
114
|
-
},
|
|
115
|
-
get alive() {
|
|
116
|
-
return alive
|
|
117
|
-
},
|
|
118
|
-
get pid() {
|
|
119
|
-
return child.pid
|
|
55
|
+
if (disposed) return
|
|
56
|
+
disposed = true
|
|
57
|
+
finish = stopTree(child, logger)
|
|
120
58
|
},
|
|
59
|
+
get alive() { return alive },
|
|
60
|
+
get pid() { return child.pid },
|
|
61
|
+
}
|
|
62
|
+
/** @private @author ddj 2026年09月28号 @param code 退出码 @param signal 退出信号 */
|
|
63
|
+
const emitExit = (code: number | null, signal: string | null): void => {
|
|
64
|
+
if (!alive) return
|
|
65
|
+
alive = false
|
|
66
|
+
disposed = true
|
|
67
|
+
if (finish) finish()
|
|
68
|
+
else killTree(child, logger)
|
|
69
|
+
running.delete(child)
|
|
70
|
+
transport.onExit?.(code, signal)
|
|
121
71
|
}
|
|
122
|
-
|
|
72
|
+
child.on('error', (error) => { logger?.('spawn error: ' + String(error)); emitExit(null, null) })
|
|
73
|
+
child.on('exit', emitExit)
|
|
74
|
+
child.on('close', emitExit)
|
|
75
|
+
child.stderr.on('data', (chunk: Buffer) => {
|
|
76
|
+
if (stderrBuf.length < STDERR_CAP) stderrBuf = Buffer.concat([stderrBuf, chunk], Math.min(stderrBuf.length + chunk.length, STDERR_CAP))
|
|
77
|
+
logger?.(chunk.toString('utf8'))
|
|
78
|
+
})
|
|
79
|
+
return transport
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 启动语言服务器并接入协议流,显式 env 在父环境清理后覆盖。
|
|
84
|
+
* @public
|
|
85
|
+
* @author ddj 2026年09月28号
|
|
86
|
+
* @param spec 启动参数,不经 shell
|
|
87
|
+
* @param onMessage stdout 协议消息回调
|
|
88
|
+
* @param logger 诊断日志
|
|
89
|
+
* @returns 传输句柄
|
|
90
|
+
* @throws spawn 的同步参数错误
|
|
91
|
+
*/
|
|
92
|
+
export function spawnServer(spec: TransportSpec, onMessage: (message: unknown) => void, logger?: (line: string) => void): Transport {
|
|
93
|
+
const child = spawn(spec.argv[0], spec.argv.slice(1), {
|
|
94
|
+
cwd: spec.cwd,
|
|
95
|
+
env: childEnv(spec.env),
|
|
96
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
97
|
+
windowsHide: true,
|
|
98
|
+
detached: process.platform !== 'win32',
|
|
99
|
+
})
|
|
100
|
+
running.add(child)
|
|
101
|
+
const transport = makeTransport(child, onMessage, logger)
|
|
102
|
+
const parser = createFrameParser()
|
|
123
103
|
child.stdout.on('data', (chunk: Buffer) => {
|
|
124
104
|
for (const message of parser.push(chunk)) transport.onMessage?.(message)
|
|
125
105
|
})
|
|
106
|
+
// 写入管道在退出竞态中可能异步 EPIPE,必须监听以免宿主未处理异常。
|
|
107
|
+
child.stdin.on('error', (error) => logger?.('stdin error: ' + String(error)))
|
|
126
108
|
return transport
|
|
127
109
|
}
|
|
128
110
|
|
|
129
|
-
/**
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
} catch (error) {
|
|
139
|
-
/* 忽略单个清理失败 */
|
|
140
|
-
}
|
|
141
|
-
}
|
|
111
|
+
/**
|
|
112
|
+
* 回收本模块所有进程树。
|
|
113
|
+
* @public
|
|
114
|
+
* @author ddj 2026年09月28号
|
|
115
|
+
* @param sync 宿主 exit 钩子使用同步 taskkill
|
|
116
|
+
*/
|
|
117
|
+
export function disposeAllServers(sync = false): void {
|
|
118
|
+
for (const child of running) killTree(child, undefined, sync)
|
|
142
119
|
running.clear()
|
|
143
120
|
}
|
|
144
121
|
|
|
145
|
-
/** 进程退出回收是否已注册(防重复挂监听)。 */
|
|
146
122
|
let exitHooked = false
|
|
147
|
-
|
|
148
123
|
/**
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* 长期占用内存与工作区文件句柄)。
|
|
154
|
-
*
|
|
155
|
-
* ⚠️ 只回收、不退出:宿主自身已注册 SIGINT/SIGTERM 处理(DSH profile-boot 走
|
|
156
|
-
* shutdown.interrupt → 优雅关闭 + 状态落盘),这里若调 process.exit 会打断该流程。
|
|
157
|
-
* 因此仅在信号到达时顺带回收子进程,退出时机与退出码仍由宿主决定。
|
|
158
|
-
*
|
|
159
|
-
* ⚠️ 信号监听只在宿主已有监听时才挂:Node 的默认信号语义(终止进程)会被首个监听器
|
|
160
|
-
* 取代,若本插件是唯一监听者,挂上去反而让 Ctrl+C 不再退出宿主。无既有监听即跳过,
|
|
161
|
-
* 交由 'exit' 事件兜底。
|
|
162
|
-
*
|
|
163
|
-
* exit 事件下只能同步执行,Windows 的 child.kill 不走进程树,故复用
|
|
164
|
-
* disposeAllServers 的 taskkill /T /F 强杀路径;best-effort,失败不阻塞退出。
|
|
165
|
-
* @author ddj 2026年09月11号
|
|
124
|
+
* 注册宿主退出回收;不夺取宿主原有信号退出语义。
|
|
125
|
+
* @public
|
|
126
|
+
* @author ddj 2026年09月28号
|
|
127
|
+
* @description exit 用同步回收;仅在宿主已有信号监听时追加收尾。
|
|
166
128
|
*/
|
|
167
129
|
export function hookExitReclaim(): void {
|
|
168
130
|
if (exitHooked) return
|
|
169
131
|
exitHooked = true
|
|
170
|
-
|
|
171
|
-
try {
|
|
172
|
-
disposeAllServers()
|
|
173
|
-
} catch (error) { /* 退出路径静默 */ }
|
|
174
|
-
}
|
|
175
|
-
process.on('exit', reclaim)
|
|
132
|
+
process.on('exit', () => disposeAllServers(true))
|
|
176
133
|
for (const signal of ['SIGINT', 'SIGTERM'] as const) {
|
|
177
134
|
if (process.listenerCount(signal) === 0) continue
|
|
178
|
-
process.on(signal,
|
|
135
|
+
process.on(signal, () => disposeAllServers())
|
|
179
136
|
}
|
|
180
137
|
}
|
package/src/mcp.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from './compat.js'
|
|
13
13
|
import type { MpcConfig, MpcServer, MpcTool } from './shared/mcp.js'
|
|
14
14
|
import type { Ctx } from './store.js'
|
|
15
|
+
import { checkMcpName } from './mcpProject.js'
|
|
15
16
|
|
|
16
17
|
export { LEGACY_PROJECT_PREFIX, MCP_PACKAGE, PROJECT_ENTRY_PREFIX, isProjectEntryId }
|
|
17
18
|
|
|
@@ -31,10 +32,17 @@ export function publicConfig(config: Record<string, unknown>): MpcConfig {
|
|
|
31
32
|
return out as unknown as MpcConfig
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
/**
|
|
35
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Read tool summaries from the caller's exact visible scope.
|
|
37
|
+
* @public @author ddj 2026年09月28号
|
|
38
|
+
* @param ctx Host tools service.
|
|
39
|
+
* @param serverName Server namespace.
|
|
40
|
+
* @param agent Optional agent scope; omitted for global entries.
|
|
41
|
+
* @returns Scoped tool summaries, allowing resource-only servers to have zero tools.
|
|
42
|
+
*/
|
|
43
|
+
export function toolsOf(ctx: Ctx, serverName: string, agent?: any): MpcTool[] {
|
|
36
44
|
const tools = ctx.get('tools')
|
|
37
|
-
const view = tools?.view?.(
|
|
45
|
+
const view = tools?.view?.(agent)
|
|
38
46
|
const visible = view?.visible
|
|
39
47
|
if (!(visible instanceof Map)) return []
|
|
40
48
|
const prefix = `mcp__${serverName}__`
|
|
@@ -44,7 +52,37 @@ export function toolsOf(ctx: Ctx, serverName: string): MpcTool[] {
|
|
|
44
52
|
}))
|
|
45
53
|
}
|
|
46
54
|
|
|
47
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* Restore unchanged masked values without inventing credentials for new keys.
|
|
57
|
+
* @public @author ddj 2026年09月28号
|
|
58
|
+
* @param next Submitted configuration, including editable unknown fields.
|
|
59
|
+
* @param previous Last persisted configuration.
|
|
60
|
+
* @returns Detached configuration with unchanged secrets restored; throws for an orphan mask.
|
|
61
|
+
*/
|
|
62
|
+
export function mergeSecrets(next: Record<string, unknown>, previous: Record<string, unknown>): Record<string, unknown> {
|
|
63
|
+
const merged = { ...next }
|
|
64
|
+
for (const field of ['env', 'headers']) {
|
|
65
|
+
const values = next[field]
|
|
66
|
+
if (!values || typeof values !== 'object' || Array.isArray(values)) continue
|
|
67
|
+
const old = previous[field] as Record<string, unknown> | undefined
|
|
68
|
+
const restored: Record<string, unknown> = Object.create(null)
|
|
69
|
+
for (const [key, value] of Object.entries(values)) {
|
|
70
|
+
if (value !== '••••••') { restored[key] = value; continue }
|
|
71
|
+
if (!old || !Object.hasOwn(old, key)) throw new Error('脱敏凭据没有原值,请重新填写:' + key)
|
|
72
|
+
restored[key] = old[key]
|
|
73
|
+
}
|
|
74
|
+
merged[field] = restored
|
|
75
|
+
}
|
|
76
|
+
return merged
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Summarize a global entry without treating plugin activation as a successful handshake.
|
|
81
|
+
* @public @author ddj 2026年09月28号
|
|
82
|
+
* @param ctx Host services.
|
|
83
|
+
* @param entry Global loader entry.
|
|
84
|
+
* @returns Masked status and globally visible tools.
|
|
85
|
+
*/
|
|
48
86
|
export function serverOf(ctx: Ctx, entry: any): MpcServer {
|
|
49
87
|
const config = (entry.options?.config ?? {}) as Record<string, unknown>
|
|
50
88
|
const serverName = typeof config.serverName === 'string' ? config.serverName : entry.options?.id ?? 'unknown'
|
|
@@ -53,10 +91,12 @@ export function serverOf(ctx: Ctx, entry: any): MpcServer {
|
|
|
53
91
|
return {
|
|
54
92
|
id: String(entry.id ?? entry.options?.id ?? serverName),
|
|
55
93
|
serverName,
|
|
56
|
-
enabled: entry.disabled !== true,
|
|
94
|
+
enabled: entry.disabled !== true && entry.options?.disabled !== true,
|
|
57
95
|
transport: config.transport === 'streamable-http' ? 'streamable-http' : 'stdio',
|
|
58
96
|
config: publicConfig(config),
|
|
59
|
-
status:
|
|
97
|
+
status: entry.disabled === true || entry.options?.disabled === true ? 'disabled'
|
|
98
|
+
: state === 2 ? 'unverified' : state === 1 || state === 0 ? 'connecting' : state === 3 ? 'error' : 'configured',
|
|
99
|
+
instanceCount: entry.fiber ? 1 : 0,
|
|
60
100
|
toolCount: tools.length,
|
|
61
101
|
tools,
|
|
62
102
|
error: state === 3 ? 'MCP 插件未正常运行' : undefined,
|
|
@@ -75,17 +115,26 @@ export function listMcp(ctx: Ctx): { servers: MpcServer[] } {
|
|
|
75
115
|
return { servers: entriesOf(ctx).filter((entry) => !isProjectEntryId(String(entry.id ?? entry.options?.id ?? ''))).map((entry) => serverOf(ctx, entry)) }
|
|
76
116
|
}
|
|
77
117
|
|
|
78
|
-
/**
|
|
118
|
+
/**
|
|
119
|
+
* Save a global loader entry while preserving advanced options and unchanged secrets.
|
|
120
|
+
* @public @author ddj 2026年09月28号
|
|
121
|
+
* @param ctx Host loader context.
|
|
122
|
+
* @param config Submitted global configuration.
|
|
123
|
+
* @returns Masked server status; project name collisions reject before writing.
|
|
124
|
+
*/
|
|
79
125
|
export async function saveMcp(ctx: Ctx, config: MpcConfig): Promise<MpcServer> {
|
|
80
126
|
validateConfig(config)
|
|
127
|
+
await checkMcpName(ctx, config.serverName)
|
|
81
128
|
const loader = ctx.get('loader')
|
|
82
129
|
if (!loader) throw new Error('缺少 loader 服务')
|
|
83
130
|
const existing = entriesOf(ctx).find((entry) => !isProjectEntryId(String(entry.id ?? entry.options?.id ?? '')) && entry.options?.config?.serverName === config.serverName)
|
|
131
|
+
const previous = existing?.options?.config ?? {}
|
|
132
|
+
const next = mergeSecrets({ ...previous, ...config }, previous)
|
|
84
133
|
if (existing) {
|
|
85
|
-
await loader.update(existing.id, { config:
|
|
134
|
+
await loader.update(existing.id, { config: next })
|
|
86
135
|
return serverOf(ctx, loader.resolve(existing.id))
|
|
87
136
|
}
|
|
88
|
-
const id = await loader.create({ name: MCP_PACKAGE, config:
|
|
137
|
+
const id = await loader.create({ name: MCP_PACKAGE, config: next })
|
|
89
138
|
return serverOf(ctx, loader.resolve(id))
|
|
90
139
|
}
|
|
91
140
|
|