dsh-vscode-mode 0.17.0 → 0.18.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 +6 -4
- package/lib/client.js +578 -49
- package/lib/client.js.map +1 -1
- package/lib/index.js +842 -244
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/dsh-vscodemode-mcp/SKILL.md +24 -7
- package/src/client/index.ts +15 -0
- package/src/client/mcpSessionState.ts +108 -0
- package/src/client/monaco/lsp/providers.ts +32 -3
- package/src/client/state/modelWarmer.ts +198 -0
- package/src/client/styles/editor.css +5 -1
- package/src/client/ui/EditorPane.ts +16 -4
- package/src/client/ui/EditorView.ts +76 -18
- package/src/client/ui/McpSessionStatus.ts +75 -0
- package/src/client/ui/McpSettings.ts +47 -13
- package/src/mcp.ts +1 -0
- package/src/mcpBridge.ts +163 -0
- package/src/mcpDiscover.ts +38 -0
- package/src/mcpPool.ts +245 -0
- package/src/mcpProject.ts +15 -12
- package/src/mcpRuntime.ts +299 -254
- package/src/rpc.ts +13 -1
- package/src/shared/mcp.ts +47 -0
- package/src/shared/rpc.ts +5 -1
package/src/mcpRuntime.ts
CHANGED
|
@@ -1,316 +1,363 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Project-owned MCP connections and scoped on-demand capabilities. @author ddj 2026年10月20号 */
|
|
2
2
|
import { loadHostModule } from './hostImport.js'
|
|
3
3
|
import { entriesOf, MCP_PACKAGE, publicConfig, toolsOf } from './mcp.js'
|
|
4
4
|
import { isProjectEntryId } from './compat.js'
|
|
5
5
|
import { matchWorkspace } from './mcpIsolation.js'
|
|
6
6
|
import { listProjects, projectEntryId, reconcileProject } from './mcpProject.js'
|
|
7
|
-
import type
|
|
7
|
+
import { parseMcpPolicy, type McpPolicy, type McpPhase, type McpSessionServer, type McpSessionStatus, type MpcConfig, type MpcServer } from './shared/mcp.js'
|
|
8
|
+
import { McpPool, waitMcp, type PoolLease, type BridgeMaker } from './mcpPool.js'
|
|
9
|
+
import { McpBinding } from './mcpBridge.js'
|
|
10
|
+
import { installDiscover, discoverPrompt } from './mcpDiscover.js'
|
|
8
11
|
import type { Ctx } from './store.js'
|
|
9
12
|
import { cwdOfSession } from './store.js'
|
|
10
13
|
|
|
11
|
-
export type ProjectDef = { config: MpcConfig; enabled: boolean }
|
|
12
|
-
type Instance = {
|
|
13
|
-
type AgentRecord = {
|
|
14
|
+
export type ProjectDef = { config: MpcConfig; enabled: boolean; policy?: McpPolicy }
|
|
15
|
+
type Instance = { key: string; path: string; lease: PoolLease; binding: McpBinding; ready: Promise<void>; startedAt: number; reused: boolean; firstCallMs?: number }
|
|
16
|
+
type AgentRecord = {
|
|
17
|
+
instances: Map<string, Instance>
|
|
18
|
+
resources: Map<string, () => void>
|
|
19
|
+
requested: Set<string>
|
|
20
|
+
live: boolean
|
|
21
|
+
tail: Promise<void>
|
|
22
|
+
remove?: () => void
|
|
23
|
+
startedAt: number
|
|
24
|
+
finishedAt?: number
|
|
25
|
+
error?: string
|
|
26
|
+
}
|
|
14
27
|
export type McpLoader = () => Promise<any>
|
|
15
28
|
const runtimes = new WeakMap<object, McpRuntime>()
|
|
16
29
|
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
* @private @author ddj 2026年09月28号
|
|
20
|
-
* @returns Official MCP plugin namespace; rejects if the host package is absent.
|
|
21
|
-
*/
|
|
22
|
-
async function loadMcp(): Promise<any> {
|
|
23
|
-
return loadHostModule(MCP_PACKAGE)
|
|
24
|
-
}
|
|
30
|
+
/** @private @author ddj 2026年09月28号 @returns Official namespace from the running Host installation. */
|
|
31
|
+
async function loadMcp(): Promise<any> { return loadHostModule(MCP_PACKAGE) }
|
|
25
32
|
|
|
26
|
-
/**
|
|
27
|
-
* Resolve one runtime per host, shared by lifecycle and RPC contexts.
|
|
28
|
-
* @public @author ddj 2026年09月28号
|
|
29
|
-
* @param ctx Host context.
|
|
30
|
-
* @returns Lazy runtime; allocating it does not start connections.
|
|
31
|
-
*/
|
|
33
|
+
/** @public @author ddj 2026年09月28号 @param ctx Host context. @returns Lazy per-host runtime, without opening connections. */
|
|
32
34
|
export function runtimeOf(ctx: Ctx): McpRuntime {
|
|
33
35
|
const owner = ctx.root ?? ctx
|
|
34
36
|
let runtime = runtimes.get(owner)
|
|
35
|
-
if (!runtime) {
|
|
36
|
-
runtime = new McpRuntime(ctx)
|
|
37
|
-
runtimes.set(owner, runtime)
|
|
38
|
-
}
|
|
37
|
+
if (!runtime) { runtime = new McpRuntime(ctx); runtimes.set(owner, runtime) }
|
|
39
38
|
return runtime
|
|
40
39
|
}
|
|
41
40
|
|
|
42
|
-
/**
|
|
41
|
+
/** The pool owns transports; each agent owns only revocable public capabilities and leases. */
|
|
43
42
|
export class McpRuntime {
|
|
44
43
|
readonly projects = new Map<string, Map<string, ProjectDef>>()
|
|
45
44
|
readonly agents = new Map<any, AgentRecord>()
|
|
46
|
-
|
|
45
|
+
readonly pool: McpPool
|
|
47
46
|
private stopped = false
|
|
47
|
+
private offDiscover?: () => void
|
|
48
48
|
private readonly retired = new WeakSet<object>()
|
|
49
49
|
private module?: Promise<any>
|
|
50
|
+
private readonly pending = new Set<Promise<unknown>>()
|
|
51
|
+
private readonly barriers = new Map<string, Promise<void>>()
|
|
52
|
+
|
|
53
|
+
/** @public @author ddj 2026年10月20号 @param ctx Host services. @param load Official module resolver. @param make Optional controlled private-scope factory for tests. */
|
|
54
|
+
constructor(private readonly ctx: Ctx, private readonly load: McpLoader = loadMcp, make?: BridgeMaker) {
|
|
55
|
+
this.pool = new McpPool(ctx, () => this.getModule(), make)
|
|
56
|
+
}
|
|
50
57
|
|
|
51
|
-
/**
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
*/
|
|
57
|
-
constructor(private readonly ctx: Ctx, private readonly load: McpLoader = loadMcp) {}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Serialize reconfiguration and creation; a failed operation cannot poison the queue.
|
|
61
|
-
* @private @author ddj 2026年09月28号
|
|
62
|
-
* @param action Mutation to execute while the runtime remains live.
|
|
63
|
-
* @returns Completion of this mutation, including official startup readiness.
|
|
64
|
-
*/
|
|
65
|
-
private enqueue(action: () => Promise<void>): Promise<void> {
|
|
66
|
-
const run = async () => { if (!this.stopped) await action() }
|
|
67
|
-
const next = this.tail.then(run)
|
|
68
|
-
this.tail = next.catch(() => undefined)
|
|
69
|
-
return next
|
|
58
|
+
/** @private @author ddj 2026年10月20号 @param task Owned background operation. Tracks failures without abandoning work. */
|
|
59
|
+
private track(task: Promise<unknown>): void {
|
|
60
|
+
this.pending.add(task)
|
|
61
|
+
void task.catch((error) => this.ctx.logger?.warn?.('Project MCP task failed: ' + String(error)))
|
|
62
|
+
.finally(() => this.pending.delete(task))
|
|
70
63
|
}
|
|
71
64
|
|
|
72
|
-
/**
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
65
|
+
/** @private @author ddj 2026年10月20号 @param agent Scope owner. @returns Live record; retired agents cannot be resurrected. */
|
|
66
|
+
private recordOf(agent: any): AgentRecord | undefined {
|
|
67
|
+
if (this.stopped || this.retired.has(agent)) return undefined
|
|
68
|
+
let record = this.agents.get(agent)
|
|
69
|
+
if (!record) {
|
|
70
|
+
record = { instances: new Map(), resources: new Map(), requested: new Set(), live: true, tail: Promise.resolve(), startedAt: Date.now() }
|
|
71
|
+
this.agents.set(agent, record)
|
|
72
|
+
}
|
|
73
|
+
return record
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** @public @author ddj 2026年10月20号 @param agent Newly created scope. Starts only configuration timing. */
|
|
77
|
+
prepare(agent: any): void { this.recordOf(agent) }
|
|
78
|
+
|
|
79
|
+
/** @public @author ddj 2026年10月20号 @param agent Owner. @param error Configuration failure, never sent raw to clients. */
|
|
80
|
+
configError(agent: any, error?: unknown): void {
|
|
81
|
+
const record = this.recordOf(agent)
|
|
82
|
+
if (record) record.error = error === undefined ? undefined : '项目 MCP 配置读取或校验失败,保留最后有效配置;请查看 Host 日志'
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** @public @author ddj 2026年09月28号 @param agent Agent identity. @returns Longest registered workspace containing its cwd. */
|
|
78
86
|
workspace(agent: any): string | undefined {
|
|
79
87
|
const paths = (this.ctx.get('workspaceRegistry')?.list?.() ?? []).map((ws: { path: string }) => ws.path)
|
|
80
88
|
return matchWorkspace(cwdOfSession(this.ctx, agent?.session) ?? undefined, paths)
|
|
81
89
|
}
|
|
82
90
|
|
|
83
|
-
/**
|
|
84
|
-
* Read configured server ownership, including disabled definitions.
|
|
85
|
-
* @public @author ddj 2026年09月28号
|
|
86
|
-
* @returns Server name to workspace mapping.
|
|
87
|
-
*/
|
|
91
|
+
/** @public @author ddj 2026年09月28号 @returns Configured namespaces including disabled definitions. */
|
|
88
92
|
owners(): Map<string, string> {
|
|
89
93
|
const names = new Map<string, string>()
|
|
90
|
-
for (const record of this.agents.values())
|
|
91
|
-
for (const [name, instance] of record.instances) names.set(name, instance.path)
|
|
92
|
-
}
|
|
94
|
+
for (const record of this.agents.values()) for (const [name, instance] of record.instances) names.set(name, instance.path)
|
|
93
95
|
for (const [path, defs] of this.projects) for (const name of defs.keys()) names.set(name, path)
|
|
94
96
|
return names
|
|
95
97
|
}
|
|
96
98
|
|
|
97
|
-
/**
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
99
|
+
/** @private @author ddj 2026年10月20号 @param def Effective definition. @returns Config and policy generation identity. */
|
|
100
|
+
private defKey(def: ProjectDef): string { return JSON.stringify([def.config, parseMcpPolicy(def.policy)]) }
|
|
101
|
+
|
|
102
|
+
/** @private @author ddj 2026年09月28号 Refreshes existing isolation restrictions synchronously. */
|
|
103
|
+
private changed(): void { this.ctx.emit?.('vscode-mode/mcp-change') }
|
|
104
|
+
|
|
105
|
+
/** @private @author ddj 2026年10月20号 @param instance Public lease to revoke. Revokes synchronously before lease release. */
|
|
106
|
+
private revoke(instance: Instance): void { instance.binding.dispose(); instance.lease.release() }
|
|
107
|
+
|
|
108
|
+
/** @public @author ddj 2026年10月20号 @param path Owner workspace. @param defs Validated snapshot. @param refresh Optional explicitly invalidated service. @param wait Await configuration cleanup, never connection readiness. @returns Publication and optionally cleanup completion. */
|
|
109
|
+
async replace(path: string, defs: Map<string, ProjectDef>, refresh?: string, wait = true): Promise<void> {
|
|
110
|
+
if (this.stopped) return
|
|
111
|
+
const previous = this.projects.get(path)
|
|
112
|
+
if (!refresh && previous && JSON.stringify([...previous]) === JSON.stringify([...defs])) return
|
|
113
|
+
this.projects.set(path, defs)
|
|
114
|
+
const closing = new Set<ReturnType<McpPool['acquire']>['entry']>()
|
|
115
|
+
for (const [agent, record] of this.agents) {
|
|
116
|
+
for (const [name, instance] of record.instances) {
|
|
117
|
+
const def = defs.get(name)
|
|
118
|
+
if (instance.path !== path) continue
|
|
119
|
+
if (this.workspace(agent) === path && def?.enabled && this.defKey(def) === instance.key && name !== refresh) continue
|
|
120
|
+
record.instances.delete(name)
|
|
121
|
+
this.revoke(instance)
|
|
122
|
+
closing.add(instance.lease.entry)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Warm entries may have no active subscribers, but must still respect config invalidation.
|
|
126
|
+
for (const entry of this.pool.entries.values()) {
|
|
127
|
+
if (entry.spec.workspace !== path) continue
|
|
128
|
+
const def = defs.get(entry.spec.config.serverName)
|
|
129
|
+
const config = def && this.options(def.config, path)
|
|
130
|
+
if (!def?.enabled || JSON.stringify(config) !== JSON.stringify(entry.spec.config)
|
|
131
|
+
|| JSON.stringify(parseMcpPolicy(def.policy)) !== JSON.stringify(entry.spec.policy) || entry.spec.config.serverName === refresh) closing.add(entry)
|
|
132
|
+
}
|
|
133
|
+
const prior = this.barriers.get(path) ?? Promise.resolve()
|
|
134
|
+
const cleanup = Promise.all([...closing].map((entry) => this.pool.retire(entry))).then(() => undefined)
|
|
135
|
+
const barrier = Promise.all([prior, cleanup]).then(() => undefined)
|
|
136
|
+
this.barriers.set(path, barrier)
|
|
137
|
+
this.track(barrier)
|
|
138
|
+
this.changed()
|
|
139
|
+
const updates = [...this.agents].map(([agent, record]) => this.enqueue(agent, record))
|
|
140
|
+
if (wait) await Promise.all([barrier, ...updates])
|
|
104
141
|
}
|
|
105
142
|
|
|
106
|
-
/**
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
* @param refresh Optional server whose unchanged fiber must also be recreated.
|
|
112
|
-
* @returns Completion after old fibers close and replacement startups settle.
|
|
113
|
-
*/
|
|
114
|
-
replace(path: string, defs: Map<string, ProjectDef>, refresh?: string): Promise<void> {
|
|
115
|
-
const update = async () => {
|
|
116
|
-
this.projects.set(path, defs)
|
|
117
|
-
this.changed()
|
|
118
|
-
for (const [agent, record] of this.agents) await this.syncAgent(agent, record, this.workspace(agent) === path ? refresh : undefined)
|
|
119
|
-
this.changed()
|
|
143
|
+
/** @private @author ddj 2026年10月20号 @param agent Owner. @param record Lightweight queue. @returns Scoped catalogue/resource update, not transport initialization. */
|
|
144
|
+
private enqueue(agent: any, record: AgentRecord): Promise<void> {
|
|
145
|
+
const update = async (): Promise<void> => {
|
|
146
|
+
if (!record.live || this.stopped) return
|
|
147
|
+
this.syncAgent(agent, record)
|
|
120
148
|
}
|
|
121
|
-
|
|
149
|
+
const task = record.tail.then(update)
|
|
150
|
+
record.tail = task.catch(() => { record.error = 'MCP 清理或入口更新失败,请查看 Host 日志' })
|
|
151
|
+
this.track(task)
|
|
152
|
+
return task
|
|
122
153
|
}
|
|
123
154
|
|
|
124
|
-
/**
|
|
125
|
-
* Attach an agent once and await its project plugin startups.
|
|
126
|
-
* @public @author ddj 2026年09月28号
|
|
127
|
-
* @param agent Created or already-active agent.
|
|
128
|
-
* @returns Completion suitable for the serial agent/created listener.
|
|
129
|
-
*/
|
|
155
|
+
/** @public @author ddj 2026年10月20号 @param agent Live agent. @returns Lightweight capability installation; never waits for MCP startup. */
|
|
130
156
|
attach(agent: any): Promise<void> {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
157
|
+
const record = this.recordOf(agent)
|
|
158
|
+
if (!record) return Promise.resolve()
|
|
159
|
+
this.offDiscover ??= installDiscover(this, this.ctx)
|
|
160
|
+
if (!record.remove) record.remove = discoverPrompt(this, agent)
|
|
161
|
+
return this.enqueue(agent, record)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** @private @author ddj 2026年10月20号 @param agent Owner. @param record Scope record. Updates lazy resources and schedules explicit background services. */
|
|
165
|
+
private syncAgent(agent: any, record: AgentRecord): void {
|
|
166
|
+
const path = this.workspace(agent)
|
|
167
|
+
const defs = this.projects.get(path ?? '') ?? new Map<string, ProjectDef>()
|
|
168
|
+
for (const [name, instance] of record.instances) {
|
|
169
|
+
const def = defs.get(name)
|
|
170
|
+
if (instance.path === path && def?.enabled && this.defKey(def) === instance.key) continue
|
|
171
|
+
record.instances.delete(name)
|
|
172
|
+
this.revoke(instance)
|
|
173
|
+
}
|
|
174
|
+
for (const remove of record.resources.values()) remove()
|
|
175
|
+
record.resources.clear()
|
|
176
|
+
for (const [name, def] of defs) {
|
|
177
|
+
if (!def.enabled) continue
|
|
178
|
+
this.lazyResource(agent, record, name)
|
|
179
|
+
if (!record.instances.has(name) && (parseMcpPolicy(def.policy).startup === 'background' || record.requested.has(name))) this.track(this.connect(agent, name))
|
|
136
180
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return this.enqueue(attach)
|
|
181
|
+
record.finishedAt ??= Date.now()
|
|
182
|
+
this.changed()
|
|
140
183
|
}
|
|
141
184
|
|
|
142
|
-
/**
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
185
|
+
/** @private @author ddj 2026年10月20号 @param agent Owner. @param record Ledger. @param name Enabled namespace. Installs a resource-only on-demand entry without starting a connection. */
|
|
186
|
+
private lazyResource(agent: any, record: AgentRecord, name: string): void {
|
|
187
|
+
const resources = agent.ctx.get('mcpResources')
|
|
188
|
+
/** @author ddj 2026年10月20号 @param request Resource operation. @param execution Actual registry caller. @returns Official provider result after demand connection. */
|
|
189
|
+
const request = async (request: any, execution: any): Promise<any> => {
|
|
190
|
+
if (execution?.agent !== agent) throw new Error('不能借用其他会话的 MCP 资源')
|
|
191
|
+
await this.connect(agent, name, execution.signal)
|
|
192
|
+
const instance = record.instances.get(name)
|
|
193
|
+
if (!instance || !this.valid(agent, record, name, instance)) throw new Error('项目 MCP 配置已失效')
|
|
194
|
+
return this.pool.run(instance.lease.entry, async () => {
|
|
195
|
+
if (!this.valid(agent, record, name, instance)) throw new Error('项目 MCP 配置已失效')
|
|
196
|
+
const provider = instance.lease.entry.bridge?.resources.get(name)
|
|
197
|
+
if (!provider) throw new Error('MCP 未提供资源入口')
|
|
198
|
+
const startedAt = Date.now()
|
|
199
|
+
const result = await provider.request(request, execution)
|
|
200
|
+
instance.firstCallMs ??= Date.now() - startedAt
|
|
201
|
+
return result
|
|
202
|
+
}, execution.signal)
|
|
203
|
+
}
|
|
204
|
+
record.resources.set(name, resources.register(name, { request }))
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** @private @author ddj 2026年10月20号 @param agent Owner. @param record Ledger. @param name Namespace. @param instance Exact lease. @returns Whether a previously issued proxy can still execute. */
|
|
208
|
+
private valid(agent: any, record: AgentRecord, name: string, instance: Instance): boolean {
|
|
209
|
+
const path = this.workspace(agent)
|
|
210
|
+
const def = this.projects.get(path ?? '')?.get(name)
|
|
211
|
+
return !this.stopped && record.live && record.instances.get(name) === instance && instance.path === path
|
|
212
|
+
&& def?.enabled === true && this.defKey(def) === instance.key
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** @private @author ddj 2026年10月20号 @param config Official config. @param path Default working directory. @returns Detached effective config; project strategy is never forwarded. */
|
|
216
|
+
private options(config: MpcConfig, path: string): MpcConfig {
|
|
217
|
+
return config.transport === 'stdio' ? { ...config, cwd: config.cwd || path } : { ...config }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** @public @author ddj 2026年10月20号 @param agent Actual execution owner. @returns Safe enabled catalogue without opening connections. */
|
|
221
|
+
catalogue(agent: any): Array<{ serverName: string; phase: McpPhase }> {
|
|
222
|
+
const record = this.agents.get(agent)
|
|
223
|
+
if (!record?.live || this.stopped) return []
|
|
224
|
+
return [...this.projects.get(this.workspace(agent) ?? '') ?? []].filter(([, def]) => def.enabled)
|
|
225
|
+
.map(([serverName]) => {
|
|
226
|
+
const entry = record.instances.get(serverName)?.lease.entry
|
|
227
|
+
const phase: McpPhase = entry?.fiber?.state === 3 ? 'error' : entry?.phase ?? 'idle'
|
|
228
|
+
return { serverName, phase }
|
|
229
|
+
})
|
|
153
230
|
}
|
|
154
231
|
|
|
155
|
-
/**
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
232
|
+
/** @private @author ddj 2026年10月20号 @param agent Owner. @param record Ledger. @param path Workspace. @param name Namespace. @param def Validated definition. @returns Synchronously reserved lease and public binding. */
|
|
233
|
+
private reserve(agent: any, record: AgentRecord, path: string, name: string, def: ProjectDef): Instance {
|
|
234
|
+
const lease = this.pool.acquire({ workspace: path, owner: agent, config: this.options(def.config, path), policy: parseMcpPolicy(def.policy) })
|
|
235
|
+
const instance = { key: this.defKey(def), path, lease, startedAt: Date.now(), reused: lease.entry.refs > 1 || lease.entry.phase === 'ready' } as Instance
|
|
236
|
+
const valid = (): boolean => this.valid(agent, record, name, instance)
|
|
237
|
+
instance.binding = new McpBinding(agent, this.pool, lease.entry, valid, (elapsed) => { instance.firstCallMs ??= elapsed })
|
|
238
|
+
record.instances.set(name, instance)
|
|
239
|
+
instance.ready = lease.entry.ready.then(() => {
|
|
240
|
+
if (!valid()) throw new Error('项目 MCP 配置已失效')
|
|
241
|
+
instance.binding.sync()
|
|
242
|
+
this.changed()
|
|
243
|
+
})
|
|
244
|
+
this.track(instance.ready)
|
|
245
|
+
return instance
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** @public @author ddj 2026年10月20号 @param agent Actual active caller. @param name Enabled configured server. @param signal Caller-local cancellation. @returns Real tool schemas after private initial sync; no guessed tools. */
|
|
249
|
+
async connect(agent: any, name: string, signal?: AbortSignal): Promise<{ serverName: string; tools: any[]; resources: boolean; nextRequest: string }> {
|
|
250
|
+
const record = this.agents.get(agent)
|
|
251
|
+
if (!record?.live || this.stopped) throw new Error('会话未激活,不能连接项目 MCP')
|
|
252
|
+
await waitMcp(record.tail, signal)
|
|
253
|
+
const path = this.workspace(agent)
|
|
254
|
+
await waitMcp(this.barriers.get(path ?? '') ?? Promise.resolve(), signal)
|
|
255
|
+
if (!record.live || this.workspace(agent) !== path) throw new Error('项目 MCP 作用域已失效')
|
|
256
|
+
const def = this.projects.get(path ?? '')?.get(name)
|
|
257
|
+
if (!def?.enabled || !path) throw new Error('当前工作区未启用该项目 MCP')
|
|
258
|
+
const global = entriesOf(this.ctx).some((entry) => !isProjectEntryId(String(entry.id ?? entry.options?.id ?? '')) && entry.options?.config?.serverName === name)
|
|
259
|
+
if (global) throw new Error('serverName 已被全局 MCP 使用')
|
|
260
|
+
let instance = record.instances.get(name)
|
|
261
|
+
if (instance?.lease.entry.phase === 'error' && (instance.lease.entry.retryAt ?? Infinity) <= Date.now()) {
|
|
262
|
+
record.instances.delete(name); this.revoke(instance); instance = undefined
|
|
166
263
|
}
|
|
167
|
-
|
|
264
|
+
record.requested.add(name)
|
|
265
|
+
instance ??= this.reserve(agent, record, path, name, def)
|
|
266
|
+
await waitMcp(instance.ready, signal)
|
|
267
|
+
if (!this.valid(agent, record, name, instance)) throw new Error('项目 MCP 配置已失效')
|
|
268
|
+
const visible = agent.ctx.get('tools')?.view?.(agent)?.visible
|
|
269
|
+
const tools = [...instance.lease.entry.bridge!.tools.values()].filter((tool) => visible?.has(tool.name))
|
|
270
|
+
.map((tool) => ({ name: tool.name, description: tool.description, parameters: tool.parameters }))
|
|
271
|
+
return { serverName: name, tools, resources: instance.lease.entry.bridge!.resources.has(name), nextRequest: 'Use these tools in the next model request; PTC must start a new program to refresh its SDK.' }
|
|
168
272
|
}
|
|
169
273
|
|
|
170
|
-
/**
|
|
171
|
-
* Cancel and drain every fiber owned by a disposed agent.
|
|
172
|
-
* @public @author ddj 2026年09月28号
|
|
173
|
-
* @param agent Disposed agent.
|
|
174
|
-
* @returns Completion of connection cleanup.
|
|
175
|
-
*/
|
|
274
|
+
/** @public @author ddj 2026年10月20号 @param agent Disposed owner. @returns Scoped teardown; shared transports remain owned by other leases. */
|
|
176
275
|
async detach(agent: any): Promise<void> {
|
|
177
276
|
this.retired.add(agent)
|
|
178
277
|
const record = this.agents.get(agent)
|
|
179
278
|
if (!record) return
|
|
180
279
|
record.live = false
|
|
181
|
-
|
|
280
|
+
record.remove?.()
|
|
281
|
+
for (const remove of record.resources.values()) remove()
|
|
282
|
+
for (const instance of record.instances.values()) this.revoke(instance)
|
|
283
|
+
record.instances.clear()
|
|
182
284
|
this.agents.delete(agent)
|
|
183
285
|
this.changed()
|
|
184
286
|
}
|
|
185
287
|
|
|
186
|
-
/**
|
|
187
|
-
* Match the agent ledger to the current enabled definitions.
|
|
188
|
-
* @private @author ddj 2026年09月28号
|
|
189
|
-
* @param agent Scoped plugin owner.
|
|
190
|
-
* @param record Agent ledger.
|
|
191
|
-
* @param refresh Optional server to reconnect.
|
|
192
|
-
* @returns Completion of removals and scoped mounts.
|
|
193
|
-
*/
|
|
194
|
-
private async syncAgent(agent: any, record: AgentRecord, refresh?: string): Promise<void> {
|
|
195
|
-
if (!record.live || this.stopped) return
|
|
196
|
-
const path = this.workspace(agent)
|
|
197
|
-
const defs = this.projects.get(path ?? '') ?? new Map<string, ProjectDef>()
|
|
198
|
-
for (const [name, instance] of record.instances) {
|
|
199
|
-
const def = defs.get(name)
|
|
200
|
-
if (def?.enabled && instance.path === path && instance.key === JSON.stringify(def.config) && name !== refresh) continue
|
|
201
|
-
await this.stopOne(record, name)
|
|
202
|
-
}
|
|
203
|
-
for (const [name, def] of defs) {
|
|
204
|
-
if (!record.live || this.stopped) break
|
|
205
|
-
if (!def.enabled || record.instances.has(name)) continue
|
|
206
|
-
await this.mount(agent, record, def.config, path!)
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Cache successful module loads while allowing explicit refresh after transient failures.
|
|
212
|
-
* @private @author ddj 2026年10月08号
|
|
213
|
-
* @returns Official plugin module; load failures propagate to the instance diagnostic.
|
|
214
|
-
*/
|
|
288
|
+
/** @private @author ddj 2026年10月08号 @returns Cached official module; failures are retryable. */
|
|
215
289
|
private async getModule(): Promise<any> {
|
|
216
290
|
const loading = this.module ??= this.load()
|
|
217
|
-
try {
|
|
218
|
-
return await loading
|
|
219
|
-
} catch (error) {
|
|
220
|
-
if (this.module === loading) this.module = undefined
|
|
221
|
-
throw error
|
|
222
|
-
}
|
|
291
|
+
try { return await loading } catch (error) { if (this.module === loading) this.module = undefined; throw error }
|
|
223
292
|
}
|
|
224
293
|
|
|
225
|
-
/**
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const options = config.transport === 'stdio' ? { ...config, cwd: config.cwd || path } : { ...config }
|
|
243
|
-
instance.fiber = agent.ctx.plugin(officialMcp, options)
|
|
244
|
-
await instance.fiber
|
|
245
|
-
} catch (error) {
|
|
246
|
-
instance.error = String(error)
|
|
247
|
-
await this.stopFiber(instance)
|
|
248
|
-
instance.fiber = undefined
|
|
294
|
+
/** @public @author ddj 2026年10月20号 @returns Detached read-only active session statuses, without IO or activation. */
|
|
295
|
+
sessionSnapshot(): { sessions: McpSessionStatus[]; sampledAt: number } {
|
|
296
|
+
const sessions: McpSessionStatus[] = []
|
|
297
|
+
for (const [agent, record] of this.agents) {
|
|
298
|
+
if (!record.live || typeof agent?.session?.id !== 'string') continue
|
|
299
|
+
const servers = this.sessionServers(agent, record)
|
|
300
|
+
const active = servers.find((server) => ['stopping', 'starting', 'loading', 'queued'].includes(server.phase))
|
|
301
|
+
let phase: McpPhase = 'ready'
|
|
302
|
+
if (record.error || servers.some((server) => server.phase === 'error')) phase = 'error'
|
|
303
|
+
else if (active) phase = active.phase
|
|
304
|
+
else if (!record.finishedAt) phase = 'preparing'
|
|
305
|
+
else if (!servers.length) phase = 'none'
|
|
306
|
+
else if (servers.some((server) => server.phase === 'idle')) phase = 'idle'
|
|
307
|
+
const attempts = servers.filter((server) => server.phase !== 'idle')
|
|
308
|
+
const startedAt = attempts.length ? Math.min(...attempts.map((server) => server.startedAt)) : record.startedAt
|
|
309
|
+
sessions.push({ configMs: Math.max(0, (record.finishedAt ?? Date.now()) - record.startedAt), sessionId: agent.session.id, phase, startedAt, phaseAt: active?.phaseAt ?? startedAt,
|
|
310
|
+
finishedAt: active ? undefined : Math.max(record.finishedAt ?? record.startedAt, ...servers.map((server) => server.finishedAt ?? 0)), error: record.error, servers })
|
|
249
311
|
}
|
|
312
|
+
return { sessions, sampledAt: Date.now() }
|
|
250
313
|
}
|
|
251
314
|
|
|
252
|
-
/**
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
315
|
+
/** @private @author ddj 2026年10月20号 @param agent Owner. @param record Ledger. @returns Server phases with caller-local timing, not pooled connection age. */
|
|
316
|
+
private sessionServers(agent: any, record: AgentRecord): McpSessionServer[] {
|
|
317
|
+
return this.catalogue(agent).map(({ serverName, phase }) => {
|
|
318
|
+
const instance = record.instances.get(serverName)
|
|
319
|
+
if (!instance) return { serverName, phase: 'idle', startedAt: record.finishedAt ?? record.startedAt, phaseAt: record.finishedAt ?? record.startedAt }
|
|
320
|
+
const entry = instance.lease.entry
|
|
321
|
+
return { serverName, phase, startedAt: instance.startedAt, phaseAt: Math.max(instance.startedAt, entry.phaseAt),
|
|
322
|
+
finishedAt: entry.finishedAt && Math.max(instance.startedAt, entry.finishedAt), error: entry.error, reused: instance.reused,
|
|
323
|
+
queuedMs: Math.max(0, (entry.startAt ?? Date.now()) - instance.startedAt),
|
|
324
|
+
connectMs: entry.connectedAt === undefined ? undefined : Math.max(0, entry.connectedAt - Math.max(instance.startedAt, entry.startAt ?? instance.startedAt)), firstCallMs: instance.firstCallMs }
|
|
325
|
+
})
|
|
261
326
|
}
|
|
262
327
|
|
|
263
|
-
/**
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
const error = instances.find((item) => item.error)?.error
|
|
282
|
-
const fibers = instances.filter((item) => item.fiber)
|
|
283
|
-
const failed = error || fibers.some((item) => item.fiber.state === 3)
|
|
284
|
-
const status = !def.enabled ? 'disabled' : failed ? 'error' : fibers.length === 0 ? 'configured'
|
|
285
|
-
: fibers.some((item) => item.fiber.state === 1 || item.fiber.state === 0) ? 'connecting' : 'unverified'
|
|
286
|
-
return {
|
|
287
|
-
id: projectEntryId(path, name), serverName: name, enabled: def.enabled,
|
|
288
|
-
transport: def.config.transport, config: publicConfig(def.config as unknown as Record<string, unknown>),
|
|
289
|
-
status, instanceCount: fibers.length, toolCount: tools.size, tools: [...tools.values()],
|
|
290
|
-
error: error || (failed ? 'MCP 插件未正常运行' : undefined),
|
|
291
|
-
}
|
|
328
|
+
/** @public @author ddj 2026年09月28号 @param path Workspace. @returns Masked configuration and pool/lease counts; initialization is not health. */
|
|
329
|
+
views(path: string): MpcServer[] {
|
|
330
|
+
return [...this.projects.get(path) ?? []].map(([name, def]) => {
|
|
331
|
+
const entries = [...this.pool.entries.values()].filter((entry) => entry.spec.workspace === path && entry.spec.config.serverName === name)
|
|
332
|
+
const tools = new Map<string, MpcServer['tools'][number]>()
|
|
333
|
+
for (const [agent] of this.agents) if (this.workspace(agent) === path) for (const tool of toolsOf(this.ctx, name, agent)) tools.set(tool.name, tool)
|
|
334
|
+
const failed = entries.some((entry) => entry.phase === 'error' || entry.error)
|
|
335
|
+
const busy = entries.some((entry) => ['queued', 'loading', 'starting', 'stopping'].includes(entry.phase))
|
|
336
|
+
let status: MpcServer['status'] = 'configured'
|
|
337
|
+
if (!def.enabled) status = 'disabled'
|
|
338
|
+
else if (failed) status = 'error'
|
|
339
|
+
else if (busy) status = 'connecting'
|
|
340
|
+
else if (entries.some((entry) => entry.phase === 'ready')) status = 'unverified'
|
|
341
|
+
return { id: projectEntryId(path, name), serverName: name, enabled: def.enabled, transport: def.config.transport,
|
|
342
|
+
config: publicConfig(def.config as unknown as Record<string, unknown>), policy: parseMcpPolicy(def.policy), status,
|
|
343
|
+
instanceCount: entries.filter((entry) => entry.fiber).length, sessionCount: entries.reduce((total, entry) => total + entry.refs, 0),
|
|
344
|
+
tools: [...tools.values()], toolCount: tools.size, error: failed ? 'MCP 初始化或清理异常,请查看 Host 日志' : undefined }
|
|
345
|
+
})
|
|
292
346
|
}
|
|
293
347
|
|
|
294
|
-
/**
|
|
295
|
-
* Close every owned connection and prevent queued work from mounting replacements.
|
|
296
|
-
* @public @author ddj 2026年09月28号
|
|
297
|
-
* @returns Completion after connection disposal and pending startup work settle.
|
|
298
|
-
*/
|
|
348
|
+
/** @public @author ddj 2026年10月20号 @returns Complete owned transport and capability cleanup. */
|
|
299
349
|
async dispose(): Promise<void> {
|
|
300
350
|
this.stopped = true
|
|
351
|
+
this.offDiscover?.()
|
|
301
352
|
await Promise.all([...this.agents.keys()].map((agent) => this.detach(agent)))
|
|
302
|
-
await this.
|
|
353
|
+
await this.pool.dispose()
|
|
354
|
+
await Promise.allSettled([...this.pending])
|
|
303
355
|
this.projects.clear()
|
|
304
356
|
this.module = undefined
|
|
305
357
|
}
|
|
306
358
|
}
|
|
307
359
|
|
|
308
|
-
/**
|
|
309
|
-
* Remove every old global project entry before any agent-scoped mount is attempted.
|
|
310
|
-
* @public @author ddj 2026年09月28号
|
|
311
|
-
* @param ctx Host loader context.
|
|
312
|
-
* @returns Completion of loader removal and connection teardown.
|
|
313
|
-
*/
|
|
360
|
+
/** @public @author ddj 2026年09月28号 @param ctx Host loader. @returns Removal of legacy global project entries before any scoped entry is installed. */
|
|
314
361
|
export async function clearLegacy(ctx: Ctx): Promise<void> {
|
|
315
362
|
const loader = ctx.get('loader')
|
|
316
363
|
for (const entry of entriesOf(ctx)) {
|
|
@@ -319,41 +366,39 @@ export async function clearLegacy(ctx: Ctx): Promise<void> {
|
|
|
319
366
|
}
|
|
320
367
|
}
|
|
321
368
|
|
|
322
|
-
/**
|
|
323
|
-
* Install awaited creation hooks, migrate global entries, and attach existing agents.
|
|
324
|
-
* @public @author ddj 2026年09月28号
|
|
325
|
-
* @param ctx Host lifecycle context; installIsolation must already be installed.
|
|
326
|
-
* @returns Readiness for index.apply to await before advertising startup completion.
|
|
327
|
-
*/
|
|
369
|
+
/** @public @author ddj 2026年10月20号 @param ctx Host lifecycle. @returns Config/catalogue readiness, independent from all project MCP handshakes. */
|
|
328
370
|
export async function installMcpRuntime(ctx: Ctx): Promise<void> {
|
|
329
371
|
const runtime = runtimeOf(ctx)
|
|
330
372
|
let ready: Promise<void>
|
|
331
|
-
|
|
373
|
+
/** @author ddj 2026年10月20号 Loads validated snapshots before exposing lazy capabilities. */
|
|
374
|
+
const initialize = async (): Promise<void> => {
|
|
332
375
|
await clearLegacy(ctx)
|
|
333
|
-
await listProjects(ctx)
|
|
334
|
-
for (const agent of ctx.get('agents')?.list?.() ?? [])
|
|
376
|
+
const { projects } = await listProjects(ctx, false)
|
|
377
|
+
for (const agent of ctx.get('agents')?.list?.() ?? []) {
|
|
378
|
+
runtime.prepare(agent)
|
|
379
|
+
const project = projects.find((item) => item.workspacePath === runtime.workspace(agent))
|
|
380
|
+
if (project?.fileError) runtime.configError(agent, project.fileError)
|
|
381
|
+
}
|
|
335
382
|
}
|
|
336
|
-
|
|
383
|
+
/** @author ddj 2026年10月20号 @param event Actual created agent. Reads configuration but never waits for connection startup. */
|
|
384
|
+
const created = async ({ agent }: { agent: any }): Promise<void> => {
|
|
385
|
+
runtime.prepare(agent)
|
|
337
386
|
await ready
|
|
338
387
|
const path = runtime.workspace(agent)
|
|
339
388
|
if (path) {
|
|
340
|
-
try { await reconcileProject(ctx, path) }
|
|
341
|
-
catch (error) { ctx.logger?.warn?.('MCP configuration retained: ' + String(error)) }
|
|
389
|
+
try { await reconcileProject(ctx, path, undefined, false); runtime.configError(agent) }
|
|
390
|
+
catch (error) { runtime.configError(agent, error); ctx.logger?.warn?.('MCP configuration retained: ' + String(error)) }
|
|
342
391
|
}
|
|
343
392
|
await runtime.attach(agent)
|
|
344
393
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}
|
|
394
|
+
/** @author ddj 2026年10月20号 @param event Actual disposed agent. Revokes public access synchronously. */
|
|
395
|
+
const disposed = ({ agent }: { agent: any }): void => { void runtime.detach(agent).catch((error) => ctx.logger?.warn?.('MCP disposal failed: ' + String(error))) }
|
|
348
396
|
const offCreate = ctx.on('agent/created', created)
|
|
349
397
|
const offDispose = ctx.on('agent/disposed', disposed)
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
offDispose?.()
|
|
353
|
-
await runtime.dispose()
|
|
354
|
-
runtimes.delete(ctx.root ?? ctx)
|
|
355
|
-
}
|
|
398
|
+
/** @author ddj 2026年10月20号 @returns Full lifecycle teardown. */
|
|
399
|
+
const cleanup = async (): Promise<void> => { offCreate?.(); offDispose?.(); await runtime.dispose(); runtimes.delete(ctx.root ?? ctx) }
|
|
356
400
|
ctx.effect(() => cleanup, 'vscode-mode:mcp-runtime')
|
|
357
401
|
ready = initialize()
|
|
358
402
|
await ready
|
|
403
|
+
await Promise.all((ctx.get('agents')?.list?.() ?? []).map((agent: any) => runtime.attach(agent)))
|
|
359
404
|
}
|