2
>
* Copyright (c) Microsoft Corporation. All rights reserved.
3
>
* Licensed under the MIT License. See License.txt in the project root for license information.
4
>
*--------------------------------------------------------------------------------------------*/
5
>
6
>
import { Disposable } from '../../../../base/common/lifecycle.js';
7
>
import { derived, observableValue, transaction, type IObservable, type ITransaction } from '../../../../base/common/observable.js';
8
>
import { URI } from '../../../../base/common/uri.js';
9
>
import { ActionType } from '../../common/state/protocol/common/actions.js';
10
>
import { CustomizationType, McpServerStatus, type AhpMcpUiHostCapabilities, type ChildCustomization, type Customization, type McpServerCustomization, type McpServerState } from '../../common/state/protocol/channels-session/state.js';
11
>
import { DEFAULT_MCP_APP, DEFAULT_MCP_APP_CAPABILITIES } from '../../common/state/protocol/mcpAppDefaults.js';
12
>
import type { SessionAction } from '../../common/state/sessionActions.js';
13
>
import { AgentHostStateManager, IAgentHostStateManager } from '../agentHostStateManager.js';
14
>
15
>
/**
16
>
* SDK-neutral description of a single MCP server, as the controller's
17
>
* caller sees it. Each provider adapts its own SDK events into this
18
>
* shape (Copilot, Claude, Codex, …) and feeds them to
19
>
* {@link McpCustomizationController}.
20
>
*/
21
>
export interface ISdkMcpServer {
22
>
/** Server name (used both as the customization name and the channel suffix). */
23
>
readonly name: string;
24
>
/** Current lifecycle state. */
25
>
readonly state: McpServerState;
26
>
/** Explicit runtime enablement when the SDK distinguishes disabled from stopped. */
27
>
readonly enabled?: boolean;
28
>
}
29
>
30
>
/**
31
>
* Runtime fields of an MCP server customization that this controller
32
>
* owns — the high-frequency `state`/`channel` pair. Consumers overlay
33
>
* these onto their published customizations (keyed by customization id)
34
>
* so a wholesale customization republish preserves live MCP status
35
>
* rather than resetting it to the `Stopped` default baked into
36
>
* `makeMcpServerCustomization`.
37
>
*/
38
>
export type IMcpServerRuntimeState = Pick<McpServerCustomization, 'state' | 'channel'>;
39
>
40
>
/**
41
>
* Re-export so existing imports of `DEFAULT_MCP_APP_CAPABILITIES` from
42
>
* the controller keep working — the canonical home is now
43
>
* `agentHost/common/state/protocol/mcpAppDefaults.ts`.
44
>
*/
45
>
export { DEFAULT_MCP_APP_CAPABILITIES, DEFAULT_MCP_APP };
46
>
47
>
/**
48
>
* Lookup callback the controller uses to find an existing child MCP
49
>
* customization id by server name. The agent's plugin layer publishes
50
>
* MCP customizations with provider-defined ids
51
>
* (e.g. `pluginParsers.makeMcpServerCustomization` uses
52
>
* `buildChildId(definitionUri, 'mcp=' + encodeURIComponent(name))`), so
53
>
* we resolve them by name at action-dispatch time rather than trying to
54
>
* reconstruct the id.
55
>
*
56
>
* Returns `undefined` when no existing entry matches — in that case the
57
>
* controller surfaces a bare top-level customization for the server.
58
>
*/
59
>
export type IMcpChildIdResolver = (serverName: string) => string | undefined;
60
>
61
>
/**
62
>
* Options for {@link McpCustomizationController}.
63
>
*/
64
>
export interface IMcpCustomizationControllerOptions {
65
>
/** Provider id (e.g. `'copilotcli'`). Used as the channel URI authority. */
66
>
readonly providerId: string;
67
>
/** Session id (the raw id, not the full URI). Used as the channel path segment. */
68
>
readonly sessionId: string;
69
>
/** Canonical session URI used to resolve persisted customization state. */
70
>
readonly sessionUri: URI;
71
>
/**
72
>
* Resolves an existing child customization id for a given server
73
>
* name. See {@link IMcpChildIdResolver}.
74
>
*/
75
>
readonly resolveChildId: IMcpChildIdResolver;
76
>
/** Emits a {@link SessionAction} into the session's action stream. */
77
>
readonly emit: (action: SessionAction) => void;
78
>
/**
79
>
* MCP App capabilities to advertise on every ready server. Defaults
80
>
* to {@link DEFAULT_MCP_APP_CAPABILITIES}.
81
>
*/
82
>
readonly capabilities?: AhpMcpUiHostCapabilities;
83
>
}
84
>
85
>
interface ILiveEntry {
86
>
readonly serverName: string;
87
>
readonly state: McpServerState;
88
>
readonly enabled: boolean;
89
>
/** Top-level customization id (when no child match was found). */
90
>
readonly topLevelId?: string;
91
>
}
92
>
93
>
export function buildMcpTopLevelCustomizationId(providerId: string, sessionId: string, serverName: string): string {
94
return `mcp-top-level:${providerId}:${sessionId}:${serverName}`;
95
}
97
>
export function buildMcpChannel(providerId: string, sessionId: string, serverName: string): string {
98
return `mcp://${providerId}/${encodeURIComponent(sessionId)}/${encodeURIComponent(serverName)}`;
99
}
101
>
/**
102
>
* Translates a stream of SDK-reported MCP server states into AHP
103
>
* customization actions:
104
>
*
105
>
* - For servers backed by an existing child customization (plugin- or
106
>
* directory-derived), the controller emits
107
>
* {@link ActionType.SessionMcpServerStateChanged} keyed on the
108
>
* resolved child id. The reducer narrowly updates `state` and
109
>
* `channel` on the matching child.
110
>
* - For servers with no matching child (typically globally-configured
111
>
* MCP servers the SDK reports), the controller emits a full
112
>
* {@link ActionType.SessionCustomizationUpdated} carrying a bare
113
>
* top-level {@link McpServerCustomization}. The same id is reused
114
>
* across updates, so the reducer's upsert keeps in-place.
115
>
*
116
>
* The controller is SDK-agnostic: providers translate their own events
117
>
* into {@link ISdkMcpServer} and call {@link applyAll} / {@link applyOne}.
118
>
* If a provider reports a coarse {@link McpServerStatus.Starting} update
119
>
* after a richer {@link McpServerStatus.AuthRequired} state, the controller
120
>
* preserves the auth-required state until a definitive
121
>
* {@link McpServerStatus.Ready}, {@link McpServerStatus.Error}, or
122
>
* {@link McpServerStatus.Stopped} update arrives.
123
>
*/
124
>
export class McpCustomizationController extends Disposable {
125
>
126
>
/** Per-server live entries, keyed by server name. */
127
>
private readonly _live = observableValue<ReadonlyMap<string, ILiveEntry>>(this, new Map());
128
>
129
>
/**
130
>
* Snapshot of every live server's runtime {@link IMcpServerRuntimeState},
131
>
* keyed by the customization id under which it is published (the
132
>
* minted top-level id, or the plugin-derived child id resolved via
133
>
* {@link IMcpChildIdResolver}). Derived from {@link _live}. Callers mirror
134
>
* this into their own published customizations so a wholesale republish
135
>
* preserves live MCP status. Servers whose child id cannot currently be
136
>
* resolved are omitted.
137
>
*/
138
>
readonly runtimeStates: IObservable<ReadonlyMap<string, IMcpServerRuntimeState>>;
139
>
140
>
constructor(
141
private readonly _options: IMcpCustomizationControllerOptions,
142
@IAgentHostStateManager private readonly _stateManager: AgentHostStateManager,